@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.
@@ -0,0 +1,548 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import type {
4
+ BranchSummaryEntry,
5
+ CustomMessageEntry,
6
+ SessionEntry,
7
+ SessionMessageEntry,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type { ResponsesCompatibleRequestPayload } from "./runtime";
10
+ import type { NativeCompactionEntry } from "./types";
11
+ import {
12
+ compareResponsesInputParity,
13
+ serializeMessagesToResponsesInput,
14
+ type ResponsesInputContentItem,
15
+ type ResponsesInputItem,
16
+ type ResponsesInputMessageItem,
17
+ } from "./serializer";
18
+
19
+ export type FreshAuthoritativePreamble = {
20
+ instructions?: string;
21
+ leadingInput: ResponsesInputMessageItem[];
22
+ trailingInput: ResponsesInputMessageItem[];
23
+ };
24
+
25
+ export type SerializedReplaySlice = {
26
+ entries: SessionEntry[];
27
+ messages: AgentMessage[];
28
+ input: ResponsesInputItem[];
29
+ };
30
+
31
+ export type NativeReplaySegments = {
32
+ boundaryIndex: number;
33
+ firstKeptEntryIndex: number;
34
+ instructions?: string;
35
+ freshPreamble: ResponsesInputMessageItem[];
36
+ trailingPreamble: ResponsesInputMessageItem[];
37
+ compactionSummary: ResponsesInputItem[];
38
+ preCompactionKeptWindow: SerializedReplaySlice;
39
+ compactedWindow: unknown[];
40
+ postCompactionTail: SerializedReplaySlice;
41
+ originalPiReplayInput: ResponsesInputItem[];
42
+ replayInput: unknown[];
43
+ };
44
+
45
+ export type NativeReplayPayloadRewrite = {
46
+ ok: true;
47
+ segments: NativeReplaySegments;
48
+ rewrittenPayload: ResponsesCompatibleRequestPayload;
49
+ };
50
+
51
+ export type NativeReplayPayloadRewriteFailureReason =
52
+ | "compaction-boundary-not-found"
53
+ | "first-kept-entry-not-found"
54
+ | "unsupported-instructions"
55
+ | "invalid-compacted-window"
56
+ | "unexpected-compaction-after-boundary"
57
+ | "expected-pi-replay-mismatch";
58
+
59
+ export type NativeReplayPayloadRewriteFailure = {
60
+ ok: false;
61
+ reason: NativeReplayPayloadRewriteFailureReason;
62
+ parity?: {
63
+ actual: string[];
64
+ expected: string[];
65
+ mismatches: string[];
66
+ };
67
+ };
68
+
69
+ export type NativeReplayPayloadRewriteResult =
70
+ | NativeReplayPayloadRewrite
71
+ | NativeReplayPayloadRewriteFailure;
72
+
73
+ function isRecord(value: unknown): value is Record<string, unknown> {
74
+ return !!value && typeof value === "object" && !Array.isArray(value);
75
+ }
76
+
77
+ function isResponsesInputContentItem(value: unknown): value is ResponsesInputContentItem {
78
+ if (!isRecord(value) || typeof value.type !== "string") {
79
+ return false;
80
+ }
81
+
82
+ if (value.type === "input_text") {
83
+ return typeof value.text === "string";
84
+ }
85
+
86
+ if (value.type === "input_image") {
87
+ return value.detail === "auto" && typeof value.image_url === "string";
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ function isResponsesInputMessageRole(value: unknown): value is ResponsesInputMessageItem["role"] {
94
+ return value === "user" || value === "developer" || value === "system";
95
+ }
96
+
97
+ function isPreambleRole(value: ResponsesInputMessageItem["role"]): value is "developer" | "system" {
98
+ return value === "developer" || value === "system";
99
+ }
100
+
101
+ function isResponsesInputMessageItem(value: unknown): value is ResponsesInputMessageItem {
102
+ if (!isRecord(value) || !isResponsesInputMessageRole(value.role)) {
103
+ return false;
104
+ }
105
+
106
+ const { content } = value;
107
+ return typeof content === "string" || (Array.isArray(content) && content.every(isResponsesInputContentItem));
108
+ }
109
+
110
+ function cloneResponsesInputContentItem(item: ResponsesInputContentItem): ResponsesInputContentItem {
111
+ return item.type === "input_text"
112
+ ? {
113
+ type: "input_text",
114
+ text: item.text,
115
+ }
116
+ : {
117
+ type: "input_image",
118
+ detail: "auto",
119
+ image_url: item.image_url,
120
+ };
121
+ }
122
+
123
+ function cloneResponsesInputMessageItem(item: ResponsesInputMessageItem): ResponsesInputMessageItem {
124
+ return {
125
+ role: item.role,
126
+ content: typeof item.content === "string" ? item.content : item.content.map(cloneResponsesInputContentItem),
127
+ };
128
+ }
129
+
130
+ function cloneStructuredValue(value: unknown): unknown {
131
+ if (
132
+ value === undefined ||
133
+ value === null ||
134
+ typeof value === "string" ||
135
+ typeof value === "number" ||
136
+ typeof value === "boolean"
137
+ ) {
138
+ return value;
139
+ }
140
+
141
+ if (Array.isArray(value)) {
142
+ return value.map(cloneStructuredValue);
143
+ }
144
+
145
+ if (isRecord(value)) {
146
+ const clone: Record<string, unknown> = {};
147
+ for (const [key, nested] of Object.entries(value)) {
148
+ clone[key] = cloneStructuredValue(nested);
149
+ }
150
+ return clone;
151
+ }
152
+
153
+ throw new Error(`Unsupported structured value: ${typeof value}`);
154
+ }
155
+
156
+ function cloneOpaqueCompactedWindow(compactedWindow: readonly unknown[]): unknown[] | undefined {
157
+ const cloned: unknown[] = [];
158
+
159
+ for (const item of compactedWindow) {
160
+ if (!isRecord(item)) {
161
+ return undefined;
162
+ }
163
+
164
+ try {
165
+ cloned.push(cloneStructuredValue(item));
166
+ } catch {
167
+ return undefined;
168
+ }
169
+ }
170
+
171
+ return cloned;
172
+ }
173
+
174
+ function cloneResponsesInputSlice(items: readonly unknown[]): ResponsesInputItem[] | undefined {
175
+ const cloned: ResponsesInputItem[] = [];
176
+
177
+ for (const item of items) {
178
+ try {
179
+ cloned.push(cloneStructuredValue(item) as ResponsesInputItem);
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ }
184
+
185
+ return cloned;
186
+ }
187
+
188
+ function areEquivalentValues(left: unknown, right: unknown): boolean {
189
+ if (Object.is(left, right)) {
190
+ return true;
191
+ }
192
+
193
+ if (Array.isArray(left) || Array.isArray(right)) {
194
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
195
+ return false;
196
+ }
197
+
198
+ for (let index = 0; index < left.length; index++) {
199
+ if (!areEquivalentValues(left[index], right[index])) {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ return true;
205
+ }
206
+
207
+ if (isRecord(left) || isRecord(right)) {
208
+ if (!isRecord(left) || !isRecord(right)) {
209
+ return false;
210
+ }
211
+
212
+ const leftKeys = Object.keys(left).sort();
213
+ const rightKeys = Object.keys(right).sort();
214
+ if (!areEquivalentValues(leftKeys, rightKeys)) {
215
+ return false;
216
+ }
217
+
218
+ for (const key of leftKeys) {
219
+ if (!areEquivalentValues(left[key], right[key])) {
220
+ return false;
221
+ }
222
+ }
223
+
224
+ return true;
225
+ }
226
+
227
+ return false;
228
+ }
229
+
230
+ function toBranchSummaryMessage(entry: BranchSummaryEntry): AgentMessage {
231
+ return {
232
+ role: "branchSummary",
233
+ summary: entry.summary,
234
+ fromId: entry.fromId,
235
+ timestamp: new Date(entry.timestamp).getTime(),
236
+ } as AgentMessage;
237
+ }
238
+
239
+ function toCustomMessage(entry: CustomMessageEntry): AgentMessage {
240
+ return {
241
+ role: "custom",
242
+ customType: entry.customType,
243
+ content: entry.content,
244
+ display: entry.display,
245
+ details: entry.details,
246
+ timestamp: new Date(entry.timestamp).getTime(),
247
+ } as AgentMessage;
248
+ }
249
+
250
+ function toSessionMessage(entry: SessionMessageEntry): AgentMessage {
251
+ return entry.message;
252
+ }
253
+
254
+ function toReplayAgentMessage(entry: SessionEntry): AgentMessage | undefined {
255
+ if (entry.type === "message") {
256
+ return toSessionMessage(entry);
257
+ }
258
+
259
+ if (entry.type === "custom_message") {
260
+ return toCustomMessage(entry);
261
+ }
262
+
263
+ if (entry.type === "branch_summary") {
264
+ return toBranchSummaryMessage(entry);
265
+ }
266
+
267
+ return undefined;
268
+ }
269
+
270
+ function isPromptEnvelopeItem(item: unknown): item is ResponsesInputMessageItem {
271
+ return isResponsesInputMessageItem(item) && isPreambleRole(item.role);
272
+ }
273
+
274
+ export function extractFreshAuthoritativePreamble(
275
+ payload: ResponsesCompatibleRequestPayload,
276
+ ): FreshAuthoritativePreamble | undefined {
277
+ if (payload.instructions !== undefined && typeof payload.instructions !== "string") {
278
+ return undefined;
279
+ }
280
+
281
+ // Developer/system items in Pi's Responses payload are prompt-level instructions,
282
+ // not transcript entries from session history. Preserve them in the same leading
283
+ // or trailing position that Pi authored so provider-added suffix prompts like
284
+ // GPT-5's trailing developer "# Juice: 0 !important" survive replay unchanged.
285
+ let leadingBoundary = 0;
286
+ while (leadingBoundary < payload.input.length && isPromptEnvelopeItem(payload.input[leadingBoundary])) {
287
+ leadingBoundary += 1;
288
+ }
289
+
290
+ let trailingBoundary = payload.input.length;
291
+ while (trailingBoundary > leadingBoundary && isPromptEnvelopeItem(payload.input[trailingBoundary - 1])) {
292
+ trailingBoundary -= 1;
293
+ }
294
+
295
+ for (let index = leadingBoundary; index < trailingBoundary; index++) {
296
+ if (isPromptEnvelopeItem(payload.input[index])) {
297
+ return undefined;
298
+ }
299
+ }
300
+
301
+ return {
302
+ ...(typeof payload.instructions === "string" ? { instructions: payload.instructions } : {}),
303
+ leadingInput: payload.input.slice(0, leadingBoundary).map((item) => cloneResponsesInputMessageItem(item as ResponsesInputMessageItem)),
304
+ trailingInput: payload.input
305
+ .slice(trailingBoundary)
306
+ .map((item) => cloneResponsesInputMessageItem(item as ResponsesInputMessageItem)),
307
+ };
308
+ }
309
+
310
+ function collectReplayMessages(entries: readonly SessionEntry[]): AgentMessage[] {
311
+ const messages: AgentMessage[] = [];
312
+
313
+ for (const entry of entries) {
314
+ const message = toReplayAgentMessage(entry);
315
+ if (message) {
316
+ messages.push(message);
317
+ }
318
+ }
319
+
320
+ return messages;
321
+ }
322
+
323
+ function createCompactionSummaryAgentMessage(entry: NativeCompactionEntry): AgentMessage {
324
+ return {
325
+ role: "compactionSummary",
326
+ summary: entry.summary,
327
+ tokensBefore: entry.tokensBefore,
328
+ timestamp: new Date(entry.timestamp).getTime(),
329
+ } as AgentMessage;
330
+ }
331
+
332
+ function createReplaySlice(
333
+ entries: readonly SessionEntry[],
334
+ messages: readonly AgentMessage[],
335
+ input: readonly ResponsesInputItem[],
336
+ ): SerializedReplaySlice {
337
+ return {
338
+ entries: [...entries],
339
+ messages: [...messages],
340
+ input: [...input],
341
+ };
342
+ }
343
+
344
+ function findEntryIndexByIdBeforeBoundary(
345
+ entries: readonly SessionEntry[],
346
+ entryId: string,
347
+ boundaryIndex: number,
348
+ ): number | undefined {
349
+ const index = entries.findIndex((entry, candidateIndex) => candidateIndex < boundaryIndex && entry.id === entryId);
350
+ return index >= 0 ? index : undefined;
351
+ }
352
+
353
+ export function findCompactionBoundaryIndex(
354
+ entries: readonly SessionEntry[],
355
+ compactionEntryId: string,
356
+ ): number | undefined {
357
+ const boundaryIndex = entries.findIndex((entry) => entry.id === compactionEntryId);
358
+ return boundaryIndex >= 0 ? boundaryIndex : undefined;
359
+ }
360
+
361
+ export function findEntriesStrictlyAfterCompactionBoundary(
362
+ entries: readonly SessionEntry[],
363
+ compactionEntryId: string,
364
+ ): SessionEntry[] | undefined {
365
+ const boundaryIndex = findCompactionBoundaryIndex(entries, compactionEntryId);
366
+ if (boundaryIndex === undefined) {
367
+ return undefined;
368
+ }
369
+
370
+ return entries.slice(boundaryIndex + 1);
371
+ }
372
+
373
+ export function collectLiveTailMessages(entries: readonly SessionEntry[]): AgentMessage[] {
374
+ return collectReplayMessages(entries);
375
+ }
376
+
377
+ export function serializeLiveTailToResponsesInput<TApi extends Api>(args: {
378
+ model: Model<TApi>;
379
+ entries: readonly SessionEntry[];
380
+ }): ResponsesInputItem[] {
381
+ return serializeMessagesToResponsesInput(args.model, collectReplayMessages(args.entries));
382
+ }
383
+
384
+ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
385
+ model: Model<TApi>;
386
+ payload: ResponsesCompatibleRequestPayload;
387
+ branchEntries: readonly SessionEntry[];
388
+ compactionEntry: NativeCompactionEntry;
389
+ }): NativeReplayPayloadRewriteResult {
390
+ const boundaryIndex = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
391
+ if (boundaryIndex === undefined) {
392
+ return {
393
+ ok: false,
394
+ reason: "compaction-boundary-not-found",
395
+ };
396
+ }
397
+
398
+ const firstKeptEntryIndex = findEntryIndexByIdBeforeBoundary(
399
+ args.branchEntries,
400
+ args.compactionEntry.firstKeptEntryId,
401
+ boundaryIndex,
402
+ );
403
+ if (firstKeptEntryIndex === undefined) {
404
+ return {
405
+ ok: false,
406
+ reason: "first-kept-entry-not-found",
407
+ };
408
+ }
409
+
410
+ const freshPreamble = extractFreshAuthoritativePreamble(args.payload);
411
+ if (!freshPreamble) {
412
+ return {
413
+ ok: false,
414
+ reason: "unsupported-instructions",
415
+ };
416
+ }
417
+
418
+ const newerCompactionEntry = args.branchEntries
419
+ .slice(boundaryIndex + 1)
420
+ .some((entry) => entry.type === "compaction");
421
+ if (newerCompactionEntry) {
422
+ return {
423
+ ok: false,
424
+ reason: "unexpected-compaction-after-boundary",
425
+ };
426
+ }
427
+
428
+ const compactedWindow = cloneOpaqueCompactedWindow(args.compactionEntry.details.compactedWindow);
429
+ if (!compactedWindow) {
430
+ return {
431
+ ok: false,
432
+ reason: "invalid-compacted-window",
433
+ };
434
+ }
435
+
436
+ const preCompactionEntries = args.branchEntries.slice(firstKeptEntryIndex, boundaryIndex);
437
+ const postCompactionEntries = args.branchEntries.slice(boundaryIndex + 1);
438
+ const preCompactionKeptMessages = collectReplayMessages(preCompactionEntries);
439
+ const postCompactionTailMessages = collectReplayMessages(postCompactionEntries);
440
+ const compactionSummaryMessage = createCompactionSummaryAgentMessage(args.compactionEntry);
441
+ const serializedPiHistoryInput = serializeMessagesToResponsesInput(args.model, [
442
+ compactionSummaryMessage,
443
+ ...preCompactionKeptMessages,
444
+ ...postCompactionTailMessages,
445
+ ]);
446
+ const originalPiReplayInput: ResponsesInputItem[] = [
447
+ ...freshPreamble.leadingInput,
448
+ ...serializedPiHistoryInput,
449
+ ...freshPreamble.trailingInput,
450
+ ];
451
+
452
+ if (!areEquivalentValues(args.payload.input, originalPiReplayInput)) {
453
+ const parity = compareResponsesInputParity(args.payload.input, originalPiReplayInput);
454
+ return {
455
+ ok: false,
456
+ reason: "expected-pi-replay-mismatch",
457
+ parity: {
458
+ actual: parity.actual,
459
+ expected: parity.expected,
460
+ mismatches: parity.mismatches,
461
+ },
462
+ };
463
+ }
464
+
465
+ const freshPreambleCount = freshPreamble.leadingInput.length;
466
+ const trailingPreambleCount = freshPreamble.trailingInput.length;
467
+ const compactionSummaryCount = serializeMessagesToResponsesInput(args.model, [compactionSummaryMessage]).length;
468
+ const preCompactionKeptCount = serializeMessagesToResponsesInput(args.model, preCompactionKeptMessages).length;
469
+ const tailStartIndex = freshPreambleCount + compactionSummaryCount + preCompactionKeptCount;
470
+ const tailEndIndex = args.payload.input.length - trailingPreambleCount;
471
+ const actualCompactionSummary = cloneResponsesInputSlice(
472
+ args.payload.input.slice(freshPreambleCount, freshPreambleCount + compactionSummaryCount),
473
+ );
474
+ const actualPreCompactionKeptWindow = cloneResponsesInputSlice(
475
+ args.payload.input.slice(
476
+ freshPreambleCount + compactionSummaryCount,
477
+ freshPreambleCount + compactionSummaryCount + preCompactionKeptCount,
478
+ ),
479
+ );
480
+ const actualPostCompactionTail = cloneResponsesInputSlice(args.payload.input.slice(tailStartIndex, tailEndIndex));
481
+ if (!actualCompactionSummary || !actualPreCompactionKeptWindow || !actualPostCompactionTail) {
482
+ return {
483
+ ok: false,
484
+ reason: "expected-pi-replay-mismatch",
485
+ };
486
+ }
487
+
488
+ const preCompactionKeptWindow = createReplaySlice(
489
+ preCompactionEntries,
490
+ preCompactionKeptMessages,
491
+ actualPreCompactionKeptWindow,
492
+ );
493
+ const postCompactionTail = createReplaySlice(
494
+ postCompactionEntries,
495
+ postCompactionTailMessages,
496
+ actualPostCompactionTail,
497
+ );
498
+
499
+ return {
500
+ ok: true,
501
+ segments: {
502
+ boundaryIndex,
503
+ firstKeptEntryIndex,
504
+ instructions: freshPreamble.instructions,
505
+ freshPreamble: freshPreamble.leadingInput,
506
+ trailingPreamble: freshPreamble.trailingInput,
507
+ compactionSummary: actualCompactionSummary,
508
+ preCompactionKeptWindow,
509
+ compactedWindow,
510
+ postCompactionTail,
511
+ originalPiReplayInput,
512
+ replayInput: [
513
+ ...freshPreamble.leadingInput,
514
+ ...compactedWindow,
515
+ ...actualPostCompactionTail,
516
+ ...freshPreamble.trailingInput,
517
+ ],
518
+ },
519
+ rewrittenPayload: {
520
+ ...args.payload,
521
+ ...(freshPreamble.instructions !== undefined ? { instructions: freshPreamble.instructions } : {}),
522
+ input: [
523
+ ...freshPreamble.leadingInput,
524
+ ...compactedWindow,
525
+ ...actualPostCompactionTail,
526
+ ...freshPreamble.trailingInput,
527
+ ],
528
+ },
529
+ };
530
+ }
531
+
532
+ export function buildNativeReplaySegments<TApi extends Api>(args: {
533
+ model: Model<TApi>;
534
+ payload: ResponsesCompatibleRequestPayload;
535
+ branchEntries: readonly SessionEntry[];
536
+ compactionEntry: NativeCompactionEntry;
537
+ }): NativeReplayPayloadRewriteResult {
538
+ return buildNativeReplaySegmentsInternal(args);
539
+ }
540
+
541
+ export function rewriteResponsesPayloadWithNativeReplay<TApi extends Api>(args: {
542
+ model: Model<TApi>;
543
+ payload: ResponsesCompatibleRequestPayload;
544
+ branchEntries: readonly SessionEntry[];
545
+ compactionEntry: NativeCompactionEntry;
546
+ }): NativeReplayPayloadRewriteResult {
547
+ return buildNativeReplaySegmentsInternal(args);
548
+ }
@@ -0,0 +1,84 @@
1
+ import type { ResponsesCompatibleRequestPayload } from "./runtime";
2
+
3
+ /**
4
+ * Fields the latest codex_rs CompactionInput accepts beyond model/input/instructions.
5
+ * The compact endpoint has no payload of its own at session_before_compact time, so we
6
+ * mirror them from the most recent live provider request for the same model/session.
7
+ */
8
+ export type CompactionRequestExtras = {
9
+ tools?: unknown[];
10
+ parallel_tool_calls?: boolean;
11
+ reasoning?: Record<string, unknown>;
12
+ service_tier?: string;
13
+ prompt_cache_key?: string;
14
+ text?: Record<string, unknown>;
15
+ };
16
+
17
+ type CachedRequestContext = {
18
+ model: string;
19
+ sessionId?: string;
20
+ extras: CompactionRequestExtras;
21
+ };
22
+
23
+ let cached: CachedRequestContext | undefined;
24
+
25
+ function isRecord(value: unknown): value is Record<string, unknown> {
26
+ return !!value && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+
29
+ /**
30
+ * Remember compact-relevant fields from a live Responses request payload.
31
+ * Purely additive metadata: failures are swallowed so caching can never
32
+ * break the provider request path.
33
+ */
34
+ export function rememberRequestContext(payload: ResponsesCompatibleRequestPayload, sessionId?: string): void {
35
+ try {
36
+ const extras: CompactionRequestExtras = {};
37
+ if (Array.isArray(payload.tools)) {
38
+ extras.tools = structuredClone(payload.tools);
39
+ }
40
+ if (typeof payload.parallel_tool_calls === "boolean") {
41
+ extras.parallel_tool_calls = payload.parallel_tool_calls;
42
+ }
43
+ if (isRecord(payload.reasoning)) {
44
+ extras.reasoning = structuredClone(payload.reasoning);
45
+ }
46
+ if (typeof payload.service_tier === "string" && payload.service_tier.trim().length > 0) {
47
+ extras.service_tier = payload.service_tier;
48
+ }
49
+ if (typeof payload.prompt_cache_key === "string" && payload.prompt_cache_key.trim().length > 0) {
50
+ extras.prompt_cache_key = payload.prompt_cache_key;
51
+ }
52
+ if (isRecord(payload.text)) {
53
+ extras.text = structuredClone(payload.text);
54
+ }
55
+
56
+ cached = {
57
+ model: payload.model,
58
+ sessionId,
59
+ extras,
60
+ };
61
+ } catch {
62
+ cached = undefined;
63
+ }
64
+ }
65
+
66
+ /** Return cached extras when they were captured for the same model (and session, when known). */
67
+ export function getCompactionRequestExtras(model: string, sessionId?: string): CompactionRequestExtras | undefined {
68
+ if (!cached || cached.model !== model) {
69
+ return undefined;
70
+ }
71
+ if (cached.sessionId !== undefined && sessionId !== undefined && cached.sessionId !== sessionId) {
72
+ return undefined;
73
+ }
74
+
75
+ try {
76
+ return structuredClone(cached.extras);
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ export function clearRequestContextCache(): void {
83
+ cached = undefined;
84
+ }