@xynogen/pix-core 0.4.60 → 0.5.0
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 +12 -12
- package/src/compaction.test.ts +71 -1
- package/src/compaction.ts +70 -7
- package/src/once.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension bundle
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Pi extension bundle — installs and activates all core pix-* extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/extension.ts",
|
|
7
7
|
"pi": {
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@xynogen/pix-runtime": "^0.5.2",
|
|
42
42
|
"@xynogen/pix-data": "^0.4.1",
|
|
43
|
-
"@xynogen/pix-pretty": "^1.
|
|
44
|
-
"@xynogen/pix-welcome": "^0.1.
|
|
45
|
-
"@xynogen/pix-footer": "^0.1.
|
|
43
|
+
"@xynogen/pix-pretty": "^1.11.0",
|
|
44
|
+
"@xynogen/pix-welcome": "^0.1.19",
|
|
45
|
+
"@xynogen/pix-footer": "^0.1.21",
|
|
46
46
|
"@xynogen/pix-diagnostics": "^0.1.10",
|
|
47
47
|
"@xynogen/pix-display": "^0.1.14",
|
|
48
48
|
"@xynogen/pix-prompts": "^0.3.5",
|
|
49
|
-
"@xynogen/pix-skills": "^0.7.
|
|
49
|
+
"@xynogen/pix-skills": "^0.7.8",
|
|
50
50
|
"@xynogen/pix-models": "^0.1.27",
|
|
51
|
-
"@xynogen/pix-update": "^0.1.
|
|
52
|
-
"@xynogen/pix-commands": "^0.2
|
|
51
|
+
"@xynogen/pix-update": "^0.1.18",
|
|
52
|
+
"@xynogen/pix-commands": "^0.3.2",
|
|
53
53
|
"@xynogen/pix-nudge": "^0.1.13",
|
|
54
54
|
"@xynogen/pix-read": "^0.1.20",
|
|
55
55
|
"@xynogen/pix-write": "^0.1.19",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"@xynogen/pix-find": "^0.1.20",
|
|
58
58
|
"@xynogen/pix-grep": "^0.1.20",
|
|
59
59
|
"@xynogen/pix-ls": "^0.1.20",
|
|
60
|
-
"@xynogen/pix-bash": "^0.1.
|
|
61
|
-
"@xynogen/pix-todo": "^0.1.
|
|
60
|
+
"@xynogen/pix-bash": "^0.1.19",
|
|
61
|
+
"@xynogen/pix-todo": "^0.1.21",
|
|
62
62
|
"@xynogen/pix-ask": "^0.2.15",
|
|
63
63
|
"@xynogen/pix-optimizer": "^1.1.22",
|
|
64
|
-
"@xynogen/pix-gate": "^0.2.
|
|
65
|
-
"@xynogen/pix-subagent": "^0.6.
|
|
64
|
+
"@xynogen/pix-gate": "^0.2.3",
|
|
65
|
+
"@xynogen/pix-subagent": "^0.6.5"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@earendil-works/pi-ai": "*",
|
package/src/compaction.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { compactionThresholdTokens } from "./compaction.ts";
|
|
2
|
+
import { compactionThresholdTokens, resumeDecisionAfterCompaction } from "./compaction.ts";
|
|
3
3
|
|
|
4
4
|
describe("compactionThresholdTokens", () => {
|
|
5
5
|
it("uses the 100k floor when the percentage threshold is lower", () => {
|
|
@@ -10,3 +10,73 @@ describe("compactionThresholdTokens", () => {
|
|
|
10
10
|
expect(compactionThresholdTokens(1_000_000, 60, 100_000)).toBe(600_000);
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
describe("resumeDecisionAfterCompaction", () => {
|
|
15
|
+
const threshold = 100_000;
|
|
16
|
+
|
|
17
|
+
it("resumes when idle, no pending messages, and under threshold", () => {
|
|
18
|
+
expect(
|
|
19
|
+
resumeDecisionAfterCompaction({
|
|
20
|
+
estimatedTokensAfter: 40_000,
|
|
21
|
+
threshold,
|
|
22
|
+
idle: true,
|
|
23
|
+
hasPending: false,
|
|
24
|
+
}),
|
|
25
|
+
).toBe("resume");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("skips when the agent is busy with the user's in-flight prompt", () => {
|
|
29
|
+
expect(
|
|
30
|
+
resumeDecisionAfterCompaction({
|
|
31
|
+
estimatedTokensAfter: 40_000,
|
|
32
|
+
threshold,
|
|
33
|
+
idle: false,
|
|
34
|
+
hasPending: false,
|
|
35
|
+
}),
|
|
36
|
+
).toBe("skip");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("skips when the user has a prompt queued during compaction", () => {
|
|
40
|
+
expect(
|
|
41
|
+
resumeDecisionAfterCompaction({
|
|
42
|
+
estimatedTokensAfter: 40_000,
|
|
43
|
+
threshold,
|
|
44
|
+
idle: true,
|
|
45
|
+
hasPending: true,
|
|
46
|
+
}),
|
|
47
|
+
).toBe("skip");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("latches off (loop) when still at/above threshold after compaction", () => {
|
|
51
|
+
expect(
|
|
52
|
+
resumeDecisionAfterCompaction({
|
|
53
|
+
estimatedTokensAfter: threshold,
|
|
54
|
+
threshold,
|
|
55
|
+
idle: true,
|
|
56
|
+
hasPending: false,
|
|
57
|
+
}),
|
|
58
|
+
).toBe("loop");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("treats a missing estimate as 0 tokens (does not latch)", () => {
|
|
62
|
+
expect(
|
|
63
|
+
resumeDecisionAfterCompaction({
|
|
64
|
+
estimatedTokensAfter: undefined,
|
|
65
|
+
threshold,
|
|
66
|
+
idle: true,
|
|
67
|
+
hasPending: false,
|
|
68
|
+
}),
|
|
69
|
+
).toBe("resume");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("loop takes priority over a busy agent", () => {
|
|
73
|
+
expect(
|
|
74
|
+
resumeDecisionAfterCompaction({
|
|
75
|
+
estimatedTokensAfter: 200_000,
|
|
76
|
+
threshold,
|
|
77
|
+
idle: false,
|
|
78
|
+
hasPending: true,
|
|
79
|
+
}),
|
|
80
|
+
).toBe("loop");
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/compaction.ts
CHANGED
|
@@ -68,6 +68,29 @@ export function compactionThresholdTokens(
|
|
|
68
68
|
return Math.max(minimumTokens, Math.ceil((contextWindow * triggerPercent) / 100));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Decide what to do after a self-triggered compaction completes. Pure so the
|
|
73
|
+
* three branches (the tricky part) are unit-testable without a Pi session.
|
|
74
|
+
*
|
|
75
|
+
* - "loop": compaction landed still at/above threshold — latch the auto-trigger
|
|
76
|
+
* off so the resume turn doesn't re-fire agent_settled forever.
|
|
77
|
+
* - "skip": the user is already driving (a prompt is in flight or queued).
|
|
78
|
+
* Injecting the resume nudge would collide with that prompt (the reported
|
|
79
|
+
* "Agent is already processing a prompt" crash) and, worse, tell the agent to
|
|
80
|
+
* resume the ORIGINAL task right after the user redirected it.
|
|
81
|
+
* - "resume": agent is idle with room to spare — send the nudge.
|
|
82
|
+
*/
|
|
83
|
+
export function resumeDecisionAfterCompaction(args: {
|
|
84
|
+
estimatedTokensAfter: number | undefined;
|
|
85
|
+
threshold: number;
|
|
86
|
+
idle: boolean;
|
|
87
|
+
hasPending: boolean;
|
|
88
|
+
}): "loop" | "skip" | "resume" {
|
|
89
|
+
if ((args.estimatedTokensAfter ?? 0) >= args.threshold) return "loop";
|
|
90
|
+
if (!args.idle || args.hasPending) return "skip";
|
|
91
|
+
return "resume";
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
// Sent as a user message after a self-triggered compaction so the agent picks
|
|
72
95
|
// its task back up on its own. Kept short — the freshly written summary already
|
|
73
96
|
// carries the goal, progress, and next steps. Edit to taste.
|
|
@@ -122,8 +145,7 @@ export default function registerCompaction(pi: ExtensionAPI): void {
|
|
|
122
145
|
);
|
|
123
146
|
|
|
124
147
|
const summary = response.content
|
|
125
|
-
.
|
|
126
|
-
.map((c) => c.text)
|
|
148
|
+
.flatMap((c) => (c.type === "text" ? [c.text] : []))
|
|
127
149
|
.join("\n")
|
|
128
150
|
.trim();
|
|
129
151
|
|
|
@@ -150,8 +172,13 @@ export default function registerCompaction(pi: ExtensionAPI): void {
|
|
|
150
172
|
|
|
151
173
|
// ── Lever 2: our own trigger, driven by live context usage ────────────────
|
|
152
174
|
let compacting = false;
|
|
175
|
+
// Latched off if a compaction lands still above threshold (e.g. a large
|
|
176
|
+
// minimumTokens floor on a small model). Without this the resume turn settles,
|
|
177
|
+
// agent_settled re-fires, and we compact again every turn. Reset by /compact or
|
|
178
|
+
// a new session; a session-scoped bool is the smallest fix.
|
|
179
|
+
let triggerDisabled = false;
|
|
153
180
|
pi.on("agent_settled", (_event, ctx) => {
|
|
154
|
-
if (compacting) return;
|
|
181
|
+
if (compacting || triggerDisabled) return;
|
|
155
182
|
const { triggerPercent, minimumTokens } = config(compactionSection);
|
|
156
183
|
if (triggerPercent <= 0) return;
|
|
157
184
|
|
|
@@ -167,12 +194,48 @@ export default function registerCompaction(pi: ExtensionAPI): void {
|
|
|
167
194
|
"info",
|
|
168
195
|
);
|
|
169
196
|
ctx.compact({
|
|
170
|
-
onComplete: () => {
|
|
197
|
+
onComplete: (result) => {
|
|
171
198
|
compacting = false;
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
const decision = resumeDecisionAfterCompaction({
|
|
200
|
+
estimatedTokensAfter: result.estimatedTokensAfter,
|
|
201
|
+
threshold,
|
|
202
|
+
idle: ctx.isIdle(),
|
|
203
|
+
hasPending: ctx.hasPendingMessages(),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (decision === "loop") {
|
|
207
|
+
// Still over the line (e.g. large minimumTokens floor on a small model).
|
|
208
|
+
// Latch the auto-trigger off so the resume turn doesn't re-fire
|
|
209
|
+
// agent_settled and compact every turn. Reset by /compact or new session.
|
|
210
|
+
triggerDisabled = true;
|
|
211
|
+
ctx.ui.notify(
|
|
212
|
+
`Compaction: ${(result.estimatedTokensAfter ?? 0).toLocaleString()} tok still ≥ ${threshold.toLocaleString()} tok threshold — auto-trigger disabled for this session`,
|
|
213
|
+
"warning",
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (decision === "skip") {
|
|
219
|
+
// User is already driving; skipping avoids colliding with their in-flight
|
|
220
|
+
// prompt and avoids overriding an explicit redirect. The fresh summary is
|
|
221
|
+
// already in context, so the agent continues without a nudge.
|
|
222
|
+
ctx.ui.notify("Compaction: resume nudge skipped (your prompt is in flight)", "info");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Idle: nudge the agent to pick its task back up. Visible as a normal user
|
|
227
|
+
// message in the transcript (no hidden automation). deliverAs "followUp" is
|
|
228
|
+
// a backstop for the narrow await-window where a prompt starts between the
|
|
229
|
+
// isIdle() check and this send; pi swallows a residual rejection into its
|
|
230
|
+
// own emitError, so it can't crash the session. The try/catch only guards a
|
|
231
|
+
// stale-ctx throw from sendUserMessage.
|
|
174
232
|
ctx.ui.notify("Compaction: resuming task", "info");
|
|
175
|
-
|
|
233
|
+
try {
|
|
234
|
+
pi.sendUserMessage(RESUME_NUDGE, { deliverAs: "followUp" });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
237
|
+
ctx.ui.notify(`Compaction: resume nudge skipped (${message})`, "warning");
|
|
238
|
+
}
|
|
176
239
|
},
|
|
177
240
|
onError: (err) => {
|
|
178
241
|
compacting = false;
|
package/src/once.test.ts
CHANGED
|
@@ -152,7 +152,7 @@ describe("member factory dedupe (pix-commands)", () => {
|
|
|
152
152
|
registerCommand(name: string) {
|
|
153
153
|
commandNames.push(name);
|
|
154
154
|
},
|
|
155
|
-
|
|
155
|
+
registerEntryRenderer() {},
|
|
156
156
|
on() {},
|
|
157
157
|
} as never;
|
|
158
158
|
return { pi, commandNames };
|