会话文件格式
会话存储为 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;
}AgentMessage 联合类型
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
}条目类型
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"}会话消息条目
对话中的一条消息。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"}ThinkingLevelChangeEntry
当用户改变 thinking/reasoning level 时发出。
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}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[]。该字段仅为兼容旧会话而可选。较新的 harness 生成的压缩会包含它,因此可以从该检查点重建上下文,而无需遍历压缩条目之前的旧条目。details:特定于实现的数据(例如默认实现使用{ readFiles: string[], modifiedFiles: string[] },扩展也可以使用自定义数据)fromHook:如果由扩展生成则为true;如果由 pi 生成则为false/undefined(旧字段名)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 usage;包含在会话 token 和成本总计中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 的消息列表:
- 从完整路径中提取当前模型和 thinking level 设置
- 将选定的条目转换为消息:
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
以编程方式处理会话的关键方法。
静态创建方法
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)- 记录 thinking 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 的消息、thinkingLevel 和模型getEntries()- 所有条目(不包括标题)getHeader()- 会话头信息元数据getSessionName()- 从最新的 session_info 条目获取显示名称getCwd()- 工作目录getSessionDir()- 会话存储目录getSessionId()- 会话 UUIDgetSessionFile()- 会话文件路径(内存中未定义)isPersisted()- 会话是否保存到磁盘