MusePi

压缩与分支摘要

English 中文

压缩与分支摘要是两种将长会话保持可用、同时不丢失先前上下文信息的机制。

两者都作为会话条目持久化,并在重建 LLM 输入时转回用户上下文消息。

关键实现文件

会话条目模型

压缩与分支摘要是一等会话条目,不是普通 assistant/user 消息。

重建上下文(buildSessionContext)时:

  1. 活动路径上最新的压缩条目被转换成一条 compactionSummary 消息。
  2. firstKeptEntryId 到压缩点的保留条目被重新包含。
  3. 路径上此后的条目被追加。
  4. branch_summary 条目被转换成 branchSummary 消息。
  5. custom_message 条目被转换成 custom 消息。

这些自定义角色随后在 convertToLlm() 中被转换为面向 LLM 的消息:compactionSummarybranchSummary 变成通过静态模板渲染的用户消息:

custom 消息以 developer 消息形式透传原始内容(无模板)。

压缩流水线

触发方式

压缩/上下文维护可按六种方式运行:

  1. 手动上下文压缩/compact [instructions] 调用 AgentSession.compact(...)
  2. 自动溢出恢复:当同模型 assistant 错误被判定为上下文溢出后触发。
  3. 自动不完整输出恢复:当同模型 assistant 消息以 stopReason === "length" 结束时触发(OpenAI/Codex 的 response.incomplete)。
  4. 自动阈值维护:一次成功对话后,当上下文超过已解析阈值时触发。
  5. 轮中阈值维护:在下次 provider 请求前,若工具循环轮次越过了阈值且 compaction.midTurnEnabled !== false 则触发。
  6. 空闲维护runIdleCompaction() 可以同样的自动维护路径触发,reason 为 "idle"

压缩形状(图示)

压缩前:

  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)

压缩后(新条目追加):

  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

LLM 看到的内容:

  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐
  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │
  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘
       ↑         ↑      └─────────────────┬────────────────┘
    prompt   from cmp          messages from firstKeptEntryId

溢出/不完整恢复与阈值/空闲维护的区别

自动路径被有意设计得不同:

Snapcompact 策略

compaction.strategy: "snapcompact" 用本地确定性归档过程替代 LLM 总结调用(来自 @musepi/snapcompactcompact):

显示转录

压缩不再让对话在视觉上重新开始。TUI 渲染显示转录buildSessionContext({ transcript: true }) / AgentSession.buildTranscriptSessionContext()):按时间顺序排列路径上的每个条目,每条压缩内联显示为一条精简分隔符 —— ── 📷 compacted · ctrl+o ── —— 在其触发点。展开(ctrl+o)显示摘要。只有 LLM 上下文在压缩边界处重置;分隔符上方的回滚保持完整,跨会话恢复也是如此。

压缩前剪枝

压缩检查前,工具结果剪枝可能运行(pruneToolOutputs)。

默认剪枝策略:

被剪枝的工具结果被替换为:

如果剪枝改变了条目,会话存储会在压缩决策前被重写,并且 agent 消息状态被刷新。

无意义结果剔除

工具可以将已完成的结果标记为上下文无意义——零匹配的搜索、超时且仍有内容在运行的 hub wait、空的 hub 收件箱清空。该标记起源于工具结果(AgentToolResult.useless,通过 ToolResultBuilder.useless() 设置或直接在返回的对象上设置),被 agent 循环复制到持久化的 ToolResultMessage 上(绝不与 isError 一起——错误总是优先),并在三个位置被消费:

该标记不会到达 provider 线格式,被标记的对也不会从历史中移除(仅原地清空),因此工具调用/结果配对和 provider 原生历史重放保持完整。

边界与切点逻辑

prepareCompaction() 只考虑自上一个压缩条目(如果存在)以来的条目。

  1. 找到上一个压缩索引。
  2. 计算 boundaryStart = prevCompactionIndex + 1
  3. 在可用时使用测量到的使用率调整 keepRecentTokens
  4. 在边界窗口上运行 findCutPoint()

有效切点包括:

硬规则:绝不切在 toolResult

如果切点前有非消息元数据条目(model_changethinking_level_change、标签等),它们会被拉入保留区域——将切点索引向后移动,直到命中消息或压缩边界。

分轮次处理

如果切点不在用户轮次开始处,压缩将其视为分轮次。

轮次开始检测将这些视为用户轮次边界:

分轮次压缩生成两条摘要:

  1. 历史摘要(messagesToSummarize
  2. 轮次前缀摘要(turnPrefixMessages

最终存储的摘要合并为:

<history summary>

---

**Turn Context (split turn):**

<turn prefix summary>

摘要生成

compact(...) 从序列化后的对话文本构建摘要:

  1. 通过 convertToLlm() 转换消息。
  2. 通过 serializeConversation() 序列化。
  3. 包装在 <conversation>...</conversation> 中。
  4. 可选包含 <previous-summary>...</previous-summary>
  5. 可选注入扩展 hook 上下文和活动内存后端压缩上下文作为 <additional-context> 条目。
  6. 使用 SUMMARIZATION_SYSTEM_PROMPT 执行总结提示词。

提示词选择:

远程摘要模式:

Handoff 生成

packages/agent/src/compaction/compaction.ts 还导出 generateHandoff(...)。Handoff 生成使用与摘要相同的 completeSimple(...) 单次风格,但它通过发送活跃系统提示词、工具数组和真实 LLM 消息历史,随后附加一条带有 handoff 提示词的 agent 归属 user 消息来保留实时 agent 缓存前缀。它强制使用 toolChoice: "none" 并直接返回连接的文本块。

Handoff 不会写入 CompactionEntryAgentSession.handoff() 负责会话过渡:它启动一个新会话,将生成的文档作为可见 custom_message 注入(customType: "handoff"),然后从该新会话重建 agent 消息。

摘要中的文件操作上下文

压缩使用 assistant 工具调用跟踪累计文件活动:

累计行为:

文件列表是一个分组、前缀折叠的目录树(find-tool 形状),每个文件带有访问标记 —— (Read) 表示只读文件,(Write) 表示从未读取的修改文件,(RW) 表示已修改且也在累计读取集中的文件。上限为 20 个文件,带有 […N files elided…] 行。LLM 摘要策略将其作为 <files> 标签追加(通过 upsertFileOperations);snapcompact 在其摘要模板中作为 FILES 部分渲染。

<files>
# packages/agent/src/compaction/
compaction.ts (Read)
utils.ts (RW)
## prompts/
file-operations.md (Write)
</files>

早期版本写入的旧 <read-files>/<modified-files> 标签会在重新附加之前被剥离(连同 <files> 一起),因此旧摘要在下次压缩时会自愈。

持久化与重载

摘要生成(或 hook 提供的摘要)后,agent session:

  1. 通过 appendCompaction(...) 追加 CompactionEntry 以进行上下文完整维护;handoff 策略创建新会话并注入 handoff custom_message
  2. 从活跃叶节点通过 buildDisplaySessionContext() 重建显示上下文。
  3. 用重建的上下文替换实时 agent 消息。
  4. 从重建的分支同步活跃 todo 阶段,并关闭历史被重写的 provider 会话。
  5. 发出 session_compact hook 事件。

分支摘要流水线

分支摘要与 /tree 导航相关,而非 token 溢出。

触发

navigateTree(...) 期间:

  1. 使用 collectEntriesForBranchSummary(...) 从旧叶节点到公共祖先计算被放弃的条目。
  2. 如果调用方请求了摘要(options.summarize),则在切换叶节点前生成摘要。
  3. 如果摘要存在,则使用 branchWithSummary(...) 将其附加到导航目标。

操作上,这通常由 branchSummary.enabled 启用时的 /tree 流驱动。

分支切换形状(图示)

导航前的树:

         ┌─ B ─ C ─ D (旧叶节点,被放弃)
    A ───┤
         └─ E ─ F (目标)

公共祖先:A
要总结的条目:B、C、D

带摘要导航后:

         ┌─ B ─ C ─ D ─ [B,C,D 摘要]
    A ───┤
         └─ E ─ F (新叶节点)

准备与 token 预算

generateBranchSummary(...) 计算预算为:

prepareBranchEntries(...) 随后:

  1. 第一遍:从所有要总结的条目中收集累计文件操作,包括先前 pi 生成的 branch_summary 详情。
  2. 第二遍:从最新到最旧遍历,添加消息直到达到 token 预算。
  3. 优先保留近期上下文。
  4. 在预算边缘附近仍可能包含大型摘要条目以保证连续性。

压缩条目作为消息(compactionSummary)包含在分支摘要输入中。

摘要生成与持久化

分支摘要:

  1. 转换并序列化选定的消息。
  2. 包装在 <conversation> 中。
  3. 如果提供了自定义指令则使用,否则使用 branch-summary.md
  4. 使用 SUMMARIZATION_SYSTEM_PROMPT 调用总结模型。
  5. 前置 branch-summary-preamble.md
  6. 附加文件操作标签。

结果作为 BranchSummaryEntry 存储,带有可选详情(readFilesmodifiedFiles)。

扩展与 hook 触点

session_before_compact

压缩前的 hook。

可以:

session.compacting

默认压缩的提示词/上下文自定义 hook。

可以返回:

session_compact

保存了 compactionEntryfromExtension 标志的压缩后通知。

session_before_tree

在默认分支摘要生成前的树导航上运行。

可以:

session_tree

暴露新/旧叶节点和可选摘要条目的导航后事件。

运行时行为与故障语义

设置与默认值

来自 settings-schema.ts

这些值在运行时由 AgentSession 和压缩/分支摘要模块消费。