Agents

The Agent trait, AgentConfig, StreamingToolLoopAgent, and the run loop.

Agents

In Forge, an agent is a stable identity bound to a runtime that drives a language model through a tool loop. Every agent satisfies the eight ANVIL contracts and carries verifiable lineage to a human root.

The Agent trait

#[async_trait]
pub trait Agent: Send + Sync {
    fn config(&self) -> &AgentConfig;
    fn identity(&self) -> &ForgeAgentIdentity;
    async fn run(&self, prompt: impl Into<String> + Send) -> ForgeResult<AgentOutput>;
    async fn run_messages(&self, messages: Vec<ModelMessage>) -> ForgeResult<AgentOutput>;
}

Two reference implementations ship in forge-agent:

  • StreamingToolLoopAgent — production runtime. Uses stream_chunks for real per-token streaming, supports observers, supports atomic-swap of observers without rebuild.
  • ToolLoopAgent — buffered runtime. Calls generate once per loop iteration. Use only for tests or providers without streaming.

AgentConfig

let config = AgentConfig::new("research-analyst", "anthropic:claude-sonnet-4-5-20250929")
    .with_identity(identity)            // ForgeAgentIdentity
    .with_system_prompt("You are a careful research analyst.")
    .with_tool_registry(&registry)      // ToolRegistry — extracts ToolDefinitions
    .with_max_iterations(8)             // tool-loop bound
    .with_temperature(0.2)
    .with_max_tokens(2048);

Available builders:

Method Purpose
with_identity(identity) Bind a ForgeAgentIdentity (recommended)
with_system_prompt(s) Per-agent system prompt
with_tool_registry(&r) Extract ToolDefinitions from a registry
with_tools(vec) Pass ToolDefinitions directly
with_max_iterations(n) Bound the tool loop (default 10)
with_temperature(t) Sampling temperature
with_max_tokens(n) Output cap

with_tool_registry is the preferred wiring — it keeps the registry as the single source of truth for both definitions and execution.

Building a StreamingToolLoopAgent

use forge::prelude::*;
use std::sync::Arc;

let registry = ToolRegistry::new();
registry.register(FnToolExecutor::new("get_weather", weather_handler));

let agent = StreamingToolLoopAgent::new(
    AgentConfig::new("weather-bot", "anthropic:claude-sonnet-4-5-20250929")
        .with_system_prompt("Look up weather precisely.")
        .with_tool_registry(&registry),
    StreamingLoopConfig::default(),
    model,
    registry,
    AutoApprove::shared(),
);

Required arguments:

  • config — the AgentConfig from above
  • loop configStreamingLoopConfig::default() is the sensible default; override max_iterations, tool_timeout, interrupt_on_error here
  • modelArc<dyn LanguageModel>
  • registry — the same ToolRegistry referenced in the config
  • approval handlerAutoApprove, RejectAll, or your own ApprovalHandler (see Tools)

The run loop

let output = agent.run("What is the weather in Mountain View?").await?;

Internally run does:

  1. Builds a message list: [system_prompt?, user_prompt]
  2. Calls model.stream_chunks(&messages, &tools, &options)
  3. Streams chunks through the loop, emitting AgentEvents to the observer
  4. When the model emits ToolCallEnd, gates each call through the approval handler, executes via the registry, records a ToolInvocationRecord
  5. Appends the tool results to the messages, re-streams
  6. Stops when the model emits Done with FinishReason::Stop, or when max_iterations is hit

AgentOutput

pub struct AgentOutput {
    pub final_message: ModelMessage,
    pub tool_invocations: Vec<ToolInvocationRecord>,
    pub usage: Usage,
    pub finish_reason: FinishReason,
    pub events: Vec<AgentEvent>,
    pub iterations: usize,
}

Common access patterns:

// Final text
println!("{}", output.text());

// Tool invocations with timings + status
for record in output.tool_invocations() {
    println!(
        "{} {:?} took {}ms",
        record.tool_name, record.status, record.duration_ms,
    );
}

// Replay the full event stream
for event in output.events() {
    match event {
        AgentEvent::Iteration { n } => /* ... */,
        AgentEvent::ToolCalled { name, .. } => /* ... */,
        AgentEvent::Stopped { reason } => break,
        _ => {}
    }
}

See Agent events for the full event taxonomy.

Observers

For long-running or autonomous loops, install an observer:

let observer = Arc::new(MyObserver::new());
agent.set_observer(observer);

The observer receives every AgentEvent in real time. Atomic-swap is supported without rebuilding the agent — see Observers.

Multi-model topology (Wave 2, Rust)

AgentConfig can bind multiple models with a routing policy:

let config = AgentConfig::new("research", "primary")
    .with_model("anthropic:claude-sonnet-4-5-20250929")
    .with_model("openai:gpt-4.1")
    .with_routing(RoutingPolicy::DomainBased { /* ... */ });

This is currently Rust-only. Other SDKs follow.