压缩与分支摘要
LLM 的上下文窗口有限。当对话变得过长时,pi 会使用压缩来摘要较早的内容,同时保留最近的工作。本页涵盖自动压缩和分支摘要。
源文件(pi-mono):
packages/coding-agent/src/core/compaction/compaction.ts- 自动压缩逻辑packages/coding-agent/src/core/compaction/branch-summarization.ts- 分支摘要packages/coding-agent/src/core/compaction/utils.ts- 共享工具(文件跟踪、序列化)packages/coding-agent/src/core/session-manager.ts- 条目类型(CompactionEntry、BranchSummaryEntry)packages/coding-agent/src/core/extensions/types.ts- 扩展事件类型
要在项目中查看 TypeScript 定义,请检查 node_modules/@earendil-works/pi-coding-agent/dist/。
Pi 有两种摘要机制:
| 机制 | 触发条件 | 用途 |
|---|---|---|
| 压缩 | 上下文超过阈值,或 /compact | 摘要旧消息以释放上下文 |
| 分支摘要 | /tree 导航 | 切换分支时保留上下文 |
两者使用相同的结构化摘要格式,并累积跟踪文件操作。
自动压缩在以下情况触发:
contextTokens > contextWindow - reserveTokens默认情况下,reserveTokens 为 16384 个 token(可在 ~/.pi/agent/settings.json 或 <project-dir>/.pi/settings.json 中配置)。这为 LLM 的响应留出空间。
你也可以使用 /compact [instructions] 手动触发,其中可选的 instructions 用于聚焦摘要内容。
- 查找切分点:从最新消息向后遍历,累积 token 估算,直到达到
keepRecentTokens(默认 20k,可在~/.pi/agent/settings.json或<project-dir>/.pi/settings.json中配置) - 提取消息:收集从上一个保留边界(或会话开始)到切分点的消息
- 生成摘要:调用 LLM 以结构化格式生成摘要,若存在则传递上一次摘要作为迭代上下文
- 追加条目:保存包含摘要和
firstKeptEntryId的CompactionEntry - 重新加载:会话重新加载,使用摘要及从
firstKeptEntryId起的消息
Before compaction:
entry: 0 1 2 3 4 5 6 7 8 9 ┌─────┬─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┐ │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘ └────────┬───────┘ └──────────────┬──────────────┘ messagesToSummarize kept messages ↑ firstKeptEntryId (entry 4)
After compaction (new entry appended):
entry: 0 1 2 3 4 5 6 7 8 9 10 ┌─────┬─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐ │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │ └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘ └──────────┬──────┘ └──────────────────────┬───────────────────┘ not sent to LLM sent to LLM ↑ starts from firstKeptEntryId
What the LLM sees:
┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐ │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │ └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘ ↑ ↑ └─────────────────┬────────────────┘ prompt from cmp messages from firstKeptEntryId在重复压缩时,被摘要的跨度从上次压缩的保留边界(firstKeptEntryId)开始,而非从压缩条目本身开始;若该保留条目在路径中找不到,则回退到上一次压缩之后的条目。这样,上次压缩中幸存的消息也会包含在下一次摘要中。Pi 还会在写入新的 CompactionEntry 之前,根据重建后的会话上下文重新计算 tokensBefore,因此 token 计数反映实际被替换的压缩前上下文。
一个「轮次」从用户消息开始,包含所有 assistant 响应和 tool 调用,直到下一条用户消息。通常,压缩在轮次边界处切分。
当单个轮次超过 keepRecentTokens 时,切分点落在 assistant 消息处,即轮次中间。这称为「分割轮次」:
Split turn (one huge turn exceeds budget):
entry: 0 1 2 3 4 5 6 7 8 ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐ │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │ └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘ ↑ ↑ turnStartIndex = 1 firstKeptEntryId = 7 │ │ └──── turnPrefixMessages (1-6) ───────┘ └── kept (7-8)
isSplitTurn = true messagesToSummarize = [] (no complete turns before) turnPrefixMessages = [usr, ass, tool, ass, tool, tool]对于分割轮次,pi 生成两个摘要并合并:
- 历史摘要:之前的上下文(如有)
- 轮次前缀摘要:分割轮次的早期部分
有效的切分点包括:
- 用户消息
- Assistant 消息
- BashExecution 消息
- 自定义消息(custom_message、branch_summary)
切勿在 tool 结果处切分(它们必须与对应的 tool 调用保持在一起)。
CompactionEntry 结构
Section titled “CompactionEntry 结构”定义于 session-manager.ts:
interface CompactionEntry<T = unknown> { type: "compaction"; id: string; parentId: string; timestamp: number; summary: string; firstKeptEntryId: string; tokensBefore: number; fromHook?: boolean; // true if provided by extension (legacy field name) details?: T; // implementation-specific data}
// Default compaction uses this for details (from compaction.ts):interface CompactionDetails { readFiles: string[]; modifiedFiles: string[];}扩展可以在 details 中存储任何可 JSON 序列化的数据。默认压缩跟踪文件操作,但自定义扩展实现可以使用自己的结构。
实现请参阅 prepareCompaction() 和 compact()。
当你使用 /tree 导航到不同分支时,pi 会提示摘要你即将离开的工作。这会将左侧分支的上下文注入到新分支中。
- 查找公共祖先:旧位置和新位置共享的最深节点
- 收集条目:从旧叶节点回溯到公共祖先
- 按预算准备:在 token 预算内包含消息(优先最新)
- 生成摘要:以结构化格式调用 LLM
- 追加条目:在导航点保存
BranchSummaryEntry
Tree before navigation:
┌─ B ─ C ─ D (old leaf, being abandoned) A ───┤ └─ E ─ F (target)
Common ancestor: AEntries to summarize: B, C, D
After navigation with summary:
┌─ B ─ C ─ D ─ [summary of B,C,D] A ───┤ └─ E ─ F (new leaf)累积文件跟踪
Section titled “累积文件跟踪”压缩和分支摘要都会累积跟踪文件。生成摘要时,pi 从以下来源提取文件操作:
- 被摘要消息中的 tool 调用
- 之前的压缩或分支摘要
details(如有)
这意味着文件跟踪在多次压缩或嵌套分支摘要中累积,保留已读和已修改文件的完整历史。
BranchSummaryEntry 结构
Section titled “BranchSummaryEntry 结构”定义于 session-manager.ts:
interface BranchSummaryEntry<T = unknown> { type: "branch_summary"; id: string; parentId: string; timestamp: number; summary: string; fromId: string; // Entry we navigated from fromHook?: boolean; // true if provided by extension (legacy field name) details?: T; // implementation-specific data}
// Default branch summarization uses this for details (from branch-summarization.ts):interface BranchSummaryDetails { readFiles: string[]; modifiedFiles: string[];}与压缩相同,扩展可以在 details 中存储自定义数据。
实现请参阅 collectEntriesForBranchSummary()、prepareBranchEntries() 和 generateBranchSummary()。
压缩和分支摘要使用相同的结构化格式:
## Goal[What the user is trying to accomplish]
## Constraints & Preferences- [Requirements mentioned by user]
## Progress### Done- [x] [Completed tasks]
### In Progress- [ ] [Current work]
### Blocked- [Issues, if any]
## Key Decisions- **[Decision]**: [Rationale]
## Next Steps1. [What should happen next]
## Critical Context- [Data needed to continue]
<read-files>path/to/file1.tspath/to/file2.ts</read-files>
<modified-files>path/to/changed.ts</modified-files>摘要之前,消息通过 serializeConversation() 序列化为文本:
[User]: What they said[Assistant thinking]: Internal reasoning[Assistant]: Response text[Assistant tool calls]: read(path="foo.ts"); edit(path="bar.ts", ...)[Tool result]: Output from tool这可以防止模型将其视为需要继续的对话。
序列化时,tool 结果会被截断至 2000 个字符。超出部分会被替换为标记,说明截断了多少字符。这有助于将摘要请求控制在合理的 token 预算内,因为 tool 结果(尤其是来自 read 和 bash 的)通常是上下文大小的主要贡献者。
通过扩展自定义摘要
Section titled “通过扩展自定义摘要”扩展可以拦截并自定义压缩和分支摘要。事件类型定义请参阅 extensions/types.ts。
session_before_compact
Section titled “session_before_compact”在自动压缩或 /compact 之前触发。可以取消或提供自定义摘要。请参阅类型文件中的 SessionBeforeCompactEvent 和 CompactionPreparation。
pi.on("session_before_compact", async (event, ctx) => { const { preparation, branchEntries, customInstructions, signal } = event;
// preparation.messagesToSummarize - messages to summarize // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn) // preparation.previousSummary - previous compaction summary // preparation.fileOps - extracted file operations // preparation.tokensBefore - context tokens before compaction // preparation.firstKeptEntryId - where kept messages start // preparation.settings - compaction settings
// branchEntries - all entries on current branch (for custom state) // signal - AbortSignal (pass to LLM calls)
// Cancel: return { cancel: true };
// Custom summary: return { compaction: { summary: "Your summary...", firstKeptEntryId: preparation.firstKeptEntryId, tokensBefore: preparation.tokensBefore, details: { /* custom data */ }, } };});将消息转换为文本
Section titled “将消息转换为文本”要使用自己的模型生成摘要,请使用 serializeConversation 将消息转换为文本:
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
pi.on("session_before_compact", async (event, ctx) => { const { preparation } = event;
// Convert AgentMessage[] to Message[], then serialize to text const conversationText = serializeConversation( convertToLlm(preparation.messagesToSummarize) ); // Returns: // [User]: message text // [Assistant thinking]: thinking content // [Assistant]: response text // [Assistant tool calls]: read(path="..."); bash(command="...") // [Tool result]: output text
// Now send to your model for summarization const summary = await myModel.summarize(conversationText);
return { compaction: { summary, firstKeptEntryId: preparation.firstKeptEntryId, tokensBefore: preparation.tokensBefore, } };});完整示例(使用不同模型)请参阅 custom-compaction.ts。
session_before_tree
Section titled “session_before_tree”在 /tree 导航之前触发。无论用户是否选择摘要都会触发。可以取消导航或提供自定义摘要。
pi.on("session_before_tree", async (event, ctx) => { const { preparation, signal } = event;
// preparation.targetId - where we're navigating to // preparation.oldLeafId - current position (being abandoned) // preparation.commonAncestorId - shared ancestor // preparation.entriesToSummarize - entries that would be summarized // preparation.userWantsSummary - whether user chose to summarize
// Cancel navigation entirely: return { cancel: true };
// Provide custom summary (only used if userWantsSummary is true): if (preparation.userWantsSummary) { return { summary: { summary: "Your summary...", details: { /* custom data */ }, } }; }});请参阅类型文件中的 SessionBeforeTreeEvent 和 TreePreparation。
在 ~/.pi/agent/settings.json 或 <project-dir>/.pi/settings.json 中配置压缩:
{ "compaction": { "enabled": true, "reserveTokens": 16384, "keepRecentTokens": 20000 }}| 设置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 启用自动压缩 |
reserveTokens | 16384 | 为 LLM 响应保留的 token |
keepRecentTokens | 20000 | 保留的最近 token(不摘要) |
使用 "enabled": false 禁用自动压缩。你仍可使用 /compact 手动压缩。