Two streams: tokens and events

Lesson 2 of 5 in Sessions and Streaming: The Conversation Plumbing.

“Streaming” is two different features wearing one word, and agent builders who only implement one of them ship a UI that feels broken.

Token streaming delivers the assistant’s text in pieces as the model generates it. Each frame is a delta — a fragment to append to what you already have. It buys exactly one thing, and it is a big one: perceived latency. First words on screen in 400 ms instead of a blank box for 40 seconds.

Event streaming delivers structured facts about the run: the run moved to in_progress, step 3 started, the model is assembling a call to search_orders and here are its arguments so far, that tool returned, the run needs an approval, the run reached a terminal status. Each frame is a state change, not a text fragment.

A chatbot can live on token streaming alone. An agent cannot — because most of the wall-clock time in an agent run is not text generation. It is tool execution, retrieval, waiting for an approval. A token stream is silent through all of it.

Token stream

What it carries: deltas of assistant text (and, on most providers, deltas of tool-call arguments as they are generated).

What the UI does with it: appends. The rendering rule is buffer += delta.

Properties you must respect:

  • Frames are order-dependent and not idempotent. Replay a text delta and the word appears twice.
  • A delta is not valid JSON on its own. Tool-call arguments arrive as {"cus, tomer_i, d": "A-9 — you cannot parse until the call is complete, so never act on a half-built argument.
  • The stream ending is not the same as the work succeeding. Always confirm terminal status from the event layer.

Event stream

What it carries: run status transitions, step created/completed, tool call started, tool output received, approval required, usage totals, terminal status, errors.

What the UI does with it: renders activity — “Searching orders…”, “Waiting for your approval”, a step timeline, a spinner that means something.

Properties you must respect:

  • Events should be individually meaningful and ideally idempotent — “run 7f3 is now in_progress” can be applied twice with no harm. Design them that way deliberately.
  • Events are what your traces and evals consume later. The same event log that drives the UI is your debugging record.
  • Best-effort delivery is normal. Assume some events are missed and reconcile against authoritative state (lesson 3).

What a real agent UI subscribes to

Both, multiplexed over one connection, with the UI keeping two separate state machines.

A production agent surface renders roughly four things at once: the transcript (from committed messages), the in-flight answer (from token deltas), the activity line (from run/step events), and the interrupt (from an approval-required event that has to block the UI until answered).

The failure mode of implementing only tokens: 30 seconds of dead air while a tool runs, users hammering the retry button, and no way to show an approval prompt.

The failure mode of implementing only events: technically correct, no perceived-latency win, and answers that materialise in one lump — which users read as slower even when it is not.

Interactive sorting exercise: You are wiring an agent UI. Sort each wire frame by which stream it belongs to — and remember that one of these is not a stream frame at all.

Now the part that bites in production: backpressure. A stream is a producer and a consumer, and the model does not care how fast your consumer is.

The mechanism is simple. Tokens arrive faster than your per-frame work completes. Frames queue in memory — in the HTTP client buffer, in your framework’s async queue, in the browser. Latency grows, memory grows, and eventually something sheds: a proxy times out, a queue is dropped, or your process is killed. The classic version of this bug is doing real work per token — a database write, a React re-render of a 5,000-node tree, a JSON re-parse of the whole buffer — at 80 frames per second.

Rule 1 · Do nothing expensive per frame

Append to a buffer and flush on a timer — every 50–100 ms is invisible to humans and cuts your work by an order of magnitude. Persist on events, not on tokens: one write when a step completes beats 400 writes while it runs.

Rule 2 · Bound every buffer, and decide what to drop

An unbounded queue is a memory leak with a delay fuse. Bound it, and make the shedding policy explicit: coalesce text deltas (three fragments concatenated are still correct), but never drop events — dropping “requires_action” means an approval prompt that never appears. Different frames, different policies.

Rule 3 · Fan-out is a multiplier

One agent run shown in a browser, a mobile app, and a Slack thread is three consumers at three speeds. Do not let the slowest one throttle the run: terminate the stream into your own durable event log, then let each client read from that at its own pace. This is the same move that makes reconnection possible.

Rule 4 · The network in between is not on your side

Reverse proxies buffer, CDNs coalesce, corporate middleboxes close idle connections, and mobile radios drop sockets on a bus ride. Send keepalives, expect disconnects as routine rather than exceptional, and never treat “the stream ended” as “the run finished”.

Tool: Trace Debugger — Event streams and traces are the same data with different consumers. The Trace Debugger walks you through a real agent run frame by frame — including the runs that ended badly.

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.