Streaming an agent run via Runner.run_streamed() yields a RunResultStreaming object whose stream_events() async iterator emits StreamEvent objects — a union of raw LLM tokens, high-level run items (messages, tool calls, handoffs), and agent updates. The streaming surface supports pausing for tool approval, cancellation with graceful turn cleanup, and resuming from intermediate states via RunState conversion, making it possible to weave user input and approvals into a live agent execution. A RunState is a serializable snapshot of an in-progress agent run, capturing conversation history, pending tool results, and staged inputs; it allows the run to be persisted, transmitted, or resumed across process boundaries or after an interruption.
To stream an agent run, call Runner.run_streamed(), which returns a RunResultStreaming object; calling result.stream_events() on it yields an async stream of StreamEvent objects.[1] The StreamEvent type alias in src/agents/stream_events.py is the union RawResponsesStreamEvent | RunItemStreamEvent | AgentUpdatedStreamEvent, representing all possible streaming events from an agent.[2]
RawResponsesStreamEvent objects wrap raw LLM events from the OpenAI Responses API; each object's data field holds an event with a type such as response.created or response.output_text.delta, making them suitable for token-by-token streaming to the user.[1] RunItemStreamEvent provides higher-level streaming events that fire when a complete item is generated — a full message, tool result, or similar — in contrast to RawResponsesStreamEvent which fires per-token. AgentUpdatedStreamEvent fires when the current agent changes due to a handoff.[1] RunItemStreamEvent in src/agents/stream_events.py wraps a RunItem and uses the type discriminator "run_item_stream_event". Its name field uses a fixed set of literals: message_output_created, handoff_requested, handoff_occured, tool_called, tool_search_called, tool_search_output_created, tool_output, reasoning_item_created, mcp_approval_requested, mcp_approval_response, and mcp_list_tools. The spelling handoff_occured is intentionally preserved for backward compatibility and cannot be changed without a breaking change.[2][1] A handoff call is emitted only as handoff_requested, not additionally as tool_called; ordinary function tool calls in the same turn still emit tool_called.[1] When hosted tool search is used, tool_search_called is emitted when the model issues a tool-search request and tool_search_output_created is emitted when the Responses API returns the loaded subset.[1]
With Programmatic Tool Calling, tool_called is emitted for the generated program and for ordinary program-owned child tool calls; tool_output is emitted for child tool outputs and the program_output. Program-owned hosted MCP mcp_approval_request and mcp_list_tools items are exceptions: they emit as mcp_approval_requested and mcp_list_tools, wrapping MCPApprovalRequestItem and MCPListToolsItem respectively.[1] Program-owned child tool calls carry a caller field whose type is program and whose caller_id identifies the parent program; inspecting the raw item's type is needed to distinguish remaining item kinds.[1] Computer-tool raw streaming events differ between preview (computer_call with a single action) and gpt-5.5+ GA (computer_call with batched actions[]). The higher-level RunItemStreamEvent surface does not distinguish these shapes — both surface as tool_called, with screenshot results as tool_output wrapping a computer_call_output item.[1]
A streaming run is not complete until the result.stream_events() async iterator finishes; post-processing such as session persistence, approval bookkeeping, and history compaction may continue after the last visible token. When the loop exits, result.is_complete reflects the final run state.[1] To stop a streaming run immediately, call result.cancel(); to let the current turn finish cleanly before stopping, call result.cancel(mode="after_turn").[1] If cancel(mode="after_turn") stops a run after a tool turn and the caller is manually continuing from result.to_input_list(mode="normalized"), they should rerun result.last_agent with that normalized input to continue the unfinished turn rather than appending a fresh user turn.[1] When new user input arrives before a cancelled run resumes, the caller should convert the drained result with result.to_state(), call state.add_input(...), and resume from the state; the runner admits the staged input immediately before the next model call.[1]
When a streaming run pauses for tool approval, result.stream_events() finishes and pending approvals are exposed in RunResultStreaming.interruptions. The caller should convert the result to a RunState via result.to_state(), approve or reject each interruption, and resume with Runner.run_streamed(...).[1] A streamed run that stopped for tool approval must not be treated as a new turn; the caller should finish draining the stream, inspect result.interruptions, and resume from result.to_state() — not start a fresh user turn.[1]
Example: streaming token-by-token output from an agent by filtering for raw_response_event events whose data is a ResponseTextDeltaEvent.[1] Example: consuming RunItemStreamEvent to print agent updates, tool calls, tool outputs, and message outputs while ignoring raw token events.[1] Example: handling streaming with tool approval interruptions — drain the stream, check result.interruptions, convert to RunState, approve, and re-run.[1]
Sources