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)開始,而不是從壓縮條目本身開始;如果在路徑中找不到該保留條目,則fallback 到前一次壓縮後的下一個條目。這樣,早期壓縮中保留下來的訊息也會進入下一輪摘要,從而繼續保留其資訊。寫入新的 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 手動壓縮。