Streaming an Agent's Progress Without Lying About It
Token streaming goes quiet exactly when an agent gets slow — inside the tool call. The fix is an event stream from the loop, and the hard part is not inventing certainty the loop doesn't have.
Token streaming solved the wrong problem for agents. It makes a chat response feel instant; it does nothing for the forty seconds an agent spends inside a tool call, which is where agent latency actually lives. The result is an interface that streams beautifully, then freezes at the moment the user most needs to know something is happening.
Filling that silence is a UI problem with an honesty constraint, and most attempts fail the constraint.
What users are actually asking during the pause
Not "what are you thinking." Three concrete things:
- Is it still working, or is it wedged?
- How much longer, roughly?
- Is it doing something I'd want to stop?
Question three is the one that gets ignored, and it's the one with consequences. A user who can see send_email about to happen can intervene. A user watching a spinner cannot.
An event stream that answers those
Emit structured events from the loop, not prose from the model:
{type: "step_started", n: 3, of: 6, tool: "fetch_page", detail: "docs.example.com/pricing"}
{type: "step_finished", n: 3, ok: true, ms: 2400}
{type: "plan_revised", from: 6, to: 8, why: "two pages redirected"}
{type: "awaiting_approval", tool: "send_email", args_summary: "to 4 recipients"}
Rendering these gives the user a live picture with no invention: which step, how many, what tool, how long the last one took, and what's about to happen that they might care about. The loop already knows all of it — this is a plumbing exercise, not a modelling one.
The honesty problems, in the order teams hit them
Never show a plan as a commitment. An agent's first-turn plan is a guess that will change. Rendering "Step 3 of 6" implies six is known; it isn't. Either label it visibly as an estimate, or emit plan_revised events so the count changes in front of the user rather than silently. A progress bar that jumps backwards is honest and users handle it fine. One that sits at 80% for two minutes destroys trust in every future estimate.
Don't stream the model's internal deliberation as if it were status. Deliberation text is exploratory — it names approaches the agent then abandons. Users read it as intent, so "I'll delete the old records first" reads as an announcement even when the agent immediately decides against it. ⚠️ If you surface reasoning at all, mark it distinctly from actions, and never let it be the only signal that a tool ran.
Show tool identity, not a euphemism. "Analyzing your data" covering a run_sql call is a small lie that becomes a large one during an incident. Name the tool and summarize the arguments. Users who don't care will ignore it; the ones who care are the ones you need.
Make the terminal state explicit. ✅ Every run ends with a finished or failed event carrying what completed and what didn't. A stream that just stops leaves the user unable to distinguish success, crash, and timeout — and they will assume whichever is most convenient.
For long-running and background agents
Once a run outlives the browser tab, the stream isn't enough on its own. Two additions carry most of the value:
- A durable run record the user can return to: the same event sequence, persisted, readable after the fact. Live progress and the audit log are the same data; building them separately is duplicated effort and they drift apart.
- Push only at decision points, not on progress. Notify when the run needs approval, finishes, or fails. A notification per step trains people to ignore all of them, including the one that mattered.
A detail worth stealing
Include the elapsed milliseconds on step_finished and render them. It costs nothing and it converts "this feels slow" into "step 4 took 31 seconds" — which is actionable for the user and the only latency telemetry some teams ever get around to collecting. The same events that make the UI honest make the system observable.
The takeaway
Agent progress UI fails by inventing certainty the loop doesn't have — fixed step counts, reassuring labels, deliberation dressed up as intent. Emit what the loop actually knows: which step, which tool, how long it took, what needs approval, and how it ended. Users tolerate uncertainty that's shown to them honestly. What they don't forgive is a confident progress bar that turns out to have been decorative.