会话格式
会话以 JSONL(JSON Lines)文件形式存储。每一行是一个带 type 字段的 JSON 对象。会话条目(entry)通过 id/parentId 字段构成树形结构,从而实现原地分支,无需新建文件。
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl其中 <path> 是工作目录,/ 替换为 -。
删除 ~/.pi/agent/sessions/ 下对应的 .jsonl 文件即可移除会话。
Pi 也支持在 /resume 中交互式删除会话(选择会话后按 Ctrl+D,再确认)。可用时,pi 使用 trash 命令行工具,避免永久删除。
会话在头部有一个版本字段:
- 版本 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 编码 mimeType: string; // 例如 "image/jpeg"、"image/png"}
interface ThinkingContent { type: "thinking"; thinking: string;}
interface ToolCall { type: "toolCall"; id: string; name: string; arguments: Record<string, any>;}基础消息类型(来自 pi-ai)
Section titled “基础消息类型(来自 pi-ai)”interface UserMessage { role: "user"; content: string | (TextContent | ImageContent)[]; timestamp: number; // Unix 毫秒}
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; // 工具特定的元数据 usage?: Usage; // 工具执行的嵌套 LLM 工作 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)
Section titled “扩展消息类型(来自 pi-coding-agent)”interface BashExecutionMessage { role: "bashExecution"; command: string; output: string; exitCode: number | undefined; cancelled: boolean; truncated: boolean; fullOutputPath?: string; excludeFromContext?: boolean; // 对以 !! 开头的命令为 true timestamp: number;}
interface CustomMessage { role: "custom"; customType: string; // 扩展标识符 content: string | (TextContent | ImageContent)[]; display: boolean; // 在 TUI 中显示 details?: any; // 扩展特定的元数据 timestamp: number;}
interface BranchSummaryMessage { role: "branchSummary"; summary: string; fromId: string; // 我们分支自的条目 timestamp: number;}
interface CompactionSummaryMessage { role: "compactionSummary"; summary: string; tokensBefore: number; timestamp: number;}AgentMessage 联合类型
Section titled “AgentMessage 联合类型”type AgentMessage = | UserMessage | AssistantMessage | ToolResultMessage | BashExecutionMessage | CustomMessage | BranchSummaryMessage | CompactionSummaryMessage;所有条目(SessionHeader 除外)都继承自 SessionEntryBase:
interface SessionEntryBase { type: string; id: string; // 8 字符十六进制 ID parentId: string | null; // 父条目 ID(第一条为 null) timestamp: string; // ISO 时间戳}SessionHeader
Section titled “SessionHeader”文件的第一行。仅元数据,不参与树结构(无 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"}SessionMessageEntry
Section titled “SessionMessageEntry”对话中的一条消息。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
Section titled “ModelChangeEntry”用户会话中途切换模型时写入。
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}ThinkingLevelChangeEntry
Section titled “ThinkingLevelChangeEntry”用户更改思考/推理级别时写入。
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}CompactionEntry
Section titled “CompactionEntry”上下文被压缩时创建,存储早期消息的摘要。
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}较新的执行框架(harness)生成的压缩会在条目上直接嵌入压缩后保留的上下文,而不是 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 usage;计入会话 token 与费用总量retainedTail:压缩后保留的物化AgentMessage[]。仅为兼容旧会话而设为可选。较新的执行框架生成的压缩会包含它,从而可以从该检查点重建上下文,而无需遍历压缩条目之前的旧条目details:实现特定的数据(例如默认的{ readFiles: string[], modifiedFiles: string[] },或扩展的自定义数据)fromHook:若由扩展生成则为true,pi 生成则为false/undefined(遗留字段名)firstKeptEntryId:为兼容旧条目格式而保留
BranchSummaryEntry
Section titled “BranchSummaryEntry”通过 /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 usage;计入会话 token 与费用总量details:默认的文件追踪数据({ readFiles: string[], modifiedFiles: string[] }),或扩展的自定义数据fromHook:若由扩展生成则为true,pi 生成则为false/undefined(遗留字段名)
CustomEntry
Section titled “CustomEntry”扩展状态持久化。不参与 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 上下文。
CustomMessageEntry
Section titled “CustomMessageEntry”扩展注入的消息,参与 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)
LabelEntry
Section titled “LabelEntry”用户定义在条目上的书签/标记。
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}将 label 设为 undefined 可清除标签。
SessionInfoEntry
Section titled “SessionInfoEntry”会话元数据(例如用户定义的显示名称)。通过 /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指向其父条目 - 分支从较早的条目创建新子节点
- “叶节点”(leaf)是树中的当前位置
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf │ └─ [branch_summary] ─── [user msg] ← alternate branchbuildContextEntries() 从当前叶节点回溯到根,生成活动条目列表,同时遵循压缩逻辑:
- 收集路径上的所有条目
- 如果路径上有
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; }}SessionManager API
Section titled “SessionManager API”以编程方式处理会话的关键方法。
静态创建方法
Section titled “静态创建方法”SessionManager.create(cwd, sessionDir?)- 新建会话SessionManager.open(path, sessionDir?)- 打开现有会话文件SessionManager.continueRecent(cwd, sessionDir?)- 继续最近一次会话,或新建会话SessionManager.inMemory(cwd?)- 不持久化到文件SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- 从另一个项目分叉会话
静态列举方法
Section titled “静态列举方法”SessionManager.list(cwd, sessionDir?, onProgress?)- 列出某个目录的会话SessionManager.listAll(onProgress?)- 列出所有项目的全部会话
实例方法 - 会话管理
Section titled “实例方法 - 会话管理”newSession(options?)- 开始新会话(选项:{ parentSession?: string })setSessionFile(path)- 切换到不同的会话文件createBranchedSession(leafId)- 将分支提取到新的会话文件
实例方法 - 追加(均返回条目 ID)
Section titled “实例方法 - 追加(均返回条目 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)- 设置/清除标签
实例方法 - 树导航
Section titled “实例方法 - 树导航”getLeafId()- 当前位置getLeafEntry()- 获取当前叶节点条目getEntry(id)- 按 ID 获取条目getBranch(fromId?)- 从条目回溯到根getTree()- 获取完整树形结构getChildren(parentId)- 获取直接子节点getLabel(id)- 获取条目的标签branch(entryId)- 将叶节点移动到较早的条目resetLeaf()- 将叶节点重置为 null(任何条目之前)branchWithSummary(entryId, summary, details?, fromHook?)- 带上下文摘要分支
实例方法 - 上下文与信息
Section titled “实例方法 - 上下文与信息”buildContextEntries()- 获取应用压缩后的活动分支条目buildSessionContext()- 获取发给 LLM 的消息、思考级别与模型getEntries()- 所有条目(不含头部)getHeader()- 会话头部元数据getSessionName()- 从最新的 session_info 条目获取显示名称getCwd()- 工作目录getSessionDir()- 会话存储目录getSessionId()- 会话 UUIDgetSessionFile()- 会话文件路径(内存模式为 undefined)isPersisted()- 会话是否已保存到磁盘