압축 및 분기 요약
LLM에는 제한된 컨텍스트 창이 있습니다. 대화가 너무 길어지면 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)에서 시작하며, 유지된 항목을 경로에서 찾을 수 없는 경우 이전 압축 이후의 항목으로 돌아갑니다. 이렇게 하면 다음 요약 단계에도 포함되어 이전 압축에서 살아남은 메시지가 보존됩니다. Pi는 또한 새 CompactionEntry를 작성하기 전에 재구축된 세션 컨텍스트에서 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는 두 개의 요약을 생성하고 병합합니다.
- 기록 요약: 이전 컨텍스트(있는 경우)
- 턴 프리픽스 요약: 스플릿 턴의 초기 부분
컷 포인트 규칙
유효한 절단점은 다음과 같습니다.
- 사용자 메시지
- 어시스턴트 메시지
- BashExecution 메시지
- 사용자 정의 메시지(custom_message, Branch_summary)
도구 결과를 자르지 마십시오(도구 호출을 유지해야 함).
압축항목 구조
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[];
}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(있는 경우)
이는 파일 추적이 여러 압축 또는 중첩된 분기 요약에 걸쳐 누적되어 읽기 및 수정된 파일의 전체 기록을 보존한다는 것을 의미합니다.
BranchSummary항목 구조
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()를 참조하세요.
요약 형식
압축과 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는 압축과 branch summarization를 모두 가로채고 사용자 정의할 수 있습니다. 이벤트 유형 정의는 extensions/types.ts를 참조하세요.
session_before_compact
자동 압축 또는 /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를 참조하세요.
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 */ },
}
};
}
});유형 파일의 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를 사용하여 수동으로 압축할 수도 있습니다.