@henryqw/pi-auto-compact 0.2.2 → 1.0.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 +27 -22
- package/extensions/auto-compact.ts +116 -10
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# `@henryqw/pi-auto-compact`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Compact context before it hits the configured threshold, then resume the current task.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
pi install npm:@henryqw/pi-task-models
|
|
8
9
|
pi install npm:@henryqw/pi-auto-compact
|
|
9
10
|
```
|
|
10
11
|
|
|
12
|
+
`pi-task-models` provides `/task-models` for the shared compaction routes. Requires Pi Coding Agent 0.84.2+.
|
|
13
|
+
|
|
11
14
|
Disable Pi's built-in auto-compaction in `~/.pi/agent/settings.json`:
|
|
12
15
|
|
|
13
16
|
```json
|
|
@@ -18,17 +21,20 @@ Disable Pi's built-in auto-compaction in `~/.pi/agent/settings.json`:
|
|
|
18
21
|
}
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
Restart Pi after
|
|
24
|
+
Restart Pi after install or settings changes. Trusted project settings in `.pi/settings.json` must not set `compaction.enabled` back to `true`. Manual `/compact` stays available.
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
## Use
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
| Surface | Purpose |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `/auto-compact` | Set the compaction threshold. |
|
|
31
|
+
| `/task-models` | Configure the shared `pi-auto-compact/autoCompact` profile (default `balanced`). |
|
|
32
|
+
|
|
33
|
+
Refuses to activate unless effective `compaction.enabled` is `false`. Checks `turn_start`, tool-call `turn_end`, `agent_end`, `context`, and resumed or forked `session_start`. Tries the assigned profile primary, then fallback; if neither route works, the current session model still compacts. After mid-task compaction, a follow-up message continues the current task.
|
|
28
34
|
|
|
29
|
-
##
|
|
35
|
+
## Config
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
`~/.pi/agent/config/pi-auto-compact.json`
|
|
32
38
|
|
|
33
39
|
```json
|
|
34
40
|
{
|
|
@@ -36,25 +42,24 @@ Run `/auto-compact`, then enter threshold percentage. Config lives in `~/.pi/age
|
|
|
36
42
|
}
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
Threshold must be at least 25
|
|
40
|
-
|
|
41
|
-
## Behavior
|
|
45
|
+
Threshold must be at least 25 and below 100. Missing config defaults to 50. Model routes live in `~/.pi/agent/config/pi-task-models.json`. Malformed shared task-model config is reported and left unchanged.
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
- Checks `turn_start`, tool-call `turn_end`, `agent_end`, `context`, and resumed/forked `session_start`.
|
|
45
|
-
- Uses Pi's default `ctx.compact()` summary and session persistence.
|
|
46
|
-
- Keeps newest 15% as temporary emergency context while compaction runs.
|
|
47
|
-
- Sends a follow-up message after mid-task compaction so task execution continues; final-answer compaction stays idle.
|
|
48
|
-
- Compacts above configured `autoCompactThreshold` percentage (50% by default).
|
|
47
|
+
## Remove
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
```bash
|
|
50
|
+
pi remove npm:@henryqw/pi-auto-compact
|
|
51
|
+
```
|
|
51
52
|
|
|
52
53
|
## Development
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
|
-
npm test
|
|
56
|
-
npm run
|
|
57
|
-
npm run
|
|
56
|
+
npm test --workspace @henryqw/pi-auto-compact
|
|
57
|
+
npm run typecheck --workspace @henryqw/pi-auto-compact
|
|
58
|
+
npm run pack:check --workspace @henryqw/pi-auto-compact
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
`test:live`
|
|
61
|
+
`test:live` needs real Pi plus authenticated model access. Set `PI_AUTO_COMPACT_AUTH_FILE` when auth is not at `~/.pi/agent/auth.json`.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm run test:live --workspace @henryqw/pi-auto-compact
|
|
65
|
+
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import {
|
|
4
|
+
compact,
|
|
4
5
|
estimateTokens,
|
|
5
6
|
getAgentDir,
|
|
6
7
|
SettingsManager,
|
|
@@ -9,6 +10,12 @@ import type {
|
|
|
9
10
|
ExtensionAPI,
|
|
10
11
|
ExtensionContext,
|
|
11
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
orderedProfileRoutes,
|
|
15
|
+
readTaskModelsConfig,
|
|
16
|
+
resolveTaskModelRoute,
|
|
17
|
+
type ResolvedTaskRoute,
|
|
18
|
+
} from "@henryqw/pi-task-models";
|
|
12
19
|
|
|
13
20
|
type AgentMessage = Parameters<typeof estimateTokens>[0];
|
|
14
21
|
|
|
@@ -24,13 +31,19 @@ type AgentMessage = Parameters<typeof estimateTokens>[0];
|
|
|
24
31
|
*/
|
|
25
32
|
const DEFAULT_COMPACT_THRESHOLD_PERCENT = 50;
|
|
26
33
|
const MIN_COMPACT_THRESHOLD_PERCENT = 25;
|
|
34
|
+
const AUTO_COMPACT_TASK = "pi-auto-compact/autoCompact";
|
|
35
|
+
const DEFAULT_AUTO_COMPACT_PROFILE = "balanced" as const;
|
|
27
36
|
const configPath = () => join(getAgentDir(), "config", "pi-auto-compact.json");
|
|
28
37
|
|
|
38
|
+
type Config = {
|
|
39
|
+
autoCompactThreshold: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
29
42
|
function isValidThreshold(value: unknown): value is number {
|
|
30
43
|
return typeof value === "number" && Number.isFinite(value) && value >= MIN_COMPACT_THRESHOLD_PERCENT && value < 100;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
function readConfig():
|
|
46
|
+
function readConfig(): Config {
|
|
34
47
|
let value: unknown;
|
|
35
48
|
try {
|
|
36
49
|
value = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
@@ -50,14 +63,46 @@ function readConfig(): { autoCompactThreshold: number } {
|
|
|
50
63
|
return { autoCompactThreshold: threshold };
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
function writeConfig(
|
|
66
|
+
function writeConfig(config: Config): void {
|
|
54
67
|
const file = configPath();
|
|
55
68
|
mkdirSync(dirname(file), { recursive: true });
|
|
56
|
-
writeFileSync(file, `${JSON.stringify(
|
|
69
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function configuredTaskRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] | undefined {
|
|
73
|
+
let config;
|
|
74
|
+
try {
|
|
75
|
+
config = readTaskModelsConfig();
|
|
76
|
+
} catch {
|
|
77
|
+
ctx.ui.notify("Couldn't read task model config; using current session model.", "error");
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const profileName = config.tasks[AUTO_COMPACT_TASK] ?? DEFAULT_AUTO_COMPACT_PROFILE;
|
|
82
|
+
const profile = config.profiles[profileName];
|
|
83
|
+
if (!profile) {
|
|
84
|
+
ctx.ui.notify(`Task model profile ${profileName} is not configured; using current session model.`, "error");
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const routes = orderedProfileRoutes(profile)
|
|
89
|
+
.map((route) => resolveTaskModelRoute(ctx, route))
|
|
90
|
+
.filter((route): route is ResolvedTaskRoute => route !== undefined);
|
|
91
|
+
if (!routes.length) {
|
|
92
|
+
ctx.ui.notify(`No usable ${profileName} task model route; using current session model.`, "error");
|
|
93
|
+
}
|
|
94
|
+
return routes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function withoutDeletedHeaders(headers: Record<string, string | null> | undefined): Record<string, string> | undefined {
|
|
98
|
+
return headers
|
|
99
|
+
? Object.fromEntries(Object.entries(headers).filter((entry): entry is [string, string] => entry[1] !== null))
|
|
100
|
+
: undefined;
|
|
57
101
|
}
|
|
58
102
|
|
|
59
103
|
// Emergency context guard keeps recent messages while default compaction runs.
|
|
60
104
|
const KEEP_RECENT_PERCENT = 15;
|
|
105
|
+
const COMPACTION_INSTRUCTIONS = "Preserve current task to be resumed after compaction.";
|
|
61
106
|
const RESUME_MESSAGE = "Auto-compact ran. Continue the current task.";
|
|
62
107
|
const COMPACTION_ABORT_ERROR = "This operation was aborted";
|
|
63
108
|
const ACTIVATION_ERROR =
|
|
@@ -127,6 +172,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
172
|
const runCompaction = (ctx: ExtensionContext, resumeTask = true) => {
|
|
128
173
|
compactionAbortExpected = Boolean(ctx.signal && !ctx.signal.aborted);
|
|
129
174
|
ctx.compact({
|
|
175
|
+
customInstructions: COMPACTION_INSTRUCTIONS,
|
|
130
176
|
onComplete: () => {
|
|
131
177
|
compactionPending = false;
|
|
132
178
|
compactionAbortExpected = false;
|
|
@@ -209,19 +255,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
255
|
});
|
|
210
256
|
|
|
211
257
|
pi.registerCommand("auto-compact", {
|
|
212
|
-
description: "
|
|
213
|
-
handler: async (
|
|
214
|
-
|
|
258
|
+
description: "configure automatic compaction threshold",
|
|
259
|
+
handler: async (args, ctx) => {
|
|
260
|
+
if (args.trim()) {
|
|
261
|
+
ctx.ui.notify("Usage: /auto-compact", "error");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let config: Config;
|
|
215
266
|
try {
|
|
216
|
-
|
|
267
|
+
config = readConfig();
|
|
217
268
|
} catch {
|
|
218
269
|
ctx.ui.notify("Couldn't read pi-auto-compact config.", "error");
|
|
219
270
|
return;
|
|
220
271
|
}
|
|
221
272
|
|
|
222
273
|
const input = await ctx.ui.input(
|
|
223
|
-
`Auto-compact threshold (%) · current: ${
|
|
224
|
-
"Enter a number
|
|
274
|
+
`Auto-compact threshold (%) · current: ${config.autoCompactThreshold}`,
|
|
275
|
+
"Enter a number at least 25 and below 100",
|
|
225
276
|
);
|
|
226
277
|
if (input === undefined) return;
|
|
227
278
|
|
|
@@ -236,7 +287,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
287
|
}
|
|
237
288
|
|
|
238
289
|
try {
|
|
239
|
-
writeConfig(threshold);
|
|
290
|
+
writeConfig({ autoCompactThreshold: threshold });
|
|
240
291
|
} catch {
|
|
241
292
|
ctx.ui.notify("Couldn't save pi-auto-compact config.", "error");
|
|
242
293
|
return;
|
|
@@ -264,4 +315,59 @@ export default function (pi: ExtensionAPI) {
|
|
|
264
315
|
// Resume/fork can load an already-large session before first turn.
|
|
265
316
|
if (event.reason === "resume" || event.reason === "fork") compactIfNeeded(ctx);
|
|
266
317
|
});
|
|
318
|
+
|
|
319
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
320
|
+
if (
|
|
321
|
+
!active ||
|
|
322
|
+
!compactionPending ||
|
|
323
|
+
event.customInstructions !== COMPACTION_INSTRUCTIONS
|
|
324
|
+
) return;
|
|
325
|
+
|
|
326
|
+
// Pi omits details from prior extension compactions when preparing next run.
|
|
327
|
+
const previous = [...event.branchEntries].reverse().find((entry) => entry.type === "compaction");
|
|
328
|
+
if (previous?.details && typeof previous.details === "object") {
|
|
329
|
+
const details = previous.details as { readFiles?: unknown; modifiedFiles?: unknown };
|
|
330
|
+
if (Array.isArray(details.readFiles)) {
|
|
331
|
+
for (const path of details.readFiles) {
|
|
332
|
+
if (typeof path === "string") event.preparation.fileOps.read.add(path);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(details.modifiedFiles)) {
|
|
336
|
+
for (const path of details.modifiedFiles) {
|
|
337
|
+
if (typeof path === "string") event.preparation.fileOps.edited.add(path);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const routes = configuredTaskRoutes(ctx);
|
|
343
|
+
if (!routes?.length) return;
|
|
344
|
+
|
|
345
|
+
for (const route of routes) {
|
|
346
|
+
try {
|
|
347
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(route.model);
|
|
348
|
+
if (!auth.ok) continue;
|
|
349
|
+
|
|
350
|
+
const requestModel = auth.baseUrl ? { ...route.model, baseUrl: auth.baseUrl } : route.model;
|
|
351
|
+
return {
|
|
352
|
+
compaction: await compact(
|
|
353
|
+
event.preparation,
|
|
354
|
+
requestModel,
|
|
355
|
+
auth.apiKey,
|
|
356
|
+
withoutDeletedHeaders(auth.headers),
|
|
357
|
+
event.customInstructions,
|
|
358
|
+
event.signal,
|
|
359
|
+
route.thinkingLevel,
|
|
360
|
+
undefined,
|
|
361
|
+
auth.env,
|
|
362
|
+
),
|
|
363
|
+
};
|
|
364
|
+
} catch {
|
|
365
|
+
if (event.signal.aborted) return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!event.signal.aborted) {
|
|
370
|
+
ctx.ui.notify("Configured task model routes failed; using current session model.", "error");
|
|
371
|
+
}
|
|
372
|
+
});
|
|
267
373
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-auto-compact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Proactively compact Pi context at a configurable threshold and resume the current task.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"pack:check": "npm pack --dry-run"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-
|
|
28
|
+
"@earendil-works/pi-ai": "^0.84.2",
|
|
29
|
+
"@earendil-works/pi-coding-agent": "^0.84.2"
|
|
29
30
|
},
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
@@ -42,5 +43,8 @@
|
|
|
42
43
|
"extensions": [
|
|
43
44
|
"./extensions"
|
|
44
45
|
]
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@henryqw/pi-task-models": "^0.1.0"
|
|
45
49
|
}
|
|
46
50
|
}
|