feat: add ToolContext.send_progress() for streaming tool progress events#3397
feat: add ToolContext.send_progress() for streaming tool progress events#33970xSudoSSH wants to merge 1 commit into
Conversation
4be89f5 to
547c6c4
Compare
547c6c4 to
a87cd6d
Compare
|
Thanks for sharing this idea and it's an interesting approach. I am still unsure whether adding this event, along with a helper method on the context object, is the best way to support this use case. We'd like to take some time to consider others as well. |
Allow function tools to emit intermediate progress events during execution via ToolContext.send_progress(data). Events appear in RunResultStreaming.stream_events() as ToolProgressStreamEvent while the tool is still running. No-op in non-streaming mode.
a87cd6d to
72070af
Compare
@seratch Alternative A: Thread Pass the queue down: Alternative B: Extend hooks with Add a We chose the Happy to prototype any alternative if there's a direction you'd prefer |
Summary
Adds
ToolContext.send_progress(data)— a simple API for function tools to emit intermediate progress events during execution. Events appear inRunResultStreaming.stream_events()asToolProgressStreamEventwhile the tool is still running. In non-streaming mode (Runner.run()), calls are silently ignored.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 way for tools to emit mid-execution progress events improves developer experience and makes responsive UIs and long-running workflows (data processing, web scraping, multi-step API calls) much easier to build.New stream event type
A new
ToolProgressStreamEventis added to theStreamEventunion alongside the existingRawResponsesStreamEvent,RunItemStreamEvent, andAgentUpdatedStreamEvent. It carries:tool_name: str— identifies which tool emitted the eventtool_call_id: str— correlates with a specific tool call (important when parallel tools run)data: Any— arbitrary progress payload (dict, string, number, etc.)type: Literal["tool_progress_stream_event"]— discriminator for pattern matchingConsumers can filter for progress events via
isinstance(event, ToolProgressStreamEvent)orevent.type == "tool_progress_stream_event".Design
_StreamContextdataclass (holdingevent_queueandevent_loop) onRunContextWrapper— piggybacking on an object already threaded through the entire execution chain. Zero intermediate function signature changes.send_progress()method onToolContext— scoped to function tools, reads per-tool identity (tool_name,tool_call_id) from the instance. No shared mutable state.loop.call_soon_threadsafe()with a stored event loop reference so sync tools (sync_invoker=True) running in worker threads can safely callsend_progress(). The loop is captured at wiring time (on the event loop thread), not at call time.Runner.run_streamed()creates a newRunContextWrapperwith its own_stream_context. No cross-contamination between outer and inner runs.Usage — basic tool with progress
Usage — agent-as-tool with
on_streamhandlerWhen an agent is used as a tool via
as_tool(), inner progress events are delivered to theon_streamcallback:Usage — non-streaming mode (no-op)
Test plan
send_progresswith active stream context, without context (no-op), and with broken context (failure isolation)ToolProgressStreamEventfield validation anddata: Anyflexibility_stream_contextsurvives_fork_with_tool_input,_fork_without_tool_input, andToolContext.from_agent_contextstream_events()send_progressas no-optool_call_idattributiontool_outputevent for the same toolIssue number
Closes #1333
Checks
make lintandmake format