Skip to content

feat(lifecycle): pass ToolContext to on_tool_start/end for all tool types#3403

Closed
adityasingh2400 wants to merge 1 commit into
openai:mainfrom
adityasingh2400:feat-tool-context-lifecycle
Closed

feat(lifecycle): pass ToolContext to on_tool_start/end for all tool types#3403
adityasingh2400 wants to merge 1 commit into
openai:mainfrom
adityasingh2400:feat-tool-context-lifecycle

Conversation

@adityasingh2400
Copy link
Copy Markdown
Contributor

Summary

Users who track timing metrics inside on_tool_start and on_tool_end cannot
correlate the start and end of the same tool call when calls execute in
parallel, because the RunContextWrapper they receive has no tool_call_id.
Function tools already pass a ToolContext (which exposes tool_call_id,
tool_name, and tool_arguments), but ComputerTool, LocalShellTool,
ShellTool, and ApplyPatchTool all passed a plain RunContextWrapper, so
hooks could not rely on the field being present.

This PR constructs a ToolContext via ToolContext.from_agent_context(...)
at the four remaining call sites in run_internal/tool_actions.py. Each site
pulls tool_call_id from the call's call_id field, sets tool_name from
the tool's .name, and serializes the structured action payload into
tool_arguments for visibility. The same tool_context is then passed to
both global RunHooks and per-agent AgentHooks for on_tool_start and
on_tool_end, mirroring the pattern already used for function tools and
custom tools.

The parameter annotation on RunHooksBase.on_tool_start,
RunHooksBase.on_tool_end, AgentHooksBase.on_tool_start, and
AgentHooksBase.on_tool_end is refined from RunContextWrapper[TContext] to
ToolContext[TContext]. Because ToolContext is a subclass of
RunContextWrapper, existing user subclasses that accept the wider type
continue to type-check; new hooks can rely on tool_call_id, tool_name,
and tool_arguments directly. New regression tests in
tests/test_computer_action.py, tests/test_local_shell_tool.py,
tests/test_shell_tool.py, and tests/test_apply_patch_tool.py capture
the hook context for each tool family and assert the tool_call_id matches
the actual call's call_id.

Closes #1849

Test plan

  • make format
  • make lint
  • make typecheck (no new errors introduced in touched files)
  • uv run pytest tests/test_run_hooks.py tests/test_agent_hooks.py tests/test_computer_action.py tests/test_local_shell_tool.py tests/test_apply_patch_tool.py tests/test_shell_tool.py tests/test_custom_tool.py tests/test_global_hooks.py tests/test_agent_llm_hooks.py tests/test_computer_tool_lifecycle.py tests/test_agent_runner.py tests/test_agent_runner_streamed.py tests/test_function_tool.py

…ypes

Computer, local shell, shell, and apply_patch tools previously passed a plain
RunContextWrapper to on_tool_start/on_tool_end, leaving callers unable to
distinguish parallel tool calls. Construct a ToolContext at each of those
call sites so hooks consistently receive tool_call_id, tool_name, and
tool_arguments. Refine the parameter annotation on RunHooksBase.on_tool_start,
RunHooksBase.on_tool_end, AgentHooksBase.on_tool_start, and
AgentHooksBase.on_tool_end from RunContextWrapper to ToolContext (a subclass)
so subclasses can rely on the tool-call-specific fields.

Closes openai#1849
@seratch
Copy link
Copy Markdown
Member

seratch commented May 14, 2026

I don't think this change could work due to #1849 (comment)

@adityasingh2400
Copy link
Copy Markdown
Contributor Author

@seratch — that comment was about why we couldn't just refine the annotation. This PR addresses ihower's concern directly by converting the four non-function-tool call sites (ComputerAction, LocalShellAction, ShellAction, ApplyPatchAction) to also construct a ToolContext via ToolContext.from_agent_context(...) before invoking the hooks, so every on_tool_start/on_tool_end path now receives the same type. The annotation refinement is only safe because of those call-site changes.

Specifically:

  • src/agents/run_internal/tool_actions.py builds a ToolContext at each of the four sites, populating tool_call_id from the action's call_id and serializing the structured action payload into tool_arguments.
  • Each regression test (tests/test_computer_action.py, tests/test_local_shell_tool.py, tests/test_shell_tool.py, tests/test_apply_patch_tool.py) subclasses RunHooks and asserts context.tool_call_id matches the upstream call_id for that tool family.
  • The annotation change is a subtype refinement (ToolContext is a RunContextWrapper subclass), so existing user hooks typed against the wider RunContextWrapper keep type-checking.

Happy to walk through any specific call site if there's a concrete path I missed, or split the call-site rewrites and the annotation refinement into two PRs if that's easier to review.

@seratch
Copy link
Copy Markdown
Member

seratch commented May 14, 2026

Thanks for clarifying it. Either way, this could be a breaking change, so we don't immediately plan to make this type of change.

@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Closing per @seratch's call — appreciate the consideration. If the team revisits this and decides a future major release can absorb the subtype refinement, happy to revive this work; in the meantime the docstring already steers users toward checking for ToolContext so the practical workaround is documented.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose Tool Call ID to Lifecycle Hooks

2 participants