Blog · · 10 min read

Make AI agent streaming resumable without replaying work

Make AI agent streaming resumable without replaying work

AI agent streaming fails at the point where a product treats one HTTP connection as the agent run. A page refresh, mobile handoff, proxy timeout, or network break can remove the delivery path while tools keep executing. The user sees a stalled screen and retries. The product may then start a second run, repeat a write, and still lose the first run's result.

A longer socket timeout will not fix this. Give the run a durable identity, store ordered semantic events, resume from an authorized cursor, and expose one authoritative final snapshot. The agent can then keep working without a browser connection, and the client can recover without replaying business work.

Why connection-owned agent runs fail

A streaming response combines two lifecycles that fail for different reasons:

  1. The agent run plans work, calls tools, waits for dependencies, and reaches a terminal outcome.
  2. The delivery connection moves events to one client until the connection closes.

A connection can disappear even when the run is healthy. Mobile operating systems suspend background tabs. Reverse proxies enforce response deadlines. A laptop moves between networks. A user opens the same conversation on another device. None of those events means the business operation should stop or restart.

OpenAI, Vercel, and LangChain each expose a different recovery primitive. OpenAI background mode stores a response for later polling or streaming, assigns sequence numbers to stream events, and supports resuming after the last received event. Vercel AI SDK resume streams separates generation from the browser connection and uses persistent stream identifiers with a Redis-backed resumable stream. LangChain's join and rejoin guidance covers attaching to an active run, persisting events, and rejoining after navigation or refresh.

Framework support for reconnection does not define the product contract. An existing product still needs its own identity, authorization, retention, cancellation, event ordering, and final-state rules. Without them, a reconnect feature can deliver the wrong tenant's run, replay an event twice, or leave the client with a complete transcript but an uncertain product outcome.

Separate the durable run from its delivery connections

Create the run before opening a stream. Store it under a product-owned identifier that is stable across transports, sessions, and devices. Put these fields on the run record:

  • run_id, tenant, initiating user, and owning product resource;
  • the agent release, model policy, tool catalog, and input version;
  • lifecycle state such as queued, running, waiting, succeeded, failed, or cancelled;
  • the latest emitted sequence and earliest retained sequence;
  • a cancellation version and terminal outcome reference;
  • timestamps for start, last activity, terminal state, and event expiry.

A connection record is separate and disposable. It tracks the authenticated principal, transport, requested cursor, connection time, and delivery acknowledgements. Closing it changes no run state.

Ably AI Transport describes this distinction as a durable session that outlives the connection that started it. The client can reconnect, reload, or move to another device while the agent continues publishing. You do not need that product to adopt the contract. Keep the separation between run and connection.

Use explicit commands for lifecycle changes:

POST /agent-runs                 create one durable run
GET  /agent-runs/{run_id}        read the authoritative snapshot
GET  /agent-runs/{run_id}/events replay events, then follow live output
POST /agent-runs/{run_id}/cancel request cancellation idempotently

The stream endpoint only delivers facts about a run; it never controls the run's lifecycle.

Define an ordered semantic event envelope

Do not retain only raw token chunks. A token stream is useful for rendering text, but it cannot prove which tool started, whether a write committed, or which final product state is authoritative. Persist semantic events with enough identity to replay them safely.

{
  "run_id": "run_7f3",
  "sequence": 184,
  "event_id": "run_7f3:184",
  "release_id": "support-agent-2026-08-24.1",
  "type": "tool.completed",
  "occurred_at": "2026-08-24T10:14:31Z",
  "resource_scope": "tenant_42/ticket_981",
  "payload": {
    "tool_call_id": "call_22",
    "tool": "tickets.add_note",
    "receipt_id": "receipt_558",
    "result_version": "ticket_981:v19"
  }
}

This schema is an implementation recommendation rather than a standard. Each field has a specific job:

  • run_id prevents a cursor from being applied to another run.
  • sequence defines one total delivery order within that run.
  • event_id gives clients a stable deduplication key.
  • release_id makes resumed output traceable to the same agent release.
  • resource_scope supports an authorization check without trusting client state.
  • receipt_id and result_version connect streamed claims to product evidence.

Persist events before making them visible. If the producer broadcasts first and writes later, a client can see an event that no reconnect can recover. A transactional outbox or equivalent append-and-publish mechanism closes that race.

The event log does not have to retain every token forever. Keep operational events and the terminal snapshot for the product's audit and recovery window. Token deltas can use a shorter retention period or be compacted into completed message events after the run ends. Publish both retention limits in the API contract.

Implement AI agent streaming as replay plus live tail

A reconnect is two operations joined behind one endpoint:

  1. Replay stored events after the client's last accepted sequence.
  2. Switch to live delivery without creating a gap or delivering an event out of order.

The WHATWG server-sent events standard defines event IDs, browser reconnection, and the Last-Event-ID request header. This transport primitive does not provide a durable agent protocol. Your server still has to validate the cursor and make stored history available.

A reconnect handler can follow this sequence:

stream_run(current_user, run_id, requested_sequence):
    run = load_run(run_id)
    authorize(current_user, "read_agent_run", run.resource_scope)

    cursor = validate_cursor(
        run_id=run_id,
        requested=requested_sequence,
        earliest=run.earliest_retained_sequence,
        latest=run.latest_sequence
    )

    subscription = event_bus.subscribe(run_id, after=run.latest_sequence)

    for event in event_log.read(run_id, after=cursor):
        send_if_authorized(current_user, event)

    for event in subscription:
        send_if_authorized(current_user, event)

The subscribe-before-replay ordering avoids a gap between reading storage and following live events. The event ID lets the server or client suppress any overlap. An alternative is a database or broker primitive that atomically returns history and attaches a consumer. Use whichever mechanism can prove there is no missing interval.

Do not accept a cursor larger than the run's latest sequence. Do not silently clamp a cursor from another run. Return a typed error for malformed, future, or mismatched cursors so client bugs do not look like successful recovery.

Reauthorize every reconnect

A valid cursor is not an authorization token. Permissions can change while a run is active. A user can leave an organization, lose access to the source record, or have a session revoked. Another device may belong to a different account.

Authenticate the new connection and authorize it against the current run owner and resource scope. Repeat authorization for sensitive event classes if an event can reveal data outside the original resource. Avoid putting credentials, tenant identifiers, or raw policy context inside a client-controlled cursor.

An opaque signed cursor can carry run_id, sequence, and expiry, but the signature only proves the server issued it. The server must still check current access. A plain integer sequence also works when the run ID is in the path and every request is authenticated.

Stardrift's streaming resumption guide similarly treats stream identity, cursor validation, authorization, retention, and transport-independent tests as parts of one contract. Storage and transport can vary, but the server still has to check current permissions.

Keep disconnect and cancellation separate

A closed socket is ambiguous. It may mean the user pressed Stop, the browser navigated, a proxy reached its deadline, or the network failed. If the server treats every disconnect as cancellation, background work becomes unreliable. If it never supports cancellation, expensive or harmful work can continue after the user explicitly stops it.

Use a separate authenticated cancellation command with an idempotency key and a compare-and-set lifecycle transition. Record cancel_requested before interrupting workers. Workers should check the cancellation version at tool boundaries and before committing mutable effects. The terminal state must distinguish:

  • cancelled_before_effect, where no business mutation committed;
  • cancelled_after_effect, where a receipt and current product state are returned;
  • cancellation_pending, where a dependency cannot be interrupted yet;
  • already_terminal, where the original outcome remains authoritative.

Vercel's resume-stream documentation notes that stream resumption and abort handling conflict when a disconnect cannot communicate an abort signal to the server. Encode cancellation in a separate API rather than connection teardown. A Stop control should call the cancellation endpoint, then keep reading until the run reaches a terminal state or the UI deliberately detaches.

Recover from expired cursors with a final snapshot

A client may return after its replayable token events have expired. Do not respond by starting the agent again or pretending the cursor was accepted.

Return a typed cursor_expired response containing:

  • the earliest retained sequence;
  • the current run lifecycle state;
  • a link to the authoritative snapshot;
  • whether a shorter replay window is still available;
  • whether the client should render a completed message, an explicit unknown outcome, or an operator recovery state.

The terminal snapshot should contain completed messages, tool receipts, the resulting product resource versions, and any unresolved actions. It is more important than preserving every token delta. A client that cannot reconstruct the animation can still render the truth.

This also handles multi-device continuity. A second device may start from no cursor, load the current snapshot, then follow new events. It does not need the first device's socket or local state.

Prevent replay from repeating tool effects

Event replay must be read-only. Replaying tool.requested or tool.completed updates the interface and local state; it never invokes the tool again. Keep event consumers that render output separate from workers that execute commands.

User retries need the same boundary. If the client is unsure whether POST /agent-runs succeeded, it should retry with the same creation idempotency key. The server returns the existing run_id rather than creating another run. Tool calls and business mutations need their own stable identities because one run may retry internally.

A final snapshot should reference tool receipts and resource versions, not infer success from a fluent assistant message. If a tool finished while no client was connected, the reconnect replays the completion event and then serves the same authoritative result.

Current issue reports expose these recovery failures. Mastra issue 11579 describes reconnect races, duplicate or out-of-order events, dropped events before listener attachment, and listener leaks. Mastra issue 22096 reports a disconnected stream leaving stale run state that blocks later messages. Hermes Agent issue 85016 reports an in-progress run that lacks a client path to reattach after connection loss. These are project reports, not universal behavior, but each is a useful regression case.

Verify recovery with forced failures

Test the contract by breaking transport at controlled points. A happy-path stream proves none of the recovery properties.

Your automated suite should verify that:

  • disconnecting before the first event does not cancel or duplicate the run;
  • disconnecting during a tool call lets the same run complete and resume;
  • reconnecting after sequence 50 delivers 51 onward in order;
  • overlap between stored replay and live subscription produces one application of each event ID;
  • a future, malformed, expired, or wrong-run cursor returns a typed response;
  • a user who lost access cannot reconnect, even with an old valid cursor;
  • two authorized devices can follow one run without creating two runs;
  • retrying run creation with one idempotency key returns one run_id;
  • pressing Stop sends a cancellation command instead of relying on socket closure;
  • a completed side effect returns the original receipt after reconnect;
  • an expired event window still returns the authoritative final snapshot;
  • proxy buffering, idle timeouts, and mobile suspension do not change run state;
  • terminal runs release listeners, workers, and retention timers without stale flags.

Record run ID, connection ID, requested cursor, accepted cursor, replay range, first live sequence, policy decision, duplicate suppression count, cancellation version, terminal state, and snapshot version in one trace. The trace should prove that no event gap and no second business operation occurred.

Start with one interrupted tool call

Choose one existing agent flow that streams text and performs a reversible product action. Add a durable run record, the ordered event envelope, one replay endpoint, explicit cancellation, and a final snapshot. Then force a disconnect while the tool is running and reconnect from the last accepted sequence.

Do not expand until the test proves three facts: the original run continued, each event was applied once, and the final product state came from an authoritative receipt. Once that contract holds, the same AI agent streaming boundary can support refreshes, proxy failures, mobile handoffs, and multiple devices without restarting or replaying work.

References

  • OpenAI background mode supports durable background responses, polling, event sequence numbers, and resuming a stream after the last received event.
  • Vercel AI SDK resume streams supports persistent stream identifiers, Redis-backed stream resumption, and the documented conflict between resumption and abort-on-disconnect behavior.
  • LangChain join and rejoin streams supports active-run attachment, persisted events, page-reload rejoining, and last-event cursor handling.
  • WHATWG server-sent events defines SSE event IDs, Last-Event-ID, automatic reconnection, and non-reconnect status behavior.
  • Ably AI Transport supports the durable-session model, connection-independent generation, reconnection, recovery, page reloads, and multi-device continuity.
  • Stardrift streaming resumptions supports a practical stream identity, cursor, authorization, retention, and recovery contract.
  • Mastra issue 11579 supplies project-reported reconnect races, duplicate or out-of-order delivery, dropped events, and listener leaks.
  • Mastra issue 22096 supplies a project-reported stale run state after stream disconnection.
  • Hermes Agent issue 85016 supplies a project-reported need to reattach to an in-progress run after connection loss.

Be first in line.

Join the waitlist and we'll email you the moment it's ready. No sales call.

Try the demo →
or talk to us