@andares/pi-toolkits 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -8
- package/package.json +11 -5
- package/src/features/compact/compact.test.ts +661 -0
- package/src/features/compact/constants.ts +13 -0
- package/src/features/compact/index.ts +371 -0
- package/src/features/compact/prompt.ts +240 -0
- package/src/features/compact/store.ts +137 -0
- package/src/features/favorites/index.ts +5 -3
- package/src/features/favorites/patch-cycle.ts +60 -22
- package/src/features/favorites/patch-selector.ts +19 -20
- package/src/features/review/index.ts +68 -36
- package/src/features/review/review.test.ts +65 -10
- package/src/features/review/store.ts +0 -5
- package/src/index.ts +3 -0
- package/src/lib/model-config.ts +44 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact-model feature — dedicated model for context compaction.
|
|
3
|
+
*
|
|
4
|
+
* pi's /compact, auto-compact (threshold), and overflow-recovery compaction
|
|
5
|
+
* all summarize with the CURRENT session model by default. This feature lets
|
|
6
|
+
* a cheaper/faster model take over every summarization while the session
|
|
7
|
+
* model stays focused on the conversation:
|
|
8
|
+
*
|
|
9
|
+
* - takeover point: the `session_before_compact` extension event, which pi
|
|
10
|
+
* emits for ALL three trigger reasons (manual / threshold / overflow)
|
|
11
|
+
* before running its own compaction
|
|
12
|
+
* - prompts, message assembly, split-turn handling, output budgets, and the
|
|
13
|
+
* summary file-list tail are verbatim pi 0.87.1 built-in behavior — only
|
|
14
|
+
* the executing model differs
|
|
15
|
+
* - lossless fallback: unset config, unavailable model, empty/failed
|
|
16
|
+
* summary, or user abort → return undefined and pi's default compaction
|
|
17
|
+
* (session model) runs untouched. The extension can never make
|
|
18
|
+
* compaction worse than stock.
|
|
19
|
+
*
|
|
20
|
+
* Config lives in `<agent-dir>/pi-toolkits-compact.json` ("provider/id",
|
|
21
|
+
* see store.ts). /compact-model views/changes/clears it; auto-compact paths
|
|
22
|
+
* never open dialogs (handler only uses notify).
|
|
23
|
+
*/
|
|
24
|
+
import type {
|
|
25
|
+
ExtensionAPI,
|
|
26
|
+
ExtensionContext,
|
|
27
|
+
ModelRegistry,
|
|
28
|
+
SessionBeforeCompactEvent,
|
|
29
|
+
SessionBeforeCompactResult,
|
|
30
|
+
SessionStartEvent,
|
|
31
|
+
Theme,
|
|
32
|
+
} from "@earendil-works/pi-coding-agent";
|
|
33
|
+
import {
|
|
34
|
+
convertToLlm,
|
|
35
|
+
serializeConversation,
|
|
36
|
+
} from "@earendil-works/pi-coding-agent";
|
|
37
|
+
import { uuidv7 } from "@earendil-works/pi-ai";
|
|
38
|
+
import {
|
|
39
|
+
modelKey,
|
|
40
|
+
resolveConfiguredModel,
|
|
41
|
+
type AvailableModel,
|
|
42
|
+
} from "../../lib/model-config.js";
|
|
43
|
+
import { COMPACT_STATUS_KEY } from "./constants.js";
|
|
44
|
+
import {
|
|
45
|
+
buildHistoryPromptText,
|
|
46
|
+
buildTurnPrefixPromptText,
|
|
47
|
+
combineUsage,
|
|
48
|
+
computeFileLists,
|
|
49
|
+
formatFileOperations,
|
|
50
|
+
SUMMARIZATION_SYSTEM_PROMPT,
|
|
51
|
+
type UsageLike,
|
|
52
|
+
} from "./prompt.js";
|
|
53
|
+
import { getCompactStore, type CompactStore } from "./store.js";
|
|
54
|
+
|
|
55
|
+
/** Usage record type returned by ModelRegistry.complete(). */
|
|
56
|
+
type CompleteUsage = NonNullable<
|
|
57
|
+
Awaited<ReturnType<ModelRegistry["complete"]>>["usage"]
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
/** Thrown when a summarization response cannot safely become a checkpoint. */
|
|
61
|
+
class SummarizationFailureError extends Error {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One-shot summarization call through the configured compact model,
|
|
65
|
+
* mirroring pi's completeSummarization options: signal honored, no prompt
|
|
66
|
+
* cache writes, fresh routing session id.
|
|
67
|
+
*/
|
|
68
|
+
async function completeSummary(
|
|
69
|
+
ctx: ExtensionContext,
|
|
70
|
+
model: AvailableModel,
|
|
71
|
+
promptText: string,
|
|
72
|
+
maxTokens: number,
|
|
73
|
+
signal: AbortSignal,
|
|
74
|
+
): Promise<{ text: string; usage: CompleteUsage }> {
|
|
75
|
+
const response = await ctx.modelRegistry.complete(
|
|
76
|
+
model,
|
|
77
|
+
{
|
|
78
|
+
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
role: "user",
|
|
82
|
+
content: [{ type: "text", text: promptText }],
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
maxTokens,
|
|
89
|
+
signal,
|
|
90
|
+
cacheRetention: "none",
|
|
91
|
+
sessionId: uuidv7(),
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
// Same safety gates as the built-in compaction: an errored or
|
|
95
|
+
// length-capped response is not a usable checkpoint, and a summarizer
|
|
96
|
+
// that tries to call tools has gone off the rails.
|
|
97
|
+
if (response.stopReason === "error" || response.stopReason === "length") {
|
|
98
|
+
throw new SummarizationFailureError(
|
|
99
|
+
response.stopReason === "error"
|
|
100
|
+
? `Summarization failed: ${response.errorMessage ?? "unknown error"}`
|
|
101
|
+
: "Summarization failed: generation hit the token cap and the summary is incomplete",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (response.content.some((block) => block.type === "toolCall")) {
|
|
105
|
+
throw new SummarizationFailureError(
|
|
106
|
+
"Summarization attempted to call a tool",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const text = response.content
|
|
110
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
111
|
+
.join("\n");
|
|
112
|
+
return { text, usage: response.usage };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* pi's output-budget formula (0.87.1 compaction.js): a fraction of
|
|
117
|
+
* reserveTokens clamped by the model's own output cap.
|
|
118
|
+
*/
|
|
119
|
+
function summarizationBudget(
|
|
120
|
+
fraction: number,
|
|
121
|
+
reserveTokens: number,
|
|
122
|
+
modelMaxTokens: number,
|
|
123
|
+
): number {
|
|
124
|
+
return Math.min(
|
|
125
|
+
Math.floor(fraction * reserveTokens),
|
|
126
|
+
modelMaxTokens > 0 ? modelMaxTokens : Number.POSITIVE_INFINITY,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run the full compaction summarization with the configured model,
|
|
132
|
+
* replicating pi 0.87.1's compact(): history summary (first-connection or
|
|
133
|
+
* iterative-update prompt, custom focus appended), plus a separate
|
|
134
|
+
* turn-prefix checkpoint when the cut point splits a turn, merged into one
|
|
135
|
+
* summary with the file-list tail. Returns undefined on any failure after
|
|
136
|
+
* notifying (pi default compaction then takes over).
|
|
137
|
+
*/
|
|
138
|
+
async function runCompactModelCompaction(
|
|
139
|
+
ctx: ExtensionContext,
|
|
140
|
+
event: SessionBeforeCompactEvent,
|
|
141
|
+
model: AvailableModel,
|
|
142
|
+
): Promise<SessionBeforeCompactResult | undefined> {
|
|
143
|
+
const { preparation, signal, customInstructions } = event;
|
|
144
|
+
const {
|
|
145
|
+
messagesToSummarize,
|
|
146
|
+
turnPrefixMessages,
|
|
147
|
+
isSplitTurn,
|
|
148
|
+
tokensBefore,
|
|
149
|
+
firstKeptEntryId,
|
|
150
|
+
previousSummary,
|
|
151
|
+
fileOps,
|
|
152
|
+
settings,
|
|
153
|
+
} = preparation;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
let summaryText: string;
|
|
157
|
+
let usage: CompleteUsage | undefined;
|
|
158
|
+
|
|
159
|
+
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
|
160
|
+
// Split turn: summarize history (if any) and the turn prefix
|
|
161
|
+
// separately, then merge — exactly like the built-in compact().
|
|
162
|
+
let historyText = previousSummary ?? "No prior history.";
|
|
163
|
+
if (messagesToSummarize.length > 0) {
|
|
164
|
+
const history = await completeSummary(
|
|
165
|
+
ctx,
|
|
166
|
+
model,
|
|
167
|
+
buildHistoryPromptText(
|
|
168
|
+
serializeConversation(convertToLlm(messagesToSummarize)),
|
|
169
|
+
previousSummary,
|
|
170
|
+
customInstructions,
|
|
171
|
+
),
|
|
172
|
+
summarizationBudget(
|
|
173
|
+
0.8,
|
|
174
|
+
settings.reserveTokens,
|
|
175
|
+
model.maxTokens,
|
|
176
|
+
),
|
|
177
|
+
signal,
|
|
178
|
+
);
|
|
179
|
+
historyText = history.text;
|
|
180
|
+
usage = history.usage;
|
|
181
|
+
}
|
|
182
|
+
const turnPrefix = await completeSummary(
|
|
183
|
+
ctx,
|
|
184
|
+
model,
|
|
185
|
+
buildTurnPrefixPromptText(
|
|
186
|
+
serializeConversation(convertToLlm(turnPrefixMessages)),
|
|
187
|
+
),
|
|
188
|
+
summarizationBudget(0.5, settings.reserveTokens, model.maxTokens),
|
|
189
|
+
signal,
|
|
190
|
+
);
|
|
191
|
+
summaryText = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefix.text}`;
|
|
192
|
+
usage = usage
|
|
193
|
+
? combineUsage(
|
|
194
|
+
usage as UsageLike,
|
|
195
|
+
turnPrefix.usage as UsageLike,
|
|
196
|
+
) as CompleteUsage
|
|
197
|
+
: turnPrefix.usage;
|
|
198
|
+
} else {
|
|
199
|
+
const history = await completeSummary(
|
|
200
|
+
ctx,
|
|
201
|
+
model,
|
|
202
|
+
buildHistoryPromptText(
|
|
203
|
+
serializeConversation(convertToLlm(messagesToSummarize)),
|
|
204
|
+
previousSummary,
|
|
205
|
+
customInstructions,
|
|
206
|
+
),
|
|
207
|
+
summarizationBudget(0.8, settings.reserveTokens, model.maxTokens),
|
|
208
|
+
signal,
|
|
209
|
+
);
|
|
210
|
+
summaryText = history.text;
|
|
211
|
+
usage = history.usage;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!summaryText.trim()) {
|
|
215
|
+
if (!signal.aborted) {
|
|
216
|
+
ctx.ui.notify(
|
|
217
|
+
"compact-model: 压缩摘要为空,已回落 pi 默认压缩。",
|
|
218
|
+
"warning",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// File-list tail + details carry (pi does not auto-carry for
|
|
225
|
+
// fromHook compactions — see plan), matching CompactionDetails shape.
|
|
226
|
+
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
227
|
+
summaryText += formatFileOperations(readFiles, modifiedFiles);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
compaction: {
|
|
231
|
+
summary: summaryText,
|
|
232
|
+
firstKeptEntryId,
|
|
233
|
+
tokensBefore,
|
|
234
|
+
usage,
|
|
235
|
+
details: { readFiles, modifiedFiles },
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// User abort is a clean cancel — pi handles the abort messaging.
|
|
240
|
+
if (signal.aborted) return undefined;
|
|
241
|
+
const message =
|
|
242
|
+
error instanceof Error ? error.message : String(error);
|
|
243
|
+
ctx.ui.notify(
|
|
244
|
+
`compact-model: 压缩摘要失败(${message}),已回落 pi 默认压缩。`,
|
|
245
|
+
"error",
|
|
246
|
+
);
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Let the user pick a compact model (or clear the config) and persist the
|
|
253
|
+
* choice. Mirrors review's pickReviewModel, plus a leading clear option.
|
|
254
|
+
*/
|
|
255
|
+
export async function pickCompactModel(
|
|
256
|
+
ctx: ExtensionContext,
|
|
257
|
+
store: CompactStore,
|
|
258
|
+
): Promise<AvailableModel | undefined> {
|
|
259
|
+
if (!ctx.hasUI) {
|
|
260
|
+
ctx.ui.notify(
|
|
261
|
+
"compact-model: 当前环境不支持模型选择对话框,请编辑 <agent-dir>/pi-toolkits-compact.json 手动配置。",
|
|
262
|
+
"error",
|
|
263
|
+
);
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
267
|
+
if (available.length === 0) {
|
|
268
|
+
ctx.ui.notify(
|
|
269
|
+
"compact-model: 当前没有可用模型,请先在 pi 中配置模型。",
|
|
270
|
+
"error",
|
|
271
|
+
);
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const configured = store.getModel();
|
|
275
|
+
const clearLabel = "✕ 清除配置(用 pi 默认压缩)";
|
|
276
|
+
const options = [
|
|
277
|
+
clearLabel,
|
|
278
|
+
...available.map(
|
|
279
|
+
(m: AvailableModel) => `${m.name} (${m.provider}/${m.id})`,
|
|
280
|
+
),
|
|
281
|
+
];
|
|
282
|
+
const choice = await ctx.ui.select(
|
|
283
|
+
configured
|
|
284
|
+
? `选择压缩模型 (当前: ${configured})`
|
|
285
|
+
: "选择压缩模型 (compact-model)",
|
|
286
|
+
options,
|
|
287
|
+
);
|
|
288
|
+
if (choice === undefined) {
|
|
289
|
+
ctx.ui.notify("compact-model: 已取消,压缩模型配置未变更。", "info");
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (choice === clearLabel) {
|
|
293
|
+
store.setModel(undefined);
|
|
294
|
+
ctx.ui.notify(
|
|
295
|
+
"compact-model: 已清除配置,压缩恢复 pi 默认(当前会话模型)。",
|
|
296
|
+
"info",
|
|
297
|
+
);
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
const model = available[options.indexOf(choice) - 1];
|
|
301
|
+
if (!model) return undefined;
|
|
302
|
+
store.setModel(modelKey(model));
|
|
303
|
+
ctx.ui.notify(
|
|
304
|
+
`compact-model: 已设置压缩模型「${model.name}」,后续压缩总结将由它执行。`,
|
|
305
|
+
"info",
|
|
306
|
+
);
|
|
307
|
+
return model;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function registerCompact(pi: ExtensionAPI): void {
|
|
311
|
+
// Take over every compaction trigger (manual / threshold / overflow)
|
|
312
|
+
// when a compact model is configured; otherwise stay invisible.
|
|
313
|
+
pi.on(
|
|
314
|
+
"session_before_compact",
|
|
315
|
+
async (
|
|
316
|
+
event: SessionBeforeCompactEvent,
|
|
317
|
+
ctx: ExtensionContext,
|
|
318
|
+
): Promise<SessionBeforeCompactResult | undefined> => {
|
|
319
|
+
const configured = getCompactStore().getModel();
|
|
320
|
+
if (!configured) return undefined;
|
|
321
|
+
const model = resolveConfiguredModel(ctx, configured);
|
|
322
|
+
if (!model) {
|
|
323
|
+
ctx.ui.notify(
|
|
324
|
+
`compact-model: 配置的压缩模型 ${configured} 不存在或不可用,已回落 pi 默认压缩。`,
|
|
325
|
+
"warning",
|
|
326
|
+
);
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
return runCompactModelCompaction(ctx, event, model);
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Config entry point: view, change, or clear the compact model.
|
|
334
|
+
pi.registerCommand("compact-model", {
|
|
335
|
+
description:
|
|
336
|
+
"Set, change, or clear the dedicated model used for /compact summaries",
|
|
337
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
338
|
+
await pickCompactModel(ctx, getCompactStore());
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Footer indicator: muted "compact" while a compact model is configured.
|
|
343
|
+
let theme: Theme | undefined;
|
|
344
|
+
let latestCtx: ExtensionContext | undefined;
|
|
345
|
+
|
|
346
|
+
const updateFooterStatus = (ctx: ExtensionContext): void => {
|
|
347
|
+
ctx.ui.setStatus(
|
|
348
|
+
COMPACT_STATUS_KEY,
|
|
349
|
+
getCompactStore().getModel() && theme
|
|
350
|
+
? theme.fg("muted", "compact")
|
|
351
|
+
: undefined,
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
pi.on("session_start", (_event: SessionStartEvent, ctx: ExtensionContext) => {
|
|
356
|
+
latestCtx = ctx;
|
|
357
|
+
try {
|
|
358
|
+
const t = ctx.ui.theme;
|
|
359
|
+
if (t) {
|
|
360
|
+
theme = t;
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// No theme in this mode — footer indicator stays off.
|
|
364
|
+
}
|
|
365
|
+
updateFooterStatus(ctx);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
getCompactStore().subscribe(() => {
|
|
369
|
+
if (latestCtx) updateFooterStatus(latestCtx);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact-model feature — summarization prompts.
|
|
3
|
+
*
|
|
4
|
+
* Every prompt below is copied VERBATIM from pi 0.87.1's built-in
|
|
5
|
+
* compaction (dist/core/compaction/compaction.js and utils.js) so that
|
|
6
|
+
* summaries produced by the configured compact model are formatted exactly
|
|
7
|
+
* like pi's own compaction output. Only the executing model differs.
|
|
8
|
+
*
|
|
9
|
+
* Update these strings whenever pi changes its built-in prompts (diff
|
|
10
|
+
* dist/core/compaction/compaction.js between versions).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** System prompt for every summarization call (pi 0.87.1 utils.js). */
|
|
14
|
+
export const SUMMARIZATION_SYSTEM_PROMPT =
|
|
15
|
+
"You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
|
|
16
|
+
|
|
17
|
+
/** First-connection history summary prompt (pi 0.87.1 compaction.js). */
|
|
18
|
+
export 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.
|
|
19
|
+
|
|
20
|
+
Use this EXACT format:
|
|
21
|
+
|
|
22
|
+
## Goal
|
|
23
|
+
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
|
|
24
|
+
|
|
25
|
+
## Constraints & Preferences
|
|
26
|
+
- [Any constraints, preferences, or requirements mentioned by user]
|
|
27
|
+
- [Or "(none)" if none were mentioned]
|
|
28
|
+
|
|
29
|
+
## Progress
|
|
30
|
+
### Done
|
|
31
|
+
- [x] [Completed tasks/changes]
|
|
32
|
+
|
|
33
|
+
### In Progress
|
|
34
|
+
- [ ] [Current work]
|
|
35
|
+
|
|
36
|
+
### Blocked
|
|
37
|
+
- [Issues preventing progress, if any]
|
|
38
|
+
|
|
39
|
+
## Key Decisions
|
|
40
|
+
- **[Decision]**: [Brief rationale]
|
|
41
|
+
|
|
42
|
+
## Next Steps
|
|
43
|
+
1. [Ordered list of what should happen next]
|
|
44
|
+
|
|
45
|
+
## Critical Context
|
|
46
|
+
- [Any data, examples, or references needed to continue]
|
|
47
|
+
- [Or "(none)" if not applicable]
|
|
48
|
+
|
|
49
|
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Iterative-update rules, used when a previous summary exists
|
|
53
|
+
* (pi 0.87.1 compaction.js).
|
|
54
|
+
*/
|
|
55
|
+
export const UPDATE_SUMMARIZATION_INSTRUCTIONS = `Update the existing structured summary with new information. RULES:
|
|
56
|
+
- PRESERVE all existing information from the previous summary
|
|
57
|
+
- ADD new progress, decisions, and context from the new messages
|
|
58
|
+
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
|
|
59
|
+
- UPDATE "Next Steps" based on what was accomplished
|
|
60
|
+
- PRESERVE exact file paths, function names, and error messages
|
|
61
|
+
- If something is no longer relevant, you may remove it
|
|
62
|
+
|
|
63
|
+
Use this EXACT format:
|
|
64
|
+
|
|
65
|
+
## Goal
|
|
66
|
+
[Preserve existing goals, add new ones if the task expanded]
|
|
67
|
+
|
|
68
|
+
## Constraints & Preferences
|
|
69
|
+
- [Preserve existing, add new ones discovered]
|
|
70
|
+
|
|
71
|
+
## Progress
|
|
72
|
+
### Done
|
|
73
|
+
- [x] [Include previously done items AND newly completed items]
|
|
74
|
+
|
|
75
|
+
### In Progress
|
|
76
|
+
- [ ] [Current work - update based on progress]
|
|
77
|
+
|
|
78
|
+
### Blocked
|
|
79
|
+
- [Current blockers - remove if resolved]
|
|
80
|
+
|
|
81
|
+
## Key Decisions
|
|
82
|
+
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
|
|
83
|
+
|
|
84
|
+
## Next Steps
|
|
85
|
+
1. [Update based on current state]
|
|
86
|
+
|
|
87
|
+
## Critical Context
|
|
88
|
+
- [Preserve important context, add new if needed]
|
|
89
|
+
|
|
90
|
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
91
|
+
|
|
92
|
+
/** Full iterative-update prompt (leading sentence + rules, pi 0.87.1). */
|
|
93
|
+
export const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
|
94
|
+
|
|
95
|
+
${UPDATE_SUMMARIZATION_INSTRUCTIONS}`;
|
|
96
|
+
|
|
97
|
+
/** Split-turn prefix checkpoint prompt (pi 0.87.1 compaction.js). */
|
|
98
|
+
export const TURN_PREFIX_SUMMARIZATION_PROMPT = `The messages above are earlier context from an ongoing conversation. Later messages are stored separately and do not need to be reconstructed.
|
|
99
|
+
|
|
100
|
+
Create a concise checkpoint of the user's request and the progress shown above. This checkpoint will be placed before the later messages so the conversation can continue with the necessary context.
|
|
101
|
+
|
|
102
|
+
## Original Request
|
|
103
|
+
[What did the user ask for?]
|
|
104
|
+
|
|
105
|
+
## Progress So Far
|
|
106
|
+
- [Key decisions and work completed in these messages]
|
|
107
|
+
|
|
108
|
+
## Context Needed to Continue
|
|
109
|
+
- [Information from these messages needed to understand the later work]
|
|
110
|
+
|
|
111
|
+
Only summarize information explicitly present above. Do not infer or recreate later messages.`;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build the history-summary prompt text (pi 0.87.1 generateSummaryWithUsage):
|
|
115
|
+
* conversation wrapped in tags, optional previous summary, then the base
|
|
116
|
+
* prompt with the custom focus appended (`Additional focus: ...` — the exact
|
|
117
|
+
* phrase pi's built-in compaction uses for /compact <instructions>).
|
|
118
|
+
*/
|
|
119
|
+
export function buildHistoryPromptText(
|
|
120
|
+
conversationText: string,
|
|
121
|
+
previousSummary: string | undefined,
|
|
122
|
+
customInstructions: string | undefined,
|
|
123
|
+
): string {
|
|
124
|
+
let basePrompt = previousSummary
|
|
125
|
+
? UPDATE_SUMMARIZATION_PROMPT
|
|
126
|
+
: SUMMARIZATION_PROMPT;
|
|
127
|
+
if (customInstructions) {
|
|
128
|
+
basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
|
|
129
|
+
}
|
|
130
|
+
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
131
|
+
if (previousSummary) {
|
|
132
|
+
promptText += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
|
133
|
+
}
|
|
134
|
+
promptText += basePrompt;
|
|
135
|
+
return promptText;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build the split-turn prefix prompt text (pi 0.87.1
|
|
140
|
+
* generateTurnPrefixSummary): `# Conversation` block followed by
|
|
141
|
+
* `# Instructions`.
|
|
142
|
+
*/
|
|
143
|
+
export function buildTurnPrefixPromptText(conversationText: string): string {
|
|
144
|
+
return `# Conversation\n${conversationText}\n\n# Instructions\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** File-operation sets extracted by pi's prepareCompaction. */
|
|
148
|
+
export interface FileOpsLike {
|
|
149
|
+
read: Set<string>;
|
|
150
|
+
written: Set<string>;
|
|
151
|
+
edited: Set<string>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Compute read/modified file lists from compaction file operations
|
|
156
|
+
* (pi 0.87.1 utils.js computeFileLists): modified = written ∪ edited,
|
|
157
|
+
* read-only = read minus modified, both sorted.
|
|
158
|
+
*/
|
|
159
|
+
export function computeFileLists(fileOps: FileOpsLike): {
|
|
160
|
+
readFiles: string[];
|
|
161
|
+
modifiedFiles: string[];
|
|
162
|
+
} {
|
|
163
|
+
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
164
|
+
// pi-lens-ignore: no-sort-without-comparator, — 逐字对齐 pi 0.87.1 的裸 .sort():文件路径按码元序排序(加 locale 比较器反而会改变顺序)
|
|
165
|
+
const readOnly = [...fileOps.read]
|
|
166
|
+
.filter((f) => !modified.has(f))
|
|
167
|
+
.sort();
|
|
168
|
+
// pi-lens-ignore: no-sort-without-comparator, — 同上(pi 0.87.1 computeFileLists 原文)
|
|
169
|
+
const modifiedFiles = [...modified].sort();
|
|
170
|
+
return { readFiles: readOnly, modifiedFiles };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format file lists as XML tags appended to the summary (pi 0.87.1 utils.js
|
|
175
|
+
* formatFileOperations): sections joined by a blank line, prefixed by one.
|
|
176
|
+
*/
|
|
177
|
+
export function formatFileOperations(
|
|
178
|
+
readFiles: string[],
|
|
179
|
+
modifiedFiles: string[],
|
|
180
|
+
): string {
|
|
181
|
+
const sections: string[] = [];
|
|
182
|
+
if (readFiles.length > 0) {
|
|
183
|
+
sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
|
|
184
|
+
}
|
|
185
|
+
if (modifiedFiles.length > 0) {
|
|
186
|
+
sections.push(
|
|
187
|
+
`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (sections.length === 0) return "";
|
|
191
|
+
return `\n\n${sections.join("\n\n")}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Combine two provider usage records field-wise (pi 0.87.1 compaction.js
|
|
196
|
+
* combineUsage): used when a split turn produces two summarization calls.
|
|
197
|
+
*/
|
|
198
|
+
export function combineUsage<T extends UsageLike>(first: T, second: T): T {
|
|
199
|
+
return {
|
|
200
|
+
...first,
|
|
201
|
+
input: first.input + second.input,
|
|
202
|
+
output: first.output + second.output,
|
|
203
|
+
cacheRead: first.cacheRead + second.cacheRead,
|
|
204
|
+
cacheWrite: first.cacheWrite + second.cacheWrite,
|
|
205
|
+
...(first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined
|
|
206
|
+
? {
|
|
207
|
+
cacheWrite1h: (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0),
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
...(first.reasoning !== undefined || second.reasoning !== undefined
|
|
211
|
+
? { reasoning: (first.reasoning ?? 0) + (second.reasoning ?? 0) }
|
|
212
|
+
: {}),
|
|
213
|
+
totalTokens: first.totalTokens + second.totalTokens,
|
|
214
|
+
cost: {
|
|
215
|
+
input: first.cost.input + second.cost.input,
|
|
216
|
+
output: first.cost.output + second.cost.output,
|
|
217
|
+
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
|
218
|
+
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
|
219
|
+
total: first.cost.total + second.cost.total,
|
|
220
|
+
},
|
|
221
|
+
} as T;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Structural view of pi's Usage record (the fields combineUsage touches). */
|
|
225
|
+
export interface UsageLike {
|
|
226
|
+
input: number;
|
|
227
|
+
output: number;
|
|
228
|
+
cacheRead: number;
|
|
229
|
+
cacheWrite: number;
|
|
230
|
+
cacheWrite1h?: number;
|
|
231
|
+
reasoning?: number;
|
|
232
|
+
totalTokens: number;
|
|
233
|
+
cost: {
|
|
234
|
+
input: number;
|
|
235
|
+
output: number;
|
|
236
|
+
cacheRead: number;
|
|
237
|
+
cacheWrite: number;
|
|
238
|
+
total: number;
|
|
239
|
+
};
|
|
240
|
+
}
|