Hooks
Introduction
Hooks let you intercept every phase of the agent’s execution lifecycle. They are the primary extension mechanism for cross-cutting concerns — logging, rate limiting, safety guards, telemetry, state transformation, and tool access control. Each hook receives aHookContext containing the current agent state and trigger-specific data, processes it, and returns a (potentially modified) context to continue the pipeline. Because both HookContext and AgentState are immutable, hooks compose safely — each hook in the chain works with the output of the previous one, and no hook can accidentally corrupt shared state.
Design Philosophy: Hooks follow the middleware pattern common in web frameworks, but adapted for agent execution. Instead of intercepting HTTP requests, hooks intercept the agent’s internal lifecycle events — giving you the same power to observe, modify, or short-circuit execution at precisely the right moment.
Lifecycle Events
The agent loop emits eight trigger types at well-defined points during execution. Each trigger corresponds to a specific moment in the loop’s lifecycle, and understanding when each fires is essential for placing your hooks correctly:| Trigger | When It Fires | Available Data |
|---|---|---|
BeforeExecution | Once, before the loop begins its first step | Agent state |
BeforeStep | Before each LLM call | Agent state |
BeforeToolUse | Before each individual tool execution | Agent state, ToolCall |
AfterToolUse | After each individual tool execution | Agent state, ToolExecution |
AfterStep | After each loop iteration completes | Agent state |
OnStop | When the loop detects a stop condition | Agent state |
AfterExecution | Once, after the loop ends | Agent state |
OnError | When an error occurs during execution | Agent state, ErrorList |
HookTrigger enum:
OnError trigger fires with the accumulated error information.
Implementing a Hook
Create a class that implementsHookInterface. The handle method receives a HookContext and must return one — either the original context unchanged, or a modified copy:
Understanding HookContext
TheHookContext object provides access to different data depending on the trigger type. It serves as both the input and output of hook processing, carrying all the information a hook needs to make decisions:
| Method | Return Type | Description | Available On |
|---|---|---|---|
state() | AgentState | The current agent state with full access to context, messages, metadata, and execution data | All triggers |
triggerType() | HookTrigger | The enum value identifying which lifecycle event fired this hook | All triggers |
toolCall() | ?ToolCall | The tool call about to be executed, including the tool name and arguments | BeforeToolUse |
toolExecution() | ?ToolExecution | The completed tool execution result, including output and status | AfterToolUse |
errorList() | ErrorList | Accumulated errors from the execution | OnError (primarily) |
metadata() | mixed | Arbitrary metadata passed with the trigger; accepts an optional key and default value | All triggers |
createdAt() | DateTimeImmutable | When this hook context was created | All triggers |
updatedAt() | DateTimeImmutable | When this hook context was last modified by a hook | All triggers |
hasErrors() | bool | Whether the error list contains any errors | All triggers |
isToolExecutionBlocked() | bool | Whether tool execution has been blocked by a hook | BeforeToolUse |
HookContext also provides convenient named constructors for each trigger type, used internally by the agent loop:
Registering Hooks
Via AgentBuilder (Recommended)
TheUseHook capability provides a declarative way to register hooks during agent construction. Each UseHook instance binds a hook implementation to one or more triggers with a specified priority:
HookTriggers::of():
HookTriggers class provides convenience constructors for every trigger type, as well as the ability to combine them:
Via HookStack (Manual)
When composing anAgentLoop directly without the builder, assemble hooks into a HookStack. The HookStack wraps a RegisteredHooks collection and implements the CanInterceptAgentLifecycle interface, making it pluggable into the agent loop:
HookStack is immutable — each with() call returns a new instance with the hook added and the collection re-sorted by priority. You can chain multiple hooks fluently:
RegisteredHook directly:
CallableHook
For quick, one-off hooks that do not warrant a dedicated class, useCallableHook with a closure. This is particularly handy for prototyping or adding simple logging during development:
CallableHook accepts any callable that takes a HookContext and returns a HookContext. It converts the callable to a Closure internally for type safety.
Hook Priority
When a trigger fires, hooks are executed in descending priority order — higher values run first. This ordering is critical when hooks have dependencies on each other. For example, guard hooks that may emit stop signals should run before business logic hooks that assume the loop will continue. TheRegisteredHooks collection sorts hooks automatically when they are added. The sort is stable, so hooks with the same priority retain their registration order.
The built-in guard hooks use a priority of 200 (or -200 for the finish reason guard, which runs on AfterStep), giving them precedence over custom hooks at the default priority of 0. Choose your priorities according to the following guidelines:
| Range | Suggested Use | Examples |
|---|---|---|
| 200+ | Safety guards, resource limits | Step limits, token limits, time limits |
| 100-199 | Infrastructure concerns | Logging, telemetry, metrics collection |
| 0-99 | Business logic, custom behavior | State enrichment, conditional branching |
| Negative | Post-processing, cleanup | Finish reason detection, result formatting |
Tip: When in doubt, use the default priority of 0. Only assign explicit priorities when you need guaranteed ordering between hooks.
Modifying Agent State
Hooks can modify the agent’s state by returning aHookContext with an updated AgentState. Since both objects are immutable, you create modified copies using the with* methods:
- Injecting context — adding metadata that downstream hooks or the driver can read
- Adjusting system prompts — dynamically modifying the system prompt based on execution state
- Attaching metadata — tagging the state with timestamps, user IDs, or feature flags
- Modifying the message store — adding, removing, or transforming messages before the next LLM call
Blocking Tool Execution
In aBeforeToolUse hook, you can prevent a tool from executing by calling withToolExecutionBlocked() on the context. This is a powerful safety mechanism for restricting which tools the model can invoke at runtime:
BeforeToolUse trigger with a high priority to ensure it runs before other hooks:
- The
HookContextis marked withisToolExecutionBlocked = true - A
ToolExecutionwith blocked status is created and attached to the context - A
ToolExecutionBlockedExceptionis recorded in the error list - The loop skips the actual tool execution
- The rejection message is fed back to the model as the tool result, so it can adjust its approach
Applying Context Configuration
The built-inApplyContextConfigHook sets the system prompt and response format on the agent context at the start of execution. This is how the builder internally applies system prompt and response format settings configured through UseContextConfig:
BeforeExecution and modifies the AgentContext inside the state, ensuring the system prompt and format are in place before the first LLM call. It only applies non-empty values — an empty system prompt or a null / empty response format will leave the existing context values unchanged.
Built-in Guard Hooks
Guard hooks enforce resource limits by emitting stop signals when thresholds are exceeded. They are the primary mechanism for preventing runaway agents that might otherwise consume unlimited tokens, time, or steps.UseGuards Capability
TheUseGuards capability bundles all four guards with sensible defaults, providing a convenient one-liner for common resource protection:
null to disable a specific guard. The defaults are:
| Parameter | Default | Description |
|---|---|---|
maxSteps | 20 | Maximum number of loop iterations |
maxTokens | 32768 | Maximum cumulative token usage across all LLM calls |
maxExecutionTime | 300.0 | Maximum wall-clock seconds for the entire execution |
finishReasons | [] | LLM finish reasons that should trigger a stop (empty = disabled) |
Individual Guard Hooks
You can also register guards individually for finer control over triggers, priorities, and configuration.StepsLimitHook
Stops the loop after a maximum number of steps. It accepts a callablestepCounter that extracts the current step count from the agent state, making it flexible enough to count different things (e.g., total steps, steps within the current execution):
StopSignal with reason StepsLimitReached and a descriptive message like "Step limit reached: 10/10".
TokenUsageLimitHook
Stops the loop when cumulative token usage (input + output tokens across all LLM calls) exceeds a threshold. Token usage is tracked automatically by the agent state through theusage() accessor:
StopSignal with reason TokenLimitReached.
ExecutionTimeLimitHook
Stops the loop after a wall-clock duration. Unlike other guards, this hook needs to listen to two triggers:BeforeExecution to record the start time, and BeforeStep to check elapsed time before each LLM call:
DateTimeImmutable with U.u format) for accurate timing. When the limit is reached, it emits a StopSignal with reason TimeLimitReached.
Note: The UseGuards capability handles the dual-trigger registration automatically. You only need to manage it manually when registering the hook directly.
FinishReasonHook
Stops the loop when the LLM’s finish reason matches a specified set. This is useful for stopping when the model indicates it has finished naturally (e.g.,stop finish reason) rather than being cut off by a token limit. It runs on AfterStep since the finish reason is only available after the model responds:
UseGuards, this hook receives a priority of -200 (running after other AfterStep hooks) to ensure all post-step processing has completed before checking the finish reason.
How Hooks Execute
When a trigger fires, theHookStack iterates through all registered hooks sorted by priority (descending). Each hook that matches the trigger type receives the HookContext, processes it, and returns a (potentially modified) context. The returned context flows into the next hook in the chain:
HookExecuted event containing the trigger type, hook name, and execution timestamp — enabling external observability and performance monitoring.
The HookStack implements CanInterceptAgentLifecycle, meaning it can be replaced entirely with a custom interception strategy. The PassThroughInterceptor is a no-op implementation that returns the context unchanged, useful for testing or when you want to disable all hooks: