Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-website/docs/pipeline-components/agents-1/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ print(response["tool_call_counts"]) # {"calculator": 1}
- `streaming_callback`: A callback invoked for each streamed token. Use the built-in `print_streaming_chunk` for console output.
- `max_agent_steps`: Maximum number of LLM + tool call iterations before the agent stops. Defaults to `100`.
- `raise_on_tool_invocation_failure`: If `True`, raises an exception when a tool call fails. If `False` (default), the error is passed back to the LLM as a message so it can recover.
- `hooks`: A dict mapping a hook point (`"before_llm"`, `"before_tool"`, `"after_tool"`, `"on_exit"`) to a list of hooks the agent runs at that point. Hooks receive the live `State` and influence the run by mutating it — for example, to build run-time context or require human confirmation of tool calls. See [Hooks](./hooks.mdx) and [Human in the Loop](./human-in-the-loop.mdx).
- `hooks`: A dict mapping a hook point (`"before_run"`, `"before_llm"`, `"before_tool"`, `"after_tool"`, `"on_exit"`, `"after_run"`) to a list of hooks the agent runs at that point. Hooks receive the live `State` and influence the run by mutating it — for example, to build run-time context or require human confirmation of tool calls. See [Hooks](./hooks.mdx) and [Human in the Loop](./human-in-the-loop.mdx).
- `tool_concurrency_limit`: Maximum number of tool calls to execute at the same time. Defaults to `4`; set to `1` to disable parallel tool execution.
- `tool_streaming_callback_passthrough`: If `True`, passes the streaming callback to tools that accept it.

Expand Down
8 changes: 5 additions & 3 deletions docs-website/docs/pipeline-components/agents-1/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
title: "Hooks"
id: hooks
slug: "/hooks"
description: "Hooks let you run custom logic at defined points of an Agent's run loop — before each LLM call, before and after tool execution, and on exit."
description: "Hooks let you run custom logic at defined points of an Agent's run loop — at the start and end of a run, before each LLM call, before and after tool execution, and on exit."
---

# Hooks

Hooks let you run custom logic at defined points of an [`Agent`](./agent.mdx)'s run loop — before each LLM call, before and after tool execution, and on exit.
Hooks let you run custom logic at defined points of an [`Agent`](./agent.mdx)'s run loop — at the start and end of a run, before each LLM call, before and after tool execution, and on exit.

<div className="key-value-table">

Expand All @@ -30,10 +30,12 @@ This enables patterns such as building run-time system context, retrieving memor

### Hook points

- `before_run`: Runs once per run, after the state is initialized and before the first chat-generator call. Use it to rewrite the initial messages or seed state — for example, to turn the user query into a task brief — without re-running on every step like `before_llm` does.
- `before_llm`: Runs before each chat-generator call.
- `before_tool`: Runs after the model requests tool calls, before any tools run. After these hooks run, the Agent re-reads the current last message from `state.data["messages"]`. If that message contains tool calls, those calls are executed. If it does not, no tools run for that step, no tool-based exit condition is triggered, and the Agent loops back to the next LLM call unless `max_agent_steps` has been reached.
- `after_tool`: Runs after tools execute, once their result messages are in `state.data["messages"]`, before the exit-condition check and the next LLM call. Use it to rewrite the freshly produced tool-result messages — for example, to offload, redact, truncate, or summarize results. It does not run on the plain-text exit step. It does still run when a `before_tool` hook removed the pending tool calls: no tools executed on that step, so don't assume the last message is a fresh tool result.
- `on_exit`: Runs when the Agent is about to stop on an exit condition. An `on_exit` hook can keep the Agent running by setting the `continue_run` control flag (`state.set("continue_run", True)`), usually alongside a message telling the model what to do next. `on_exit` hooks run when the Agent stops on an exit condition, but not when it stops because `max_agent_steps` is reached.
- `on_exit`: Runs when the Agent is about to stop on an exit condition. An `on_exit` hook can keep the Agent running by setting the `continue_run` control flag (`state.set("continue_run", True)`), usually alongside a message telling the model what to do next. `on_exit` hooks run when the Agent stops on an exit condition, but not when it stops because `max_agent_steps` is reached — use `after_run` for logic that must run however the run ends.
- `after_run`: Runs once per run, after the step loop has ended and before the Agent builds its return value — regardless of whether the run stopped on an exit condition or because `max_agent_steps` was reached (unlike `on_exit`). Mutations to the state, such as appending a final message, are reflected in the returned `messages` / `last_message` and `state_schema` outputs. Setting `continue_run` here has no effect.

Registering a hook under an unknown hook point raises a `ValueError` at construction. A hook class can declare an `allowed_hook_points` attribute listing the hook points it supports; the Agent validates it and fails fast if the hook is registered somewhere it doesn't belong.

Expand Down
24 changes: 23 additions & 1 deletion haystack/components/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@
from haystack.core.serialization import component_to_dict, default_from_dict, default_to_dict
from haystack.dataclasses import ChatMessage, ChatRole, StreamingCallbackT, select_streaming_callback
from haystack.hooks.invocation import _run_hooks, _run_hooks_async
from haystack.hooks.protocol import AFTER_TOOL, BEFORE_LLM, BEFORE_TOOL, ON_EXIT, VALID_HOOK_POINTS, Hook, HookPoint
from haystack.hooks.protocol import (
AFTER_RUN,
AFTER_TOOL,
BEFORE_LLM,
BEFORE_RUN,
BEFORE_TOOL,
ON_EXIT,
VALID_HOOK_POINTS,
Hook,
HookPoint,
)
from haystack.hooks.utils import (
_deserialize_hooks_dictionary,
_serialize_hooks_dictionary,
Expand Down Expand Up @@ -579,6 +589,9 @@ def __init__( # noqa: PLR0913
:param hooks: A dictionary mapping a hook point to a list of hooks the Agent runs at that point. Each hook
receives the live `State` and influences the run by mutating it in place; hooks for a hook point run in
list order. Valid hook points are:
- "before_run": Runs once per run, after the state is initialized and before the first chat-generator
call. Use it to rewrite the initial messages or seed state (e.g. turn the user query into a task
brief) without re-running on every step like "before_llm" does.
- "before_llm": Runs before each chat-generator call.
- "before_tool": Runs after the model requests tool calls, before any tools run. After these hooks run,
the Agent re-reads the current last message from `state.data["messages"]`. If that message contains tool
Expand All @@ -592,6 +605,11 @@ def __init__( # noqa: PLR0913
Agent running by setting the `continue_run` control flag (`state.set("continue_run", True)`), usually
alongside a message telling the model what to do next. "on_exit" hooks run when the Agent stops on an
exit condition, but not when it stops because `max_agent_steps` is reached.
- "after_run": Runs once per run, after the step loop has ended and before the Agent builds its return
value — regardless of whether the run stopped on an exit condition or because `max_agent_steps` was
reached (unlike "on_exit"). Mutations to the state (e.g. appending a final message) are reflected in
the returned `messages` / `last_message` and `state_schema` outputs. Setting `continue_run` here has
no effect.
:raises TypeError: If the chat_generator does not support tools parameter in its run method.
:raises ValueError: If any `user_prompt` variable overlaps with the `state_schema` or `run` method parameters,
if a hook is registered under an unknown hook point, or if a hook is registered under a hook point it does
Expand Down Expand Up @@ -1030,6 +1048,7 @@ def run(

with self._create_agent_span(exe_context.tools) as span:
span.set_content_tag("haystack.agent.input", agent_inputs)
_run_hooks(self.hooks, BEFORE_RUN, exe_context.state)
while exe_context.counter < self.max_agent_steps:
if not self._run_step(exe_context, span):
break
Expand All @@ -1038,6 +1057,7 @@ def run(
"Agent reached maximum agent steps of {max_agent_steps}, stopping.",
max_agent_steps=self.max_agent_steps,
)
_run_hooks(self.hooks, AFTER_RUN, exe_context.state)
result = _public_outputs(exe_context.state)
if msgs := result.get("messages"):
result["last_message"] = msgs[-1]
Expand Down Expand Up @@ -1102,6 +1122,7 @@ async def run_async(

with self._create_agent_span(exe_context.tools) as span:
span.set_content_tag("haystack.agent.input", agent_inputs)
await _run_hooks_async(self.hooks, BEFORE_RUN, exe_context.state)
while exe_context.counter < self.max_agent_steps:
if not await self._run_step_async(exe_context, span):
break
Expand All @@ -1110,6 +1131,7 @@ async def run_async(
"Agent reached maximum agent steps of {max_agent_steps}, stopping.",
max_agent_steps=self.max_agent_steps,
)
await _run_hooks_async(self.hooks, AFTER_RUN, exe_context.state)
result = _public_outputs(exe_context.state)
if msgs := result.get("messages"):
result["last_message"] = msgs[-1]
Expand Down
14 changes: 13 additions & 1 deletion haystack/hooks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@
from lazy_imports import LazyImporter

_import_structure = {
"protocol": ["Hook", "HookPoint", "BEFORE_LLM", "BEFORE_TOOL", "AFTER_TOOL", "ON_EXIT", "VALID_HOOK_POINTS"],
"protocol": [
"Hook",
"HookPoint",
"BEFORE_RUN",
"BEFORE_LLM",
"BEFORE_TOOL",
"AFTER_TOOL",
"ON_EXIT",
"AFTER_RUN",
"VALID_HOOK_POINTS",
],
"from_function": ["FunctionHook", "hook"],
}

if TYPE_CHECKING:
from .from_function import FunctionHook as FunctionHook
from .from_function import hook as hook
from .protocol import AFTER_RUN as AFTER_RUN
from .protocol import AFTER_TOOL as AFTER_TOOL
from .protocol import BEFORE_LLM as BEFORE_LLM
from .protocol import BEFORE_RUN as BEFORE_RUN
from .protocol import BEFORE_TOOL as BEFORE_TOOL
from .protocol import ON_EXIT as ON_EXIT
from .protocol import VALID_HOOK_POINTS as VALID_HOOK_POINTS
Expand Down
4 changes: 3 additions & 1 deletion haystack/hooks/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from haystack.components.agents.state.state import State

# Points in the Agent's run loop at which hooks can be registered.
HookPoint = Literal["before_llm", "before_tool", "after_tool", "on_exit"]
HookPoint = Literal["before_run", "before_llm", "before_tool", "after_tool", "on_exit", "after_run"]

BEFORE_RUN: HookPoint = "before_run"
BEFORE_LLM: HookPoint = "before_llm"
BEFORE_TOOL: HookPoint = "before_tool"
AFTER_TOOL: HookPoint = "after_tool"
ON_EXIT: HookPoint = "on_exit"
AFTER_RUN: HookPoint = "after_run"
VALID_HOOK_POINTS: tuple[HookPoint, ...] = get_args(HookPoint)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
features:
- |
Added ``before_run`` and ``after_run`` hook points to the ``Agent``. Hooks registered under ``before_run`` run
exactly once per run, after the state is initialized and before the first LLM call — use them to rewrite the
initial messages or seed state without re-running on every step like ``before_llm`` does. Hooks registered under
``after_run`` run exactly once per run, after the step loop has ended and before the Agent builds its return
value, regardless of whether the run stopped on an exit condition or because ``max_agent_steps`` was reached
(unlike ``on_exit``); mutations to the state are reflected in the returned ``messages``, ``last_message``, and
``state_schema`` outputs. Register them like any other hook:
``hooks={"before_run": [my_hook], "after_run": [my_other_hook]}``.
Loading
Loading