Pi - Context Manage
Pi - Context Manage
Context Manage (in-Session Context)
- 使用session tree, 树形结构
- 支持message粒度branch(同一个session内)
- 支持从某个message fork一个新session, 或者创建一个空白session
- 使用JSONL持久化
- 核心代码位置
- JSONL的第一行header, header只提供元信息, 不是tree node
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* JSONL 文件第一行的 header。
*
* 注意:
* - header 本身不是树节点;
* - 它只描述 session 的元信息,例如 cwd、session id、父 session;
* - 真正参与树结构的是后续各个 entry,它们通过 id/parentId 形成关系。
*/
export interface SessionHeader {
type: "session";
version?: number; // v1 sessions don't have this
id: string;
timestamp: string;
cwd: string;
parentSession?: string;
}
tree node
1
2
3
4
5
6
7
8
9
/** Tree node for getTree() - defensive copy of session structure */
export interface SessionTreeNode {
entry: SessionEntry;
children: SessionTreeNode[];
/** Resolved label for this entry, if any */
label?: string;
/** Timestamp of the latest label change for this entry, if any */
labelTimestamp?: string;
}
session entry
- entry base
1
2
3
4
5
6
export interface SessionEntryBase {
type: string;
id: string;
parentId: string | null;
timestamp: string;
}
- 多种类型, 这些类型在base上进行extension
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export type SessionEntry =
/* 例如
export interface SessionMessageEntry extends SessionEntryBase {
type: "message";
message: AgentMessage;
}
*/
| SessionMessageEntry
| ThinkingLevelChangeEntry
| ModelChangeEntry
| CompactionEntry
| BranchSummaryEntry
| CustomEntry
| CustomMessageEntry
| LabelEntry
| SessionInfoEntry;
compaction
compaction entry
- 进行上下文压缩
- 压缩A->B, 插入一个新的compacttion节点在当前branch中
- 原本可能是A->B->C->… CompactionEntry要标记未压缩的第一个节点(C), 对应firstKeptEntryId
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
type: "compaction";
summary: string;
/**
* 压缩后仍然保留的最早 entry。
* buildSessionContext() 会从这个边界重新拼接被保留的历史,
* 再加上 compaction summary 与之后的新消息。
*/
firstKeptEntryId: string;
tokensBefore: number;
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
details?: T;
/** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
fromHook?: boolean;
}
branch summary entry
- 当使用/tree command, 从一个branch切换到另一个, 可以选择主动进行一个summary
- 可以将原本在的branch的内容进行一个总结, 形成一个summary entry到新branch中
1
2
3
4
5
6
7
8
9
10
11
12
13
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
type: "branch_summary";
/**
* 这条摘要对应的是从哪个 entry/leaf 分叉离开的。
* 它帮助后续理解“这段摘要是从哪条旧分支沉淀过来的”。
*/
fromId: string;
summary: string;
/** Extension-specific data (not sent to LLM) */
details?: T;
/** True if generated by an extension, false if pi-generated */
fromHook?: boolean;
}
构建Context
- leaf沿着当前branch向上扫描, 提取整个path
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* export function buildSessionContext(
entries: SessionEntry[],
leafId?: string | null,
byId?: Map<string, SessionEntry>,
): SessionContext {}
*/
// 从 leaf 一直沿 parentId 向上回溯到 root,得到当前 branch 的完整 path。
// 这一步决定了:
// - session 文件里虽然可能有很多分叉;
// - 但真正参与当前上下文构建的,只是当前 leaf 对应的一条路径。
const path: SessionEntry[] = [];
let current: SessionEntry | undefined = leaf;
while (current) {
path.unshift(current);
current = current.parentId ? byId.get(current.parentId) : undefined;
}
- 构造发送给模型的消息列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 真正构造“要发给 LLM 的消息列表”。
// 关键点:这里不是简单地把 path 上所有 message 原样输出。
// 如果存在 compaction,逻辑会切换为:
// 1. 先输出一条 compactionSummary;
// 2. 再输出从 firstKeptEntryId 开始保留的那段旧消息;
// 3. 最后输出 compaction 之后新增的消息。
const messages: AgentMessage[] = [];
const appendMessage = (entry: SessionEntry) => {
// 只有“真正能参与模型上下文理解”的 entry 才会被映射成消息。
// 例如 label / session_info / custom 不会进入这里。
if (entry.type === "message") {
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(
createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
);
} else if (entry.type === "branch_summary" && entry.summary) {
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
}
};
- 如果没有compact, path全部进入消息列表
1
2
3
4
// 没有压缩边界时,当前 path 上所有可见消息直接进入上下文。
for (const entry of path) {
appendMessage(entry);
}
- 如果有compact, 将summary放到第一条, 然后再保留firstKeptEntryId之后的消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 压缩后的上下文第一条一定是 summary,
// 这样模型先拿到历史摘要,再读取保留的尾部上下文。
messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp));
// 只保留从 firstKeptEntryId 开始的那一段历史。
// 这能保证 compaction 不是粗暴截断,而是“摘要 + 保留尾部”。
let foundFirstKept = false;
for (let i = 0; i < compactionIdx; i++) {
const entry = path[i];
if (entry.id === compaction.firstKeptEntryId) {
foundFirstKept = true;
}
if (foundFirstKept) {
appendMessage(entry);
}
}
Compaction
Overview
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 长 session 的上下文压缩(compaction)实现。
*
* 这个文件只负责“纯逻辑层”的压缩决策与摘要生成,不负责 session 文件的落盘:
* - SessionManager 负责把 append-only tree 持久化到 JSONL;
* - 本文件负责决定“从哪里切”、“哪些消息被总结”、“summary 怎么生成”;
* - 压缩完成后,SessionManager 会把 compaction entry 追加进树,然后重新加载上下文。
*
* 可以把它理解成:
* 1. 先根据 keepRecentTokens 找到应该保留的尾部边界;
* 2. 将边界之前的历史消息总结成 summary;
* 3. 后续 buildSessionContext() 不再直接送入那些旧消息,而是送入:
* compaction summary + firstKeptEntryId 之后的保留消息。
*/
- 访问过得文件信息保留
1
2
3
4
5
6
7
8
/**
* 这里不是“压缩算法本身”的必要字段,而是 Pi 默认实现附带保存的辅助信息:
* 用于告诉后续模型/用户,这段被总结的历史里读过哪些文件、改过哪些文件。
*/
export interface CompactionDetails {
readFiles: string[];
modifiedFiles: string[];
}
- 压缩判断
- contextTokens > contextWindow - settings.reserveTokens;
1
2
3
4
5
6
7
/**
* Check if compaction should trigger based on context usage.
*/
export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
if (!settings.enabled) return false;
return contextTokens > contextWindow - settings.reserveTokens;
}
- 寻找压缩点(cut point)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 在 path entries 中找到“从哪里开始保留尾部”的切点。
*
* 算法思路:
* 1. 从最新 entry 往回走,累计消息大小;
* 2. 一旦累计值达到 keepRecentTokens,就说明“尾部预算”已经够了;
* 3. 再把当前位置对齐到最近的合法 cut point;
* 4. 最终返回 firstKeptEntryIndex。
*
* 这里的“切”不是把树裁掉,而是决定:
* - cut 点之前的内容进入 summary;
* - cut 点及之后的内容继续原样保留在运行时上下文中。
*
* 还要特别处理 split turn:
* - 如果 cut 点不是 user 起点,而是落在 assistant / custom-like message 上,
* 说明一个 turn 太大,大到必须在 turn 中间切;
* - 此时需要找到 turnStartIndex,把这段 turn prefix 单独做摘要,保证语义连贯。
*/
export function findCutPoint(
entries: SessionEntry[],
startIndex: number,
endIndex: number,
keepRecentTokens: number,
): CutPointResult {
const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
// 从后往前累计“要原样保留”的尾部大小。
let accumulatedTokens = 0;
let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
for (let i = endIndex - 1; i >= startIndex; i--) {
const entry = entries[i];
if (entry.type !== "message") continue;
// 这里只看 message entry 的估算大小;控制态 entry 不直接计入消息 token。
const messageTokens = estimateTokens(entry.message);
accumulatedTokens += messageTokens;
// ...
}
// 如果 cut 点前面紧挨着一些非消息 entry(例如 model/thinking 变化),
// 也一并纳入 kept 区域,避免上下文从中间断开配置态。
// ...
}
prompt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
Use this EXACT format:
## Goal
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
## Constraints & Preferences
- [Any constraints, preferences, or requirements mentioned by user]
- [Or "(none)" if none were mentioned]
## Progress
### Done
- [x] [Completed tasks/changes]
### In Progress
- [ ] [Current work]
### Blocked
- [Issues preventing progress, if any]
## Key Decisions
- **[Decision]**: [Brief rationale]
## Next Steps
1. [Ordered list of what should happen next]
## Critical Context
- [Any data, examples, or references needed to continue]
- [Or "(none)" if not applicable]
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
Update the existing structured summary with new information. RULES:
- PRESERVE all existing information from the previous summary
- ADD new progress, decisions, and context from the new messages
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
- UPDATE "Next Steps" based on what was accomplished
- PRESERVE exact file paths, function names, and error messages
- If something is no longer relevant, you may remove it
Use this EXACT format:
## Goal
[Preserve existing goals, add new ones if the task expanded]
## Constraints & Preferences
- [Preserve existing, add new ones discovered]
## Progress
### Done
- [x] [Include previously done items AND newly completed items]
### In Progress
- [ ] [Current work - update based on progress]
### Blocked
- [Current blockers - remove if resolved]
## Key Decisions
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
## Next Steps
1. [Update based on current state]
## Critical Context
- [Preserve important context, add new if needed]
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
生成摘要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 调用 LLM 为一段历史对话生成结构化摘要。
*
* 这是 compaction 的核心摘要函数,负责把“待压缩的消息序列”变成可重新注入上下文的 summary 文本。
* 它既支持首轮总结,也支持在已有 summary 基础上做增量更新。
*
* 两种工作模式:
* 1. 首次总结
* - 当 previousSummary 不存在时,使用 SUMMARIZATION_PROMPT;
* - 模型直接根据 currentMessages 生成一份新的结构化 checkpoint summary。
*
* 2. 增量更新
* - 当 previousSummary 存在时,使用 UPDATE_SUMMARIZATION_PROMPT;
* - 模型会把“旧 summary + 新消息”合并成一份更新后的 summary,
* 而不是从零开始重写,借此实现滚动 compaction。
*
* 重要实现点:
* - currentMessages 不会直接以“原始对话消息数组”形式喂给模型,
* 而是先 convertToLlm(),再 serializeConversation() 序列化成纯文本;
* - 这样做是为了明确告诉模型:这些内容是“待总结材料”,不是要继续续写的对话;
* - previousSummary 会放进 <previous-summary> 标签中,让模型把它当作现有状态快照;
* - customInstructions 不是替换默认模板,而是以 Additional focus 的形式附加,
* 用来强调本次总结应额外关注的点;
* - maxTokens 使用 reserveTokens 的 80% 作为上限,避免摘要本身把保留预算全部耗尽。
*
* 返回值:
* - 返回纯文本 summary;
* - 调用方(如 compact())会再决定是否附加文件列表、如何写入 compaction entry。
*/
export async function generateSummary(
currentMessages: AgentMessage[],
model: Model<any>,
reserveTokens: number,
apiKey: string | undefined,
headers?: Record<string, string>,
signal?: AbortSignal,
customInstructions?: string,
previousSummary?: string,
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
): Promise<string> {}
Main compaction function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 根据 prepareCompaction() 的结果真正调用模型生成摘要。
*
* 主流程:
* - 普通情况:只生成一份 history summary;
* - split turn:并行生成 history summary + turn prefix summary,再合并成最终 summary;
* - 最后再附加文件读写清单,作为对后继模型非常有价值的工作记忆。
*
* 返回的是“待写入 compaction entry 的 payload”,不是最终 session entry。
*/
export async function compact(
preparation: CompactionPreparation,
model: Model<any>,
apiKey: string | undefined,
headers?: Record<string, string>,
customInstructions?: string,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
): Promise<CompactionResult> {
// 调用generateSummary
}
Branch Summary
Overview
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 树导航(tree navigation)场景下的分支摘要逻辑。
*
* 这个文件处理的不是“上下文过长时的 compaction”,而是另一类问题:
* 当用户从 session tree 的一个分支切换到另一个分支时,
* 被离开的那条分支上可能已经积累了很多决策、进展和文件操作痕迹。
* 如果直接切走,这些认知成果会暂时脱离当前活跃路径。
*
* 因此 branch summarization 的职责是:
* - 找出“旧位置 -> 新位置”之间被放下的那段分支;
* - 将这段分支压缩成一条 branch_summary;
* - 让后续在新分支继续工作时,仍能带着对旧分支成果的高层理解。
*
* 可以把它理解为:
* - compaction 解决的是“上下文窗口不够大”;
* - branch summarization 解决的是“切换分支后,如何保留离开分支的工作记忆”。
*/
要总结的范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 收集“从旧位置切到新位置时,应该被总结”的那段 entry。
*
* 核心语义:
* - oldLeafId 是当前所在位置,也就是即将离开的分支末端;
* - targetId 是即将跳转到的新位置;
* - 真正要总结的,不是整棵树,而是 oldLeaf 到两条路径公共祖先之间的那一段。
*
* 这里不会在 compaction 边界处停止:
* - 因为 compaction entry 本身也代表历史语义;
* - 进入 branch summary 时,它应该像普通上下文一样参与理解。
*
* @param session - Session manager (read-only access)
* @param oldLeafId - Current position (where we're navigating from)
* @param targetId - Target position (where we're navigating to)
* @returns Entries to summarize and the common ancestor
*/
Summary Generation
- prompt
- summary = BRANCH_SUMMARY_PREAMBLE + generatedSummary(using BRANCH_SUMMARY_PROMPT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.
Summary of that exploration:`;
const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.
Use this EXACT format:
## Goal
[What was the user trying to accomplish in this branch?]
## Constraints & Preferences
- [Any constraints, preferences, or requirements mentioned]
- [Or "(none)" if none were mentioned]
## Progress
### Done
- [x] [Completed tasks/changes]
### In Progress
- [ ] [Work that was started but not finished]
### Blocked
- [Issues preventing progress, if any]
## Key Decisions
- **[Decision]**: [Brief rationale]
## Next Steps
1. [What should happen next to continue this work]
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
- generateBranchSummary function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 为“被离开的分支”生成 branch summary。
*
* 这个函数是分支摘要的主入口,职责包括:
* - 根据模型上下文窗口计算可用预算;
* - 从 entries 中筛出适合进入 prompt 的消息;
* - 将消息序列序列化为纯文本,避免模型把它当成“继续对话”;
* - 调用 LLM 生成结构化总结;
* - 在 summary 末尾追加文件读写清单。
*
* 最终产物会被上层写成 branch_summary entry,之后在新分支上作为一条上下文消息使用。
*/
export async function generateBranchSummary(
entries: SessionEntry[],
options: GenerateBranchSummaryOptions,
): Promise<BranchSummaryResult> {}
This post is licensed under CC BY 4.0 by the author.
