pi-openai-toolkit 0.20.7 → 0.21.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 -21
- package/NOTICE +83 -83
- package/README.md +8 -26
- package/README.zh.md +8 -26
- package/config.schema.json +278 -366
- package/docs/configuration.md +294 -310
- package/docs/internals.md +166 -175
- package/package.json +3 -13
- package/src/compact-client.ts +412 -389
- package/src/compaction-errors.ts +96 -0
- package/src/compaction-projection.ts +35 -35
- package/src/config/legacy.ts +519 -717
- package/src/config/migration.ts +103 -100
- package/src/config/notifications.ts +36 -28
- package/src/config/policy.ts +10 -18
- package/src/config/v2.ts +1167 -1380
- package/src/config.ts +310 -319
- package/src/context-management/codex-provider.ts +270 -270
- package/src/context-management/history-notes.ts +357 -357
- package/src/context-management/manual-compact.ts +0 -11
- package/src/context-management/tools.ts +371 -371
- package/src/context-management/types.ts +168 -168
- package/src/context-management/window-manager.ts +801 -801
- package/src/debug.ts +238 -237
- package/src/deferred-tool-carryover.ts +282 -282
- package/src/details-store.ts +151 -151
- package/src/extension-runtime.ts +1662 -1623
- package/src/image-generation/extension.ts +189 -189
- package/src/image-generation/protocol.ts +579 -579
- package/src/image-generation/types.ts +300 -300
- package/src/native-fallback.ts +4 -132
- package/src/payload-rewrite.ts +515 -518
- package/src/pi-context-hook.ts +117 -117
- package/src/remote-v2-client.ts +811 -814
- package/src/request-context-cache.ts +155 -155
- package/src/responses-headers.ts +180 -180
- package/src/runtime.ts +865 -832
- package/src/serializer.ts +882 -882
- package/src/supported-environment.ts +27 -27
- package/src/types.ts +565 -648
- package/src/web-search/alpha-client.ts +876 -876
- package/src/web-search/extension.ts +530 -530
- package/src/web-search/payload.ts +299 -299
- package/src/web-search/prompt.ts +60 -60
- package/src/web-search/types.ts +166 -166
- package/tsconfig.check.json +50 -54
- package/src/auto-mode/classifier.ts +0 -161
- package/src/auto-mode/extension.ts +0 -691
- package/src/auto-mode/model-selection-guard.ts +0 -52
- package/src/auto-mode/policy.ts +0 -146
- package/src/auto-mode/prompt.ts +0 -279
- package/src/auto-mode/reviewer.ts +0 -325
- package/src/auto-mode/tool-review-tui.ts +0 -211
- package/src/auto-mode/transcript.ts +0 -135
- package/src/auto-mode/types.ts +0 -137
package/src/payload-rewrite.ts
CHANGED
|
@@ -1,518 +1,515 @@
|
|
|
1
|
-
import { buildSessionProjection, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
6
|
-
import { hasVerifiedCompactionInputProvenance } from "./compaction-projection";
|
|
7
|
-
import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
|
|
8
|
-
import type {
|
|
9
|
-
NativeCompactionEntry,
|
|
10
|
-
NativeCompactionInputProvenance,
|
|
11
|
-
} from "./types";
|
|
12
|
-
import {
|
|
13
|
-
serializeMessagesToResponsesInput,
|
|
14
|
-
serializeMessagesToResponsesInputWithLimits,
|
|
15
|
-
type ResponsesInputContentItem,
|
|
16
|
-
type ResponsesInputItem,
|
|
17
|
-
type ResponsesInputMessageItem,
|
|
18
|
-
} from "./serializer";
|
|
19
|
-
|
|
20
|
-
export type NativeReplaySegments = {
|
|
21
|
-
boundaryIndex: number;
|
|
22
|
-
firstKeptEntryIndex: number;
|
|
23
|
-
instructions?: string;
|
|
24
|
-
leading: ResponsesInputItem[];
|
|
25
|
-
compactionSummary: ResponsesInputItem[];
|
|
26
|
-
compactedWindow: unknown[];
|
|
27
|
-
post: ResponsesInputItem[];
|
|
28
|
-
replayInput: unknown[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type NativeReplayPayloadRewrite = {
|
|
32
|
-
ok: true;
|
|
33
|
-
segments: NativeReplaySegments;
|
|
34
|
-
rewrittenPayload: ResponsesCompatibleRequestPayload;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type NativeReplayPayloadRewriteFailureReason =
|
|
38
|
-
| "compaction-boundary-not-found"
|
|
39
|
-
| "first-kept-entry-not-found"
|
|
40
|
-
| "invalid-compacted-window"
|
|
41
|
-
| "unexpected-compaction-after-boundary"
|
|
42
|
-
| "compaction-summary-not-found"
|
|
43
|
-
| "retained-context-mismatch"
|
|
44
|
-
| "unverified-compaction-input"
|
|
45
|
-
| "checkpoint-context-edited";
|
|
46
|
-
|
|
47
|
-
export type NativeReplayPayloadRewriteFailure = {
|
|
48
|
-
ok: false;
|
|
49
|
-
reason: NativeReplayPayloadRewriteFailureReason;
|
|
50
|
-
parity?: {
|
|
51
|
-
actual: string[];
|
|
52
|
-
expected: string[];
|
|
53
|
-
mismatches: string[];
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type NativeReplayPayloadRewriteResult =
|
|
58
|
-
| NativeReplayPayloadRewrite
|
|
59
|
-
| NativeReplayPayloadRewriteFailure;
|
|
60
|
-
|
|
61
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isResponsesInputContentItem(value: unknown): value is ResponsesInputContentItem {
|
|
66
|
-
if (!isRecord(value) || typeof value.type !== "string") {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (value.type === "input_text") {
|
|
71
|
-
return typeof value.text === "string";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (value.type === "input_image") {
|
|
75
|
-
return value.detail === "auto" && typeof value.image_url === "string";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function isResponsesInputMessageRole(value: unknown): value is ResponsesInputMessageItem["role"] {
|
|
82
|
-
return value === "user" || value === "developer" || value === "system";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function cloneResponsesInputContentItem(item: ResponsesInputContentItem): ResponsesInputContentItem {
|
|
86
|
-
return item.type === "input_text"
|
|
87
|
-
? {
|
|
88
|
-
type: "input_text",
|
|
89
|
-
text: item.text,
|
|
90
|
-
}
|
|
91
|
-
: {
|
|
92
|
-
type: "input_image",
|
|
93
|
-
detail: "auto",
|
|
94
|
-
image_url: item.image_url,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function cloneResponsesInputMessageItem(item: ResponsesInputMessageItem): ResponsesInputMessageItem {
|
|
99
|
-
return {
|
|
100
|
-
role: item.role,
|
|
101
|
-
content: typeof item.content === "string" ? item.content : item.content.map(cloneResponsesInputContentItem),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function cloneStructuredValue(value: unknown): unknown {
|
|
106
|
-
if (
|
|
107
|
-
value === undefined ||
|
|
108
|
-
value === null ||
|
|
109
|
-
typeof value === "string" ||
|
|
110
|
-
typeof value === "number" ||
|
|
111
|
-
typeof value === "boolean"
|
|
112
|
-
) {
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (Array.isArray(value)) {
|
|
117
|
-
return value.map(cloneStructuredValue);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (isRecord(value)) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
boundaryIndex
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
entries
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
*
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
compactionEntry
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
return { ok: false, reason: "
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
);
|
|
314
|
-
if (
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
model
|
|
371
|
-
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
model
|
|
380
|
-
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
compactionEntry
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}): NativeReplayPayloadRewriteResult {
|
|
517
|
-
return buildNativeReplaySegmentsInternal(args);
|
|
518
|
-
}
|
|
1
|
+
import { buildSessionProjection, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
6
|
+
import { hasVerifiedCompactionInputProvenance } from "./compaction-projection";
|
|
7
|
+
import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
|
|
8
|
+
import type {
|
|
9
|
+
NativeCompactionEntry,
|
|
10
|
+
NativeCompactionInputProvenance,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import {
|
|
13
|
+
serializeMessagesToResponsesInput,
|
|
14
|
+
serializeMessagesToResponsesInputWithLimits,
|
|
15
|
+
type ResponsesInputContentItem,
|
|
16
|
+
type ResponsesInputItem,
|
|
17
|
+
type ResponsesInputMessageItem,
|
|
18
|
+
} from "./serializer";
|
|
19
|
+
|
|
20
|
+
export type NativeReplaySegments = {
|
|
21
|
+
boundaryIndex: number;
|
|
22
|
+
firstKeptEntryIndex: number;
|
|
23
|
+
instructions?: string;
|
|
24
|
+
leading: ResponsesInputItem[];
|
|
25
|
+
compactionSummary: ResponsesInputItem[];
|
|
26
|
+
compactedWindow: unknown[];
|
|
27
|
+
post: ResponsesInputItem[];
|
|
28
|
+
replayInput: unknown[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type NativeReplayPayloadRewrite = {
|
|
32
|
+
ok: true;
|
|
33
|
+
segments: NativeReplaySegments;
|
|
34
|
+
rewrittenPayload: ResponsesCompatibleRequestPayload;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NativeReplayPayloadRewriteFailureReason =
|
|
38
|
+
| "compaction-boundary-not-found"
|
|
39
|
+
| "first-kept-entry-not-found"
|
|
40
|
+
| "invalid-compacted-window"
|
|
41
|
+
| "unexpected-compaction-after-boundary"
|
|
42
|
+
| "compaction-summary-not-found"
|
|
43
|
+
| "retained-context-mismatch"
|
|
44
|
+
| "unverified-compaction-input"
|
|
45
|
+
| "checkpoint-context-edited";
|
|
46
|
+
|
|
47
|
+
export type NativeReplayPayloadRewriteFailure = {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: NativeReplayPayloadRewriteFailureReason;
|
|
50
|
+
parity?: {
|
|
51
|
+
actual: string[];
|
|
52
|
+
expected: string[];
|
|
53
|
+
mismatches: string[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type NativeReplayPayloadRewriteResult =
|
|
58
|
+
| NativeReplayPayloadRewrite
|
|
59
|
+
| NativeReplayPayloadRewriteFailure;
|
|
60
|
+
|
|
61
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isResponsesInputContentItem(value: unknown): value is ResponsesInputContentItem {
|
|
66
|
+
if (!isRecord(value) || typeof value.type !== "string") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (value.type === "input_text") {
|
|
71
|
+
return typeof value.text === "string";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (value.type === "input_image") {
|
|
75
|
+
return value.detail === "auto" && typeof value.image_url === "string";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isResponsesInputMessageRole(value: unknown): value is ResponsesInputMessageItem["role"] {
|
|
82
|
+
return value === "user" || value === "developer" || value === "system";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cloneResponsesInputContentItem(item: ResponsesInputContentItem): ResponsesInputContentItem {
|
|
86
|
+
return item.type === "input_text"
|
|
87
|
+
? {
|
|
88
|
+
type: "input_text",
|
|
89
|
+
text: item.text,
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
type: "input_image",
|
|
93
|
+
detail: "auto",
|
|
94
|
+
image_url: item.image_url,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cloneResponsesInputMessageItem(item: ResponsesInputMessageItem): ResponsesInputMessageItem {
|
|
99
|
+
return {
|
|
100
|
+
role: item.role,
|
|
101
|
+
content: typeof item.content === "string" ? item.content : item.content.map(cloneResponsesInputContentItem),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cloneStructuredValue(value: unknown): unknown {
|
|
106
|
+
if (
|
|
107
|
+
value === undefined ||
|
|
108
|
+
value === null ||
|
|
109
|
+
typeof value === "string" ||
|
|
110
|
+
typeof value === "number" ||
|
|
111
|
+
typeof value === "boolean"
|
|
112
|
+
) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map(cloneStructuredValue);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isRecord(value)) {
|
|
121
|
+
// Define own data properties, including provider JSON keys such as __proto__.
|
|
122
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, cloneStructuredValue(nested)]));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error(`Unsupported structured value: ${typeof value}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cloneOpaqueCompactedWindow(compactedWindow: readonly unknown[]): unknown[] | undefined {
|
|
129
|
+
const cloned: unknown[] = [];
|
|
130
|
+
|
|
131
|
+
for (const item of compactedWindow) {
|
|
132
|
+
if (!isRecord(item)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
cloned.push(cloneStructuredValue(item));
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return cloned;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cloneResponsesInputSlice(items: readonly unknown[]): ResponsesInputItem[] | undefined {
|
|
147
|
+
const cloned: ResponsesInputItem[] = [];
|
|
148
|
+
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
try {
|
|
151
|
+
cloned.push(cloneStructuredValue(item) as ResponsesInputItem);
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return cloned;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function areEquivalentValues(left: unknown, right: unknown): boolean {
|
|
161
|
+
if (Object.is(left, right)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
166
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (let index = 0; index < left.length; index++) {
|
|
171
|
+
if (!areEquivalentValues(left[index], right[index])) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isRecord(left) || isRecord(right)) {
|
|
180
|
+
if (!isRecord(left) || !isRecord(right)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const leftKeys = Object.keys(left).sort();
|
|
185
|
+
const rightKeys = Object.keys(right).sort();
|
|
186
|
+
if (!areEquivalentValues(leftKeys, rightKeys)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const key of leftKeys) {
|
|
191
|
+
if (!areEquivalentValues(left[key], right[key])) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function extractUserItemText(item: Record<string, unknown>): unknown {
|
|
203
|
+
const { content } = item;
|
|
204
|
+
if (typeof content === "string") {
|
|
205
|
+
return content;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(content)) {
|
|
209
|
+
const first = content[0];
|
|
210
|
+
if (isRecord(first) && first.type === "input_text" && typeof first.text === "string") {
|
|
211
|
+
return first.text;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Locate the Pi-authored compaction summary item. The marker text is unique to
|
|
220
|
+
* this compaction entry, so a content match on the user item is a reliable
|
|
221
|
+
* single anchor; nothing else in the payload needs to be prefixed or counted.
|
|
222
|
+
*/
|
|
223
|
+
function findCompactionSummaryIndex(input: readonly unknown[], summaryMarker: string): number {
|
|
224
|
+
return input.findIndex((item) => {
|
|
225
|
+
if (!isRecord(item) || item.role !== "user") {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const text = extractUserItemText(item);
|
|
230
|
+
return typeof text === "string" && text.includes("<summary>") && text.includes(summaryMarker);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function collectReplayMessages(entries: readonly SessionEntry[]): AgentMessage[] {
|
|
235
|
+
return buildSessionProjection([...entries]).messages;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** An opaque checkpoint cannot be selectively edited after it was produced. */
|
|
239
|
+
export function hasInvalidatedCompactionContext(
|
|
240
|
+
entries: readonly SessionEntry[],
|
|
241
|
+
compactionEntryId: string,
|
|
242
|
+
): boolean {
|
|
243
|
+
const boundary = findCompactionBoundaryIndex(entries, compactionEntryId);
|
|
244
|
+
if (boundary === undefined) return false;
|
|
245
|
+
const coveredIds = new Set(entries.slice(0, boundary).map((entry) => entry.id));
|
|
246
|
+
return entries.slice(boundary + 1).some((entry) =>
|
|
247
|
+
entry.type === "context_edit" && coveredIds.has(entry.targetId),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findKeptEntryIndex(
|
|
252
|
+
entries: readonly SessionEntry[],
|
|
253
|
+
entryId: string,
|
|
254
|
+
boundaryIndex: number,
|
|
255
|
+
): number | undefined {
|
|
256
|
+
// Pi 0.87 uses the compaction itself as the retained boundary for retain-none.
|
|
257
|
+
if (entries[boundaryIndex]?.id === entryId) return boundaryIndex;
|
|
258
|
+
const index = entries.findIndex((entry, candidateIndex) => candidateIndex < boundaryIndex && entry.id === entryId);
|
|
259
|
+
return index >= 0 ? index : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function findCompactionBoundaryIndex(
|
|
263
|
+
entries: readonly SessionEntry[],
|
|
264
|
+
compactionEntryId: string,
|
|
265
|
+
): number | undefined {
|
|
266
|
+
const boundaryIndex = entries.findIndex((entry) => entry.id === compactionEntryId);
|
|
267
|
+
return boundaryIndex >= 0 ? boundaryIndex : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function findEntriesStrictlyAfterCompactionBoundary(
|
|
271
|
+
entries: readonly SessionEntry[],
|
|
272
|
+
compactionEntryId: string,
|
|
273
|
+
): SessionEntry[] | undefined {
|
|
274
|
+
const boundaryIndex = findCompactionBoundaryIndex(entries, compactionEntryId);
|
|
275
|
+
if (boundaryIndex === undefined) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return entries.slice(boundaryIndex + 1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function collectLiveTailMessages(entries: readonly SessionEntry[]): AgentMessage[] {
|
|
283
|
+
return collectReplayMessages(entries);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Remote V2 covers the complete pre-compaction context. Pi also retains
|
|
288
|
+
* recent messages, so remove those verified copies before provider conversion
|
|
289
|
+
* (where IDs/fields may change across models). Persisted session entries are
|
|
290
|
+
* untouched. Preserve transient additions and all post-compaction messages.
|
|
291
|
+
*/
|
|
292
|
+
export function removeNativeCompactionRetainedMessages(args: {
|
|
293
|
+
messages: AgentMessage[];
|
|
294
|
+
branchEntries: readonly SessionEntry[];
|
|
295
|
+
compactionEntry: NativeCompactionEntry;
|
|
296
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
297
|
+
}): { ok: true; messages: AgentMessage[] } | NativeReplayPayloadRewriteFailure {
|
|
298
|
+
const boundary = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
|
|
299
|
+
if (boundary === undefined) return { ok: false, reason: "compaction-boundary-not-found" };
|
|
300
|
+
if (!hasVerifiedCompactionInputProvenance(args.compactionEntry.details, args.expectedInputProvenance)) {
|
|
301
|
+
return { ok: false, reason: "unverified-compaction-input" };
|
|
302
|
+
}
|
|
303
|
+
if (hasInvalidatedCompactionContext(args.branchEntries, args.compactionEntry.id)) {
|
|
304
|
+
return { ok: false, reason: "checkpoint-context-edited" };
|
|
305
|
+
}
|
|
306
|
+
const firstKept = findKeptEntryIndex(args.branchEntries, args.compactionEntry.firstKeptEntryId, boundary);
|
|
307
|
+
if (firstKept === undefined) return { ok: false, reason: "first-kept-entry-not-found" };
|
|
308
|
+
const summaryMessage = sessionEntryToContextMessages(args.compactionEntry).find(
|
|
309
|
+
(message) => message.role === "compactionSummary",
|
|
310
|
+
);
|
|
311
|
+
if (!summaryMessage) return { ok: false, reason: "compaction-summary-not-found" };
|
|
312
|
+
const summary = structuredClone(summaryMessage);
|
|
313
|
+
const summaryIndex = args.messages.findIndex((message) => areEquivalentValues(message, summary));
|
|
314
|
+
if (summaryIndex < 0) return { ok: false, reason: "compaction-summary-not-found" };
|
|
315
|
+
const retainedIds = new Set(args.branchEntries.slice(firstKept, boundary).map((entry) => entry.id));
|
|
316
|
+
const retained = buildSessionProjection([...args.branchEntries]).entries
|
|
317
|
+
.filter((entry) => retainedIds.has(entry.sourceEntry.id))
|
|
318
|
+
.flatMap((entry) => entry.messages)
|
|
319
|
+
.map((message) => structuredClone(message));
|
|
320
|
+
// Custom messages are optional context-hook entries: a hook may filter or
|
|
321
|
+
// rewrite them before provider serialization. An exact custom copy is still
|
|
322
|
+
// removed when present, while a missing or rewritten copy is left alone.
|
|
323
|
+
// Required user/assistant/tool messages keep the ordered, fail-closed
|
|
324
|
+
// matching contract.
|
|
325
|
+
// Pi 0.86 promotes transcript system messages to the leading prompt when it
|
|
326
|
+
// rebuilds context after compaction. They can therefore appear before the
|
|
327
|
+
// compaction summary even when their session entry lies inside the retained
|
|
328
|
+
// span; keep them in place instead of treating that relocation as a mismatch.
|
|
329
|
+
const requiredRetained = retained.filter(
|
|
330
|
+
(message) => message.role !== "custom" && (message.role as string) !== "system",
|
|
331
|
+
);
|
|
332
|
+
const optionalRetained = retained.filter((message) => message.role === "custom");
|
|
333
|
+
const removed = new Set<number>();
|
|
334
|
+
let cursor = summaryIndex + 1;
|
|
335
|
+
for (const expected of requiredRetained) {
|
|
336
|
+
const index = args.messages.findIndex((message, index) => index >= cursor && areEquivalentValues(message, expected));
|
|
337
|
+
if (index >= 0) {
|
|
338
|
+
removed.add(index);
|
|
339
|
+
cursor = index + 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// An empty required span is valid. For a non-empty span, zero matches is
|
|
343
|
+
// ambiguous with the entire required history having been removed or changed;
|
|
344
|
+
// fail closed instead of treating it as an already-filtered no-op. Partial
|
|
345
|
+
// matches are equally unsafe because they can remove half a call/result batch.
|
|
346
|
+
if (requiredRetained.length > 0 && removed.size !== requiredRetained.length) {
|
|
347
|
+
return { ok: false, reason: "retained-context-mismatch" };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Optional entries do not affect the strict parity decision. Search for each
|
|
351
|
+
// exact copy independently so a reordered custom entry can still be removed,
|
|
352
|
+
// but a provider-visible metadata/content rewrite remains untouched.
|
|
353
|
+
for (const expected of optionalRetained) {
|
|
354
|
+
const index = args.messages.findIndex(
|
|
355
|
+
(message, candidateIndex) =>
|
|
356
|
+
candidateIndex > summaryIndex && !removed.has(candidateIndex) && areEquivalentValues(message, expected),
|
|
357
|
+
);
|
|
358
|
+
if (index >= 0) removed.add(index);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return removed.size === 0
|
|
362
|
+
? { ok: true, messages: args.messages }
|
|
363
|
+
: { ok: true, messages: args.messages.filter((_, index) => !removed.has(index)) };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function serializeLiveTailToResponsesInput<TApi extends Api>(args: {
|
|
367
|
+
model: Model<TApi>;
|
|
368
|
+
entries: readonly SessionEntry[];
|
|
369
|
+
}): ResponsesInputItem[] {
|
|
370
|
+
return serializeMessagesToResponsesInput(args.model, collectReplayMessages(args.entries), {
|
|
371
|
+
firstSystemMessageIsUpdate: true,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function serializeLiveTailToResponsesInputWithLimits<TApi extends Api>(args: {
|
|
376
|
+
model: Model<TApi>;
|
|
377
|
+
entries: readonly SessionEntry[];
|
|
378
|
+
}): Promise<ResponsesInputItem[]> {
|
|
379
|
+
return serializeMessagesToResponsesInputWithLimits(args.model, collectReplayMessages(args.entries), {
|
|
380
|
+
firstSystemMessageIsUpdate: true,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
385
|
+
model: Model<TApi>;
|
|
386
|
+
payload: ResponsesCompatibleRequestPayload;
|
|
387
|
+
branchEntries: readonly SessionEntry[];
|
|
388
|
+
compactionEntry: NativeCompactionEntry;
|
|
389
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
390
|
+
}): NativeReplayPayloadRewriteResult {
|
|
391
|
+
const boundaryIndex = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
|
|
392
|
+
if (boundaryIndex === undefined) {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
reason: "compaction-boundary-not-found",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (hasInvalidatedCompactionContext(args.branchEntries, args.compactionEntry.id)) {
|
|
400
|
+
return { ok: false, reason: "checkpoint-context-edited" };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const firstKeptEntryIndex = findKeptEntryIndex(
|
|
404
|
+
args.branchEntries,
|
|
405
|
+
args.compactionEntry.firstKeptEntryId,
|
|
406
|
+
boundaryIndex,
|
|
407
|
+
);
|
|
408
|
+
if (firstKeptEntryIndex === undefined) {
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
reason: "first-kept-entry-not-found",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const newerCompactionEntry = args.branchEntries
|
|
416
|
+
.slice(boundaryIndex + 1)
|
|
417
|
+
.some((entry) => entry.type === "compaction");
|
|
418
|
+
if (newerCompactionEntry) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
reason: "unexpected-compaction-after-boundary",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const details = args.compactionEntry.details;
|
|
426
|
+
if (!details) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
reason: "invalid-compacted-window",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (!hasVerifiedCompactionInputProvenance(details, args.expectedInputProvenance)) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: "unverified-compaction-input",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const compactedWindow = cloneOpaqueCompactedWindow(details.compactedWindow);
|
|
440
|
+
if (!compactedWindow) {
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
reason: "invalid-compacted-window",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// The context hook has removed Pi's verified retained copies. Replace only
|
|
448
|
+
// the summary anchor here; preserve provider fields and transient/post items.
|
|
449
|
+
const summaryIndex = findCompactionSummaryIndex(args.payload.input, args.compactionEntry.summary);
|
|
450
|
+
if (summaryIndex < 0) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
reason: "compaction-summary-not-found",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const leadingInput = cloneResponsesInputSlice(args.payload.input.slice(0, summaryIndex));
|
|
458
|
+
const actualCompactionSummary = cloneResponsesInputSlice([args.payload.input[summaryIndex]]);
|
|
459
|
+
const actualPostInput = cloneResponsesInputSlice(args.payload.input.slice(summaryIndex + 1));
|
|
460
|
+
if (!leadingInput || !actualCompactionSummary || !actualPostInput) {
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
reason: "compaction-summary-not-found",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const baseRewrittenPayload: ResponsesCompatibleRequestPayload = {
|
|
468
|
+
...args.payload,
|
|
469
|
+
input: [...leadingInput, ...compactedWindow, ...actualPostInput],
|
|
470
|
+
};
|
|
471
|
+
const carryoverRewrite = rewritePayloadWithDeferredToolCarryover({
|
|
472
|
+
payload: baseRewrittenPayload,
|
|
473
|
+
carryover: details.deferredToolCarryover,
|
|
474
|
+
compactionEntryId: args.compactionEntry.id,
|
|
475
|
+
checkpointEndIndex: leadingInput.length + compactedWindow.length,
|
|
476
|
+
compat: args.model.compat as
|
|
477
|
+
| { supportsAdditionalTools?: boolean; supportsToolSearch?: boolean }
|
|
478
|
+
| undefined,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
segments: {
|
|
484
|
+
boundaryIndex,
|
|
485
|
+
firstKeptEntryIndex,
|
|
486
|
+
instructions: typeof args.payload.instructions === "string" ? args.payload.instructions : undefined,
|
|
487
|
+
leading: leadingInput,
|
|
488
|
+
compactionSummary: actualCompactionSummary,
|
|
489
|
+
compactedWindow,
|
|
490
|
+
post: actualPostInput,
|
|
491
|
+
replayInput: carryoverRewrite.payload.input,
|
|
492
|
+
},
|
|
493
|
+
rewrittenPayload: carryoverRewrite.payload,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function buildNativeReplaySegments<TApi extends Api>(args: {
|
|
498
|
+
model: Model<TApi>;
|
|
499
|
+
payload: ResponsesCompatibleRequestPayload;
|
|
500
|
+
branchEntries: readonly SessionEntry[];
|
|
501
|
+
compactionEntry: NativeCompactionEntry;
|
|
502
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
503
|
+
}): NativeReplayPayloadRewriteResult {
|
|
504
|
+
return buildNativeReplaySegmentsInternal(args);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function rewriteResponsesPayloadWithNativeReplay<TApi extends Api>(args: {
|
|
508
|
+
model: Model<TApi>;
|
|
509
|
+
payload: ResponsesCompatibleRequestPayload;
|
|
510
|
+
branchEntries: readonly SessionEntry[];
|
|
511
|
+
compactionEntry: NativeCompactionEntry;
|
|
512
|
+
expectedInputProvenance: NativeCompactionInputProvenance;
|
|
513
|
+
}): NativeReplayPayloadRewriteResult {
|
|
514
|
+
return buildNativeReplaySegmentsInternal(args);
|
|
515
|
+
}
|