@xynogen/pix-core 0.7.4 → 0.7.6
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/package.json +18 -18
- package/src/compaction.ts +136 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Pi extension bundle \u2014 installs and activates all core pix-* extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/extension.ts",
|
|
@@ -43,32 +43,32 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@xynogen/pix-runtime": "^0.13.
|
|
46
|
+
"@xynogen/pix-runtime": "^0.13.2",
|
|
47
47
|
"@xynogen/pix-data": "^0.5.4",
|
|
48
|
-
"@xynogen/pix-pretty": "^1.
|
|
48
|
+
"@xynogen/pix-pretty": "^1.31.0",
|
|
49
49
|
"@xynogen/pix-welcome": "^0.2.7",
|
|
50
|
-
"@xynogen/pix-footer": "^0.2.
|
|
51
|
-
"@xynogen/pix-diagnostics": "^0.4.
|
|
50
|
+
"@xynogen/pix-footer": "^0.2.10",
|
|
51
|
+
"@xynogen/pix-diagnostics": "^0.4.1",
|
|
52
52
|
"@xynogen/pix-display": "^0.3.1",
|
|
53
|
-
"@xynogen/pix-prompts": "^0.5.
|
|
53
|
+
"@xynogen/pix-prompts": "^0.5.7",
|
|
54
54
|
"@xynogen/pix-skills": "^0.10.2",
|
|
55
|
-
"@xynogen/pix-models": "^0.4.
|
|
55
|
+
"@xynogen/pix-models": "^0.4.1",
|
|
56
56
|
"@xynogen/pix-update": "^0.2.5",
|
|
57
57
|
"@xynogen/pix-commands": "^0.6.9",
|
|
58
58
|
"@xynogen/pix-nudge": "^0.3.3",
|
|
59
|
-
"@xynogen/pix-read": "^0.4.
|
|
60
|
-
"@xynogen/pix-write": "^0.2.
|
|
61
|
-
"@xynogen/pix-edit": "^0.2.
|
|
62
|
-
"@xynogen/pix-find": "^0.3.
|
|
63
|
-
"@xynogen/pix-grep": "^0.3.
|
|
64
|
-
"@xynogen/pix-ls": "^0.3.
|
|
65
|
-
"@xynogen/pix-bash": "^0.2.
|
|
66
|
-
"@xynogen/pix-powershell": "^0.1.
|
|
67
|
-
"@xynogen/pix-todo": "^0.4.
|
|
59
|
+
"@xynogen/pix-read": "^0.4.4",
|
|
60
|
+
"@xynogen/pix-write": "^0.2.9",
|
|
61
|
+
"@xynogen/pix-edit": "^0.2.10",
|
|
62
|
+
"@xynogen/pix-find": "^0.3.5",
|
|
63
|
+
"@xynogen/pix-grep": "^0.3.5",
|
|
64
|
+
"@xynogen/pix-ls": "^0.3.5",
|
|
65
|
+
"@xynogen/pix-bash": "^0.2.11",
|
|
66
|
+
"@xynogen/pix-powershell": "^0.1.2",
|
|
67
|
+
"@xynogen/pix-todo": "^0.4.7",
|
|
68
68
|
"@xynogen/pix-ask": "^0.3.2",
|
|
69
|
-
"@xynogen/pix-optimizer": "^1.3.
|
|
69
|
+
"@xynogen/pix-optimizer": "^1.3.4",
|
|
70
70
|
"@xynogen/pix-gate": "^0.4.1",
|
|
71
|
-
"@xynogen/pix-subagent": "^0.10.
|
|
71
|
+
"@xynogen/pix-subagent": "^0.10.8"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@earendil-works/pi-ai": "*",
|
package/src/compaction.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 1. Summary prompt — ALWAYS ours. `session_before_compact` fires for every
|
|
8
8
|
* compaction (manual, threshold, overflow); we generate the summary with
|
|
9
|
-
* the CURRENT conversation model (no silent routing) using
|
|
9
|
+
* the CURRENT conversation model (no silent routing) using summaryPrompt()
|
|
10
10
|
* below. Edit the prompt/format to experiment.
|
|
11
11
|
*
|
|
12
12
|
* 2. Trigger — ours when `compaction.triggerPercent > 0`. After each settled
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* notify line with reason, model, and token counts.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import { contentText, retryAssistantCall, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
29
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
29
30
|
import type { ContextEditableContent, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
30
31
|
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
31
32
|
import { getErrorMessage } from "@xynogen/pix-pretty/utils";
|
|
@@ -37,11 +38,41 @@ import { compactionSection } from "@xynogen/pix-runtime/sections";
|
|
|
37
38
|
// pi keeps recent turns verbatim (via firstKeptEntryId). This matches current
|
|
38
39
|
// context-engineering guidance — recency is highest-signal, and "context rot"
|
|
39
40
|
// makes full-history replacement risky. Tune freely.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
//
|
|
42
|
+
// Adopted from pi 1.4.0's own summarizer: a system prompt that forbids
|
|
43
|
+
// continuing the chat, data first and instructions last, /compact focus text,
|
|
44
|
+
// no fixed output cap (the model's own limit), the current thinking level,
|
|
45
|
+
// transient-error retry, and refusing truncated (`length`) summaries.
|
|
46
|
+
// Kept ours: one call for a split turn (pi makes two) and file lists that
|
|
47
|
+
// carry across compactions, which pi drops for extension-made summaries.
|
|
48
|
+
const SYSTEM_PROMPT =
|
|
49
|
+
"You are a context summarization assistant. Read the conversation between a user and an AI assistant and produce the structured summary requested. Do NOT continue the conversation or answer its questions. Output ONLY the summary.";
|
|
50
|
+
|
|
51
|
+
export function summaryPrompt(p: {
|
|
52
|
+
conversation: string;
|
|
53
|
+
turnPrefix?: string;
|
|
54
|
+
previousSummary?: string;
|
|
55
|
+
customInstructions?: string;
|
|
56
|
+
}): string {
|
|
57
|
+
const blocks: string[] = [];
|
|
58
|
+
if (p.conversation) blocks.push(`<conversation>\n${p.conversation}\n</conversation>`);
|
|
59
|
+
if (p.turnPrefix) blocks.push(`<turn-prefix>\n${p.turnPrefix}\n</turn-prefix>`);
|
|
60
|
+
if (p.previousSummary)
|
|
61
|
+
blocks.push(`<previous-summary>\n${p.previousSummary}\n</previous-summary>`);
|
|
62
|
+
|
|
63
|
+
const prior = p.previousSummary
|
|
64
|
+
? " Fold <previous-summary> in: keep what still matters, update Progress and Next Steps, do not repeat it verbatim."
|
|
65
|
+
: "";
|
|
66
|
+
const split = p.turnPrefix
|
|
67
|
+
? "\n\n<turn-prefix> is the start of the current turn; its later messages are kept verbatim. Add a `## Turn Context` section with that turn's request and the progress shown in it."
|
|
43
68
|
: "";
|
|
44
|
-
|
|
69
|
+
const focus = p.customInstructions?.trim()
|
|
70
|
+
? `\n\nAdditional focus: ${p.customInstructions.trim()}`
|
|
71
|
+
: "";
|
|
72
|
+
|
|
73
|
+
return `${blocks.join("\n\n")}
|
|
74
|
+
|
|
75
|
+
Summarize the OLDER part of this coding session above. Recent turns are kept verbatim after your summary, so capture only what they would otherwise lose.${prior}
|
|
45
76
|
|
|
46
77
|
Produce compact, high-signal markdown with these sections (omit a section if empty):
|
|
47
78
|
|
|
@@ -52,14 +83,55 @@ Produce compact, high-signal markdown with these sections (omit a section if emp
|
|
|
52
83
|
## Next Steps
|
|
53
84
|
## Critical Context (facts, paths, values needed to continue)
|
|
54
85
|
|
|
55
|
-
Be terse. Prefer names, paths, numbers over prose. No filler.
|
|
86
|
+
Be terse. Prefer names, paths, numbers over prose. No filler. Preserve exact file paths, function names, and error messages.${split}${focus}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── File lists ───────────────────────────────────────────────────────────────────
|
|
90
|
+
// Same <read-files>/<modified-files> tags pi appends. Pi rebuilds them from
|
|
91
|
+
// entry details only for its own compactions, so we carry them through the
|
|
92
|
+
// previous summary text instead — that works for both pi's and ours.
|
|
93
|
+
const FILE_TAGS = /\n*<(read-files|modified-files)>\n([\s\S]*?)\n<\/\1>/g;
|
|
56
94
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
95
|
+
export function mergeFileLists(
|
|
96
|
+
fileOps: { read: Set<string>; written: Set<string>; edited: Set<string> },
|
|
97
|
+
previousSummary: string | undefined,
|
|
98
|
+
): { readFiles: string[]; modifiedFiles: string[]; previousSummary: string | undefined } {
|
|
99
|
+
const read = new Set(fileOps.read);
|
|
100
|
+
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
101
|
+
for (const [, tag, body] of previousSummary?.matchAll(FILE_TAGS) ?? []) {
|
|
102
|
+
const into = tag === "read-files" ? read : modified;
|
|
103
|
+
for (const line of body?.split("\n") ?? []) if (line.trim()) into.add(line.trim());
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
readFiles: [...read].filter((f) => !modified.has(f)).sort(),
|
|
107
|
+
modifiedFiles: [...modified].sort(),
|
|
108
|
+
previousSummary: previousSummary?.replace(FILE_TAGS, "").trim() || undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
61
111
|
|
|
62
|
-
|
|
112
|
+
export function formatFileLists(readFiles: string[], modifiedFiles: string[]): string {
|
|
113
|
+
const sections: string[] = [];
|
|
114
|
+
if (readFiles.length) sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
|
|
115
|
+
if (modifiedFiles.length)
|
|
116
|
+
sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
|
|
117
|
+
return sections.length ? `\n\n${sections.join("\n\n")}` : "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Why a summary response must not become a checkpoint, or undefined when it is usable. */
|
|
121
|
+
export function summaryFailure(response: {
|
|
122
|
+
stopReason: string;
|
|
123
|
+
errorMessage?: string;
|
|
124
|
+
content: { type: string }[];
|
|
125
|
+
}): string | undefined {
|
|
126
|
+
if (response.stopReason === "error") return response.errorMessage || "unknown error";
|
|
127
|
+
if (response.stopReason === "length") return "hit the output limit; summary incomplete";
|
|
128
|
+
if (response.content.some((c) => c.type === "toolCall")) return "model tried to call a tool";
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ponytail: mirrors pi's default `retry` settings; extensions cannot read
|
|
133
|
+
// settings.retry. Move to the `compaction` config section if someone tunes it.
|
|
134
|
+
const SUMMARY_RETRY = { enabled: true, maxRetries: 3, baseDelayMs: 2000 };
|
|
63
135
|
|
|
64
136
|
export function compactionThresholdTokens(
|
|
65
137
|
contextWindow: number,
|
|
@@ -151,56 +223,76 @@ export default function registerCompaction(pi: ExtensionAPI): void {
|
|
|
151
223
|
return;
|
|
152
224
|
}
|
|
153
225
|
|
|
154
|
-
const
|
|
155
|
-
|
|
226
|
+
const toText = (m: typeof preparation.messagesToSummarize) =>
|
|
227
|
+
m.length ? serializeConversation(convertToLlm(m)) : "";
|
|
228
|
+
const files = mergeFileLists(preparation.fileOps, preparation.previousSummary);
|
|
229
|
+
const prompt = summaryPrompt({
|
|
230
|
+
conversation: toText(preparation.messagesToSummarize),
|
|
231
|
+
turnPrefix: toText(preparation.turnPrefixMessages),
|
|
232
|
+
previousSummary: files.previousSummary,
|
|
233
|
+
customInstructions: event.customInstructions,
|
|
234
|
+
});
|
|
235
|
+
const level = pi.getThinkingLevel?.();
|
|
236
|
+
const reasoning =
|
|
237
|
+
model.reasoning && level && level !== "off" ? (level as ThinkingLevel) : undefined;
|
|
238
|
+
const count = preparation.messagesToSummarize.length + preparation.turnPrefixMessages.length;
|
|
156
239
|
ctx.ui.notify(
|
|
157
|
-
`Compaction (${reason}): summarizing ${
|
|
240
|
+
`Compaction (${reason}): summarizing ${count} msgs / ${preparation.tokensBefore.toLocaleString()} tok with ${model.id}${reasoning ? ` (thinking ${reasoning})` : ""}`,
|
|
158
241
|
"info",
|
|
159
242
|
);
|
|
160
243
|
|
|
161
244
|
try {
|
|
162
|
-
const response = await
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
245
|
+
const response = await retryAssistantCall(
|
|
246
|
+
() =>
|
|
247
|
+
completeSimple(
|
|
248
|
+
model,
|
|
166
249
|
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
type: "text" as const,
|
|
171
|
-
text: SUMMARY_PROMPT(preparation.previousSummary, conversation),
|
|
172
|
-
},
|
|
250
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
251
|
+
messages: [
|
|
252
|
+
{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() },
|
|
173
253
|
],
|
|
174
|
-
timestamp: Date.now(),
|
|
175
254
|
},
|
|
176
|
-
|
|
177
|
-
|
|
255
|
+
// No maxTokens: the model's own output limit, so long reasoning cannot
|
|
256
|
+
// starve the summary (pi #9512).
|
|
257
|
+
{
|
|
258
|
+
apiKey: auth.apiKey,
|
|
259
|
+
headers: auth.headers,
|
|
260
|
+
env: auth.env,
|
|
261
|
+
signal,
|
|
262
|
+
cacheRetention: "none",
|
|
263
|
+
reasoning,
|
|
264
|
+
},
|
|
265
|
+
),
|
|
266
|
+
SUMMARY_RETRY,
|
|
267
|
+
signal,
|
|
178
268
|
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
cacheRetention: "none",
|
|
269
|
+
onRetryScheduled: (attempt, max, delayMs, error) =>
|
|
270
|
+
ctx.ui.notify(
|
|
271
|
+
`Compaction: retry ${attempt}/${max} in ${Math.round(delayMs / 1000)}s (${error})`,
|
|
272
|
+
"warning",
|
|
273
|
+
),
|
|
185
274
|
},
|
|
186
275
|
);
|
|
276
|
+
if (signal.aborted) return;
|
|
187
277
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
278
|
+
const failure = summaryFailure(response);
|
|
279
|
+
if (failure) {
|
|
280
|
+
ctx.ui.notify(`Compaction: summary failed (${failure}); using built-in`, "warning");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const summary = contentText(response.content).trim();
|
|
193
284
|
if (!summary) {
|
|
194
|
-
|
|
285
|
+
ctx.ui.notify("Compaction: empty summary; using built-in", "warning");
|
|
195
286
|
return;
|
|
196
287
|
}
|
|
197
288
|
|
|
198
289
|
return {
|
|
199
290
|
compaction: {
|
|
200
|
-
summary,
|
|
291
|
+
summary: summary + formatFileLists(files.readFiles, files.modifiedFiles),
|
|
201
292
|
firstKeptEntryId: preparation.firstKeptEntryId,
|
|
202
293
|
tokensBefore: preparation.tokensBefore,
|
|
203
294
|
usage: response.usage,
|
|
295
|
+
details: { readFiles: files.readFiles, modifiedFiles: files.modifiedFiles },
|
|
204
296
|
},
|
|
205
297
|
};
|
|
206
298
|
} catch (error) {
|
|
@@ -256,8 +348,9 @@ export default function registerCompaction(pi: ExtensionAPI): void {
|
|
|
256
348
|
return { entries: [...event.entries, ...edits] };
|
|
257
349
|
});
|
|
258
350
|
|
|
259
|
-
pi.on("agent_settled", (
|
|
260
|
-
|
|
351
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
352
|
+
// Escape means the user wants control back, not a surprise compaction.
|
|
353
|
+
if (event.aborted || compacting || triggerDisabled) return;
|
|
261
354
|
const { triggerPercent, minimumTokens } = config(compactionSection);
|
|
262
355
|
if (triggerPercent <= 0) return;
|
|
263
356
|
|