最近在阅读Pi Agent的源码,分析其会话保存的机制
我们知道Pi Agent支持从当前对话切出一条分支来进行
为了满足这个需求,会话保存主要包含下面几个要点
- 存储方式:JSONL形式存储
- 会话加载:历史对化参与上下文构建,其采用树形
JSONL
目前大多数Agent App(Codex、Claude、Qwen Code ….)本地存储使用的都是JSONL格式存储会话历史
主要得益于如下几大特点:
- JSONL每一行都能单独进行解析,解析失败只影响当前对话的记录,不影响后续加载
- 日志友好,兼容日志审计系统
- 写入时纯追加,不需要重写整个JSON文件
会话管理
为了满足树形结构的要求,会话条目(SessionEntry)需要满足下面基本结构
export interface SessionEntry {
type: "message";
id: string;
parentId: string | null;
messages: Message
timestamp: string;
}
在初始化阶段,将由会话管理器(SessionManager)从JSONL文件中加载到SessionEntry[]中,同时维护一个id到SessionEntry对象的Map。
通过buildSessionContext()方法,从叶子节点沿着parentId回溯到根节点,并构建Path
class SessionManager {
private entries: SessionEntry[] = [];
private byId = new Map<string, SessionEntry>();
private leafId: string | null = null; // 当前对话id作为叶子
buildSessionContext() : SessionEntry[] {
// 构建上下文时只取 leaf 到 root 的路径
const path: SessionEntry[] = [];
let current = leafId ? this.byId.get(leafId) : undefined;
while (current) {
path.unshift(current);
current = current.parentId ? this.byId.get(current.parentId) : undefined;
}
return path;
}
}
上下文压缩
实际上SessionEntry的type不只是”message”类型,它可以是多种类型
在pi的定义中SessionEntry是一个集合类型
export type SessionEntry =
| SessionMessageEntry
| ThinkingLevelChangeEntry
| ModelChangeEntry
| UsageEntry
| CompactionEntry
| BranchSummaryEntry
| CustomEntry
| CustomMessageEntry
| ContextEditEntry
| LabelEntry
| SessionInfoEntry;
message:实际对话消息。user、assistant、system、toolResult 都放在它的message字段里。model_change、thinking_level_change、usage:记录模型、思考级别和用量状态。custom、custom_message:分别保存扩展状态、扩展注入的上下文消息。label、session_info:保存标签和 session 名称。context_edit:追加一条对某条历史消息的上下文覆盖规则。compaction:保存压缩摘要和保留边界。branch_summary:保存从另一条分支返回时使用的摘要。
因此我们可能在一个JSONL文件中看到如下的信息
{"type":"session","version":3,"id":"s1","timestamp":"2026-10-30T09:00:00.000Z","cwd":"D:/demo"}
{"type":"model_change","id":"e1","parentId":null,"timestamp":"2026-10-30T09:00:01.000Z","provider":"deepseek","modelId":"deepseek-flash"}
{"type":"thinking_level_change","id":"e2","parentId":"e1","timestamp":"2026-10-30T09:00:02.000Z","thinkingLevel":"off"}
{"type":"message","id":"e3","parentId":"e2","timestamp":"2026-10-30T09:00:03.000Z","message":{"role":"system","content":"你是一个编程助手。","timestamp":1793350803000}}
{"type":"message","id":"e4","parentId":"e3","timestamp":"2026-10-30T09:00:04.000Z","message":{"role":"user","content":"读取配置文件","timestamp":1793350804000}}
{"type":"message","id":"e5","parentId":"e4","timestamp":"2026-10-30T09:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"我读取了配置文件。"}],"api":"openai-completions","provider":"deepseek","model":"deepseek-flash","usage":{"input":10,"output":5,"cacheRead":0,"cacheWrite":0,"totalTokens":15,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1793350805000}}
{"type":"usage","id":"e6","parentId":"e5","timestamp":"2026-10-30T09:00:06.000Z","kind":"request","provider":"deepseek","model":"deepseek-flash","usage":{"input":10,"output":5,"cacheRead":0,"cacheWrite":0,"totalTokens":15,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}}}
{"type":"custom","id":"e7","parentId":"e6","timestamp":"2026-10-30T09:00:07.000Z","customType":"my-extension-state","data":{"key":"value"}}
{"type":"custom_message","id":"e8","parentId":"e7","timestamp":"2026-10-30T09:00:08.000Z","customType":"note","content":"扩展注入的上下文","display":true,"details":{"source":"extension"}}
{"type":"label","id":"e9","parentId":"e8","timestamp":"2026-10-30T09:00:09.000Z","targetId":"e4","label":"读取配置"}
{"type":"session_info","id":"e10","parentId":"e9","timestamp":"2026-10-30T09:00:10.000Z","name":"配置排查"}
{"type":"context_edit","id":"e11","parentId":"e10","timestamp":"2026-10-30T09:00:11.000Z","targetId":"e4","replacement":{"content":"检查项目配置文件"}}
{"type":"compaction","id":"e12","parentId":"e11","timestamp":"2026-10-30T09:00:12.000Z","summary":"用户要求检查项目配置;助手已读取配置文件。","firstKeptEntryId":"e5","tokensBefore":12000,"systemMessage":{"role":"system","content":"你是一个编程助手。","timestamp":1793350812000}}
{"type":"branch_summary","id":"e13","parentId":"e12","timestamp":"2026-10-30T09:00:13.000Z","fromId":"e5","summary":"另一条分支中,助手分析了配置文件。"}
在这里我们主要关注CompactionEntry和SessionMessageEntry
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
type: "compaction";
summary: string;
/** 保留原始消息的起点 ID。这个节点之前的旧消息由 `summary` 代表;从这个节点开始的消息继续保留原文 */
firstKeptEntryId: string;
/** 压缩前上下文的 token 数估算,用于记录压缩效果和后续判断。 */
tokensBefore: number;
/** 生成此摘要的模型调用用量(如果有) */
usage?: Usage;
/** 不进入摘要文本,单独保存为 systemMessage 快照 */
systemMessage?: SystemMessage;
}
在发给LLM上下文的时候实际上包含了摘要的上下文,不包含原消息。
E1 → E2 → E3 → E4 → E5 → [compaction] → E6
↑ firstKeptEntryId
摘要:E1 → E2
保留原文:E3 → E4 → E5
上下文:compaction 摘要 → E3 → E4 → E5
在这里当AI在进行长任务时候,可能触发上下文压缩,此时为了让AI感知最新任务进展不被模糊,从而选择保留最近的几条历史消息,而first指针之前的内容会被摘要压缩
评论(0)
暂无评论