@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/README.md +55 -41
- package/dist/index.js +1009 -600
- package/index.ts +279 -456
- package/package.json +11 -9
- package/policy.ts +2 -2
- package/protocol.ts +50 -12
- package/provider.ts +85 -0
- package/replay.ts +141 -0
package/dist/index.js
CHANGED
|
@@ -1,22 +1,239 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
16
|
-
function
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
722
|
+
closePendingToolCalls();
|
|
347
723
|
result.push(msg);
|
|
348
724
|
} else {
|
|
349
725
|
result.push(msg);
|
|
350
726
|
}
|
|
351
727
|
}
|
|
352
|
-
|
|
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(
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1441
|
+
const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
|
|
1442
|
+
return { ...result, output: validateCompactedOutput(result) };
|
|
1159
1443
|
}
|
|
1160
1444
|
|
|
1161
|
-
//
|
|
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
|
-
|
|
1167
|
-
|
|
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
|
|
1170
|
-
if (!isObject(
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1477
|
+
if (payload.previous_response_id || payload.conversation) {
|
|
1478
|
+
reject("stateful request");
|
|
1479
|
+
return;
|
|
1179
1480
|
}
|
|
1180
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
1199
|
-
if (!
|
|
1517
|
+
function mappedPrefix(snapshot, prefix) {
|
|
1518
|
+
if (!equal(snapshot.canonical.messages.slice(0, prefix.messages.length), prefix.messages))
|
|
1200
1519
|
return;
|
|
1201
|
-
|
|
1520
|
+
const head = prefix.messages[0]?.role === "system" ? 1 : 0;
|
|
1521
|
+
if (head && snapshot.context.messages[0]?.role !== "system")
|
|
1202
1522
|
return;
|
|
1203
|
-
if (
|
|
1523
|
+
if (!equal(snapshot.context.messages.slice(head, prefix.messages.length), prefix.messages.slice(head)))
|
|
1204
1524
|
return;
|
|
1205
|
-
|
|
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
|
-
|
|
1208
|
-
if (!
|
|
1531
|
+
}
|
|
1532
|
+
if (!closedTools(prefix)) {
|
|
1533
|
+
reject("prefix has an incomplete tool exchange or failed assistant");
|
|
1209
1534
|
return;
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1279
|
-
result.authorization = `Bearer ${apiKey}`;
|
|
1280
|
-
return result;
|
|
1559
|
+
return structuredClone(snapshot.payload.input.slice(start, end));
|
|
1281
1560
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
if (
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
1315
|
-
const
|
|
1316
|
-
if (!
|
|
1570
|
+
function summarySlot(model, snapshot, summaryPrefix) {
|
|
1571
|
+
const mapped = mappedPrefix(snapshot, summaryPrefix);
|
|
1572
|
+
if (!mapped)
|
|
1317
1573
|
return;
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1409
|
-
|
|
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
|
|
1412
|
-
if (
|
|
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
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
|
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
|
|
1447
|
-
const
|
|
1448
|
-
return
|
|
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
|
|
1454
|
-
const
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1545
|
-
|
|
1546
|
-
if (!candidate || genericCompactionSelected(pi, ctx))
|
|
1730
|
+
if (!supportsNativeCompaction(model)) {
|
|
1731
|
+
captureRejected("unsupported API");
|
|
1547
1732
|
return;
|
|
1548
|
-
|
|
1549
|
-
if (!
|
|
1733
|
+
}
|
|
1734
|
+
if (!options.onPayload) {
|
|
1735
|
+
captureRejected("no payload callback");
|
|
1550
1736
|
return;
|
|
1551
|
-
|
|
1552
|
-
if (
|
|
1737
|
+
}
|
|
1738
|
+
if (generic(owner)) {
|
|
1739
|
+
captureRejected("generic compaction model takes precedence");
|
|
1553
1740
|
return;
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
const
|
|
1560
|
-
|
|
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("
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
};
|