26 函数级剖析:压缩与截断恢复
模块目标
拆解 context overflow 场景下的两级恢复策略:compaction(摘要压缩)+ tool result truncation(工具结果截断)。
核心文件
src/agents/pi-embedded-runner/compact.tssrc/agents/pi-embedded-runner/tool-result-truncation.tssrc/agents/session-tool-result-guard.tssrc/agents/compaction.ts
一、overflow 恢复的完整决策树
context overflow 触发
│
▼
[Step 1] compactEmbeddedPiSessionDirect(...)
最多 MAX_OVERFLOW_COMPACTION_ATTEMPTS 次
│
├─ compacted: true → continue(重试 prompt)
│
└─ compacted: false(nothing to compact)
│
▼
[Step 2] sessionLikelyHasOversizedToolResults(...)
用 messagesSnapshot 快速检测(启发式)
│
├─ false → 没有可截断的工具结果
│ → 给用户可读错误,退出
│
└─ true → [Step 3] truncateOversizedToolResultsInSession(...)
toolResultTruncationAttempted = true(只尝试一次)
│
├─ truncated: true → 重试 prompt
│
└─ truncated: false → 给用户可读错误,退出toolResultTruncationAttempted 标志保证截断只尝试一次,防无限重试。
二、truncation 核心常量(源码精确)
ts
// src/agents/pi-embedded-runner/tool-result-truncation.ts
/**
* 单条工具结果文本块的硬性上限。
* 即使 2M token 的最大上下文窗口,单条结果也不应超过 ~400K 字符(~100K tokens)。
* 这是在不知道上下文窗口大小时的安全兜底。
*/
export const HARD_MAX_TOOL_RESULT_CHARS = 400_000;
/**
* 截断时最少保留字符数。
* 总保留第一段内容让模型理解结果的含义。
*/
const MIN_KEEP_CHARS = 2_000;三、calculateMaxToolResultChars(动态上限计算)
ts
// 启发式:~4 字符 ≈ 1 token(保守估算)
export function calculateMaxToolResultChars(contextWindowTokens: number): number {
const maxTokens = Math.floor(contextWindowTokens * MAX_TOOL_RESULT_CONTEXT_SHARE);
const maxChars = maxTokens * 4;
return Math.min(maxChars, HARD_MAX_TOOL_RESULT_CHARS);
}MAX_TOOL_RESULT_CONTEXT_SHARE ≈ 30%(由测试验证:128K tokens → calculateMaxToolResultChars 返回 > 100K chars)。
128K 上下文 → 约 128_000 * 0.3 * 4 = 153_600 chars(受 HARD_MAX 上限约束)。
四、截断操作的两个作用域
in-session(持久化截断)
truncateOversizedToolResultsInSession(params):
- 打开
SessionManager。 - 遍历当前分支,找到所有超过
maxChars的toolResult消息,记录索引。 - 无超大结果 →
{ truncated: false, truncatedCount: 0, reason: "no oversized tool results" }。 - 有超大结果 → 从首个超大条目的父节点创建新分支。
- 重新
appendMessage每条消息,超大项替换为截断版本。 - 返回
{ truncated: true, truncatedCount }。
原分支: [msg0][msg1][msg2-oversized][msg3][msg4]
↓ 从 msg1 创建新分支
新分支: [msg0][msg1][msg2-truncated][msg3][msg4]这不是原地修改,而是"重放分支":保留历史,新分支不含超大内容。
in-memory(发送前拦截)
truncateOversizedToolResultsInMessages(messages, contextWindowTokens):
- 不修改 session 文件,只处理内存中待发给 LLM 的消息数组。
- 用作发送前的预防性守卫。
五、截断位置策略
ts
// 尽量在换行处切断(不要切到单词中间)
let cutPoint = blockBudget;
const lastNewline = textBlock.text.lastIndexOf("\n", blockBudget);
if (lastNewline > blockBudget * 0.8) {
cutPoint = lastNewline; // 最后 20% 范围内有换行,就用换行处
}截断后追加后缀(持久化守卫 session-tool-result-guard.ts):
ts
const GUARD_TRUNCATION_SUFFIX =
"\n\n⚠️ [Content truncated during persistence — original exceeded size limit. " +
"Use offset/limit parameters or request specific sections for large content.]";六、compaction 的完整运行环境
compactEmbeddedPiSessionDirect(...) 不是轻量操作,它完整构建运行环境:
repairSessionFileIfNeeded— 先修复可能损坏的 session 文件loadWorkspaceSkillEntries— 技能加载resolveSkillsPromptForRun— 技能 prompt- 构建 system prompt + tools(与正常 run 相同)
SessionManager.open— 打开 sessionensurePiCompactionReserveTokens— 确保保留 token 预算- 触发
before_compactionHook,执行摘要,触发after_compactionHook
之所以要完整构建,是因为压缩本身也是一次 LLM 调用,需要完整上下文。
七、sessionLikelyHasOversizedToolResults(启发式检测)
ts
// 这是启发式(heuristic),而不是精确检测
// 用于快速判断"值不值得尝试截断"
export function sessionLikelyHasOversizedToolResults(params: {
messages: AgentMessage[];
contextWindowTokens: number;
}): boolean {
const maxChars = calculateMaxToolResultChars(contextWindowTokens);
for (const msg of params.messages) {
if (msg.role === "toolResult" && getToolResultTextLength(msg) > maxChars) {
return true;
}
}
return false;
}传入的 messages 是 attempt.messagesSnapshot(快照),不需要重新读 session 文件。
八、自检清单
- compaction 失败后是否检查
sessionLikelyHasOversizedToolResults,而不是直接报错。 toolResultTruncationAttempted标志是否只允许截断一次。- 截断是否用"重放分支"而不是原地修改(保留历史可追溯)。
calculateMaxToolResultChars结果是否受HARD_MAX_TOOL_RESULT_CHARS = 400_000上限约束。- 截断后缀是否添加,让模型知道内容被裁剪(而不是无声截断)。
MIN_KEEP_CHARS = 2_000是否确保至少保留 2000 字符(让模型能理解工具结果)。- compaction 是否构建了完整运行环境(包括 skills、tools、system prompt)。
九、开发避坑
- 不要把压缩失败直接当 fatal error,要先检查是否有超大工具结果。
- 截断函数有两个版本(in-session 和 in-memory),场景不同,不要混用。
sessionLikelyHasOversizedToolResults是启发式,不是精确判断,但足够快。- compaction 是重量级操作(完整 LLM 调用),不应在普通流程里频繁触发。