Hooks
Hooks let your code observe and steer the agent loop while AgentRunner drives the model and tool IO.
Use hooks for logging, metrics, audit trails, approval flows, guardrails, request shaping, invalid tool
call recovery, and streaming UI integration.
A hook implements one method: AgentHook::on_event. It receives a StepEvent and returns a Flow.
StepEventtells you what is happening: a model call, a model response, a tool call, a tool result, a streamed delta, or an invalid tool call.Flowtells Rig what to do next: continue, terminate, skip, rewrite, retry, repair, or override the next request.
Add hooks to a request, runner, or agent
Section titled “Add hooks to a request, runner, or agent”For one request, append a hook to the prompt builder:
let answer = agent .prompt("Check the account balance, then summarize it.") .max_turns(3) .add_hook(ToolAudit) .await?;For an explicit runner, append hooks before run() or stream():
let response = agent .runner("Check the account balance, then summarize it.") .max_turns(3) .add_hook(ToolAudit) .run() .await?;For every request created by an agent, install default hooks on the agent builder:
let agent = openai::Client::from_env()? .agent(openai::GPT_4O) .add_hook(ToolAudit) .build();Agent-level hooks run first. Per-request or per-run hooks are appended after the defaults.
A minimal hook
Section titled “A minimal hook”Hooks match on the events they care about and return Flow::cont() for everything else.
use rig::agent::{AgentHook, Flow, StepEvent};use rig::completion::CompletionModel;
struct ToolAudit;
impl<M: CompletionModel> AgentHook<M> for ToolAudit { async fn on_event(&self, event: StepEvent<'_, M>) -> Flow { match event { StepEvent::ToolCall { tool_name, args, .. } => { println!("calling {tool_name} with {args}"); } StepEvent::ToolResult { tool_name, result, .. } => { println!("{tool_name} returned {result}"); } _ => {} }
Flow::cont() }}StepEvent borrows its payload, so hooks inspect event data without taking ownership.
Hook events
Section titled “Hook events”| Event | When it fires | Common uses |
|---|---|---|
CompletionCall { prompt, history, turn } | Before each model request | logging, metrics, per-turn request overrides |
CompletionResponse { prompt, response } | After a non-streaming model response | audit raw responses, count calls |
InvalidToolCall(ctx) | The model called an unknown or disallowed tool | fail, retry with feedback, repair the name, or skip |
ToolCall { tool_name, args, ... } | Before executing a valid tool | approvals, argument rewriting, deny/skip policies |
ToolResult { tool_name, result, ... } | After a tool returns | redact, truncate, normalize, or log results |
TextDelta { delta, aggregated } | Streaming only | live UI updates, cancellation on content policies |
ToolCallDelta { ... } | Streaming only | display partial tool-call arguments |
StreamResponseFinish { prompt, response } | Streaming text response finished | streaming-side metrics and cleanup |
CompletionResponse and StreamResponseFinish are suppressed for turns recovered by invalid tool-call
repair, skip, or retry. Streaming-only events fire only when using the streaming runner/request surface.
Flow actions
Section titled “Flow actions”A hook returns a Flow. Flow::cont() means “observe only”. Other actions steer the run.
| Flow | Valid events | Effect |
|---|---|---|
cont() | all | Continue normally. |
terminate(reason) | all | Stop the run with a cancellation error containing the current history. |
override_request(RequestOverride) | CompletionCall | Patch this turn’s request only. |
rewrite_args(json) | ToolCall | Execute the tool with replacement JSON arguments. |
skip(reason) | ToolCall, InvalidToolCall | Do not execute the tool; return reason to the model as the tool result. |
rewrite_result(result) | ToolResult | Replace what the model sees as the tool output. |
fail() | InvalidToolCall | Preserve Rig’s default fail-fast behavior. |
retry(feedback) | InvalidToolCall | Append corrective feedback and ask the model again. |
repair(tool_name) | InvalidToolCall | Rewrite the emitted tool name, then revalidate it. |
The runner is fail-closed: if a hook returns an action an event cannot honor, Rig terminates the
run with a diagnostic instead of silently ignoring it. For example, returning Flow::fail() from a
ToolCall event stops the run rather than letting the tool execute.
Request overrides
Section titled “Request overrides”Use Flow::override_request from CompletionCall to patch one turn of the model request without
mutating the agent. This is useful for phased agents: force a search on the first turn, lower
temperature for a critical step, or shrink the advertised tool list.
use rig::agent::{AgentHook, Flow, RequestOverride, StepEvent};use rig::completion::CompletionModel;use rig::message::ToolChoice;
struct ForceSearchFirst;
impl<M: CompletionModel> AgentHook<M> for ForceSearchFirst { async fn on_event(&self, event: StepEvent<'_, M>) -> Flow { match event { StepEvent::CompletionCall { turn: 1, .. } => Flow::override_request( RequestOverride::new() .active_tools(["search_web"]) .tool_choice(ToolChoice::Specific { function_names: vec!["search_web".to_string()], }) .temperature(0.0), ), _ => Flow::cont(), } }}Overrides are per-turn and non-sticky. additional_params are shallow-merged with the agent’s
configured params; the other fields replace the baseline for that turn. If you narrow active_tools,
make sure any tool_choice still names an advertised tool.
Guardrails and approvals
Section titled “Guardrails and approvals”Hooks are a good place for UX and policy controls around tools. A hook can approve safe tools, skip risky calls, rewrite arguments, or terminate the run.
use rig::agent::{AgentHook, Flow, StepEvent};use rig::completion::CompletionModel;
struct TransferPolicy { max_auto_transfer: u64,}
impl<M: CompletionModel> AgentHook<M> for TransferPolicy { async fn on_event(&self, event: StepEvent<'_, M>) -> Flow { let StepEvent::ToolCall { tool_name, args, .. } = event else { return Flow::cont(); };
if tool_name != "transfer_funds" { return Flow::cont(); }
let amount = serde_json::from_str::<serde_json::Value>(args) .ok() .and_then(|v| v.get("amount").and_then(|a| a.as_u64()));
match amount { Some(n) if n <= self.max_auto_transfer => Flow::cont(), Some(n) => Flow::skip(format!( "denied by policy: ${n} exceeds the ${} automatic transfer limit", self.max_auto_transfer )), None => Flow::skip("denied by policy: missing transfer amount"), } }}This is a guardrail, not a security boundary. Enforce real authorization inside the tool implementation or the downstream service as well.
Invalid tool calls
Section titled “Invalid tool calls”An invalid tool call is a tool name that is unknown, not advertised for the turn, or disallowed by the
active ToolChoice. By default, Rig fails fast. A hook can opt into recovery:
use rig::agent::{AgentHook, Flow, StepEvent};use rig::completion::CompletionModel;
struct RepairDefaultApi;
impl<M: CompletionModel> AgentHook<M> for RepairDefaultApi { async fn on_event(&self, event: StepEvent<'_, M>) -> Flow { match event { StepEvent::InvalidToolCall(ctx) if ctx.tool_name == "default_api" => { Flow::repair("search_web") } StepEvent::InvalidToolCall(ctx) => { Flow::retry(format!("Use one of these tools: {:?}", ctx.available_tools)) } _ => Flow::cont(), } }}Recovery options:
Flow::fail()preserves the default fail-fast behavior.Flow::retry(feedback)appends corrective feedback and asks the model again. Configure the budget withmax_invalid_tool_call_retries(n)on the prompt request or runner.Flow::repair(tool_name)rewrites only the tool name and revalidates it against the allowed tools.Flow::skip(reason)records a synthetic tool result without executing the invalid tool. If any invalid call in a model turn is skipped, Rig suppresses execution of the turn’s other tool calls too and returns synthetic “not executed” results for them. Skip recovery is rejected underToolChoice::None.
Returning Flow::cont() for InvalidToolCall is treated as Flow::fail().
Hook composition
Section titled “Hook composition”Hooks are stored in a HookStack and run in registration order. The first hook that returns anything
other than Flow::cont() (Flow::Continue) wins for that event; later hooks are not called for that event.
That short-circuiting keeps policy outcomes clear: a hook that skips, rewrites, or terminates has made the decision. If several policies need to combine into one action, compose them inside a single hook.
let response = agent .runner("Process this request.") .add_hook(AuditLog) .add_hook(TransferPolicy { max_auto_transfer: 500 }) .run() .await?;In this example, AuditLog sees events first. If it returns Flow::cont(), the transfer policy sees
the same event. If it returns a non-continue action, the policy is skipped for that event.
Streaming and observes
Section titled “Streaming and observes”Text and tool-call deltas can be frequent. Override observes so Rig can skip work when no hook cares
about a high-frequency event kind:
use rig::agent::{AgentHook, Flow, StepEvent, StepEventKind};use rig::completion::CompletionModel;
struct ToolOnlyHook;
impl<M: CompletionModel> AgentHook<M> for ToolOnlyHook { fn observes(&self, kind: StepEventKind) -> bool { matches!(kind, StepEventKind::ToolCall | StepEventKind::ToolResult) }
async fn on_event(&self, event: StepEvent<'_, M>) -> Flow { if let StepEvent::ToolCall { tool_name, .. } = event { println!("tool: {tool_name}"); } Flow::cont() }}Even with observes, on_event should still return Flow::cont() for events it ignores: hook stacks
OR-combine interest across hooks, so a sibling hook may cause an event to be dispatched.
Best practices
Section titled “Best practices”- Keep hooks lightweight. Hooks are awaited inline, so slow hooks delay the next model or tool step.
- Offload heavy logging, network writes, and audit persistence to background tasks.
- Treat hook guardrails as UX/policy controls, not authorization boundaries.
- Make hook state concurrency-safe when using
tool_concurrency > 1. - Prefer one composed policy hook when multiple rules need to produce one decision.
See also
Section titled “See also”- AgentRunner — the driver that runs hooks.
- Agents — the high-level agent abstraction.
- Tools — define tools hooks can inspect, rewrite, or skip.
- Streaming — receive streaming deltas and final responses.
agent::hooksource on GitHub — exact main-branch hook, event, and flow signatures.
