Skip to content

Streaming

Set "stream": true to receive Server-Sent Events instead of one buffered object. Stream a turn when you want to show progress, or when the agent runs enough tools to make a long wait uncomfortable.

const stream = await client.responses.create({
model: "agent",
input: "Where is order 4821?",
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") {
process.stdout.write(event.delta);
}
}
stream = client.responses.create(
model="agent",
input="Where is order 4821?",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="")

The x-thread-id header arrives with the stream headers, before the first event.

Each frame carries the event name on the event: line and a JSON object on the data: line. The object repeats the name as type, and it carries a sequence_number.

event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_9f2c...","output_index":2,"content_index":0,"delta":"Order 4821 "}

Sequence numbers start at 0 and count up by one, with no gaps.

The stream has no [DONE] frame. It ends after response.completed or response.failed.

A turn where the agent thinks, calls one tool, and then answers produces this order:

response.created
response.in_progress
response.output_item.added reasoning
response.reasoning_summary_text.delta
response.output_item.added function_call
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done the tool finished
response.output_item.added message
response.content_part.added
response.output_text.delta
response.output_text.delta
response.reasoning_summary_text.done
response.output_item.done reasoning
response.output_text.done
response.content_part.done
response.output_item.done message
response.completed

Items open in the order they occur, and each one keeps its output_index for the whole stream.

Two points differ from what you may expect:

Reasoning closes at the end of the turn, not before the answer. Providers do not agree on where reasoning sits. Gemini writes a first text chunk before its first reasoning chunk. Urai stores one reasoning summary for a turn, so the reasoning item stays open and collects text until the turn ends.

The output array of response.completed is the truth. It is built from the stored rows, and its order is the order of the work. The SDKs replace the items they collected with it, so you end with the correct answer either way.

Event Meaning
response.created The turn started. Carries the response object with status: "in_progress".
response.in_progress The same object again, as OpenAI sends it.
response.output_item.added An item opened. item holds its id and type.
response.output_item.done An item closed. item restates it in full.
response.reasoning_summary_text.delta More reasoning text.
response.reasoning_summary_text.done The whole reasoning summary, as text.
response.function_call_arguments.delta The arguments of a tool call.
response.function_call_arguments.done The same arguments, complete.
response.content_part.added The text part of the message opened.
response.output_text.delta More answer text, as delta.
response.output_text.done The whole answer, as text.
response.content_part.done The text part closed.
response.completed The turn finished. Carries the final response object.
response.failed The turn failed. Carries the response object with status: "failed" and an error.

response.completed and response.failed are terminal, and a stream sends one of them, never both.

A tool call appears as a function_call item.

event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":4,"output_index":1,"item":{"id":"fc_5b3e...","type":"function_call","call_id":"call_1","name":"orders_lookup","arguments":"","status":"in_progress"}}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","sequence_number":5,"item_id":"fc_5b3e...","output_index":1,"delta":"{\"order\":\"4821\"}"}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","sequence_number":6,"item_id":"fc_5b3e...","output_index":1,"arguments":"{\"order\":\"4821\"}"}

The arguments arrive in one delta and not as a trickle of tokens. Urai records the call after the model finishes writing it, so the whole string is ready at that moment.

The closing response.output_item.done reports the result:

  • "status": "completed" when the tool succeeded.
  • "status": "incomplete" when the tool returned an error. The agent reads that error and keeps working, so the turn continues.

The value the tool returned is not on the wire. The agent runs tools on the server, and it tells you what it found in its answer.

Nothing else about the run reaches the stream. Progress notes, tool summaries, and widget commands are for the chat interface, and they have no Responses equivalent.

A turn that fails sends response.failed and closes.

event: response.failed
data: {"type":"response.failed","sequence_number":17,"response":{"id":"resp_9f2c...","status":"failed","error":{"code":"agent_error","message":"the tool sandbox stopped"}, ...}}

The response object in that event holds whatever the turn produced before it failed, so you can show the partial answer.

The non-streaming path reports the same failure. The HTTP status stays 200, and the body carries "status": "failed" with the error object.

A non-streaming request that runs past 10 minutes fails with the message the agent did not finish in time. A streamed request has no such limit.