Session:JSONL 文件与树形结构
Pi 把「一次会话」抽象成一颗 JSONL 树。每条 entry 是一个节点,节点之间靠
id和parentId串成父子关系;当前活跃的位置是「叶子」。这个数据 结构撑起了分支、回滚、/tree导航、压缩摘要等所有能力。
存储位置与会话命令
会话自动存到 ~/.pi/agent/sessions/,按工作目录分组:
~/.pi/agent/sessions/
--Users-kimi-Projects-mysite--/
2026-06-30T10-23-45_a1b2c3d4.jsonl
2026-06-30T14-08-12_e5f6g7h8.jsonl
每个 .jsonl 文件就是一次完整的 session(包含它的分支树)。最常用的 CLI flags:
| Flag | 行为 |
|---|---|
pi -c | 续接最近一次 session |
pi -r | 打开 picker 选择历史 session |
pi --no-session | 临时模式,不写盘 |
pi --name "..." | 启动时设置 display name |
pi --session <path|id> | 用指定文件或 partial id |
pi --fork <path|id> | 从指定 session fork |
交互模式里也有 /resume、/new、/name、/session、/tree、/fork、/clone、/compact、/export、/share 等命令。/share 会把 session 上传到私有 GitHub gist 并生成可分享的 HTML 链接——这是 Pi 鼓励公开 session 的一个具体机制(社区有 badlogic/pi-share-hf 工具一键发到 Hugging Face)。
版本演进
Session 文件格式经历过三代:
| 版本 | 形态 | 说明 |
|---|---|---|
| v1 | 线性 entry 序列 | legacy,老 session 自动迁移 |
| v2 | 树形结构(id / parentId) | 引入真正的分支 |
| v3 | hookMessage role 改名为 custom | 当前版本 |
加载时自动迁移——你手上 v1 的老 session 不需要手动转。
文件结构
每行是一个 JSON 对象,靠 type 字段区分;节点间靠 id / parentId 串成树。第一行是 header:
{"type":"session","version":3,"id":"uuid","timestamp":"...","cwd":"/path"}
后续行是各种 entry。
基础消息类型
Pi 的消息模型分两层:pi-ai 提供 LLM 协议层的基础类型;pi-coding-agent 在此之上扩展出 coding 场景需要的几种专用消息。
Base(pi-ai)
interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number;
}
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, output, cacheRead, cacheWrite, total: number };
}
Usage 字段已经把 cost 都算好了——cacheRead / cacheWrite 单独列项,方便优化 prompt 缓存命中率。
Extended(pi-coding-agent)
interface BashExecutionMessage {
role: "bashExecution";
command: string; output: string;
exitCode: number | undefined;
cancelled: boolean; truncated: boolean;
fullOutputPath?: string;
excludeFromContext?: boolean; // !! 前缀的命令 不进入 context
timestamp: number;
}
interface CustomMessage {
role: "custom"; // 旧名 hookMessage,v3 重命名
customType: string; // extension identifier
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;
}
type AgentMessage =
| UserMessage | AssistantMessage | ToolResultMessage
| BashExecutionMessage | CustomMessage
| BranchSummaryMessage | CompactionSummaryMessage;
注意 BashExecutionMessage 是 Pi 特有的——Claude Code 把 bash 执行结果当成普通的 tool result,Pi 单独建了一种消息类型。这让 !command 和 !!command(输出不进入 context)这种特殊行为可以被结构化地表达。
Entry 类型
JSONL 文件里实际存的 entry(每行一个 JSON),除了上面那些 message,还有几种控制型 entry:
| Entry | 用途 |
|---|---|
message | 一条对话消息(携带 message: AgentMessage) |
model_change | 用户切换模型 |
thinking_level_change | 推理等级变化 |
compaction | 上下文压缩摘要 |
branch_summary | 切换分支时摘要废弃的分支 |
custom | 扩展状态,不进 LLM context |
custom_message | 扩展消息,进入 LLM context |
label | 用户书签(label: undefined 清除) |
session_info | session display name |
custom 和 custom_message 的区别是工程上非常重要的:前者只存在磁盘上供扩展恢复状态,后者会作为消息送给 LLM。扩展可以借此「给未来的自己留便条」而不污染 context。
树形结构示例
[user]─[assistant]─[user]─[assistant]─┬─[user] ← 当前 leaf
└─[branch_summary]─[user] ← 分支
每条 entry 有 id(8 字符 hex)和 parentId:
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"...","message":{"role":"user","content":"Hello"}}
「Leaf」就是当前会话活跃位置。所有 appendXxx 操作都会以当前 leaf 作为父节点;branch(entryId) 把 leaf 退回到更早的某个节点,让你从那里继续(产生新分支);resetLeaf() 把 leaf 退到 null,从空会话开始。
SessionManager API
SessionManager 是树操作的入口。静态工厂方法:
SessionManager.create(cwd, sessionDir?) // 新建
SessionManager.open(path, sessionDir?) // 打开已有
SessionManager.continueRecent(cwd, sessionDir?) // 续接最近或新建
SessionManager.inMemory(cwd?) // 纯内存,不持久化
SessionManager.list(cwd, sessionDir?, onProgress?)// 列出某个 cwd 的所有 session
SessionManager.listAll(onProgress?) // 列出全部
SessionManager.forkFrom(srcPath, targetCwd, ...) // 从别的项目 fork
实例上的「追加」方法全部返回新 entry 的 id:
sessionManager.appendMessage(message)
sessionManager.appendThinkingLevelChange(level)
sessionManager.appendModelChange(provider, modelId)
sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)
sessionManager.appendCustomEntry(customType, data?) // 不进 context
sessionManager.appendCustomMessageEntry(customType, content, display, details?) // 进 context
sessionManager.appendLabelChange(targetId, label) // 设置/清除书签
sessionManager.appendSessionInfo(name)
树操作:
sessionManager.getLeafId()
sessionManager.getLeafEntry()
sessionManager.getEntry(id)
sessionManager.getBranch(fromId?) // 从某节点走到 root
sessionManager.getTree()
sessionManager.getChildren(parentId)
sessionManager.getLabel(id)
sessionManager.branch(entryId) // 把 leaf 退到 entryId
sessionManager.resetLeaf()
sessionManager.branchWithSummary(entryId, summary, details?, fromHook?) // 退到 entryId 并插入 branch summary
buildSessionContext() 是最关键的「把树转回线性消息流」的方法:它从当前 leaf 往 root 走,收集路径上的 entries,遇到 CompactionEntry 时把 summary 替换掉被压缩的旧消息,最终输出 LLM 需要的 { messages, thinkingLevel, model }。