會話文件格式
會話儲存為 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",但該值是為流事件中的部分訊息保留的。在 pi 保留助手訊息之前,終端 done/error 訊息將其替換為完成原因,因此 "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}}模型變更條目
當使用者在會話中切換模型時發出。
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}思維層次改變入口
當使用者改變思維/推理層次時發出。
{"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(如果由擴充產生),false/undefined(如果由 pi 產生)(舊欄位名稱)firstKeptEntryId:為了與舊的條目格式相容。
分支摘要條目
當透過 /tree 切換分支時創建,並使用 LLM 產生的左分支到共同祖先的摘要。從廢棄的路徑捕獲上下文。
{"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(如果由擴充產生),false/undefined(如果由 pi 產生)(舊欄位名稱)
自訂條目
擴展狀態持久性。不參與法學碩士背景。
{"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()- 將葉子重設為空(在任何條目之前)branchWithSummary(entryId, summary, details?, fromHook?)- 帶有上下文摘要的分支
實例方法 - 上下文和訊息
buildContextEntries()- 取得應用壓縮的活動分支條目buildSessionContext()- 取得LLM的訊息、思考層次和模型getEntries()- 所有條目(不包括標題)getHeader()- 會話標頭元數據getSessionName()- 從最新的 session_info 條目取得顯示名稱getCwd()- 工作目錄getSessionDir()- 會話儲存目錄getSessionId()- 會話 UUIDgetSessionFile()- 會話檔案路徑(記憶體中未定義)isPersisted()- 會話是否儲存到磁碟