Skip to content

Commit bd871cd

Browse files
authored
docs: clarify ToolContext availability in function-tool lifecycle hooks (#2687)
1 parent 4892211 commit bd871cd

File tree

6 files changed

+72
-4
lines changed

6 files changed

+72
-4
lines changed

docs/agents.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ Typical hook timing:
262262
- `on_agent_start` / `on_agent_end`: when a specific agent begins or finishes producing a final output.
263263
- `on_llm_start` / `on_llm_end`: immediately around each model call.
264264
- `on_tool_start` / `on_tool_end`: around each local tool invocation.
265+
For function tools, the hook `context` is typically a `ToolContext`, so you can inspect tool-call metadata such as `tool_call_id`.
265266
- `on_handoff`: when control moves from one agent to another.
266267

267268
Use `RunHooks` when you want a single observer for the whole workflow, and `AgentHooks` when one agent needs custom side effects.

docs/context.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ This is represented via the [`RunContextWrapper`][agents.run_context.RunContextW
1313
2. You pass that object to the various run methods (e.g. `Runner.run(..., context=whatever)`).
1414
3. All your tool calls, lifecycle hooks etc will be passed a wrapper object, `RunContextWrapper[T]`, where `T` represents your context object type which you can access via `wrapper.context`.
1515

16+
For some runtime-specific callbacks, the SDK may pass a more specialized subclass of `RunContextWrapper[T]`. For example, function-tool lifecycle hooks typically receive `ToolContext`, which also exposes tool-call metadata like `tool_call_id`, `tool_name`, and `tool_arguments`.
17+
1618
The **most important** thing to be aware of: every agent, tool function, lifecycle etc for a given agent run must use the same _type_ of context.
1719

1820
You can use the context for things like:

src/agents/lifecycle.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ async def on_tool_start(
7373
agent: TAgent,
7474
tool: Tool,
7575
) -> None:
76-
"""Called immediately before a local tool is invoked."""
76+
"""Called immediately before a local tool is invoked.
77+
78+
For function-tool invocations, ``context`` is typically a ``ToolContext`` instance,
79+
which exposes tool-call-specific metadata such as ``tool_call_id``, ``tool_name``,
80+
and ``tool_arguments``. Other local tool families may provide a plain
81+
``RunContextWrapper`` instead.
82+
"""
7783
pass
7884

7985
async def on_tool_end(
@@ -83,7 +89,13 @@ async def on_tool_end(
8389
tool: Tool,
8490
result: str,
8591
) -> None:
86-
"""Called immediately after a local tool is invoked."""
92+
"""Called immediately after a local tool is invoked.
93+
94+
For function-tool invocations, ``context`` is typically a ``ToolContext`` instance,
95+
which exposes tool-call-specific metadata such as ``tool_call_id``, ``tool_name``,
96+
and ``tool_arguments``. Other local tool families may provide a plain
97+
``RunContextWrapper`` instead.
98+
"""
8799
pass
88100

89101

@@ -135,7 +147,13 @@ async def on_tool_start(
135147
agent: TAgent,
136148
tool: Tool,
137149
) -> None:
138-
"""Called immediately before a local tool is invoked."""
150+
"""Called immediately before a local tool is invoked.
151+
152+
For function-tool invocations, ``context`` is typically a ``ToolContext`` instance,
153+
which exposes tool-call-specific metadata such as ``tool_call_id``, ``tool_name``,
154+
and ``tool_arguments``. Other local tool families may provide a plain
155+
``RunContextWrapper`` instead.
156+
"""
139157
pass
140158

141159
async def on_tool_end(
@@ -145,7 +163,13 @@ async def on_tool_end(
145163
tool: Tool,
146164
result: str,
147165
) -> None:
148-
"""Called immediately after a local tool is invoked."""
166+
"""Called immediately after a local tool is invoked.
167+
168+
For function-tool invocations, ``context`` is typically a ``ToolContext`` instance,
169+
which exposes tool-call-specific metadata such as ``tool_call_id``, ``tool_name``,
170+
and ``tool_arguments``. Other local tool families may provide a plain
171+
``RunContextWrapper`` instead.
172+
"""
149173
pass
150174

151175
async def on_llm_start(

tests/test_agent_hooks.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from agents.run import Runner
1313
from agents.run_context import AgentHookContext, RunContextWrapper, TContext
1414
from agents.tool import Tool
15+
from agents.tool_context import ToolContext
1516

1617
from .fake_model import FakeModel
1718
from .test_responses import (
@@ -26,9 +27,11 @@
2627
class AgentHooksForTests(AgentHooks):
2728
def __init__(self):
2829
self.events: dict[str, int] = defaultdict(int)
30+
self.tool_context_ids: list[str] = []
2931

3032
def reset(self):
3133
self.events.clear()
34+
self.tool_context_ids.clear()
3235

3336
async def on_start(self, context: AgentHookContext[TContext], agent: Agent[TContext]) -> None:
3437
self.events["on_start"] += 1
@@ -56,6 +59,8 @@ async def on_tool_start(
5659
tool: Tool,
5760
) -> None:
5861
self.events["on_tool_start"] += 1
62+
if isinstance(context, ToolContext):
63+
self.tool_context_ids.append(context.tool_call_id)
5964

6065
async def on_tool_end(
6166
self,
@@ -65,6 +70,8 @@ async def on_tool_end(
6570
result: str,
6671
) -> None:
6772
self.events["on_tool_end"] += 1
73+
if isinstance(context, ToolContext):
74+
self.tool_context_ids.append(context.tool_call_id)
6875

6976

7077
@pytest.mark.asyncio
@@ -94,6 +101,17 @@ async def test_non_streamed_agent_hooks():
94101
assert hooks.events == {"on_start": 1, "on_end": 1}, f"{output}"
95102
hooks.reset()
96103

104+
model.add_multiple_turn_outputs(
105+
[
106+
[get_function_tool_call("some_function", json.dumps({"a": "b"}))],
107+
[get_text_message("done")],
108+
]
109+
)
110+
await Runner.run(agent_3, input="user_message")
111+
assert len(hooks.tool_context_ids) == 2
112+
assert len(set(hooks.tool_context_ids)) == 1
113+
hooks.reset()
114+
97115
model.add_multiple_turn_outputs(
98116
[
99117
# First turn: a tool call

tests/test_global_hooks.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing_extensions import TypedDict
99

1010
from agents import Agent, RunContextWrapper, RunHooks, Runner, TContext, Tool
11+
from agents.tool_context import ToolContext
1112

1213
from .fake_model import FakeModel
1314
from .test_responses import (
@@ -22,9 +23,11 @@
2223
class RunHooksForTests(RunHooks):
2324
def __init__(self):
2425
self.events: dict[str, int] = defaultdict(int)
26+
self.tool_context_ids: list[str] = []
2527

2628
def reset(self):
2729
self.events.clear()
30+
self.tool_context_ids.clear()
2831

2932
async def on_agent_start(
3033
self, context: RunContextWrapper[TContext], agent: Agent[TContext]
@@ -54,6 +57,8 @@ async def on_tool_start(
5457
tool: Tool,
5558
) -> None:
5659
self.events["on_tool_start"] += 1
60+
if isinstance(context, ToolContext):
61+
self.tool_context_ids.append(context.tool_call_id)
5762

5863
async def on_tool_end(
5964
self,
@@ -63,6 +68,8 @@ async def on_tool_end(
6368
result: str,
6469
) -> None:
6570
self.events["on_tool_end"] += 1
71+
if isinstance(context, ToolContext):
72+
self.tool_context_ids.append(context.tool_call_id)
6673

6774

6875
@pytest.mark.asyncio
@@ -85,6 +92,17 @@ async def test_non_streamed_agent_hooks():
8592
assert hooks.events == {"on_agent_start": 1, "on_agent_end": 1}, f"{output}"
8693
hooks.reset()
8794

95+
model.add_multiple_turn_outputs(
96+
[
97+
[get_function_tool_call("some_function", json.dumps({"a": "b"}))],
98+
[get_text_message("done")],
99+
]
100+
)
101+
await Runner.run(agent_3, input="user_message", hooks=hooks)
102+
assert len(hooks.tool_context_ids) == 2
103+
assert len(set(hooks.tool_context_ids)) == 1
104+
hooks.reset()
105+
88106
model.add_multiple_turn_outputs(
89107
[
90108
# First turn: a tool call

tests/test_run_hooks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from agents.run import Runner
1111
from agents.run_context import AgentHookContext, RunContextWrapper, TContext
1212
from agents.tool import Tool
13+
from agents.tool_context import ToolContext
1314
from tests.test_agent_llm_hooks import AgentHooksForTests
1415

1516
from .fake_model import FakeModel
@@ -22,9 +23,11 @@
2223
class RunHooksForTests(RunHooks):
2324
def __init__(self):
2425
self.events: dict[str, int] = defaultdict(int)
26+
self.tool_context_ids: list[str] = []
2527

2628
def reset(self):
2729
self.events.clear()
30+
self.tool_context_ids.clear()
2831

2932
async def on_agent_start(
3033
self, context: AgentHookContext[TContext], agent: Agent[TContext]
@@ -57,6 +60,8 @@ async def on_tool_end(
5760
result: str,
5861
) -> None:
5962
self.events["on_tool_end"] += 1
63+
if isinstance(context, ToolContext):
64+
self.tool_context_ids.append(context.tool_call_id)
6065

6166
async def on_llm_start(
6267
self,

0 commit comments

Comments
 (0)