@nijaru/pi-provider-compaction 0.0.3 → 0.0.4

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
  }
@@ -197,6 +414,149 @@ function resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools) {
197
414
  throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: ${message}.`);
198
415
  }
199
416
  }
417
+ function createGrammarToolInputProperties(tools, supportsOpenAIGrammarTools) {
418
+ const properties = new Map;
419
+ for (const tool of tools ?? []) {
420
+ const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
421
+ if (grammar) {
422
+ properties.set(tool.name, grammar.inputProperty);
423
+ }
424
+ }
425
+ return properties;
426
+ }
427
+
428
+ // node_modules/@earendil-works/pi-ai/dist/utils/text.js
429
+ function contentText(content, separator = `
430
+ `) {
431
+ if (typeof content === "string")
432
+ return content;
433
+ return content.filter((block) => block.type === "text").map((block) => block.text).join(separator);
434
+ }
435
+ function getSystemMessageText(message) {
436
+ const parts = [contentText(message.content)];
437
+ for (const text of Object.values(message.sections ?? {})) {
438
+ if (text !== null)
439
+ parts.push(text);
440
+ }
441
+ return parts.filter((part) => part.length > 0).join(`
442
+
443
+ `);
444
+ }
445
+ function renderSystemMessageUpdate(message) {
446
+ const parts = [];
447
+ const text = contentText(message.content);
448
+ if (text.length > 0)
449
+ parts.push(text);
450
+ for (const [name, value] of Object.entries(message.sections ?? {})) {
451
+ parts.push(value === null ? `Removed system prompt section "${name}".` : `Updated system prompt section "${name}":
452
+
453
+ ${value}`);
454
+ }
455
+ return parts.join(`
456
+
457
+ `);
458
+ }
459
+
460
+ // node_modules/@earendil-works/pi-ai/dist/utils/transcript.js
461
+ function isSystemMessage(message) {
462
+ return message.role === "system";
463
+ }
464
+ function getInitialSystemMessage(messages) {
465
+ const first = messages[0];
466
+ return first && isSystemMessage(first) ? first : undefined;
467
+ }
468
+ function getCurrentTools(messages) {
469
+ const tools = new Map;
470
+ for (const message of messages) {
471
+ if (!isSystemMessage(message))
472
+ continue;
473
+ for (const tool of message.toolsRemoved ?? [])
474
+ tools.delete(tool.name);
475
+ for (const tool of message.toolsAdded ?? [])
476
+ tools.set(tool.name, tool);
477
+ }
478
+ return [...tools.values()];
479
+ }
480
+ function getCurrentSystemMessage(messages) {
481
+ const content = [];
482
+ const sections = new Map;
483
+ let timestamp;
484
+ for (const message of messages) {
485
+ if (!isSystemMessage(message))
486
+ continue;
487
+ timestamp ??= message.timestamp;
488
+ const text = contentText(message.content);
489
+ if (text.length > 0)
490
+ content.push(text);
491
+ for (const [name, value] of Object.entries(message.sections ?? {})) {
492
+ if (value === null)
493
+ sections.delete(name);
494
+ else
495
+ sections.set(name, value);
496
+ }
497
+ }
498
+ const tools = getCurrentTools(messages);
499
+ if (timestamp === undefined && tools.length === 0)
500
+ return;
501
+ return {
502
+ role: "system",
503
+ content: content.join(`
504
+
505
+ `),
506
+ ...sections.size > 0 ? { sections: Object.fromEntries(sections) } : {},
507
+ ...tools.length > 0 ? { toolsAdded: tools } : {},
508
+ timestamp: timestamp ?? 0
509
+ };
510
+ }
511
+ function collapseSystemMessages(context) {
512
+ const head = getCurrentSystemMessage(context.messages);
513
+ const messages = context.messages.filter((message) => message.role !== "system");
514
+ return { messages: head ? [head, ...messages] : messages };
515
+ }
516
+ function resolveTranscript(context, supportsMidConvoSystemMessages) {
517
+ return supportsMidConvoSystemMessages ? context : collapseSystemMessages(context);
518
+ }
519
+ function hasNonAdditiveToolChanges(messages) {
520
+ const declared = new Set;
521
+ for (const message of messages) {
522
+ if (!isSystemMessage(message))
523
+ continue;
524
+ if ((message.toolsRemoved?.length ?? 0) > 0)
525
+ return true;
526
+ for (const tool of message.toolsAdded ?? []) {
527
+ if (declared.has(tool.name))
528
+ return true;
529
+ declared.add(tool.name);
530
+ }
531
+ }
532
+ return false;
533
+ }
534
+ function resolveTranscriptTools(messages, supportsToolAdditions) {
535
+ const anchorsAdditions = supportsToolAdditions && !hasNonAdditiveToolChanges(messages);
536
+ return {
537
+ requestTools: anchorsAdditions ? getInitialSystemMessage(messages)?.toolsAdded ?? [] : getCurrentTools(messages),
538
+ anchorsAdditions
539
+ };
540
+ }
541
+
542
+ // node_modules/@earendil-works/pi-ai/dist/utils/hash.js
543
+ function shortHash(str) {
544
+ let h1 = 3735928559;
545
+ let h2 = 1103547991;
546
+ for (let i = 0;i < str.length; i++) {
547
+ const ch = str.charCodeAt(i);
548
+ h1 = Math.imul(h1 ^ ch, 2654435761);
549
+ h2 = Math.imul(h2 ^ ch, 1597334677);
550
+ }
551
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
552
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
553
+ return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
554
+ }
555
+
556
+ // node_modules/@earendil-works/pi-ai/dist/utils/sanitize-unicode.js
557
+ function sanitizeSurrogates(text) {
558
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
559
+ }
200
560
 
201
561
  // node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js
202
562
  var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
@@ -242,7 +602,7 @@ function transformMessages(messages, model, normalizeToolCallId) {
242
602
  const normalizedMessages = messages.map((msg) => msg.content == null ? { ...msg, content: [] } : msg);
243
603
  const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
244
604
  const transformed = imageAwareMessages.map((msg) => {
245
- if (msg.role === "user") {
605
+ if (msg.role === "system" || msg.role === "user") {
246
606
  return msg;
247
607
  }
248
608
  if (msg.role === "toolResult") {
@@ -307,7 +667,8 @@ function transformMessages(messages, model, normalizeToolCallId) {
307
667
  const result = [];
308
668
  let pendingToolCalls = [];
309
669
  let existingToolResultIds = new Set;
310
- const insertSyntheticToolResults = () => {
670
+ const heldSystemMessages = [];
671
+ const closePendingToolCalls = () => {
311
672
  if (pendingToolCalls.length > 0) {
312
673
  for (const tc of pendingToolCalls) {
313
674
  if (!existingToolResultIds.has(tc.id)) {
@@ -324,11 +685,13 @@ function transformMessages(messages, model, normalizeToolCallId) {
324
685
  pendingToolCalls = [];
325
686
  existingToolResultIds = new Set;
326
687
  }
688
+ result.push(...heldSystemMessages);
689
+ heldSystemMessages.length = 0;
327
690
  };
328
691
  for (let i = 0;i < transformed.length; i++) {
329
692
  const msg = transformed[i];
330
693
  if (msg.role === "assistant") {
331
- insertSyntheticToolResults();
694
+ closePendingToolCalls();
332
695
  const assistantMsg = msg;
333
696
  if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
334
697
  continue;
@@ -342,14 +705,20 @@ function transformMessages(messages, model, normalizeToolCallId) {
342
705
  } else if (msg.role === "toolResult") {
343
706
  existingToolResultIds.add(msg.toolCallId);
344
707
  result.push(msg);
708
+ } else if (msg.role === "system") {
709
+ if (pendingToolCalls.length > 0) {
710
+ heldSystemMessages.push(msg);
711
+ } else {
712
+ result.push(msg);
713
+ }
345
714
  } else if (msg.role === "user") {
346
- insertSyntheticToolResults();
715
+ closePendingToolCalls();
347
716
  result.push(msg);
348
717
  } else {
349
718
  result.push(msg);
350
719
  }
351
720
  }
352
- insertSyntheticToolResults();
721
+ closePendingToolCalls();
353
722
  return result;
354
723
  }
355
724
 
@@ -392,8 +761,8 @@ function convertToolResultOutput(model, content) {
392
761
  return output;
393
762
  }
394
763
  function convertResponsesMessages(model, context, allowedToolCallProviders, options) {
764
+ const normalizedContext = resolveTranscript(context, options?.supportsMidConvoSystemMessages);
395
765
  const messages = [];
396
- const loadedToolNames = new Set;
397
766
  const normalizeIdPart = (part) => {
398
767
  const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
399
768
  const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
@@ -417,19 +786,56 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
417
786
  }
418
787
  return `${normalizedCallId}|${normalizedItemId}`;
419
788
  };
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";
789
+ const transformedMessages = transformMessages(normalizedContext.messages, model, normalizeToolCallId);
790
+ const transcriptTools = resolveTranscriptTools(normalizedContext.messages, (options?.supportsAdditionalTools ?? false) || (options?.supportsToolSearch ?? false));
791
+ const appendSystemToolAdditions = (message, seed) => {
792
+ const tools = transcriptTools.anchorsAdditions ? message.toolsAdded ?? [] : [];
793
+ if (tools.length === 0)
794
+ return;
795
+ if (options?.supportsAdditionalTools) {
796
+ messages.push({
797
+ type: "additional_tools",
798
+ role: "developer",
799
+ tools: convertResponsesTools(tools, options.toolOptions)
800
+ });
801
+ return;
802
+ }
803
+ if (!options?.supportsToolSearch)
804
+ return;
805
+ const names = tools.map((tool) => tool.name);
806
+ const callId = `pi_tool_load_${shortHash(`${seed}:${names.join(",")}`)}`;
425
807
  messages.push({
426
- role,
427
- content: sanitizeSurrogates(context.systemPrompt)
808
+ type: "tool_search_call",
809
+ call_id: callId,
810
+ execution: "client",
811
+ status: "completed",
812
+ arguments: { query: names.join(" "), limit: names.length }
428
813
  });
429
- }
814
+ messages.push({
815
+ type: "tool_search_output",
816
+ call_id: callId,
817
+ execution: "client",
818
+ status: "completed",
819
+ tools: convertResponsesTools(tools, { ...options.toolOptions, toolSearchResult: true })
820
+ });
821
+ };
822
+ const includeInitialSystemMessage = options?.includeSystemPrompt ?? true;
823
+ const compat = model.compat;
824
+ const instructionRole = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
430
825
  let msgIndex = 0;
826
+ let sourceIndex = 0;
431
827
  for (const msg of transformedMessages) {
432
- if (msg.role === "user") {
828
+ const isLeadingSystemMessage = sourceIndex++ === 0 && msg.role === "system";
829
+ if (msg.role === "system") {
830
+ if (!isLeadingSystemMessage)
831
+ appendSystemToolAdditions(msg, `system:${msgIndex}`);
832
+ if (!isLeadingSystemMessage || includeInitialSystemMessage) {
833
+ const text = isLeadingSystemMessage ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);
834
+ if (text.length > 0) {
835
+ messages.push({ role: instructionRole, content: sanitizeSurrogates(text) });
836
+ }
837
+ }
838
+ } else if (msg.role === "user") {
433
839
  if (typeof msg.content === "string") {
434
840
  messages.push({
435
841
  role: "user",
@@ -496,7 +902,6 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
496
902
  if (isDifferentModel && itemId?.startsWith("fc_") || customInputProperty === undefined && !itemId?.startsWith("fc_")) {
497
903
  itemId = undefined;
498
904
  }
499
- const canReplayNamespace = isSameModel || options?.deferredTools?.has(toolCall.name) === true;
500
905
  if (customInputProperty !== undefined) {
501
906
  output.push({
502
907
  type: "custom_tool_call",
@@ -504,7 +909,7 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
504
909
  call_id: callId,
505
910
  name: toolCall.name,
506
911
  input: sanitizeSurrogates(getGrammarToolInput(toolCall.name, toolCall.arguments, customInputProperty)),
507
- ...canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
912
+ ...isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
508
913
  });
509
914
  } else {
510
915
  output.push({
@@ -513,7 +918,7 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
513
918
  call_id: callId,
514
919
  name: toolCall.name,
515
920
  arguments: JSON.stringify(toolCall.arguments),
516
- ...canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
921
+ ...isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
517
922
  });
518
923
  }
519
924
  }
@@ -537,43 +942,9 @@ function convertResponsesMessages(model, context, allowedToolCallProviders, opti
537
942
  output
538
943
  });
539
944
  }
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
945
  }
576
- msgIndex++;
946
+ if (!isLeadingSystemMessage)
947
+ msgIndex++;
577
948
  }
578
949
  return messages;
579
950
  }
@@ -593,7 +964,7 @@ function convertResponsesTools(tools, options) {
593
964
  syntax: grammar.format,
594
965
  definition: grammar.definition
595
966
  },
596
- ...options?.deferLoading ? { defer_loading: true } : {}
967
+ ...options?.toolSearchResult ? { defer_loading: true } : {}
597
968
  };
598
969
  }
599
970
  const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode);
@@ -603,7 +974,7 @@ function convertResponsesTools(tools, options) {
603
974
  name: tool.name,
604
975
  description: tool.description,
605
976
  parameters: getJsonSchemaToolParameters(tool, strict === true),
606
- ...options?.deferLoading ? { defer_loading: true } : {}
977
+ ...options?.toolSearchResult ? { defer_loading: true } : {}
607
978
  };
608
979
  if (supportsStrictMode) {
609
980
  functionTool.strict = strict;
@@ -612,157 +983,6 @@ function convertResponsesTools(tools, options) {
612
983
  });
613
984
  }
614
985
 
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
986
  // protocol.ts
767
987
  var RESPONSES_APIS = new Set([
768
988
  "openai-responses",
@@ -794,14 +1014,52 @@ function validateItemSize(item) {
794
1014
  if (byteLength(item) > MAX_ITEM_BYTES)
795
1015
  throw new Error("Native compaction output item exceeded 2 MiB");
796
1016
  }
1017
+ function validContent(value) {
1018
+ return typeof value === "string" || Array.isArray(value) && value.every((part) => {
1019
+ if (!isObject(part))
1020
+ return false;
1021
+ if (part.type === "input_text" || part.type === "output_text" || part.type === "summary_text")
1022
+ return typeof part.text === "string";
1023
+ if (part.type === "input_image")
1024
+ return typeof part.image_url === "string" || typeof part.file_id === "string";
1025
+ if (part.type === "refusal")
1026
+ return typeof part.refusal === "string";
1027
+ return false;
1028
+ });
1029
+ }
797
1030
  function validateCompactedOutput(value) {
798
1031
  if (!isObject(value) || !Array.isArray(value.output) || value.output.length === 0) {
799
1032
  throw new Error("Responses Compact returned an invalid output window");
800
1033
  }
1034
+ if (byteLength(value.output) > MAX_RESPONSE_BYTES)
1035
+ throw new Error("Native compaction output exceeded 8 MiB");
801
1036
  const output = value.output.map((item) => {
802
1037
  if (!isObject(item))
803
1038
  throw new Error("Responses Compact returned a non-object output item");
804
1039
  validateItemSize(item);
1040
+ const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
1041
+ 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)))
1042
+ throw new Error("Responses Compact returned an unsupported output item");
1043
+ if (type === "message" && (!["user", "assistant", "system", "developer"].includes(String(item.role)) || !validContent(item.content)))
1044
+ throw new Error("Responses Compact returned an invalid message item");
1045
+ if (type === "compaction" && !isCompactionItem(item))
1046
+ throw new Error("Responses Compact returned an invalid checkpoint");
1047
+ if (type === "reasoning" && !(typeof item.encrypted_content === "string" || Array.isArray(item.summary) && validContent(item.summary)))
1048
+ throw new Error("Responses Compact returned invalid reasoning");
1049
+ if (String(type).includes("call") || type === "tool_search_output") {
1050
+ if (typeof item.call_id !== "string" || !item.call_id)
1051
+ throw new Error("Responses Compact returned an invalid tool id");
1052
+ if (type === "function_call" && (typeof item.name !== "string" || typeof item.arguments !== "string"))
1053
+ throw new Error("Responses Compact returned an invalid function call");
1054
+ if (type === "custom_tool_call" && (typeof item.name !== "string" || typeof item.input !== "string"))
1055
+ throw new Error("Responses Compact returned an invalid custom call");
1056
+ if ((type === "function_call_output" || type === "custom_tool_call_output") && !validContent(item.output))
1057
+ throw new Error("Responses Compact returned an invalid tool output");
1058
+ if (type === "tool_search_call" && !isObject(item.arguments))
1059
+ throw new Error("Responses Compact returned an invalid tool search");
1060
+ }
1061
+ if ((type === "additional_tools" || type === "tool_search_output") && (!Array.isArray(item.tools) || !item.tools.every(isObject)))
1062
+ throw new Error("Responses Compact returned invalid tools");
805
1063
  return structuredClone(item);
806
1064
  });
807
1065
  const checkpoints = output.filter(isCompactionItem);
@@ -841,6 +1099,9 @@ async function readJsonBounded(response, signal) {
841
1099
  }
842
1100
  } finally {
843
1101
  signal.removeEventListener("abort", onAbort);
1102
+ reader.cancel().catch(() => {
1103
+ return;
1104
+ });
844
1105
  reader.releaseLock();
845
1106
  }
846
1107
  const bytes = new Uint8Array(total);
@@ -988,7 +1249,9 @@ async function requestResponsesCompact(request) {
988
1249
  onPayload: (payload) => {
989
1250
  if (!isObject(payload))
990
1251
  throw new Error("Responses provider exposed a non-object payload");
991
- preparedPayload = { ...structuredClone(payload), input: structuredClone(request.input) };
1252
+ if (request.preparedPayload && request.preparedPayload.model !== payload.model)
1253
+ throw new Error("Native route model changed since capture");
1254
+ preparedPayload = { ...structuredClone(request.preparedPayload ?? payload), input: structuredClone(request.input) };
992
1255
  return preparedPayload;
993
1256
  }
994
1257
  });
@@ -1081,6 +1344,9 @@ async function collectCompactionSse(stream, signal) {
1081
1344
  dispatch();
1082
1345
  } finally {
1083
1346
  signal.removeEventListener("abort", onAbort);
1347
+ reader.cancel().catch(() => {
1348
+ return;
1349
+ });
1084
1350
  reader.releaseLock();
1085
1351
  }
1086
1352
  if (!completed)
@@ -1118,13 +1384,15 @@ function buildRemoteV2History(input, item) {
1118
1384
  async function requestRemoteV2(request) {
1119
1385
  const baseFetch = request.fetch ?? globalThis.fetch;
1120
1386
  let sentInput;
1387
+ const controller = new AbortController;
1388
+ const signal = AbortSignal.any([request.signal, controller.signal]);
1121
1389
  const inspections = [];
1122
1390
  const inspectedFetch = async (input, init) => {
1123
1391
  const response = await baseFetch(input, init);
1124
1392
  if (!response.ok || !response.body)
1125
1393
  return response;
1126
1394
  const [providerBody, inspectionBody] = response.body.tee();
1127
- inspections.push(collectCompactionSse(inspectionBody, request.signal));
1395
+ inspections.push(collectCompactionSse(inspectionBody, signal).then((result) => ({ result }), (error) => ({ error })));
1128
1396
  return new Response(providerBody, {
1129
1397
  status: response.status,
1130
1398
  statusText: response.statusText,
@@ -1144,440 +1412,572 @@ async function requestRemoteV2(request) {
1144
1412
  if (!isObject(payload))
1145
1413
  throw new Error("Codex provider exposed a non-object payload");
1146
1414
  sentInput = structuredClone(request.input);
1147
- return { ...structuredClone(payload), input: [...sentInput, { type: "compaction_trigger" }] };
1415
+ if (request.preparedPayload && request.preparedPayload.model !== payload.model)
1416
+ throw new Error("Native route model changed since capture");
1417
+ return { ...structuredClone(request.preparedPayload ?? payload), stream: true, input: [...sentInput, { type: "compaction_trigger" }] };
1148
1418
  }
1149
1419
  });
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`);
1420
+ try {
1421
+ const usage = await collectProviderUsage(stream, request.signal);
1422
+ if (!sentInput || inspections.length !== 1)
1423
+ throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
1424
+ const inspection = await inspections[0];
1425
+ if ("error" in inspection)
1426
+ throw inspection.error;
1427
+ return { output: buildRemoteV2History(sentInput, inspection.result.item), usage };
1428
+ } finally {
1429
+ controller.abort();
1153
1430
  }
1154
- const inspection = await inspections[0];
1155
- return { output: buildRemoteV2History(sentInput, inspection.item), usage };
1156
1431
  }
1157
1432
  async function requestProviderCompaction(request) {
1158
- return request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request);
1433
+ const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
1434
+ return { ...result, output: validateCompactedOutput(result) };
1159
1435
  }
1160
1436
 
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.";
1437
+ // replay.ts
1165
1438
  var TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
1166
- function supportsNativeCompaction(model) {
1167
- return supportsResponsesApi(model);
1439
+ var MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024;
1440
+ var SNAPSHOT_TTL_MS = 5 * 60000;
1441
+ function equal(left, right) {
1442
+ return JSON.stringify(left) === JSON.stringify(right);
1443
+ }
1444
+ function serializeInput(model, context, declarations = context) {
1445
+ const compat = model.compat;
1446
+ const flag = (name) => compat?.[name] === true;
1447
+ const codex = model.api === "openai-codex-responses";
1448
+ const normalized = resolveTranscript2(context, flag("supportsMidConvoSystemMessages"));
1449
+ const tools = resolveTranscript2(declarations, flag("supportsMidConvoSystemMessages"));
1450
+ return convertResponsesMessages(model, normalized, model.api === "azure-openai-responses" ? new Set([...TOOL_CALL_PROVIDERS, "azure-openai-responses"]) : TOOL_CALL_PROVIDERS, {
1451
+ includeSystemPrompt: !codex,
1452
+ grammarToolInputProperties: createGrammarToolInputProperties(getDeclaredTools(tools.messages), flag("supportsOpenAIGrammarTools")),
1453
+ supportsMidConvoSystemMessages: flag("supportsMidConvoSystemMessages"),
1454
+ supportsAdditionalTools: flag("supportsAdditionalTools"),
1455
+ supportsToolSearch: flag("supportsToolSearch"),
1456
+ toolOptions: {
1457
+ ...codex ? { strict: null } : {},
1458
+ supportsStrictMode: compat?.supportsStrictMode ?? model.api !== "openai-responses",
1459
+ supportsOpenAIGrammarTools: flag("supportsOpenAIGrammarTools")
1460
+ }
1461
+ });
1168
1462
  }
1169
- function readUsage(value) {
1170
- if (!isObject(value) || !isObject(value.cost))
1463
+ function capturePreparedRequest(model, context, canonical, payload, reject = () => {}) {
1464
+ if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject)) {
1465
+ reject("unsupported payload shape");
1171
1466
  return;
1172
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
1173
- if (typeof value[key] !== "number" || !Number.isFinite(value[key]))
1174
- return;
1175
1467
  }
1176
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
1177
- if (typeof value.cost[key] !== "number" || !Number.isFinite(value.cost[key]))
1178
- return;
1468
+ if (payload.previous_response_id || payload.conversation) {
1469
+ reject("stateful request");
1470
+ return;
1179
1471
  }
1180
- return value;
1181
- }
1182
- function readIdentity(value) {
1183
- if (!isObject(value) || typeof value.endpoint !== "string" || !value.endpoint)
1472
+ if (Buffer.byteLength(JSON.stringify({ context, payload })) > MAX_SNAPSHOT_BYTES) {
1473
+ reject("snapshot exceeds size limit");
1184
1474
  return;
1185
- for (const key of ["organization", "project", "accountId", "azureApiVersion", "azureDeployment"]) {
1186
- if (value[key] !== undefined && typeof value[key] !== "string")
1187
- return;
1188
1475
  }
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
- };
1476
+ const expected = serializeInput(model, context);
1477
+ if (!equal(expected, payload.input)) {
1478
+ reject("prepared input differs from serializer");
1479
+ return;
1480
+ }
1481
+ return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload), portableInput: expected, createdAt: Date.now() };
1482
+ }
1483
+ function promptCount(input) {
1484
+ let count = 0;
1485
+ while (input[count]?.role === "system" || input[count]?.role === "developer")
1486
+ count++;
1487
+ return count;
1488
+ }
1489
+ function closedTools(context) {
1490
+ const pending = new Set;
1491
+ for (const message of context.messages) {
1492
+ if (message.role === "assistant") {
1493
+ if (message.stopReason === "error" || message.stopReason === "aborted")
1494
+ return false;
1495
+ for (const block of message.content)
1496
+ if (block.type === "toolCall") {
1497
+ if (pending.has(block.id))
1498
+ return false;
1499
+ pending.add(block.id);
1500
+ }
1501
+ } else if (message.role === "toolResult") {
1502
+ if (!pending.delete(message.toolCallId))
1503
+ return false;
1504
+ }
1505
+ }
1506
+ return pending.size === 0;
1197
1507
  }
1198
- function readNativeCompactionDetails(value) {
1199
- if (!isObject(value) || value.type !== NATIVE_COMPACTION_TYPE)
1508
+ function mappedPrefix(snapshot, prefix) {
1509
+ if (!equal(snapshot.canonical.messages.slice(0, prefix.messages.length), prefix.messages))
1200
1510
  return;
1201
- if (value.version !== 1 && value.version !== NATIVE_COMPACTION_VERSION)
1511
+ const head = prefix.messages[0]?.role === "system" ? 1 : 0;
1512
+ if (head && snapshot.context.messages[0]?.role !== "system")
1202
1513
  return;
1203
- if (typeof value.provider !== "string" || typeof value.api !== "string" || typeof value.model !== "string")
1514
+ if (!equal(snapshot.context.messages.slice(head, prefix.messages.length), prefix.messages.slice(head)))
1204
1515
  return;
1205
- if (!Array.isArray(value.output) || !value.output.every(isObject) || !value.output.some(isCompactionItem))
1516
+ return normalizeContext({ messages: snapshot.context.messages.slice(0, prefix.messages.length) });
1517
+ }
1518
+ function selectCoveredPrefix(model, snapshot, prefix, reject = () => {}) {
1519
+ if (Date.now() - snapshot.createdAt > SNAPSHOT_TTL_MS) {
1520
+ reject("snapshot expired");
1206
1521
  return;
1207
- const protocol = value.version === 1 ? "responses-compact" : value.protocol === "responses-compact" || value.protocol === "remote-v2" ? value.protocol : undefined;
1208
- if (!protocol)
1522
+ }
1523
+ if (!closedTools(prefix)) {
1524
+ reject("prefix has an incomplete tool exchange or failed assistant");
1209
1525
  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))
1526
+ }
1527
+ const mapped = mappedPrefix(snapshot, prefix);
1528
+ if (!mapped) {
1529
+ reject("projected prefix differs from request context");
1236
1530
  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
1531
  }
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))];
1532
+ const expected = serializeInput(model, mapped, snapshot.context);
1533
+ if (!equal(snapshot.portableInput.slice(0, expected.length), expected)) {
1534
+ reject("serialized prefix does not match prepared input");
1535
+ return;
1536
+ }
1537
+ const start = promptCount(expected);
1538
+ let end = expected.length;
1539
+ if (snapshot.replacement) {
1540
+ if (snapshot.replacement.index < start || snapshot.replacement.index >= end) {
1541
+ reject("previous native slot is outside covered prefix");
1542
+ return;
1264
1543
  }
1544
+ end += snapshot.replacement.count - 1;
1265
1545
  }
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;
1546
+ if (end <= start) {
1547
+ reject("empty covered prefix");
1548
+ return;
1277
1549
  }
1278
- if (!hasAuthorization && apiKey)
1279
- result.authorization = `Bearer ${apiKey}`;
1280
- return result;
1550
+ return structuredClone(snapshot.payload.input.slice(start, end));
1281
1551
  }
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
- };
1552
+ function substituteSummary(payload, index, summary, output) {
1553
+ if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject))
1554
+ return;
1555
+ if (!equal(payload.input[index], summary))
1556
+ return;
1557
+ if (payload.input.filter((item) => equal(item, summary)).length !== 1)
1558
+ return;
1559
+ return { ...payload, input: [...payload.input.slice(0, index), ...structuredClone(output), ...payload.input.slice(index + 1)] };
1313
1560
  }
1314
- function compactUsage(model, result) {
1315
- const usage = result.usage;
1316
- if (!isObject(usage))
1561
+ function summarySlot(model, snapshot, summaryPrefix) {
1562
+ const mapped = mappedPrefix(snapshot, summaryPrefix);
1563
+ if (!mapped)
1317
1564
  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
- }
1565
+ const prefix = serializeInput(model, mapped, snapshot.context);
1566
+ const index = promptCount(prefix);
1567
+ if (prefix.length !== index + 1 || !equal(snapshot.portableInput.slice(0, prefix.length), prefix))
1568
+ return;
1569
+ return index;
1403
1570
  }
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;
1571
+ function requestPolicy(model, snapshot) {
1572
+ return {
1573
+ model,
1574
+ system: getCurrentSystemPrompt(snapshot.context.messages),
1575
+ currentTools: getCurrentTools2(snapshot.context.messages),
1576
+ declarations: getDeclaredTools(resolveTranscript2(snapshot.context, model.compat?.supportsMidConvoSystemMessages).messages),
1577
+ instructions: snapshot.payload.instructions,
1578
+ tools: snapshot.payload.tools,
1579
+ prompt: snapshot.portableInput.slice(0, promptCount(snapshot.portableInput))
1580
+ };
1407
1581
  }
1408
- function codexAccountId(apiKey) {
1409
- if (!apiKey)
1582
+
1583
+ // index.ts
1584
+ var NATIVE_COMPACTION_TYPE = "pi-provider-compaction/openai-responses";
1585
+ var NATIVE_COMPACTION_VERSION = 3;
1586
+ var NATIVE_COMPACTION_SUMMARY = "This context was compacted using the provider's native OpenAI Responses state.";
1587
+ var supportsNativeCompaction = supportsResponsesApi;
1588
+ var digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
1589
+ var transcript = (messages) => normalizeContext2({ messages: convertToLlm(messages) });
1590
+ function readNativeCompactionDetails(value) {
1591
+ if (!isObject(value) || value.type !== NATIVE_COMPACTION_TYPE || value.version !== 1 && value.version !== 2 && value.version !== 3)
1410
1592
  return;
1411
- const parts = apiKey.split(".");
1412
- if (parts.length !== 3)
1593
+ if (typeof value.provider !== "string" || typeof value.api !== "string" || typeof value.model !== "string")
1594
+ return;
1595
+ const protocol = value.version === 1 ? "responses-compact" : value.protocol;
1596
+ if (protocol !== "responses-compact" && protocol !== "remote-v2")
1413
1597
  return;
1414
1598
  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;
1599
+ const output = validateCompactedOutput(value);
1600
+ if (value.version === 3) {
1601
+ const coverage = value.coverage;
1602
+ if (!isObject(coverage) || coverage.kind !== "summarized-prefix")
1603
+ return;
1604
+ for (const field of ["firstKeptEntryId", "sourceLeafId", "sourceHash", "policyHash", "inputHash", "runtimeId"]) {
1605
+ if (typeof coverage[field] !== "string" || !coverage[field])
1606
+ return;
1607
+ }
1608
+ if (!isObject(value.identity) || typeof value.identity.endpoint !== "string")
1609
+ return;
1610
+ }
1611
+ return { ...structuredClone(value), protocol, output };
1420
1612
  } catch {
1421
1613
  return;
1422
1614
  }
1423
1615
  }
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;
1616
+ function latestCompaction(branch) {
1617
+ for (let index = branch.length - 1;index >= 0; index--) {
1618
+ const entry = branch[index];
1619
+ if (entry.type === "compaction")
1620
+ return entry;
1432
1621
  }
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
- };
1622
+ return;
1445
1623
  }
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
- });
1624
+ function header(headers, name) {
1625
+ const value = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name)?.[1];
1626
+ return typeof value === "string" && value ? value : undefined;
1452
1627
  }
1453
- function providerContext(pi, ctx, activeEntries) {
1454
- const messages = activeEntries.flatMap((entry) => sessionEntryToContextMessages(entry));
1628
+ function identity(model, options) {
1629
+ const azure = model.api === "azure-openai-responses";
1630
+ 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);
1631
+ const url = new URL(azure ? azureBase ?? model.baseUrl : model.baseUrl);
1632
+ url.username = "";
1633
+ url.password = "";
1634
+ url.search = "";
1635
+ url.hash = "";
1636
+ let accountId = header(options.headers, "chatgpt-account-id");
1637
+ if (!accountId && model.api === "openai-codex-responses" && options.apiKey) {
1638
+ try {
1639
+ const token = JSON.parse(Buffer.from(options.apiKey.split(".")[1], "base64url").toString("utf8"));
1640
+ const id = token["https://api.openai.com/auth"]?.chatgpt_account_id;
1641
+ if (typeof id === "string")
1642
+ accountId = id;
1643
+ } catch {}
1644
+ }
1645
+ const map = options.env?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP?.split(",").map((entry) => entry.split("=").map((part) => part.trim()));
1455
1646
  return {
1456
- systemPrompt: ctx.getSystemPrompt(),
1457
- messages: convertToLlm(messages),
1458
- tools: activeTools(pi)
1647
+ endpoint: url.toString().replace(/\/+$/, ""),
1648
+ organization: header(options.headers, "openai-organization"),
1649
+ project: header(options.headers, "openai-project"),
1650
+ accountId,
1651
+ azureApiVersion: azure ? options.env?.AZURE_OPENAI_API_VERSION ?? "v1" : undefined,
1652
+ azureDeployment: azure ? map?.find(([id]) => id === model.id)?.[1] ?? model.id : undefined
1459
1653
  };
1460
1654
  }
1461
- function cleanHeaders(headers) {
1462
- if (!headers)
1463
- return;
1464
- return Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null));
1465
- }
1466
1655
  function combineUsage(first, second) {
1467
1656
  if (!first)
1468
1657
  return second;
1469
1658
  if (!second)
1470
1659
  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
- };
1660
+ const result = structuredClone(first);
1661
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"])
1662
+ result[key] += second[key];
1663
+ for (const key of ["reasoning", "cacheWrite1h"])
1664
+ if (first[key] !== undefined || second[key] !== undefined)
1665
+ result[key] = (first[key] ?? 0) + (second[key] ?? 0);
1666
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"])
1667
+ result.cost[key] += second.cost[key];
1668
+ return result;
1487
1669
  }
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
1670
+ function supportedRouteOptions(model, options) {
1671
+ if (model.api !== "azure-openai-responses")
1672
+ return true;
1673
+ const extra = options;
1674
+ if (["azureBaseUrl", "azureResourceName", "azureApiVersion", "azureDeploymentName"].some((key) => extra[key] !== undefined))
1675
+ return false;
1676
+ 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);
1677
+ }
1678
+ function pi_provider_compaction_default(pi) {
1679
+ const secret = randomBytes(32);
1680
+ const routeDigest = (value) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
1681
+ let runtimeId = randomUUID();
1682
+ let ctx;
1683
+ let snapshot;
1684
+ let active;
1685
+ let pending;
1686
+ let uninstall;
1687
+ let installedProvider;
1688
+ let generation = 0;
1689
+ let captureStatus = "no request observed";
1690
+ let compactionStatus = "not attempted in this runtime";
1691
+ const captureRejected = (reason) => {
1692
+ captureStatus = reason;
1530
1693
  };
1531
- return {
1532
- compaction: {
1533
- ...portable,
1534
- usage: combineUsage(native.usage, portable.usage),
1535
- details
1694
+ const fallback = (reason) => {
1695
+ compactionStatus = `portable: ${reason}`;
1696
+ };
1697
+ pi.registerCommand("provider-compaction-status", {
1698
+ description: "Show content-free native compaction eligibility and last attempt",
1699
+ handler: async (_args, context) => {
1700
+ context.ui.notify(`Provider compaction
1701
+ Observer: ${installedProvider ? "installed" : "unavailable for current registration"}
1702
+ Capture: ${captureStatus}
1703
+ Last attempt: ${compactionStatus}`, "info");
1536
1704
  }
1705
+ });
1706
+ const invalidate = () => {
1707
+ generation++;
1708
+ runtimeId = randomUUID();
1709
+ snapshot = undefined;
1710
+ active = undefined;
1711
+ pending = undefined;
1712
+ captureStatus = "invalidated by lifecycle/history/policy change";
1537
1713
  };
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))
1714
+ const generic = (context) => resolveCompactionModelPolicy(pi, context).hasSelectors;
1715
+ const route = (model, options) => routeDigest({ model, apiKey: options.apiKey, headers: options.headers, env: options.env });
1716
+ const unchanged = (branch, length, hash) => branch.length >= length && digest(branch.slice(0, length)) === hash && !branch.slice(length).some((entry) => entry.type === "context_edit");
1717
+ const observe = (model, context, options) => {
1718
+ const owner = ctx;
1719
+ if (!owner || options.sessionId !== owner.sessionManager.getSessionId())
1543
1720
  return;
1544
- const branch = ctx.sessionManager.getBranch();
1545
- const candidate = findLatestNativeCompaction(branch, model);
1546
- if (!candidate || genericCompactionSelected(pi, ctx))
1721
+ if (!supportsNativeCompaction(model)) {
1722
+ captureRejected("unsupported API");
1547
1723
  return;
1548
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
1549
- if (!auth.ok)
1724
+ }
1725
+ if (!options.onPayload) {
1726
+ captureRejected("no payload callback");
1727
+ return;
1728
+ }
1729
+ if (generic(owner)) {
1730
+ captureRejected("generic compaction model takes precedence");
1731
+ return;
1732
+ }
1733
+ if (!supportedRouteOptions(model, options)) {
1734
+ captureRejected("unsupported route overrides");
1735
+ return;
1736
+ }
1737
+ const ticket = generation;
1738
+ const branch = owner.sessionManager.getBranch();
1739
+ const branchHash = digest(branch);
1740
+ const canonical = transcript(owner.sessionManager.buildSessionProjection().messages);
1741
+ const current = () => generation === ticket && ctx === owner && !options.signal?.aborted && supportedRouteOptions(model, options) && digest(owner.sessionManager.getBranch()) === branchHash;
1742
+ return async (payload) => {
1743
+ if (!current())
1744
+ return payload;
1745
+ snapshot = undefined;
1746
+ const prepared = capturePreparedRequest(model, context, canonical, payload, captureRejected);
1747
+ if (!prepared) {
1748
+ active = undefined;
1749
+ return payload;
1750
+ }
1751
+ const auth = await owner.modelRegistry.getApiKeyAndHeaders(model);
1752
+ if (!current())
1753
+ return payload;
1754
+ const normalizedHeaders = (headers) => Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]).sort(([a], [b]) => String(a).localeCompare(String(b)));
1755
+ 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 ?? {})) {
1756
+ active = undefined;
1757
+ captureRejected("resolved auth/route differs from prepared request, or generic policy changed");
1758
+ return payload;
1759
+ }
1760
+ const provider = owner.modelRegistry.getProvider(model.provider);
1761
+ if (!provider) {
1762
+ captureRejected("provider unavailable");
1763
+ return payload;
1764
+ }
1765
+ const routeHash = route(model, options);
1766
+ const policyHash = digest(requestPolicy(model, prepared));
1767
+ const latest = latestCompaction(branch);
1768
+ let result = payload;
1769
+ 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) {
1770
+ const details = readNativeCompactionDetails(latest.details);
1771
+ const projection = owner.sessionManager.buildSessionProjection();
1772
+ const contribution = projection.entries.find((entry) => entry.sourceEntry.id === latest.id);
1773
+ const prefix = contribution ? transcript(contribution.messages) : undefined;
1774
+ const slot = prefix ? summarySlot(model, prepared, prefix) : undefined;
1775
+ if (details?.version === 3 && details.coverage?.runtimeId === runtimeId && details.coverage.firstKeptEntryId === latest.firstKeptEntryId && slot !== undefined) {
1776
+ const replaced = substituteSummary(payload, slot, prepared.portableInput[slot], details.output);
1777
+ if (replaced) {
1778
+ result = replaced;
1779
+ prepared.payload = structuredClone(replaced);
1780
+ prepared.replacement = { index: slot, count: details.output.length };
1781
+ } else
1782
+ active = undefined;
1783
+ } else
1784
+ active = undefined;
1785
+ } else
1786
+ active = undefined;
1787
+ captureStatus = "eligible prepared request captured";
1788
+ snapshot = {
1789
+ prepared,
1790
+ model: structuredClone(model),
1791
+ provider,
1792
+ options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch },
1793
+ branchLength: branch.length,
1794
+ branchHash,
1795
+ routeHash,
1796
+ authHash: routeDigest(auth),
1797
+ policyHash,
1798
+ sessionId: owner.sessionManager.getSessionId()
1799
+ };
1800
+ return result;
1801
+ };
1802
+ };
1803
+ const install = (context) => {
1804
+ ctx = context;
1805
+ if (installedProvider === context.model?.provider)
1550
1806
  return;
1551
- const latest = findLatestNativeCompaction(branch, model, routeIdentity(model, auth));
1552
- if (!latest)
1807
+ uninstall?.();
1808
+ uninstall = undefined;
1809
+ installedProvider = undefined;
1810
+ if (!supportsNativeCompaction(context.model))
1553
1811
  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;
1812
+ uninstall = installObserver(pi, context, observe);
1813
+ if (uninstall)
1814
+ installedProvider = context.model.provider;
1815
+ captureStatus = uninstall ? "awaiting prepared request" : "unsupported provider registration";
1816
+ };
1817
+ pi.on("session_start", (_event, context) => {
1818
+ invalidate();
1819
+ install(context);
1820
+ const latest = latestCompaction(context.sessionManager.getBranch());
1821
+ if (latest?.summary === NATIVE_COMPACTION_SUMMARY)
1822
+ 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
1823
  });
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)}`);
1824
+ pi.on("before_agent_start", (_event, context) => {
1825
+ install(context);
1826
+ });
1827
+ pi.on("model_select", (_event, context) => {
1828
+ invalidate();
1829
+ install(context);
1830
+ });
1831
+ pi.on("session_tree", () => {
1832
+ invalidate();
1833
+ });
1834
+ pi.on("session_shutdown", () => {
1835
+ invalidate();
1836
+ uninstall?.();
1837
+ uninstall = undefined;
1838
+ installedProvider = undefined;
1839
+ ctx = undefined;
1840
+ });
1841
+ const removeInvalidation = pi.events.on("pi-provider-compaction:invalidate", invalidate);
1842
+ pi.on("session_shutdown", () => {
1843
+ if (typeof removeInvalidation === "function")
1844
+ removeInvalidation();
1845
+ });
1846
+ pi.on("session_before_compact", async (event, context) => {
1847
+ const capture = snapshot;
1848
+ snapshot = undefined;
1849
+ pending = undefined;
1850
+ if (!capture) {
1851
+ fallback(`no eligible snapshot (${captureStatus})`);
1852
+ return;
1853
+ }
1854
+ if (!context.model || generic(context) || event.signal.aborted) {
1855
+ fallback("missing model, generic model override, or aborted attempt");
1856
+ return;
1857
+ }
1858
+ const protocol = resolveNativeProtocol(context.model);
1859
+ if (!protocol || context.sessionManager.getSessionId() !== capture.sessionId) {
1860
+ fallback("unsupported protocol or changed session");
1861
+ return;
1862
+ }
1863
+ const ticket = generation;
1864
+ const branch = context.sessionManager.getBranch();
1865
+ const branchHash = digest(branch);
1866
+ if (!equal(branch, event.branchEntries) || !unchanged(branch, capture.branchLength, capture.branchHash)) {
1867
+ fallback("history changed since capture");
1569
1868
  return;
1570
1869
  }
1870
+ const projection = context.sessionManager.buildSessionProjection();
1871
+ const boundary = projection.entries.findIndex((entry) => entry.sourceEntry.id === event.preparation.firstKeptEntryId);
1872
+ if (boundary < 0) {
1873
+ fallback("retained boundary missing from projection");
1874
+ return;
1875
+ }
1876
+ const prefix = transcript(projection.entries.slice(0, boundary).flatMap((entry) => entry.messages));
1877
+ const input = selectCoveredPrefix(capture.model, capture.prepared, prefix, fallback);
1878
+ if (!input)
1879
+ return;
1880
+ 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;
1881
+ const auth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
1882
+ if (!current())
1883
+ return { cancel: true };
1884
+ if (!auth.ok || routeDigest(auth) !== capture.authHash) {
1885
+ fallback("auth changed since capture");
1886
+ return;
1887
+ }
1888
+ compactionStatus = "generating portable summary";
1889
+ let portable;
1890
+ try {
1891
+ 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);
1892
+ } catch {
1893
+ return !current() ? { cancel: true } : undefined;
1894
+ }
1895
+ if (!current() || !portable.summary.trim() || portable.summary === NATIVE_COMPACTION_SUMMARY)
1896
+ return { cancel: true };
1897
+ try {
1898
+ const freshAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
1899
+ if (!current())
1900
+ return { cancel: true };
1901
+ if (!freshAuth.ok || routeDigest(freshAuth) !== capture.authHash) {
1902
+ fallback("auth changed after portable generation");
1903
+ return { compaction: portable };
1904
+ }
1905
+ compactionStatus = "requesting native checkpoint";
1906
+ const native = await requestProviderCompaction({
1907
+ provider: capture.provider,
1908
+ model: capture.model,
1909
+ context: capture.prepared.context,
1910
+ input,
1911
+ preparedPayload: capture.prepared.payload,
1912
+ protocol,
1913
+ apiKey: capture.options.apiKey,
1914
+ headers: capture.options.headers,
1915
+ env: capture.options.env,
1916
+ signal: event.signal,
1917
+ fetch: capture.options.fetch
1918
+ });
1919
+ if (!current())
1920
+ return { cancel: true };
1921
+ const finalAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
1922
+ if (!current())
1923
+ return { cancel: true };
1924
+ if (!finalAuth.ok || routeDigest(finalAuth) !== capture.authHash) {
1925
+ fallback("auth changed after native generation");
1926
+ return { compaction: portable };
1927
+ }
1928
+ const details = {
1929
+ ...isObject(portable.details) ? portable.details : {},
1930
+ type: NATIVE_COMPACTION_TYPE,
1931
+ version: 3,
1932
+ provider: capture.model.provider,
1933
+ api: capture.model.api,
1934
+ model: capture.model.id,
1935
+ protocol,
1936
+ identity: identity(capture.model, capture.options),
1937
+ output: native.output,
1938
+ nativeUsage: native.usage,
1939
+ portableUsage: portable.usage,
1940
+ coverage: { kind: "summarized-prefix", firstKeptEntryId: portable.firstKeptEntryId, sourceLeafId: branch.at(-1).id, sourceHash: branchHash, policyHash: capture.policyHash, inputHash: digest(input), runtimeId }
1941
+ };
1942
+ pending = { details, capture, summary: portable.summary };
1943
+ return { compaction: { ...portable, usage: combineUsage(native.usage, portable.usage), details } };
1944
+ } catch {
1945
+ if (!current())
1946
+ return { cancel: true };
1947
+ fallback("native acquisition or validation failed");
1948
+ return { compaction: portable };
1949
+ }
1950
+ });
1951
+ pi.on("session_compact", (event, context) => {
1952
+ if (readNativeCompactionDetails(event.compactionEntry.details)?.version === 3)
1953
+ compactionStatus = "native checkpoint committed";
1954
+ else if (!compactionStatus.startsWith("portable:"))
1955
+ fallback("another handler supplied the committed result");
1956
+ if (compactionStatus.startsWith("portable:"))
1957
+ pi.appendEntry("pi-provider-compaction:diagnostic", { status: compactionStatus });
1958
+ active = undefined;
1959
+ snapshot = undefined;
1960
+ const candidate = pending;
1961
+ pending = undefined;
1962
+ if (!candidate || !equal(event.compactionEntry.details, candidate.details) || event.compactionEntry.summary !== candidate.summary)
1963
+ return;
1964
+ const branch = context.sessionManager.getBranch();
1965
+ const coverage = candidate.details.coverage;
1966
+ 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)
1967
+ return;
1968
+ 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 };
1969
+ });
1970
+ pi.on("session_compact_failed", () => {
1971
+ pending = undefined;
1972
+ snapshot = undefined;
1973
+ compactionStatus = "compaction failed or cancelled";
1571
1974
  });
1572
1975
  }
1573
1976
  export {
1574
1977
  NATIVE_COMPACTION_SUMMARY,
1575
1978
  NATIVE_COMPACTION_TYPE,
1576
1979
  NATIVE_COMPACTION_VERSION,
1577
- compactOpenAIResponses,
1578
1980
  pi_provider_compaction_default as default,
1579
- findLatestNativeCompaction,
1580
1981
  readNativeCompactionDetails,
1581
- rewriteResponsesPayload,
1582
1982
  supportsNativeCompaction
1583
1983
  };