跪拜 Guibai
← All articles
Frontend · Agent · Interview

Running Production Agents Steadily: SSE Streaming, HITL Pause/Resume, and the State That Holds It Together

By Sailing ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Long-running Agent tasks in production fail in predictable ways: no feedback, lost context, duplicate dangerous operations. This design gives teams a concrete protocol for streaming progress, persisting run state, and resuming after human confirmation, which is the difference between a chat demo and a deployable system.

Summary

Once an Agent moves past demo chat into a real business, the hard part isn't the model's answers. It's that a task runs for minutes with no progress shown, a user says "continue investigating it" and the system doesn't know what "it" is, or a restart action sits waiting for approval and the backend has no idea how to continue after the click. The article builds the missing pieces from scratch: distinguishing Conversation, Message, and Run as separate data records; pushing progress over SSE with idempotent, sequence-carrying business events; and treating human confirmation as a normal `interrupt` state rather than an error.

For the HITL path, the key discipline is that resuming a Run must not be a fresh chat. The resume API takes an `interruptId`, an idempotency key, and a decision, re-validates permissions and reloads the target resource before executing, and relies on a conditional database update to let only one of two concurrent clicks start the dangerous operation. SSE disconnects are also separated from task cancellation: query tasks can die with the connection, but publish/approve/restart tasks keep running server-side and write results to the database so a refresh just re-reads state.

The article also maps its design to LangGraph concepts, notes that framework state save doesn't make external tools idempotent, and recommends a rollout order: POST + SSE with a frontend buffer first, then Run/checkpoint/resume, then event replay and multi-instance sync. The closing tests list includes split events, Chinese characters crossing chunk boundaries, double-click confirm, expired interrupts, and checking whether a failed tool call pollutes the confirmed `state`.

Takeaways
— A production Agent run needs separate data records for Conversation, Message, and Run, because they have different lifecycles and purposes.
— Context assembly should split into confirmed business facts (state), a compressed summary of earlier turns, and recent verbatim turns, so the model can resolve references like "it" without full history.
— A Run waiting for human input should enter an `interrupted` status and emit an `interrupt` event; the confirmation card is normal business flow, not an error.
— User confirmation must hit a resume API with the interrupt id, decision, and an idempotency key, not a new chat message.
— Before resuming a dangerous operation, the server must reload the resource and re-authorize, because both can change while the user is deciding.
— Conditional database updates (`WHERE status = 'pending'`) let the database pick the first of two concurrent resume requests; the loser returns without executing.
— SSE disconnect does not equal task cancellation: query tasks may cancel, but publish/approve/restart tasks should continue and persist results.
— Event types should be stable business events (run_started, text_delta, tool_started, tool_finished, interrupt, done, error), each with a per-Run sequence for dedup and later recovery.
— Heartbeats in SSE keep idle connections alive but cannot replace persisted run state; browser auto-reconnect alone will not recover missed events.
— LangGraph's checkpointer and interrupt mechanics save graph state, but they do not make external tool calls idempotent or implement frontend confirmation UI.
— Recommended rollout: POST + SSE with buffer parsing and DB writes first, then Run/checkpoint/resume and double-click prevention, then event replay and multi-instance sync.
Conclusions

The hardest part of production Agents isn't the agent loop itself, it's the boundaries: what happens when a human steps away, a connection drops, or two clicks race the same operation.

A framework like LangGraph can manage graph state, but the external contract (SSE events, resume API, permission checks) still has to be built by the application team.

Basing resume on reloading the resource and re-checking permissions is a strong pattern: it treats the confirmation interval as a potentially stale window, not a fixed moment.

The conditional UPDATE trick is a reminder that idempotency keys alone don't stop racing requests; you need a database-enforced state transition.

Simplifying SSE over WebSocket for Agent progress is pragmatic; many teams over-engineer real-time communication before the backend even has a stable event protocol.

Concepts & terms
HITL (Human in the Loop)
A design pattern where a computer system pauses execution to request human input, typically for approval, parameter completion, or handling ambiguous cases. In Agent pipelines, it's implemented as an `interrupt` state that can later be resumed via an API.
SSE (Server-Sent Events)
An HTTP-based protocol that lets a server push events to a client over a single long-lived connection. Unlike WebSocket, it's unidirectional (server to client), has a standard event format, and native browser support via `EventSource`.
Run
A single task execution triggered by one user message in an Agent system. It carries its own status (running, interrupted, completed, failed, cancelled) and a checkpoint for resuming after interruption. Distinct from the parent Conversation and individual Messages.
Checkpoint
A minimal slice of state saved when a Run is interrupted, containing the next step and necessary parameter references so execution can continue from that point after human confirmation.
Idempotency key
A unique token sent by a client to identify a specific action; when the server receives the same key, it returns the stored result instead of executing the action again. Used here to prevent double-click or retry storms from duplicating dangerous operations.
From the discussion
Featured comments
yangyuan02

Drop the repo link so we can check it out.

kongzhi

That's right, not publishing the code leaves us with only theory and no way to see the entire runtime process.

Sailing  → kongzhi

Hang on, still putting together a demo.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗