Skip to main content
Your users are watching an agent work. A spinner and the word “processing” is a poor showing — what they want is what it is reading, what it changed, what it ran, and what came out, with the ability to scroll back afterwards. This page builds that: a live feed, resumable across a dropped connection.

Before you start

A token and a model, as in the quickstart. The stream is plain Server-Sent Events over HTTP — every language has a client, and the one below is thirty lines of standard library.

The whole thing

Subscribe before you send. Subscribing after loses the opening frames whenever the agent starts quickly — a bug that never reproduces on a developer’s machine and always reproduces in production.

What arrives

The run above, counted frame by frame: Forty-seven of the seventy-five frames were text deltas, and not one of them was durable. That is the shape of every run: most of the traffic is animation, and all of the facts are in the rest. A given run shows a subset of the full vocabulary, depending on what the agent does and which model you are on.

Which frames you can build on

One rule: look for id:.
Note the payload keys differ: a delta carries payload.delta, the settled message carries payload.text. Rendering a delta by reading payload.text prints nothing at all, silently — which is a mistake this page made until it was run. A durable frame has an integer seq and an id: line. It is stored, it is replayed on reconnect, and it is a fact. A transient frame has seq: null and no id:. It is not stored and not replayed. Drive your state machine from durable frames and treat transient ones as animation. Printing agent.text deltas as they arrive is exactly right. Deciding “this step finished” from them is not: after a reconnect those deltas are gone, while the agent.message they settled into is replayed.

Reconnect without losing anything

Every durable event after that id, then live frames. The script above returns the cursor when it finishes — resume from: 23781 — so remembering one integer is the whole of crash recovery. No reconciliation pass, no diffing against /items. What you do not get back are the text deltas: the animation from a disconnected period is gone, and the agent.message it settled into is replayed in its place. Which is why the state machine runs on durable frames.

The full vocabulary

Twenty-six types, grouped by the question they answer on screen: Three worth singling out:
  • file.changed covers creation, modification and deletion; the payload says which. For a live diff view this is more useful than the assistant’s prose.
  • preview.ready means the agent started a service and a port answered. It is the signal that there is something to click.
  • agent.todos is the agent’s own checklist. Rendered as a task list, your users can see how many steps remain.

You have it working when

  • user.message is the first frame you receive, because you subscribed first
  • turn.started, agent.tool_call, agent.tool_result and turn.ended all arrive
  • Every frame either has an integer seq and an id:, or seq: null and neither — there is no third shape
  • After reconnecting mid-run, durable events are strictly increasing, all greater than your cursor, and agree with GET /items
  • No agent.text is replayed
  • Your interface never polls /turns

What to know

Terminal output is not on this stream. You see what a command returned through agent.tool_result, but not the output arriving line by line. Transient frames are never replayed. The animation from a disconnected period is gone. The facts are not. Payload shapes vary by type and are not individually described in the OpenAPI document, which covers the envelope — type, seq, created_at, payload. Branch on the type rather than expecting one parser to handle all of them. There is a ceiling on concurrent streams per organization — see limits.md. In a multi-tenant interface, hold one stream per user rather than one per component.

When it goes wrong