pi-openai-toolkit 0.14.6 → 0.14.8
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 +36 -7
- package/README.zh.md +36 -7
- package/docs/internals.md +9 -1
- package/package.json +5 -2
- package/src/compaction-projection.ts +31 -0
- package/src/config.ts +100 -1
- package/src/context-management/messages.ts +1 -1
- package/src/context-management/tools.ts +24 -39
- package/src/context-management/window-manager.ts +110 -28
- package/src/extension-runtime.ts +263 -34
- package/src/payload-rewrite.ts +55 -10
- package/src/pi-context-hook.ts +116 -0
- package/src/request-context-cache.ts +16 -1
- package/src/runtime.ts +52 -0
- package/src/types.ts +46 -0
- package/src/web-search/alpha-client.ts +875 -0
- package/src/web-search/extension.ts +469 -26
- package/src/web-search/payload.ts +209 -10
- package/src/web-search/prompt.ts +44 -5
- package/src/web-search/types.ts +107 -8
- package/tsconfig.check.json +2 -0
package/src/extension-runtime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
1
2
|
import type {
|
|
2
3
|
BeforeProviderRequestEvent,
|
|
3
4
|
ContextEvent,
|
|
@@ -18,8 +19,22 @@ import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
|
18
19
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
19
20
|
import { registerContextManagementTools } from "./context-management/tools";
|
|
20
21
|
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
22
|
+
import {
|
|
23
|
+
COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
24
|
+
COMPACTION_PROJECTION_UNAVAILABLE,
|
|
25
|
+
COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
|
|
26
|
+
COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
27
|
+
hasVerifiedCompactionInputProvenance,
|
|
28
|
+
type UnprojectedCompactionReason,
|
|
29
|
+
} from "./compaction-projection";
|
|
21
30
|
import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
|
|
22
31
|
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
32
|
+
import {
|
|
33
|
+
getPiContextHookProjector,
|
|
34
|
+
installPiContextHookPatch,
|
|
35
|
+
reportPiContextHookFailure,
|
|
36
|
+
type PiContextHookPatchResult,
|
|
37
|
+
} from "./pi-context-hook";
|
|
23
38
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
24
39
|
import {
|
|
25
40
|
rewriteResponsesPayloadWithNativeReplay,
|
|
@@ -27,29 +42,44 @@ import {
|
|
|
27
42
|
serializeLiveTailToResponsesInput,
|
|
28
43
|
} from "./payload-rewrite";
|
|
29
44
|
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
45
|
+
import { resolveWebSearchRoute } from "./web-search/types";
|
|
30
46
|
import { executeRemoteV2Compaction } from "./remote-v2-client";
|
|
31
47
|
import {
|
|
32
48
|
resolveNativeCompactionEnvironment,
|
|
33
49
|
resolveRemoteCompactionExecution,
|
|
34
50
|
type RemoteCompactionExecution,
|
|
35
51
|
} from "./runtime";
|
|
36
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
serializeMessagesToCompactRequest,
|
|
54
|
+
serializeMessagesToResponsesInput,
|
|
55
|
+
type NativeCompactionRequestBody,
|
|
56
|
+
type ResponsesInputItem,
|
|
57
|
+
} from "./serializer";
|
|
37
58
|
import {
|
|
38
59
|
createNativeCompactionDetails,
|
|
39
60
|
createNativeCompactionResult,
|
|
40
61
|
COMPACTION_EXTENSION_ID,
|
|
41
62
|
getLatestDeferredToolCarryover,
|
|
63
|
+
getRemoteV2InputProvenance,
|
|
42
64
|
isNativeCompactionDetails,
|
|
43
65
|
type CompactionConfig,
|
|
44
66
|
type NativeCompactionDetails,
|
|
45
67
|
type NativeCompactionRequestMeta,
|
|
46
68
|
} from "./types";
|
|
47
69
|
|
|
70
|
+
type CompactionContextProjection = (
|
|
71
|
+
messages: readonly AgentMessage[],
|
|
72
|
+
ctx: ExtensionContext,
|
|
73
|
+
) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
|
|
74
|
+
|
|
48
75
|
type CompactionDependencies = {
|
|
49
76
|
loadConfig: typeof loadToolkitConfig;
|
|
50
77
|
remoteCompact: typeof executeRemoteV2Compaction;
|
|
51
78
|
nativeFallback: typeof runNativeFallbackCompaction;
|
|
52
79
|
contextWindows: CodexContextWindowManager;
|
|
80
|
+
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
81
|
+
projectCompactionContext?: CompactionContextProjection;
|
|
82
|
+
piContextHookPatch: PiContextHookPatchResult;
|
|
53
83
|
};
|
|
54
84
|
|
|
55
85
|
type RemoteContextActive = (
|
|
@@ -61,7 +91,11 @@ type RemoteContextActive = (
|
|
|
61
91
|
type ResponsesCompactOutcome =
|
|
62
92
|
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
63
93
|
| { outcome: "aborted" }
|
|
64
|
-
| { outcome: "failed" }
|
|
94
|
+
| { outcome: "failed" }
|
|
95
|
+
| {
|
|
96
|
+
outcome: "unprojected-input";
|
|
97
|
+
reason: UnprojectedCompactionReason;
|
|
98
|
+
};
|
|
65
99
|
|
|
66
100
|
function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
|
|
67
101
|
return {
|
|
@@ -142,12 +176,86 @@ function buildCompactionInstructions(systemPrompt: string, customInstructions?:
|
|
|
142
176
|
return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
function findLatestProjectedCompactionSummaryIndex(
|
|
180
|
+
messages: readonly AgentMessage[],
|
|
181
|
+
summary: string,
|
|
182
|
+
): number {
|
|
183
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
184
|
+
const message = messages[index];
|
|
185
|
+
if (message?.role === "compactionSummary" && message.summary === summary) {
|
|
186
|
+
return index;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return -1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
type SessionManagerWithOptionalContext = ExtensionContext["sessionManager"] & {
|
|
194
|
+
buildSessionContext?: () => { messages?: readonly AgentMessage[] };
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
|
|
198
|
+
const sessionManager = ctx.sessionManager as SessionManagerWithOptionalContext;
|
|
199
|
+
if (typeof sessionManager.buildSessionContext !== "function") {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const sessionContext = sessionManager.buildSessionContext();
|
|
205
|
+
if (!sessionContext || !Array.isArray(sessionContext.messages)) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
return structuredClone([...sessionContext.messages]);
|
|
209
|
+
} catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
type CompactionProjectionResult =
|
|
215
|
+
| { ok: true; messages: readonly AgentMessage[] }
|
|
216
|
+
| { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
217
|
+
|
|
218
|
+
async function projectSessionContextForCompaction(
|
|
219
|
+
ctx: ExtensionContext,
|
|
220
|
+
projectCompactionContext: CompactionContextProjection | undefined,
|
|
221
|
+
): Promise<CompactionProjectionResult> {
|
|
222
|
+
const messages = readSessionContextMessages(ctx);
|
|
223
|
+
if (!messages) {
|
|
224
|
+
return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
|
|
228
|
+
if (!projector) {
|
|
229
|
+
return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const projected = await projector(messages, ctx);
|
|
234
|
+
return Array.isArray(projected)
|
|
235
|
+
? { ok: true, messages: projected }
|
|
236
|
+
: { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
237
|
+
} catch {
|
|
238
|
+
return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getLegacySessionContextMessages(
|
|
243
|
+
event: SessionBeforeCompactEvent,
|
|
244
|
+
ctx: ExtensionContext,
|
|
245
|
+
): AgentMessage[] {
|
|
246
|
+
return readSessionContextMessages(ctx) ?? [
|
|
247
|
+
...event.preparation.messagesToSummarize,
|
|
248
|
+
...event.preparation.turnPrefixMessages,
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
145
252
|
async function runResponsesNativeCompact(
|
|
146
253
|
event: SessionBeforeCompactEvent,
|
|
147
254
|
ctx: ExtensionContext,
|
|
148
255
|
config: CompactionConfig,
|
|
149
256
|
execution: RemoteCompactionExecution,
|
|
150
257
|
remoteCompact: typeof executeRemoteV2Compaction,
|
|
258
|
+
projectCompactionContext: CompactionContextProjection | undefined,
|
|
151
259
|
): Promise<ResponsesCompactOutcome> {
|
|
152
260
|
const { consumer, compactor } = execution;
|
|
153
261
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
@@ -157,24 +265,61 @@ async function runResponsesNativeCompact(
|
|
|
157
265
|
baseUrl: consumer.baseUrl,
|
|
158
266
|
});
|
|
159
267
|
|
|
268
|
+
const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
|
|
160
269
|
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
161
270
|
let request: NativeCompactionRequestBody;
|
|
162
271
|
if (latestNativeCompaction.ok) {
|
|
163
|
-
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
164
272
|
const details = latestNativeCompaction.entry.details;
|
|
165
273
|
if (!details) {
|
|
166
274
|
return { outcome: "failed" };
|
|
167
275
|
}
|
|
276
|
+
if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
|
|
277
|
+
return {
|
|
278
|
+
outcome: "unprojected-input",
|
|
279
|
+
reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
168
282
|
requestSource = "latest-native-replay";
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
283
|
+
|
|
284
|
+
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
285
|
+
const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
|
|
286
|
+
if (!projection.ok) {
|
|
287
|
+
return { outcome: "unprojected-input", reason: projection.reason };
|
|
288
|
+
}
|
|
289
|
+
const summaryIndex = findLatestProjectedCompactionSummaryIndex(
|
|
290
|
+
projection.messages,
|
|
291
|
+
latestNativeCompaction.entry.summary,
|
|
292
|
+
);
|
|
293
|
+
if (summaryIndex < 0) {
|
|
294
|
+
return {
|
|
295
|
+
outcome: "unprojected-input",
|
|
296
|
+
reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const input: ResponsesInputItem[] = [
|
|
300
|
+
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
301
|
+
...serializeMessagesToResponsesInput(
|
|
302
|
+
compactor.currentModel,
|
|
303
|
+
[...projection.messages.slice(summaryIndex + 1)],
|
|
304
|
+
),
|
|
305
|
+
];
|
|
306
|
+
request = {
|
|
307
|
+
model: compactor.model,
|
|
308
|
+
input,
|
|
309
|
+
instructions,
|
|
310
|
+
};
|
|
311
|
+
} else {
|
|
312
|
+
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
313
|
+
const input: ResponsesInputItem[] = [
|
|
314
|
+
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
315
|
+
...serializeLiveTailToResponsesInput({ model: compactor.currentModel, entries: liveTailEntries }),
|
|
316
|
+
];
|
|
317
|
+
request = {
|
|
318
|
+
model: compactor.model,
|
|
319
|
+
input,
|
|
320
|
+
instructions,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
178
323
|
} else if (
|
|
179
324
|
latestNativeCompaction.reason === "no-compaction" ||
|
|
180
325
|
(latestNativeCompaction.reason === "latest-compaction-not-native" &&
|
|
@@ -182,18 +327,23 @@ async function runResponsesNativeCompact(
|
|
|
182
327
|
) {
|
|
183
328
|
requestSource =
|
|
184
329
|
latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
330
|
+
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
331
|
+
const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
|
|
332
|
+
if (!projection.ok) {
|
|
333
|
+
return { outcome: "unprojected-input", reason: projection.reason };
|
|
334
|
+
}
|
|
335
|
+
request = serializeMessagesToCompactRequest({
|
|
336
|
+
model: compactor.currentModel,
|
|
337
|
+
messages: [...projection.messages],
|
|
338
|
+
instructions,
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
request = serializeMessagesToCompactRequest({
|
|
342
|
+
model: compactor.currentModel,
|
|
343
|
+
messages: getLegacySessionContextMessages(event, ctx),
|
|
344
|
+
instructions,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
197
347
|
} else {
|
|
198
348
|
writeDebugArtifact(
|
|
199
349
|
"compaction-event",
|
|
@@ -275,6 +425,7 @@ async function runResponsesNativeCompact(
|
|
|
275
425
|
compactResponseId: compactResult.compactResponseId,
|
|
276
426
|
createdAt: compactResult.createdAt,
|
|
277
427
|
requestMeta: buildCompactionRequestMeta(event),
|
|
428
|
+
inputProvenance,
|
|
278
429
|
});
|
|
279
430
|
} catch (error) {
|
|
280
431
|
writeDebugArtifact(
|
|
@@ -324,6 +475,8 @@ async function runResponsesNativeCompact(
|
|
|
324
475
|
baseUrl: compactor.baseUrl,
|
|
325
476
|
},
|
|
326
477
|
requestSource,
|
|
478
|
+
remoteV2ContextSource: config.remoteV2ContextSource,
|
|
479
|
+
inputProvenance,
|
|
327
480
|
requestInputItems: request.input.length,
|
|
328
481
|
requestExtras: extras ? Object.keys(extras) : [],
|
|
329
482
|
compactResponseId: compactResult.compactResponseId,
|
|
@@ -405,13 +558,47 @@ async function handleSessionBeforeCompact(
|
|
|
405
558
|
);
|
|
406
559
|
if (resolution.ok) {
|
|
407
560
|
remoteAttempted = true;
|
|
408
|
-
const responsesOutcome = await runResponsesNativeCompact(
|
|
561
|
+
const responsesOutcome = await runResponsesNativeCompact(
|
|
562
|
+
event,
|
|
563
|
+
ctx,
|
|
564
|
+
config,
|
|
565
|
+
resolution.execution,
|
|
566
|
+
dependencies.remoteCompact,
|
|
567
|
+
dependencies.projectCompactionContext,
|
|
568
|
+
);
|
|
409
569
|
if (responsesOutcome.outcome === "success") {
|
|
410
570
|
return { compaction: responsesOutcome.compaction };
|
|
411
571
|
}
|
|
412
572
|
if (responsesOutcome.outcome === "aborted") {
|
|
413
573
|
return { cancel: true };
|
|
414
574
|
}
|
|
575
|
+
if (responsesOutcome.outcome === "unprojected-input") {
|
|
576
|
+
const message = responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
|
|
577
|
+
? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
|
|
578
|
+
: responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
579
|
+
? "Pi did not provide the current session context required by its context-hook projection."
|
|
580
|
+
: responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
|
|
581
|
+
? "Pi's projected context did not include the current compaction summary anchor."
|
|
582
|
+
: "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
|
|
583
|
+
writeDebugArtifact(
|
|
584
|
+
"compaction-event",
|
|
585
|
+
{
|
|
586
|
+
event: "session_before_compact.remote-v2-unprojected-input",
|
|
587
|
+
reason: responsesOutcome.reason,
|
|
588
|
+
contextSource: config.remoteV2ContextSource,
|
|
589
|
+
inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
590
|
+
piContextHookPatch: dependencies.piContextHookPatch,
|
|
591
|
+
message,
|
|
592
|
+
},
|
|
593
|
+
config,
|
|
594
|
+
ctx,
|
|
595
|
+
);
|
|
596
|
+
notifyWarning(
|
|
597
|
+
ctx,
|
|
598
|
+
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} Keep the context-source setting aligned with the checkpoint or choose legacy mode explicitly.`,
|
|
599
|
+
);
|
|
600
|
+
return { cancel: true };
|
|
601
|
+
}
|
|
415
602
|
// failed: fall through to the configured-model fallback below.
|
|
416
603
|
} else {
|
|
417
604
|
writeDebugArtifact(
|
|
@@ -492,7 +679,7 @@ async function handleSessionBeforeCompact(
|
|
|
492
679
|
return undefined;
|
|
493
680
|
}
|
|
494
681
|
|
|
495
|
-
async function
|
|
682
|
+
async function handleContextInternal(
|
|
496
683
|
event: ContextEvent,
|
|
497
684
|
ctx: ExtensionContext,
|
|
498
685
|
pi: ExtensionAPI,
|
|
@@ -513,6 +700,7 @@ async function handleContext(
|
|
|
513
700
|
contextWindows.synchronize(ctx);
|
|
514
701
|
} catch {
|
|
515
702
|
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
703
|
+
reportPiContextHookFailure(ctx, "malformed-window-state");
|
|
516
704
|
ctx.abort();
|
|
517
705
|
return undefined;
|
|
518
706
|
}
|
|
@@ -532,6 +720,7 @@ async function handleContext(
|
|
|
532
720
|
: { messages: projected };
|
|
533
721
|
} catch (error) {
|
|
534
722
|
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
723
|
+
reportPiContextHookFailure(ctx, "malformed-window-state");
|
|
535
724
|
ctx.abort();
|
|
536
725
|
return undefined;
|
|
537
726
|
}
|
|
@@ -544,6 +733,7 @@ async function handleContext(
|
|
|
544
733
|
if (isCodexContextModel(ctx.model, config)) {
|
|
545
734
|
if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
|
|
546
735
|
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
736
|
+
reportPiContextHookFailure(ctx, "codex-context-unavailable");
|
|
547
737
|
ctx.abort();
|
|
548
738
|
return undefined;
|
|
549
739
|
}
|
|
@@ -568,16 +758,43 @@ async function handleContext(
|
|
|
568
758
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
569
759
|
const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
|
|
570
760
|
if (!latest.ok) return undefined;
|
|
571
|
-
const result = removeNativeCompactionRetainedMessages({
|
|
761
|
+
const result = removeNativeCompactionRetainedMessages({
|
|
762
|
+
messages: replayEvent.messages,
|
|
763
|
+
branchEntries,
|
|
764
|
+
compactionEntry: latest.entry,
|
|
765
|
+
expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
766
|
+
});
|
|
572
767
|
if (!result.ok) {
|
|
573
768
|
writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
|
|
574
769
|
if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
|
|
770
|
+
reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
|
|
575
771
|
ctx.abort();
|
|
576
772
|
return undefined;
|
|
577
773
|
}
|
|
578
774
|
return result.messages === replayEvent.messages ? undefined : { messages: result.messages };
|
|
579
775
|
}
|
|
580
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
|
|
779
|
+
* also report the failure so the compaction projector cannot mistake the
|
|
780
|
+
* caught exception for a successful projection.
|
|
781
|
+
*/
|
|
782
|
+
async function handleContext(
|
|
783
|
+
event: ContextEvent,
|
|
784
|
+
ctx: ExtensionContext,
|
|
785
|
+
pi: ExtensionAPI,
|
|
786
|
+
loadConfig: typeof loadToolkitConfig,
|
|
787
|
+
contextWindows: CodexContextWindowManager,
|
|
788
|
+
remoteContextActive: RemoteContextActive,
|
|
789
|
+
) {
|
|
790
|
+
try {
|
|
791
|
+
return await handleContextInternal(event, ctx, pi, loadConfig, contextWindows, remoteContextActive);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
581
798
|
async function handleBeforeProviderRequest(
|
|
582
799
|
event: BeforeProviderRequestEvent,
|
|
583
800
|
ctx: ExtensionContext,
|
|
@@ -660,13 +877,20 @@ async function handleBeforeProviderRequest(
|
|
|
660
877
|
// synthetic compact request using the active consumer's effective runtime identity.
|
|
661
878
|
// This hook runs before the separate Web Search transform, so injected native search
|
|
662
879
|
// tools are not copied into remote_compaction_v2.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
880
|
+
const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
|
|
881
|
+
rememberRequestContext(
|
|
882
|
+
payload,
|
|
883
|
+
{
|
|
884
|
+
provider: runtime.provider,
|
|
885
|
+
api: runtime.api,
|
|
886
|
+
model: runtime.model,
|
|
887
|
+
baseUrl: runtime.baseUrl,
|
|
888
|
+
sessionId: getSessionId(ctx),
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
|
|
892
|
+
},
|
|
893
|
+
);
|
|
670
894
|
|
|
671
895
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
672
896
|
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
@@ -699,6 +923,7 @@ async function handleBeforeProviderRequest(
|
|
|
699
923
|
payload,
|
|
700
924
|
branchEntries,
|
|
701
925
|
compactionEntry: latestNativeCompactionEntry,
|
|
926
|
+
expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
702
927
|
});
|
|
703
928
|
if (!rewrite.ok) {
|
|
704
929
|
writeDebugArtifact(
|
|
@@ -776,6 +1001,7 @@ export default function registerCompactionExtension(
|
|
|
776
1001
|
overrides: Partial<CompactionDependencies> = {},
|
|
777
1002
|
) {
|
|
778
1003
|
const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
|
|
1004
|
+
const piContextHookPatch = installPiContextHookPatch();
|
|
779
1005
|
const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal) =>
|
|
780
1006
|
loadHistoryNotesThreadHint(ctx, signal, loadConfig().config.compaction.gatewayContextModels)
|
|
781
1007
|
);
|
|
@@ -785,6 +1011,7 @@ export default function registerCompactionExtension(
|
|
|
785
1011
|
nativeFallback: runNativeFallbackCompaction,
|
|
786
1012
|
contextWindows,
|
|
787
1013
|
...overrides,
|
|
1014
|
+
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
788
1015
|
};
|
|
789
1016
|
let tools!: ReturnType<typeof registerContextManagementTools>;
|
|
790
1017
|
tools = registerContextManagementTools(
|
|
@@ -847,9 +1074,11 @@ export default function registerCompactionExtension(
|
|
|
847
1074
|
activation: {
|
|
848
1075
|
active,
|
|
849
1076
|
contextManagement: config.contextManagement,
|
|
1077
|
+
remoteV2ContextSource: config.remoteV2ContextSource,
|
|
850
1078
|
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
|
|
851
1079
|
...(activationReason ? { reason: activationReason } : {}),
|
|
852
1080
|
},
|
|
1081
|
+
piContextHookPatch: dependencies.piContextHookPatch,
|
|
853
1082
|
},
|
|
854
1083
|
config,
|
|
855
1084
|
ctx,
|
package/src/payload-rewrite.ts
CHANGED
|
@@ -8,8 +8,12 @@ import type {
|
|
|
8
8
|
SessionMessageEntry,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
11
|
+
import { hasVerifiedCompactionInputProvenance } from "./compaction-projection";
|
|
11
12
|
import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
|
|
12
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
NativeCompactionEntry,
|
|
15
|
+
NativeCompactionInputProvenance,
|
|
16
|
+
} from "./types";
|
|
13
17
|
import { serializeMessagesToResponsesInput, type ResponsesInputContentItem, type ResponsesInputItem, type ResponsesInputMessageItem } from "./serializer";
|
|
14
18
|
|
|
15
19
|
export type NativeReplaySegments = {
|
|
@@ -35,7 +39,8 @@ export type NativeReplayPayloadRewriteFailureReason =
|
|
|
35
39
|
| "invalid-compacted-window"
|
|
36
40
|
| "unexpected-compaction-after-boundary"
|
|
37
41
|
| "compaction-summary-not-found"
|
|
38
|
-
| "retained-context-mismatch"
|
|
42
|
+
| "retained-context-mismatch"
|
|
43
|
+
| "unverified-compaction-input";
|
|
39
44
|
|
|
40
45
|
export type NativeReplayPayloadRewriteFailure = {
|
|
41
46
|
ok: false;
|
|
@@ -323,29 +328,60 @@ export function removeNativeCompactionRetainedMessages(args: {
|
|
|
323
328
|
messages: AgentMessage[];
|
|
324
329
|
branchEntries: readonly SessionEntry[];
|
|
325
330
|
compactionEntry: NativeCompactionEntry;
|
|
331
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
326
332
|
}): { ok: true; messages: AgentMessage[] } | NativeReplayPayloadRewriteFailure {
|
|
327
333
|
const boundary = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
|
|
328
334
|
if (boundary === undefined) return { ok: false, reason: "compaction-boundary-not-found" };
|
|
335
|
+
if (!hasVerifiedCompactionInputProvenance(args.compactionEntry.details, args.expectedInputProvenance)) {
|
|
336
|
+
return { ok: false, reason: "unverified-compaction-input" };
|
|
337
|
+
}
|
|
329
338
|
const firstKept = findEntryIndexByIdBeforeBoundary(args.branchEntries, args.compactionEntry.firstKeptEntryId, boundary);
|
|
330
339
|
if (firstKept === undefined) return { ok: false, reason: "first-kept-entry-not-found" };
|
|
331
|
-
const summary = sessionEntryToContextMessages(args.compactionEntry)[0];
|
|
340
|
+
const summary = structuredClone(sessionEntryToContextMessages(args.compactionEntry)[0]);
|
|
332
341
|
const summaryIndex = args.messages.findIndex((message) => areEquivalentValues(message, summary));
|
|
333
342
|
if (summaryIndex < 0) return { ok: false, reason: "compaction-summary-not-found" };
|
|
334
|
-
const retained = args.branchEntries
|
|
343
|
+
const retained = args.branchEntries
|
|
344
|
+
.slice(firstKept, boundary)
|
|
345
|
+
.flatMap(sessionEntryToContextMessages)
|
|
346
|
+
.map((message) => structuredClone(message));
|
|
347
|
+
// Custom messages are optional context-hook entries: a hook may filter or
|
|
348
|
+
// rewrite them before provider serialization. An exact custom copy is still
|
|
349
|
+
// removed when present, while a missing or rewritten copy is left alone.
|
|
350
|
+
// Required user/assistant/tool messages keep the ordered, fail-closed
|
|
351
|
+
// matching contract.
|
|
352
|
+
const requiredRetained = retained.filter((message) => message.role !== "custom");
|
|
353
|
+
const optionalRetained = retained.filter((message) => message.role === "custom");
|
|
335
354
|
const removed = new Set<number>();
|
|
336
355
|
let cursor = summaryIndex + 1;
|
|
337
|
-
for (const expected of
|
|
356
|
+
for (const expected of requiredRetained) {
|
|
338
357
|
const index = args.messages.findIndex((message, index) => index >= cursor && areEquivalentValues(message, expected));
|
|
339
358
|
if (index >= 0) {
|
|
340
359
|
removed.add(index);
|
|
341
360
|
cursor = index + 1;
|
|
342
361
|
}
|
|
343
362
|
}
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
363
|
+
// An empty required span is valid. For a non-empty span, zero matches is
|
|
364
|
+
// ambiguous with the entire required history having been removed or changed;
|
|
365
|
+
// fail closed instead of treating it as an already-filtered no-op. Partial
|
|
366
|
+
// matches are equally unsafe because they can remove half a call/result batch.
|
|
367
|
+
if (requiredRetained.length > 0 && removed.size !== requiredRetained.length) {
|
|
368
|
+
return { ok: false, reason: "retained-context-mismatch" };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Optional entries do not affect the strict parity decision. Search for each
|
|
372
|
+
// exact copy independently so a reordered custom entry can still be removed,
|
|
373
|
+
// but a provider-visible metadata/content rewrite remains untouched.
|
|
374
|
+
for (const expected of optionalRetained) {
|
|
375
|
+
const index = args.messages.findIndex(
|
|
376
|
+
(message, candidateIndex) =>
|
|
377
|
+
candidateIndex > summaryIndex && !removed.has(candidateIndex) && areEquivalentValues(message, expected),
|
|
378
|
+
);
|
|
379
|
+
if (index >= 0) removed.add(index);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return removed.size === 0
|
|
383
|
+
? { ok: true, messages: args.messages }
|
|
384
|
+
: { ok: true, messages: args.messages.filter((_, index) => !removed.has(index)) };
|
|
349
385
|
}
|
|
350
386
|
|
|
351
387
|
export function serializeLiveTailToResponsesInput<TApi extends Api>(args: {
|
|
@@ -360,6 +396,7 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
360
396
|
payload: ResponsesCompatibleRequestPayload;
|
|
361
397
|
branchEntries: readonly SessionEntry[];
|
|
362
398
|
compactionEntry: NativeCompactionEntry;
|
|
399
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
363
400
|
}): NativeReplayPayloadRewriteResult {
|
|
364
401
|
const boundaryIndex = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
|
|
365
402
|
if (boundaryIndex === undefined) {
|
|
@@ -398,6 +435,12 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
398
435
|
reason: "invalid-compacted-window",
|
|
399
436
|
};
|
|
400
437
|
}
|
|
438
|
+
if (!hasVerifiedCompactionInputProvenance(details, args.expectedInputProvenance)) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
reason: "unverified-compaction-input",
|
|
442
|
+
};
|
|
443
|
+
}
|
|
401
444
|
|
|
402
445
|
const compactedWindow = cloneOpaqueCompactedWindow(details.compactedWindow);
|
|
403
446
|
if (!compactedWindow) {
|
|
@@ -462,6 +505,7 @@ export function buildNativeReplaySegments<TApi extends Api>(args: {
|
|
|
462
505
|
payload: ResponsesCompatibleRequestPayload;
|
|
463
506
|
branchEntries: readonly SessionEntry[];
|
|
464
507
|
compactionEntry: NativeCompactionEntry;
|
|
508
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
465
509
|
}): NativeReplayPayloadRewriteResult {
|
|
466
510
|
return buildNativeReplaySegmentsInternal(args);
|
|
467
511
|
}
|
|
@@ -471,6 +515,7 @@ export function rewriteResponsesPayloadWithNativeReplay<TApi extends Api>(args:
|
|
|
471
515
|
payload: ResponsesCompatibleRequestPayload;
|
|
472
516
|
branchEntries: readonly SessionEntry[];
|
|
473
517
|
compactionEntry: NativeCompactionEntry;
|
|
518
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
474
519
|
}): NativeReplayPayloadRewriteResult {
|
|
475
520
|
return buildNativeReplaySegmentsInternal(args);
|
|
476
521
|
}
|