Skip to content

feat: add on_tool_progress hook for mid-execution tool progress updates#3406

Open
0xSudoSSH wants to merge 1 commit into
openai:mainfrom
0xSudoSSH:feat/tool-progress-lifecycle
Open

feat: add on_tool_progress hook for mid-execution tool progress updates#3406
0xSudoSSH wants to merge 1 commit into
openai:mainfrom
0xSudoSSH:feat/tool-progress-lifecycle

Conversation

@0xSudoSSH
Copy link
Copy Markdown
Contributor

@0xSudoSSH 0xSudoSSH commented May 14, 2026

Summary

Adds on_tool_progress to RunHooks and AgentHooks — a lifecycle hook that fires when a tool emits intermediate progress updates via await ctx.send_progress(data).

This solves the core problem from #1333 (streaming events from long-running tools) by extending the existing on_tool_start/on_tool_end lifecycle pattern, giving tools an official way to report mid-execution progress without requiring external shared state or event buses.

Motivation (re: #1333)

Existing lifecycle hooks (on_tool_start/on_tool_end) fire at the boundaries of tool execution, but they don't cover cases where a tool needs to emit multiple intermediate updates from inside the tool body. Without framework support, developers resort to external shared state or event buses, which add complexity and couple tool logic to infrastructure concerns. Providing an official lifecycle hook for mid-execution progress makes responsive UIs and long-running workflows (data processing, web scraping, multi-step API calls) much easier to build.

How it works

Tool authors call await ctx.send_progress(data) at any point during execution. The framework fires on_tool_progress(context, agent, tool, data) on both run-level and agent-level hooks concurrently (via asyncio.gather), following the same dispatch pattern as on_tool_start/on_tool_end.

from agents import Agent, Runner, RunHooks, function_tool
from agents.tool_context import ToolContext

class ProgressHooks(RunHooks):
    async def on_tool_progress(self, ctx, agent, tool, data):
        print(f"[{tool.name}] {data}")

@function_tool
async def analyze_data(ctx: ToolContext, query: str) -> str:
    await ctx.send_progress({"status": "fetching", "progress": 0.25})
    # ... work ...
    await ctx.send_progress({"status": "processing", "progress": 0.75})
    # ... more work ...
    return "analysis complete"

agent = Agent(name="Analyst", tools=[analyze_data])

# Works in both streaming and non-streaming mode
result = await Runner.run(agent, "Analyze Q4", hooks=ProgressHooks())

Per-agent hooks

class AnalystHooks(AgentHooks):
    async def on_tool_progress(self, ctx, agent, tool, data):
        await websocket.send({"tool": tool.name, "progress": data})

agent = Agent(name="Analyst", tools=[analyze_data], hooks=AnalystHooks())

Design

  • on_tool_progress added to both RunHooksBase and AgentHooksBase as async no-op methods.
  • ToolContext.send_progress(data) is an async method that delegates to a _progress_fn callback. It is a no-op by default.
  • The framework wires _progress_fn in _run_single_tool via a closure that captures hooks, agent_hooks, agent, and tool — all already in scope. This closure calls asyncio.gather on both hook levels, matching on_tool_start/on_tool_end exactly.
  • A fresh ToolContext is created per tool call, so there is no stale closure risk.
  • Works in both streaming (Runner.run_streamed) and non-streaming (Runner.run) modes.
  • No new stream event types — StreamEvent union is unchanged.
  • No changes to RunContextWrapper, RunState, or run.py.

Test plan

  • Unit: send_progress fires callback when set, no-op when unset, multiple events arrive in order
  • Integration: RunHooks.on_tool_progress fires during Runner.run()
  • Integration: AgentHooks.on_tool_progress fires during Runner.run()
  • Integration: both RunHooks and AgentHooks fire concurrently
  • Integration: hooks fire during Runner.run_streamed()
  • Integration: parallel tools report progress with correct tool identity
  • Integration: multiple progress events arrive in emission order
  • 9 tests total, all passing

Issue number

Closes #1333

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run make lint and make format
  • I've made sure tests pass

@0xSudoSSH
Copy link
Copy Markdown
Contributor Author

0xSudoSSH commented May 14, 2026

@seratch

This is an alternative approach to PR3397, which added a ToolProgressStreamEvent and send_progress() helper on ToolContext. The feedback on that PR was that adding a new stream event type along with a helper method on the context object may not be the best way to support this use case. This PR takes a different direction — using the existing lifecycle hooks pattern instead, with no new event types and no changes to the StreamEvent union.

@0xSudoSSH 0xSudoSSH force-pushed the feat/tool-progress-lifecycle branch from d12edb4 to 3c9a736 Compare May 14, 2026 06:30
@0xSudoSSH 0xSudoSSH force-pushed the feat/tool-progress-lifecycle branch from 3c9a736 to deb8593 Compare May 14, 2026 06:38
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.

Support for Streaming Events from Long-Running Tools

2 participants