@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 CHANGED
@@ -1,13 +1,16 @@
1
1
  # `@henryqw/pi-auto-compact`
2
2
 
3
- Pi extension that compacts context before it reaches 50% of current model context, then resumes current task.
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 installation or settings changes. Trusted project settings in `.pi/settings.json` must not override `compaction.enabled` back to `true`. Manual `/compact` remains available.
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
- Remove with:
26
+ ## Use
24
27
 
25
- ```bash
26
- pi remove npm:@henryqw/pi-auto-compact
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
- ## Configure
35
+ ## Config
30
36
 
31
- Run `/auto-compact`, then enter threshold percentage. Config lives in `~/.pi/agent/config/pi-auto-compact.json`:
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% and below 100%; lower values are not meaningful. Missing config defaults to 50%. Restart or `/reload` after manual edits; command changes apply immediately.
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
- - Refuses activation with an error when Pi's effective `compaction.enabled` setting is not `false`; competing automatic compactors can start duplicate summaries.
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
- `ctx.compact()` aborts current low-level run. Extension hides that empty internal abort message, then starts new run with current task resume message. Other aborts and provider errors remain visible.
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 pack:check
57
- npm run test:live
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` uses real Pi plus authenticated model access. It disables Pi's built-in auto-compaction, sets a temporary 12K context window, sends a large prompt, and verifies extension compaction, automatic resume, persisted resume message, and assistant response. Set `PI_AUTO_COMPACT_AUTH_FILE` when auth is not at `~/.pi/agent/auth.json`.
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(): { autoCompactThreshold: number } {
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(autoCompactThreshold: number): void {
66
+ function writeConfig(config: Config): void {
54
67
  const file = configPath();
55
68
  mkdirSync(dirname(file), { recursive: true });
56
- writeFileSync(file, `${JSON.stringify({ autoCompactThreshold }, null, 2)}\n`);
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: "set automatic compaction threshold",
213
- handler: async (_args, ctx) => {
214
- let current: number;
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
- current = readConfig().autoCompactThreshold;
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: ${current}`,
224
- "Enter a number above 0 and below 100",
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.2.2",
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-coding-agent": "*"
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
  }