前の2回では「Pi Session Tree」と「DeepSeek Harness Session Event Log」をそれぞれ分析した。永続化モデルは異なるが、どちらも同じ原則に従っている。プロセスが終了した後も、新しいRuntimeは永続化された状態からセッションを再構築する。古いRuntimeオブジェクトがそのまま残っていることに依存してはならない。
この回では、TypeScript制成的小さなDemoで、この原則を検証する。
Demoは実際のLLMを実装せず、DSHの完全なSessionEventプロトコルを再現することもない。残す仕組みは次の4つだけだ。
append-only Event Log
continuous seq
Message Projection
Crash Repair
最終的に確認するのは次の流れだ。
この連鎖が成立すれば、SessionとRuntimeのライフサイクルはすでに本当に分離されている。
最小のEventモデルを定義する
Demoでは、次の種類のEventだけを保持する。
ここでは意図的に3種類の情報を分けている。
1つ目は、モデルから見える履歴だ。
user/message
assistant/message
tool/result
2つ目は、実行の事実だ。
turn/start
turn/end
tool/call
3つ目は、リクエスト環境だ。
request/header
この区分はDSHの完全なモデルに近いが、DemoではStep、Chunk、Surface Replace、Request Contextといった高度な機能は外している。
すべてのEventには、連続したseqが付く。
seqは単なる表示フィールドではない。Replayの順序を定め、基本的な完全性チェックも担う。
永続的なAppendをメモリのProjectionより先に
append()の実装は短い。
ここでは意図的に、次の書き込み順序を保っている。
append JSONL
↓ success
push in-memory events
Demoでは同期ファイル書き込みを使うため、この境界を比較的はっきり表現できる。
実際のシステムでは非同期のバッチ処理を使うことが多く、その場合には明示的なflush / checkpointのセマンティクスが必要になる。DSHのPersistence Pluginはsession/eventをバックグラウンドbufferへ複製し、永続性チェックポイントが必要なときにsession/flushで排出する。[1]
Demoで同期書き込みにしているのは状態モデルの検証に集中するためだけで、本番系统在Agent Loop内でChunkごとに同期ディスクI/Oを実行すべきだという意味ではない。
MessageはEvent LogのProjection
Session内では、永続化可能なmessages.jsonを別に保持していない。
deriveMessages()は、毎回Event LogからMessageを組み立てる。
この結果、次のようになる。
turn/startとtool/callが消えたわけではない。引き続きSession Factとして存在するが、Model Message Projectionには入らない。
これにより、2つの永続状態の間にあるcheckpoint問題を防げる。
session.jsonl authoritative
messages derived
将来UIでTool Callを表示したければ、別のProjectionを追加すればよい。Model Message Projectionを変更する必要はない。
Request HeaderでContextをMessageだけに依存させない
Messageだけを復元しただけでは、次のモデルリクエストを完全には組み立てられない。
そこでDemoでは、簡略化したrequest/headerを保存する。
Contextを復元するときは、最新のHeaderを読み込む。
これにより、Model Contextそのものが永続化対象ではないことがわかる。
2種類のDurable Stateを組み合わせて構成される。
request/header
+
derived messages
↓
Model Context
実際のシステムでは、さらに次のようなものを追加するかもしれない。
Memory
Compaction
Current Agent Policy
Runtime Injection
ただし、構築の原則は変わらない。
未完了のTurnをシミュレートする
Process AがSessionを作成する。
その後、通常どおり次のEventを追加する。
0 request/header
1 turn/start
2 user/message
3 assistant/message
4 tool/call
5 tool/result
コードは意図的に、次を書いていない。
turn/end
そこでPhase 1を止め、プロセスがTurnの途中で終了した状況をシミュレートする。
この時点でディスク上には、次の2つの事実を表現するのに十分な情報がある。
Turn 1 曾经开始
Tool Result 已经完成并落盘
同時に、次のこともはっきり確認できる。
Turn 1 没有正常结束
run.status = RUNNINGだけの場合より、復旧に使える情報が多い。どの事実まで完了しているかが、具体的にログに残っているからだ。
open()はまずseqを検証する
Process Bが起動するとJSONLを読み込む。
assertContiguousSeq()は次のことを検証する。
ログが次の形になっていたら、
0
1
2
4
システムが黙ってReplayすることはしない。
seq=3がまったく発生しなかったのか、発生したが失われたのかを判別できないからだ。
こうした厳格な検証は、Event Logが事実の正本になるための前提条件のひとつだ。DSHもSessionのseq連続性を要求し、Surface Foldとロード境界でEvent構造を検証する。[2]
Crash Repairがinterruptedを追加する
復旧時にTurn Boundaryを走査する。
そのため、sessionを開き直した後のログは次のようになる。
0 request/header
1 turn/start
2 user/message
3 assistant/message
4 tool/call
5 tool/result
さらに、
0 request/header
1 turn/start
2 user/message
3 assistant/message
4 tool/call
5 tool/result
6 turn/end:interrupted
古いEventは削除されていないし、completedに偽装してもいない。
追加された復旧事実は、次を述べるだけだ。
当前进程接管时,发现上一 Turn 未闭合;
该 Turn 因中断结束。
この考え方は、DSHのcold SessionにおけるCrash Repair戦略にそのまま対応している。[1]
復元後のMessageも確定している
turn/end:interruptedはMessage Projectionに含まれない。
そのため、Process Bが実行するのは次の処理だ。
結果は同じになる。
User
Check UserService.
Assistant
I will inspect the file first.
Tool Result
class UserService { ... }
すでに永続化済みのTool Resultは、Turnの中断によって消えることはない。
これがEvent Logの実用上の利点のひとつだ。実行の完全性と事実の保持を分けて扱える。
Turnが失敗または中断しても、それ以前に無事完了したEventは残っている。
その後の業務ロジックは、Eventのセマンティクスに応じて判断できる。
保留 Tool Result
忽略半完成 Assistant Chunk
重新执行未完成 Tool
要求用户确认
自动继续下一 Turn
Crash時に履歴を書き換えなくてもよい。
新しいRuntimeは作業を続けられる
復旧が完了すると、 Demoは2つ目のTurnを追加し続ける。
完全な履歴は次のようになる。
Turn 1
start
messages
tool result
interrupted
Turn 2
start
messages
completed
Process Bは、Process Aのメモリオブジェクトを一度も受け取っていない。
依存しているのは次のものだけだ。
Session Event Log
SessionとRuntimeを疎結合にした後の、もっとも基本的な復旧能力がこれだ。
このDemoにまだ足りないもの
現在の実装は状態モデルの実験にとどまり、本番級のSessionとはまだ明確な差がある。
少なくとも次のものが不足している。
最も難しい問題は、通常append()ではなく外部副作用のほうにある。
たとえば、ログに次のEventがあるとする。
tool/call
その後、プロセスが支払い、メール送信、DB変更を実行したが、tool/resultが永続化される前にクラッシュした。
復旧後、Toolを単純に再実行すると、副作用が重複する可能性がある。
そのため、Tool layerでさらに設計が必要になる。
Idempotency Key
Execution Record
External Transaction ID
Reconciliation
Event LogはAgentが把握している事実を正確に記録できる。しかし、分散一貫性の問題をすべて自動的に解決できるわけではない。
ここには重要な境界がある。Agent SessionのCrash Recoveryと、外部システムのExactly-once Executionは別の問題だ。
Demoから得られる4つの設計上の結論
この小さな実装で4点を確認できる。
第1に、SessionはRuntimeオブジェクトを保存する必要がない。永続化された事実が十分なら、新しいプロセスでRuntime Stateを再構築できる。
第2に、MessageはProjectionとして扱える。这样することで、実行Event、UI Event、Model Messageを同じデータ構造に押し込む必要がなくなる。
第3に、Model Contextも再構築できる。Message Historyだけでなく、System Prompt、Tools、モデル設定など、リクエスト状態の明確な出所を用意する必要がある。
第4に、Crash Recoveryは、既に永続化された履歴を改竄するより、新しい復旧事実として表現するのがよい。未完了のTurnはinterruptedとしてマークし、完了済みのTool Resultはそのまま保持できる。
このモデルと前の2記事を合わせると、3段階の複雑さが見える。
システムは、製品要件に一致する最小限の複雑さを持つモデルを選ぶべきだ。
チャット内容の保存だけならMessageで十分だ。分岐可能な長期Coding Sessionが必要なら、PiのTreeがその問題cai very targeted。Replay、復旧、複数Projection、リクエスト再構築が中核機能になるなら、DSHのようなEvent-sourced Sessionのほうが価値がある。
ユニット2はここまでで、「Sessionには何を保存すべきか」から始め、2つの実際のフレームワーク設計を経て、実行できる最小実装まで一巡した。
次のユニットでは別の問題に入る。Tool、Memory、Sandbox、MCP、SubAgent、UIなどの機能がAgentシステムに増え続けるとき、Runtime Coreが膨らみ続けるのをどう防ぐか。Pi Extension、Cordis、DeepSeek HarnessのEverything is a Pluginは、すべてこの問題に答えている。
Demo
ディレクトリ構造:
demo/
├── package.json
├── README.md
└── src/
├── event-session.ts
└── demo.ts
実行:
cd demo
npm run demo
参考文献
[1] DeepSeek Harness Persistence: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/persistence.zh.md
[2] DeepSeek Harness Session / Surface: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/session.md ; https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/surface.ts