Files
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

610 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Hooks"
id: hooks-api
description: "Hooks that run at points in the Agent's run loop and influence it by mutating State, including built-in tool result offloading."
slug: "/hooks-api"
---
## from_function
### FunctionHook
Wraps a function (or a sync/async pair) into a serializable `Hook`.
Produced by the `@hook` decorator for the single-function case. To give a hook both an optimized sync and async
path, construct it directly with both `function` and `async_function` set.
#### __init__
```python
__init__(
function: Callable[[State], None] | None = None,
async_function: Callable[[State], Awaitable[None]] | None = None,
) -> None
```
Initialize the hook with a synchronous function, an async function, or both.
**Parameters:**
- **function** (<code>Callable\\[[State\], None\] | None</code>) The synchronous function invoked by `run`. Must be a regular function — coroutine functions
should be passed to `async_function` instead. Either `function` or `async_function` (or both) must be set.
- **async_function** (<code>Callable\\[[State\], Awaitable[None]\] | None</code>) Optional coroutine function awaited by `run_async`. When only `async_function` is set,
`run` raises a `RuntimeError`. When only `function` is set, `run_async` calls `function`.
**Raises:**
- <code>ValueError</code> If neither is set, if `function` is a coroutine function, if `async_function` is not, or
if a provided function does not declare a `State`-typed parameter.
#### run
```python
run(state: State) -> None
```
Run the synchronous function against the live `State`.
**Parameters:**
- **state** (<code>State</code>) The Agent's live `State`, mutated in place by the wrapped function.
**Raises:**
- <code>RuntimeError</code> If the hook only has an `async_function`; use the Agent's async run methods instead.
#### run_async
```python
run_async(state: State) -> None
```
Await the async function if set, otherwise call the synchronous function.
**Parameters:**
- **state** (<code>State</code>) The Agent's live `State`, mutated in place by the wrapped function.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the hook, storing each wrapped function as an importable reference.
**Returns:**
- <code>dict\[str, Any\]</code> A dictionary with the hook's type and the import paths of its sync/async functions.
#### from_dict
```python
from_dict(data: dict[str, Any]) -> FunctionHook
```
Deserialize the hook, resolving each function from its importable reference.
**Parameters:**
- **data** (<code>dict\[str, Any\]</code>) The serialized hook dictionary produced by `to_dict`.
**Returns:**
- <code>FunctionHook</code> The reconstructed `FunctionHook`.
### hook
```python
hook(function: Callable[[State], None | Awaitable[None]]) -> FunctionHook
```
Wrap a function into a `Hook` the Agent can invoke during its run loop.
The decorated function receives the Agent's `State` and influences the run by mutating it in place. A coroutine
function is wrapped as the hook's async path; a regular function as its sync path. To give a single hook both
paths, construct a `FunctionHook` directly with both `function` and `async_function`.
### Usage example
```python
from haystack.components.agents import Agent
from haystack.hooks import hook
from haystack.components.agents.state import State
from haystack.dataclasses import ChatMessage
@hook
def require_save(state: State) -> None:
if state.get("tool_call_counts", {}).get("save", 0) == 0:
state.set("messages", [ChatMessage.from_system("You must call `save` before finishing.")])
state.set("continue_run", True)
agent = Agent(chat_generator=..., tools=[...], hooks={"on_exit": [require_save]})
```
**Parameters:**
- **function** (<code>Callable\\[[State\], None | Awaitable[None]\]</code>) A callable taking the Agent's `State` and returning `None` (sync or async).
**Returns:**
- <code>FunctionHook</code> A `FunctionHook` wrapping the function.
## protocol
### Hook
Bases: <code>Protocol</code>
A callable the Agent invokes at a point in its run loop, receiving the live `State`.
A hook influences the run only by mutating `State` in place. At least `messages` (the conversation),
`step_count`, `token_usage` and `tool_call_counts` are available; any additional keys defined in the Agent's
`state_schema` are available too. The same hook object can be registered under multiple hook points.
Implement this protocol directly for stateful hooks (e.g. one wrapping a component), or use the `@hook` decorator to
wrap a plain `(State) -> None` function.
A hook may additionally define `async def run_async(self, state: State) -> None` for true async behavior; when
absent, the Agent calls `run` during async runs. It is left off this protocol on purpose so sync-only hooks
don't have to implement it.
A hook may also implement the optional lifecycle methods `warm_up` / `warm_up_async` and `close` / `close_async`.
The Agent calls them from its own `warm_up` / `warm_up_async` and `close` / `close_async`, so a hook can defer
opening clients or reading credentials until warm-up and release them on close.
#### run
```python
run(state: State) -> None
```
Run the hook against the live `State`, mutating it in place.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the hook to a dictionary.
#### from_dict
```python
from_dict(data: dict[str, Any]) -> Hook
```
Deserialize the hook from a dictionary.
## tool_result_offloading/hooks
### ToolResultOffloadHook
Offload tool results to a `ToolResultStore`, replacing them in the conversation with a compact pointer.
This `after_tool` Agent hook writes the full result to the store so the next LLM call sees a reference instead of
the full result. Register it on an `Agent` under the `after_tool` hook point. Which tools offload, and under what
condition, is controlled per tool by `offload_strategies`:
```python
from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.hooks.tool_result_offloading import (
AlwaysOffload,
FileSystemToolResultStore,
NeverOffload,
OffloadOverChars,
ToolResultOffloadHook,
)
hook = ToolResultOffloadHook(
store=FileSystemToolResultStore(root="tool_results"),
offload_strategies={
"web_search": AlwaysOffload(), # force offload
"get_time": NeverOffload(), # opt out
("read_file", "list_dir"): OffloadOverChars(4000), # tuple key: shared policy
"*": OffloadOverChars(8000), # wildcard default for any unlisted tool
},
)
agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[web_search, get_time, read_file, list_dir],
hooks={"after_tool": [hook]},
)
```
A key may be a single tool name, a tuple of tool names sharing one policy, or the wildcard `"*"` which applies to
any tool without a more specific entry. More specific keys win. A tool with no matching key (and no `"*"`) is not
offloaded.
Only successful, text tool output is offloaded. Error results (including `before_tool` human-in-the-loop
rejections) are always left in context. Non-text results (image or file content) are also left in context, and a
warning is logged when such a result has a matching offload policy; supporting only text is a deliberate choice
for now. Each result is offloaded at most once, even though the hook runs on every tool step.
The hook keeps no mutable state, so a single instance can be shared across concurrent runs. The constructor
`store`, however, is shared by every run that does not override it — fine for single-user or local use, but in a
multi-user server give each run its own isolated store (a per-session directory or sandbox) via `hook_context`
under the key `RESULT_STORE_CONTEXT_KEY`
(`agent.run(messages=[...], hook_context={RESULT_STORE_CONTEXT_KEY: per_request_store})`); it overrides the
constructor store for that run. Isolating the store per run keeps concurrent users from colliding on store keys or
reading each other's offloaded results — important especially when a bash/read tool is scoped to the store.
#### __init__
```python
__init__(
store: ToolResultStore,
offload_strategies: dict[str | tuple[str, ...], OffloadPolicy],
*,
preview_chars: int = 200
) -> None
```
Initialize the hook with a store and per-tool offload strategies.
**Parameters:**
- **store** (<code>ToolResultStore</code>) Where offloaded results are written. Can be overridden per run via `hook_context`.
- **offload_strategies** (<code>dict\[str | tuple\[str, ...\], OffloadPolicy\]</code>) Mapping of tool name (or a tuple of tool names, or the wildcard `"*"`) to the
`OffloadPolicy` that decides whether that tool's results are offloaded.
- **preview_chars** (<code>int</code>) Number of leading characters of the original result to include in the pointer left in
the conversation, so the model knows roughly what was offloaded.
#### run
```python
run(state: State) -> None
```
Offload the freshly produced tool results in `state.data["messages"]` according to `offload_strategies`.
Considers only the trailing block of tool-result messages (the current step's results); earlier history is
left untouched. Offloads each of those messages its policy opts in for, and writes the rewritten conversation
back to `messages` only if at least one message changed.
Results are written to the store this run resolves to: a per-run store passed in `state`'s `hook_context`
under `RESULT_STORE_CONTEXT_KEY` if present, otherwise the store the hook was constructed with. Supply the
per-run store when calling the Agent, e.g.
`agent.run(messages=[...], hook_context={RESULT_STORE_CONTEXT_KEY: per_request_store})`. In a multi-user
server, pass an isolated store per run this way so concurrent users write to separate locations and never
read each other's results.
The hook keeps no mutable state, so a single instance is safe to share across concurrent runs; isolation
comes entirely from giving each run its own store via `hook_context`.
**Parameters:**
- **state** (<code>State</code>) The Agent's live `State`. Reads the per-run store from `hook_context` and rewrites the offloaded
tool-result messages back into `messages`.
**Returns:**
- <code>None</code> None. The hook mutates `state` in place.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the hook, including its store and per-tool offload strategies.
**Returns:**
- <code>dict\[str, Any\]</code> A dictionary representation of the hook.
#### from_dict
```python
from_dict(data: dict[str, Any]) -> ToolResultOffloadHook
```
Deserialize the hook, reconstructing its store and offload strategies.
**Parameters:**
- **data** (<code>dict\[str, Any\]</code>) A dictionary representation produced by `to_dict`.
**Returns:**
- <code>ToolResultOffloadHook</code> The deserialized `ToolResultOffloadHook`.
## tool_result_offloading/policies
### AlwaysOffload
Bases: <code>OffloadPolicy</code>
Offload every result of the tool it is assigned to.
#### should_offload
```python
should_offload(tool_name: str, result: str, state: State) -> bool
```
Decide whether to offload the given tool result.
**Parameters:**
- **tool_name** (<code>str</code>) The name of the tool that produced the result (unused; this policy always offloads).
- **result** (<code>str</code>) The tool result string (unused; this policy always offloads).
- **state** (<code>State</code>) The Agent's live `State` (unused; this policy always offloads).
**Returns:**
- <code>bool</code> Always True.
### NeverOffload
Bases: <code>OffloadPolicy</code>
Never offload; keep the tool's full result in context. Use to opt a tool out of a wildcard default.
#### should_offload
```python
should_offload(tool_name: str, result: str, state: State) -> bool
```
Decide whether to offload the given tool result.
**Parameters:**
- **tool_name** (<code>str</code>) The name of the tool that produced the result (unused; this policy never offloads).
- **result** (<code>str</code>) The tool result string (unused; this policy never offloads).
- **state** (<code>State</code>) The Agent's live `State` (unused; this policy never offloads).
**Returns:**
- <code>bool</code> Always False.
### OffloadOverChars
Bases: <code>OffloadPolicy</code>
Offload a result only when its string length exceeds `threshold` characters.
#### __init__
```python
__init__(threshold: int) -> None
```
Initialize the policy with its character threshold.
**Parameters:**
- **threshold** (<code>int</code>) Offload the result when its length in characters is strictly greater than this value.
#### should_offload
```python
should_offload(tool_name: str, result: str, state: State) -> bool
```
Decide whether to offload the given tool result based on its length.
**Parameters:**
- **tool_name** (<code>str</code>) The name of the tool that produced the result (unused; only length is considered).
- **result** (<code>str</code>) The tool result string whose length is compared against the threshold.
- **state** (<code>State</code>) The Agent's live `State` (unused; only length is considered).
**Returns:**
- <code>bool</code> True when `result` is longer than `threshold` characters, otherwise False.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the policy, including its threshold.
**Returns:**
- <code>dict\[str, Any\]</code> A dictionary representation of the policy.
## tool_result_offloading/stores
### FileSystemToolResultStore
Bases: <code>ToolResultStore</code>
A `ToolResultStore` that writes offloaded tool results to files under a root directory on the local file system.
```python
from haystack.hooks.tool_result_offloading import FileSystemToolResultStore
store = FileSystemToolResultStore(root="tool_results")
reference = store.write(key="search_1.txt", content="...")
store.read(reference)
```
#### __init__
```python
__init__(root: str | Path) -> None
```
Initialize the store with the root directory results are written under.
**Parameters:**
- **root** (<code>str | Path</code>) Directory under which result files are written. Created on first write if it does not exist.
#### write
```python
write(*, key: str, content: str) -> str
```
Write `content` to `<root>/<key>`, creating parent directories, and return the file path.
The resolved target must stay within the root directory: a `key` that escapes it (e.g. containing `../` or an
absolute path) is rejected, so a tool-provided key cannot write outside the store.
**Parameters:**
- **key** (<code>str</code>) Relative file name for the result within the store root.
- **content** (<code>str</code>) The tool result to persist.
**Returns:**
- <code>str</code> The absolute path the content was written to, as a string, for use with `read`.
**Raises:**
- <code>ValueError</code> If `key` resolves to a location outside the store root.
#### read
```python
read(reference: str) -> str
```
Read back the content previously written to `reference`.
**Parameters:**
- **reference** (<code>str</code>) A path returned by `write`.
**Returns:**
- <code>str</code> The stored content.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the store, storing its root directory as a string.
**Returns:**
- <code>dict\[str, Any\]</code> A dictionary representation of the store.
#### from_dict
```python
from_dict(data: dict[str, Any]) -> FileSystemToolResultStore
```
Deserialize the store from a dictionary.
**Parameters:**
- **data** (<code>dict\[str, Any\]</code>) A dictionary representation produced by `to_dict`.
**Returns:**
- <code>FileSystemToolResultStore</code> The deserialized `FileSystemToolResultStore`.
## tool_result_offloading/types/protocol
### ToolResultStore
Bases: <code>Protocol</code>
A place a `ToolResultOffloadHook` writes offloaded tool results to, and reads them back from.
Implementations decide where and how the content lives (local disk, an isolated sandbox filesystem, object
storage, ...). `write` returns an opaque reference string that the Agent puts in the conversation in place of the
full result; `read` resolves that reference back to the original content.
Implement both `to_dict` and `from_dict` to make a custom store serializable; the default implementations below
cover stores whose constructor takes no arguments.
#### write
```python
write(*, key: str, content: str) -> str
```
Persist `content` under `key` and return an opaque reference to it.
**Parameters:**
- **key** (<code>str</code>) A stable, per-result identifier the hook derives from the tool call (e.g. a file name).
- **content** (<code>str</code>) The tool result to persist.
**Returns:**
- <code>str</code> A reference string (e.g. a path or URI) that `read` can later resolve.
#### read
```python
read(reference: str) -> str
```
Return the content previously stored under `reference`.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the store to a dictionary.
#### from_dict
```python
from_dict(data: dict[str, Any]) -> ToolResultStore
```
Deserialize the store from a dictionary.
### OffloadPolicy
Bases: <code>Protocol</code>
Decides, per tool result, whether the `ToolResultOffloadHook` offloads it to the store or leaves it in context.
A `ToolResultOffloadHook` maps tool names to policies, so different tools can offload under different conditions
(always, never, or a custom rule such as a size threshold).
Implement both `to_dict` and `from_dict` to make a custom policy serializable; the default implementations below
cover policies whose constructor takes no arguments.
#### should_offload
```python
should_offload(tool_name: str, result: str, state: State) -> bool
```
Return whether the given tool result should be offloaded.
**Parameters:**
- **tool_name** (<code>str</code>) The name of the tool that produced the result.
- **result** (<code>str</code>) The tool result as a string (the content that would otherwise stay in the conversation).
- **state** (<code>State</code>) The Agent's live `State`, for policies that decide based on run context.
**Returns:**
- <code>bool</code> True to offload the result to the store, False to leave it in context.
#### to_dict
```python
to_dict() -> dict[str, Any]
```
Serialize the policy to a dictionary.
#### from_dict
```python
from_dict(data: dict[str, Any]) -> OffloadPolicy
```
Deserialize the policy from a dictionary.