Pi 的配置、扩展、平台设置和 API 参考。

压缩和分支摘要

LLM 的上下文窗口是有限的。当对话变得太长时,Pi 会使用压缩总结较早内容,同时保留最近的工作。本页介绍自动压缩和分支摘要。

源文件 (pi-mono):

对于项目中的 TypeScript 定义,请检查 node_modules/@earendil-works/pi-coding-agent/dist/

概览

Pi 有两种摘要机制:

机制 触发条件 目的
压缩 上下文超过阈值,或 /compact 总结较早消息以释放上下文
分支摘要 /tree 导航 切换分支时保留上下文

两者都使用相同的结构化摘要格式,并累积跟踪文件操作。压缩和分支摘要请求会使用新的路由会话 ID;如果 Provider 支持,还会禁用 prompt-cache 写入,因为这类一次性 Prompt 通常不会复用。

压缩

触发时机

自动压缩在以下情况下触发:

contextTokens > contextWindow - reserveTokens

默认情况下,reserveTokens 为 16384 个 token(可在 ~/.pi/agent/settings.json<project-dir>/.pi/settings.json 中配置)。这会为 LLM 响应预留空间。

也可以使用 /compact [instructions] 手动触发;可选 instructions 用于指定摘要重点。

工作方式

  1. 查找切点:从最新消息向前回溯,累计 token 估算值,直到达到 keepRecentTokens(默认 20k,可在 ~/.pi/agent/settings.json<project-dir>/.pi/settings.json 中配置)
  2. 提取消息:收集从上一次保留边界(或会话开始处)到切点之间的消息
  3. 生成摘要:调用 LLM 按结构化格式生成摘要;如果已有前一份摘要,会将其作为迭代上下文传入
  4. 追加条目:保存带有摘要和 firstKeptEntryIdCompactionEntry
  5. 重建上下文:会话会使用摘要和从 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,因此 token 数能反映实际被替换的压缩前上下文。

Split Turn

一个“轮次”以用户消息开始,并包含下一条用户消息之前的所有助手响应和工具调用。通常,压缩会在轮次边界处切分。

当单个轮次超过 keepRecentTokens 时,切点会落在该轮次中间的某条助手消息处。这称为 split turn:

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]

对于 split turn,Pi 会生成两份摘要并合并:

  1. 历史摘要:之前的上下文(如果有)
  2. 轮次前缀摘要:split turn 的前半部分

切点规则

有效的切点是:

  • 用户消息
  • 助理消息
  • BashExecution 消息
  • 自定义消息(custom_message、branch_summary)

不要在工具结果处切分(工具结果必须与对应的工具调用保持在一起)。

CompactionEntry 结构

定义于session-manager.ts

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[];
}

扩展可以在 details 中存储任何可 JSON 序列化的数据。默认压缩会跟踪文件操作,但自定义扩展实现可以使用自己的结构。生成的摘要和扩展提供的摘要会在可用时存储其 LLM usage,因此会话总计会包含摘要工作。

具体实现参见 prepareCompaction()compact()。对于直接编程式摘要,generateSummary() 返回摘要文本,generateSummaryWithUsage() 返回 { text, usage }

分支摘要

触发时机

使用 /tree 导航到不同分支时,Pi 会询问是否总结即将离开的工作。这会把被离开分支的上下文注入到新分支中。

工作方式

  1. 找到共同祖先:新旧位置共享的最深节点
  2. 收集条目:从旧叶节点回溯到共同祖先
  3. 按预算准备:在 token 预算内纳入消息(优先纳入最新消息)
  4. 生成摘要:调用 LLM 按结构化格式生成摘要
  5. 追加条目:在导航点保存 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)

累积文件追踪

压缩和分支摘要都会累积跟踪文件。生成摘要时,pi 会从以下位置提取文件操作:

  • 正在汇总的消息中的工具调用
  • 先前压缩或分支摘要的 details(如果有)

这意味着文件跟踪会在多个压缩或嵌套分支摘要中累积,从而保留读取和修改文件的完整历史记录。

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
  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()

摘要格式

压缩和分支摘要使用相同的结构化格式:

## 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 个字符。超出限制的内容会替换为一个标记,说明截断了多少字符。这样可以将摘要请求保持在合理的 token 预算内,因为工具结果(尤其是来自 readbash 的结果)通常是上下文大小的主要来源。

通过扩展自定义摘要

扩展可以拦截并自定义压缩和分支摘要。事件类型定义见 extensions/types.ts

session_before_compact

在自动压缩或 /compact 之前触发。可以取消或提供自定义摘要。请参阅类型文件中的 SessionBeforeCompactEventCompactionPreparation

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

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...",
        // usage: summaryResponse.usage, // Optional; included in session totals
        details: { /* custom data */ },
      }
    };
  }
});

请参阅类型文件中的 SessionBeforeTreeEventTreePreparation

设置

~/.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 手动压缩。