Skip to content

feat: add on_tool_call_sealed hook for eager tool dispatch#3558

Open
Oxygen56 wants to merge 2 commits into
openai:mainfrom
Oxygen56:feat/sealed-tool-call-event
Open

feat: add on_tool_call_sealed hook for eager tool dispatch#3558
Oxygen56 wants to merge 2 commits into
openai:mainfrom
Oxygen56:feat/sealed-tool-call-event

Conversation

@Oxygen56
Copy link
Copy Markdown

@Oxygen56 Oxygen56 commented Jun 1, 2026

Summary

Adds a new lifecycle hook on_tool_call_sealed to both RunHooksBase and AgentHooksBase that fires during streaming when a tool call's arguments are sealed and ready, before the full response is complete and before the tool is invoked.

This provides a hook point for consumers to implement early tool preparation or eager dispatch without changing the SDK's execution flow. The default runner does not act on this hook — callers that do nothing with it see unchanged behavior.

Refs #3404

Changes

  1. src/agents/lifecycle.py: Added on_tool_call_sealed method to RunHooksBase and AgentHooksBase
  2. src/agents/run_internal/run_loop.py: Wired the hook into run_single_turn_streamed at the point where ToolCallItem is created from a ResponseOutputItemDoneEvent
  3. tests/test_run_hooks.py: Added test_streamed_run_hooks_tool_call_sealed test that verifies the hook fires with the correct tool name and call_id during streaming

Test plan

  • make format — passed
  • make lint — passed
  • make tests — all 44 relevant tests pass
  • New test test_streamed_run_hooks_tool_call_sealed passes

Closes #3404

Add a new lifecycle hook  to both RunHooksBase and
AgentHooksBase that fires during streaming when a tool call's arguments
are sealed and ready, before the full response is complete and before
the tool is invoked.

This provides a hook point for consumers to implement early tool
preparation or eager dispatch without changing the SDK's execution flow.
The default runner does not act on this hook - callers that do nothing
with it see unchanged behavior.

The hook is wired into run_single_turn_streamed at the point where
ToolCallItem is created from a ResponseOutputItemDoneEvent.

Refs: openai#3404

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8f480a434

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/lifecycle.py

This hook fires when the LLM has finished producing a tool call during streaming, before
the full response is complete and before the tool is invoked. It carries the sealed
``ToolCallItem`` with ``call_id``, ``tool_name``, and ``arguments``.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose sealed arguments on the hook item

When a consumer implements this new hook and follows the documented contract, tool_call.arguments raises AttributeError: ToolCallItem currently only exposes call_id and tool_name, with arguments reachable only through the raw OpenAI item. Since the hook is specifically for eager dispatch after arguments are sealed, this makes the advertised API fail for normal streamed function-call hooks unless callers know to use tool_call.raw_item.arguments instead.

Useful? React with 👍 / 👎.

@bmd1905
Copy link
Copy Markdown

bmd1905 commented Jun 2, 2026

Thanks for picking this up, this is the read-only seam I wanted in #3404. Going through ResponseOutputItemDoneEvent is the right call, it avoids the index-to-id bookkeeping you'd need to infer completion from a changing call_id. And the runner stays hands-off: it surfaces the sealed call and commits nothing, so ignoring the hook changes no behavior. That's the early-preparation vs early-commitment split @scosemicolon described.

This is enough to build real eager dispatch on top. My reference impl (cloudthinker-ai/eager-tools) takes a sealed (call_id, tool_name, arguments) and runs the tool early in a pool, with an idempotency gate so destructive calls fall back to the normal turn boundary. A hook like this is exactly where it would attach.

One thing to confirm: when several tool calls stream in one turn, does it fire once per call as each seals? The test covers the single-call case. A two-tool case asserting it fires twice with the right call_id would nail down what downstream dispatch depends on.

@Oxygen56
Copy link
Copy Markdown
Author

Oxygen56 commented Jun 2, 2026

@bmd1905 Good catch — I've added test_streamed_run_hooks_tool_call_sealed_multiple that verifies the hook fires twice with correct call_id values when two tool calls seal in the same turn. Also changed the tracking from a single last_sealed_tool_call to a sealed_tool_calls list so the test can assert on both calls.

The implementation already fires per-call via the ResponseOutputItemDoneEvent loop in run_loop.py, so no runtime change was needed — just the test coverage.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hook point for eager tool dispatch (overlap tool execution with model streaming)

2 participants