Hooks and contexts
Hooks that change the agent loop, the contexts your code gets, and the helpers that give the same values when a deployed agent runs the loop again.
Give a hook result's content as content blocks, such as agents.TextContentBlock(text="..."), or use agents.skip(reason), agents.retry(feedback) and agents.reject(reason) with a string.
Guide: Guardrails.
| Name | What it is |
|---|---|
Hook | Code that runs at points of the agent loop: before and after each turn, model call and tool call. |
HookDecision | A hook's decision, turned into the result of the point that returns it. |
agents.skip | Stop the turn, model call or tool call. |
agents.retry | Discard the model's response and call the model again with feedback. |
agents.reject | Reject the model's response and end the turn with reason. |
agents.hook | What a hook gets at each point and returns: continue, or replace the input or output. |
State | The saved form of a hook or sandbox: its type, and the JSON data it keeps across turns. |
hook.PreAgentTurn | The turn a pre_agent_turn hook receives. |
hook.PreLlmCall | The model call a pre_llm_call hook receives. |
hook.PostLlmCall | The model reply a post_llm_call hook receives. |
hook.PostAgentTurn | The final answer of the turn a post_agent_turn hook receives. |
hook.PreToolCall | The tool call a pre_tool_call hook receives. |
hook.PostToolCall | The tool call and result a post_tool_call hook receives. |
hook.PreAgentTurnHookContinue | What pre_agent_turn returns to start the turn with other user content. |
hook.PreAgentTurnHookSkip | What pre_agent_turn returns to end the turn before the model runs, as agents.skip does. |
hook.PreLlmCallHookContinue | What pre_llm_call returns to let the model call run, as None does. |
hook.PreLlmCallHookSkip | What pre_llm_call returns to end the turn instead of calling the model, as agents.skip does. |
hook.PostLlmCallHookAccept | What post_llm_call returns to keep the model's response, or to replace its content. |
hook.PostLlmCallHookRetry | What post_llm_call returns to drop the response and call the model again, as agents.retry does. |
hook.PostLlmCallHookReject | What post_llm_call returns to reject the response and end the turn, as agents.reject does. |
hook.PreToolCallHookContinue | What pre_tool_call returns to run the tool with other arguments. |
hook.PreToolCallHookSkip | What pre_tool_call returns to skip the call, as agents.skip does. |
hook.PostToolCallHookContinue | What post_tool_call returns to give the model another tool result. |
hook.PostAgentTurnHookAccept | What post_agent_turn returns to keep the final answer, or to replace its content. |
hook.ReplaceAssistantContent | Replace the model's reply with content, from post_llm_call or post_agent_turn. |
pattern.hook.StructuredOutputHook | Validate the final answer of each turn with a Pydantic model, and ask the model to correct it. |
CallbackContext | What a callback gets as ctx: the session it runs in and the session's sandbox. |
CallbackSession | The session a callback runs in: its ID and the turn's. |
HookCallbackContext | What a hook method gets as ctx: the session it runs in and the session's sandbox. |
ToolCallbackContext | What a tool gets as its first, positional-only parameter: the session, its sandbox and the call. |
ApprovalCallbackContext | What an approval handler gets as ctx: the session that asked, and the approval's ID. |
SandboxCallbackContext | Context a sandbox hook receives in on_create and on_destroy. |
agents.now | The current time in UTC, as a timezone-aware datetime. |
agents.sleep | Wait for duration, in the agent loop, a tool, a Hook or a local run. |
agents.uuid4 | A random UUID that stays the same when a deployed agent runs the agent loop again. |
agents.random | A random number generator that gives the same numbers when a deployed agent runs the loop again. |
Hooks
HookHook
class Hook(*, tools: Sequence[PythonTool[..., Any] | ClientTool[..., Any]] = (), **data: Any)Code that runs at points of the agent loop: before and after each turn, model call and tool call.
Override the points you need; returning None continues unchanged. A hook is a Pydantic
model. Its fields are its state: the session keeps their values and every call starts from the
values the last successful call left. Anything else the hook holds, such as a client, is a
PrivateAttr, which is never saved. __init__ runs only when you build the hook: each call
starts from a copy.
A saved value records its class, and the SDK finds the class again among the modules already
loaded. Loading a value never imports a module. Define every hook class, and the class of every
hook kept in a field, at module level, in a module your agent's module imports. Otherwise,
creating a session fails. Keep hooks held in a field in a list or a tuple. A set raises
SdkError.
A field with an alias, such as max_calls: int = Field(0, alias="maxCalls"), takes its name
when you build the hook, Budget(max_calls=5), and its alias in the saved value. To build the
hook by alias too, set validate_by_alias=True in model_config. Pydantic's own
model_validate reads a hook by field name, so it does not load the saved value of a hook
with an aliased field.
| Attribute | Type | Description |
|---|---|---|
tools | Sequence[PythonTool[..., Any]] | The tools that the hook adds to the agent's tools. |
Hook.pre_agent_turnHook.pre_agent_turn
async def pre_agent_turn(
ctx: HookCallbackContext,
request: hook.PreAgentTurn,
) -> hook.PreAgentTurnHookContinue | hook.PreAgentTurnHookSkip | HookDecision | NoneRuns before each turn, with its user content in request.user_content.
Return hook.PreAgentTurnHookContinue to send other content, such as
[*request.user_content, agents.TextContentBlock(text="...")], or agents.skip or
hook.PreAgentTurnHookSkip to skip the turn.
Hook.post_agent_turnHook.post_agent_turn
async def post_agent_turn(
ctx: HookCallbackContext,
result: hook.PostAgentTurn,
) -> hook.PostAgentTurnHookAccept | NoneRuns after each turn, with the model's final answer in result: result.text, result.content.
Return hook.PostAgentTurnHookAccept() to keep the answer, or replace its content with
acceptance=hook.ReplaceAssistantContent(content=[agents.TextContentBlock(text="...")]).
Hook.pre_llm_callHook.pre_llm_call
async def pre_llm_call(
ctx: HookCallbackContext,
invocation: hook.PreLlmCall,
) -> hook.PreLlmCallHookContinue | hook.PreLlmCallHookSkip | HookDecision | NoneRuns before each model call. Return agents.skip or hook.PreLlmCallHookSkip to end the turn
without calling the model (stop_reason="skipped").
Hook.post_llm_callHook.post_llm_call
async def post_llm_call(
ctx: HookCallbackContext,
invocation: hook.PostLlmCall,
) -> hook.PostLlmCallHookAccept | hook.PostLlmCallHookRetry | hook.PostLlmCallHookReject | HookDecision | NoneRuns after each model call, with its response in invocation: invocation.text,
invocation.tool_calls, invocation.content.
Return hook.PostLlmCallHookAccept to keep the response or replace its content, as
hook.PostAgentTurnHookAccept does, agents.retry to call the model again with feedback, or
agents.reject to end the turn.
Hook.pre_tool_callHook.pre_tool_call
async def pre_tool_call(
ctx: HookCallbackContext,
call: hook.PreToolCall,
) -> hook.PreToolCallHookContinue | hook.PreToolCallHookSkip | HookDecision | NoneRuns before each tool call, with the call in call.
Return hook.PreToolCallHookContinue to run the tool with other effective_arguments, or
agents.skip or hook.PreToolCallHookSkip to skip the call: the model sees the reason instead.
The history's tool call entry shows the arguments the hook set; a skipped call keeps the model's.
Hook.post_tool_callHook.post_tool_call
async def post_tool_call(
ctx: HookCallbackContext,
result: hook.PostToolCall,
) -> hook.PostToolCallHookContinue | NoneRuns after each tool call, with the call and its result, an agents.ToolResult, in result.
Return hook.PostToolCallHookContinue with another tool_result to change what the model gets,
such as agents.ToolResult(content=[agents.TextContentBlock(text="...")]).
HookDecisionHookDecision
class HookDecisionA hook's decision, turned into the result of the point that returns it.
Build it with agents.skip, agents.retry or agents.reject.
| Attribute | Type | Description |
|---|---|---|
kind | Literal['skip', 'retry', 'reject'] | "skip", "retry" or "reject": the function that built it. |
message | str | The reason or feedback the function took. |
agents.skipagents.skip
def agents.skip(reason: str) -> HookDecisionStop the turn, model call or tool call.
From pre_tool_call, reason is what the model sees in place of the tool
result. From pre_agent_turn or pre_llm_call, the turn ends with
stop_reason="skipped" and reason as its text.
agents.retryagents.retry
def agents.retry(feedback: str) -> HookDecisionDiscard the model's response and call the model again with feedback.
agents.rejectagents.reject
def agents.reject(reason: str) -> HookDecisionReject the model's response and end the turn with reason.
agents.hookagents.hook
What a hook gets at each point and returns: continue, or replace the input or output.
A hook that continues unchanged returns None; one that stops returns agents.skip,
agents.retry or agents.reject.
Its classes are under Classes of agents.hook.
StateState
class State(*, type: str, state: dict[str, JsonValue])The saved form of a hook or sandbox: its type, and the JSON data it keeps across turns.
| Parameter | Type | Default | Description |
|---|---|---|---|
type | str | required | The type that rebuilds the value, as module:QualifiedName. |
state | dict[str, JsonValue] | required | The value's saved data: a hook's fields, or what a sandbox hook's SandboxHook.to_unified_state() returns. |
Classes of agents.hook
hook.PreAgentTurnhook.PreAgentTurn
class PreAgentTurn(*, turn_id: str, user_content: list[ContentBlock])The turn a pre_agent_turn hook receives.
hook.PreLlmCallhook.PreLlmCall
class PreLlmCall(*, turn_id: str)The model call a pre_llm_call hook receives.
hook.PostLlmCallhook.PostLlmCall
class PostLlmCall(
*,
turn_id: str,
content: list[ContentBlock],
text: str,
tool_calls: tuple[ToolCall, ...],
reasoning: str,
finish_reason: Literal['stop', 'tool_call', 'length', 'content_filter', 'other'],
usage: TokenUsage | None,
)The model reply a post_llm_call hook receives.
hook.PostAgentTurnhook.PostAgentTurn
class PostAgentTurn(
*,
turn_id: str,
content: list[ContentBlock],
text: str,
tool_calls: tuple[ToolCall, ...],
reasoning: str,
finish_reason: Literal['stop', 'tool_call', 'length', 'content_filter', 'other'],
usage: TokenUsage | None,
)The final answer of the turn a post_agent_turn hook receives.
hook.PreToolCallhook.PreToolCall
class PreToolCall(
*,
turn_id: str,
tool_name: str,
tool_id: str,
tool_call_id: str,
arguments: dict[str, JsonValue],
)The tool call a pre_tool_call hook receives.
| Parameter | Type | Default | Description |
|---|---|---|---|
turn_id | str | required | The turn's ID. |
tool_name | str | required | The authored tool's name, or the built-in's, such as file_system.bash. |
tool_id | str | required | The tool's ID: <namespace>.<name>, such as billing.charge, or the built-in's name. |
tool_call_id | str | required | The call's ID, as ToolCallEntry.tool_call_id and ToolCallbackContext.tool_call_id give it. |
arguments | dict[str, JsonValue] | required | The arguments the model produced. |
hook.PostToolCallhook.PostToolCall
class PostToolCall(
*,
turn_id: str,
tool_name: str,
tool_id: str,
tool_call_id: str,
arguments: dict[str, JsonValue],
tool_result: ToolResult,
)The tool call and result a post_tool_call hook receives.
Inherits turn_id, tool_name, tool_id and tool_call_id from hook.PreToolCall.
| Parameter | Type | Default | Description |
|---|---|---|---|
arguments | dict[str, JsonValue] | required | The arguments the tool ran with, after any pre_tool_call hook changed them. |
tool_result | ToolResult | required | The call's result. Return it, or a copy, in hook.PostToolCallHookContinue: blocks handed back unchanged reach the model as they came, and a failed call kept is_error=True keeps its error. |
hook.PreAgentTurnHookContinuehook.PreAgentTurnHookContinue
class PreAgentTurnHookContinue(*, user_content: Sequence[ContentBlock])What pre_agent_turn returns to start the turn with other user content.
hook.PreAgentTurnHookSkiphook.PreAgentTurnHookSkip
class PreAgentTurnHookSkip(*, reason: Sequence[ContentBlock] = [])What pre_agent_turn returns to end the turn before the model runs, as agents.skip does.
hook.PreLlmCallHookContinuehook.PreLlmCallHookContinue
class PreLlmCallHookContinue()What pre_llm_call returns to let the model call run, as None does.
hook.PreLlmCallHookSkiphook.PreLlmCallHookSkip
class PreLlmCallHookSkip(*, reason: Sequence[ContentBlock] = [])What pre_llm_call returns to end the turn instead of calling the model, as agents.skip does.
hook.PostLlmCallHookAccepthook.PostLlmCallHookAccept
class PostLlmCallHookAccept(*, acceptance: ReplaceAssistantContent = ...)What post_llm_call returns to keep the model's response, or to replace its content.
| Parameter | Type | Default | Description |
|---|---|---|---|
acceptance | ReplaceAssistantContent | ... | The default keeps the response as the model gave it. hook.ReplaceAssistantContent(content=[...]) replaces its content. |
hook.PostLlmCallHookRetryhook.PostLlmCallHookRetry
class PostLlmCallHookRetry(*, feedback: Sequence[ContentBlock])What post_llm_call returns to drop the response and call the model again, as agents.retry does.
hook.PostLlmCallHookRejecthook.PostLlmCallHookReject
class PostLlmCallHookReject(*, reason: Sequence[ContentBlock])What post_llm_call returns to reject the response and end the turn, as agents.reject does.
hook.PreToolCallHookContinuehook.PreToolCallHookContinue
class PreToolCallHookContinue(*, effective_arguments: dict[str, JsonValue] = {})What pre_tool_call returns to run the tool with other arguments. Pass effective_arguments=; with none, the tool runs with no arguments.
hook.PreToolCallHookSkiphook.PreToolCallHookSkip
class PreToolCallHookSkip(*, reason: Sequence[ContentBlock] = [])What pre_tool_call returns to skip the call, as agents.skip does.
hook.PostToolCallHookContinuehook.PostToolCallHookContinue
class PostToolCallHookContinue(*, tool_result: ToolResult)What post_tool_call returns to give the model another tool result.
hook.PostAgentTurnHookAccepthook.PostAgentTurnHookAccept
class PostAgentTurnHookAccept(*, acceptance: ReplaceAssistantContent = ...)What post_agent_turn returns to keep the final answer, or to replace its content.
| Parameter | Type | Default | Description |
|---|---|---|---|
acceptance | ReplaceAssistantContent | ... | The default keeps the answer as the model gave it. hook.ReplaceAssistantContent(content=[...]) replaces its content. |
hook.ReplaceAssistantContenthook.ReplaceAssistantContent
class ReplaceAssistantContent(*, content: Sequence[ContentBlock])Replace the model's reply with content, from post_llm_call or post_agent_turn.
Ready-made hooks
pattern.hook.StructuredOutputHookpattern.hook.StructuredOutputHook
class StructuredOutputHook[T](
*,
tools: Sequence[PythonTool[..., Any] | ClientTool[..., Any]] = (),
**data: Any,
)Validate the final answer of each turn with a Pydantic model, and ask the model to correct it.
Name the Pydantic model in brackets, as in StructuredOutputHook[City](max_retries=2), or subclass
StructuredOutputHook[City] and set new defaults for its fields.
The hook asks for a correction only while max_retries allows it and the turn has an
iteration left under the agent's max_iterations. At that limit it accepts the last answer,
or, when on_limit is "reject", raises ValueError, which fails the turn. Set
schema_in_prompt to False to leave the JSON Schema out of the user message, for example
when output_type already gives it to the model; corrections still include it. Set
enabled to False to let answers through unchecked.
The hook counts each model call before it is made, so a call that a guardrail or an earlier
hook retries still counts toward max_iterations.
Based on Hook.
| Attribute | Type | Description |
|---|---|---|
max_retries | int | How many corrections the hook asks for in one turn. Default: 2. |
on_limit | Literal['accept', 'reject'] | What the hook does at the limit: "accept", the default, keeps the last answer; "reject" raises ValueError, which fails the turn. |
schema_in_prompt | bool | Whether the user message includes the JSON Schema. Default: True. Corrections always include it. |
enabled | bool | Whether the hook checks answers. Default: True. False lets them through unchecked. |
Contexts
CallbackContextCallbackContext
class CallbackContextWhat a callback gets as ctx: the session it runs in and the session's sandbox.
The shared base: a hook method gets a HookCallbackContext, and a tool a ToolCallbackContext.
| Attribute | Type | Description |
|---|---|---|
session | CallbackSession | The session the callback runs in. |
sandbox | HookSandbox | The session's sandbox, to run commands and read and write files. await ctx.sandbox.execute(command, cwd=None, timeout_seconds=None, env=None) runs a command once and returns a SandboxExecutionResult. await ctx.sandbox.read_file(path, max_bytes=1048576) returns a file's bytes. A relative path is under the workspace. A file larger than max_bytes raises an error, and a missing one raises FileNotFoundError. await ctx.sandbox.write_file(name, content) writes bytes to a file in a folder the sandbox keeps for hook files, outside the workspace, and returns its path. An absolute name, or one that leaves that folder, raises ValueError. In a sandbox hook of an agent that agents serve or agents deploy runs, content must be UTF-8 text and the request, path included, must fit in 1 MiB of JSON. Each method raises RuntimeError when the session has no sandbox that runs commands. |
CallbackSessionCallbackSession
class CallbackSessionThe session a callback runs in: its ID and the turn's.
| Attribute | Type | Description |
|---|---|---|
id | str | The session's ID. |
turn_id | str | The ID of the turn the callback runs in. |
HookCallbackContextHookCallbackContext
class HookCallbackContext(CallbackContext)What a hook method gets as ctx: the session it runs in and the session's sandbox.
ToolCallbackContextToolCallbackContext
class ToolCallbackContext(CallbackContext)What a tool gets as its first, positional-only parameter: the session, its sandbox and the call.
A tool takes it by type, under any name: def search(ctx: agents.ToolCallbackContext, /, query: str).
The model never sees it. A direct call can pass ToolCallbackContext.local(), or leave it out to
get that local context. A client tool's answer gets it too, with no sandbox, so the methods of
ctx.sandbox raise RuntimeError.
| Attribute | Type | Description |
|---|---|---|
tool_call_id | str | The ID of the model's call to the tool, as in the session's tool call entry. |
ToolCallbackContext.localToolCallbackContext.local
@classmethod
def local() -> ToolCallbackContextA context for a call outside any session: every ID is "local" and the sandbox is unbound.
ApprovalCallbackContextApprovalCallbackContext
class ApprovalCallbackContext(CallbackContext)What an approval handler gets as ctx: the session that asked, and the approval's ID.
ctx.session.id and ctx.session.turn_id name the session and turn that asked: a
subagent's approval has the subagent's session and its own turn there. ctx.callback_id is the approval's ID, the
one turn.answer takes. The handler runs in your program, so the methods of ctx.sandbox
raise RuntimeError.
| Attribute | Type | Description |
|---|---|---|
callback_id | str | The approval's ID, the one turn.answer takes. |
SandboxCallbackContextSandboxCallbackContext
class SandboxCallbackContextContext a sandbox hook receives in on_create and on_destroy.
| Attribute | Type | Description |
|---|---|---|
sandbox | HookSandbox | The sandbox, to run commands and read and write files in it. |
Same values when the loop runs again
agents.nowagents.now
def agents.now() -> datetimeThe current time in UTC, as a timezone-aware datetime.
Use it instead of datetime.now(). In the agent loop of a deployed agent, such as in a custom
sandbox's methods or a SandboxHook, it gets the same time when the agent runs the loop again
to resume a session, so a deadline holds. In a tool, a Hook or a local run, it is the clock's
time: tools and Hook methods run as recorded steps, outside the part of the loop a deployed
agent replays.
agents.sleepagents.sleep
async def agents.sleep(duration: timedelta | float, *, summary: str | None = None) -> NoneWait for duration, in the agent loop, a tool, a Hook or a local run.
Use it instead of asyncio.sleep(). In the agent loop of a deployed agent, such as in a custom
sandbox's methods or a SandboxHook, the wait is a timer on the server: it survives a restart
of the agent, and nothing of the agent runs while it waits. Tools and Hook methods run as
recorded steps, outside the part of the loop a deployed agent replays: there the wait must end
within the step's time limit. In a local run, it is asyncio.sleep().
agents.uuid4agents.uuid4
def agents.uuid4() -> UUIDA random UUID that stays the same when a deployed agent runs the agent loop again.
Use it instead of uuid.uuid4(). In a tool, a Hook or a local run, it is uuid.uuid4().
agents.randomagents.random
def agents.random() -> RandomA random number generator that gives the same numbers when a deployed agent runs the loop again.
Use it instead of the random module. It has methods such as random, uniform,
choice and shuffle. In a tool, a Hook or a local run, it is a new
random.Random.