@nijaru/pi-provider-compaction 0.0.3 → 0.0.5

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/dist/index.js CHANGED
@@ -1,22 +1,239 @@
1
- // node_modules/@earendil-works/pi-ai/dist/utils/hash.js
2
- function shortHash(str) {
3
- let h1 = 3735928559;
4
- let h2 = 1103547991;
5
- for (let i = 0;i < str.length; i++) {
6
- const ch = str.charCodeAt(i);
7
- h1 = Math.imul(h1 ^ ch, 2654435761);
8
- h2 = Math.imul(h2 ^ ch, 1597334677);
1
+ // index.ts
2
+ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
3
+ import { normalizeContext as normalizeContext2 } from "@earendil-works/pi-ai";
4
+ import { compact, convertToLlm } from "@earendil-works/pi-coding-agent";
5
+
6
+ // policy.ts
7
+ import {
8
+ CONFIG_DIR_NAME,
9
+ getAgentDir,
10
+ hasTrustRequiringProjectResources,
11
+ ProjectTrustStore
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import { closeSync, constants as fsConstants, fstatSync, openSync, readSync } from "node:fs";
14
+ import { join, resolve, sep } from "node:path";
15
+ var COMPACTION_MODEL_FLAG = "compaction-model";
16
+ var PROJECT_POLICY = "compaction-policy.json";
17
+ var AGENT_DIR_OVERRIDE = "PI_COMPACTOR_AGENT_DIR";
18
+ var MAX_POLICY_BYTES = 64 * 1024;
19
+ var MAX_MODEL_SELECTORS = 8;
20
+ var MAX_SELECTOR_LENGTH = 512;
21
+ function normalizeSelector(value) {
22
+ if (typeof value !== "string")
23
+ return;
24
+ const selector = value.trim();
25
+ if (selector.length === 0 || selector.length > MAX_SELECTOR_LENGTH)
26
+ return;
27
+ return selector;
28
+ }
29
+ function parsePolicySelectors(value) {
30
+ if (typeof value !== "object" || value === null || Array.isArray(value))
31
+ return;
32
+ if (!("models" in value) || !Array.isArray(value.models))
33
+ return;
34
+ const selectors = [];
35
+ for (const model of value.models) {
36
+ const selector = normalizeSelector(model);
37
+ if (selector === undefined || selectors.includes(selector))
38
+ continue;
39
+ selectors.push(selector);
40
+ if (selectors.length === MAX_MODEL_SELECTORS)
41
+ break;
9
42
  }
10
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
11
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
12
- return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
43
+ return selectors;
44
+ }
45
+ function readPolicySelectors(configPath) {
46
+ let fd;
47
+ try {
48
+ fd = openSync(configPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
49
+ const stat = fstatSync(fd);
50
+ if (!stat.isFile() || stat.size > MAX_POLICY_BYTES)
51
+ return;
52
+ const buffer = Buffer.allocUnsafe(MAX_POLICY_BYTES + 1);
53
+ let bytesRead = 0;
54
+ while (bytesRead < buffer.length) {
55
+ const count = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null);
56
+ if (count === 0)
57
+ break;
58
+ bytesRead += count;
59
+ }
60
+ if (bytesRead > MAX_POLICY_BYTES)
61
+ return;
62
+ return parsePolicySelectors(JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")));
63
+ } catch {
64
+ return;
65
+ } finally {
66
+ if (fd !== undefined) {
67
+ try {
68
+ closeSync(fd);
69
+ } catch {}
70
+ }
71
+ }
72
+ }
73
+ function activeAgentDir(ctx) {
74
+ const override = process.env[AGENT_DIR_OVERRIDE]?.trim();
75
+ if (override)
76
+ return resolve(override);
77
+ try {
78
+ const sessionDir = ctx.sessionManager?.getSessionDir();
79
+ if (sessionDir) {
80
+ const resolvedCwd = resolve(ctx.cwd);
81
+ const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
82
+ const suffix = `${sep}${join("sessions", safePath)}`;
83
+ const resolvedSessionDir = resolve(sessionDir);
84
+ if (resolvedSessionDir.endsWith(suffix)) {
85
+ return resolvedSessionDir.slice(0, -suffix.length);
86
+ }
87
+ }
88
+ } catch {}
89
+ return getAgentDir();
90
+ }
91
+ function isProjectPolicyTrusted(ctx, agentDir) {
92
+ try {
93
+ if (!ctx.isProjectTrusted())
94
+ return false;
95
+ return hasTrustRequiringProjectResources(ctx.cwd) || new ProjectTrustStore(agentDir).get(ctx.cwd) === true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function readCompactionModelFlagFromArgv(argv = process.argv.slice(2)) {
101
+ let value;
102
+ const prefix = `--${COMPACTION_MODEL_FLAG}=`;
103
+ for (let i = 0;i < argv.length; i++) {
104
+ const arg = argv[i];
105
+ if (arg === "--")
106
+ break;
107
+ if (arg === `--${COMPACTION_MODEL_FLAG}`) {
108
+ const next = argv[i + 1];
109
+ if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
110
+ value = next;
111
+ i++;
112
+ } else {
113
+ value = true;
114
+ }
115
+ } else if (arg.startsWith(prefix)) {
116
+ value = arg.slice(prefix.length);
117
+ }
118
+ }
119
+ return value;
120
+ }
121
+ function resolveCompactionModelPolicy(pi, ctx, argv = process.argv.slice(2)) {
122
+ const argvRaw = readCompactionModelFlagFromArgv(argv);
123
+ if (typeof argvRaw === "string" && argvRaw.trim()) {
124
+ const selector = normalizeSelector(argvRaw);
125
+ return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
126
+ }
127
+ try {
128
+ const rawFlag = pi.getFlag(COMPACTION_MODEL_FLAG);
129
+ if (typeof rawFlag === "string" && rawFlag.trim()) {
130
+ const selector = normalizeSelector(rawFlag);
131
+ return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
132
+ }
133
+ } catch {}
134
+ const agentDir = activeAgentDir(ctx);
135
+ const candidates = [];
136
+ if (isProjectPolicyTrusted(ctx, agentDir)) {
137
+ candidates.push({ path: join(ctx.cwd, CONFIG_DIR_NAME, PROJECT_POLICY), source: "project-policy" });
138
+ }
139
+ candidates.push({ path: join(agentDir, PROJECT_POLICY), source: "agent-policy" });
140
+ for (const candidate of candidates) {
141
+ const selectors = readPolicySelectors(candidate.path);
142
+ if (selectors !== undefined) {
143
+ return selectors.length > 0 ? { hasSelectors: true, source: candidate.source } : { hasSelectors: false };
144
+ }
145
+ }
146
+ return { hasSelectors: false };
13
147
  }
14
148
 
15
- // node_modules/@earendil-works/pi-ai/dist/utils/sanitize-unicode.js
16
- function sanitizeSurrogates(text) {
17
- return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
149
+ // provider.ts
150
+ function optionsWithObserver(model, context, options, observe) {
151
+ const handler = observe(model, context, options ?? {});
152
+ if (!handler)
153
+ return options;
154
+ return {
155
+ ...options,
156
+ onPayload: async (payload, requestModel) => {
157
+ const replacement = await options?.onPayload?.(payload, requestModel);
158
+ const prepared = replacement === undefined ? payload : replacement;
159
+ try {
160
+ return await handler(prepared);
161
+ } catch {
162
+ return prepared;
163
+ }
164
+ }
165
+ };
166
+ }
167
+ function decorateProvider(base, observe) {
168
+ const stream = (model, context, options) => base.stream(model, context, optionsWithObserver(model, context, options, observe));
169
+ const streamSimple = (model, context, options) => base.streamSimple(model, context, optionsWithObserver(model, context, options, observe));
170
+ const bound = new Map;
171
+ return new Proxy(base, {
172
+ get(target, key) {
173
+ if (key === "stream")
174
+ return stream;
175
+ if (key === "streamSimple")
176
+ return streamSimple;
177
+ const value = Reflect.get(target, key, target);
178
+ if (typeof value !== "function")
179
+ return value;
180
+ if (!bound.has(key))
181
+ bound.set(key, value.bind(target));
182
+ return bound.get(key);
183
+ }
184
+ });
185
+ }
186
+ function installObserver(pi, ctx, observe) {
187
+ const model = ctx.model;
188
+ if (!model)
189
+ return;
190
+ const registry = ctx.modelRegistry;
191
+ const id = model.provider;
192
+ const native = registry.getRegisteredNativeProvider(id);
193
+ if (native) {
194
+ const guarded = (...args) => {
195
+ const handler = observe(...args);
196
+ return handler ? (payload) => registry.getRegisteredNativeProvider(id) === wrapper ? handler(payload) : payload : undefined;
197
+ };
198
+ const wrapper = decorateProvider(native, guarded);
199
+ pi.registerProvider(wrapper);
200
+ return () => {
201
+ if (registry.getRegisteredNativeProvider(id) === wrapper)
202
+ pi.registerProvider(native);
203
+ };
204
+ }
205
+ const original = registry.getRegisteredProviderConfig(id);
206
+ if (!original?.streamSimple || original.refreshModels || original.oauth?.modifyModels)
207
+ return;
208
+ if (original.api !== model.api)
209
+ return;
210
+ const delegate = original.streamSimple.bind(original);
211
+ const guarded = (...args) => {
212
+ const handler = observe(...args);
213
+ return handler ? (payload) => registry.getRegisteredProviderConfig(id)?.streamSimple === wrapper ? handler(payload) : payload : undefined;
214
+ };
215
+ const wrapper = (requestModel, context, options) => delegate(requestModel, context, optionsWithObserver(requestModel, context, options, guarded));
216
+ pi.registerProvider(id, { ...original, api: original?.api ?? model.api, streamSimple: wrapper });
217
+ return () => {
218
+ const current = registry.getRegisteredProviderConfig(id);
219
+ if (current?.streamSimple !== wrapper || registry.getRegisteredNativeProvider(id))
220
+ return;
221
+ const restored = { ...current };
222
+ if (original?.streamSimple)
223
+ restored.streamSimple = original.streamSimple;
224
+ else
225
+ delete restored.streamSimple;
226
+ if (!original?.api && restored.api === model.api)
227
+ delete restored.api;
228
+ pi.unregisterProvider(id);
229
+ if (Object.keys(restored).length)
230
+ pi.registerProvider(id, restored);
231
+ };
18
232
  }
19
233
 
234
+ // replay.ts
235
+ import { getCurrentSystemPrompt, getCurrentTools as getCurrentTools2, getDeclaredTools, normalizeContext, resolveTranscript as resolveTranscript2 } from "@earendil-works/pi-ai";
236
+
20
237
  // node_modules/@earendil-works/pi-ai/dist/api/constrained-sampling.js
21
238
  class UnsupportedStrictJsonSchemaError extends Error {
22
239
  }
@@ -56,7 +273,7 @@ function schemaAllowsNull(schema) {
56
273
  return true;
57
274
  return Array.isArray(schema.anyOf) && schema.anyOf.some((variant) => schemaAllowsNull(variant));
58
275
  }
59
- function makeJsonSchemaNodeStrict(schema) {
276
+ function makeJsonSchemaNodeStrict(schema, isUnsupportedKeyword) {
60
277
  if (!isJsonSchemaObject(schema)) {
61
278
  throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
62
279
  }
@@ -65,6 +282,13 @@ function makeJsonSchemaNodeStrict(schema) {
65
282
  throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
66
283
  }
67
284
  }
285
+ if (isUnsupportedKeyword) {
286
+ for (const [key, value] of Object.entries(schema)) {
287
+ if (isUnsupportedKeyword(key, value)) {
288
+ throw new UnsupportedStrictJsonSchemaError(`${key}: ${JSON.stringify(value)} is unsupported`);
289
+ }
290
+ }
291
+ }
68
292
  if (schema.anyOf !== undefined) {
69
293
  if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) {
70
294
  throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
@@ -73,14 +297,14 @@ function makeJsonSchemaNodeStrict(schema) {
73
297
  if (isStructuredSchema(variant)) {
74
298
  throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
75
299
  }
76
- makeJsonSchemaNodeStrict(variant);
300
+ makeJsonSchemaNodeStrict(variant, isUnsupportedKeyword);
77
301
  }
78
302
  }
79
303
  if (schema.items !== undefined) {
80
304
  if (Array.isArray(schema.items)) {
81
305
  throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
82
306
  }
83
- makeJsonSchemaNodeStrict(schema.items);
307
+ makeJsonSchemaNodeStrict(schema.items, isUnsupportedKeyword);
84
308
  }
85
309
  const isObjectSchema = schema.type === "object";
86
310
  if (schema.properties !== undefined && !isObjectSchema) {
@@ -104,7 +328,7 @@ function makeJsonSchemaNodeStrict(schema) {
104
328
  throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
105
329
  }
106
330
  for (const [key, property] of Object.entries(properties)) {
107
- makeJsonSchemaNodeStrict(property);
331
+ makeJsonSchemaNodeStrict(property, isUnsupportedKeyword);
108
332
  if (!required.has(key) && !schemaAllowsNull(property)) {
109
333
  properties[key] = { anyOf: [property, { type: "null" }] };
110
334
  }
@@ -112,12 +336,12 @@ function makeJsonSchemaNodeStrict(schema) {
112
336
  schema.required = propertyNames;
113
337
  schema.additionalProperties = false;
114
338
  }
115
- function makeStrictJsonSchema(schema) {
339
+ function makeStrictJsonSchema(schema, isUnsupportedKeyword) {
116
340
  const cloned = structuredClone(schema);
117
341
  if (!isJsonSchemaObject(cloned)) {
118
342
  throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
119
343
  }
120
- makeJsonSchemaNodeStrict(cloned);
344
+ makeJsonSchemaNodeStrict(cloned, isUnsupportedKeyword);
121
345
  if (cloned.type !== "object") {
122
346
  throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
123
347
  }
@@ -150,13 +374,13 @@ function inferGrammarInputProperty(tool) {
150
374
  }
151
375
  return inputProperty;
152
376
  }
153
- function resolveJsonSchemaStrictSampling(tool, supportsStrictMode) {
377
+ function resolveJsonSchemaStrictSampling(tool, supportsStrictMode, isUnsupportedKeyword) {
154
378
  const config = tool.constrainedSampling;
155
379
  if (!config || config.type !== "json_schema")
156
380
  return;
157
381
  if (supportsStrictMode) {
158
382
  try {
159
- makeStrictJsonSchema(tool.parameters);
383
+ makeStrictJsonSchema(tool.parameters, isUnsupportedKeyword);
160
384
  return true;
161
385
  } catch (error) {
162
386
  if (!(error instanceof UnsupportedStrictJsonSchemaError))
@@ -197,6 +421,149 @@ function resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools) {
197
421
  throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: ${message}.`);
198
422
  }
199
423
  }
424
+ function createGrammarToolInputProperties(tools, supportsOpenAIGrammarTools) {
425
+ const properties = new Map;
426
+ for (const tool of tools ?? []) {
427
+ const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
428
+ if (grammar) {
429
+ properties.set(tool.name, grammar.inputProperty);
430
+ }
431
+ }
432
+ return properties;
433
+ }
434
+
435
+ // node_modules/@earendil-works/pi-ai/dist/utils/text.js
436
+ function contentText(content, separator = `
437
+ `) {
438
+ if (typeof content === "string")
439
+ return content;
440
+ return content.filter((block) => block.type === "text").map((block) => block.text).join(separator);
441
+ }
442
+ function getSystemMessageText(message) {
443
+ const parts = [contentText(message.content)];
444
+ for (const text of Object.values(message.sections ?? {})) {
445
+ if (text !== null)
446
+ parts.push(text);
447
+ }
448
+ return parts.filter((part) => part.length > 0).join(`
449
+
450
+ `);
451
+ }
452
+ function renderSystemMessageUpdate(message) {
453
+ const parts = [];
454
+ const text = contentText(message.content);
455
+ if (text.length > 0)
456
+ parts.push(text);
457
+ for (const [name, value] of Object.entries(message.sections ?? {})) {
458
+ parts.push(value === null ? `Removed system prompt section "${name}".` : `Updated system prompt section "${name}":
459
+
460
+ ${value}`);
461
+ }
462
+ return parts.join(`
463
+
464
+ `);
465
+ }
466
+
467
+ // node_modules/@earendil-works/pi-ai/dist/utils/transcript.js
468
+ function isSystemMessage(message) {
469
+ return message.role === "system";
470
+ }
471
+ function getInitialSystemMessage(messages) {
472
+ const first = messages[0];
473
+ return first && isSystemMessage(first) ? first : undefined;
474
+ }
475
+ function getCurrentTools(messages) {
476
+ const tools = new Map;
477
+ for (const message of messages) {
478
+ if (!isSystemMessage(message))
479
+ continue;
480
+ for (const tool of message.toolsRemoved ?? [])
481
+ tools.delete(tool.name);
482
+ for (const tool of message.toolsAdded ?? [])
483
+ tools.set(tool.name, tool);
484
+ }
485
+ return [...tools.values()];
486
+ }
487
+ function getCurrentSystemMessage(messages) {
488
+ const content = [];
489
+ const sections = new Map;
490
+ let timestamp;
491
+ for (const message of messages) {
492
+ if (!isSystemMessage(message))
493
+ continue;
494
+ timestamp ??= message.timestamp;
495
+ const text = contentText(message.content);
496
+ if (text.length > 0)
497
+ content.push(text);
498
+ for (const [name, value] of Object.entries(message.sections ?? {})) {
499
+ if (value === null)
500
+ sections.delete(name);
501
+ else
502
+ sections.set(name, value);
503
+ }
504
+ }
505
+ const tools = getCurrentTools(messages);
506
+ if (timestamp === undefined && tools.length === 0)
507
+ return;
508
+ return {
509
+ role: "system",
510
+ content: content.join(`
511
+
512
+ `),
513
+ ...sections.size > 0 ? { sections: Object.fromEntries(sections) } : {},
514
+ ...tools.length > 0 ? { toolsAdded: tools } : {},
515
+ timestamp: timestamp ?? 0
516
+ };
517
+ }
518
+ function collapseSystemMessages(context) {
519
+ const head = getCurrentSystemMessage(context.messages);
520
+ const messages = context.messages.filter((message) => message.role !== "system");
521
+ return { messages: head ? [head, ...messages] : messages };
522
+ }
523
+ function resolveTranscript(context, supportsMidConvoSystemMessages) {
524
+ return supportsMidConvoSystemMessages ? context : collapseSystemMessages(context);
525
+ }
526
+ function hasNonAdditiveToolChanges(messages) {
527
+ const declared = new Set;
528
+ for (const message of messages) {
529
+ if (!isSystemMessage(message))
530
+ continue;
531
+ if ((message.toolsRemoved?.length ?? 0) > 0)
532
+ return true;
533
+ for (const tool of message.toolsAdded ?? []) {
534
+ if (declared.has(tool.name))
535
+ return true;
536
+ declared.add(tool.name);
537
+ }
538
+ }
539
+ return false;
540
+ }
541
+ function resolveTranscriptTools(messages, supportsToolAdditions) {
542
+ const anchorsAdditions = supportsToolAdditions && !hasNonAdditiveToolChanges(messages);
543
+ return {
544
+ requestTools: anchorsAdditions ? getInitialSystemMessage(messages)?.toolsAdded ?? [] : getCurrentTools(messages),
545
+ anchorsAdditions
546
+ };
547
+ }
548
+
549
+ // node_modules/@earendil-works/pi-ai/dist/utils/hash.js
550
+ function shortHash(str) {
551
+ let h1 = 3735928559;
552
+ let h2 = 1103547991;
553
+ for (let i = 0;i < str.length; i++) {
554
+ const ch = str.charCodeAt(i);
555
+ h1 = Math.imul(h1 ^ ch, 2654435761);
556
+ h2 = Math.imul(h2 ^ ch, 1597334677);
557
+ }
558
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
559
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
560
+ return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
561
+ }
562
+
563
+ // node_modules/@earendil-works/pi-ai/dist/utils/sanitize-unicode.js
564
+ function sanitizeSurrogates(text) {
565
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
566
+ }
200
567
 
201
568
  // node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js
202
569
  var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
@@ -242,7 +609,7 @@ function transformMessages(messages, model, normalizeToolCallId) {
242
609
  const normalizedMessages = messages.map((msg) => msg.content == null ? { ...msg, content: [] } : msg);
243
610
  const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
244
611
  const transformed = imageAwareMessages.map((msg) => {
245
- if (msg.role === "user") {
612
+ if (msg.role === "system" || msg.role === "user") {
246
613
  return msg;
247
614
  }
248
615
  if (msg.role === "toolResult") {
@@ -307,7 +674,8 @@ function transformMessages(messages, model, normalizeToolCallId) {
307
674
  const result = [];
308
675
  let pendingToolCalls = [];
309
676
  let existingToolResultIds = new Set;
310
- const insertSyntheticToolResults = () => {
677
+ const heldSystemMessages = [];
678
+ const closePendingToolCalls = () => {
311
679
  if (pendingToolCalls.length > 0) {
312
680
  for (const tc of pendingToolCalls) {
313
681
  if (!existingToolResultIds.has(tc.id)) {
@@ -324,11 +692,13 @@ function transformMessages(messages, model, normalizeToolCallId) {
324
692
  pendingToolCalls = [];
325
693
  existingToolResultIds = new Set;
326
694
  }
695
+ result.push(...heldSystemMessages);
696
+ heldSystemMessages.length = 0;
327
697
  };
328
698
  for (let i = 0;i < transformed.length; i++) {
329
699
  const msg = transformed[i];
330
700
  if (msg.role === "assistant") {
331
- insertSyntheticToolResults();
701
+ closePendingToolCalls();
332
702
  const assistantMsg = msg;
333
703
  if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
334
704
  continue;
@@ -342,14 +712,20 @@ function transformMessages(messages, model, normalizeToolCallId) {
342
712
  } else if (msg.role === "toolResult") {
343
713
  existingToolResultIds.add(msg.toolCallId);
344
714
  result.push(msg);
715
+ } else if (msg.role === "system") {
716
+ if (pendingToolCalls.length > 0) {
717
+ heldSystemMessages.push(msg);
718
+ } else {
719
+ result.push(msg);
720
+ }
345
721
  } else if (msg.role === "user") {
346
- insertSyntheticToolResults();
722
+ closePendingToolCalls();
347
723
  result.push(msg);
348
724
  } else {
349
725
  result.push(msg);
350
726
  }
351
727
  }
352
- insertSyntheticToolResults();
728
+ closePendingToolCalls();
353
729
  return result;
354
730
  }
355
731
 
@@ -392,8 +768,8 @@ function convertToolResultOutput(model, content) {
392
768
  return output;
393
769
  }
394
770
  function convertResponsesMessages(model, context, allowedToolCallProviders, options) {
771
+ const normalizedContext = resolveTranscript(context, options?.supportsMidConvoSystemMessages);
395
772
  const messages = [];
396
- const loadedToolNames = new Set;
397
773
  const normalizeIdPart = (part) => {
398
774
  const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
399
775
  const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
@@ -417,19 +793,56 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
417
793
  }
418
794
  return `${normalizedCallId}|${normalizedItemId}`;
419
795
  };
420
- const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
421
- const includeSystemPrompt = options?.includeSystemPrompt ?? true;
422
- if (includeSystemPrompt && context.systemPrompt) {
423
- const compat = model.compat;
424
- const role = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
796
+ const transformedMessages = transformMessages(normalizedContext.messages, model, normalizeToolCallId);
797
+ const transcriptTools = resolveTranscriptTools(normalizedContext.messages, (options?.supportsAdditionalTools ?? false) || (options?.supportsToolSearch ?? false));
798
+ const appendSystemToolAdditions = (message, seed) => {
799
+ const tools = transcriptTools.anchorsAdditions ? message.toolsAdded ?? [] : [];
800
+ if (tools.length === 0)
801
+ return;
802
+ if (options?.supportsAdditionalTools) {
803
+ messages.push({
804
+ type: "additional_tools",
805
+ role: "developer",
806
+ tools: convertResponsesTools(tools, options.toolOptions)
807
+ });
808
+ return;
809
+ }
810
+ if (!options?.supportsToolSearch)
811
+ return;
812
+ const names = tools.map((tool) => tool.name);
813
+ const callId = `pi_tool_load_${shortHash(`${seed}:${names.join(",")}`)}`;
425
814
  messages.push({
426
- role,
427
- content: sanitizeSurrogates(context.systemPrompt)
815
+ type: "tool_search_call",
816
+ call_id: callId,
817
+ execution: "client",
818
+ status: "completed",
819
+ arguments: { query: names.join(" "), limit: names.length }
428
820
  });
429
- }
821
+ messages.push({
822
+ type: "tool_search_output",
823
+ call_id: callId,
824
+ execution: "client",
825
+ status: "completed",
826
+ tools: convertResponsesTools(tools, { ...options.toolOptions, toolSearchResult: true })
827
+ });
828
+ };
829
+ const includeInitialSystemMessage = options?.includeSystemPrompt ?? true;
830
+ const compat = model.compat;
831
+ const instructionRole = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
430
832
  let msgIndex = 0;
833
+ let sourceIndex = 0;
431
834
  for (const msg of transformedMessages) {
432
- if (msg.role === "user") {
835
+ const isLeadingSystemMessage = sourceIndex++ === 0 && msg.role === "system";
836
+ if (msg.role === "system") {
837
+ if (!isLeadingSystemMessage)
838
+ appendSystemToolAdditions(msg, `system:${msgIndex}`);
839
+ if (!isLeadingSystemMessage || includeInitialSystemMessage) {
840
+ const text = isLeadingSystemMessage ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);
841
+ if (text.length > 0) {
842
+ messages.push({ role: instructionRole, content: sanitizeSurrogates(text) });
843
+ }
844
+ }
845
+ } else if (msg.role === "user") {
433
846
  if (typeof msg.content === "string") {
434
847
  messages.push({
435
848
  role: "user",
@@ -493,10 +906,10 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
493
906
  const [callId, itemIdRaw] = toolCall.id.split("|");
494
907
  const customInputProperty = options?.grammarToolInputProperties?.get(toolCall.name);
495
908
  let itemId = itemIdRaw;
496
- if (isDifferentModel && itemId?.startsWith("fc_") || customInputProperty === undefined && !itemId?.startsWith("fc_")) {
909
+ const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_";
910
+ if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) {
497
911
  itemId = undefined;
498
912
  }
499
- const canReplayNamespace = isSameModel || options?.deferredTools?.has(toolCall.name) === true;
500
913
  if (customInputProperty !== undefined) {
501
914
  output.push({
502
915
  type: "custom_tool_call",
@@ -504,7 +917,7 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
504
917
  call_id: callId,
505
918
  name: toolCall.name,
506
919
  input: sanitizeSurrogates(getGrammarToolInput(toolCall.name, toolCall.arguments, customInputProperty)),
507
- ...canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
920
+ ...isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
508
921
  });
509
922
  } else {
510
923
  output.push({
@@ -513,7 +926,7 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
513
926
  call_id: callId,
514
927
  name: toolCall.name,
515
928
  arguments: JSON.stringify(toolCall.arguments),
516
- ...canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
929
+ ...isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
517
930
  });
518
931
  }
519
932
  }
@@ -537,43 +950,9 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
537
950
  output
538
951
  });
539
952
  }
540
- const deferredTools = [];
541
- for (const name of msg.addedToolNames ?? []) {
542
- const tool = options?.deferredTools?.get(name);
543
- if (!tool || loadedToolNames.has(name))
544
- continue;
545
- loadedToolNames.add(name);
546
- deferredTools.push(tool);
547
- }
548
- if (deferredTools.length > 0 && options?.deferredToolsMode === "additional-tools") {
549
- messages.push({
550
- type: "additional_tools",
551
- role: "developer",
552
- tools: convertResponsesTools(deferredTools, options.toolOptions)
553
- });
554
- } else if (deferredTools.length > 0 && options?.deferredToolsMode === "tool-search") {
555
- const names = deferredTools.map((tool) => tool.name);
556
- const searchCallId = `pi_tool_load_${shortHash(`${msg.toolCallId}:${names.join(",")}`)}`;
557
- messages.push({
558
- type: "tool_search_call",
559
- call_id: searchCallId,
560
- execution: "client",
561
- status: "completed",
562
- arguments: { query: names.join(" "), limit: names.length }
563
- });
564
- messages.push({
565
- type: "tool_search_output",
566
- call_id: searchCallId,
567
- execution: "client",
568
- status: "completed",
569
- tools: convertResponsesTools(deferredTools, {
570
- ...options.toolOptions,
571
- deferLoading: true
572
- })
573
- });
574
- }
575
953
  }
576
- msgIndex++;
954
+ if (!isLeadingSystemMessage)
955
+ msgIndex++;
577
956
  }
578
957
  return messages;
579
958
  }
@@ -593,7 +972,7 @@ function convertResponsesTools(tools, options) {
593
972
  syntax: grammar.format,
594
973
  definition: grammar.definition
595
974
  },
596
- ...options?.deferLoading ? { defer_loading: true } : {}
975
+ ...options?.toolSearchResult ? { defer_loading: true } : {}
597
976
  };
598
977
  }
599
978
  const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode);
@@ -603,7 +982,7 @@ function convertResponsesTools(tools, options) {
603
982
  name: tool.name,
604
983
  description: tool.description,
605
984
  parameters: getJsonSchemaToolParameters(tool, strict === true),
606
- ...options?.deferLoading ? { defer_loading: true } : {}
985
+ ...options?.toolSearchResult ? { defer_loading: true } : {}
607
986
  };
608
987
  if (supportsStrictMode) {
609
988
  functionTool.strict = strict;
@@ -612,157 +991,6 @@ function convertResponsesTools(tools, options) {
612
991
  });
613
992
  }
614
993
 
615
- // index.ts
616
- import { calculateCost } from "@earendil-works/pi-ai";
617
- import {
618
- compact,
619
- convertToLlm,
620
- sessionEntryToContextMessages
621
- } from "@earendil-works/pi-coding-agent";
622
-
623
- // policy.ts
624
- import {
625
- CONFIG_DIR_NAME,
626
- getAgentDir,
627
- hasTrustRequiringProjectResources,
628
- ProjectTrustStore
629
- } from "@earendil-works/pi-coding-agent";
630
- import { closeSync, constants as fsConstants, fstatSync, openSync, readSync } from "node:fs";
631
- import { join, resolve, sep } from "node:path";
632
- var COMPACTION_MODEL_FLAG = "compaction-model";
633
- var PROJECT_POLICY = "compaction-policy.json";
634
- var AGENT_DIR_OVERRIDE = "PI_COMPACTOR_AGENT_DIR";
635
- var MAX_POLICY_BYTES = 64 * 1024;
636
- var MAX_MODEL_SELECTORS = 8;
637
- var MAX_SELECTOR_LENGTH = 512;
638
- function normalizeSelector(value) {
639
- if (typeof value !== "string")
640
- return;
641
- const selector = value.trim();
642
- if (selector.length === 0 || selector.length > MAX_SELECTOR_LENGTH)
643
- return;
644
- return selector;
645
- }
646
- function parsePolicySelectors(value) {
647
- if (typeof value !== "object" || value === null || Array.isArray(value))
648
- return;
649
- if (!("models" in value) || !Array.isArray(value.models))
650
- return;
651
- const selectors = [];
652
- for (const model of value.models) {
653
- const selector = normalizeSelector(model);
654
- if (selector === undefined || selectors.includes(selector))
655
- continue;
656
- selectors.push(selector);
657
- if (selectors.length === MAX_MODEL_SELECTORS)
658
- break;
659
- }
660
- return selectors;
661
- }
662
- function readPolicySelectors(configPath) {
663
- let fd;
664
- try {
665
- fd = openSync(configPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
666
- const stat = fstatSync(fd);
667
- if (!stat.isFile() || stat.size > MAX_POLICY_BYTES)
668
- return;
669
- const buffer = Buffer.allocUnsafe(MAX_POLICY_BYTES + 1);
670
- let bytesRead = 0;
671
- while (bytesRead < buffer.length) {
672
- const count = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null);
673
- if (count === 0)
674
- break;
675
- bytesRead += count;
676
- }
677
- if (bytesRead > MAX_POLICY_BYTES)
678
- return;
679
- return parsePolicySelectors(JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")));
680
- } catch {
681
- return;
682
- } finally {
683
- if (fd !== undefined) {
684
- try {
685
- closeSync(fd);
686
- } catch {}
687
- }
688
- }
689
- }
690
- function activeAgentDir(ctx) {
691
- const override = process.env[AGENT_DIR_OVERRIDE]?.trim();
692
- if (override)
693
- return resolve(override);
694
- try {
695
- const sessionDir = ctx.sessionManager?.getSessionDir();
696
- if (sessionDir) {
697
- const resolvedCwd = resolve(ctx.cwd);
698
- const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
699
- const suffix = `${sep}${join("sessions", safePath)}`;
700
- const resolvedSessionDir = resolve(sessionDir);
701
- if (resolvedSessionDir.endsWith(suffix)) {
702
- return resolvedSessionDir.slice(0, -suffix.length);
703
- }
704
- }
705
- } catch {}
706
- return getAgentDir();
707
- }
708
- function isProjectPolicyTrusted(ctx, agentDir) {
709
- try {
710
- if (!ctx.isProjectTrusted())
711
- return false;
712
- return hasTrustRequiringProjectResources(ctx.cwd) || new ProjectTrustStore(agentDir).get(ctx.cwd) === true;
713
- } catch {
714
- return false;
715
- }
716
- }
717
- function readCompactionModelFlagFromArgv(argv = process.argv.slice(2)) {
718
- let value;
719
- const prefix = `--${COMPACTION_MODEL_FLAG}=`;
720
- for (let i = 0;i < argv.length; i++) {
721
- const arg = argv[i];
722
- if (arg === "--")
723
- break;
724
- if (arg === `--${COMPACTION_MODEL_FLAG}`) {
725
- const next = argv[i + 1];
726
- if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
727
- value = next;
728
- i++;
729
- } else {
730
- value = true;
731
- }
732
- } else if (arg.startsWith(prefix)) {
733
- value = arg.slice(prefix.length);
734
- }
735
- }
736
- return value;
737
- }
738
- function resolveCompactionModelPolicy(pi, ctx, argv = process.argv.slice(2)) {
739
- const argvRaw = readCompactionModelFlagFromArgv(argv);
740
- if (typeof argvRaw === "string" && argvRaw.trim()) {
741
- const selector = normalizeSelector(argvRaw);
742
- return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
743
- }
744
- try {
745
- const rawFlag = pi.getFlag(COMPACTION_MODEL_FLAG);
746
- if (typeof rawFlag === "string" && rawFlag.trim()) {
747
- const selector = normalizeSelector(rawFlag);
748
- return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
749
- }
750
- } catch {}
751
- const agentDir = activeAgentDir(ctx);
752
- const candidates = [];
753
- if (isProjectPolicyTrusted(ctx, agentDir)) {
754
- candidates.push({ path: join(ctx.cwd, CONFIG_DIR_NAME, PROJECT_POLICY), source: "project-policy" });
755
- }
756
- candidates.push({ path: join(agentDir, PROJECT_POLICY), source: "agent-policy" });
757
- for (const candidate of candidates) {
758
- const selectors = readPolicySelectors(candidate.path);
759
- if (selectors !== undefined) {
760
- return selectors.length > 0 ? { hasSelectors: true, source: candidate.source } : { hasSelectors: false };
761
- }
762
- }
763
- return { hasSelectors: false };
764
- }
765
-
766
994
  // protocol.ts
767
995
  var RESPONSES_APIS = new Set([
768
996
  "openai-responses",
@@ -794,14 +1022,52 @@ function validateItemSize(item) {
794
1022
  if (byteLength(item) > MAX_ITEM_BYTES)
795
1023
  throw new Error("Native compaction output item exceeded 2 MiB");
796
1024
  }
1025
+ function validContent(value) {
1026
+ return typeof value === "string" || Array.isArray(value) && value.every((part) => {
1027
+ if (!isObject(part))
1028
+ return false;
1029
+ if (part.type === "input_text" || part.type === "output_text" || part.type === "summary_text")
1030
+ return typeof part.text === "string";
1031
+ if (part.type === "input_image")
1032
+ return typeof part.image_url === "string" || typeof part.file_id === "string";
1033
+ if (part.type === "refusal")
1034
+ return typeof part.refusal === "string";
1035
+ return false;
1036
+ });
1037
+ }
797
1038
  function validateCompactedOutput(value) {
798
1039
  if (!isObject(value) || !Array.isArray(value.output) || value.output.length === 0) {
799
1040
  throw new Error("Responses Compact returned an invalid output window");
800
1041
  }
1042
+ if (byteLength(value.output) > MAX_RESPONSE_BYTES)
1043
+ throw new Error("Native compaction output exceeded 8 MiB");
801
1044
  const output = value.output.map((item) => {
802
1045
  if (!isObject(item))
803
1046
  throw new Error("Responses Compact returned a non-object output item");
804
1047
  validateItemSize(item);
1048
+ const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
1049
+ if (!["compaction", "message", "reasoning", "function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "tool_search_call", "tool_search_output", "additional_tools"].includes(String(type)))
1050
+ throw new Error("Responses Compact returned an unsupported output item");
1051
+ if (type === "message" && (!["user", "assistant", "system", "developer"].includes(String(item.role)) || !validContent(item.content)))
1052
+ throw new Error("Responses Compact returned an invalid message item");
1053
+ if (type === "compaction" && !isCompactionItem(item))
1054
+ throw new Error("Responses Compact returned an invalid checkpoint");
1055
+ if (type === "reasoning" && !(typeof item.encrypted_content === "string" || Array.isArray(item.summary) && validContent(item.summary)))
1056
+ throw new Error("Responses Compact returned invalid reasoning");
1057
+ if (String(type).includes("call") || type === "tool_search_output") {
1058
+ if (typeof item.call_id !== "string" || !item.call_id)
1059
+ throw new Error("Responses Compact returned an invalid tool id");
1060
+ if (type === "function_call" && (typeof item.name !== "string" || typeof item.arguments !== "string"))
1061
+ throw new Error("Responses Compact returned an invalid function call");
1062
+ if (type === "custom_tool_call" && (typeof item.name !== "string" || typeof item.input !== "string"))
1063
+ throw new Error("Responses Compact returned an invalid custom call");
1064
+ if ((type === "function_call_output" || type === "custom_tool_call_output") && !validContent(item.output))
1065
+ throw new Error("Responses Compact returned an invalid tool output");
1066
+ if (type === "tool_search_call" && !isObject(item.arguments))
1067
+ throw new Error("Responses Compact returned an invalid tool search");
1068
+ }
1069
+ if ((type === "additional_tools" || type === "tool_search_output") && (!Array.isArray(item.tools) || !item.tools.every(isObject)))
1070
+ throw new Error("Responses Compact returned invalid tools");
805
1071
  return structuredClone(item);
806
1072
  });
807
1073
  const checkpoints = output.filter(isCompactionItem);
@@ -841,6 +1107,9 @@ async function readJsonBounded(response, signal) {
841
1107
  }
842
1108
  } finally {
843
1109
  signal.removeEventListener("abort", onAbort);
1110
+ reader.cancel().catch(() => {
1111
+ return;
1112
+ });
844
1113
  reader.releaseLock();
845
1114
  }
846
1115
  const bytes = new Uint8Array(total);
@@ -988,7 +1257,9 @@ async function requestResponsesCompact(request) {
988
1257
  onPayload: (payload) => {
989
1258
  if (!isObject(payload))
990
1259
  throw new Error("Responses provider exposed a non-object payload");
991
- preparedPayload = { ...structuredClone(payload), input: structuredClone(request.input) };
1260
+ if (request.preparedPayload && request.preparedPayload.model !== payload.model)
1261
+ throw new Error("Native route model changed since capture");
1262
+ preparedPayload = { ...structuredClone(request.preparedPayload ?? payload), input: structuredClone(request.input) };
992
1263
  return preparedPayload;
993
1264
  }
994
1265
  });
@@ -1081,6 +1352,9 @@ async function collectCompactionSse(stream, signal) {
1081
1352
  dispatch();
1082
1353
  } finally {
1083
1354
  signal.removeEventListener("abort", onAbort);
1355
+ reader.cancel().catch(() => {
1356
+ return;
1357
+ });
1084
1358
  reader.releaseLock();
1085
1359
  }
1086
1360
  if (!completed)
@@ -1118,13 +1392,15 @@ function buildRemoteV2History(input, item) {
1118
1392
  async function requestRemoteV2(request) {
1119
1393
  const baseFetch = request.fetch ?? globalThis.fetch;
1120
1394
  let sentInput;
1395
+ const controller = new AbortController;
1396
+ const signal = AbortSignal.any([request.signal, controller.signal]);
1121
1397
  const inspections = [];
1122
1398
  const inspectedFetch = async (input, init) => {
1123
1399
  const response = await baseFetch(input, init);
1124
1400
  if (!response.ok || !response.body)
1125
1401
  return response;
1126
1402
  const [providerBody, inspectionBody] = response.body.tee();
1127
- inspections.push(collectCompactionSse(inspectionBody, request.signal));
1403
+ inspections.push(collectCompactionSse(inspectionBody, signal).then((result) => ({ result }), (error) => ({ error })));
1128
1404
  return new Response(providerBody, {
1129
1405
  status: response.status,
1130
1406
  statusText: response.statusText,
@@ -1144,440 +1420,573 @@ async function requestRemoteV2(request) {
1144
1420
  if (!isObject(payload))
1145
1421
  throw new Error("Codex provider exposed a non-object payload");
1146
1422
  sentInput = structuredClone(request.input);
1147
- return { ...structuredClone(payload), input: [...sentInput, { type: "compaction_trigger" }] };
1423
+ if (request.preparedPayload && request.preparedPayload.model !== payload.model)
1424
+ throw new Error("Native route model changed since capture");
1425
+ return { ...structuredClone(request.preparedPayload ?? payload), stream: true, input: [...sentInput, { type: "compaction_trigger" }] };
1148
1426
  }
1149
1427
  });
1150
- const usage = await collectProviderUsage(stream, request.signal);
1151
- if (!sentInput || inspections.length !== 1) {
1152
- throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
1428
+ try {
1429
+ const usage = await collectProviderUsage(stream, request.signal);
1430
+ if (!sentInput || inspections.length !== 1)
1431
+ throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
1432
+ const inspection = await inspections[0];
1433
+ if ("error" in inspection)
1434
+ throw inspection.error;
1435
+ return { output: buildRemoteV2History(sentInput, inspection.result.item), usage };
1436
+ } finally {
1437
+ controller.abort();
1153
1438
  }
1154
- const inspection = await inspections[0];
1155
- return { output: buildRemoteV2History(sentInput, inspection.item), usage };
1156
1439
  }
1157
1440
  async function requestProviderCompaction(request) {
1158
- return request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request);
1441
+ const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
1442
+ return { ...result, output: validateCompactedOutput(result) };
1159
1443
  }
1160
1444
 
1161
- // index.ts
1162
- var NATIVE_COMPACTION_TYPE = "pi-provider-compaction/openai-responses";
1163
- var NATIVE_COMPACTION_VERSION = 2;
1164
- var NATIVE_COMPACTION_SUMMARY = "This context was compacted using the provider's native OpenAI Responses state.";
1445
+ // replay.ts
1165
1446
  var TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
1166
- function supportsNativeCompaction(model) {
1167
- return supportsResponsesApi(model);
1447
+ var AZURE_TOOL_CALL_PROVIDERS = new Set([...TOOL_CALL_PROVIDERS, "azure"]);
1448
+ var MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024;
1449
+ var SNAPSHOT_TTL_MS = 5 * 60000;
1450
+ function equal(left, right) {
1451
+ return JSON.stringify(left) === JSON.stringify(right);
1452
+ }
1453
+ function serializeInput(model, context, declarations = context) {
1454
+ const compat = model.compat;
1455
+ const flag = (name) => compat?.[name] === true;
1456
+ const codex = model.api === "openai-codex-responses";
1457
+ const normalized = resolveTranscript2(context, flag("supportsMidConvoSystemMessages"));
1458
+ const tools = resolveTranscript2(declarations, flag("supportsMidConvoSystemMessages"));
1459
+ return convertResponsesMessages(model, normalized, model.api === "azure-openai-responses" ? AZURE_TOOL_CALL_PROVIDERS : TOOL_CALL_PROVIDERS, {
1460
+ includeSystemPrompt: !codex,
1461
+ grammarToolInputProperties: createGrammarToolInputProperties(getDeclaredTools(tools.messages), flag("supportsOpenAIGrammarTools")),
1462
+ supportsMidConvoSystemMessages: flag("supportsMidConvoSystemMessages"),
1463
+ supportsAdditionalTools: flag("supportsAdditionalTools"),
1464
+ supportsToolSearch: flag("supportsToolSearch"),
1465
+ toolOptions: {
1466
+ ...codex ? { strict: null } : {},
1467
+ supportsStrictMode: compat?.supportsStrictMode ?? model.api !== "openai-responses",
1468
+ supportsOpenAIGrammarTools: flag("supportsOpenAIGrammarTools")
1469
+ }
1470
+ });
1168
1471
  }
1169
- function readUsage(value) {
1170
- if (!isObject(value) || !isObject(value.cost))
1472
+ function capturePreparedRequest(model, context, canonical, payload, reject = () => {}) {
1473
+ if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject)) {
1474
+ reject("unsupported payload shape");
1171
1475
  return;
1172
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
1173
- if (typeof value[key] !== "number" || !Number.isFinite(value[key]))
1174
- return;
1175
1476
  }
1176
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
1177
- if (typeof value.cost[key] !== "number" || !Number.isFinite(value.cost[key]))
1178
- return;
1477
+ if (payload.previous_response_id || payload.conversation) {
1478
+ reject("stateful request");
1479
+ return;
1179
1480
  }
1180
- return value;
1181
- }
1182
- function readIdentity(value) {
1183
- if (!isObject(value) || typeof value.endpoint !== "string" || !value.endpoint)
1481
+ if (Buffer.byteLength(JSON.stringify({ context, payload })) > MAX_SNAPSHOT_BYTES) {
1482
+ reject("snapshot exceeds size limit");
1184
1483
  return;
1185
- for (const key of ["organization", "project", "accountId", "azureApiVersion", "azureDeployment"]) {
1186
- if (value[key] !== undefined && typeof value[key] !== "string")
1187
- return;
1188
1484
  }
1189
- return {
1190
- endpoint: value.endpoint,
1191
- organization: value.organization,
1192
- project: value.project,
1193
- accountId: value.accountId,
1194
- azureApiVersion: value.azureApiVersion,
1195
- azureDeployment: value.azureDeployment
1196
- };
1485
+ const expected = serializeInput(model, context);
1486
+ if (!equal(expected, payload.input)) {
1487
+ reject("prepared input differs from serializer");
1488
+ return;
1489
+ }
1490
+ return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload), portableInput: expected, createdAt: Date.now() };
1491
+ }
1492
+ function promptCount(input) {
1493
+ let count = 0;
1494
+ while (input[count]?.role === "system" || input[count]?.role === "developer")
1495
+ count++;
1496
+ return count;
1497
+ }
1498
+ function closedTools(context) {
1499
+ const pending = new Set;
1500
+ for (const message of context.messages) {
1501
+ if (message.role === "assistant") {
1502
+ if (message.stopReason === "error" || message.stopReason === "aborted")
1503
+ return false;
1504
+ for (const block of message.content)
1505
+ if (block.type === "toolCall") {
1506
+ if (pending.has(block.id))
1507
+ return false;
1508
+ pending.add(block.id);
1509
+ }
1510
+ } else if (message.role === "toolResult") {
1511
+ if (!pending.delete(message.toolCallId))
1512
+ return false;
1513
+ }
1514
+ }
1515
+ return pending.size === 0;
1197
1516
  }
1198
- function readNativeCompactionDetails(value) {
1199
- if (!isObject(value) || value.type !== NATIVE_COMPACTION_TYPE)
1517
+ function mappedPrefix(snapshot, prefix) {
1518
+ if (!equal(snapshot.canonical.messages.slice(0, prefix.messages.length), prefix.messages))
1200
1519
  return;
1201
- if (value.version !== 1 && value.version !== NATIVE_COMPACTION_VERSION)
1520
+ const head = prefix.messages[0]?.role === "system" ? 1 : 0;
1521
+ if (head && snapshot.context.messages[0]?.role !== "system")
1202
1522
  return;
1203
- if (typeof value.provider !== "string" || typeof value.api !== "string" || typeof value.model !== "string")
1523
+ if (!equal(snapshot.context.messages.slice(head, prefix.messages.length), prefix.messages.slice(head)))
1204
1524
  return;
1205
- if (!Array.isArray(value.output) || !value.output.every(isObject) || !value.output.some(isCompactionItem))
1525
+ return normalizeContext({ messages: snapshot.context.messages.slice(0, prefix.messages.length) });
1526
+ }
1527
+ function selectCoveredPrefix(model, snapshot, prefix, reject = () => {}) {
1528
+ if (Date.now() - snapshot.createdAt > SNAPSHOT_TTL_MS) {
1529
+ reject("snapshot expired");
1206
1530
  return;
1207
- const protocol = value.version === 1 ? "responses-compact" : value.protocol === "responses-compact" || value.protocol === "remote-v2" ? value.protocol : undefined;
1208
- if (!protocol)
1531
+ }
1532
+ if (!closedTools(prefix)) {
1533
+ reject("prefix has an incomplete tool exchange or failed assistant");
1209
1534
  return;
1210
- const strings = (candidate) => candidate === undefined ? undefined : Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? [...candidate] : undefined;
1211
- return {
1212
- type: NATIVE_COMPACTION_TYPE,
1213
- version: value.version,
1214
- provider: value.provider,
1215
- api: value.api,
1216
- model: value.model,
1217
- protocol,
1218
- identity: value.version === 1 ? undefined : readIdentity(value.identity),
1219
- output: structuredClone(value.output),
1220
- nativeUsage: readUsage(value.nativeUsage ?? value.usage),
1221
- usage: readUsage(value.usage),
1222
- portableUsage: readUsage(value.portableUsage),
1223
- readFiles: strings(value.readFiles),
1224
- modifiedFiles: strings(value.modifiedFiles)
1225
- };
1226
- }
1227
- function identityEqual(left, right) {
1228
- if (!left)
1229
- return true;
1230
- if (!right)
1231
- return false;
1232
- return left.endpoint === right.endpoint && left.organization === right.organization && left.project === right.project && left.accountId === right.accountId && left.azureApiVersion === right.azureApiVersion && left.azureDeployment === right.azureDeployment;
1233
- }
1234
- function findLatestNativeCompaction(entries, model, identity) {
1235
- if (!supportsNativeCompaction(model))
1535
+ }
1536
+ const mapped = mappedPrefix(snapshot, prefix);
1537
+ if (!mapped) {
1538
+ reject("projected prefix differs from request context");
1236
1539
  return;
1237
- for (let index = entries.length - 1;index >= 0; index--) {
1238
- const entry = entries[index];
1239
- if (entry.type !== "compaction")
1240
- continue;
1241
- const details = readNativeCompactionDetails(entry.details);
1242
- if (!details)
1243
- return;
1244
- if (details.provider !== model.provider || details.api !== model.api || details.model !== model.id)
1245
- return;
1246
- if (!identityEqual(details.identity, identity))
1247
- return;
1248
- return { ...details, entryId: entry.id };
1249
1540
  }
1250
- return;
1251
- }
1252
- function responseItemsFromMessages(model, messages) {
1253
- const input = convertResponsesMessages(model, { messages: convertToLlm(messages), tools: [] }, TOOL_CALL_PROVIDERS, { includeSystemPrompt: false });
1254
- return input.filter(isObject);
1255
- }
1256
- function responseItemsFromEntries(model, entries) {
1257
- return responseItemsFromMessages(model, entries.flatMap((entry) => sessionEntryToContextMessages(entry)));
1258
- }
1259
- function nativeRequestInput(model, branchEntries, activeEntries, previous) {
1260
- if (previous) {
1261
- const previousIndex = branchEntries.findIndex((entry) => entry.id === previous.entryId);
1262
- if (previousIndex >= 0) {
1263
- return [...structuredClone(previous.output), ...responseItemsFromEntries(model, branchEntries.slice(previousIndex + 1))];
1541
+ const expected = serializeInput(model, mapped, snapshot.context);
1542
+ if (!equal(snapshot.portableInput.slice(0, expected.length), expected)) {
1543
+ reject("serialized prefix does not match prepared input");
1544
+ return;
1545
+ }
1546
+ const start = promptCount(expected);
1547
+ let end = expected.length;
1548
+ if (snapshot.replacement) {
1549
+ if (snapshot.replacement.index < start || snapshot.replacement.index >= end) {
1550
+ reject("previous native slot is outside covered prefix");
1551
+ return;
1264
1552
  }
1553
+ end += snapshot.replacement.count - 1;
1265
1554
  }
1266
- return responseItemsFromEntries(model, activeEntries);
1267
- }
1268
- function authHeaders(apiKey, headers) {
1269
- const result = { "content-type": "application/json" };
1270
- let hasAuthorization = false;
1271
- for (const [name, value] of Object.entries(headers ?? {})) {
1272
- if (value === null)
1273
- continue;
1274
- result[name] = value;
1275
- if (name.toLowerCase() === "authorization" && value.trim())
1276
- hasAuthorization = true;
1555
+ if (end <= start) {
1556
+ reject("empty covered prefix");
1557
+ return;
1277
1558
  }
1278
- if (!hasAuthorization && apiKey)
1279
- result.authorization = `Bearer ${apiKey}`;
1280
- return result;
1559
+ return structuredClone(snapshot.payload.input.slice(start, end));
1281
1560
  }
1282
- async function compactOpenAIResponses(options) {
1283
- const endpoint = `${options.baseUrl.replace(/\/+$/, "")}/responses/compact`;
1284
- const body = { model: options.model.id, input: options.input };
1285
- if (options.instructions)
1286
- body.instructions = options.instructions;
1287
- const response = await (options.fetchImpl ?? fetch)(endpoint, {
1288
- method: "POST",
1289
- headers: authHeaders(options.apiKey, options.headers),
1290
- body: JSON.stringify(body),
1291
- signal: options.signal
1292
- });
1293
- if (!response.ok) {
1294
- const detail = await response.text().catch(() => "");
1295
- throw new Error(`OpenAI Responses compact request failed (${response.status}${detail ? `: ${detail.slice(0, 400)}` : ""})`);
1296
- }
1297
- const result = await response.json();
1298
- if (!isObject(result) || !Array.isArray(result.output) || !result.output.every(isObject) || !result.output.some(isCompactionItem)) {
1299
- throw new Error("OpenAI Responses compact response did not contain opaque compaction state");
1300
- }
1301
- const usage = compactUsage(options.model, result);
1302
- return {
1303
- type: NATIVE_COMPACTION_TYPE,
1304
- version: NATIVE_COMPACTION_VERSION,
1305
- provider: options.model.provider,
1306
- api: options.model.api,
1307
- model: options.model.id,
1308
- protocol: "responses-compact",
1309
- output: structuredClone(result.output),
1310
- nativeUsage: usage,
1311
- usage
1312
- };
1561
+ function substituteSummary(payload, index, summary, output) {
1562
+ if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject))
1563
+ return;
1564
+ if (!equal(payload.input[index], summary))
1565
+ return;
1566
+ if (payload.input.filter((item) => equal(item, summary)).length !== 1)
1567
+ return;
1568
+ return { ...payload, input: [...payload.input.slice(0, index), ...structuredClone(output), ...payload.input.slice(index + 1)] };
1313
1569
  }
1314
- function compactUsage(model, result) {
1315
- const usage = result.usage;
1316
- if (!isObject(usage))
1570
+ function summarySlot(model, snapshot, summaryPrefix) {
1571
+ const mapped = mappedPrefix(snapshot, summaryPrefix);
1572
+ if (!mapped)
1317
1573
  return;
1318
- const cached = nestedNumber(usage.input_tokens_details, "cached_tokens");
1319
- const cacheWrite = nestedNumber(usage.input_tokens_details, "cache_write_tokens");
1320
- const mapped = {
1321
- input: Math.max(0, readNumber(usage.input_tokens) - cached - cacheWrite),
1322
- output: readNumber(usage.output_tokens),
1323
- cacheRead: cached,
1324
- cacheWrite,
1325
- reasoning: nestedNumber(usage.output_tokens_details, "reasoning_tokens"),
1326
- totalTokens: readNumber(usage.total_tokens),
1327
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
1328
- };
1329
- try {
1330
- calculateCost(model, mapped);
1331
- } catch {}
1332
- return mapped;
1333
- }
1334
- function readNumber(value) {
1335
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
1336
- }
1337
- function nestedNumber(value, key) {
1338
- return isObject(value) ? readNumber(value[key]) : 0;
1339
- }
1340
- function stableStringify(value) {
1341
- if (Array.isArray(value))
1342
- return `[${value.map(stableStringify).join(",")}]`;
1343
- if (isObject(value)) {
1344
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
1345
- }
1346
- return JSON.stringify(value) ?? "null";
1347
- }
1348
- function itemMatches(left, right) {
1349
- const leftId = typeof left.id === "string" ? left.id : undefined;
1350
- const rightId = typeof right.id === "string" ? right.id : undefined;
1351
- if (leftId && rightId)
1352
- return leftId === rightId;
1353
- const leftCallId = typeof left.call_id === "string" ? left.call_id : undefined;
1354
- const rightCallId = typeof right.call_id === "string" ? right.call_id : undefined;
1355
- if (leftCallId && rightCallId)
1356
- return leftCallId === rightCallId && left.type === right.type;
1357
- if (left.type === "message" && right.type === "message") {
1358
- return left.role === right.role && stableStringify(left.content) === stableStringify(right.content);
1359
- }
1360
- return stableStringify(left) === stableStringify(right);
1361
- }
1362
- function findLastSubsequence(items, sequence) {
1363
- if (sequence.length === 0)
1364
- return -1;
1365
- for (let start = items.length - sequence.length;start >= 0; start--) {
1366
- if (sequence.every((item, offset) => itemMatches(items[start + offset], item)))
1367
- return start;
1368
- }
1369
- return -1;
1370
- }
1371
- function rewriteResponsesPayload(payload, details, postCompactionItems) {
1372
- if (!isObject(payload) || !Array.isArray(payload.input))
1373
- return payload;
1374
- const currentInput = payload.input.filter(isObject);
1375
- if (currentInput.length !== payload.input.length)
1376
- return payload;
1377
- const postStart = findLastSubsequence(currentInput, postCompactionItems);
1378
- if (postStart >= 0)
1379
- return { ...payload, input: [...structuredClone(details.output), ...currentInput.slice(postStart)] };
1380
- const retainedOutput = details.output.filter((item) => item.type !== "compaction");
1381
- const retainedStart = findLastSubsequence(currentInput, retainedOutput);
1382
- if (retainedStart >= 0) {
1383
- return { ...payload, input: [...structuredClone(details.output), ...currentInput.slice(retainedStart + retainedOutput.length)] };
1384
- }
1385
- return { ...payload, input: [...structuredClone(details.output), ...structuredClone(postCompactionItems)] };
1386
- }
1387
- function genericCompactionSelected(pi, ctx) {
1388
- return resolveCompactionModelPolicy(pi, ctx).hasSelectors;
1389
- }
1390
- function normalizedEndpoint(value) {
1391
- if (!value)
1392
- return "";
1393
- try {
1394
- const url = new URL(value);
1395
- url.username = "";
1396
- url.password = "";
1397
- url.search = "";
1398
- url.hash = "";
1399
- return url.toString().replace(/\/+$/, "");
1400
- } catch {
1401
- return value.replace(/\/+$/, "");
1402
- }
1574
+ const prefix = serializeInput(model, mapped, snapshot.context);
1575
+ const index = promptCount(prefix);
1576
+ if (prefix.length !== index + 1 || !equal(snapshot.portableInput.slice(0, prefix.length), prefix))
1577
+ return;
1578
+ return index;
1403
1579
  }
1404
- function headerValue(headers, name) {
1405
- const match = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name.toLowerCase());
1406
- return typeof match?.[1] === "string" && match[1] ? match[1] : undefined;
1580
+ function requestPolicy(model, snapshot) {
1581
+ return {
1582
+ model,
1583
+ system: getCurrentSystemPrompt(snapshot.context.messages),
1584
+ currentTools: getCurrentTools2(snapshot.context.messages),
1585
+ declarations: getDeclaredTools(resolveTranscript2(snapshot.context, model.compat?.supportsMidConvoSystemMessages).messages),
1586
+ instructions: snapshot.payload.instructions,
1587
+ tools: snapshot.payload.tools,
1588
+ prompt: snapshot.portableInput.slice(0, promptCount(snapshot.portableInput))
1589
+ };
1407
1590
  }
1408
- function codexAccountId(apiKey) {
1409
- if (!apiKey)
1591
+
1592
+ // index.ts
1593
+ var NATIVE_COMPACTION_TYPE = "pi-provider-compaction/openai-responses";
1594
+ var NATIVE_COMPACTION_VERSION = 3;
1595
+ var NATIVE_COMPACTION_SUMMARY = "This context was compacted using the provider's native OpenAI Responses state.";
1596
+ var supportsNativeCompaction = supportsResponsesApi;
1597
+ var digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
1598
+ var transcript = (messages) => normalizeContext2({ messages: convertToLlm(messages) });
1599
+ function readNativeCompactionDetails(value) {
1600
+ if (!isObject(value) || value.type !== NATIVE_COMPACTION_TYPE || value.version !== 1 && value.version !== 2 && value.version !== 3)
1601
+ return;
1602
+ if (typeof value.provider !== "string" || typeof value.api !== "string" || typeof value.model !== "string")
1410
1603
  return;
1411
- const parts = apiKey.split(".");
1412
- if (parts.length !== 3)
1604
+ const protocol = value.version === 1 ? "responses-compact" : value.protocol;
1605
+ if (protocol !== "responses-compact" && protocol !== "remote-v2")
1413
1606
  return;
1414
1607
  try {
1415
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1416
- if (!isObject(payload))
1417
- return;
1418
- const auth = payload["https://api.openai.com/auth"];
1419
- return isObject(auth) && typeof auth.chatgpt_account_id === "string" ? auth.chatgpt_account_id : undefined;
1608
+ const output = validateCompactedOutput(value);
1609
+ if (value.version === 3) {
1610
+ const coverage = value.coverage;
1611
+ if (!isObject(coverage) || coverage.kind !== "summarized-prefix")
1612
+ return;
1613
+ for (const field of ["firstKeptEntryId", "sourceLeafId", "sourceHash", "policyHash", "inputHash", "runtimeId"]) {
1614
+ if (typeof coverage[field] !== "string" || !coverage[field])
1615
+ return;
1616
+ }
1617
+ if (!isObject(value.identity) || typeof value.identity.endpoint !== "string")
1618
+ return;
1619
+ }
1620
+ return { ...structuredClone(value), protocol, output };
1420
1621
  } catch {
1421
1622
  return;
1422
1623
  }
1423
1624
  }
1424
- function azureDeployment(modelId, env) {
1425
- const raw = env?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
1426
- if (!raw)
1427
- return modelId;
1428
- for (const entry of raw.split(",")) {
1429
- const [candidate, deployment] = entry.split("=", 2).map((part) => part?.trim());
1430
- if (candidate === modelId && deployment)
1431
- return deployment;
1625
+ function latestCompaction(branch) {
1626
+ for (let index = branch.length - 1;index >= 0; index--) {
1627
+ const entry = branch[index];
1628
+ if (entry.type === "compaction")
1629
+ return entry;
1432
1630
  }
1433
- return modelId;
1434
- }
1435
- function routeIdentity(model, auth) {
1436
- const azure = model.api === "azure-openai-responses";
1437
- return {
1438
- endpoint: normalizedEndpoint(auth.baseUrl ?? auth.env?.AZURE_OPENAI_BASE_URL ?? model.baseUrl),
1439
- organization: headerValue(auth.headers, "openai-organization"),
1440
- project: headerValue(auth.headers, "openai-project"),
1441
- accountId: model.api === "openai-codex-responses" ? codexAccountId(auth.apiKey) : undefined,
1442
- azureApiVersion: azure ? auth.env?.AZURE_OPENAI_API_VERSION || "v1" : undefined,
1443
- azureDeployment: azure ? azureDeployment(model.id, auth.env) : undefined
1444
- };
1631
+ return;
1445
1632
  }
1446
- function activeTools(pi) {
1447
- const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
1448
- return pi.getActiveTools().flatMap((name) => {
1449
- const tool = available.get(name);
1450
- return tool ? [{ name: tool.name, description: tool.description, parameters: tool.parameters }] : [];
1451
- });
1633
+ function header(headers, name) {
1634
+ const value = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name)?.[1];
1635
+ return typeof value === "string" && value ? value : undefined;
1452
1636
  }
1453
- function providerContext(pi, ctx, activeEntries) {
1454
- const messages = activeEntries.flatMap((entry) => sessionEntryToContextMessages(entry));
1637
+ function identity(model, options) {
1638
+ const azure = model.api === "azure-openai-responses";
1639
+ const azureBase = options.env?.AZURE_OPENAI_BASE_URL || (options.env?.AZURE_OPENAI_RESOURCE_NAME ? `https://${options.env.AZURE_OPENAI_RESOURCE_NAME}.openai.azure.com/openai/v1` : undefined);
1640
+ const url = new URL(azure ? azureBase ?? model.baseUrl : model.baseUrl);
1641
+ url.username = "";
1642
+ url.password = "";
1643
+ url.search = "";
1644
+ url.hash = "";
1645
+ let accountId = header(options.headers, "chatgpt-account-id");
1646
+ if (!accountId && model.api === "openai-codex-responses" && options.apiKey) {
1647
+ try {
1648
+ const token = JSON.parse(Buffer.from(options.apiKey.split(".")[1], "base64url").toString("utf8"));
1649
+ const id = token["https://api.openai.com/auth"]?.chatgpt_account_id;
1650
+ if (typeof id === "string")
1651
+ accountId = id;
1652
+ } catch {}
1653
+ }
1654
+ const map = options.env?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP?.split(",").map((entry) => entry.split("=").map((part) => part.trim()));
1455
1655
  return {
1456
- systemPrompt: ctx.getSystemPrompt(),
1457
- messages: convertToLlm(messages),
1458
- tools: activeTools(pi)
1656
+ endpoint: url.toString().replace(/\/+$/, ""),
1657
+ organization: header(options.headers, "openai-organization"),
1658
+ project: header(options.headers, "openai-project"),
1659
+ accountId,
1660
+ azureApiVersion: azure ? options.env?.AZURE_OPENAI_API_VERSION ?? "v1" : undefined,
1661
+ azureDeployment: azure ? map?.find(([id]) => id === model.id)?.[1] ?? model.id : undefined
1459
1662
  };
1460
1663
  }
1461
- function cleanHeaders(headers) {
1462
- if (!headers)
1463
- return;
1464
- return Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null));
1465
- }
1466
1664
  function combineUsage(first, second) {
1467
1665
  if (!first)
1468
1666
  return second;
1469
1667
  if (!second)
1470
1668
  return first;
1471
- return {
1472
- input: first.input + second.input,
1473
- output: first.output + second.output,
1474
- cacheRead: first.cacheRead + second.cacheRead,
1475
- cacheWrite: first.cacheWrite + second.cacheWrite,
1476
- cacheWrite1h: first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined ? (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) : undefined,
1477
- reasoning: first.reasoning !== undefined || second.reasoning !== undefined ? (first.reasoning ?? 0) + (second.reasoning ?? 0) : undefined,
1478
- totalTokens: first.totalTokens + second.totalTokens,
1479
- cost: {
1480
- input: first.cost.input + second.cost.input,
1481
- output: first.cost.output + second.cost.output,
1482
- cacheRead: first.cost.cacheRead + second.cost.cacheRead,
1483
- cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
1484
- total: first.cost.total + second.cost.total
1485
- }
1486
- };
1669
+ const result = structuredClone(first);
1670
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"])
1671
+ result[key] += second[key];
1672
+ for (const key of ["reasoning", "cacheWrite1h"])
1673
+ if (first[key] !== undefined || second[key] !== undefined)
1674
+ result[key] = (first[key] ?? 0) + (second[key] ?? 0);
1675
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"])
1676
+ result.cost[key] += second.cost[key];
1677
+ return result;
1487
1678
  }
1488
- async function nativeAndPortableCompaction(pi, event, ctx) {
1489
- const model = ctx.model;
1490
- const protocol = resolveNativeProtocol(model);
1491
- if (!model || !protocol || genericCompactionSelected(pi, ctx))
1492
- return;
1493
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
1494
- if (!auth.ok)
1495
- return;
1496
- const provider = ctx.modelRegistry.getProvider(model.provider);
1497
- if (!provider)
1498
- return;
1499
- const identity = routeIdentity(model, auth);
1500
- const branchEntries = event.branchEntries;
1501
- const activeEntries = ctx.sessionManager.buildContextEntries();
1502
- const previous = findLatestNativeCompaction(branchEntries, model, identity);
1503
- const input = nativeRequestInput(model, branchEntries, activeEntries, previous);
1504
- const native = await requestProviderCompaction({
1505
- provider,
1506
- model,
1507
- context: providerContext(pi, ctx, activeEntries),
1508
- input,
1509
- protocol,
1510
- apiKey: auth.apiKey,
1511
- headers: auth.headers,
1512
- env: auth.env,
1513
- signal: event.signal
1514
- });
1515
- const portable = await compact(event.preparation, model, auth.apiKey, cleanHeaders(auth.headers), event.customInstructions, event.signal, ctx.thinkingLevel, provider.streamSimple.bind(provider), auth.env, undefined, undefined, ctx.sessionManager.getSessionId());
1516
- const portableDetails = isObject(portable.details) ? portable.details : {};
1517
- const details = {
1518
- type: NATIVE_COMPACTION_TYPE,
1519
- version: NATIVE_COMPACTION_VERSION,
1520
- provider: model.provider,
1521
- api: model.api,
1522
- model: model.id,
1523
- protocol,
1524
- identity,
1525
- output: native.output,
1526
- nativeUsage: native.usage,
1527
- portableUsage: portable.usage,
1528
- readFiles: Array.isArray(portableDetails.readFiles) ? portableDetails.readFiles.filter((item) => typeof item === "string") : undefined,
1529
- modifiedFiles: Array.isArray(portableDetails.modifiedFiles) ? portableDetails.modifiedFiles.filter((item) => typeof item === "string") : undefined
1679
+ function supportedRouteOptions(model, options) {
1680
+ if (model.api !== "azure-openai-responses")
1681
+ return true;
1682
+ const extra = options;
1683
+ if (["azureBaseUrl", "azureResourceName", "azureApiVersion", "azureDeploymentName"].some((key) => extra[key] !== undefined))
1684
+ return false;
1685
+ return !["AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_RESOURCE_NAME", "AZURE_OPENAI_API_VERSION", "AZURE_OPENAI_DEPLOYMENT_NAME_MAP"].some((key) => process.env[key] !== undefined && options.env?.[key] === undefined);
1686
+ }
1687
+ function pi_provider_compaction_default(pi) {
1688
+ const secret = randomBytes(32);
1689
+ const routeDigest = (value) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
1690
+ let runtimeId = randomUUID();
1691
+ let ctx;
1692
+ let snapshot;
1693
+ let active;
1694
+ let pending;
1695
+ let uninstall;
1696
+ let installedProvider;
1697
+ let generation = 0;
1698
+ let captureStatus = "no request observed";
1699
+ let compactionStatus = "not attempted in this runtime";
1700
+ const captureRejected = (reason) => {
1701
+ captureStatus = reason;
1530
1702
  };
1531
- return {
1532
- compaction: {
1533
- ...portable,
1534
- usage: combineUsage(native.usage, portable.usage),
1535
- details
1703
+ const fallback = (reason) => {
1704
+ compactionStatus = `portable: ${reason}`;
1705
+ };
1706
+ pi.registerCommand("provider-compaction-status", {
1707
+ description: "Show content-free native compaction eligibility and last attempt",
1708
+ handler: async (_args, context) => {
1709
+ context.ui.notify(`Provider compaction
1710
+ Observer: ${installedProvider ? "installed" : "unavailable for current registration"}
1711
+ Capture: ${captureStatus}
1712
+ Last attempt: ${compactionStatus}`, "info");
1536
1713
  }
1714
+ });
1715
+ const invalidate = () => {
1716
+ generation++;
1717
+ runtimeId = randomUUID();
1718
+ snapshot = undefined;
1719
+ active = undefined;
1720
+ pending = undefined;
1721
+ captureStatus = "invalidated by lifecycle/history/policy change";
1537
1722
  };
1538
- }
1539
- function pi_provider_compaction_default(pi) {
1540
- pi.on("before_provider_request", async (event, ctx) => {
1541
- const model = ctx.model;
1542
- if (!supportsNativeCompaction(model))
1723
+ const generic = (context) => resolveCompactionModelPolicy(pi, context).hasSelectors;
1724
+ const route = (model, options) => routeDigest({ model, apiKey: options.apiKey, headers: options.headers, env: options.env });
1725
+ const unchanged = (branch, length, hash) => branch.length >= length && digest(branch.slice(0, length)) === hash && !branch.slice(length).some((entry) => entry.type === "context_edit");
1726
+ const observe = (model, context, options) => {
1727
+ const owner = ctx;
1728
+ if (!owner || options.sessionId !== owner.sessionManager.getSessionId())
1543
1729
  return;
1544
- const branch = ctx.sessionManager.getBranch();
1545
- const candidate = findLatestNativeCompaction(branch, model);
1546
- if (!candidate || genericCompactionSelected(pi, ctx))
1730
+ if (!supportsNativeCompaction(model)) {
1731
+ captureRejected("unsupported API");
1547
1732
  return;
1548
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
1549
- if (!auth.ok)
1733
+ }
1734
+ if (!options.onPayload) {
1735
+ captureRejected("no payload callback");
1550
1736
  return;
1551
- const latest = findLatestNativeCompaction(branch, model, routeIdentity(model, auth));
1552
- if (!latest)
1737
+ }
1738
+ if (generic(owner)) {
1739
+ captureRejected("generic compaction model takes precedence");
1553
1740
  return;
1554
- const index = branch.findIndex((entry) => entry.id === latest.entryId);
1555
- const postItems = index >= 0 ? responseItemsFromEntries(model, branch.slice(index + 1)) : [];
1556
- const rewritten = rewriteResponsesPayload(event.payload, latest, postItems);
1557
- if (rewritten === event.payload || !isObject(rewritten))
1558
- return rewritten;
1559
- const systemPrompt = ctx.getSystemPrompt();
1560
- return systemPrompt ? { ...rewritten, instructions: systemPrompt } : rewritten;
1741
+ }
1742
+ if (!supportedRouteOptions(model, options)) {
1743
+ captureRejected("unsupported route overrides");
1744
+ return;
1745
+ }
1746
+ const ticket = generation;
1747
+ const branch = owner.sessionManager.getBranch();
1748
+ const branchHash = digest(branch);
1749
+ const canonical = transcript(owner.sessionManager.buildSessionProjection().messages);
1750
+ const current = () => generation === ticket && ctx === owner && !options.signal?.aborted && supportedRouteOptions(model, options) && digest(owner.sessionManager.getBranch()) === branchHash;
1751
+ return async (payload) => {
1752
+ if (!current())
1753
+ return payload;
1754
+ snapshot = undefined;
1755
+ const prepared = capturePreparedRequest(model, context, canonical, payload, captureRejected);
1756
+ if (!prepared) {
1757
+ active = undefined;
1758
+ return payload;
1759
+ }
1760
+ const auth = await owner.modelRegistry.getApiKeyAndHeaders(model);
1761
+ if (!current())
1762
+ return payload;
1763
+ const normalizedHeaders = (headers) => Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]).sort(([a], [b]) => String(a).localeCompare(String(b)));
1764
+ if (!auth.ok || generic(owner) || auth.apiKey !== options.apiKey || (auth.baseUrl ?? model.baseUrl) !== model.baseUrl || !equal(normalizedHeaders(auth.headers), normalizedHeaders(options.headers)) || !equal(auth.env ?? {}, options.env ?? {})) {
1765
+ active = undefined;
1766
+ captureRejected("resolved auth/route differs from prepared request, or generic policy changed");
1767
+ return payload;
1768
+ }
1769
+ const provider = owner.modelRegistry.getProvider(model.provider);
1770
+ if (!provider) {
1771
+ captureRejected("provider unavailable");
1772
+ return payload;
1773
+ }
1774
+ const routeHash = route(model, options);
1775
+ const policyHash = digest(requestPolicy(model, prepared));
1776
+ const latest = latestCompaction(branch);
1777
+ let result = payload;
1778
+ if (active && latest?.id === active.entryId && latest.summary === active.summary && digest(latest.details) === active.detailsHash && unchanged(branch, active.branchLength, active.branchHash) && active.routeHash === routeHash && active.policyHash === policyHash) {
1779
+ const details = readNativeCompactionDetails(latest.details);
1780
+ const projection = owner.sessionManager.buildSessionProjection();
1781
+ const contribution = projection.entries.find((entry) => entry.sourceEntry.id === latest.id);
1782
+ const prefix = contribution ? transcript(contribution.messages) : undefined;
1783
+ const slot = prefix ? summarySlot(model, prepared, prefix) : undefined;
1784
+ if (details?.version === 3 && details.coverage?.runtimeId === runtimeId && details.coverage.firstKeptEntryId === latest.firstKeptEntryId && slot !== undefined) {
1785
+ const replaced = substituteSummary(payload, slot, prepared.portableInput[slot], details.output);
1786
+ if (replaced) {
1787
+ result = replaced;
1788
+ prepared.payload = structuredClone(replaced);
1789
+ prepared.replacement = { index: slot, count: details.output.length };
1790
+ } else
1791
+ active = undefined;
1792
+ } else
1793
+ active = undefined;
1794
+ } else
1795
+ active = undefined;
1796
+ captureStatus = "eligible prepared request captured";
1797
+ snapshot = {
1798
+ prepared,
1799
+ model: structuredClone(model),
1800
+ provider,
1801
+ options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch },
1802
+ branchLength: branch.length,
1803
+ branchHash,
1804
+ routeHash,
1805
+ authHash: routeDigest(auth),
1806
+ policyHash,
1807
+ sessionId: owner.sessionManager.getSessionId()
1808
+ };
1809
+ return result;
1810
+ };
1811
+ };
1812
+ const install = (context) => {
1813
+ ctx = context;
1814
+ if (installedProvider === context.model?.provider)
1815
+ return;
1816
+ uninstall?.();
1817
+ uninstall = undefined;
1818
+ installedProvider = undefined;
1819
+ if (!supportsNativeCompaction(context.model))
1820
+ return;
1821
+ uninstall = installObserver(pi, context, observe);
1822
+ if (uninstall)
1823
+ installedProvider = context.model.provider;
1824
+ captureStatus = uninstall ? "awaiting prepared request" : "unsupported provider registration";
1825
+ };
1826
+ pi.on("session_start", (_event, context) => {
1827
+ invalidate();
1828
+ install(context);
1829
+ const latest = latestCompaction(context.sessionManager.getBranch());
1830
+ if (latest?.summary === NATIVE_COMPACTION_SUMMARY)
1831
+ context.ui.notify("Legacy native compaction has no portable summary. Native replay is disabled; recover from earlier history or start a new session.", "warning");
1561
1832
  });
1562
- pi.on("session_before_compact", async (event, ctx) => {
1563
- try {
1564
- return await nativeAndPortableCompaction(pi, event, ctx);
1565
- } catch (error) {
1566
- if (event.signal.aborted || error instanceof Error && error.name === "AbortError")
1567
- return;
1568
- console.error(`[pi-provider-compaction] native compaction failed: ${error instanceof Error ? error.message : String(error)}`);
1833
+ pi.on("before_agent_start", (_event, context) => {
1834
+ install(context);
1835
+ });
1836
+ pi.on("model_select", (_event, context) => {
1837
+ invalidate();
1838
+ install(context);
1839
+ });
1840
+ pi.on("session_tree", () => {
1841
+ invalidate();
1842
+ });
1843
+ pi.on("session_shutdown", () => {
1844
+ invalidate();
1845
+ uninstall?.();
1846
+ uninstall = undefined;
1847
+ installedProvider = undefined;
1848
+ ctx = undefined;
1849
+ });
1850
+ const removeInvalidation = pi.events.on("pi-provider-compaction:invalidate", invalidate);
1851
+ pi.on("session_shutdown", () => {
1852
+ if (typeof removeInvalidation === "function")
1853
+ removeInvalidation();
1854
+ });
1855
+ pi.on("session_before_compact", async (event, context) => {
1856
+ const capture = snapshot;
1857
+ snapshot = undefined;
1858
+ pending = undefined;
1859
+ if (!capture) {
1860
+ fallback(`no eligible snapshot (${captureStatus})`);
1861
+ return;
1862
+ }
1863
+ if (!context.model || generic(context) || event.signal.aborted) {
1864
+ fallback("missing model, generic model override, or aborted attempt");
1865
+ return;
1866
+ }
1867
+ const protocol = resolveNativeProtocol(context.model);
1868
+ if (!protocol || context.sessionManager.getSessionId() !== capture.sessionId) {
1869
+ fallback("unsupported protocol or changed session");
1870
+ return;
1871
+ }
1872
+ const ticket = generation;
1873
+ const branch = context.sessionManager.getBranch();
1874
+ const branchHash = digest(branch);
1875
+ if (!equal(branch, event.branchEntries) || !unchanged(branch, capture.branchLength, capture.branchHash)) {
1876
+ fallback("history changed since capture");
1569
1877
  return;
1570
1878
  }
1879
+ const projection = context.sessionManager.buildSessionProjection();
1880
+ const boundary = projection.entries.findIndex((entry) => entry.sourceEntry.id === event.preparation.firstKeptEntryId);
1881
+ if (boundary < 0) {
1882
+ fallback("retained boundary missing from projection");
1883
+ return;
1884
+ }
1885
+ const prefix = transcript(projection.entries.slice(0, boundary).flatMap((entry) => entry.messages));
1886
+ const input = selectCoveredPrefix(capture.model, capture.prepared, prefix, fallback);
1887
+ if (!input)
1888
+ return;
1889
+ const current = () => ticket === generation && !event.signal.aborted && supportedRouteOptions(capture.model, capture.options) && !generic(context) && context.modelRegistry.getProvider(capture.model.provider) === capture.provider && digest(context.sessionManager.getBranch()) === branchHash;
1890
+ const auth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
1891
+ if (!current())
1892
+ return { cancel: true };
1893
+ if (!auth.ok || routeDigest(auth) !== capture.authHash) {
1894
+ fallback("auth changed since capture");
1895
+ return;
1896
+ }
1897
+ compactionStatus = "generating portable summary";
1898
+ let portable;
1899
+ try {
1900
+ portable = await compact(event.preparation, capture.model, auth.apiKey, auth.headers, event.customInstructions, event.signal, context.thinkingLevel, capture.provider.streamSimple.bind(capture.provider), auth.env, undefined, undefined, capture.sessionId);
1901
+ } catch {
1902
+ return !current() ? { cancel: true } : undefined;
1903
+ }
1904
+ if (!current() || !portable.summary.trim() || portable.summary === NATIVE_COMPACTION_SUMMARY)
1905
+ return { cancel: true };
1906
+ try {
1907
+ const freshAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
1908
+ if (!current())
1909
+ return { cancel: true };
1910
+ if (!freshAuth.ok || routeDigest(freshAuth) !== capture.authHash) {
1911
+ fallback("auth changed after portable generation");
1912
+ return { compaction: portable };
1913
+ }
1914
+ compactionStatus = "requesting native checkpoint";
1915
+ const native = await requestProviderCompaction({
1916
+ provider: capture.provider,
1917
+ model: capture.model,
1918
+ context: capture.prepared.context,
1919
+ input,
1920
+ preparedPayload: capture.prepared.payload,
1921
+ protocol,
1922
+ apiKey: capture.options.apiKey,
1923
+ headers: capture.options.headers,
1924
+ env: capture.options.env,
1925
+ signal: event.signal,
1926
+ fetch: capture.options.fetch
1927
+ });
1928
+ if (!current())
1929
+ return { cancel: true };
1930
+ const finalAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
1931
+ if (!current())
1932
+ return { cancel: true };
1933
+ if (!finalAuth.ok || routeDigest(finalAuth) !== capture.authHash) {
1934
+ fallback("auth changed after native generation");
1935
+ return { compaction: portable };
1936
+ }
1937
+ const details = {
1938
+ ...isObject(portable.details) ? portable.details : {},
1939
+ type: NATIVE_COMPACTION_TYPE,
1940
+ version: 3,
1941
+ provider: capture.model.provider,
1942
+ api: capture.model.api,
1943
+ model: capture.model.id,
1944
+ protocol,
1945
+ identity: identity(capture.model, capture.options),
1946
+ output: native.output,
1947
+ nativeUsage: native.usage,
1948
+ portableUsage: portable.usage,
1949
+ coverage: { kind: "summarized-prefix", firstKeptEntryId: portable.firstKeptEntryId, sourceLeafId: branch.at(-1).id, sourceHash: branchHash, policyHash: capture.policyHash, inputHash: digest(input), runtimeId }
1950
+ };
1951
+ pending = { details, capture, summary: portable.summary };
1952
+ return { compaction: { ...portable, usage: combineUsage(native.usage, portable.usage), details } };
1953
+ } catch {
1954
+ if (!current())
1955
+ return { cancel: true };
1956
+ fallback("native acquisition or validation failed");
1957
+ return { compaction: portable };
1958
+ }
1959
+ });
1960
+ pi.on("session_compact", (event, context) => {
1961
+ if (readNativeCompactionDetails(event.compactionEntry.details)?.version === 3)
1962
+ compactionStatus = "native checkpoint committed";
1963
+ else if (!compactionStatus.startsWith("portable:"))
1964
+ fallback("another handler supplied the committed result");
1965
+ if (compactionStatus.startsWith("portable:"))
1966
+ pi.appendEntry("pi-provider-compaction:diagnostic", { status: compactionStatus });
1967
+ active = undefined;
1968
+ snapshot = undefined;
1969
+ const candidate = pending;
1970
+ pending = undefined;
1971
+ if (!candidate || !equal(event.compactionEntry.details, candidate.details) || event.compactionEntry.summary !== candidate.summary)
1972
+ return;
1973
+ const branch = context.sessionManager.getBranch();
1974
+ const coverage = candidate.details.coverage;
1975
+ if (!coverage || event.compactionEntry.firstKeptEntryId !== coverage.firstKeptEntryId || event.compactionEntry.parentId !== coverage.sourceLeafId || branch.at(-1)?.id !== event.compactionEntry.id || digest(branch.slice(0, -1)) !== coverage.sourceHash)
1976
+ return;
1977
+ active = { entryId: event.compactionEntry.id, detailsHash: digest(candidate.details), summary: candidate.summary, branchLength: branch.length, branchHash: digest(branch), routeHash: candidate.capture.routeHash, policyHash: candidate.capture.policyHash };
1978
+ });
1979
+ pi.on("session_compact_failed", () => {
1980
+ pending = undefined;
1981
+ snapshot = undefined;
1982
+ compactionStatus = "compaction failed or cancelled";
1571
1983
  });
1572
1984
  }
1573
1985
  export {
1574
1986
  NATIVE_COMPACTION_SUMMARY,
1575
1987
  NATIVE_COMPACTION_TYPE,
1576
1988
  NATIVE_COMPACTION_VERSION,
1577
- compactOpenAIResponses,
1578
1989
  pi_provider_compaction_default as default,
1579
- findLatestNativeCompaction,
1580
1990
  readNativeCompactionDetails,
1581
- rewriteResponsesPayload,
1582
1991
  supportsNativeCompaction
1583
1992
  };