Skip to content

feat: expose tool_call_id on RunContextWrapper for lifecycle hooks#2915

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

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

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
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

1 participant