synwire-agent: The Agent Runtime

synwire-agent is the concrete implementation layer for the Synwire agent runtime. It depends only on synwire-core and provides everything you need to build and run an agentic application: the Runner, execution strategies, backends, middleware, MCP transports, session management, and permissions.

Background: Agent Components — the Prompt Engineering Guide covers memory, tools, and planning as the three components of an agent. synwire-agent implements all three: session management (memory), Vfs (tools), and ExecutionStrategy (planning).

The Runner: entry point

Runner drives the agent turn loop. It ties together an AgentNode (the decision logic), an ExecutionStrategy (the turn-sequencing logic), a middleware stack, a backend, and a session.

#![allow(unused)]
fn main() {
use synwire_agent::runner::Runner;
use synwire_agent::strategies::DirectStrategy;
use synwire_agent::vfs::MemoryProvider;
use futures_util::StreamExt;

// Assume my_agent implements AgentNode
// let runner = Runner::builder()
//     .agent(my_agent)
//     .strategy(DirectStrategy::new())
//     .backend(MemoryProvider::new())
//     .build()?;
//
// let mut events = runner.run("Hello!".to_string(), Default::default()).await?;
// while let Some(event) = events.next().await {
//     match event? {
//         AgentEvent::Text { content } => print!("{content}"),
//         AgentEvent::Done { usage } => println!("\nTokens: {:?}", usage),
//         _ => {}
//     }
// }
}

Execution strategies

Two built-in strategies:

DirectStrategy

The model decides everything. No state machine constrains which directives the agent may emit. Use for open-ended assistants where you want maximum flexibility.

#![allow(unused)]
fn main() {
use synwire_agent::strategies::DirectStrategy;

let strategy = DirectStrategy::new();
}

FsmStrategy

A finite state machine controls the turn sequence. Named states and typed guard conditions — closures over Directive values — determine which transitions fire.

#![allow(unused)]
fn main() {
use synwire_agent::strategies::{FsmStrategyBuilder, ClosureGuard};
use synwire_core::agents::directive::Directive;

let strategy = FsmStrategyBuilder::new()
    .add_state("idle")
    .add_state("executing")
    .set_initial_state("idle")
    .add_transition(
        "idle",
        "executing",
        ClosureGuard::new(|d| matches!(d, Directive::RunInstruction { .. })),
    )
    .add_transition("executing", "idle", ClosureGuard::always())
    .build()?;
}

For the design rationale, see FSM Strategy Design. For when to choose FsmStrategy vs StateGraph, see StateGraph vs FsmStrategy.

Backends

All backends implement Vfs. The backend you choose determines the scope and risk of the agent's operations:

#![allow(unused)]
fn main() {
use synwire_agent::vfs::{
    MemoryProvider,           // ephemeral in-memory
    LocalProvider,      // scoped to a root path
    GitBackend,             // within a git repo
    HttpBackend,            // external HTTP calls
    Shell,      // sandboxed shell execution
};

// Ephemeral backend — safe for testing and sandboxed agents
let safe = MemoryProvider::new();

// Filesystem backend — path traversal is blocked by normalize_path()
let fs = LocalProvider::new("/workspace");

// Git backend — all operations stay within the repo
let git = GitBackend::new("/workspace/.git");
}
BackendScopeRisk level
MemoryProviderNone (ephemeral)None
LocalProviderRooted pathLow (scoped)
GitBackendGit repo boundaryLow (version-controlled)
HttpBackendExternal networkMedium
ShellSandboxed working dirMedium
ProcessManagerAny processHigh
CompositeProviderMount tableDepends on mounts

Middleware

Middleware runs before each turn in declaration order. Use middleware for: injecting tools, adding system prompts, rate limiting, or audit logging.

#![allow(unused)]
fn main() {
use synwire_agent::middleware::{
    SystemPromptMiddleware,
    ToolInjectionMiddleware,
    AuditLogMiddleware,
    RateLimitMiddleware,
};

// let runner = Runner::builder()
//     .middleware(SystemPromptMiddleware::new("You are a helpful assistant."))
//     .middleware(ToolInjectionMiddleware::new(vec![my_tool]))
//     .middleware(RateLimitMiddleware::tokens_per_minute(60_000))
//     .middleware(AuditLogMiddleware::new(std::io::stderr()))
//     .build()?;
}

Middleware cannot do post-processing — there is no reverse pass. For post-turn processing, use a Plugin with after_run lifecycle hooks.

Permissions and approval gates

Configure what the agent is allowed to do:

#![allow(unused)]
fn main() {
use synwire_agent::permissions::{PermissionMode, PermissionRule, PermissionBehavior};
use synwire_agent::gates::{ThresholdGate, RiskLevel};

// Preset: block all tool calls by default
// let mode = PermissionMode::Restricted;

// Custom: require approval for shell commands
// let mode = PermissionMode::Custom(vec![
//     PermissionRule {
//         tool_pattern: "shell_*".to_string(),
//         behavior: PermissionBehavior::Ask,
//     },
// ]);

// Approval gate: trigger human review for HIGH+ risk directives
// let gate = ThresholdGate::new(RiskLevel::High, |req| {
//     println!("Approve directive {:?}? (y/n)", req.directive);
//     // read stdin, return ApprovalDecision::Allow or Deny
//     ApprovalDecision::Allow
// });
}

MCP integration

Connect external tool servers via the Model Context Protocol:

#![allow(unused)]
fn main() {
use synwire_agent::mcp::{StdioMcpTransport, McpLifecycleManager};

// Spawn a subprocess MCP server and manage its lifecycle:
// let transport = StdioMcpTransport::new("npx", &["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]);
// let manager = McpLifecycleManager::new(transport);
// manager.connect().await?;
// let tools = manager.list_tools().await?;
}

Session management

#![allow(unused)]
fn main() {
use synwire_agent::sessions::InMemorySessionManager;
use synwire_core::agents::session::SessionManager;

let mgr = InMemorySessionManager::new();
let session = mgr.create("my-agent").await?;
// session.id, session.metadata.created_at, session.messages
}

See also