プロセス内だけで動くAgentなら、1回の実行は関数呼び出しとして扱えます。入力が入り、モデルとツールのループが進み、関数が戻り、呼び出し元が結果を受け取ります。しかしAgentをWebで動かすと、この前提はすぐに成立しなくなります。
ブラウザページが再読み込みされ、ネットワーク接続が切断され、ユーザーがタブを閉じても、Agentは実行を続けることがあります。別のページで同じSessionを開き直す際には、それまでに生成されたメッセージ、ツール呼び出し、現在の実行状態を復元しなければなりません。このとき、HTTPリクエスト、SSE、WebSocket接続は、いずれも一区間の通信関係しか表せず、Agentの実行ライフサイクルを自然に担えません。
したがって、Web Agentで最初に考えるべきなのは「tokenをブラウザへ流す方法」ではなく、複数のライフサイクルを誰が所有するかの関係です。
Session 长期会话事实
Run 一次独立执行
Connection 一段网络连接
Browser Page 一个前端实例
この中のどの2つをまとめても、再読み込み・切断・復旧・同時アクセスで意味の矛盾が生じます。
通常のStreamingリクエストから始める
Webでの最も直接的な実装は、たいてい次の形です。
Browser
│ POST /chat
▼
Server
│ stream model output
▼
HTTP Response / SSE
│
▼
Browser UI
実行時間が短ければ、この構成で十分です。リクエストオブジェクトがAbortSignalを保持し、モデルのStreamingは接続に追従し、接続が終われば実行も終わります。
実行のライフサイクルが接続のライフサイクルより長くなった途端に、問題が現れます。
例えば、Agentの処理が次のようになります。
LLM
↓
Tool A
↓
LLM
↓
等待外部审批
↓
Tool B
↓
LLM
待機中は元のHTTP呼び出しスタックを維持する必要がありません。ブラウザは再読み込みされているかもしれません。Serverが「今回の実行」をResponseに直接結び付けると、Responseが消えた時点で、Runtimeには互いに矛盾する2つの選択肢が残ります。
- 実行をキャンセルする。この場合、ネットワーク障害が業務の意味を変えてしまいます。
- 実行を継続する。この場合、元のResponseは実行状態を所有するオブジェクトではなくなります。
実際、後者では、接続とは独立した実行オブジェクトをシステムへ導入することが求められています。
SessionはRunの代替にはならない
Sessionは長期的な事実を表すのに向いています。メッセージ、分岐、ユーザー入力、モデル出力、ツール結果、セッションメタデータなどを保持できます。加えて、後でContextを構築するために必要な永続状態も扱えます。
1回の実行には、別の状態群が必要です。
queued
running
waiting
completed
failed
cancelled
さらに、次のようなフィールドも必要です。
startedAt
finishedAt
abortHandle
currentStep
waitingReason
parentRunId?
これらのフィールドをすべてSessionへ直接書き込むと、構造上の問題が生じます。Sessionのライフサイクルは1回の実行よりはるかに長い一方、Run状態には明確な開始・終了の境界があります。同じSessionから複数のRunが順次生じるケースに加え、バックグラウンドRun、SubAgent Run、リトライRunが含まれることもあります。
アプリケーション層では、次のモデルのほうが安定します。
Session
├── Message / Event ...
├── Run 1
├── Run 2
└── Run 3
ここでのRunは、プロダクトとDurable Web Runtime層にまたがる抽象化です。すべてのAgentフレームワークがRunを中核のドメインオブジェクトとして扱うわけではありません。たとえば、現在のDeepSeek HarnessではSession、Turn、Stepをより重視しています。それでもWebプロダクトでは、その上に独立したRunレコードを設け、タスク状態、復旧、監査、スケジューリングに利用できます。
Runは「いまこの実行がどの状態か」を示します。Sessionは「この長期的な相互作用では何が起きたか」を表します。
ConnectionがRunを所有してはならない理由
接続のライフサイクルは、偶発的なものにすぎません。
ここで最も重要な関係は次のとおりです。
接続は、あるクライアントがRunとSessionを観測するための経路にすぎません。
ネットワークが切断された途端にServerがRunを自動キャンセルすると、Connectionが業務ライフサイクルの支配権を持ちます。ページを再読み込みして新しいRunが生成されるなら、Browser Pageがその支配権を持ちます。どちらも「通信の失敗」と「業務上のキャンセル」を混同してしまいます。
制御関係を明確にするには、この2つを分けます。
transport disconnect
→ 结束当前连接
cancel run
→ 显式调用 Runtime cancellation
ユーザーがStopをクリックするのは後者です。Wi-Fiが切断されるのは前者です。
Vercel AI SDKが示す境界
Vercel AI SDKのuseChatは、Web AgentのUIとTransport層を観察するのに向いています。現在のAPIでは、useChatはUIMessage[]、status、エラー、および送信・停止・復旧の各操作を管理します。Transportは、カスタムHTTP、WebSocket、Agentを直接呼び出す実装に置き換えられます。[1][2]
これ意味着、useChatの主な責務は次の範囲にあり、アプリケーションの完全なDurable Sessionモデルを定義する役割ではありません。
UI State
+
Transport
AI SDKの永続化ドキュメントは、メッセージの永続化をアプリケーションの担当だと明記しています。UIMessageはフロントエンドでの表示用であり、モデルに送るModelMessageとは異なる構造です。[3]
この点は、重要なレイヤリングに対応しています。
Frontend UIMessage[]
│
│ Transport payload
▼
Server Canonical Session
│
│ Context Builder
▼
ModelMessage[]
フロントエンドは表示に必要なUI Stateを所有しますが、セッションの事実について最終的な解釈権を持つべきなのはServerです。
アプリケーションが小さければ、UIMessage[]を直接永続化できます。ただし、AgentにEvent Log、Tool State、Approval、SubAgent、Compactionが加わると、Server Canonical Stateは通常UIMessage[]より多くの情報を持ちます。UI Messageはその投影の一つにすぎません。
Stream Resumeにも永続状態が必要
AI SDKは現在、useChatのstream resumeをサポートしています。公式ドキュメントでは、復旧処理にはMessageとActive Streamをアプリケーション側で永続化すると明記されています。ChatとStream IDの関係も、アプリケーション側で維持する必要があります。[4]
復旧時の実際の問題は、次のように表せます。
Streaming Transport自体だけでは、この問題には答えられません。
少なくとも1つの復旧可能な事実源が必要です。
Durable Event Tail
或
Resumable Stream Store
或
Server Projection + Durable Delta
そのうえでクライアントは次を実行できます。
load baseline at N
+
replay durable events > N
+
attach live stream
システムが厳密な復旧に対応すると主張するなら、「履歴の読み込み完了」と「リアルタイム購読の確立」のあいだのgapを処理しなければなりません。一般的な手法は、Server側でreplayとlive attachに連続したsequence空間を共有させるか、checkpointを作成してから購読することです。
AbortとResumeには本質的な矛盾がある
AI SDKの現行ドキュメントでは、resume: trueを指定したresumable streamとabort/stop機構との競合が明示されています。ページを再読み込みしたり閉じたりするとAbortSignalが発火し、stream resumptionを損なう可能性があります。[4][5]
これは、あるライブラリの偶発的な制約ではありません。一般的な問題を浮き彫りにしています。
Connection Abort
与
Execution Cancel
単純なStreaming構成では、この2つで同じAbortSignalを共有することがよくあります。
Runが接続から独立して動き続ける設計にするなら、2種類のキャンセルを分離しなければなりません。
connection.abort()
只关闭客户端消费通道
run.cancel()
终止 Agent Runtime
これが、Web Agentが「ストリーミングHTTPリクエスト」からRuntimeへ進化する明確な境界です。
Browser Pageが所有するのはビューインスタンスだけ
ページを再読み込みすると、JavaScriptのメモリはすべて失われます。ページオブジェクトがSessionの唯一のコピーを保持していれば、再読み込みはSessionを失うのと同じです。Runの制御状態を一意に所有していれば、現在の実行が失われます。
より安定したフロントエンドの関係は次のとおりです。
ページはClient Runtime全体を破棄できます。再読み込み時の新しいRuntimeは、Server Canonical Stateから再構築されます。
Client RuntimeはlastSeq、現在のSession ID、UI preferenceなどをキャッシュできます。ただし、これらは復旧効率を高めるためのデータであり、業務上の事実の唯一の情報源として扱ってはいけません。
DeepSeek HarnessのClient Runtime
DeepSeek Harnessのブラウザ側では、このレイヤリングがすでに明確に採用されています。クライアントの制約文書では、データオブジェクト層を次のように書いています。
ConnectionController
↓
SessionManager
↓
Session
この層はReactに依存できません。Session自身がイベントウィンドウ、Streaming accumulation、Reconnect時の修復、観測可能なSnapshotを管理します。Reactとのバインドは独立したweb-react層に置かれています。[6]
ConnectionController自身がconnection generationとexponential backoffを管理します。新しいgenerationが確立するとonConnectedが発火します。SessionManager.handleConnected()はSession一覧を更新し、すでにインスタンス化されたSessionにresync()を実行させます。[7][8]
Sessionのresync()はopenGenerationを進め、古い接続上のin-flight openを破棄し、ウィンドウをクリアしてから履歴を読み直します。リアルタイムEventは、open中またはgap repair中にliveBufferへ入ります。履歴が確定したら、seqに基づいてbufferをウィンドウ末尾へつなぎます。[9]
この実装から分かるのは、Web Runtimeの復旧対象がReact ComponentやSocketではなく、明示的な状態とsequence規則を持つClient Sessionだということです。
Server State、Client State、Run State
これらの関係を整理すると、比較的に安定した所有権モデルは次のようになります。
ここから、所有境界を次のように整理できます。
- Sessionが表す事実は、ブラウザ接続が所有するものではない。
- Runの終了は、ネットワーク切断によって暗黙に決まるものではない。
- Event Streamは変化の配信を担いますが、唯一の情報源ではない。
- Client Runtimeは復旧と投影を担当し、この種の業務状態をReact Componentへ押し込まない。
- UIは表示と局所的な操作状態を扱う。
一つの設計判断
Web Agentの中核的な問題は、SSEとWebSocketのどちらを選ぶかではありません。Transportは差し替えられます。より安定した設計上の問いは、実行、事実、接続、ビューをそれぞれ誰が所有するか、そして1つのオブジェクトが消えた後も、残りのオブジェクトが正しい意味を保てるかです。
この4種類のライフサイクルを分けると、切断後の復旧構造は自然に形成されます。
Session / Run 继续存在
↓
Connection 可以反复建立
↓
Client Runtime 根据 checkpoint 重建
↓
UI 只是当前投影
次回は、Client Runtime内部の最も重要な問題を扱います。Serverが送るEventと、Reactが最終的に必要とするView Stateとの間にあるProjectionはどこに置くべきか。また、履歴のReplayとリアルタイムのAppendに、同じ計算ロジックをどう使うかです。
参考資料
[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