The Problem
The engineering works perfectly. The agent takes 8 steps over 45 seconds to complete a research task. The user sees: a blank screen with a spinner.
They wait 10 seconds. 20 seconds. They refresh the page. The agent starts over from step 1. The 30 seconds of completed work is lost.
Or worse: the agent finishes and emails 500 contacts with a draft the user hasn't reviewed. The action was irreversible. There was no confirmation step.
These are not frontend problems. They're engineering problems. The agent doesn't emit progress events — that's an architect's decision. The agent doesn't pause before irreversible actions — that's a control flow decision. UX is baked into how the agent is designed.
This lesson covers the three patterns that make agents usable in production: streaming progress events, human-readable action logs, and confirmation gates before irreversible actions.
The trust contract
Users accept agent autonomy when they have three things: visibility (I can see what it's doing), understanding (I can read what it did), and control (I can stop it before it does something I don't want). Streaming progress provides visibility. Action logs provide understanding. Confirmation gates provide control. Remove any one of these and user trust collapses.
Streaming agent progress
Don't make users watch a spinner for 45 seconds. Stream progress events.
Event structure:
@dataclass
class ProgressEvent:
type: EventType # step_start | step_complete | step_failed | agent_complete
step: str # human-readable step description
step_num: int # current step number
total_steps: int # total expected steps
result_preview: str # brief result summary (e.g., "Found 23 papers")
timestamp: float
Event flow for a research task:
{type: "agent_started", step: "Research on: transformers in long-context models"}
{type: "step_start", step: "Searching for relevant papers", step_num: 2, total_steps: 7}
{type: "step_complete", step: "Searching for relevant papers", result_preview: "Found 23 papers"}
{type: "step_start", step: "Filtering papers by recency", step_num: 3, total_steps: 7}
...
{type: "agent_complete", step: "Research summary complete"}
Delivery: Server-Sent Events (SSE) for server-to-client streaming. One persistent HTTP connection, events pushed as they're emitted. No polling, no WebSocket complexity.
# Flask SSE endpoint
@app.route("/events/<agent_run_id>")
def event_stream(agent_run_id):
def generate():
for event in event_queue.listen(agent_run_id):
yield event.to_sse() # "data: {...}\n\n"
return Response(generate(), mimetype="text/event-stream")
The frontend renders each event as it arrives: "Step 3/7: Filtering papers by recency..." When the step completes, the line updates: "Step 3/7: Filtering papers by recency ✓ (8 papers remaining)". The 45-second wait feels much shorter when you can watch the work happening.
Why SSE over WebSockets: SSE is unidirectional (server → client) — exactly what progress streaming needs. WebSockets are bidirectional and require connection management. For streaming progress events, SSE is simpler, more HTTP-friendly, and works through proxies and load balancers without special configuration.
An agent task takes 40 seconds. The best UX is:
Explanation: A spinner is a black box — users don't know if anything is happening, and they can't tell if the system is stuck or working. Streaming events give users a real-time window into the agent's work. Psychologically, the same 40 seconds feels much shorter when you can see progress ("Step 5/8: Drafting report..."). A warning message helps but doesn't solve the blank-screen problem. A percentage bar is better than a spinner but requires knowing the total duration, which agents generally don't.
Human-readable action logs
After an agent task completes, show the user what it did. Not a JSON dump. Prose.
Bad:
[{"tool": "web_search", "args": {"q": "transformers"}, "result": "[{...}, {...}]", "duration_ms": 1200}]
Good:
Agent Action Log
========================================
1. [✓] Analyzed your research question
→ 2-part question: mechanisms + practical applications
2. [✓] Searched for relevant papers
→ Found 23 papers on transformer attention mechanisms
3. [✓] Filtered papers by recency (2024-2025)
→ 8 papers from 2024-2025 remain
4. [✓] Read top 3 papers in detail
→ Read 3 papers, 47 pages total
5. [✓] Identified key themes
→ 3 key themes identified
6. [✓] Drafted research summary
→ Draft complete: 612 words
The action log is also your debugging output. When something goes wrong, you have a structured record of exactly what the agent did, in what order, with what results. A failed step shows [✗] with the error message.
Users who understand what the agent did trust it more. The action log is not a nice-to-have — it's the mechanism by which users build trust in an agent over time. After 10 successful runs with visible action logs, users become comfortable giving the agent more autonomy.
Interruption and confirmation gates
Before irreversible actions — sending emails, making purchases, publishing content, deleting data — pause and ask for confirmation.
The confirmation gate pattern:
decision, revision = gate.check(
ConfirmationRequest(
action_description="Send report to 500 recipients",
details={"recipients": email_list, "subject": "Q3 Report"},
stakes="irreversible",
)
)
if decision == ConfirmationDecision.CANCEL:
return {"status": "cancelled"}
elif decision == ConfirmationDecision.REVISE:
# revision contains user's modification instructions
apply_revision(revision)
# continue with modified plan
Three outcomes: Continue (proceed as planned), Revise (user provides modification instructions, agent adapts), Cancel (abort the action entirely).
Stakes levels: low (auto-approve), medium (show but auto-continue after timeout), high (require explicit confirmation), irreversible (hard block — always requires explicit confirmation before proceeding).
The UX contract: "I will tell you before I do anything that can't be undone." This is what makes users comfortable giving agents more autonomy over time. Users who've been surprised by irreversible agent actions don't give those agents more access. Users who trust the confirmation gate do.
A user sees the agent is about to email all 500 of their contacts with a mass message they didn't intend. With a confirmation gate, they can:
Explanation: Irreversible actions need confirmation gates — period. Once 500 emails are sent, you cannot unsend them. The confirmation gate surfaces the pending action (what will happen, to whom, with what content) before it executes, giving the user one last opportunity to review and stop it. This is what makes the difference between an agent users trust with real access versus one they only run in sandbox mode.