Expand description
Hooks for observing and steering an agent run.
A hook is a single AgentHook::on_event method that the agent loop calls
at every observable point of a run — before each model call, on each model
response, around every tool call, on streamed deltas, and when the model
emits an invalid tool call. Each call receives a HookContext (run-scoped
identity + a shared scratchpad) and a StepEvent describing what is
happening, and returns a Flow that lets the hook observe, patch the
request, skip a tool, terminate the run early, or (for invalid tool calls)
retry/repair/skip recovery.
Unlike the old multi-method hook trait, a hook implements one method and
matches on the event it cares about — every other event falls through to the
default Flow::Continue. Hooks compose in a HookStack that runs several
hooks in registration order.
§Composition: mergeable patches vs. terminal control actions
How a HookStack combines several hooks’ Flow results depends on the
event, and this is the central behavior to understand:
StepEvent::CompletionCall— accumulate & merge. Every hook is consulted. A hook that returnsFlow::PatchRequestdoes not stop the others: patches from all hooks are merged in registration order into one effective patch (seeRequestPatchfor the per-field merge rules). This lets a RAG hook, a tool-policy hook, and a provider-param hook all contribute to the same turn.Flow::Terminatestops the stack and is honored; any other (unsupported) flow stops the stack and fails closed, discarding the accumulated patch.StepEvent::ToolCall/StepEvent::ToolResult— chain. Every hook is consulted; aFlow::RewriteArgs/Flow::RewriteResultdoes not stop the others — the rewritten value is threaded into the next hook’s event, so hook N observes the value as rewritten by hooks 1..N-1 and may rewrite further (a redaction hook and a truncation hook compose).Flow::Skip/Flow::Terminateare terminal mid-chain.- Every other event — first non-
Continuewins. These are observe-only or recovery events (CompletionResponse,ModelTurnFinished,InvalidToolCall, the streamed deltas): the first hook to return a non-Continueresult short-circuits the rest.
Blind merge. During accumulation/chaining a hook does not see earlier
hooks’ contributions in its event payload for CompletionCall (it sees the
agent baseline); for ToolCall/ToolResult it does see the running
rewritten value. CompletionCall patches are declarative with documented
conflict rules, so blind merge is sufficient and keeps StepEvent Copy.
Ordering guidance. Because Flow::Terminate short-circuits the stack,
register observe-only hooks (telemetry) before steering hooks so a later
terminate cannot hide the run from them. A HookStack pushed as a hook
into another stack composes correctly: it returns its own net flow (a merged
patch, a threaded rewrite, or a terminal action) which the outer stack folds
in again — nesting never reintroduces short-circuiting on mergeable results.
§Why a returned Flow, not a next()-style middleware
A hook returns a typed Flow rather than receiving a next continuation it
must invoke. A next()/middleware model — where each layer has to call
next(ctx) to let the rest of the chain and the wrapped action run — carries
a well-known footgun: forgetting the call silently disables every downstream
hook and the action itself, with no error. The declarative returned-Flow
model makes that impossible: proceeding is the explicit Flow::Continue, and
any action an event cannot honor is fail-closed (it terminates the run) rather
than silently skipped.
Hooks are a driver concern: they are async, side-effecting and generic over
the model, so they live in the AgentRunner
layer rather than inside the sans-IO, serializable
AgentRun state machine.
§Migrating from PromptHook
The previous eight-method PromptHook<M> trait is replaced by the single
AgentHook::on_event method. Each old method becomes one match arm on a
StepEvent variant, and the value it used to return becomes the Flow
you return from that arm (every event you don’t care about falls through to
Flow::Continue). Every on_event now also receives a HookContext
first argument. Attach one or more hooks with add_hook.
Old PromptHook method | StepEvent variant | Flow to return |
|---|---|---|
on_completion_call | CompletionCall { prompt, history, turn } | cont / patch_request / terminate |
on_completion_response | CompletionResponse { prompt, response } | cont / terminate |
on_invalid_tool_call | InvalidToolCall(ctx) | fail (default) / retry / repair / skip / terminate |
on_tool_call | ToolCall { tool_name, tool_call_id, internal_call_id, args } | cont / rewrite_args / skip / terminate |
on_tool_result | ToolResult { tool_name, .., result, outcome, extensions } | cont / rewrite_result / terminate |
on_text_delta | TextDelta { delta, aggregated } | cont / terminate |
on_tool_call_delta | ToolCallDelta { tool_call_id, internal_call_id, tool_name, delta } | cont / terminate |
on_stream_completion_response_finish | StreamResponseFinish { prompt, response } | cont / terminate |
| (new, both surfaces) | ModelTurnFinished { turn, content, usage } | cont / terminate |
Behavioral notes:
- The invalid-tool-call default is still fail-fast: returning
Flow::ContinueforStepEvent::InvalidToolCallis treated asFlow::fail, matching the old trait’s defaulton_invalid_tool_call. - A hook opts out of an event by returning
Flow::contfrom that arm, instead of leaving a trait method unimplemented. - For per-delta hooks, override
AgentHook::observesto skip the high-frequencyTextDelta/ToolCallDeltaevents you don’t consume.
§Steering on structured tool outcomes
StepEvent::ToolResult carries a structured
ToolOutcome alongside the model-visible
result, so a hook can branch on why a tool failed — a timeout vs. a 404 —
without parsing strings. The motivating case: abort after repeated timeouts,
but let a not-found flow back to the model as recoverable feedback.
use rig_core::agent::{AgentHook, Flow, HookContext, StepEvent};
use rig_core::completion::CompletionModel;
use rig_core::tool::ToolFailureKind;
#[derive(Clone, Default)]
struct TimeoutCount(usize);
struct OutcomePolicy;
impl<M: CompletionModel> AgentHook<M> for OutcomePolicy {
async fn on_event(&self, ctx: &HookContext, event: StepEvent<'_, M>) -> Flow {
if let StepEvent::ToolResult { outcome, .. } = event {
// Repeated timeouts abort the run; a 404 does not.
if outcome.is_error_kind(ToolFailureKind::Timeout) {
let count = ctx.scratchpad().update(|c: &mut TimeoutCount| {
c.0 += 1;
c.0
});
if count >= 10 {
return Flow::terminate("aborting after repeated tool timeouts");
}
}
// `NotFound` falls through to `Flow::cont`: the model sees the
// error text and may try another path.
}
Flow::cont()
}
}Structs§
- Hook
Context - Run-scoped context passed by shared reference to every
AgentHook::on_eventcall. - Hook
Stack - An ordered list of hooks run as one hook.
- Invalid
Tool Call Context - Context passed to a hook on a
StepEvent::InvalidToolCallevent when the model emits a tool call that Rig would reject before normal tool-call handling or execution. - Request
Patch - A partial patch over the model request for a single turn, returned by a hook
via
Flow::PatchRequeston aStepEvent::CompletionCallevent. - RunId
- Opaque, process-scoped identifier for a single agent run.
- Scratchpad
- A run-scoped, shared scratchpad passed to every hook via
HookContext.
Enums§
- Flow
- Control-flow result returned by
AgentHook::on_event. - Invalid
Tool Call Hook Action - Recovery action for an invalid tool call, used internally by
AgentRun. Hooks express recovery viaFlow; theAgentRunnertranslates aFlowreturned for aStepEvent::InvalidToolCallinto this type. - Step
Event - An observable point in an agent run, passed to
AgentHook::on_event. - Step
Event Kind - The discriminant of a
StepEvent.
Traits§
- Agent
Hook - A per-run hook that observes and steers an agent run.