pub trait AgentObserver: Send {
// Required methods
fn check_interrupt(&mut self) -> LoopControl;
fn on_status(&mut self, message: &str);
fn on_tool_result(
&mut self,
tool_name: &str,
tool_call_id: &str,
action: &AgentAction,
result: &AgentActionResult,
);
fn on_error(&mut self, error: &str);
fn on_generation_start(&mut self);
fn on_generation_complete(&mut self, tokens: usize);
// Provided methods
fn on_message_appended(&mut self, _msg: &ChatMessage) { ... }
fn call_model<'life0, 'life1, 'life2, 'async_trait>(
&'life0 mut self,
model: Arc<RwLock<Box<dyn Model>>>,
messages: &'life1 [ChatMessage],
config: &'life2 ModelConfig,
) -> Pin<Box<dyn Future<Output = Result<ModelCallOutput>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait { ... }
fn run_subagents<'life0, 'life1, 'async_trait>(
&'life0 mut self,
specs: Vec<(String, String)>,
model: Arc<RwLock<Box<dyn Model>>>,
config: &'life1 ModelConfig,
) -> Pin<Box<dyn Future<Output = Vec<SubagentResult>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait { ... }
}Expand description
How the agent loop communicates with its environment.
The six small sync hooks (check_interrupt, on_status, etc.) are
called between steps. The two async hooks (call_model,
run_subagents) do the heavy lifting and have default
implementations suitable for non-interactive use — the TUI overrides
them to thread channel-based streaming and live rendering through
the shared loop without forking it.
Required Methods§
Sourcefn check_interrupt(&mut self) -> LoopControl
fn check_interrupt(&mut self) -> LoopControl
Called between steps to check for user interruption or injected messages.
Returns LoopControl::Continue to proceed, Interrupt to stop,
or InjectMessage(text) to redirect the agent with new user input.
Sourcefn on_status(&mut self, message: &str)
fn on_status(&mut self, message: &str)
Called when the loop status changes (e.g., “Iteration 3 - executing tools”)
Sourcefn on_tool_result(
&mut self,
tool_name: &str,
tool_call_id: &str,
action: &AgentAction,
result: &AgentActionResult,
)
fn on_tool_result( &mut self, tool_name: &str, tool_call_id: &str, action: &AgentAction, result: &AgentActionResult, )
Called after a tool call is executed.
Observers may use this to mirror tool results into side storage
(e.g., the TUI commits the tool message to session_state for
live rendering) — but the loop ALSO appends the tool message to
its own messages vec so the next model call sees it. When
that vec IS the side storage (TUI passes &mut session_state .messages), both paths land on the same data.
Sourcefn on_generation_start(&mut self)
fn on_generation_start(&mut self)
Called when model generation starts (for status tracking).
Sourcefn on_generation_complete(&mut self, tokens: usize)
fn on_generation_complete(&mut self, tokens: usize)
Called when model generation completes with token count.
Provided Methods§
Sourcefn on_message_appended(&mut self, _msg: &ChatMessage)
fn on_message_appended(&mut self, _msg: &ChatMessage)
Called whenever the loop appends a message to its messages vec.
Default: no-op. The TUI overrides this to mirror the message into
app.session_state.messages (its UI source of truth) without
requiring run_agent_loop to borrow session_state mutably (which
would alias through the observer).
Sourcefn call_model<'life0, 'life1, 'life2, 'async_trait>(
&'life0 mut self,
model: Arc<RwLock<Box<dyn Model>>>,
messages: &'life1 [ChatMessage],
config: &'life2 ModelConfig,
) -> Pin<Box<dyn Future<Output = Result<ModelCallOutput>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn call_model<'life0, 'life1, 'life2, 'async_trait>(
&'life0 mut self,
model: Arc<RwLock<Box<dyn Model>>>,
messages: &'life1 [ChatMessage],
config: &'life2 ModelConfig,
) -> Pin<Box<dyn Future<Output = Result<ModelCallOutput>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Call the model and return its response.
Default: direct model.chat_typed() with a typed-event accumulator —
matches what non-interactive mode and subagents need. Reasoning
chunks are accumulated separately so they don’t pollute the
returned text content. Tool calls are deduped against the response
(the adapter populates ModelResponse.tool_calls as a fallback if
the typed stream emitted none).
Override for: channel-based streaming, mid-stream interrupt, UI rendering between chunks.
Sourcefn run_subagents<'life0, 'life1, 'async_trait>(
&'life0 mut self,
specs: Vec<(String, String)>,
model: Arc<RwLock<Box<dyn Model>>>,
config: &'life1 ModelConfig,
) -> Pin<Box<dyn Future<Output = Vec<SubagentResult>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn run_subagents<'life0, 'life1, 'async_trait>(
&'life0 mut self,
specs: Vec<(String, String)>,
model: Arc<RwLock<Box<dyn Model>>>,
config: &'life1 ModelConfig,
) -> Pin<Box<dyn Future<Output = Vec<SubagentResult>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Run a batch of subagents to completion and return their results.
Default: spawn_subagents + collect_subagent_results with no
rendering between polls. Override for live progress rendering.