Skip to main content
Set "stream": true on any execution endpoint and the body is text/event-stream instead of JSON. Nothing else about the request changes. One contract covers all three — /responses, /conversations/{id}/messages and /agents/{id}/runs — so an event means the same thing wherever it arrives. A client that can read a stream from one can read a stream from the others without a second code path, because there is only one. Provider-specific event shapes are never exposed. What you read is normalised.

The event vocabulary

Contract version 2026-09-07, sent on response.started as stream_version so you can pin without a separate discovery call. It is a date rather than an integer, so “which contract is this client on” and “when did it change” are the same question. cancelled is its own terminal event rather than a flavour of success or failure. Folding it into either would misreport a run that did exactly what it was told.

Exactly one terminal event, ever

response.completed, response.failed and response.cancelled are terminal. Exactly one is emitted per stream. A stream that ends without one is a bug on our side, not a state for you to handle. This is structural rather than a rule someone follows. The object the execution writes into has no method that can emit a terminal event — progress is the whole of its API — and the terminal event is produced once, by the single consumer, after the worker has finished.

Deltas are advisory; the completion event is authoritative

Do not assemble your final answer by concatenating deltas. Read it off response.completed.
The internal queue is bounded and drops fragments rather than blocking when a client stops reading, because a worker blocked behind a slow consumer is a wedged thread holding a database connection. What you lose in that case is a fragment you would have rendered. What you keep is response.completed, which carries the whole output. Render deltas for the feel of it. Trust the completion event for the content.

Comments are not events

A line beginning : is an SSE comment, sent about every 15 seconds so that proxies and load balancers do not close an idle connection.
Skip them. Do not parse them. They are not in the contract and carry no data.

Failures before the first event are not streamed

No credential, no credits, an unpublished agent, a schema this runtime cannot enforce: each is the ordinary JSON error with its own status code, because nothing has been written to the wire yet and a failed execution must not answer 200.
Once response.started is on the wire the response is already a 200, and a later failure arrives as response.failed carrying the same error payload inside it. So a streaming client needs both paths:
1

Check the response status and content type first

A non-2xx, or a Content-Type that is not text/event-stream, is an ordinary JSON error. Handle it exactly as you handle a non-streaming one.
2

Then read events until a terminal one

Skip : comments. Render deltas if you want them. Stop at response.completed, response.failed or response.cancelled, and take your result from that event.

Disconnecting

A client that vanishes mid-stream does not leave a run burning tokens forever, and is not billed differently. The execution is told to stop and settles once, like any other run: it still closes its run row, still settles its usage, and notices the cancellation at its next checkpoint. The worker is never killed — a design that killed it on disconnect would be the one that orphans spend, because the half-finished execution would never write its ledger line. Usage is metered exactly once per execution, in the execution itself. Streaming adds no settlement path at all. A disconnect does not settle, a cancel does not settle, and a timeout does not settle — the one execution that already owns the ledger write does it, exactly as it does for a non-streaming request.

Reconnecting

There is no resume. Events carry an id: that numbers them within one stream, but a reconnect starts a new stream and Last-Event-ID is not honoured. If you lost the connection and need the outcome, poll the run instead:
That needs logs:read. The run row is the authoritative record of what happened, whatever the stream managed to deliver.

Timeouts

The stream stops waiting shortly after your own timeout_ms — the caller’s deadline plus about ten seconds of slack — and then gives the execution a few seconds to notice. If it does not, the stream ends with response.failed rather than reporting a completion nobody has. The run row still settles. A daemon thread that outlives its stream still closes its own row, and that row stays the authoritative record.

Reading it

-N on curl, and no buffering proxy in front of your client. A reverse proxy that buffers the response body will hold every event until the run finishes, which looks exactly like streaming being broken.