Hooks
Defined in: primitives/loop.ts:105
Lifecycle hooks for guardrails and context shaping.
Remarks
Every hook is optional and may be sync or async. They run at fixed points in a turn: transformContext just before the model call, gateToolCalls once per turn ahead of execution — the single point that admits or blocks calls — then afterToolCall after each individual call runs.
Methods
afterToolCall()?
optional afterToolCall(info):
| void
| ToolResultOverride
| Promise<void | ToolResultOverride>;Defined in: primitives/loop.ts:148
Inspect/override a tool result after it executes.
Parameters
| Parameter | Type | Description |
|---|---|---|
info | { args: ToolArguments; isError: boolean; result: ToolResult; toolCall: ToolCall; } | The call, its args, the produced result, and isError. |
info.args | ToolArguments | - |
info.isError | boolean | - |
info.result | ToolResult | - |
info.toolCall | ToolCall | - |
Returns
| void
| ToolResultOverride
| Promise<void | ToolResultOverride>
Nothing to keep the result, or a ToolResultOverride.
drainFollowUp()?
optional drainFollowUp():
| Message[]
| Promise<Message[]>;Defined in: primitives/loop.ts:192
Pull queued follow-up messages when the run would otherwise stop at a natural final answer (a turn with no tool calls).
Returns
| Message[]
| Promise<Message[]>
Messages to inject now, or an empty array to let the run end.
Remarks
Returning a non-empty array continues the run in place — the messages are
appended (and emitted as
message_injected) and another turn
runs — instead of ending. Keeping it one continuous run (rather than a fresh
runAgent call) is what keeps the trace a single
agent_start → agent_end with monotonic steps. Like
drainSteering, it is not consulted once the
maxSteps cap is reached.
drainSteering()?
optional drainSteering():
| Message[]
| Promise<Message[]>;Defined in: primitives/loop.ts:175
Pull queued steering messages to inject before the next turn.
Returns
| Message[]
| Promise<Message[]>
Messages to inject now, or an empty array to leave the run as-is.
Remarks
The loop calls this once per turn, right after the turn's tool results are
recorded — so tool_use/tool_result pairing is always intact and the next
turn sees [assistant tool_calls][tool results][steering]. Returning a
non-empty array redirects the run: the messages are appended (and emitted as
message_injected), and the run takes
another turn even if a tool asked to terminate
or a stopWhen would have fired. It never
overrides the maxSteps cap — neither queue
is drained once the cap is reached, so queued messages stay for a later run.
The caller owns the queue; the loop only pulls. Returning how many messages (one vs all queued) is the caller's "one-at-a-time" vs "all" policy. This is the seam steering only matters across — input that arrives while a run is in flight — so the feeding source must be non-blocking.
gateToolCalls()?
optional gateToolCalls(batch):
| GateDecision[]
| Promise<GateDecision[]>;Defined in: primitives/loop.ts:141
Admit or block tool calls as a batch, before any of them execute.
Parameters
| Parameter | Type | Description |
|---|---|---|
batch | ToolGateRequest[] | The turn's well-formed tool calls with parsed arguments. |
Returns
| GateDecision[]
| Promise<GateDecision[]>
One GateDecision per request, index-aligned with batch.
Remarks
The whole turn's calls arrive together, so this runs once per turn —
serially, ahead of the parallel execution phase — which makes it the right
place to prompt for permission without racing concurrent prompts. Only
well-formed calls (known tool + valid args) are presented; unknown/invalid
calls skip the gate and surface as the usual error results. See
../permissions for an allow/deny/ask implementation.
See
transformContext()?
optional transformContext(messages):
| Message[]
| Promise<Message[]>;Defined in: primitives/loop.ts:124
Reshape history right before it's sent to the model.
Parameters
| Parameter | Type | Description |
|---|---|---|
messages | Message[] | The current working history about to be sent. |
Returns
| Message[]
| Promise<Message[]>
The (possibly reshaped) messages to send to the model.
Remarks
This is the seam for long-horizon context management — compaction (summarize, then restart from the summary), structured note-taking, and tool-result clearing — that keeps a long-running agent inside its context window.
Sources of truth:
- Anthropic, effective context engineering for AI agents: https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
- Anthropic, effective harnesses for long-running agents: https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents