callModel agent loop. Use them to:
- Inspect, modify, or block tool calls before execution
- Record successful and failed tool executions
- Rewrite or reject user prompts
- Decide whether approval-gated tools run
- Override stop conditions
- Track sessions, model calls, token usage, latency, and cost
- Run custom, type-safe application events
SDKHooks and
BeforeRequestHook. Transport hooks intercept HTTP requests. Lifecycle hooks
fire on events within the agent loop.
Quick start
Pass built-in hooks directly tocallModel with the hooks option:
HooksManager when
you need custom hooks, dynamic registration, programmatic emission, or
explicit lifecycle control.
HooksManager
Create a manager and pass it to one or morecallModel calls:
HooksManager is available from @openrouter/agent and from the focused
@openrouter/agent/hooks-manager subpath.
Every handler receives a validated payload and a context object:
callModel runs without
mixing their session IDs.
Built-in hooks
SessionStart
SessionStart fires once before the initial model request. Its optional
config summarizes the run without exposing the full request:
The exported payload type allows an extensible config record:
UserPromptSubmit
UserPromptSubmit fires before the initial user prompt is sent:
mutatedPrompt replaces the prompt. reject: true aborts with a default
error, while a non-empty string becomes the rejection reason.
For structured input, the SDK uses the latest user-role text. If it cannot
extract user text, it skips this hook.
PostModelCall
PostModelCall fires once for every completed model response, including:
- The initial request
- Approval or HITL resume requests
- Follow-up requests after tool rounds
- The final request created by
allowFinalResponse - Empty-final retries
responseId is the OpenRouter generation ID. durationMs measures from
request dispatch until the full response is materialized, including stream
consumption. The hook is observational and is suitable for one tracing span
per model call.
PostModelCall. A materialized response.incomplete response does emit it.
usage is omitted when the server does not report usage, and usage.cost is
only present when server-side usage accounting reports cost.
On a no-tools streaming path, the response becomes complete only after stream
consumption. The hook therefore fires during session teardown, immediately
before SessionEnd.
PermissionRequest
PermissionRequest fires when a tool requires approval, before the SDK
pauses for a user decision:
allowskips the approval gate and runs the tool through the normal loop.denydoes not run the tool and sends a rejected result to the model.ask_usercontinues into the normal approval flow.
riskLevel from the approval rule: callback-based rules are
high, blanket true rules are medium, and other cases are low.
PreToolUse
PreToolUse fires before client tool execution, including normal tool rounds
and resumed approval flows:
mutatedInput replaces the arguments before schema validation and execution.
block: true skips execution with a default reason. A non-empty string skips
execution and is sent to the model as the reason.
If the model supplied invalid JSON arguments, the SDK produces a parse error
without emitting the tool hooks because no valid tool input exists.
PostToolUse
PostToolUse fires after successful tool execution:
toolInput contains the effective
arguments after any PreToolUse mutation.
PostToolUseFailure
PostToolUseFailure fires when a tool executes and throws or returns an
error:
PermissionRequest
denial, user rejection, PreToolUse block, invalid JSON arguments, or an HITL
tool pausing for later input. Observe those outcomes in the corresponding
gating hook. The post-use hook fires after a paused HITL tool is resumed and
successfully completes.
Stop
Stop fires when a stopWhen condition halts the loop:
appendPrompt injects a user message, but does not by itself continue the
tool loop. Prompts from multiple handlers are joined with newlines. If any
handler returns forceResume: true, the loop continues, subject to a limit of
three consecutive overrides without progress.
A tool output or a fresh model response resets that counter. Blocked and
denied tool outputs also count as progress because the model receives the
feedback. A bare forceResume normally triggers the same stop condition again
and uses the override limit. Pair it with appendPrompt or a stop condition
whose external state can change.
SessionEnd
SessionEnd fires once when a started session exits:
totalUsage is present when at least one model response completed. Token
fields are summed across calls. cost is included when at least one response
reported it.
When SessionStart succeeds, SessionEnd fires exactly once for that run on
completion, approval pause, interruption, error, and no-tools streaming
teardown. An approval pause currently ends with reason: 'complete'.
Approval and HITL resume calls skip SessionStart and SessionEnd, but tool
hooks still fire and detached work is still drained. A teardown error is
logged and never replaces the run’s original error.
Matchers and filters
Usematcher to register handlers for selected tool names:
filter receives the hook payload and can add any further condition.
Matchers are intended for tool-scoped hooks. Use filter by itself for
session, prompt, model-call, and stop hooks.
Handler chain behavior
Handlers run sequentially in registration order.- The SDK checks the handler’s matcher and filter.
- It calls the handler with the latest payload.
- A mutation replaces the corresponding payload field for later handlers.
- A block or rejection stops the remaining handler chain.
mutatedInput or mutatedPrompt
instead of mutating nested payload values in place.
By default, a throwing handler, matcher, or filter is logged and skipped. Use
strict mode to propagate these failures, which is useful in tests:
Asynchronous handlers
Handlers may return a promise. The chain waits for a normal asynchronous handler before running the next handler:AsyncOutput signal:
context.signal.
The manager tracks detached work. Use its lifecycle methods during shutdown:
abortInflight() aborts the signals for active handler work. drain() waits
for all detached work to settle. The agent loop also drains pending hook work
when a run ends.
Use the exported isAsyncOutput(value) type guard when handling hook return
values yourself. Objects with extra fields are not asynchronous signals, so a
mutation cannot be accidentally discarded.
Custom hooks
Define custom hooks with Zod payload and result schemas:emit(). Zod transforms,
defaults, and coercion are applied before handlers run, so handlers receive
the schema output type.
Custom hook names must be non-empty and cannot collide with built-in names.
Custom hooks do not inherit built-in mutation or blocking behavior. Inline
configuration only supports built-in hooks.
When directly emitting from a manager shared by concurrent operations, pass a
per-emit session ID:
setSessionId() sets a manager-wide default for direct emissions. It is
last-writer-wins, so prefer the per-emit value when operations may overlap.
HooksManager API
constructor
customHooksmaps custom names to Zodpayloadandresultschemas.options.throwOnHandlerErrorpropagates hook errors whentrue.
on
handler and optional matcher and filter.
off
removeAll
setSessionId
context.sessionId for direct emit() calls. This mutable
default is last-writer-wins. Use the per-emit override for concurrent work.
emit
toolName supplies the value used by matchers. sessionId overrides the
manager default for this emission.
hasHandlers
abortInflight and drain
Next steps
- Tools - Define and execute typed tools
- Tool Approval & State - Pause and resume approval-gated tools
- Stop Conditions - Control when the agent loop ends
- API Reference - Review complete
callModeltypes and exports