Agent events
Every Forge agent emits a stream of typed events as it runs. These power
observers, drive the harness terminal, and form the post-hoc record returned
in AgentOutput.
AgentEvent
pub enum AgentEvent {
/// The agent's run loop is starting iteration `n`.
Iteration { n: usize },
/// The agent has begun streaming from the model.
ModelStart,
/// A `StreamChunk::TextDelta` arrived.
TextDelta { text: String },
/// The model has produced a tool call.
ToolCalled {
invocation_id: String,
tool_name: String,
input: serde_json::Value,
},
/// A tool call completed (success or failure).
ToolFinished {
invocation_id: String,
tool_name: String,
status: ToolInvocationStatus,
duration_ms: u64,
output: Option<serde_json::Value>,
error: Option<String>,
},
/// The model finished one iteration of streaming.
ModelFinished {
finish_reason: FinishReason,
usage: Usage,
},
/// The agent's run loop stopped (success or bound).
Stopped { reason: StopReason },
}
ToolInvocationRecord
Each tool execution produces one record:
pub struct ToolInvocationRecord {
pub invocation_id: String,
pub tool_name: String,
pub input: serde_json::Value,
pub status: ToolInvocationStatus,
pub duration_ms: u64,
pub output: Option<serde_json::Value>,
pub error: Option<String>,
}
pub enum ToolInvocationStatus {
Success,
Failed,
Rejected, // approval handler rejected
Timeout,
}
Iterating the post-hoc record
let output = agent.run("Look up the weather and write a summary.").await?;
// Typed iterator over the full event stream
for event in output.events() {
match event {
AgentEvent::ToolCalled { tool_name, input, .. } => {
println!("→ {tool_name}({input})");
}
AgentEvent::ToolFinished { tool_name, status, duration_ms, .. } => {
println!("← {tool_name} {:?} ({}ms)", status, duration_ms);
}
AgentEvent::Stopped { reason } => {
println!("✓ stopped: {:?}", reason);
}
_ => {}
}
}
// Compact tool-call log
for r in output.tool_invocations() {
println!("{:>4}ms {:<24} {:?}", r.duration_ms, r.tool_name, r.status);
}
Symmetry with observers
The same events that fill output.events() are sent to any installed
AgentLoopObserver in real time. The two views are guaranteed identical:
observer.on_event(&event); // real-time
output.events.push(event); // post-hoc
This means you can drive a live status panel from the observer and persist
the full invocation log from output.tool_invocations() — both views match.
Filtering helpers
// Only the failures
let failures: Vec<_> = output
.tool_invocations()
.iter()
.filter(|r| matches!(r.status, ToolInvocationStatus::Failed | ToolInvocationStatus::Timeout))
.collect();
// Total tool wall time
let total_ms: u64 = output
.tool_invocations()
.iter()
.map(|r| r.duration_ms)
.sum();