Skip to main content
Every run writes a journal: numbered events, durable, in order. One endpoint serves it both ways — live as Server-Sent Events, or as JSON pages you can walk at your own pace — and the events are identical either way.
The response is text/event-stream; charset=utf-8 with Cache-Control: no-cache, and each frame carries three lines:
  • id — the event’s cursor. It is what you send back to resume.
  • event — the event type, so a client can dispatch without parsing the body.
  • data — the whole event object, the same JSON the paged mode returns.
Every 15 seconds of silence the server writes a comment frame, :hb, to keep the connection from being reaped by something in the middle. Ignore it; SSE clients already do.

Or read it as pages

Ask for JSON — or send no Accept at all — and the same journal comes back a page at a time:
limit runs from 1 to 500 and defaults to 100. next_cursor is the last row’s cursor even when has_more is false, so a client that has caught up can keep the cursor and resume later instead of starting over. Asking for something neither mode serves is a 406:

The event types

sequence counts from 1 and is monotonic per run, so it also tells you whether you missed anything. hook.called is the one worth watching while you build: it names the file that answered each point of the loop and what it decided.
A whole short run, in order, looks like this — 16 events for one question with one tool call in it:

Reconnecting

Delivery is at-least-once, so a client that reconnects may see an event it already has. Deduplicate by event id and you can reconnect as often as you like. To resume, hand back the last cursor you saw — either as the standard SSE header or as a query parameter:
Browser EventSource sends that header for you. The stream continues strictly after the named event. Cursors are opaque and bound to the endpoint and run that issued them. Three ways to get it wrong, three distinct answers:

How a stream ends

The stream closes after the run.status_changed event whose to is succeeded, failed or cancelled. That event is the last frame you will see:
The server drains anything that landed in the moment around that transition before it hangs up, but the journal is the authority and the stream is not: if you need to be certain you have everything, do one paged read from your last cursor after the stream closes. It is cheap, and it is the difference between usually complete and complete.

In the SDKs

The reconnect, the deduplication and the terminal close are already written:
All three reconnect with Last-Event-ID, drop duplicates by event id, and end the iteration after the terminal event — leaving you the same “do a final paged read” choice the raw protocol does.