Skip to content

feat: expose tool_call_id on RunContextWrapper for lifecycle hooks#2915

Closed
nileshpatil6 wants to merge 1 commit intoopenai:mainfrom
nileshpatil6:feat/expose-tool-call-id-in-hooks
Closed

feat: expose tool_call_id on RunContextWrapper for lifecycle hooks#2915
nileshpatil6 wants to merge 1 commit intoopenai:mainfrom
nileshpatil6:feat/expose-tool-call-id-in-hooks

Conversation

@nileshpatil6
Copy link
Copy Markdown
Contributor

Summary

Closes #1849.

on_tool_start and on_tool_end hooks receive a RunContextWrapper as their context parameter. When the invocation is a function-tool call, the runtime actually passes a ToolContext (a subclass) which already carries tool_call_id. However, users who want to correlate parallel tool calls in observability/metrics code were forced to write:

async def on_tool_start(self, context, agent, tool):
    if isinstance(context, ToolContext):          # painful isinstance guard
        call_id = context.tool_call_id

This PR adds tool_call_id: str | None as an init=False field (default None) directly on RunContextWrapper, so the pattern becomes:

async def on_tool_start(self, context, agent, tool):
    call_id = context.tool_call_id  # str for function tools, None otherwise

Changes

  • src/agents/run_context.py — adds tool_call_id: str | None = field(default=None, init=False) to RunContextWrapper; propagates the value through _fork_with_tool_input and _fork_without_tool_input.
  • tests/test_run_hooks.py — adds two tests:
    • test_tool_call_id_exposed_on_run_context_wrapper — verifies the correct call_id is surfaced in both on_tool_start and on_tool_end without any isinstance check.
    • test_tool_call_id_is_none_outside_tool_context — verifies the field is None on a plain RunContextWrapper.

Compatibility

  • ToolContext redeclares tool_call_id as a required init=True field (str, not str | None), so subclass behaviour and construction are unchanged.
  • The new base field uses init=False to avoid clashing with ToolContext.from_agent_context, which copies base-class init fields by name.
  • All existing tests pass. The two sandbox/Unix-only test files (tests/sandbox/, tests/test_run_state.py, tests/test_sandbox_memory.py) fail due to the missing fcntl module on Windows — this is pre-existing and unrelated to this change.

Test plan

  • uv run pytest tests/test_run_hooks.py tests/test_function_tool.py tests/test_tool_context.py — 68 passed
  • uv run pyright src/agents/run_context.py src/agents/tool_context.py — 0 errors

Add a `tool_call_id: str | None` field (init=False, default None) to
`RunContextWrapper` so that callers of `on_tool_start` / `on_tool_end`
hooks can read the tool call ID directly from the context parameter
without needing an `isinstance(context, ToolContext)` guard.

`ToolContext` already declares `tool_call_id` as a required init field
(str, not str | None), so subclass behaviour is unchanged.  The base
field is `init=False` to avoid clashing with `ToolContext.from_agent_context`
which copies base-class init fields by name.

Both `_fork_with_tool_input` and `_fork_without_tool_input` are updated
to propagate the value when forking.

Closes openai#1849
@github-actions github-actions bot added enhancement New feature or request feature:core labels Apr 16, 2026
@seratch
Copy link
Copy Markdown
Member

seratch commented Apr 17, 2026

We're aware of this pain point, but we still don't want to add the property to the parent level. We will consider changing the type hints when releasing a major version with migration guide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature:core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose Tool Call ID to Lifecycle Hooks

2 participants