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. Usesstream_chunksfor real per-token streaming, supports observers, supports atomic-swap of observers without rebuild.ToolLoopAgent— buffered runtime. Callsgenerateonce 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(®istry) // 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(®istry),
StreamingLoopConfig::default(),
model,
registry,
AutoApprove::shared(),
);
Required arguments:
- config — the
AgentConfigfrom above - loop config —
StreamingLoopConfig::default()is the sensible default; overridemax_iterations,tool_timeout,interrupt_on_errorhere - model —
Arc<dyn LanguageModel> - registry — the same
ToolRegistryreferenced in the config - approval handler —
AutoApprove,RejectAll, or your ownApprovalHandler(see Tools)
The run loop
let output = agent.run("What is the weather in Mountain View?").await?;
Internally run does:
- Builds a message list:
[system_prompt?, user_prompt] - Calls
model.stream_chunks(&messages, &tools, &options) - Streams chunks through the loop, emitting
AgentEvents to the observer - When the model emits
ToolCallEnd, gates each call through the approval handler, executes via the registry, records aToolInvocationRecord - Appends the tool results to the messages, re-streams
- Stops when the model emits
DonewithFinishReason::Stop, or whenmax_iterationsis 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.