PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/How to Stream Harness Output to a UI
AI Harness

How to Stream Harness Output to a UI

Users bail on silent spinners fast. An agent harness streaming ui streams structured events — tool calls, results, tokens — so a long run shows its work as it happens.

September 9, 2026·8 min read
ShareShare
⚡Featured Prompt— copy and use right now
@app.post("/run")
def run_endpoint(req):
    result = agent.run(req.prompt)     # blocks for 5-90 seconds
    return {"answer": result.text}     # user sees nothing until this returns

Users abandon a task at a rate that climbs sharply after about ten seconds of staring at a spinner with no feedback — and an agent run that calls three tools and thinks in between routinely takes far longer than that. That single fact is why an agent harness streaming ui isn't a nice-to-have polish layer; it's often the difference between a tool people trust and one they close before it finishes. This teardown starts with the way most harnesses first wire up their UI — wait for everything, then dump it — shows exactly why that fails users, and rebuilds it to stream the run as it happens.

The mistake underneath the bad version is treating an agent run like a normal API call: send request, wait, get response. But an agent run is a process, sometimes a long one, and a process that shows nothing until it's done feels broken even when it's working perfectly.

Before: The Weak Prompt

Here's the pattern almost everyone ships first — run the whole agent, return the final answer.

hljs python
[object Object],
,[object Object], ,[object Object],(,[object Object],):
    result = agent.run(req.prompt)     ,[object Object],
    ,[object Object], {,[object Object],: result.text}     ,[object Object],

What this does: Runs the entire agent loop server-side and returns only when it's completely finished, then hands the UI a single final blob. For a one-step run this is fine. For a run that calls tools, reasons, and calls more tools, the user sits watching a loading indicator for a minute with zero signal that anything is happening — or whether it's hung.

Why It Fails

The obvious problem is perceived performance: the run isn't slow, but it feels slow because nothing visible happens until the end. Perceived latency is the latency users actually judge you on, and a silent minute reads as a failure even when the work is proceeding fine.

The subtler problem is trust and debuggability. When the agent finally returns, the user has no idea what it did to get there — did it search? which tools? did something error and get retried? A run that shows its work as it goes lets the user follow the reasoning, catch a wrong turn early, and trust the result because they watched it form. A run that only shows the answer asks the user to trust a black box — and users are right to be wary of black boxes that took a full minute to produce something.

And there's a real failure mode hiding in the block: if the connection drops at second 45 of a 60-second run, the user gets nothing and the work is lost. There was no partial result to salvage because nothing was ever sent — the entire run existed only in the server's memory, invisible and unrecoverable, until the moment it chose to reveal everything at once.

⚠️ Common mistake: Streaming only the final model tokens and calling it "streaming." Token streaming makes the answer appear gradually, which helps, but it does nothing for the 40 seconds of tool calls before the answer starts. A real agent harness streaming ui streams events — every tool call, every result, every state change — not just the closing paragraph.

After: The Improved Prompt

The rebuilt version streams a structured event for everything the agent does, over Server-Sent Events, so the UI can render a live timeline of the run.

hljs python
[object Object],
,[object Object], ,[object Object],(,[object Object],):
    ,[object Object], ,[object Object],():
        ,[object Object], event ,[object Object], agent.run_streaming(req.prompt):
            ,[object Object], ,[object Object],
            ,[object Object], ,[object Object],
            ,[object Object], ,[object Object],
    ,[object Object], Response(event_stream(), mimetype=,[object Object],)

What this does: Iterates the agent as a stream of typed events and pushes each one to the browser immediately as an SSE message, tagged with a sequence ID and an event type. The moment the agent decides to call a tool, the UI knows. The moment a result comes back, the UI knows. The user watches the run unfold instead of watching a spinner.

The agent loop yields events at every meaningful point rather than returning once at the end.

hljs python
[object Object], ,[object Object],(,[object Object],):
    state = RunState(prompt)
    ,[object Object], Event(,[object Object],, seq=state.,[object Object],(), payload={})
    ,[object Object], ,[object Object], state.done:
        ,[object Object], chunk ,[object Object], ,[object Object],.model.stream(state.messages):
            ,[object Object], Event(,[object Object],, seq=state.,[object Object],(), payload={,[object Object],: chunk})
        ,[object Object], call ,[object Object], state.pending_tool_calls:
            ,[object Object], Event(,[object Object],, seq=state.,[object Object],(),
                        payload={,[object Object],: call.name, ,[object Object],: call.args})
            result = ,[object Object],.dispatch(call)
            ,[object Object], Event(,[object Object],, seq=state.,[object Object],(),
                        payload={,[object Object],: call.name, ,[object Object],: result.ok})
    ,[object Object], Event(,[object Object],, seq=state.,[object Object],(), payload={,[object Object],: state.answer})

What this does: Emits a distinct event when the run starts, for each token as the model streams, when each tool starts and finishes, and when the run completes. The UI gets a running narrative — "searching docs… found 3 results… writing answer…" — built from real events, not a fabricated progress bar. Every event carries a sequence number, which matters for the reconnection trick below.

Breaking Down Each Element

Each event type maps to something the user cares about seeing.

token
events drive the familiar typewriter effect on the answer.
tool_started
and
tool_finished
are what let you show "🔍 Searching documentation…" and then check it off — this is the part plain token streaming misses entirely, and it's often the most reassuring thing on screen during a long run.
run_started
and
run_finished
bracket the whole thing so the UI knows when to show and hide its live state.

The

seq
field on every event is the quiet hero. It's a monotonic counter that lets the client detect gaps and, crucially, resume after a dropped connection.

hljs javascript
[object Object], es = ,[object Object], ,[object Object],(,[object Object],);
es.,[object Object], = ,[object Object], { lastSeq = e.,[object Object],; ,[object Object],(e); };

What this does: The browser's

EventSource
automatically sends the last event ID it saw when it reconnects, and the server can resume the stream from just after that sequence number. A blip in the user's wifi no longer loses the run — the stream picks up where it left off, because every event was numbered and the server can replay from the gap.

⚡ Pro tip: Buffer recent events server-side keyed by sequence number so you can actually honor a reconnect. The client promising to send

last-event-id
is worthless if the server threw those events away. A short ring buffer of the last few hundred events per run is cheap and turns "stream" into "resumable stream."

Variations for Different Contexts

SSE is the right default for agent runs because they're one-directional — the server talks, the client mostly listens — and SSE reconnects automatically and rides over plain HTTP. Reach for it first.

Use WebSockets when the user needs to interject mid-run — approving an action, answering a clarifying question, canceling. That two-way channel is worth the extra complexity only when interaction during the run is a real requirement, not just streaming output.

For a CLI or a backend consumer rather than a browser, stream newline-delimited JSON (

ndjson
) over the response body. Same event model, no SSE framing — each line is one event the consumer parses as it arrives. It's the streaming format that composes cleanly with pipes and log processors.

⚠️ Common mistake: Sending the model's raw internal reasoning tokens straight to the UI without a filter. Streaming everything can leak scratch work, tool arguments containing sensitive values, or half-formed statements the model later corrects. Decide which event types are user-facing and stream only those; keep the rest for your logs.

⚡ Pro tip: Send a heartbeat event every few seconds during long tool calls, even when nothing changed. Some proxies and load balancers silently close a connection that goes quiet, and a periodic no-op event keeps the pipe warm — and doubles as a signal to the UI that the run is alive and working, not stuck.

Turning the Stream Into Observability

The same event stream that drives your UI is, almost for free, a live view for your operators — and building an agent harness streaming ui that also feeds monitoring means you get debugging visibility without a second pipeline. The events a user sees as "🔍 searching…" are the same events an on-call engineer can watch to spot a run stuck retrying the same tool five times.

The trick is to emit richer events on an operator channel than on the user channel. Users see friendly, filtered progress; operators see timing, token counts, retry attempts, and error details on every step. Both come from the same underlying event generator — you're just choosing which fields each audience gets.

hljs python
[object Object], ,[object Object],(,[object Object],):
    ,[object Object], audience == ,[object Object],:
        event.payload |= {,[object Object],: ,[object Object],.step_latency,
                          ,[object Object],: ,[object Object],.step_tokens,
                          ,[object Object],: ,[object Object],.attempt}
    ,[object Object],.channels[audience].send(event)

What this does: Enriches events destined for the operator channel with timing, token, and retry metadata while keeping the user channel clean. One event source, two views — the user's stays reassuring and readable, the operator's becomes a real-time diagnostic feed. When a run misbehaves, you're already watching it happen instead of reconstructing it from logs after the fact.

⚡ Pro tip: Emit a

step_summary
event at the end of each step with its duration and token cost, and graph those live. A run whose per-step latency is climbing or whose token cost is spiking is a run heading for trouble, and the streaming events you already built are the cheapest early-warning system you'll ever add.

Save and Reuse This

The event model — the set of event types, the sequence numbering, the reconnection contract — is the part worth getting right once and reusing everywhere, because every agent you build will want to stream, and inconsistent event schemas across services make shared UI components impossible. A

tool_started
event that means one thing in one service and something different in another is how a UI component that worked yesterday breaks when you point it at a new agent. Standardize the event vocabulary once and every future agent plugs into the same rendering layer.

Keep your streaming event definitions and the SSE wiring versioned alongside your prompts and tool schemas in a library like PromptABCD, so every new agent streams the same event types your UI already knows how to render, and "add a live view" becomes wiring, not a rebuild.

ai-harnessstreamingsseuieventsuser-experience

Continue Reading

Managing Prompt Templates Across a Harness Codebase
AI Harness

Managing Prompt Templates Across a Harness Codebase

Four divergent copies of one prompt caused a two-day bug. Harness prompt templates management makes prompts versioned, tested, single-source artifacts instead of scattered strings.

September 10, 2026·8 min read
How to Open-Source Your Agent Harness
AI Harness

How to Open-Source Your Agent Harness

An agent harness isn't an ordinary library — it's security-sensitive infra tangled with your secrets. Release an open source agent harness without leaking a key or shipping unusable code.

September 10, 2026·8 min read
Error Taxonomy: Classifying Harness Failures
AI Harness

Error Taxonomy: Classifying Harness Failures

When every failure looks the same, you can't retry, route, or alert correctly. Agent harness error classification gives failures types that drive real behavior.

September 10, 2026·8 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousValidating Tool Arguments Before ExecutionNext →Building a Harness for Long-Running Agents
Share this post:
ShareShare