PiのSessionファイルは見かけ上単純です。1つのJSONLファイルで、各行に1件のJSONレコードが入っています。
会話全体を直接messages[]として保存するのでも、ブランチごとに独立したデータベース表を作るのでもありません。現行版のSession Entryはid / parentIdで木構造につながり、ファイルは追記順で増え続ける一方、現在のleafが現在の履歴パスを決めます。[1]
そのため、同じSessionには次の2つの構造が同時に存在します。
物理结构
append-only JSONL
逻辑结构
entry tree
この2つの構造が分かれていることこそ、PiのSession設計を理解する鍵です。
まずファイルそのものを見る
Sessionファイルの1行目はHeaderです。
この後のEntryから木構造が始まります。基本的なフィールドは次のようなものです。
たとえば、単純な会話1回は次のように記録されます。
ディスク上から見れば、依然として順番に追記されているだけです。
A
B
C
D
ただし、parentIdがすでにこれらのレコードに論理的な関係を与えています。
A
└── B
└── C
└── D
ユーザーが後からBに戻り、そこから入力を再開すると、新しいEntryは引き続きファイル末尾へ追記されます。
line 6 user message id=E parent=B
line 7 assistant message id=F parent=E
物理的な順序は次のようになります。
A B C D E F
論理的な構造は次のようになります。
A
└── B
├── C
│ └── D
│
└── E
└── F ← current leaf
ファイルはCやDを書き戻す必要も、AやBを複製する必要もありません。ブランチの作成は、新しいEntryから古いノードを指すだけで済みます。
これはローカルで動くCoding Agentに適した永続化モデルです。追記が扱いやすく、履歴も目視で確認できる一方、ユーザーが同じSession内の古い位置に戻って作業を再開できます。
現在のSessionはleafで決まる
木構造には複数のブランチが含まれますが、モデル側がそれらをすべて同時に現在の履歴として扱うことはできません。
そのため、SessionManagerには「現在位置」も必要です。Piのドキュメントでは、これをcurrent leafと呼んでいます。現在のパスを構築するときは、leafからparentIdをたどりrootまで進み、並びを逆にします。[1]
概念的には次のようなコードになります。
先ほどの木構造でleafがFなら、次のとおりです。
A
└── B
├── C
│ └── D
└── E
└── F
現在のパスは次のとおりです。
A → B → E → F
CとDはSessionファイルに残っていますが、現在のブランチのモデル履歴には含まれません。
Session FileとModel Contextは、最初から同じオブジェクトではありません。
この3層の区別を前提に、後述の多くの機能が成り立っています。
/treeの本質は、現在位置を移動すること
PiはSession Tree内のナビゲーションに対応しています。ユーザーが古いEntryを選ぶと、そこから作業を再開できます。
データ構造の点では、この操作で古いEntryを変更する必要はありません。SessionManagerは、後続するappendのparentを対象ノードへ切り替えるだけで足ります。
たとえば、元のパスが次のだとします。
A → B → C → D
↑
original path
Bに戻って作業を続けると、次のようになります。
A → B → C → D
\
E → F
この設計はGitのコミットグラフと似ています。ノードが親の参照を保持し、現在のleafが現在の作業位置に対応します。ただし、PiのEntryのセマンティクス、Contextの構築、Branch SummaryはAgentセッション固有のものです。Gitのモデルをそのまま当てはめることはできません。
より重要なのは、木構造によって「旧履歴を保持すること」と「現在のContextを変えること」を両立できる点です。
システムが通常の配列を使う場合、実装は次のようになります。
ユーザーが古いノードに戻ると、後続の履歴が直接削除されます。
一方、Session Treeは古いブランチを保持し、新しい入力は別の経路を追加するだけです。
これはCoding Agentにとって非常に有効です。ユーザーは案Aを試して合わなければ、初期状態に戻って案Bを試せます。モデルが以前行った分析や実行の記録は残りますが、現在のContextはBのブランチだけをたどって進められます。
Branch Summaryが、離れたブランチの情報損失を解決する
ブランチを完全に切り替えると問題が生じます。古いブランチのほうに、有価値な情報がすでに生まれている可能性があります。
たとえば、次のようになります。
A → B → C → D
D 中发现:
某个接口不能修改,因为另一个模块依赖它
ユーザーがBに戻り、BからEを作成すると、現在のパスは次のようになります。
A → B → E
Dは現在のパスには含まれないため、その価値のある情報が後続のContextに自然に入ることもありません。
そこでPiはBranchSummaryEntryをサポートしています。Session Treeのナビゲーションで、離れたブランチの情報を残したい場合は、要約を生成して、新しいパスのEntryとして追加できます。[1]
構造はおおむね次のようになります。
A
└── B
├── C
│ └── D
│
└── BranchSummary
└── E
Branch Summaryが表すのは次の意味です。
旧分支虽然不再是当前 path,
其中一部分有价值的上下文被摘要带入新分支。
これはDをそのままContextに残すのとは異なります。新しいブランチは旧ブランチのモデル履歴全体を引き継ぐのではなく、必要な情報を圧縮したうえで引き継ぎます。
PiのSession Tree的一个重要な特徴がここにあります。
CompactionもEntryを通じてContextを変える
長いセッションでは、古い履歴を圧縮する必要があります。Piは元のEntryを削除せず、CompactionEntryを追加します。[2]
現在の構造は次のようになります。
現在のブランチが次の構成だとします。
A B C D E F G H
Contextが大きくなりすぎると、Compactionが生成されることがあります。
summary(A...E)
firstKeptEntryId = F
Sessionには依然として次のような内容が保存されています。
A B C D E F G H Compaction
一方、buildSessionContext()がモデル履歴を構築するときには、次を使います。
Summary(A...E)
F
G
H
ここでも同じ設計方針が見えてきます。永続的な履歴は完全なまま保持し、モデルから見た履歴は圧縮できます。
PiのCompactionドキュメントでは、圧縮の切れ目をTool Resultに自由に置けないことも明記しています。Tool Resultは、対応するTool Callと構造を保ったままペアになっている必要があるからです。[2]
これはContext BuilderがToken数だけでなく、Provider Messageの構造制約も満たす必要があることを示しています。
たとえば、次のような履歴を誤って分割してはいけません。
Assistant(tool_call)
--- cut ---
ToolResult
モデルプロトコルでは通常、Tool CallとTool Resultが正しく対応している必要があるためです。
したがって、Compactionは実質的に2つの問題を同時に扱います。
Token budget
+
Message protocol invariants
buildSessionContext()が本当の復元入口
Sessionファイルを再び開いても、ランタイムが単純に次を実行するわけではありません。
PiのSessionManager.buildSessionContext()は、現在のleafからpathを構築し、そのパス上のEntryを解釈します。[1]
ドキュメントでは、次の手順が説明されています。
- current leafからrootまでのパスを収集する
- パス上のmodel / thinking levelなどの設定を復元する
- Compactionがある場合はSummaryとfirstKeptEntryIdを使ってメッセージを再構築する
- Branch Summary、Custom MessageなどのEntryを対応するメッセージに変換する
- 現在のモデルセッションに必要なMessage Historyを構成する
SDKでAgent Sessionを作成するときも、まずsessionManager.buildSessionContext()を呼び出して旧セッションの有無を判定します。その結果に基づいて、既存のメッセージやモデル関連の状態を復元します。[3]
復元の流れはより次のようになります。
ここでのSessionManagerは、明確なProjectionとしての役割を担っています。ディスク上のEntry Treeが永続的な事実であり、Agent Runtimeが必要とするのは現在のブランチに対応する実行状態です。
なぜ1つのSessionファイルで木構造全体を保存できるのか
多くのシステムでは、Branchをそのまま新しいSessionとしてモデル化します。
Session A
↓ fork
Session B
Piが同じファイル内にTreeを保持する選択には、明確な製品上の理由があります。
Coding Agentのユーザーには、次のような操作がよく必要になります。
查看历史节点
回到某个节点
尝试另一个方案
再回到原分支
これらの操作のたびに新しいSessionを作ると大量のセッションファイルが生じ、ユーザーは複数のSessionの識別情報も管理する必要があります。
Treeモデルは、これらのパスを同じ作業履歴の別ルートとして扱います。
Session
└── Tree
├── branch A
├── branch B
└── branch C
ユーザーが実際に特定のブランチを独立させたいときは、fork / branched sessionを使って新しいSessionファイルを作成します。PiのSession Headerには、このSession間の関係を記録するparentSessionフィールドもあります。[1]
ここで2種類の分岐があります。
同一个 Session 内
Entry Tree Branch
跨 Session
Fork / parentSession
これらを区別すると、「履歴のナビゲーション」と「新しい長期的な作業単位の作成」を同じ操作として扱う混同を避けられます。
Treeモデルのコスト
Session Treeは軽量ですが、いくつかの制約も持ちます。
第一に、すべてのContext構築では先に現在のパスを確定する必要があります。「ファイル内の最後のN件のメッセージが現在の履歴だ」と仮定できなくなります。
第二に、Entryのparentチェーンは完全な状態を保たなければなりません。parentIdが1つでも壊れていれば、現在のブランチの復元に直接影響します。
第三に、Tool CallやTool Resultなど、プロトコル上のペアリングが要求されるノードを、任意のBranch Boundaryとして扱うことはできません。木構造ではどのノードへも戻れますが、すべてのノードがモデル履歴として妥当な終点になるわけではありません。
第四に、ブランチをまたぐ情報は、明示的に処理する必要があります。Branch Summaryは、古いブランチの有价值な情報が完全に失われるのを避けるためにあります。
第五に、Session Entryの意味範囲は次第に広がっています。Messageに加えて、Compaction、Branch Summary、設定変更などのEntryがあります。SessionManagerは、それらのEntryが現在のContextにどう影響するかを理解する必要があります。
これは完全なEvent Sourcingとは本質的に異なります。PiのEntryは主に「Coding AgentのセッションとモデルContextをどう復元するか」を中心に構成されており、実行中のすべてのStep、Chunk、Toolのライフサイクルイベントを記録しようとしているわけではありません。
Pi Sessionは「分岐可能な永続履歴」に近い
PiのSessionモデルは次の4層にまとめられます。
TreeもCompactionも、過去を直接書き換えるわけではありません。
Branchは現在のパスを変え、Compactionは現在のContextの解釈方法を変えます。古いEntryはそのまま残ります。
これによりPiには次の実用的な性質が得られます。
文件格式简单
历史可人工检查
分支不需要复制前缀
Compaction 不破坏原始历史
恢复逻辑集中在 SessionManager
同時に、Sessionの責務範囲も明確に限定されています。Pi Sessionが主に保存するのは、会話の文脈を再構築できるEntryです。Runtimeレベルの完全な実行履歴が、この木構造にすべて入るわけではありません。
次回のDeepSeek Harnessは別の境界線を選びます。Turn、Step、Raw Stream Chunk、Tool Call、Tool Result、Request Headerをすべてappend-onlyのSessionEvent Logへ格納し、Surfaceからモデル履歴を投影します。するとSessionは「現在のセッションをどう復元するか」だけを担う存在ではなく、Replay、UI、Persistence、リクエスト再構築を支える統一的な事実ソースになります。
参考資料
[1] Pi Session File Format: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/session-format.md
[2] Pi Compaction: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/compaction.md
[3] Pi SDK Session Restore: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/sdk.ts
[4] Pi SDK Session API: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/sdk.md