压缩和分支汇总
法学硕士的背景窗口有限。当对话变得太长时,Pi 使用压缩来总结旧内容,同时保留最近的工作。本页涵盖了自动压缩和branch summarization。
源文件 (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导航 |
切换分支时保留上下文 |
两者都使用相同的结构化摘要格式并累积跟踪文件操作。压缩和分支摘要请求使用新的路由会话 ID,并且在提供程序支持的情况下禁用提示缓存写入,因为这些一次性提示不太可能被重用。
压缩
当它触发时
自动压缩在以下情况下触发:
contextTokens > contextWindow - reserveTokens默认情况下,reserveTokens为16384个令牌(可在~/.pi/agent/settings.json或<project-dir>/.pi/settings.json中配置)。这为LLM的回应留下了空间。
您还可以使用 /compact [instructions] 手动触发,其中可选指令重点关注摘要。
它是如何运作的
- 查找切点:从最新消息向后走,累积令牌估计,直到达到
keepRecentTokens(默认20k,可在~/.pi/agent/settings.json或<project-dir>/.pi/settings.json中配置) - 提取消息:收集从先前保留的边界(或会话开始)到切入点的消息
- 生成摘要:调用LLM以结构化格式进行摘要,将先前的摘要作为迭代上下文(如果存在)传递
- 追加条目:保存
CompactionEntry和摘要以及firstKeptEntryId - 重新加载:会话重新加载,使用从
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)开始,而不是从压缩条目本身开始,如果在路径中找不到该保留条目,则返回到先前压缩后的条目。这通过将早期压缩中幸存下来的消息也包含在下一个摘要过程中来保留它们。在写入新的 CompactionEntry 之前,Pi 还会根据重建的会话上下文重新计算 tokensBefore,因此令牌计数反映了被替换的实际预压缩上下文。
分叉转弯
“轮次”以用户消息开始,包括所有助手响应和工具调用,直到下一条用户消息。通常,压实会在转弯边界处进行切割。
当单圈超过 keepRecentTokens 时,切入点会在转弯中途出现一条辅助消息。这是一个“分裂回合”:
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 生成两个摘要并将它们合并:
- 历史摘要:以前的背景(如果有)
- 回合前缀总结:分割回合的早期部分
切点规则
有效的切点是:
- 用户留言
- 助理消息
- Bash执行消息
- 自定义消息(custom_message、branch_summary)
切勿削减工具结果(它们必须保留工具调用)。
压实入口结构
interface CompactionEntry<T = unknown> {
type: "compaction";
id: string;
parentId: string;
timestamp: number;
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
usage?: Usage; // LLM usage that generated the summary
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[];
}Extensions可以在details中存储任何JSON可序列化的数据。默认压缩跟踪文件操作,但自定义扩展实现可以使用自己的结构。生成的和扩展提供的摘要会存储其 LLM usage(如果可用),因此会话总数包括摘要工作。
具体实现参见prepareCompaction()和compact()。对于直接编程摘要,generateSummary() 返回摘要文本,generateSummaryWithUsage() 返回{ text, usage }。
分支总结
当它触发时
当您使用 /tree 导航到不同的分支时,Pi 会总结您要离开的工作。这会将左分支的上下文注入到新分支中。
它是如何运作的
- 找到共同祖先:新旧位置共享的最深节点
- 收集条目:从老叶子回到共同的祖先
- 准备预算:包括不超过代币预算的消息(最新的优先)
- 生成摘要:以结构化格式调用LLM
- 追加条目:在导航点保存
BranchSummaryEntry
Tree before navigation:
┌─ B ─ C ─ D (old leaf, being abandoned)
A ───┤
└─ E ─ F (target)
Common ancestor: A
Entries to summarize: B, C, D
After navigation with summary:
┌─ B ─ C ─ D
A ───┤
└─ E ─ F ─ [summary of B,C,D] (new leaf)累积文件追踪
压缩和branch summarization都累积跟踪文件。生成摘要时,pi 从以下位置提取文件操作:
- 正在汇总的消息中的工具调用
- 先前的压缩或分支摘要
details(如果有)
这意味着文件跟踪会在多个压缩或嵌套分支摘要中累积,从而保留读取和修改文件的完整历史记录。
BranchSummaryEntry 结构
interface BranchSummaryEntry<T = unknown> {
type: "branch_summary";
id: string;
parentId: string;
timestamp: number;
summary: string;
fromId: string; // Entry we navigated from
usage?: Usage; // LLM usage that generated the summary
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()。
摘要格式
压缩和branch summarization都使用相同的结构化格式:
## 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 Steps
1. [What should happen next]
## Critical Context
- [Data needed to continue]
<read-files>
path/to/file1.ts
path/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这会阻止模型将其视为继续对话。
工具结果在序列化期间被截断为 2000 个字符。超出该限制的内容将替换为指示被截断字符数的标记。这使汇总请求保持在合理的令牌预算内,因为工具结果(尤其是来自read和bash)通常是上下文大小的最大贡献者。
通过 Extensions 自定义摘要
Extensions可以拦截并自定义compaction和branch summarization。有关事件类型定义,请参阅extensions/types.ts。
压缩前的会话
在自动压缩或 /compact 之前触发。可以取消或提供自定义摘要。请参阅类型文件中的 SessionBeforeCompactEvent 和 CompactionPreparation。
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, 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)
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// signal - AbortSignal (pass to LLM calls)
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "Your summary...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
// usage: summaryResponse.usage, // Optional; included in session totals
details: { /* custom data */ },
}
};
});将消息转换为文本
要使用您自己的模型生成摘要,请使用 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, usage } = await myModel.summarize(conversationText);
return {
compaction: {
summary,
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
usage,
}
};
});有关使用不同模型的完整示例,请参阅custom-compaction.ts。
树之前的会话
在 /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...",
// usage: summaryResponse.usage, // Optional; included in session totals
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 响应保留的代币 |
keepRecentTokens |
20000 |
最近要保留的令牌(未汇总) |
使用 "enabled": false 禁用自动压缩。您仍然可以使用 /compact 手动压缩。