"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 version2026-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
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 isresponse.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.
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.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 anid: 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:
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 owntimeout_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.
