セッションファイル形式
セッションは JSONL (JSON 行) ファイルとして保存されます。各行は、type フィールドを持つ JSON オブジェクトです。セッション エントリは、id/parentId フィールドを介してツリー構造を形成し、新しいファイルを作成せずにインプレース分岐を可能にします。
ファイルの場所
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonlここで、<path> は、/ を - に置き換えた作業ディレクトリです。
セッションの削除
セッションは、~/.pi/agent/sessions/ にある .jsonl ファイルを削除することで削除できます。
Pi は、/resume から対話的にセッションを削除することもサポートしています (セッションを選択し、Ctrl+D を押して確認します)。利用可能な場合、pi は永久的な削除を避けるために trash CLI を使用します。
セッションバージョン
セッションのヘッダーにはバージョン フィールドがあります。
- バージョン 1: 線形エントリ シーケンス (レガシー、負荷時に自動移行)
- バージョン 2:
id/parentIdリンクを備えたツリー構造 - バージョン 3:
hookMessageロールの名前がcustomに変更されました (拡張機能の統合)
既存のセッションは、ロード時に現在のバージョン (v3) に自動的に移行されます。
ソースファイル
GitHub (pi-mono) のソース:
packages/coding-agent/src/core/session-manager.ts- セッション エントリ タイプと SessionManagerpackages/coding-agent/src/core/messages.ts- 拡張メッセージ タイプ (BashExecutionMessage、CustomMessage など)packages/ai/src/types.ts- 基本メッセージ タイプ (UserMessage、AssistantMessage、ToolResultMessage)packages/agent/src/types.ts- AgentMessage 共用体タイプ
プロジェクト内の TypeScript 定義については、node_modules/@earendil-works/pi-coding-agent/dist/ と node_modules/@earendil-works/pi-ai/dist/ を調べてください。
メッセージの種類
セッションエントリには AgentMessage オブジェクトが含まれています。これらの型を理解することは、セッションを解析したり拡張機能を作成したりするために不可欠です。
コンテンツブロック
メッセージには、型指定されたコンテンツ ブロックの配列が含まれます。
interface TextContent {
type: "text";
text: string;
}
interface ImageContent {
type: "image";
data: string; // base64 encoded
mimeType: string; // e.g., "image/jpeg", "image/png"
}
interface ThinkingContent {
type: "thinking";
thinking: string;
}
interface ToolCall {
type: "toolCall";
id: string;
name: string;
arguments: Record<string, any>;
}基本メッセージ タイプ (pi-ai より)
interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number; // Unix ms
}
interface AssistantMessage {
role: "assistant";
content: (TextContent | ThinkingContent | ToolCall)[];
api: string;
provider: string;
model: string;
usage: Usage;
stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
errorMessage?: string;
timestamp: number;
}
interface ToolResultMessage {
role: "toolResult";
toolCallId: string;
toolName: string;
content: (TextContent | ImageContent)[];
details?: any; // Tool-specific metadata
usage?: Usage; // Nested LLM work performed by the tool
isError: boolean;
timestamp: number;
}
interface Usage {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
cost: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
total: number;
};
}エクスポートされた pi-ai StopReason タイプには "pending" も含まれていますが、その値はストリーミング イベントの部分メッセージ用に予約されています。ターミナル done/error メッセージは、pi がアシスタント メッセージを永続化する前に完了理由に置き換えられるため、"pending" はセッション JSONL に表示されるべきではありません。
拡張メッセージ タイプ (pi-coding-agent から)
interface BashExecutionMessage {
role: "bashExecution";
command: string;
output: string;
exitCode: number | undefined;
cancelled: boolean;
truncated: boolean;
fullOutputPath?: string;
excludeFromContext?: boolean; // true for !! prefix commands
timestamp: number;
}
interface CustomMessage {
role: "custom";
customType: string; // Extension identifier
content: string | (TextContent | ImageContent)[];
display: boolean; // Show in TUI
details?: any; // Extension-specific metadata
timestamp: number;
}
interface BranchSummaryMessage {
role: "branchSummary";
summary: string;
fromId: string; // Entry we branched from
timestamp: number;
}
interface CompactionSummaryMessage {
role: "compactionSummary";
summary: string;
tokensBefore: number;
timestamp: number;
}エージェントメッセージユニオン
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;エントリーベース
すべてのエントリ (SessionHeader を除く) は SessionEntryBase を拡張します。
interface SessionEntryBase {
type: string;
id: string; // 8-char hex ID
parentId: string | null; // Parent entry ID (null for first entry)
timestamp: string; // ISO timestamp
}エントリの種類
セッションヘッダー
ファイルの最初の行。メタデータのみ。ツリーの一部ではありません (id/parentId なし)。
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}親とのセッションの場合 (/fork、/clone、または newSession({ parentSession }) で作成):
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}セッションメッセージエントリ
会話中のメッセージ。 message フィールドには AgentMessage が含まれています。
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}ModelChangeEntry
ユーザーがセッション中にモデルを切り替えると発生します。
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}ThinkingLevelChangeEntry
ユーザーが思考/推論レベルを変更したときに発生します。
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}圧縮エントリ
コンテキストが圧縮されたときに作成されます。以前のメッセージの概要を保存します。
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}新しいハーネス生成の圧縮では、保持された圧縮後のコンテキストが、firstKeptEntryId ではなくエントリに直接埋め込まれます。
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","tokensBefore":50000,"retainedTail":[{"role":"user","content":"latest request"},{"role":"assistant","content":[{"type":"text","text":"latest reply"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}]}オプションのフィールド:
usage: 概要の生成による LLM の使用。セッショントークンとコストの合計に含まれますretainedTail: 圧縮後に実体化されたAgentMessage[]が保持されます。これは、古いセッションとの下位互換性のためのみのオプションです。新しいハーネスによって生成された圧縮にはこれが含まれているため、圧縮エントリの前に古いエントリをたどることなく、このチェックポイントからコンテキストを再構築できます。details: 実装固有のデータ (例: デフォルトの場合は{ readFiles: string[], modifiedFiles: string[] }、拡張機能の場合はカスタム データ)fromHook: 拡張機能によって生成された場合はtrue、pi で生成された場合はfalse/undefined(従来のフィールド名)firstKeptEntryId: 古いエントリ形式との互換性のため。
ブランチ概要エントリ
共通の祖先までの左ブランチの LLM 生成サマリーを使用して、/tree を介してブランチを切り替えるときに作成されます。放棄されたパスからコンテキストをキャプチャします。
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}オプションのフィールド:
usage: 概要の生成による LLM の使用。セッショントークンとコストの合計に含まれますdetails: デフォルトのファイル追跡データ ({ readFiles: string[], modifiedFiles: string[] })、または拡張子のカスタム データfromHook: 拡張機能によって生成された場合はtrue、pi で生成された場合はfalse/undefined(従来のフィールド名)
カスタムエントリー
拡張機能の状態の永続性。 LLM コンテキストには参加しません。
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}リロード時に拡張機能のエントリを識別するには、customType を使用します。インタラクティブ モードでは、pi.registerEntryRenderer(customType, renderer) を介してカスタム エントリをレンダリングできますが、それでも LLM コンテキストには参加しません。
カスタムメッセージエントリ
LLM コンテキストに参加する、拡張機能によって挿入されたメッセージ。
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}フィールド:
content: 文字列または(TextContent | ImageContent)[](UserMessage と同じ)display:true= 個別のスタイルで TUI に表示、false= 非表示details: オプションの拡張機能固有のメタデータ (LLM には送信されません)
ラベルエントリ
エントリ上のユーザー定義のブックマーク/マーカー。
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}ラベルをクリアするには、label を undefined に設定します。
セッション情報エントリ
セッションのメタデータ (ユーザー定義の表示名など)。拡張機能の/name、--name / -n、または pi.setSessionName() で設定します。
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}セッション名は、設定すると最初のメッセージの代わりにセッション セレクター (/resume) に表示されます。
ツリー構造
エントリはツリーを形成します。
- 最初のエントリには
parentId: nullが付いています - 後続の各エントリは、
parentIdを介してその親を指します。 - 分岐すると、以前のエントリから新しい子が作成されます
- 「リーフ」はツリー内の現在の位置です。
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branchコンテキストの構築
buildContextEntries() は現在のリーフからルートまで歩き、圧縮を尊重しながらアクティブなエントリ リストを生成します。
- パス上のすべてのエントリを収集します
CompactionEntryがパス上にある場合:- 最初に圧縮エントリを含めます
retainedTailが存在する場合、自己完結型チェックポイントとして機能し、圧縮後のエントリが含まれます。- それ以外の場合は、
firstKeptEntryIdから圧縮までのエントリが含まれます - 圧縮後のエントリが含まれます
- 選択した範囲内の非メッセージ エントリを保持して、対話モードでレンダリングできるようにします。
buildSessionContext() は、そのエントリ リストに基づいて、LLM のメッセージ リストを生成します。
- フルパスから現在のモデルと思考レベルの設定を抽出します。
- 選択したエントリをメッセージに変換します。
message-> 保存AgentMessagecompaction->compactionSummaryプラスretainedTail(存在する場合)branch_summary->branchSummarycustom_message->CustomMessagecustom-> コンテキストメッセージなし
これにより、新しい圧縮が自己完結型チェックポイントのように機能します。 retainedTail は、firstKeptEntryId のみを保存する古いセッションが引き続き正しく読み込まれるようにするためのオプションです。
解析例
import { readFileSync } from "fs";
const lines = readFileSync("session.jsonl", "utf8").trim().split("\n");
for (const line of lines) {
const entry = JSON.parse(line);
switch (entry.type) {
case "session":
console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
break;
case "message":
console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
break;
case "compaction":
console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
break;
case "branch_summary":
console.log(`[${entry.id}] Branch from ${entry.fromId}`);
break;
case "custom":
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
break;
case "custom_message":
console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
break;
case "label":
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
break;
case "model_change":
console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
break;
case "thinking_level_change":
console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
break;
}
}セッションマネージャー API
プログラムでセッションを操作するための主要なメソッド。
静的作成方法
SessionManager.create(cwd, sessionDir?)- 新しいセッションSessionManager.open(path, sessionDir?)- 既存のセッション ファイルを開きますSessionManager.continueRecent(cwd, sessionDir?)- 最新のものを継続するか、新しく作成しますSessionManager.inMemory(cwd?)- ファイルの永続性なしSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- 別のプロジェクトからセッションをフォークする
静的リスト方法
SessionManager.list(cwd, sessionDir?, onProgress?)- ディレクトリのセッションをリストします。SessionManager.listAll(onProgress?)- すべてのプロジェクトにわたるすべてのセッションをリストします。
インスタンスメソッド - セッション管理
newSession(options?)- 新しいセッションを開始します (オプション:{ parentSession?: string })setSessionFile(path)- 別のセッション ファイルに切り替えますcreateBranchedSession(leafId)- ブランチを新しいセッション ファイルに抽出します
インスタンス メソッド - 追加 (すべてエントリ ID を返します)
appendMessage(message)- メッセージを追加appendThinkingLevelChange(level)- 思考の変化を記録するappendModelChange(provider, modelId)- 機種変更の記録appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)- 圧縮を追加しますappendCustomEntry(customType, data?)- 拡張状態 (コンテキスト内ではない)appendSessionInfo(name)- セッション表示名を設定しますappendCustomMessageEntry(customType, content, display, details?)- 拡張メッセージ (コンテキスト内)appendLabelChange(targetId, label)- ラベルの設定/クリア
インスタンス メソッド - ツリー ナビゲーション
getLeafId()- 現在の位置getLeafEntry()- 現在のリーフ エントリを取得しますgetEntry(id)- ID によるエントリーの取得getBranch(fromId?)- 入り口から根まで歩きますgetTree()- 完全なツリー構造を取得するgetChildren(parentId)- 直接の子を取得するgetLabel(id)- エントリ用のラベルを取得しますbranch(entryId)- リーフを前のエントリに移動しますresetLeaf()- リーフを null にリセットします (エントリの前)branchWithSummary(entryId, summary, details?, fromHook?)- コンテキストの概要を含む分岐
インスタンス メソッド - コンテキストと情報
buildContextEntries()- 圧縮が適用されたアクティブなブランチ エントリを取得しますbuildSessionContext()- LLM のメッセージ、思考レベル、モデルを取得しますgetEntries()- すべてのエントリ (ヘッダーを除く)getHeader()- セッションヘッダーのメタデータgetSessionName()- 最新の session_info エントリから表示名を取得しますgetCwd()- 作業ディレクトリgetSessionDir()- セッション保存ディレクトリgetSessionId()- セッション UUIDgetSessionFile()- セッション ファイル パス (メモリ内では未定義)isPersisted()- セッションがディスクに保存されるかどうか