第四章:会话管理与上下文压缩

对应源文件:

  • packages/coding-agent/src/core/session-manager.ts
  • packages/coding-agent/src/core/compaction/compaction.ts

会话管理

存储格式:JSONL + 树形结构

每个会话是一个 .jsonl 文件。每行一条 JSON 记录:

{"type":"session","version":3,"id":"0192...","timestamp":"2024-01-15T10:30:00Z","cwd":"/project"}
{"type":"message","id":"a1b2c3d4","parentId":null,"message":{"role":"user","content":"hello",...},"timestamp":"..."}
{"type":"message","id":"e5f6g7h8","parentId":"a1b2c3d4","message":{"role":"assistant","content":[...],...},"timestamp":"..."}
{"type":"thinking_level_change","id":"i9j0k1l2","parentId":"e5f6g7h8","thinkingLevel":"high","timestamp":"..."}

为什么是树形结构

传统聊天记录是线性的:A → B → C → D → E。

Pi 支持分支。你可以从 C 点创建一个新分支:

A → B → C → D → E          (主分支)
         └→ F → G → H      (新分支)

每条记录通过 idparentId 建立父子关系。最后一条记录叫 leaf(叶子节点)。从 leaf 回溯到根节点,就是当前对话的完整路径。

记录类型

类型说明
session文件头部,包含版本号、ID、cwd
message消息(user/assistant/toolResult/bashExecution/custom/...)
thinking_level_change思考级别变更记录
model_change模型切换记录
compaction压缩摘要(包含被压缩消息的 summary)
branch_summary分支切换时的摘要
custom扩展自定义数据(不参与 LLM 上下文)
custom_message扩展自定义消息(参与 LLM 上下文)
label用户定义的书签/标签
session_info会话元数据(如显示名称)

buildSessionContext -- 从树构建 LLM 上下文

这是最关键的函数:给定一棵记录树和一个 leaf ID,构建 LLM 需要的消息列表。

步骤:
1. 从 leaf 回溯到根,得到路径 [root, ..., leaf]
2. 沿途收集 settings(thinking_level、model)
3. 找到最新的 compaction 记录(如果有)
4. 构建消息列表:
   - 如果有 compaction:
     a. 先放 compaction summary(作为用户消息)
     b. 再放 firstKeptEntryId 到 compaction 之间的保留消息
     c. 再放 compaction 之后的所有消息
   - 如果没有 compaction:
     a. 直接放所有消息

SessionManager 类

SessionManager 管理文件 I/O 和树形操作:

创建方式说明
SessionManager.create(cwd)新建会话
SessionManager.open(path)打开已有会话文件
SessionManager.continueRecent(cwd)继续最近的会话
SessionManager.inMemory()内存中不持久化
SessionManager.forkFrom(path, cwd)从已有会话 fork

核心操作:

方法说明
append(entry)追加记录(自动设 id 和 parentId)
setLeaf(id)导航到指定记录(改变 leaf 指针)
buildSessionContext()构建当前路径的 LLM 上下文
getTree()获取完整的树结构
getBranch(id)获取从根到指定节点的路径

文件是 append-only(只追加)。setLeaf 不会删除任何记录,只是改变"当前位置"。历史记录永远保留。

数据迁移

session 文件有版本号。当格式升级时,加载时自动迁移:

  • v1 → v2:添加 id/parentId 树结构(旧版是线性的)
  • v2 → v3:重命名 hookMessage 角色为 custom

迁移是原地进行的:加载时检测版本,修改内存中的数据,重写文件。


上下文压缩(Compaction)

为什么需要

LLM 有 context window 限制(比如 Claude 的 200K tokens)。长对话会超出限制,导致 API 调用失败。

什么时候触发

shouldCompact(contextTokens, contextWindow, settings) {
  return contextTokens > contextWindow - reserveTokens;
}

默认 reserveTokens = 16384。也就是说,当已用 token 数超过 contextWindow - 16384 时触发。

压缩流程

1. 准备阶段(prepareCompaction)
   ├── 找到上一次压缩记录(如果有)
   ├── 估算当前 token 数
   ├── 找到切割点(保留最近 ~20000 tokens 的消息)
   ├── 分出两部分:
   │   ├── messagesToSummarize(旧消息,将被摘要替代)
   │   └── keptMessages(新消息,保持不变)
   └── 提取文件操作记录(read/edit/write 过哪些文件)

2. 生成摘要(compact / generateSummary)
   ├── 把旧消息序列化为文本
   ├── 如果有上一次的摘要 → 用"更新摘要"的 prompt
   ├── 如果没有 → 用"初始摘要"的 prompt
   ├── 调用 LLM 生成结构化摘要:
   │   ## Goal
   │   ## Constraints & Preferences
   │   ## Progress
   │   ## Key Decisions
   │   ## Next Steps
   │   ## Critical Context
   └── 附加文件操作列表(哪些文件读过/改过)

3. 保存(session-manager 处理)
   ├── 创建 CompactionEntry
   │   ├── summary: 生成的摘要文本
   │   ├── firstKeptEntryId: 保留部分的第一条记录 ID
   │   ├── tokensBefore: 压缩前的 token 数
   │   └── details: { readFiles, modifiedFiles }
   └── 追加到 session 文件

4. 重建上下文
   ├── buildSessionContext 检测到 compaction 记录
   ├── 用 summary 替代旧消息
   └── 上下文大幅缩小

切割点算法

从最新消息往前累计 token 数
当累计超过 keepRecentTokens (20000) 时,找到最近的合法切割点

合法切割点:user 消息、assistant 消息、custom 消息
不合法切割点:toolResult 消息(必须跟在 tool call 之后)

特殊情况 - 分裂 turn:
如果切割点在一个 turn 的中间(比如在某个 tool result 之后、
下一个 assistant 消息之前),需要对 turn 的"前半段"也生成
一个摘要,叫做 "turn prefix summary"。

增量压缩

如果之前已经压缩过,新的压缩使用增量更新 prompt:

"这是新的对话消息,请更新之前的摘要。
 保留所有旧信息,添加新进展,
 把完成的任务从 In Progress 移到 Done..."

这比每次重新摘要整个对话要高效得多。


系统提示构建

system-prompt.ts 负责动态组装 system prompt:

buildSystemPrompt({
  selectedTools: ["read", "bash", "edit", "write"],
  toolSnippets: { read: "Read file contents", ... },
  promptGuidelines: ["Use read instead of cat"],
  contextFiles: [{ path: "AGENTS.md", content: "..." }],
  skills: [{ name: "code-review", ... }],
  cwd: "/project",
})

输出结构:

You are an expert coding assistant...

Available tools:
- read: Read file contents
- bash: Run shell commands
- edit: Edit file contents
- write: Write file contents

Guidelines:
- Use read to examine files instead of cat or sed
- Be concise in your responses
- Show file paths clearly when working with files

Pi documentation...

# Project Context
## AGENTS.md
(项目级规则的内容)

# Skills
(可用的 skill 指令)

Current date: 2024-01-15
Current working directory: /project

如果用户通过 --system-prompt 提供了自定义 prompt,则替代默认 prompt(但仍然附加 context files 和 skills)。


小结

会话管理
├── JSONL 文件格式(每行一条记录)
├── 树形结构(id + parentId)
│   ├── 支持分支和导航
│   └── append-only 不丢失历史
├── buildSessionContext → 从树构建 LLM 上下文
└── 版本迁移(v1 → v2 → v3)

上下文压缩
├── 触发条件:超过 contextWindow - reserveTokens
├── 切割策略:保留最近 ~20000 tokens
├── 摘要生成:结构化 LLM 摘要 + 文件操作列表
├── 增量更新:基于上次摘要追加修改
└── 存储:CompactionEntry 追加到 session 文件

系统提示
├── 动态组装:根据启用的工具生成
├── 上下文文件:.pi/ 或 AGENTS.md
├── Skills:可复用的指令片段
└── 可自定义:--system-prompt 覆盖