24 会话文件格式
24.1 概述
Pi 的会话(Session)以 JSONL(JSON Lines)格式存储。每个会话文件中,每一行是一个独立的 JSON 对象,包含 type 字段用于标识条目类型。会话条目通过 id / parentId 字段构成树形结构,支持原地分支(branching)而无需创建新文件。
理解会话文件格式对于以下场景至关重要:
- 编写解析脚本,从会话文件中提取信息
- 开发自定义扩展(Extensions),注入或修改会话内容
- 通过 SDK 编程式操作会话
- 调试会话分支、压缩(compaction)等高级功能
如果你只是日常使用 Pi,不需要深入理解本章内容。本章面向需要编程式处理会话的开发者。
24.2 文件位置
会话文件存储在以下路径:
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl
其中 <path> 是当前工作目录路径,将 / 替换为 -。例如,工作目录 /home/user/myproject 对应的会话目录为 --home-user-myproject--。
这种命名方式确保不同项目的会话相互隔离,同时便于按项目查找历史会话。
24.3 删除会话
可以通过直接删除 ~/.pi/agent/sessions/ 下的 .jsonl 文件来移除会话。
Pi 还支持在交互模式下通过 /resume 删除会话:选中某个会话后按 Ctrl+D,然后确认删除。当系统中安装了 trash CLI 工具时,Pi 会优先使用它,将文件移入回收站而非永久删除。
直接删除 .jsonl 文件是永久性操作,无法恢复。建议使用 trash CLI 或通过 /resume 交互删除。
24.4 会话版本
会话头部(header)包含 version 字段,标识会话格式版本:
| 版本 | 说明 |
|---|---|
| v1 | 线性条目序列(旧版格式,加载时自动迁移) |
| v2 | 树形结构,使用 id / parentId 链接条目 |
| v3 | 将 hookMessage 角色重命名为 custom(扩展统一) |
旧版会话在加载时会自动迁移到当前版本(v3)。
24.5 消息类型
会话条目中包含 AgentMessage 对象。理解这些类型对于解析会话和编写扩展至关重要。
24.5.1 内容块(Content Blocks)
消息中包含类型化的内容块数组:
// 文本内容
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>;
}24.5.2 基础消息类型(来自 pi-ai)
这些类型定义在 @earendil-works/pi-ai 包中,是 Pi 最核心的消息类型:
// 用户消息
interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number; // Unix 毫秒时间戳
}
// 助手消息
interface AssistantMessage {
role: "assistant";
content: (TextContent | ThinkingContent | ToolCall)[];
api: string;
provider: string; // 提供商,如 "anthropic"
model: string; // 模型名,如 "claude-sonnet-4-5"
usage: Usage; // token 用量统计
stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
errorMessage?: string; // 错误时的详细信息
timestamp: number;
}
// 工具结果消息
interface ToolResultMessage {
role: "toolResult";
toolCallId: string; // 对应的 ToolCall ID
toolName: string; // 工具名称
content: (TextContent | ImageContent)[];
details?: any; // 工具特定的元数据
usage?: Usage; // 工具内部嵌套 LLM 调用的用量
isError: boolean; // 是否执行出错
timestamp: number;
}
// token 用量统计
interface Usage {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
cost: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
total: number;
};
}StopReason 类型还包含 "pending" 值,但该值仅用于流式事件中的部分消息。当 Pi 将助手消息持久化到会话文件时,"pending" 会被替换为实际的完成原因,因此会话 JSONL 中永远不会出现 "pending"。
24.5.3 扩展消息类型(来自 pi-coding-agent)
这些类型定义在 @earendil-works/pi-coding-agent 包中,是 Pi 在基础消息类型之上扩展的专有消息:
// Bash 命令执行记录
interface BashExecutionMessage {
role: "bashExecution";
command: string;
output: string;
exitCode: number | undefined;
cancelled: boolean; // 是否被用户取消
truncated: boolean; // 输出是否被截断
fullOutputPath?: string; // 完整输出的文件路径
excludeFromContext?: boolean; // true 表示 !! 前缀命令,不送入 LLM
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; // 分支起点的条目 ID
timestamp: number;
}
// 压缩摘要消息
interface CompactionSummaryMessage {
role: "compactionSummary";
summary: string; // 被压缩内容的摘要
tokensBefore: number; // 压缩前的 token 数
timestamp: number;
}24.5.4 AgentMessage 联合类型
所有消息类型组成 AgentMessage 联合类型:
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;编写扩展或解析器时,通过 role 字段判断消息类型即可。
24.6 条目基础结构(Entry Base)
所有条目(SessionHeader 除外)都继承自 SessionEntryBase:
interface SessionEntryBase {
type: string; // 条目类型标识
id: string; // 8 位十六进制 ID
parentId: string | null; // 父条目 ID(首条目为 null)
timestamp: string; // ISO 8601 时间戳
}id 和 parentId 构成了会话的树形结构。通过 parentId 可以追溯到根条目,形成从叶子到根的路径。
24.7 条目类型详解
24.7.1 SessionHeader(会话头)
会话文件的第一行,包含会话元数据。它不是树结构的一部分(没有 id / parentId)。
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}如果是通过 /fork、/clone 或 newSession({ parentSession }) 创建的会话,还会包含 parentSession 字段,指向原始会话文件路径:
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}24.7.2 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}}24.7.3 ModelChangeEntry(模型切换条目)
当用户在会话中途切换模型时生成:
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}24.7.4 ThinkingLevelChangeEntry(思考等级变更条目)
当用户改变思考/推理(thinking/reasoning)等级时生成:
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}24.7.5 CompactionEntry(压缩条目)
当上下文被压缩(compaction)时创建,存储早期消息的摘要:
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}较新的 Pi 生成的压缩条目会直接在条目上嵌入压缩后保留的上下文(retainedTail),而非仅依赖 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 用量统计;包含在会话 token 和费用总计中 |
retainedTail |
压缩后保留的 AgentMessage[];可选仅为向后兼容旧会话 |
details |
实现特定的数据(如 { readFiles: string[], modifiedFiles: string[] }) |
fromHook |
true 表示由扩展生成,false/undefined 表示 Pi 自动生成(旧字段名) |
firstKeptEntryId |
旧版格式兼容字段 |
retainedTail 使得新版压缩条目成为自包含的检查点——无需遍历更早的条目即可重建上下文。旧会话如果只有 firstKeptEntryId,仍然可以通过回溯旧条目正确加载。
24.7.6 BranchSummaryEntry(分支摘要条目)
通过 /tree 切换分支时创建,包含被放弃分支的 LLM 生成摘要,捕获离开路径的上下文:
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}可选字段与 CompactionEntry 类似:usage、details(文件追踪数据)、fromHook。
24.7.7 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) 为自定义条目注册 TUI 渲染器,但这些条目不会被发送到 LLM。
24.7.8 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) |
24.7.9 LabelEntry(标签条目)
用户在条目上定义的书签/标记:
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}将 label 设为 undefined 即可清除标签。标签在 /tree 视图中显示,便于快速定位重要节点。
24.7.10 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 会话选择器中将显示该名称而非首条消息预览。
24.8 树形结构
会话条目通过 id / parentId 构成树形结构:
- 首条目的
parentId为null - 每个后续条目通过
parentId指向其父条目 - 分支(branching) 从已有条目创建新的子节点
- 叶子节点(leaf) 是当前在树中的位置
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← 当前叶子
│
└─ [branch_summary] ─── [user msg] ← 替代分支
这种设计使得原地分支成为可能:无需创建新的会话文件,直接从历史某点继续对话即可形成新分支。通过 /tree 命令可以在树中自由导航。
24.9 上下文构建
Pi 使用两个函数从树形结构构建发送给 LLM 的上下文。
24.9.1 buildContextEntries()
从当前叶子节点向根节点遍历,生成活动条目列表,同时处理压缩逻辑:
- 收集路径条目:收集叶子上溯到根的所有条目
- 处理压缩条目:如果路径上存在
CompactionEntry:- 如果有
retainedTail:作为自包含检查点,直接使用其中保存的消息 - 否则:使用
firstKeptEntryId到压缩条目之间的条目
- 如果有
- 保留非消息条目:保留选中范围内的非消息条目(如
model_change、label等),以便交互模式正确渲染
24.9.2 buildSessionContext()
在 buildContextEntries() 的基础上,生成发送给 LLM 的最终消息列表:
- 从完整路径中提取当前的模型和思考等级设置
- 将选中条目转换为消息:
message→ 存储的AgentMessagecompaction→compactionSummary消息 +retainedTail(如果存在)branch_summary→branchSummary消息custom_message→CustomMessagecustom→ 不生成上下文消息
新版压缩条目通过 retainedTail 充当自包含检查点。这意味着即使不回溯到压缩点之前的条目,也能完整重建上下文。retainedTail 可选仅为向后兼容旧版会话。
24.10 解析示例
以下 TypeScript 示例展示如何解析会话 JSONL 文件:
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;
}
}24.11 SessionManager API
SessionManager 是 Pi 提供的用于编程式操作会话的核心类。以下是其完整接口。
24.11.1 静态创建方法
// 创建新会话
SessionManager.create(cwd, sessionDir?)
// 打开已有会话文件
SessionManager.open(path, sessionDir?)
// 继续最近的会话,如果没有则创建新会话
SessionManager.continueRecent(cwd, sessionDir?)
// 创建内存会话(不持久化到文件)
SessionManager.inMemory(cwd?)
// 从其他项目的会话派生(fork)
SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)24.11.2 静态列表方法
// 列出指定目录的会话
SessionManager.list(cwd, sessionDir?, onProgress?)
// 列出所有项目的会话
SessionManager.listAll(onProgress?)24.11.3 实例方法 — 会话管理
// 开始新会话(可选指定父会话)
newSession(options?: { parentSession?: string })
// 切换到其他会话文件
setSessionFile(path)
// 将当前分支提取到新会话文件
createBranchedSession(leafId)24.11.4 实例方法 — 追加条目(均返回条目 ID)
// 追加消息
appendMessage(message: AgentMessage): string
// 记录思考等级变更
appendThinkingLevelChange(level: string): string
// 记录模型切换
appendModelChange(provider: string, modelId: string): string
// 添加压缩条目
appendCompaction(
summary: string,
firstKeptEntryId: string,
tokensBefore: number,
details?: unknown,
fromHook?: boolean
): string
// 添加扩展状态条目(不参与 LLM 上下文)
appendCustomEntry(customType: string, data?: unknown): string
// 设置会话显示名称
appendSessionInfo(name: string): string
// 添加扩展消息条目(参与 LLM 上下文)
appendCustomMessageEntry(
customType: string,
content: string | (TextContent | ImageContent)[],
display: boolean,
details?: unknown
): string
// 设置/清除标签
appendLabelChange(targetId: string, label: string | undefined): string24.11.5 实例方法 — 树导航
// 获取当前位置(叶子 ID)
getLeafId(): string | null
// 获取当前叶子条目
getLeafEntry(): SessionEntry | null
// 按 ID 获取条目
getEntry(id: string): SessionEntry | undefined
// 从指定条目遍历到根
getBranch(fromId?: string): SessionEntry[]
// 获取完整树结构
getTree(): TreeNode[]
// 获取直接子节点
getChildren(parentId: string): SessionEntry[]
// 获取条目的标签
getLabel(id: string): string | undefined
// 跳转到更早的条目(移动叶子位置)
branch(entryId: string): void
// 重置叶子为 null(回到空对话)
resetLeaf(): void
// 带摘要的分支切换
branchWithSummary(
entryId: string,
summary: string,
details?: unknown,
fromHook?: boolean
): void24.11.6 实例方法 — 上下文与信息
// 获取活动分支条目(应用压缩后的)
buildContextEntries(): SessionEntry[]
// 获取发送给 LLM 的消息列表
buildSessionContext(): {
messages: AgentMessage[];
thinkingLevel: string;
model: { provider: string; modelId: string } | null;
}
// 获取所有条目(不含头部)
getEntries(): SessionEntry[]
// 获取会话头部元数据
getHeader(): SessionHeader
// 获取会话显示名称
getSessionName(): string | undefined
// 获取工作目录
getCwd(): string
// 获取会话存储目录
getSessionDir(): string
// 获取会话 UUID
getSessionId(): string
// 获取会话文件路径(内存会话返回 undefined)
getSessionFile(): string | undefined
// 是否持久化到磁盘
isPersisted(): boolean通过 SDK 使用 SessionManager 时,导入路径为 @earendil-works/pi-coding-agent。在你的项目 node_modules/@earendil-works/pi-coding-agent/dist/ 中可以查看完整的 TypeScript 类型定义。
24.12 源文件参考
相关源代码位于 pi-mono 仓库:
| 文件 | 说明 |
|---|---|
packages/coding-agent/src/core/session-manager.ts |
会话条目类型定义和 SessionManager 实现 |
packages/coding-agent/src/core/messages.ts |
扩展消息类型(BashExecutionMessage、CustomMessage 等) |
packages/ai/src/types.ts |
基础消息类型(UserMessage、AssistantMessage、ToolResultMessage) |
packages/agent/src/types.ts |
AgentMessage 联合类型 |