@xynogen/pix-core 0.4.52 → 0.4.59
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 +5 -3
- package/package.json +9 -8
- package/src/compaction.test.ts +12 -0
- package/src/compaction.ts +183 -0
- package/src/extension.ts +4 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Pi activates extensions per installed package via each package's `pi.extensions`
|
|
|
22
22
|
| `pix-diagnostics` | Compact LSP diagnostic widget |
|
|
23
23
|
| `pix-display` | Paste chip rendering + thinking block display |
|
|
24
24
|
| `pix-prompts` | System-prompt injection (AGENTS.md + repo directive files) |
|
|
25
|
-
| `pix-skills` | Agent skill loader (`read_skills` tool +
|
|
25
|
+
| `pix-skills` | Agent skill loader (`read_skills` tool + bundled on-demand skills, including TOON) |
|
|
26
26
|
| `pix-nudge` | Tool + capability nudge hooks |
|
|
27
27
|
|
|
28
28
|
**Tool suite** (drop-in replacements for Pi's built-in tools)
|
|
@@ -49,10 +49,12 @@ Pi activates extensions per installed package via each package's `pi.extensions`
|
|
|
49
49
|
|
|
50
50
|
| Package | Description |
|
|
51
51
|
|---|---|
|
|
52
|
-
| `pix-data` | Shared model data layer (
|
|
53
|
-
| `pix-
|
|
52
|
+
| `pix-data` | Shared model data layer (modelgrep + BenchLM) cached at `~/.cache/pi` |
|
|
53
|
+
| `pix-runtime` | Unified `~/.pi/agent/pix.json` config runtime, `/pix` settings command, once guard, and auto-collapse policy |
|
|
54
|
+
| `pix-optimizer` | Caveman mode + RTK tool rewriting + ponytail lazy-dev mode (`/optimizer`) |
|
|
54
55
|
| `pix-gate` | Permission gate for dangerous bash commands |
|
|
55
56
|
| `pix-subagent` | `agent` / `agent_result` / `agent_steer` — planner-driven sub-agents with live widget |
|
|
57
|
+
| `compaction` | Built into pix-core: replaces pi's built-in context compaction with two levers. **Summary prompt is always pix's** — every compaction (manual `/compact`, threshold, overflow) generates the summary with the current conversation model (no silent routing) from an editable source prompt. **Trigger is pix's when `compaction.triggerPercent > 0`** — after each settled turn pix reads live context usage and compacts at `max(contextWindow × triggerPercent, compaction.minimumTokens)`. The default 100K floor prevents low percentages from compacting too early (for example, 10% of a 300K model waits for 100K rather than 30K); `0` disables pix's trigger and lets pi decide, while pix's summary prompt still applies. After a pix-triggered compaction pix sends a short, visible "resume" user message so the agent continues on its own. Every trigger, resume, and summary run emits a notify line (reason, model, token counts). |
|
|
56
58
|
|
|
57
59
|
## Install
|
|
58
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.59",
|
|
4
4
|
"description": "Pi extension bundle \u2014 installs and activates all core pix-* extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/extension.ts",
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@xynogen/pix-runtime": "^0.
|
|
41
|
+
"@xynogen/pix-runtime": "^0.5.1",
|
|
42
42
|
"@xynogen/pix-data": "^0.4.1",
|
|
43
43
|
"@xynogen/pix-pretty": "^1.8.1",
|
|
44
44
|
"@xynogen/pix-welcome": "^0.1.18",
|
|
45
45
|
"@xynogen/pix-footer": "^0.1.20",
|
|
46
46
|
"@xynogen/pix-diagnostics": "^0.1.10",
|
|
47
|
-
"@xynogen/pix-display": "^0.1.
|
|
47
|
+
"@xynogen/pix-display": "^0.1.14",
|
|
48
48
|
"@xynogen/pix-prompts": "^0.3.5",
|
|
49
|
-
"@xynogen/pix-skills": "^0.7.
|
|
50
|
-
"@xynogen/pix-models": "^0.1.
|
|
49
|
+
"@xynogen/pix-skills": "^0.7.6",
|
|
50
|
+
"@xynogen/pix-models": "^0.1.27",
|
|
51
51
|
"@xynogen/pix-update": "^0.1.17",
|
|
52
52
|
"@xynogen/pix-commands": "^0.2.5",
|
|
53
53
|
"@xynogen/pix-nudge": "^0.1.13",
|
|
@@ -59,12 +59,13 @@
|
|
|
59
59
|
"@xynogen/pix-ls": "^0.1.20",
|
|
60
60
|
"@xynogen/pix-bash": "^0.1.18",
|
|
61
61
|
"@xynogen/pix-todo": "^0.1.20",
|
|
62
|
-
"@xynogen/pix-ask": "^0.2.
|
|
63
|
-
"@xynogen/pix-optimizer": "^1.1.
|
|
64
|
-
"@xynogen/pix-gate": "^0.2.
|
|
62
|
+
"@xynogen/pix-ask": "^0.2.15",
|
|
63
|
+
"@xynogen/pix-optimizer": "^1.1.22",
|
|
64
|
+
"@xynogen/pix-gate": "^0.2.2",
|
|
65
65
|
"@xynogen/pix-subagent": "^0.6.3"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
+
"@earendil-works/pi-ai": "*",
|
|
68
69
|
"@earendil-works/pi-coding-agent": "*",
|
|
69
70
|
"@earendil-works/pi-tui": "*"
|
|
70
71
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { compactionThresholdTokens } from "./compaction.ts";
|
|
3
|
+
|
|
4
|
+
describe("compactionThresholdTokens", () => {
|
|
5
|
+
it("uses the 100k floor when the percentage threshold is lower", () => {
|
|
6
|
+
expect(compactionThresholdTokens(300_000, 10, 100_000)).toBe(100_000);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("uses the percentage threshold when it is higher than the floor", () => {
|
|
10
|
+
expect(compactionThresholdTokens(1_000_000, 60, 100_000)).toBe(600_000);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pix-core compaction — replaces pi's built-in compaction so the prompt and
|
|
3
|
+
* trigger are ours to experiment with.
|
|
4
|
+
*
|
|
5
|
+
* Two independent levers (see AGENTS.md product constraints):
|
|
6
|
+
*
|
|
7
|
+
* 1. Summary prompt — ALWAYS ours. `session_before_compact` fires for every
|
|
8
|
+
* compaction (manual, threshold, overflow); we generate the summary with
|
|
9
|
+
* the CURRENT conversation model (no silent routing) using SUMMARY_PROMPT
|
|
10
|
+
* below. Edit the prompt/format to experiment.
|
|
11
|
+
*
|
|
12
|
+
* 2. Trigger — ours when `compaction.triggerPercent > 0`. After each settled
|
|
13
|
+
* turn we read live context usage and call ctx.compact() once token usage
|
|
14
|
+
* reaches the larger of that percentage of the active model's context
|
|
15
|
+
* window and `compaction.minimumTokens`. This prevents low percentages on
|
|
16
|
+
* smaller models from compacting too early. Pi's own threshold stays as an
|
|
17
|
+
* overflow safety net. `triggerPercent === 0` disables our trigger: we
|
|
18
|
+
* keep our prompt but let pi decide when to compact.
|
|
19
|
+
*
|
|
20
|
+
* After a self-triggered compaction we send a short user message (RESUME_NUDGE)
|
|
21
|
+
* so the agent picks its task back up on its own. It's a normal user message —
|
|
22
|
+
* visible in the transcript, no hidden automation.
|
|
23
|
+
*
|
|
24
|
+
* Everything stays visible: each self-trigger, resume, and summary run emit a
|
|
25
|
+
* notify line with reason, model, and token counts.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
29
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
import { config } from "@xynogen/pix-runtime/config";
|
|
32
|
+
import { compactionSection } from "@xynogen/pix-runtime/sections";
|
|
33
|
+
|
|
34
|
+
// ── Editable summary prompt ──────────────────────────────────────────────────
|
|
35
|
+
// Keep-recent-turns strategy: summarize the OLD span into high-signal notes;
|
|
36
|
+
// pi keeps recent turns verbatim (via firstKeptEntryId). This matches current
|
|
37
|
+
// context-engineering guidance — recency is highest-signal, and "context rot"
|
|
38
|
+
// makes full-history replacement risky. Tune freely.
|
|
39
|
+
const SUMMARY_PROMPT = (previousSummary: string | undefined, conversation: string): string => {
|
|
40
|
+
const prior = previousSummary
|
|
41
|
+
? `\n\nPrevious summary (fold in, do not repeat verbatim):\n${previousSummary}`
|
|
42
|
+
: "";
|
|
43
|
+
return `You are summarizing the OLDER part of a coding session. Recent turns are kept verbatim after your summary, so capture only what those recent turns would otherwise lose.${prior}
|
|
44
|
+
|
|
45
|
+
Produce compact, high-signal markdown with these sections (omit a section if empty):
|
|
46
|
+
|
|
47
|
+
## Goal
|
|
48
|
+
## Constraints & Preferences
|
|
49
|
+
## Progress (Done / In Progress / Blocked)
|
|
50
|
+
## Key Decisions (with rationale)
|
|
51
|
+
## Next Steps
|
|
52
|
+
## Critical Context (facts, paths, values needed to continue)
|
|
53
|
+
|
|
54
|
+
Be terse. Prefer names, paths, numbers over prose. No filler.
|
|
55
|
+
|
|
56
|
+
<conversation>
|
|
57
|
+
${conversation}
|
|
58
|
+
</conversation>`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const MAX_SUMMARY_TOKENS = 8192;
|
|
62
|
+
|
|
63
|
+
export function compactionThresholdTokens(
|
|
64
|
+
contextWindow: number,
|
|
65
|
+
triggerPercent: number,
|
|
66
|
+
minimumTokens: number,
|
|
67
|
+
): number {
|
|
68
|
+
return Math.max(minimumTokens, Math.ceil((contextWindow * triggerPercent) / 100));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sent as a user message after a self-triggered compaction so the agent picks
|
|
72
|
+
// its task back up on its own. Kept short — the freshly written summary already
|
|
73
|
+
// carries the goal, progress, and next steps. Edit to taste.
|
|
74
|
+
const RESUME_NUDGE =
|
|
75
|
+
"Context was just compacted. Review the summary above and continue the task from where you left off, following its Next Steps. If the task is already complete, say so.";
|
|
76
|
+
|
|
77
|
+
export default function registerCompaction(pi: ExtensionAPI): void {
|
|
78
|
+
// ── Lever 1: always supply our own summary ────────────────────────────────
|
|
79
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
80
|
+
const { preparation, reason, signal } = event;
|
|
81
|
+
const model = ctx.model;
|
|
82
|
+
if (!model) return; // no model → fall back to pi's built-in compaction
|
|
83
|
+
|
|
84
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
85
|
+
if (!auth.ok) {
|
|
86
|
+
ctx.ui.notify(`Compaction: auth failed (${auth.error}); using built-in`, "warning");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const messages = [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
|
|
91
|
+
const conversation = serializeConversation(convertToLlm(messages));
|
|
92
|
+
ctx.ui.notify(
|
|
93
|
+
`Compaction (${reason}): summarizing ${messages.length} msgs / ${preparation.tokensBefore.toLocaleString()} tok with ${model.id}`,
|
|
94
|
+
"info",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const response = await complete(
|
|
99
|
+
model,
|
|
100
|
+
{
|
|
101
|
+
messages: [
|
|
102
|
+
{
|
|
103
|
+
role: "user" as const,
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text" as const,
|
|
107
|
+
text: SUMMARY_PROMPT(preparation.previousSummary, conversation),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
apiKey: auth.apiKey,
|
|
116
|
+
headers: auth.headers,
|
|
117
|
+
env: auth.env,
|
|
118
|
+
maxTokens: MAX_SUMMARY_TOKENS,
|
|
119
|
+
signal,
|
|
120
|
+
cacheRetention: "none",
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const summary = response.content
|
|
125
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
126
|
+
.map((c) => c.text)
|
|
127
|
+
.join("\n")
|
|
128
|
+
.trim();
|
|
129
|
+
|
|
130
|
+
if (!summary) {
|
|
131
|
+
if (!signal.aborted) ctx.ui.notify("Compaction: empty summary; using built-in", "warning");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
compaction: {
|
|
137
|
+
summary,
|
|
138
|
+
firstKeptEntryId: preparation.firstKeptEntryId,
|
|
139
|
+
tokensBefore: preparation.tokensBefore,
|
|
140
|
+
usage: response.usage,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (signal.aborted) return;
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
ctx.ui.notify(`Compaction failed (${message}); using built-in`, "error");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── Lever 2: our own trigger, driven by live context usage ────────────────
|
|
152
|
+
let compacting = false;
|
|
153
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
154
|
+
if (compacting) return;
|
|
155
|
+
const { triggerPercent, minimumTokens } = config(compactionSection);
|
|
156
|
+
if (triggerPercent <= 0) return;
|
|
157
|
+
|
|
158
|
+
const usage = ctx.getContextUsage?.();
|
|
159
|
+
if (!usage || usage.tokens === null) return;
|
|
160
|
+
const tokens = usage.tokens;
|
|
161
|
+
const threshold = compactionThresholdTokens(usage.contextWindow, triggerPercent, minimumTokens);
|
|
162
|
+
if (tokens < threshold) return;
|
|
163
|
+
|
|
164
|
+
compacting = true;
|
|
165
|
+
ctx.ui.notify(
|
|
166
|
+
`Compaction: ${tokens.toLocaleString()} tok ≥ ${threshold.toLocaleString()} tok threshold (${triggerPercent}% ctx, ${minimumTokens.toLocaleString()} floor) — compacting`,
|
|
167
|
+
"info",
|
|
168
|
+
);
|
|
169
|
+
ctx.compact({
|
|
170
|
+
onComplete: () => {
|
|
171
|
+
compacting = false;
|
|
172
|
+
// Nudge the agent to pick its task back up. Visible as a normal
|
|
173
|
+
// user message in the transcript (no hidden automation).
|
|
174
|
+
ctx.ui.notify("Compaction: resuming task", "info");
|
|
175
|
+
pi.sendUserMessage(RESUME_NUDGE);
|
|
176
|
+
},
|
|
177
|
+
onError: (err) => {
|
|
178
|
+
compacting = false;
|
|
179
|
+
ctx.ui.notify(`Compaction trigger failed: ${err.message}`, "error");
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
package/src/extension.ts
CHANGED
|
@@ -38,6 +38,7 @@ import registerTodo from "@xynogen/pix-todo/src/index.ts";
|
|
|
38
38
|
import registerUpdate from "@xynogen/pix-update/src/extension.ts";
|
|
39
39
|
import registerWelcome from "@xynogen/pix-welcome/src/extension.ts";
|
|
40
40
|
import registerWrite from "@xynogen/pix-write/src/extension.ts";
|
|
41
|
+
import registerCompaction from "./compaction.ts";
|
|
41
42
|
|
|
42
43
|
// Members accept either the full `ExtensionAPI` or pix-pretty's looser
|
|
43
44
|
// `PiPrettyApi` view of it. `ExtensionAPI` satisfies both, so we erase the
|
|
@@ -76,6 +77,9 @@ const MEMBERS: Factory[] = [
|
|
|
76
77
|
registerOptimizer,
|
|
77
78
|
registerGate,
|
|
78
79
|
registerSubagent,
|
|
80
|
+
// Custom compaction: replaces pi's built-in summary + trigger (reads
|
|
81
|
+
// compaction section; runs after runtime is live).
|
|
82
|
+
registerCompaction,
|
|
79
83
|
];
|
|
80
84
|
|
|
81
85
|
export default function (pi: ExtensionAPI): void {
|