@lll9p/pi-better-compaction 0.4.0 → 0.6.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/README.md +6 -0
- package/README.zh-CN.md +7 -1
- package/package.json +2 -1
- package/src/config.ts +23 -0
- package/src/extension-runtime.ts +54 -15
- package/src/midrun.ts +229 -0
- package/src/types.ts +10 -0
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ If the file doesn't exist, all defaults apply. The extension never creates this
|
|
|
44
44
|
```jsonc
|
|
45
45
|
{
|
|
46
46
|
"enabled": true,
|
|
47
|
+
"midRun": {
|
|
48
|
+
"enabled": false,
|
|
49
|
+
"thresholdPercent": 80
|
|
50
|
+
},
|
|
47
51
|
"compactionVersion": "v2",
|
|
48
52
|
"compactionModel": null,
|
|
49
53
|
"compactionThinkingLevel": "off",
|
|
@@ -65,6 +69,8 @@ If the file doesn't exist, all defaults apply. The extension never creates this
|
|
|
65
69
|
| Option | Type | Default | Description |
|
|
66
70
|
|--------|------|---------|-------------|
|
|
67
71
|
| `enabled` | `boolean` | `true` | Master switch. Set `false` to disable the extension entirely. |
|
|
72
|
+
| `midRun.enabled` | `boolean` | `false` | Reserved and currently ignored. The mid-run guard remains disabled even when set to `true`. |
|
|
73
|
+
| `midRun.thresholdPercent` | `number` | `80` | Reserved threshold for the disabled mid-run guard. Must be greater than 0 and at most 100. |
|
|
68
74
|
| `compactionVersion` | `"v1" \| "v2"` | `"v2"` | Protocol for Responses-family APIs. **V2** (streaming, encrypted blob) is the current OpenAI default. **V1** uses the legacy `/responses/compact` endpoint. |
|
|
69
75
|
| `compactionModel` | `string \| null` | `null` | Model for fallback compaction (non-Responses APIs, or when native compact fails). Format: `"provider/model-id"`, e.g. `"openai/gpt-5.1-mini"`. `null` = let pi use the current chat model. |
|
|
70
76
|
| `compactionThinkingLevel` | `string` | `"off"` | Thinking level for the fallback compaction model. One of: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. |
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
一个 [pi](https://github.com/nicepkg/pi)
|
|
5
|
+
一个 [pi](https://github.com/nicepkg/pi) 扩展,通过两条协同策略提升上下文压缩效果:
|
|
6
6
|
|
|
7
7
|
1. **OpenAI Responses 系列 API** 使用提供商原生压缩端点,保留纯文本摘要无法留存的不透明上下文。
|
|
8
8
|
2. **其他所有 API**(Anthropic、Gemini 等)可用一个**独立的低成本模型**执行 pi 内置压缩,避免在主模型上消耗额度。
|
|
@@ -44,6 +44,10 @@ cd pi-better-compaction && pi install .
|
|
|
44
44
|
```jsonc
|
|
45
45
|
{
|
|
46
46
|
"enabled": true,
|
|
47
|
+
"midRun": {
|
|
48
|
+
"enabled": false,
|
|
49
|
+
"thresholdPercent": 80
|
|
50
|
+
},
|
|
47
51
|
"compactionVersion": "v2",
|
|
48
52
|
"compactionModel": null,
|
|
49
53
|
"compactionThinkingLevel": "off",
|
|
@@ -65,6 +69,8 @@ cd pi-better-compaction && pi install .
|
|
|
65
69
|
| 选项 | 类型 | 默认值 | 说明 |
|
|
66
70
|
|------|------|--------|------|
|
|
67
71
|
| `enabled` | `boolean` | `true` | 总开关。设为 `false` 完全禁用扩展。 |
|
|
72
|
+
| `midRun.enabled` | `boolean` | `false` | 保留配置,当前会被忽略。即使设为 `true`,mid-run guard 仍保持禁用。 |
|
|
73
|
+
| `midRun.thresholdPercent` | `number` | `80` | 已禁用的 mid-run guard 的保留阈值。必须大于 0 且不超过 100。 |
|
|
68
74
|
| `compactionVersion` | `"v1" \| "v2"` | `"v2"` | Responses 系列 API 的压缩协议。**V2**(流式,加密 blob)是 OpenAI 当前默认协议;**V1** 使用旧版 `/responses/compact` 端点。 |
|
|
69
75
|
| `compactionModel` | `string \| null` | `null` | 回退压缩使用的模型(用于非 Responses API,或原生压缩失败时)。格式:`"provider/model-id"`,如 `"openai/gpt-5.1-mini"`。`null` = 由 pi 使用当前对话模型。 |
|
|
70
76
|
| `compactionThinkingLevel` | `string` | `"off"` | 回退压缩模型的思考级别。可选:`off`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`。 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lll9p/pi-better-compaction",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Better compaction for pi: native /responses/compact replay for OpenAI Responses APIs, plus a configurable compaction model driving pi's native summarization everywhere else.",
|
|
6
6
|
"author": "Lilin Lao",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"src/debug.ts",
|
|
39
39
|
"src/details-store.ts",
|
|
40
40
|
"src/extension-runtime.ts",
|
|
41
|
+
"src/midrun.ts",
|
|
41
42
|
"src/native-fallback.ts",
|
|
42
43
|
"src/payload-rewrite.ts",
|
|
43
44
|
"src/request-context-cache.ts",
|
package/src/config.ts
CHANGED
|
@@ -60,6 +60,15 @@ function toBoolean(value: unknown, fieldPath: string, warnings: string[]): boole
|
|
|
60
60
|
return undefined;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function toThresholdPercent(value: unknown, fieldPath: string, warnings: string[]): number | undefined {
|
|
64
|
+
if (value === undefined) return undefined;
|
|
65
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 100) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
warnings.push(`Ignoring ${fieldPath}: expected a number greater than 0 and at most 100.`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
|
|
64
73
|
if (value === undefined) return undefined;
|
|
65
74
|
// Explicit null clears a spec, matching the documented "unset = current model" behavior.
|
|
@@ -119,6 +128,7 @@ export function loadExtensionConfig(configPath: string = CONFIG_PATH): LoadedExt
|
|
|
119
128
|
const warnings: string[] = [];
|
|
120
129
|
const resolved: ExtensionConfig = {
|
|
121
130
|
...DEFAULT_EXTENSION_CONFIG,
|
|
131
|
+
midRun: { ...DEFAULT_EXTENSION_CONFIG.midRun },
|
|
122
132
|
responsesCompactApis: [...DEFAULT_EXTENSION_CONFIG.responsesCompactApis],
|
|
123
133
|
};
|
|
124
134
|
let source: string | undefined;
|
|
@@ -128,6 +138,19 @@ export function loadExtensionConfig(configPath: string = CONFIG_PATH): LoadedExt
|
|
|
128
138
|
source = configPath;
|
|
129
139
|
|
|
130
140
|
resolved.enabled = toBoolean(raw.enabled, "enabled", warnings) ?? resolved.enabled;
|
|
141
|
+
|
|
142
|
+
if (raw.midRun === undefined) {
|
|
143
|
+
// Keep defaults.
|
|
144
|
+
} else if (isRecord(raw.midRun)) {
|
|
145
|
+
resolved.midRun.enabled =
|
|
146
|
+
toBoolean(raw.midRun.enabled, "midRun.enabled", warnings) ?? resolved.midRun.enabled;
|
|
147
|
+
resolved.midRun.thresholdPercent =
|
|
148
|
+
toThresholdPercent(raw.midRun.thresholdPercent, "midRun.thresholdPercent", warnings) ??
|
|
149
|
+
resolved.midRun.thresholdPercent;
|
|
150
|
+
} else {
|
|
151
|
+
warnings.push("Ignoring midRun: expected a JSON object.");
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
resolved.allowCompactionContinuityBreak =
|
|
132
155
|
toBoolean(raw.allowCompactionContinuityBreak, "allowCompactionContinuityBreak", warnings) ??
|
|
133
156
|
resolved.allowCompactionContinuityBreak;
|
package/src/extension-runtime.ts
CHANGED
|
@@ -40,6 +40,20 @@ type ResponsesCompactOutcome =
|
|
|
40
40
|
| { outcome: "aborted" }
|
|
41
41
|
| { outcome: "failed" };
|
|
42
42
|
|
|
43
|
+
export type ExtensionRuntimeDependencies = {
|
|
44
|
+
loadExtensionConfig: typeof loadExtensionConfig;
|
|
45
|
+
executeNativeCompaction: typeof executeNativeCompaction;
|
|
46
|
+
executeV2Compaction: typeof executeV2Compaction;
|
|
47
|
+
runNativeFallbackCompaction: typeof runNativeFallbackCompaction;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const DEFAULT_DEPENDENCIES: ExtensionRuntimeDependencies = {
|
|
51
|
+
loadExtensionConfig,
|
|
52
|
+
executeNativeCompaction,
|
|
53
|
+
executeV2Compaction,
|
|
54
|
+
runNativeFallbackCompaction,
|
|
55
|
+
};
|
|
56
|
+
|
|
43
57
|
function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
|
|
44
58
|
return {
|
|
45
59
|
tokensBefore: event.preparation.tokensBefore,
|
|
@@ -91,7 +105,7 @@ function buildCompactionInstructions(systemPrompt: string, customInstructions?:
|
|
|
91
105
|
return systemPrompt;
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
return `${systemPrompt}\n\nAdditional
|
|
108
|
+
return `${systemPrompt}\n\nAdditional compaction guidance:\n${guidance}`;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
async function runResponsesV1Compact(
|
|
@@ -99,6 +113,7 @@ async function runResponsesV1Compact(
|
|
|
99
113
|
ctx: ExtensionContext,
|
|
100
114
|
config: ExtensionConfig,
|
|
101
115
|
runtime: NativeCompactionRuntime,
|
|
116
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
102
117
|
): Promise<ResponsesCompactOutcome> {
|
|
103
118
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
104
119
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
@@ -161,7 +176,7 @@ async function runResponsesV1Compact(
|
|
|
161
176
|
request = { ...request, ...extras };
|
|
162
177
|
}
|
|
163
178
|
|
|
164
|
-
const compactResult = await executeNativeCompaction({
|
|
179
|
+
const compactResult = await dependencies.executeNativeCompaction({
|
|
165
180
|
runtime,
|
|
166
181
|
request,
|
|
167
182
|
signal: event.signal,
|
|
@@ -251,6 +266,7 @@ async function runResponsesV2Compact(
|
|
|
251
266
|
ctx: ExtensionContext,
|
|
252
267
|
config: ExtensionConfig,
|
|
253
268
|
runtime: NativeCompactionRuntime,
|
|
269
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
254
270
|
): Promise<ResponsesCompactOutcome> {
|
|
255
271
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
256
272
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
@@ -311,7 +327,7 @@ async function runResponsesV2Compact(
|
|
|
311
327
|
request = { ...request, ...extras };
|
|
312
328
|
}
|
|
313
329
|
|
|
314
|
-
const v2Result = await executeV2Compaction({
|
|
330
|
+
const v2Result = await dependencies.executeV2Compaction({
|
|
315
331
|
runtime,
|
|
316
332
|
request,
|
|
317
333
|
signal: event.signal,
|
|
@@ -400,8 +416,12 @@ async function runResponsesV2Compact(
|
|
|
400
416
|
return { outcome: "success", compaction };
|
|
401
417
|
}
|
|
402
418
|
|
|
403
|
-
async function handleSessionBeforeCompact(
|
|
404
|
-
|
|
419
|
+
async function handleSessionBeforeCompact(
|
|
420
|
+
event: SessionBeforeCompactEvent,
|
|
421
|
+
ctx: ExtensionContext,
|
|
422
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
423
|
+
) {
|
|
424
|
+
const { config } = dependencies.loadExtensionConfig();
|
|
405
425
|
if (!config.enabled) {
|
|
406
426
|
return undefined;
|
|
407
427
|
}
|
|
@@ -436,9 +456,9 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
436
456
|
let responsesOutcome: ResponsesCompactOutcome;
|
|
437
457
|
|
|
438
458
|
if (config.compactionVersion === "v2") {
|
|
439
|
-
responsesOutcome = await runResponsesV2Compact(event, ctx, config, resolution.runtime);
|
|
459
|
+
responsesOutcome = await runResponsesV2Compact(event, ctx, config, resolution.runtime, dependencies);
|
|
440
460
|
} else {
|
|
441
|
-
responsesOutcome = await runResponsesV1Compact(event, ctx, config, resolution.runtime);
|
|
461
|
+
responsesOutcome = await runResponsesV1Compact(event, ctx, config, resolution.runtime, dependencies);
|
|
442
462
|
}
|
|
443
463
|
|
|
444
464
|
if (responsesOutcome.outcome === "success") {
|
|
@@ -465,7 +485,12 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
465
485
|
}
|
|
466
486
|
|
|
467
487
|
// Branch 2: run pi's native compaction method with the configured model.
|
|
468
|
-
const fallback = await runNativeFallbackCompaction({
|
|
488
|
+
const fallback = await dependencies.runNativeFallbackCompaction({
|
|
489
|
+
ctx,
|
|
490
|
+
event,
|
|
491
|
+
config,
|
|
492
|
+
sessionId: getSessionId(ctx),
|
|
493
|
+
});
|
|
469
494
|
if (fallback.ok) {
|
|
470
495
|
if (ctx.hasUI) {
|
|
471
496
|
ctx.ui.notify(
|
|
@@ -514,8 +539,12 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
514
539
|
return undefined;
|
|
515
540
|
}
|
|
516
541
|
|
|
517
|
-
async function handleBeforeProviderRequest(
|
|
518
|
-
|
|
542
|
+
async function handleBeforeProviderRequest(
|
|
543
|
+
event: BeforeProviderRequestEvent,
|
|
544
|
+
ctx: ExtensionContext,
|
|
545
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
546
|
+
) {
|
|
547
|
+
const { config } = dependencies.loadExtensionConfig();
|
|
519
548
|
if (!config.enabled) {
|
|
520
549
|
return undefined;
|
|
521
550
|
}
|
|
@@ -638,9 +667,13 @@ async function handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ct
|
|
|
638
667
|
return rewrite.rewrittenPayload;
|
|
639
668
|
}
|
|
640
669
|
|
|
641
|
-
export
|
|
670
|
+
export function registerExtensionRuntime(
|
|
671
|
+
pi: ExtensionAPI,
|
|
672
|
+
dependencies: ExtensionRuntimeDependencies = DEFAULT_DEPENDENCIES,
|
|
673
|
+
): void {
|
|
674
|
+
// Mid-run compaction is intentionally disabled regardless of config.
|
|
642
675
|
pi.on("session_start", (_event, ctx) => {
|
|
643
|
-
const { config, source, warnings } = loadExtensionConfig();
|
|
676
|
+
const { config, source, warnings } = dependencies.loadExtensionConfig();
|
|
644
677
|
if (!config.enabled) return;
|
|
645
678
|
|
|
646
679
|
if (warnings.length > 0 && ctx.hasUI && config.debug) {
|
|
@@ -669,11 +702,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
669
702
|
}
|
|
670
703
|
});
|
|
671
704
|
|
|
672
|
-
pi.on("session_before_compact",
|
|
673
|
-
|
|
705
|
+
pi.on("session_before_compact", (event, ctx) =>
|
|
706
|
+
handleSessionBeforeCompact(event, ctx, dependencies),
|
|
707
|
+
);
|
|
708
|
+
pi.on("before_provider_request", (event, ctx) =>
|
|
709
|
+
handleBeforeProviderRequest(event, ctx, dependencies),
|
|
710
|
+
);
|
|
674
711
|
|
|
675
712
|
pi.on("session_compact_failed", (event, ctx) => {
|
|
676
|
-
const { config } = loadExtensionConfig();
|
|
713
|
+
const { config } = dependencies.loadExtensionConfig();
|
|
677
714
|
if (!config.enabled) return;
|
|
678
715
|
|
|
679
716
|
writeDebugArtifact(
|
|
@@ -691,3 +728,5 @@ export default function (pi: ExtensionAPI) {
|
|
|
691
728
|
);
|
|
692
729
|
});
|
|
693
730
|
}
|
|
731
|
+
|
|
732
|
+
export default registerExtensionRuntime;
|
package/src/midrun.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadExtensionConfig } from "./config";
|
|
3
|
+
import { writeDebugArtifact } from "./debug";
|
|
4
|
+
import { EXTENSION_ID, type LoadedExtensionConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
type MidRunPhase = "idle" | "abort-pending" | "compacting" | "resume-pending" | "failed";
|
|
7
|
+
|
|
8
|
+
type MidRunState = {
|
|
9
|
+
phase: MidRunPhase;
|
|
10
|
+
generation: number;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
baselineCompactionId?: string;
|
|
13
|
+
triggerTokens?: number;
|
|
14
|
+
triggerPercent?: number;
|
|
15
|
+
triggerContextWindow?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ConfigLoader = () => LoadedExtensionConfig;
|
|
19
|
+
|
|
20
|
+
const RESUME_CUSTOM_TYPE = "pi-better-compaction-midrun-resume";
|
|
21
|
+
const RESUME_PROMPT = `[pi-better-compaction/midrun]
|
|
22
|
+
Context compaction completed. Continue the interrupted task from the exact point where execution stopped.`;
|
|
23
|
+
const MIDRUN_COMPACTION_INSTRUCTIONS =
|
|
24
|
+
"Preserve the active task, completed work, decisions, changed files, failures, current tool-loop state, and exact next steps so execution can resume immediately after compaction.";
|
|
25
|
+
|
|
26
|
+
function getSessionId(ctx: ExtensionContext): string | undefined {
|
|
27
|
+
try {
|
|
28
|
+
return ctx.sessionManager.getSessionId();
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getLatestCompactionId(ctx: ExtensionContext): string | undefined {
|
|
35
|
+
const branch = ctx.sessionManager.getBranch();
|
|
36
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
37
|
+
const entry = branch[index];
|
|
38
|
+
if (entry?.type === "compaction") return entry.id;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetState(state: MidRunState): void {
|
|
44
|
+
state.phase = "idle";
|
|
45
|
+
state.generation += 1;
|
|
46
|
+
state.sessionId = undefined;
|
|
47
|
+
state.baselineCompactionId = undefined;
|
|
48
|
+
state.triggerTokens = undefined;
|
|
49
|
+
state.triggerPercent = undefined;
|
|
50
|
+
state.triggerContextWindow = undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sameSession(state: MidRunState, ctx: ExtensionContext): boolean {
|
|
54
|
+
return state.sessionId !== undefined && state.sessionId === getSessionId(ctx);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function notifyFailure(ctx: ExtensionContext, message: string): void {
|
|
58
|
+
if (ctx.hasUI) {
|
|
59
|
+
ctx.ui.notify(`${EXTENSION_ID}: ${message}`, "error");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function scheduleResume(
|
|
64
|
+
pi: ExtensionAPI,
|
|
65
|
+
ctx: ExtensionContext,
|
|
66
|
+
state: MidRunState,
|
|
67
|
+
generation: number,
|
|
68
|
+
): void {
|
|
69
|
+
state.phase = "resume-pending";
|
|
70
|
+
|
|
71
|
+
setImmediate(() => {
|
|
72
|
+
if (
|
|
73
|
+
state.generation !== generation ||
|
|
74
|
+
state.phase !== "resume-pending" ||
|
|
75
|
+
!sameSession(state, ctx)
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
81
|
+
resetState(state);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resetState(state);
|
|
86
|
+
pi.sendMessage(
|
|
87
|
+
{
|
|
88
|
+
customType: RESUME_CUSTOM_TYPE,
|
|
89
|
+
content: RESUME_PROMPT,
|
|
90
|
+
display: false,
|
|
91
|
+
details: { source: "midrun-compaction" },
|
|
92
|
+
},
|
|
93
|
+
{ triggerTurn: true },
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function registerMidRunGuard(
|
|
99
|
+
pi: ExtensionAPI,
|
|
100
|
+
loadConfig: ConfigLoader = loadExtensionConfig,
|
|
101
|
+
): void {
|
|
102
|
+
const state: MidRunState = { phase: "idle", generation: 0 };
|
|
103
|
+
|
|
104
|
+
pi.on("turn_end", (event, ctx) => {
|
|
105
|
+
const { config } = loadConfig();
|
|
106
|
+
if (!config.enabled || !config.midRun.enabled || state.phase !== "idle") return;
|
|
107
|
+
if (event.toolResults.length === 0 || ctx.hasPendingMessages()) return;
|
|
108
|
+
|
|
109
|
+
const usage = ctx.getContextUsage();
|
|
110
|
+
if (!usage || usage.tokens == null || usage.percent == null) return;
|
|
111
|
+
if (usage.percent < config.midRun.thresholdPercent) return;
|
|
112
|
+
|
|
113
|
+
const sessionId = getSessionId(ctx);
|
|
114
|
+
if (!sessionId) return;
|
|
115
|
+
|
|
116
|
+
state.phase = "abort-pending";
|
|
117
|
+
state.generation += 1;
|
|
118
|
+
state.sessionId = sessionId;
|
|
119
|
+
state.baselineCompactionId = getLatestCompactionId(ctx);
|
|
120
|
+
state.triggerTokens = usage.tokens;
|
|
121
|
+
state.triggerPercent = usage.percent;
|
|
122
|
+
state.triggerContextWindow = usage.contextWindow;
|
|
123
|
+
|
|
124
|
+
writeDebugArtifact(
|
|
125
|
+
"lifecycle",
|
|
126
|
+
{
|
|
127
|
+
event: "midrun.threshold",
|
|
128
|
+
turnIndex: event.turnIndex,
|
|
129
|
+
tokens: usage.tokens,
|
|
130
|
+
contextWindow: usage.contextWindow,
|
|
131
|
+
percent: usage.percent,
|
|
132
|
+
thresholdPercent: config.midRun.thresholdPercent,
|
|
133
|
+
baselineCompactionId: state.baselineCompactionId,
|
|
134
|
+
},
|
|
135
|
+
config,
|
|
136
|
+
ctx,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Never compact while the agent run is active; let Pi settle first.
|
|
140
|
+
ctx.abort();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
144
|
+
if (state.phase !== "abort-pending") return;
|
|
145
|
+
if (!sameSession(state, ctx)) {
|
|
146
|
+
resetState(state);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// An earlier agent_settled handler may already have started or queued another run.
|
|
151
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
152
|
+
resetState(state);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const generation = state.generation;
|
|
157
|
+
const latestCompactionId = getLatestCompactionId(ctx);
|
|
158
|
+
if (latestCompactionId !== state.baselineCompactionId) {
|
|
159
|
+
const { config } = loadConfig();
|
|
160
|
+
writeDebugArtifact(
|
|
161
|
+
"lifecycle",
|
|
162
|
+
{
|
|
163
|
+
event: "midrun.coalesced",
|
|
164
|
+
reason: "compaction-already-occurred-during-abort",
|
|
165
|
+
baselineCompactionId: state.baselineCompactionId,
|
|
166
|
+
latestCompactionId,
|
|
167
|
+
},
|
|
168
|
+
config,
|
|
169
|
+
ctx,
|
|
170
|
+
);
|
|
171
|
+
scheduleResume(pi, ctx, state, generation);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { config } = loadConfig();
|
|
176
|
+
if (!config.enabled || !config.midRun.enabled) {
|
|
177
|
+
scheduleResume(pi, ctx, state, generation);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const usage = ctx.getContextUsage();
|
|
182
|
+
if (usage?.percent != null && usage.percent < config.midRun.thresholdPercent) {
|
|
183
|
+
scheduleResume(pi, ctx, state, generation);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
state.phase = "compacting";
|
|
188
|
+
ctx.compact({
|
|
189
|
+
customInstructions: MIDRUN_COMPACTION_INSTRUCTIONS,
|
|
190
|
+
onComplete: () => {
|
|
191
|
+
if (
|
|
192
|
+
state.generation !== generation ||
|
|
193
|
+
state.phase !== "compacting" ||
|
|
194
|
+
!sameSession(state, ctx)
|
|
195
|
+
) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
scheduleResume(pi, ctx, state, generation);
|
|
199
|
+
},
|
|
200
|
+
onError: (error) => {
|
|
201
|
+
if (state.generation !== generation || !sameSession(state, ctx)) return;
|
|
202
|
+
|
|
203
|
+
const latest = getLatestCompactionId(ctx);
|
|
204
|
+
if (/Already compacted/i.test(error.message) && latest !== state.baselineCompactionId) {
|
|
205
|
+
scheduleResume(pi, ctx, state, generation);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
state.phase = "failed";
|
|
210
|
+
writeDebugArtifact(
|
|
211
|
+
"lifecycle",
|
|
212
|
+
{
|
|
213
|
+
event: "midrun.failed",
|
|
214
|
+
errorMessage: error.message,
|
|
215
|
+
triggerTokens: state.triggerTokens,
|
|
216
|
+
triggerPercent: state.triggerPercent,
|
|
217
|
+
triggerContextWindow: state.triggerContextWindow,
|
|
218
|
+
},
|
|
219
|
+
config,
|
|
220
|
+
ctx,
|
|
221
|
+
);
|
|
222
|
+
notifyFailure(ctx, `mid-run compaction failed: ${error.message}`);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.on("session_start", () => resetState(state));
|
|
228
|
+
pi.on("session_shutdown", () => resetState(state));
|
|
229
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -33,8 +33,14 @@ export type DebugArtifactKind =
|
|
|
33
33
|
| "compaction-event"
|
|
34
34
|
| "lifecycle";
|
|
35
35
|
|
|
36
|
+
export type MidRunConfig = {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
thresholdPercent: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
36
41
|
export type ExtensionConfig = {
|
|
37
42
|
enabled: boolean;
|
|
43
|
+
midRun: MidRunConfig;
|
|
38
44
|
/**
|
|
39
45
|
* Allow a Responses session whose latest compaction was not created by this extension
|
|
40
46
|
* to restart native compaction from Pi's current serialized session context.
|
|
@@ -304,6 +310,10 @@ export function createNativeCompactionResult(
|
|
|
304
310
|
|
|
305
311
|
export const DEFAULT_EXTENSION_CONFIG: ExtensionConfig = {
|
|
306
312
|
enabled: true,
|
|
313
|
+
midRun: {
|
|
314
|
+
enabled: false,
|
|
315
|
+
thresholdPercent: 80,
|
|
316
|
+
},
|
|
307
317
|
allowCompactionContinuityBreak: false,
|
|
308
318
|
compactionModel: undefined,
|
|
309
319
|
compactionThinkingLevel: "off",
|