An Agent that runs only within a process can treat a single execution as a function call: input comes in, the model and tools loop until completion, the function returns, and the caller gets the result. Once it enters the Web, this assumption breaks quickly.
Browser pages may refresh, network connections may drop, users may close tabs, yet the Agent may still be executing; another page may later reopen the same Session and need to recover the messages, tool calls, and current execution state that have already been produced. At this point, HTTP requests, SSE, or WebSocket connections can only represent a segment of the communication relationship and cannot naturally bear the lifecycle of an Agent's execution.
Therefore, the first thing a Web Agent must handle is not "how to stream tokens to the browser," but the ownership relationships between several lifecycles:
Session long-running conversation fact
Run a single execution
Connection a network connection segment
Browser Page a frontend instance
As long as any two of these objects are merged, refresh, disconnection, recovery, or concurrent access will cause semantic conflicts.
Starting from Regular Streaming Requests
The most straightforward Web implementation is usually:
Browser
│ POST /chat
▼
Server
│ stream model output
▼
HTTP Response / SSE
│
▼
Browser UI
If execution time is very short, this structure is sufficient. The request object holds an AbortSignal, model Streaming follows the connection, and execution ends when the connection ends.
The problem appears when the execution lifecycle starts exceeding the connection lifecycle.
For example, an Agent job might include:
LLM
↓
Tool A
↓
LLM
↓
waiting for external approval
↓
Tool B
↓
LLM
During the wait, there's no need to maintain the original HTTP call stack; the browser may have already refreshed. If the Server binds "this execution" directly to the Response, once the Response disappears, the Runtime faces two conflicting choices:
- Cancel the execution. This way network failures would change business semantics.
- Continue the execution. This way the original Response is no longer the owner of the execution state.
The second case actually requires the system to introduce an execution object independent of the connection.
Session Cannot Replace Run
Session is suitable for expressing long-running facts: messages, branches, user inputs, model outputs, tool results, conversation metadata, and the persistent state needed for building Context later.
A single execution needs another group of states:
queued
running
waiting
completed
failed
cancelled
And also:
startedAt
finishedAt
abortHandle
currentStep
waitingReason
parentRunId?
If all these fields are written directly into Session, a structural problem emerges: Session's lifecycle is much longer than a single execution, but Run state has clear start and end boundaries. The same Session may also produce multiple Runs in sequence, and there may even be background Runs, SubAgent Runs, or retry Runs.
Therefore, a more stable model at the application layer is:
Session
├── Message / Event ...
├── Run 1
├── Run 2
└── Run 3
Here, Run is an abstraction at the product and Durable Web Runtime layer. Not all Agent frameworks elevate it to a core domain object. For example, DeepSeek Harness currently emphasizes Session, Turn, and Step more; Web products can still build independent Run records on top of these for task state, recovery, auditing, and scheduling.
Run solves "what state is a particular execution currently in," while Session solves "what has happened in this long-running interaction."
Connection Should Not Own Run Even More
Connection's lifecycle has contingency.
Connection #1
│
├── event 10
├── event 11
└── disconnect
Run is still continuing
│
├── event 12
├── event 13
└── event 14
Connection #2
│
└── resume from 11
The most important relationship here is:
Run 1 ────────────────┐
│
Connection #1 ───X │
│
Connection #2 ─────────┘
A connection is just a channel through which a particular client observes Run and Session.
If the Server automatically cancels Run after network disconnection, then Connection gains control over the business lifecycle; if refreshing the page creates a new Run, then Browser Page gains control over the business lifecycle. Both cause "communication failure" and "business cancellation" to become conflated.
A clearer control approach is to separate the two things:
transport disconnect
→ end current connection
cancel run
→ explicitly call Runtime cancellation
User clicking Stop belongs to the latter. Wi-Fi disconnection belongs to the former.
What Boundaries Does Vercel AI SDK Provide
Vercel AI SDK's useChat is well-suited for observing Web Agent's UI and Transport layers. In the current API, useChat manages UIMessage[], status, errors, and send, stop, and recovery operations; Transport can be replaced with custom HTTP, WebSocket, or direct Agent implementation.[1][2]
This shows that useChat's main responsibilities lie in:
UI State
+
Transport
Instead of defining the complete Durable Session model for the application.
AI SDK's persistence documentation also clearly delegates message persistence to the application. UIMessage is oriented toward frontend display and is not the same structure as ModelMessage sent to the model.[3]
This corresponds to an important layering:
Frontend UIMessage[]
│
│ Transport payload
▼
Server Canonical Session
│
│ Context Builder
▼
ModelMessage[]
The frontend owns the UI State needed for display, while the Server should still own the final interpretation of conversation facts.
If the application is very small, UIMessage[] can be persisted directly. When an Agent introduces Event Log, Tool State, Approval, SubAgent, or Compaction, Server Canonical State is usually richer than UIMessage[], and UI Message becomes one projection among them.
Stream Resume Still Needs Persistent State
AI SDK currently supports stream resume for useChat, but the official documentation also clearly states: the recovery mechanism requires the application to persist Messages and Active Stream itself, and maintain the relationship between Chat and Stream ID.[4]
The actual problem of recovery can be written as:
Client has already seen seq = N
│
X connection lost
│
Server continues producing N+1 ... M
│
New connection established
│
How to fill in N+1 ... M?
Only the Streaming Transport itself cannot answer this question.
At least one resumable fact source is needed:
Durable Event Tail
or
Resumable Stream Store
or
Server Projection + Durable Delta
Then the client can execute:
load baseline at N
+
replay durable events > N
+
attach live stream
If a system claims to support strict recovery, it must handle the gap between "history reading completed" and "real-time subscription established." A common approach is to let replay and live attach share a continuous sequence space on the Server side, or establish a checkpoint before subscribing.
There Is Real Conflict Between Abort and Resume
AI SDK's current documentation specifically points out that resumable streams with resume: true conflict with abort/stop mechanisms: refreshing or closing the page triggers AbortSignal, which may break stream resumption.[4][5]
This is not an accidental limitation of a certain library; it reveals a more general problem:
Connection Abort
versus
Execution Cancel
In simple Streaming architectures, these often share the same AbortSignal.
Once Run is required to exist independently of the connection, the two cancellations must be separated:
connection.abort()
only close the client consumption channel
run.cancel()
terminate the Agent Runtime
This is a clear boundary where Web Agent evolves from "streaming HTTP request" to Runtime.
Browser Page Only Owns View Instances
Page refresh means all JavaScript memory is lost. If the page object owns the only copy of Session, then refresh is equivalent to losing Session; if the page object owns the only control state of Run, then refresh loses the current execution.
A more stable frontend relationship is:
Browser Page
│
├── Client Runtime
│ ├── Session mirror
│ ├── connection state
│ └── projection cache
│
└── React / UI
The page can destroy the entire Client Runtime. When reloading, the new Runtime rebuilds from Server Canonical State.
Client Runtime can cache lastSeq, current Session ID, or UI preference, but these data are used to improve recovery efficiency and should not be the sole source of business facts.
DeepSeek Harness's Client Runtime
DeepSeek Harness's current browser side has clearly adopted this layering. Its client constraints document writes the data object layer as:
ConnectionController
↓
SessionManager
↓
Session
This layer prohibits React dependencies; Session itself maintains event windows, Streaming accumulation, Reconnect repair, and observable Snapshots. React bindings are placed in a separate web-react layer.[6]
ConnectionController itself manages connection generation and exponential backoff. When a new generation is established, it triggers onConnected; SessionManager.handleConnected() refreshes the Session list and lets already-instantiated Sessions execute resync().[7][8]
Session's resync() raises openGeneration, abandons in-flight opens on the old connection, clears the window, and re-reads history; real-time Events enter liveBuffer during open or gap repair. After history is persisted, the buffer is attached to the window tail according to seq.[9]
This implementation shows that the recovery object for Web Runtime is neither a React Component nor a Socket, but a Client Session with clear state and sequence rules.
Server State, Client State, and Run State
After converging these relationships, a more stable ownership model can be obtained:
Server
│
├── Session State
│ durable facts
│
├── Run State
│ active execution
│
└── Event Stream
transport projection
│ network
▼
Client Runtime
│
├── Session Mirror
├── Projection Cache
├── Connection State
└── lastSeq / resync state
│ observable snapshot
▼
UI
├── local interaction state
└── rendered view
Several boundaries can thus be clarified:
- Session facts are not owned by browser connections.
- Run termination is not implicitly determined by network disconnection.
- Event Stream is responsible for transporting changes and does not bear the sole state source.
- Client Runtime is responsible for recovery and projection, and does not stuff such business state into React Components.
- UI is responsible for display and local interaction state.
A Design Judgment
The core problem of Web Agent is not choosing SSE or WebSocket.
Transport can be replaced. The more stable design question is: who owns execution, facts, connection, and view respectively, and whether the remaining objects can maintain correct semantics after one object disappears.
When these four lifecycles are separated, the structure for disconnection recovery naturally forms:
Session / Run continue to exist
↓
Connection can be established repeatedly
↓
Client Runtime rebuilds from checkpoint
↓
UI is just the current projection
The next article will continue to handle the most critical issue inside Client Runtime: the Server sends Events, React ultimately needs View State, where should the Projection go in between, and how historical Replay and real-time Append can use the same computational logic.
References
[1] Vercel AI SDK useChat: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
[2] Vercel AI SDK Transport: https://ai-sdk.dev/docs/ai-sdk-ui/transport
[3] Vercel AI SDK Chatbot Message Persistence: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence
[4] Vercel AI SDK Chatbot Resume Streams: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-resume-streams
[5] Vercel AI SDK Abort breaks resumable streams: https://ai-sdk.dev/docs/troubleshooting/abort-breaks-resumable-streams
[6] DeepSeek Harness Web Client Rules: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/AGENTS.md
[7] DeepSeek Harness ConnectionController: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/connection/src/client/connection.ts
[8] DeepSeek Harness SessionManager: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/runtime/src/client/sessions/manager.ts
[9] DeepSeek Harness Client Session: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/runtime/src/client/sessions/session.ts