agent·interface

Streaming progress: what to show while an agent works

Why a spinner isn't enough anymore

Short-lived requests can get away with a loading indicator. Agents can't. A task that runs for two minutes and shows nothing but a spinner reads as broken by the halfway mark, whether or not it actually is. Claude Code's VS Code extension ran into exactly this: when the agent invokes a bash command, the extension buffers the entire output and only renders it after the command finishes, so a multi-minute build or test run shows nothing but a static spinner the whole way through. A GitHub issue against the project lays out the consequence plainly — users can't tell a slow command from a stuck one, can't catch an early failure before the full run completes, and lose confidence that anything is happening at all. The fixes proposed in that thread (always stream, an opt-in setting, or a time-based threshold before buffering kicks in) are really three different answers to one question every agent interface has to settle: what do you show while the model is working, and what do you hold back?

Two layers of streaming, not one

"Streaming" gets used for two different things that are worth separating. The first is token-level streaming — text appearing as the model generates it, the pattern chat UIs inherited from the earliest LLM APIs. The second is what LangChain's engineering team calls intermediate-step streaming in their UX-for-agents writeup: showing which tool the agent decided to call, what arguments it passed, and what came back, distinct from the token stream of its reasoning or final answer. Their framing is that streaming chat became the default agent UX specifically because it exposes both layers at once — "you can see when it decides to use a tool, what tool it uses, what input it gives, and what results it gets back" — which is a different transparency claim than just "the words appear gradually."

The Claude Agent SDK treats this as a setting rather than a given. By default it only yields a complete message once a turn finishes; flipping includePartialMessages on (include_partial_messages in Python) switches it to emitting granular StreamEvents as text and tool-call arguments are generated, which the SDK's streaming docs describe as the mechanism that "powers chat UIs and progress indicators." That's a deliberate off-by-default choice: full granularity is available, but a builder has to opt into the noise it can create before deciding how much of it belongs in front of a user.

What earns a line on screen

Not everything a streaming API can emit deserves to reach the interface. A rough split, based on what the products above actually chose to surface:

Signal:

  • Which tool or action is about to run, and what it targets — the thing a human would want to interrupt before, not after
  • The result of that action, especially a failure, before the agent moves on
  • State transitions: waiting on input, blocked, done, errored
  • For a single long-running step, some throughput signal — lines written, tests passed — rather than silence

Noise:

  • Every token of scratch reasoning, replayed live with no way to act on it mid-sentence
  • The same plan restated after each step
  • Retry attempts that don't change the decision being made

The dividing line isn't "is this technically available in the stream" — it almost always is, at the token level, if the underlying API exposes it. It's whether seeing it earlier would change what the person watching does next. A tool call about to touch a production database changes what they do next: they might stop it. A partial sentence of internal reasoning usually doesn't.

Same company, two different answers

What makes this worth writing about rather than obvious is that even one team can land in different places on the same question. Claude Code's terminal CLI has supported granular streaming for a while — --output-format stream-json paired with --verbose and --include-partial-messages emits newline-delimited events a caller can parse into a live status line. The VS Code extension, built on the same underlying agent, buffers bash output until completion instead. Same product family, same model, two different defaults, because a terminal and an IDE panel put different constraints on how much motion a user wants to see.

OpenAI's ChatGPT agent takes a third approach, leaning toward summarized status over a raw event feed. Its launch post describes a workflow where, instead of parsing a stream yourself, you can interrupt mid-task to redirect it, ask for a progress summary if a run feels stuck, or stop it outright and get back whatever partial result exists. That's progress-as-a-service rather than progress-as-a-feed: the agent is still doing the same intermediate steps under the hood, but the interface's job is answering "where are we" on demand rather than narrating continuously.

The protocol layer is catching up

This is also why AG-UI exists as a protocol rather than each framework inventing its own event shape. It separates lifecycle events (a run or step starting and finishing) from text-delta events, tool-call events, and state-sync events specifically so a consuming UI can filter by category instead of parsing a single undifferentiated firehose — a status bar can subscribe to lifecycle and tool events only, while a full chat pane also takes the text deltas. Standardizing the categories doesn't settle which ones a given product should render; that's still a product decision, as the Claude Code CLI-versus-extension split shows. But it means the decision can be "which event types do we subscribe to" instead of "how do we parse whatever this particular SDK happens to emit."

A rule of thumb

Stream what would change the person's next move — whether to keep watching, interrupt, or walk away. Suppress what wouldn't, no matter how satisfying the scroll looks. A progress view that shows everything is barely better than the spinner it replaced; the point was never motion, it was giving someone enough information to decide whether to keep trusting the run.

The site's tracker keeps a current read on where this pattern and the protocols underneath it (AG-UI in particular) stand.


Tracking this space daily on the agent-interface tracker. Start at the hub if you're new to the term.