本页面是 Pi 官方文档 的中文翻译。仅供学习参考。
会话使用 JSONL(JSON Lines)格式。每行是一个带有 type 字段的 JSON 对象。条目通过 id/parentId 字段形成树状结构,支持原地分支,无需创建新文件。
文件位置
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl
其中 <path> 是工作目录,路径中的 / 替换为 -。
删除会话
删除 ~/.pi/agent/sessions/ 下的 .jsonl 文件即可移除会话。
Pi 也支持从 /resume 交互式删除会话(选择一个会话后按 Ctrl+D,然后确认)。如果可用,Pi 会使用 trash CLI 以避免永久删除。
会话版本
- Version 1:线性条目序列(旧格式,加载时自动迁移)
- Version 2:带
id/parentId 链接的树状结构
- Version 3:将
hookMessage 角色重命名为 custom(扩展统一)
现有会话在加载时自动迁移到当前版本(v3)。
源文件
GitHub 上的源代码(pi-mono):
关于 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)
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; // 工具特定的元数据
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-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 联合类型
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 时间戳
}
条目类型
文件的第一行。仅元数据,不属于树结构(没有 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
对话中的一条消息。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
当用户更改 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}
可选字段:
details:实现特定的数据(例如默认的 { readFiles: string[], modifiedFiles: string[] },或扩展的自定义数据)
fromHook:如果由扩展生成则为 true,如果由 Pi 生成则为 false/undefined(旧字段名)
BranchSummaryEntry
当通过 /tree 切换分支时创建,包含 LLM 生成的从左分支到公共祖先的摘要。捕获被放弃路径的上下文。
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}
可选字段:
details:文件追踪数据(默认的 { readFiles: string[], modifiedFiles: string[] },或扩展的自定义数据)
fromHook:如果由扩展生成则为 true,如果由 Pi 生成则为 false/undefined(旧字段名)
CustomEntry
扩展状态持久化。不参与 LLM 上下文。
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}
使用 customType 在重新加载时识别你的扩展条目。
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
用户定义的书签/标记。
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}
将 label 设为 undefined 以清除标记。
SessionInfoEntry
会话元数据(例如用户定义的显示名称)。通过 /name 命令或扩展中的 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] ← 当前叶节点
│
└─ [branch_summary] ─── [user msg] ← 替代分支
上下文构建
buildSessionContext() 从当前叶节点向根节点遍历,生成发送给 LLM 的消息列表:
- 收集路径上的所有条目
- 提取当前模型和 thinking level 设置
- 如果路径上存在
CompactionEntry:
- 首先输出摘要
- 然后是从
firstKeptEntryId 到压缩的消息
- 然后是压缩之后的消息
- 将
BranchSummaryEntry 和 CustomMessageEntry 转换为适当的消息格式
解析示例
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?) - 开始新会话(options:{ parentSession?: string })
setSessionFile(path) - 切换到不同的会话文件
createBranchedSession(leafId) - 将分支提取到新会话文件
实例方法 - 追加(全部返回条目 ID)
appendMessage(message) - 追加消息
appendThinkingLevelChange(level) - 记录 thinking 变更
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?) - 带上下文摘要的分支
实例方法 - 上下文和信息
buildSessionContext() - 获取 LLM 所需的消息、thinkingLevel 和模型
getEntries() - 所有条目(不包括头部)
getHeader() - 会话头部元数据
getSessionName() - 从最新的 session_info 条目获取显示名称
getCwd() - 工作目录
getSessionDir() - 会话存储目录
getSessionId() - 会话 UUID
getSessionFile() - 会话文件路径(内存模式为 undefined)
isPersisted() - 会话是否保存到磁盘
法律声明:本页面是 pi.dev 官方文档的中文翻译版本,仅供学习参考。本网站与 pi.dev 及 Earendil Inc. 无任何法律关系。