@lll9p/pi-better-compaction 0.2.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/LICENSE +21 -0
- package/README.md +183 -0
- package/index.ts +1 -0
- package/package.json +59 -0
- package/src/compact-client.ts +428 -0
- package/src/config.ts +157 -0
- package/src/debug.ts +165 -0
- package/src/details-store.ts +151 -0
- package/src/extension-runtime.ts +499 -0
- package/src/native-fallback.ts +149 -0
- package/src/payload-rewrite.ts +548 -0
- package/src/request-context-cache.ts +84 -0
- package/src/runtime.ts +250 -0
- package/src/serializer.ts +555 -0
- package/src/supported-environment.ts +16 -0
- package/src/types.ts +296 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BeforeProviderRequestEvent,
|
|
3
|
+
CompactionResult,
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
SessionBeforeCompactEvent,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { executeNativeCompaction } from "./compact-client";
|
|
9
|
+
import { loadExtensionConfig } from "./config";
|
|
10
|
+
import { writeDebugArtifact } from "./debug";
|
|
11
|
+
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
12
|
+
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
13
|
+
import {
|
|
14
|
+
rewriteResponsesPayloadWithNativeReplay,
|
|
15
|
+
serializeLiveTailToResponsesInput,
|
|
16
|
+
} from "./payload-rewrite";
|
|
17
|
+
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
18
|
+
import {
|
|
19
|
+
isResponsesCompatiblePayload,
|
|
20
|
+
resolveNativeCompactionEnvironment,
|
|
21
|
+
type NativeCompactionRuntime,
|
|
22
|
+
} from "./runtime";
|
|
23
|
+
import { serializeMessagesToCompactRequest, type NativeCompactionRequestBody, type ResponsesInputItem } from "./serializer";
|
|
24
|
+
import {
|
|
25
|
+
createNativeCompactionDetails,
|
|
26
|
+
createNativeCompactionResult,
|
|
27
|
+
EXTENSION_ID,
|
|
28
|
+
isNativeCompactionDetails,
|
|
29
|
+
type ExtensionConfig,
|
|
30
|
+
type NativeCompactionDetails,
|
|
31
|
+
type NativeCompactionRequestMeta,
|
|
32
|
+
} from "./types";
|
|
33
|
+
|
|
34
|
+
type ResponsesCompactOutcome =
|
|
35
|
+
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
36
|
+
| { outcome: "aborted" }
|
|
37
|
+
| { outcome: "failed" };
|
|
38
|
+
|
|
39
|
+
function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
|
|
40
|
+
return {
|
|
41
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
42
|
+
previousSummaryPresent: Boolean(event.preparation.previousSummary),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getCurrentModelDebugInfo(ctx: ExtensionContext) {
|
|
47
|
+
return ctx.model
|
|
48
|
+
? {
|
|
49
|
+
provider: ctx.model.provider,
|
|
50
|
+
id: ctx.model.id,
|
|
51
|
+
}
|
|
52
|
+
: undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
|
|
56
|
+
return isNativeCompactionDetails(entry?.details)
|
|
57
|
+
? {
|
|
58
|
+
provider: entry.details.provider,
|
|
59
|
+
api: entry.details.api,
|
|
60
|
+
model: entry.details.model,
|
|
61
|
+
baseUrl: entry.details.baseUrl,
|
|
62
|
+
}
|
|
63
|
+
: undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSessionId(ctx: ExtensionContext): string | undefined {
|
|
67
|
+
try {
|
|
68
|
+
return ctx.sessionManager.getSessionId();
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function notifyWarning(ctx: ExtensionContext, message: string): void {
|
|
75
|
+
if (ctx.hasUI) {
|
|
76
|
+
ctx.ui.notify(`${EXTENSION_ID}: ${message}`, "warning");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
|
|
81
|
+
return window.map((item) => structuredClone(item));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
|
|
85
|
+
const guidance = customInstructions?.trim();
|
|
86
|
+
if (!guidance) {
|
|
87
|
+
return systemPrompt;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runResponsesNativeCompact(
|
|
94
|
+
event: SessionBeforeCompactEvent,
|
|
95
|
+
ctx: ExtensionContext,
|
|
96
|
+
config: ExtensionConfig,
|
|
97
|
+
runtime: NativeCompactionRuntime,
|
|
98
|
+
): Promise<ResponsesCompactOutcome> {
|
|
99
|
+
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
100
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
101
|
+
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
102
|
+
provider: runtime.provider,
|
|
103
|
+
api: runtime.api,
|
|
104
|
+
model: runtime.model,
|
|
105
|
+
baseUrl: runtime.baseUrl,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let requestSource: "session-context" | "latest-native-replay";
|
|
109
|
+
let request: NativeCompactionRequestBody;
|
|
110
|
+
if (latestNativeCompaction.ok) {
|
|
111
|
+
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
112
|
+
requestSource = "latest-native-replay";
|
|
113
|
+
const input: ResponsesInputItem[] = [
|
|
114
|
+
...(cloneOpaqueWindow(latestNativeCompaction.entry.details.compactedWindow) as ResponsesInputItem[]),
|
|
115
|
+
...serializeLiveTailToResponsesInput({ model: runtime.currentModel, entries: liveTailEntries }),
|
|
116
|
+
];
|
|
117
|
+
request = {
|
|
118
|
+
model: runtime.currentModel.id,
|
|
119
|
+
input,
|
|
120
|
+
instructions,
|
|
121
|
+
};
|
|
122
|
+
} else if (latestNativeCompaction.reason === "no-compaction") {
|
|
123
|
+
requestSource = "session-context";
|
|
124
|
+
request = serializeMessagesToCompactRequest({
|
|
125
|
+
model: runtime.currentModel,
|
|
126
|
+
messages: ctx.sessionManager.buildSessionContext().messages,
|
|
127
|
+
instructions,
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
writeDebugArtifact(
|
|
131
|
+
"compaction-event",
|
|
132
|
+
{
|
|
133
|
+
event: "session_before_compact.responses-compact-skip",
|
|
134
|
+
reason: latestNativeCompaction.reason,
|
|
135
|
+
provider: runtime.provider,
|
|
136
|
+
api: runtime.api,
|
|
137
|
+
model: runtime.model,
|
|
138
|
+
baseUrl: runtime.baseUrl,
|
|
139
|
+
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
140
|
+
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
141
|
+
},
|
|
142
|
+
config,
|
|
143
|
+
ctx,
|
|
144
|
+
);
|
|
145
|
+
return { outcome: "failed" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Mirror the latest codex_rs CompactionInput fields captured from the most
|
|
149
|
+
// recent live provider request for this model (tools, reasoning, etc.).
|
|
150
|
+
const extras = getCompactionRequestExtras(runtime.model, getSessionId(ctx));
|
|
151
|
+
if (extras) {
|
|
152
|
+
request = { ...request, ...extras };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const compactResult = await executeNativeCompaction({
|
|
156
|
+
runtime,
|
|
157
|
+
request,
|
|
158
|
+
signal: event.signal,
|
|
159
|
+
settings: config,
|
|
160
|
+
context: ctx,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (compactResult.ok === false) {
|
|
164
|
+
writeDebugArtifact(
|
|
165
|
+
"compaction-event",
|
|
166
|
+
{
|
|
167
|
+
event: "session_before_compact.responses-compact-failure",
|
|
168
|
+
reason: compactResult.reason,
|
|
169
|
+
status: compactResult.status,
|
|
170
|
+
errorMessage: compactResult.errorMessage,
|
|
171
|
+
},
|
|
172
|
+
config,
|
|
173
|
+
ctx,
|
|
174
|
+
);
|
|
175
|
+
return compactResult.reason === "aborted" ? { outcome: "aborted" } : { outcome: "failed" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let details: NativeCompactionDetails;
|
|
179
|
+
try {
|
|
180
|
+
details = createNativeCompactionDetails({
|
|
181
|
+
provider: runtime.provider,
|
|
182
|
+
api: runtime.api,
|
|
183
|
+
model: runtime.model,
|
|
184
|
+
baseUrl: runtime.baseUrl,
|
|
185
|
+
compactedWindow: compactResult.compactedWindow,
|
|
186
|
+
compactResponseId: compactResult.compactResponseId,
|
|
187
|
+
createdAt: compactResult.createdAt,
|
|
188
|
+
requestMeta: buildCompactionRequestMeta(event),
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
writeDebugArtifact(
|
|
192
|
+
"compaction-event",
|
|
193
|
+
{
|
|
194
|
+
event: "session_before_compact.invalid-native-details",
|
|
195
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
196
|
+
provider: runtime.provider,
|
|
197
|
+
api: runtime.api,
|
|
198
|
+
model: runtime.model,
|
|
199
|
+
baseUrl: runtime.baseUrl,
|
|
200
|
+
},
|
|
201
|
+
config,
|
|
202
|
+
ctx,
|
|
203
|
+
);
|
|
204
|
+
return { outcome: "failed" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const compaction = createNativeCompactionResult({
|
|
208
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
209
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
210
|
+
details,
|
|
211
|
+
summary: compactResult.summaryText,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
writeDebugArtifact(
|
|
215
|
+
"compaction-event",
|
|
216
|
+
{
|
|
217
|
+
event: "session_before_compact.responses-compact-success",
|
|
218
|
+
provider: runtime.provider,
|
|
219
|
+
api: runtime.api,
|
|
220
|
+
model: runtime.model,
|
|
221
|
+
requestSource,
|
|
222
|
+
requestInputItems: request.input.length,
|
|
223
|
+
requestExtras: extras ? Object.keys(extras) : [],
|
|
224
|
+
compactResponseId: compactResult.compactResponseId,
|
|
225
|
+
compactedItems: compactResult.compactedWindow.length,
|
|
226
|
+
summaryExtracted: Boolean(compactResult.summaryText),
|
|
227
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
228
|
+
},
|
|
229
|
+
config,
|
|
230
|
+
ctx,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return { outcome: "success", compaction };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx: ExtensionContext) {
|
|
237
|
+
const { config } = loadExtensionConfig();
|
|
238
|
+
if (!config.enabled) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
writeDebugArtifact(
|
|
243
|
+
"compaction-event",
|
|
244
|
+
{
|
|
245
|
+
event: "session_before_compact",
|
|
246
|
+
customInstructions: event.customInstructions,
|
|
247
|
+
preparation: {
|
|
248
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
249
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
250
|
+
previousSummaryPresent: Boolean(event.preparation.previousSummary),
|
|
251
|
+
messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
|
|
252
|
+
turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
config,
|
|
256
|
+
ctx,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (event.signal.aborted) {
|
|
260
|
+
return { cancel: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Branch 1: Responses-family APIs use the native /responses/compact endpoint.
|
|
264
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
265
|
+
enabled: config.enabled,
|
|
266
|
+
responsesCompactApis: config.responsesCompactApis,
|
|
267
|
+
});
|
|
268
|
+
if (resolution.ok) {
|
|
269
|
+
const responsesOutcome = await runResponsesNativeCompact(event, ctx, config, resolution.runtime);
|
|
270
|
+
if (responsesOutcome.outcome === "success") {
|
|
271
|
+
return { compaction: responsesOutcome.compaction };
|
|
272
|
+
}
|
|
273
|
+
if (responsesOutcome.outcome === "aborted") {
|
|
274
|
+
return { cancel: true };
|
|
275
|
+
}
|
|
276
|
+
// failed: fall through to the configured-model fallback below.
|
|
277
|
+
} else {
|
|
278
|
+
writeDebugArtifact(
|
|
279
|
+
"compaction-event",
|
|
280
|
+
{
|
|
281
|
+
event: "session_before_compact.responses-compact-unavailable",
|
|
282
|
+
reason: resolution.reason,
|
|
283
|
+
provider: resolution.provider,
|
|
284
|
+
api: resolution.api,
|
|
285
|
+
model: resolution.model,
|
|
286
|
+
baseUrl: resolution.baseUrl,
|
|
287
|
+
},
|
|
288
|
+
config,
|
|
289
|
+
ctx,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Branch 2: run pi's native compaction method with the configured model.
|
|
294
|
+
const fallback = await runNativeFallbackCompaction({ ctx, event, config });
|
|
295
|
+
if (fallback.ok) {
|
|
296
|
+
if (ctx.hasUI) {
|
|
297
|
+
ctx.ui.notify(
|
|
298
|
+
`${EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
|
|
299
|
+
"info",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
writeDebugArtifact(
|
|
303
|
+
"compaction-event",
|
|
304
|
+
{
|
|
305
|
+
event: "session_before_compact.fallback-success",
|
|
306
|
+
model: fallback.model,
|
|
307
|
+
},
|
|
308
|
+
config,
|
|
309
|
+
ctx,
|
|
310
|
+
);
|
|
311
|
+
return { compaction: fallback.result };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (fallback.reason === "aborted") {
|
|
315
|
+
return { cancel: true };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
writeDebugArtifact(
|
|
319
|
+
"compaction-event",
|
|
320
|
+
{
|
|
321
|
+
event: "session_before_compact.fallback-skip",
|
|
322
|
+
reason: fallback.reason,
|
|
323
|
+
modelSpec: fallback.modelSpec,
|
|
324
|
+
errorMessage: fallback.errorMessage,
|
|
325
|
+
},
|
|
326
|
+
config,
|
|
327
|
+
ctx,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Intentional pi-default paths: no configured model, or it matches the current one.
|
|
331
|
+
if (fallback.reason !== "no-model-configured" && fallback.reason !== "same-as-current-model") {
|
|
332
|
+
notifyWarning(
|
|
333
|
+
ctx,
|
|
334
|
+
`compaction model "${fallback.modelSpec}" unusable (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); using pi's default compaction`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Branch 3: pi's default native compaction with the current model.
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ctx: ExtensionContext) {
|
|
343
|
+
const { config } = loadExtensionConfig();
|
|
344
|
+
if (!config.enabled) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Capture compact-relevant request fields (tools, reasoning, ...) for the next
|
|
349
|
+
// /responses/compact call, regardless of whether this request gets rewritten.
|
|
350
|
+
if (isResponsesCompatiblePayload(event.payload)) {
|
|
351
|
+
rememberRequestContext(event.payload, getSessionId(ctx));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const resolution = await resolveNativeCompactionEnvironment(
|
|
355
|
+
ctx,
|
|
356
|
+
{
|
|
357
|
+
enabled: config.enabled,
|
|
358
|
+
responsesCompactApis: config.responsesCompactApis,
|
|
359
|
+
},
|
|
360
|
+
event.payload,
|
|
361
|
+
);
|
|
362
|
+
if (resolution.ok === false) {
|
|
363
|
+
writeDebugArtifact(
|
|
364
|
+
"provider-request",
|
|
365
|
+
{
|
|
366
|
+
event: "before_provider_request.skip",
|
|
367
|
+
reason: resolution.reason,
|
|
368
|
+
provider: resolution.provider,
|
|
369
|
+
api: resolution.api,
|
|
370
|
+
model: resolution.model,
|
|
371
|
+
baseUrl: resolution.baseUrl,
|
|
372
|
+
currentModel: getCurrentModelDebugInfo(ctx),
|
|
373
|
+
payload: event.payload,
|
|
374
|
+
},
|
|
375
|
+
config,
|
|
376
|
+
ctx,
|
|
377
|
+
);
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const runtime = resolution.runtime;
|
|
382
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
383
|
+
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
384
|
+
provider: runtime.provider,
|
|
385
|
+
api: runtime.api,
|
|
386
|
+
model: runtime.model,
|
|
387
|
+
baseUrl: runtime.baseUrl,
|
|
388
|
+
});
|
|
389
|
+
if (!latestNativeCompaction.ok) {
|
|
390
|
+
writeDebugArtifact(
|
|
391
|
+
"provider-request",
|
|
392
|
+
{
|
|
393
|
+
event: "before_provider_request.no-native-compaction",
|
|
394
|
+
reason: latestNativeCompaction.reason,
|
|
395
|
+
provider: runtime.provider,
|
|
396
|
+
api: runtime.api,
|
|
397
|
+
model: runtime.model,
|
|
398
|
+
baseUrl: runtime.baseUrl,
|
|
399
|
+
branchEntries: branchEntries.length,
|
|
400
|
+
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
401
|
+
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
402
|
+
payload: runtime.payload,
|
|
403
|
+
},
|
|
404
|
+
config,
|
|
405
|
+
ctx,
|
|
406
|
+
);
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const latestNativeCompactionEntry = latestNativeCompaction.entry;
|
|
411
|
+
const rewrite = rewriteResponsesPayloadWithNativeReplay({
|
|
412
|
+
model: runtime.currentModel,
|
|
413
|
+
payload: runtime.payload,
|
|
414
|
+
branchEntries,
|
|
415
|
+
compactionEntry: latestNativeCompactionEntry,
|
|
416
|
+
});
|
|
417
|
+
if (!rewrite.ok) {
|
|
418
|
+
writeDebugArtifact(
|
|
419
|
+
"provider-request",
|
|
420
|
+
{
|
|
421
|
+
event: "before_provider_request.rewrite-failed",
|
|
422
|
+
reason: rewrite.reason,
|
|
423
|
+
provider: runtime.provider,
|
|
424
|
+
api: runtime.api,
|
|
425
|
+
model: runtime.model,
|
|
426
|
+
baseUrl: runtime.baseUrl,
|
|
427
|
+
compactionEntryId: latestNativeCompactionEntry.id,
|
|
428
|
+
parity: rewrite.parity,
|
|
429
|
+
payload: runtime.payload,
|
|
430
|
+
},
|
|
431
|
+
config,
|
|
432
|
+
ctx,
|
|
433
|
+
);
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
writeDebugArtifact(
|
|
438
|
+
"provider-request",
|
|
439
|
+
{
|
|
440
|
+
event: "before_provider_request.native-rewrite",
|
|
441
|
+
provider: runtime.provider,
|
|
442
|
+
api: runtime.api,
|
|
443
|
+
model: runtime.model,
|
|
444
|
+
baseUrl: runtime.baseUrl,
|
|
445
|
+
compactionEntryId: latestNativeCompactionEntry.id,
|
|
446
|
+
boundaryIndex: rewrite.segments.boundaryIndex,
|
|
447
|
+
firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
|
|
448
|
+
originalInputItems: runtime.payload.input.length,
|
|
449
|
+
rewrittenInputItems: rewrite.rewrittenPayload.input.length,
|
|
450
|
+
freshPreambleItems: rewrite.segments.freshPreamble.length,
|
|
451
|
+
trailingPreambleItems: rewrite.segments.trailingPreamble.length,
|
|
452
|
+
compactionSummaryItems: rewrite.segments.compactionSummary.length,
|
|
453
|
+
preCompactionKeptItems: rewrite.segments.preCompactionKeptWindow.input.length,
|
|
454
|
+
compactedItems: rewrite.segments.compactedWindow.length,
|
|
455
|
+
postCompactionTailItems: rewrite.segments.postCompactionTail.input.length,
|
|
456
|
+
payload: rewrite.rewrittenPayload,
|
|
457
|
+
originalPayload: runtime.payload,
|
|
458
|
+
},
|
|
459
|
+
config,
|
|
460
|
+
ctx,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return rewrite.rewrittenPayload;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export default function (pi: ExtensionAPI) {
|
|
467
|
+
pi.on("session_start", (_event, ctx) => {
|
|
468
|
+
const { config, source, warnings } = loadExtensionConfig();
|
|
469
|
+
if (!config.enabled) return;
|
|
470
|
+
|
|
471
|
+
if (warnings.length > 0 && ctx.hasUI && config.debug) {
|
|
472
|
+
ctx.ui.notify(`${EXTENSION_ID}: ${warnings[0]}`, "warning");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const artifactPath = writeDebugArtifact(
|
|
476
|
+
"lifecycle",
|
|
477
|
+
{
|
|
478
|
+
event: "session_start",
|
|
479
|
+
config,
|
|
480
|
+
configSource: source,
|
|
481
|
+
warnings,
|
|
482
|
+
},
|
|
483
|
+
config,
|
|
484
|
+
ctx,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
if (ctx.hasUI && (config.notifyOnLoad || config.debug)) {
|
|
488
|
+
ctx.ui.notify(
|
|
489
|
+
artifactPath
|
|
490
|
+
? `${EXTENSION_ID} loaded • debug artifacts → ${artifactPath}`
|
|
491
|
+
: `${EXTENSION_ID} loaded`,
|
|
492
|
+
"info",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
pi.on("session_before_compact", handleSessionBeforeCompact);
|
|
498
|
+
pi.on("before_provider_request", handleBeforeProviderRequest);
|
|
499
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compact,
|
|
3
|
+
type CompactionResult,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type SessionBeforeCompactEvent,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ExtensionConfig } from "./types";
|
|
8
|
+
|
|
9
|
+
export type ParsedModelSpec = {
|
|
10
|
+
provider: string;
|
|
11
|
+
modelId: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type NativeFallbackFailureReason =
|
|
15
|
+
| "no-model-configured"
|
|
16
|
+
| "invalid-model-spec"
|
|
17
|
+
| "model-not-found"
|
|
18
|
+
| "same-as-current-model"
|
|
19
|
+
| "auth-failed"
|
|
20
|
+
| "aborted"
|
|
21
|
+
| "empty-summary"
|
|
22
|
+
| "compact-failed";
|
|
23
|
+
|
|
24
|
+
export type NativeFallbackResult =
|
|
25
|
+
| {
|
|
26
|
+
ok: true;
|
|
27
|
+
result: CompactionResult;
|
|
28
|
+
model: { provider: string; id: string };
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: NativeFallbackFailureReason;
|
|
33
|
+
modelSpec?: string;
|
|
34
|
+
errorMessage?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** pi's exported native compact(); injectable for tests. */
|
|
38
|
+
export type NativeCompactFn = typeof compact;
|
|
39
|
+
|
|
40
|
+
type ResolvedAuth =
|
|
41
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
|
|
42
|
+
| { ok: false; error: string };
|
|
43
|
+
|
|
44
|
+
/** Parse "provider/model-id" (model ids may themselves contain slashes). */
|
|
45
|
+
export function parseModelSpec(spec: string): ParsedModelSpec | undefined {
|
|
46
|
+
const trimmed = spec.trim();
|
|
47
|
+
const separatorIndex = trimmed.indexOf("/");
|
|
48
|
+
if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const provider = trimmed.slice(0, separatorIndex).trim();
|
|
53
|
+
const modelId = trimmed.slice(separatorIndex + 1).trim();
|
|
54
|
+
if (!provider || !modelId) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { provider, modelId };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isAbortError(error: unknown): boolean {
|
|
62
|
+
return (
|
|
63
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
64
|
+
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toErrorMessage(error: unknown): string {
|
|
69
|
+
return error instanceof Error ? error.message : String(error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run pi's native compaction method with the user-configured compaction model.
|
|
74
|
+
*
|
|
75
|
+
* Only handles the "configured model differs from the current one" case: when no model
|
|
76
|
+
* is configured (or it equals the current model), the caller should return undefined from
|
|
77
|
+
* session_before_compact so pi runs the same native path itself, keeping its internal
|
|
78
|
+
* streamFn/thinkingLevel wiring.
|
|
79
|
+
*/
|
|
80
|
+
export async function runNativeFallbackCompaction(args: {
|
|
81
|
+
ctx: ExtensionContext;
|
|
82
|
+
event: SessionBeforeCompactEvent;
|
|
83
|
+
config: ExtensionConfig;
|
|
84
|
+
compactFn?: NativeCompactFn;
|
|
85
|
+
}): Promise<NativeFallbackResult> {
|
|
86
|
+
const { ctx, event, config } = args;
|
|
87
|
+
const compactFn = args.compactFn ?? compact;
|
|
88
|
+
|
|
89
|
+
const spec = config.compactionModel?.trim();
|
|
90
|
+
if (!spec) {
|
|
91
|
+
return { ok: false, reason: "no-model-configured" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = parseModelSpec(spec);
|
|
95
|
+
if (!parsed) {
|
|
96
|
+
return { ok: false, reason: "invalid-model-spec", modelSpec: spec };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
100
|
+
if (!model) {
|
|
101
|
+
return { ok: false, reason: "model-not-found", modelSpec: spec };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ctx.model && ctx.model.provider === model.provider && ctx.model.id === model.id) {
|
|
105
|
+
return { ok: false, reason: "same-as-current-model", modelSpec: spec };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let auth: ResolvedAuth;
|
|
109
|
+
try {
|
|
110
|
+
auth = (await ctx.modelRegistry.getApiKeyAndHeaders(model)) as ResolvedAuth;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: toErrorMessage(error) };
|
|
113
|
+
}
|
|
114
|
+
if (!auth.ok) {
|
|
115
|
+
return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: auth.error };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = await compactFn(
|
|
120
|
+
event.preparation,
|
|
121
|
+
model,
|
|
122
|
+
auth.apiKey,
|
|
123
|
+
auth.headers,
|
|
124
|
+
event.customInstructions,
|
|
125
|
+
event.signal,
|
|
126
|
+
config.compactionThinkingLevel,
|
|
127
|
+
undefined,
|
|
128
|
+
auth.env,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (event.signal.aborted) {
|
|
132
|
+
return { ok: false, reason: "aborted", modelSpec: spec };
|
|
133
|
+
}
|
|
134
|
+
if (!result.summary || result.summary.trim().length === 0) {
|
|
135
|
+
return { ok: false, reason: "empty-summary", modelSpec: spec };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
result,
|
|
141
|
+
model: { provider: model.provider, id: model.id },
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (event.signal.aborted || isAbortError(error)) {
|
|
145
|
+
return { ok: false, reason: "aborted", modelSpec: spec };
|
|
146
|
+
}
|
|
147
|
+
return { ok: false, reason: "compact-failed", modelSpec: spec, errorMessage: toErrorMessage(error) };
|
|
148
|
+
}
|
|
149
|
+
}
|