@opengeni/runtime 0.7.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES +32 -0
- package/dist/agent-UMUIWWD2.js +8 -0
- package/dist/agent-UMUIWWD2.js.map +1 -0
- package/dist/chunk-4YJ5Z52Z.js +1789 -0
- package/dist/chunk-4YJ5Z52Z.js.map +1 -0
- package/dist/chunk-DPULLAJG.js +215 -0
- package/dist/chunk-DPULLAJG.js.map +1 -0
- package/dist/chunk-K4V4QYCL.js +37683 -0
- package/dist/chunk-K4V4QYCL.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-YUAFQEFY.js +21522 -0
- package/dist/chunk-YUAFQEFY.js.map +1 -0
- package/dist/{index-DIiiwQY2.d.ts → index-D6yi-JcU.d.ts} +138 -15
- package/dist/index.d.ts +329 -121
- package/dist/index.js +8149 -307
- package/dist/index.js.map +1 -1
- package/dist/localSources-T3HZ6KY4.js +15 -0
- package/dist/localSources-T3HZ6KY4.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +42 -5
- package/dist/skill-library.d.ts +74 -0
- package/dist/skill-library.js +18 -0
- package/dist/skill-library.js.map +1 -0
- package/package.json +13 -5
- package/src/bundled_hashicorp_terraform_skills/README.md +3 -3
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +1 -1
- package/src/context-compaction.ts +421 -24
- package/src/history-sanitizer.ts +14 -5
- package/src/index.ts +1335 -280
- package/src/sandbox/channel-a.ts +535 -97
- package/src/sandbox/index.ts +36 -2
- package/src/sandbox/routing/routing-session.ts +10 -2
- package/src/sandbox/run-credentials.ts +818 -0
- package/src/sandbox/select.ts +3 -3
- package/src/sandbox/selfhosted/capabilities.ts +1 -1
- package/src/sandbox/selfhosted/op-stream.ts +40 -0
- package/src/sandbox/selfhosted/op-testing.ts +50 -3
- package/src/sandbox/selfhosted/session.ts +17 -4
- package/src/sandbox/selfhosted/testing.ts +1 -1
- package/src/sandbox/stream-port.ts +1 -1
- package/src/sandbox/stream-token.ts +1 -1
- package/src/sandbox/terminal-server.ts +6 -2
- package/src/sandbox/toolspace-token.ts +147 -0
- package/src/sandbox/turn-tool-cancellation.ts +940 -0
- package/src/skill-library.ts +323 -0
- package/dist/chunk-RYDCVTMW.js +0 -5531
- package/dist/chunk-RYDCVTMW.js.map +0 -1
- /package/src/{bundled_hashicorp_terraform_skills → bundled_skill_library}/azure-verified-modules/SKILL.md +0 -0
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
* from the active model-facing history; the database audit rows remain.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE,
|
|
13
|
+
sanitizeHistoryItemsForModel,
|
|
14
|
+
} from "./history-sanitizer";
|
|
15
|
+
import { boundModelToolOutputItem } from "@opengeni/codex";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
12
17
|
|
|
13
18
|
export type CompactionItem = Record<string, unknown>;
|
|
14
19
|
|
|
@@ -53,6 +58,19 @@ export const USER_MESSAGE_TRUNCATION_MARKER =
|
|
|
53
58
|
|
|
54
59
|
const RESULT_TYPE_BY_CALL_TYPE = TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE;
|
|
55
60
|
const RESULT_TYPES = new Set(Object.values(RESULT_TYPE_BY_CALL_TYPE));
|
|
61
|
+
const MODEL_GENERATED_ITEM_TYPES = new Set([
|
|
62
|
+
"reasoning",
|
|
63
|
+
"function_call",
|
|
64
|
+
"custom_tool_call",
|
|
65
|
+
"tool_search_call",
|
|
66
|
+
"web_search_call",
|
|
67
|
+
"image_generation_call",
|
|
68
|
+
"computer_call",
|
|
69
|
+
"shell_call",
|
|
70
|
+
"apply_patch_call",
|
|
71
|
+
"compaction",
|
|
72
|
+
"context_compaction",
|
|
73
|
+
]);
|
|
56
74
|
|
|
57
75
|
function itemType(item: unknown): string | undefined {
|
|
58
76
|
if (!item || typeof item !== "object") {
|
|
@@ -88,9 +106,24 @@ export function isEphemeralInternalContext(item: unknown): boolean {
|
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
/**
|
|
91
|
-
*
|
|
92
|
-
* the
|
|
109
|
+
* Conservative tokenizer-independent text estimate used for every local input
|
|
110
|
+
* budget. Preserve the stable ASCII `chars / 4` heuristic, but never discount
|
|
111
|
+
* Unicode as UTF-16 length/4: each non-ASCII code unit costs at least one token
|
|
112
|
+
* (CJK ≈ 1/code point; an astral emoji's surrogate pair ≈ 2). This intentionally
|
|
113
|
+
* errs toward compaction rather than letting multilingual current input exceed
|
|
114
|
+
* a provider context window before an authoritative usage anchor exists.
|
|
93
115
|
*/
|
|
116
|
+
export function estimateTextTokens(text: string): number {
|
|
117
|
+
let asciiCodeUnits = 0;
|
|
118
|
+
let nonAsciiCodeUnits = 0;
|
|
119
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
120
|
+
if (text.charCodeAt(index) <= 0x7f) asciiCodeUnits += 1;
|
|
121
|
+
else nonAsciiCodeUnits += 1;
|
|
122
|
+
}
|
|
123
|
+
return nonAsciiCodeUnits + Math.ceil(asciiCodeUnits / 4);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Serialized-item estimate for pre-call accounting and retained user budgets. */
|
|
94
127
|
export function estimateItemTokens(item: CompactionItem): number {
|
|
95
128
|
let text: string;
|
|
96
129
|
try {
|
|
@@ -98,7 +131,7 @@ export function estimateItemTokens(item: CompactionItem): number {
|
|
|
98
131
|
} catch {
|
|
99
132
|
text = String(item);
|
|
100
133
|
}
|
|
101
|
-
return
|
|
134
|
+
return estimateTextTokens(text);
|
|
102
135
|
}
|
|
103
136
|
|
|
104
137
|
export function estimateTokens(items: readonly CompactionItem[]): number {
|
|
@@ -109,6 +142,109 @@ export function estimateTokens(items: readonly CompactionItem[]): number {
|
|
|
109
142
|
return total;
|
|
110
143
|
}
|
|
111
144
|
|
|
145
|
+
export function estimateSerializedValueTokens(value: unknown): number {
|
|
146
|
+
let serialized: string;
|
|
147
|
+
try {
|
|
148
|
+
serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
149
|
+
} catch {
|
|
150
|
+
serialized = String(value);
|
|
151
|
+
}
|
|
152
|
+
return estimateTextTokens(serialized ?? "");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type CompleteModelInputFootprint = {
|
|
156
|
+
input: readonly CompactionItem[];
|
|
157
|
+
instructionsTokens: number;
|
|
158
|
+
toolSchemaTokens: number;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export type ProviderContextTokenSignal = {
|
|
162
|
+
/** Monotonic within one sampled run; advances after every model response. */
|
|
163
|
+
revision: number;
|
|
164
|
+
/** Provider total tokens after that response (input + generated output). */
|
|
165
|
+
totalTokens: number;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type CompleteModelInputEstimate = {
|
|
169
|
+
tokens: number;
|
|
170
|
+
source: "complete_estimate" | "provider_plus_local";
|
|
171
|
+
inputTokens: number;
|
|
172
|
+
instructionsTokens: number;
|
|
173
|
+
toolSchemaTokens: number;
|
|
174
|
+
appendedAfterModelTokens: number;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Match Codex history accounting: after one provider response, start from its
|
|
179
|
+
* authoritative TOTAL token count and add only local items placed after the
|
|
180
|
+
* newest model-generated item. System instructions and tool schemas are
|
|
181
|
+
* compared with the exact request footprint that produced the provider count;
|
|
182
|
+
* positive growth is added. Without a bound anchor, estimate the entire
|
|
183
|
+
* outgoing request rather than trusting stale usage from an earlier turn.
|
|
184
|
+
*/
|
|
185
|
+
export function estimateCompleteModelInput(input: {
|
|
186
|
+
current: CompleteModelInputFootprint;
|
|
187
|
+
provider?: ProviderContextTokenSignal | null;
|
|
188
|
+
providerRequestFootprint?: CompleteModelInputFootprint | null;
|
|
189
|
+
}): CompleteModelInputEstimate {
|
|
190
|
+
const inputTokens = estimateTokens(input.current.input);
|
|
191
|
+
const instructionsTokens = input.current.instructionsTokens;
|
|
192
|
+
const toolSchemaTokens = input.current.toolSchemaTokens;
|
|
193
|
+
if (!input.provider || !input.providerRequestFootprint || input.provider.totalTokens <= 0) {
|
|
194
|
+
return {
|
|
195
|
+
tokens: inputTokens + instructionsTokens + toolSchemaTokens,
|
|
196
|
+
source: "complete_estimate",
|
|
197
|
+
inputTokens,
|
|
198
|
+
instructionsTokens,
|
|
199
|
+
toolSchemaTokens,
|
|
200
|
+
appendedAfterModelTokens: 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const appended = itemsAfterLastModelGeneratedItem(input.current.input);
|
|
205
|
+
const appendedAfterModelTokens = estimateTokens(appended);
|
|
206
|
+
const instructionGrowth = Math.max(
|
|
207
|
+
0,
|
|
208
|
+
instructionsTokens - input.providerRequestFootprint.instructionsTokens,
|
|
209
|
+
);
|
|
210
|
+
const toolSchemaGrowth = Math.max(
|
|
211
|
+
0,
|
|
212
|
+
toolSchemaTokens - input.providerRequestFootprint.toolSchemaTokens,
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
tokens:
|
|
216
|
+
input.provider.totalTokens + appendedAfterModelTokens + instructionGrowth + toolSchemaGrowth,
|
|
217
|
+
source: "provider_plus_local",
|
|
218
|
+
inputTokens,
|
|
219
|
+
instructionsTokens,
|
|
220
|
+
toolSchemaTokens,
|
|
221
|
+
appendedAfterModelTokens,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function itemsAfterLastModelGeneratedItem(
|
|
226
|
+
items: readonly CompactionItem[],
|
|
227
|
+
): CompactionItem[] {
|
|
228
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
229
|
+
if (isModelGeneratedItem(items[index])) {
|
|
230
|
+
return items.slice(index + 1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Codex treats a provider token anchor without any model-generated item as
|
|
234
|
+
// unbound. Callers therefore fall back to a complete estimate in that case.
|
|
235
|
+
return items.slice();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function hasModelGeneratedItem(items: readonly CompactionItem[]): boolean {
|
|
239
|
+
return items.some(isModelGeneratedItem);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isModelGeneratedItem(item: unknown): boolean {
|
|
243
|
+
const type = itemType(item);
|
|
244
|
+
if (type === "message") return itemRole(item) === "assistant";
|
|
245
|
+
return MODEL_GENERATED_ITEM_TYPES.has(type ?? "");
|
|
246
|
+
}
|
|
247
|
+
|
|
112
248
|
export function clampCompactionThresholdRatio(value: number | undefined | null): number {
|
|
113
249
|
const numeric =
|
|
114
250
|
typeof value === "number" && Number.isFinite(value)
|
|
@@ -159,17 +295,41 @@ export function decideCompaction(input: {
|
|
|
159
295
|
typeof input.lastInputTokens === "number" && input.lastInputTokens > 0
|
|
160
296
|
? input.lastInputTokens
|
|
161
297
|
: 0;
|
|
162
|
-
const
|
|
298
|
+
const activeHistoryEstimate = estimateTokens(input.items);
|
|
299
|
+
// A durable provider count belongs to an earlier request. The full active
|
|
300
|
+
// estimate is a conservative cross-turn floor until an exact same-run anchor
|
|
301
|
+
// is available in the per-call guard.
|
|
302
|
+
const signalTokens = Math.max(recorded, activeHistoryEstimate);
|
|
163
303
|
if (input.items.length === 0) {
|
|
164
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
shouldCompact: false,
|
|
306
|
+
reason: "no_history",
|
|
307
|
+
signalTokens,
|
|
308
|
+
thresholdTokens,
|
|
309
|
+
};
|
|
165
310
|
}
|
|
166
311
|
if (input.force) {
|
|
167
|
-
return {
|
|
312
|
+
return {
|
|
313
|
+
shouldCompact: true,
|
|
314
|
+
reason: "force",
|
|
315
|
+
signalTokens,
|
|
316
|
+
thresholdTokens,
|
|
317
|
+
};
|
|
168
318
|
}
|
|
169
319
|
if (signalTokens >= thresholdTokens) {
|
|
170
|
-
return {
|
|
320
|
+
return {
|
|
321
|
+
shouldCompact: true,
|
|
322
|
+
reason: "above_threshold",
|
|
323
|
+
signalTokens,
|
|
324
|
+
thresholdTokens,
|
|
325
|
+
};
|
|
171
326
|
}
|
|
172
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
shouldCompact: false,
|
|
329
|
+
reason: "below_threshold",
|
|
330
|
+
signalTokens,
|
|
331
|
+
thresholdTokens,
|
|
332
|
+
};
|
|
173
333
|
}
|
|
174
334
|
|
|
175
335
|
export class CompactionNeededError extends Error {
|
|
@@ -198,6 +358,48 @@ export class CompactionNeededError extends Error {
|
|
|
198
358
|
}
|
|
199
359
|
}
|
|
200
360
|
|
|
361
|
+
export class EmptyCompactionSummaryError extends Error {
|
|
362
|
+
readonly diagnostics: Record<string, unknown>;
|
|
363
|
+
|
|
364
|
+
constructor(diagnostics: Record<string, unknown> = {}) {
|
|
365
|
+
const compact = JSON.stringify(diagnostics).slice(0, 2_000);
|
|
366
|
+
super(
|
|
367
|
+
`Compaction summarizer returned no assistant text; active history was preserved${compact ? ` (${compact})` : ""}`,
|
|
368
|
+
);
|
|
369
|
+
this.name = "EmptyCompactionSummaryError";
|
|
370
|
+
this.diagnostics = diagnostics;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The checkpoint model request ended in a provider/transport failure rather
|
|
376
|
+
* than a successful response with an empty assistant message. Diagnostics are
|
|
377
|
+
* deliberately bounded and content-free so this error can be persisted on a
|
|
378
|
+
* turn without copying provider messages or conversation input into events.
|
|
379
|
+
*/
|
|
380
|
+
export class CompactionProviderResponseError extends Error {
|
|
381
|
+
readonly diagnostics: Record<string, unknown>;
|
|
382
|
+
readonly status?: number;
|
|
383
|
+
readonly code?: string;
|
|
384
|
+
readonly type?: string;
|
|
385
|
+
override readonly cause?: unknown;
|
|
386
|
+
|
|
387
|
+
constructor(diagnostics: Record<string, unknown> = {}, cause?: unknown) {
|
|
388
|
+
const compact = JSON.stringify(diagnostics).slice(0, 2_000);
|
|
389
|
+
super(
|
|
390
|
+
`Compaction provider request failed; active history was preserved${compact ? ` (${compact})` : ""}`,
|
|
391
|
+
);
|
|
392
|
+
this.name = "CompactionProviderResponseError";
|
|
393
|
+
this.diagnostics = diagnostics;
|
|
394
|
+
if (typeof diagnostics.httpStatus === "number") this.status = diagnostics.httpStatus;
|
|
395
|
+
if (typeof diagnostics.code === "string") this.code = diagnostics.code;
|
|
396
|
+
if (typeof diagnostics.type === "string") this.type = diagnostics.type;
|
|
397
|
+
if (cause !== undefined) {
|
|
398
|
+
Object.defineProperty(this, "cause", { value: cause, enumerable: false });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
201
403
|
export function findCompactionNeededError(
|
|
202
404
|
error: unknown,
|
|
203
405
|
seen = new WeakSet<object>(),
|
|
@@ -233,6 +435,98 @@ export function buildCompactionPromptInput(items: readonly CompactionItem[]): Co
|
|
|
233
435
|
];
|
|
234
436
|
}
|
|
235
437
|
|
|
438
|
+
export type PreparedCompactionPromptInput = {
|
|
439
|
+
input: CompactionItem[];
|
|
440
|
+
estimatedInputTokens: number;
|
|
441
|
+
rewrittenToolOutputs: number;
|
|
442
|
+
droppedHistoryItems: number;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Fit the explicit checkpoint request without mutating canonical history.
|
|
447
|
+
*
|
|
448
|
+
* Codex first replaces oversized tool outputs in its temporary remote-
|
|
449
|
+
* compaction input. OpenGeni does the same oldest-first, preserving the most
|
|
450
|
+
* recent tool detail for the plaintext summary. If that is still insufficient,
|
|
451
|
+
* whole oldest user-delimited work units are removed and the remaining suffix
|
|
452
|
+
* is protocol-sanitized so no call/result/reasoning fragment is orphaned.
|
|
453
|
+
* The raw active history is still the source for the eventual replacement and
|
|
454
|
+
* remains unchanged if the provider call fails.
|
|
455
|
+
*/
|
|
456
|
+
export function prepareCompactionPromptInput(
|
|
457
|
+
items: readonly CompactionItem[],
|
|
458
|
+
maxInputTokens: number,
|
|
459
|
+
): PreparedCompactionPromptInput {
|
|
460
|
+
const budget = Math.max(0, Math.floor(maxInputTokens));
|
|
461
|
+
let history = items.slice();
|
|
462
|
+
let estimatedInputTokens = estimateTokens(buildCompactionPromptInput(history));
|
|
463
|
+
let rewrittenToolOutputs = 0;
|
|
464
|
+
|
|
465
|
+
for (let index = 0; index < history.length && estimatedInputTokens > budget; index += 1) {
|
|
466
|
+
const current = history[index]!;
|
|
467
|
+
const replacement = minimalToolResultForCompaction(current);
|
|
468
|
+
if (replacement === current) continue;
|
|
469
|
+
const before = estimateItemTokens(current);
|
|
470
|
+
const after = estimateItemTokens(replacement);
|
|
471
|
+
if (after >= before) continue;
|
|
472
|
+
history[index] = replacement;
|
|
473
|
+
estimatedInputTokens -= before - after;
|
|
474
|
+
rewrittenToolOutputs += 1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const beforeDropLength = history.length;
|
|
478
|
+
if (estimatedInputTokens > budget && history.length > 0) {
|
|
479
|
+
const prefixTokens = new Array<number>(history.length + 1).fill(0);
|
|
480
|
+
for (let index = 0; index < history.length; index += 1) {
|
|
481
|
+
prefixTokens[index + 1] = prefixTokens[index]! + estimateItemTokens(history[index]!);
|
|
482
|
+
}
|
|
483
|
+
const cuts = oldestLogicalUnitCuts(history);
|
|
484
|
+
const cut =
|
|
485
|
+
cuts.find((candidate) => estimatedInputTokens - prefixTokens[candidate]! <= budget) ??
|
|
486
|
+
history.length;
|
|
487
|
+
history = sanitizeHistoryItemsForModel(history.slice(cut));
|
|
488
|
+
estimatedInputTokens = estimateTokens(buildCompactionPromptInput(history));
|
|
489
|
+
// The synthesized checkpoint instruction is the irreducible floor. The
|
|
490
|
+
// real model budgets are far above it, but keep the helper total for tests
|
|
491
|
+
// and malformed configuration instead of constructing invalid fragments.
|
|
492
|
+
if (estimatedInputTokens > budget) {
|
|
493
|
+
history = [];
|
|
494
|
+
estimatedInputTokens = estimateTokens(buildCompactionPromptInput(history));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
input: buildCompactionPromptInput(history),
|
|
500
|
+
estimatedInputTokens,
|
|
501
|
+
rewrittenToolOutputs,
|
|
502
|
+
droppedHistoryItems: beforeDropLength - history.length,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function minimalToolResultForCompaction(item: CompactionItem): CompactionItem {
|
|
507
|
+
const type = itemType(item);
|
|
508
|
+
if (!type || !RESULT_TYPES.has(type)) return item;
|
|
509
|
+
if (type === "tool_search_output" && Array.isArray(item.tools) && item.tools.length > 0) {
|
|
510
|
+
return { ...item, tools: [] };
|
|
511
|
+
}
|
|
512
|
+
return boundModelToolOutputItem(item, 0);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function oldestLogicalUnitCuts(items: readonly CompactionItem[]): number[] {
|
|
516
|
+
const userMessageIndexes: number[] = [];
|
|
517
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
518
|
+
if (isUserMessage(items[index])) userMessageIndexes.push(index);
|
|
519
|
+
}
|
|
520
|
+
const cuts: number[] = [];
|
|
521
|
+
if (userMessageIndexes.length === 0) {
|
|
522
|
+
return [items.length];
|
|
523
|
+
}
|
|
524
|
+
if (userMessageIndexes[0]! > 0) cuts.push(userMessageIndexes[0]!);
|
|
525
|
+
for (const index of userMessageIndexes.slice(1)) cuts.push(index);
|
|
526
|
+
cuts.push(items.length);
|
|
527
|
+
return cuts;
|
|
528
|
+
}
|
|
529
|
+
|
|
236
530
|
/**
|
|
237
531
|
* Build the active history after compaction:
|
|
238
532
|
* the newest real user messages that fit one cumulative 20k-token budget
|
|
@@ -249,7 +543,7 @@ export function buildCompactionReplacementHistory(
|
|
|
249
543
|
if (!isUserMessage(item) || isCompactionSummary(item)) {
|
|
250
544
|
continue;
|
|
251
545
|
}
|
|
252
|
-
const textTokens =
|
|
546
|
+
const textTokens = estimateTextTokens(messageText(item));
|
|
253
547
|
retainedReversed.push(compactMessageToTokenBudget(item, remaining));
|
|
254
548
|
if (textTokens > remaining) {
|
|
255
549
|
remaining = 0;
|
|
@@ -262,12 +556,46 @@ export function buildCompactionReplacementHistory(
|
|
|
262
556
|
return history;
|
|
263
557
|
}
|
|
264
558
|
|
|
559
|
+
export function compactionReplacementFingerprint(items: readonly CompactionItem[]): string {
|
|
560
|
+
// PostgreSQL JSONB does not preserve JavaScript object-key insertion order.
|
|
561
|
+
// Canonicalize recursively so a replacement has the same identity before
|
|
562
|
+
// and after its durable round trip.
|
|
563
|
+
const serialized = JSON.stringify(canonicalJsonValue(items));
|
|
564
|
+
return createHash("sha256").update(serialized, "utf8").digest("hex");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
568
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
569
|
+
if (!value || typeof value !== "object") return value;
|
|
570
|
+
return Object.fromEntries(
|
|
571
|
+
Object.entries(value as Record<string, unknown>)
|
|
572
|
+
.filter(([, entry]) => entry !== undefined)
|
|
573
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
574
|
+
.map(([key, entry]) => [key, canonicalJsonValue(entry)]),
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Fingerprint the latest durable replacement prefix in active history. */
|
|
579
|
+
export function latestCompactionReplacementFingerprint(
|
|
580
|
+
items: readonly CompactionItem[],
|
|
581
|
+
): string | null {
|
|
582
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
583
|
+
if (isCompactionSummary(items[index])) {
|
|
584
|
+
return compactionReplacementFingerprint(items.slice(0, index + 1));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
265
590
|
/**
|
|
266
591
|
* Build the synthetic summary item (a plain user message) appended to the
|
|
267
592
|
* rebuilt active history.
|
|
268
593
|
*/
|
|
269
594
|
export function buildSummaryItem(summaryBody: string): CompactionItem {
|
|
270
|
-
const trimmed = summaryBody.trim()
|
|
595
|
+
const trimmed = summaryBody.trim();
|
|
596
|
+
if (!trimmed) {
|
|
597
|
+
throw new EmptyCompactionSummaryError({ stage: "build_summary_item" });
|
|
598
|
+
}
|
|
271
599
|
return {
|
|
272
600
|
type: "message",
|
|
273
601
|
role: "user",
|
|
@@ -279,7 +607,7 @@ export function buildSummaryItem(summaryBody: string): CompactionItem {
|
|
|
279
607
|
function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): CompactionItem {
|
|
280
608
|
const text = messageText(item);
|
|
281
609
|
const next = { ...item };
|
|
282
|
-
if (
|
|
610
|
+
if (estimateTextTokens(text) > maxTokens) {
|
|
283
611
|
next.content = truncateMiddleByEstimatedTokens(text, maxTokens);
|
|
284
612
|
return next;
|
|
285
613
|
}
|
|
@@ -287,22 +615,91 @@ function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): C
|
|
|
287
615
|
return next;
|
|
288
616
|
}
|
|
289
617
|
|
|
290
|
-
function estimatedTextTokens(text: string): number {
|
|
291
|
-
return Math.ceil(text.length / 4);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
618
|
function truncateMiddleByEstimatedTokens(text: string, maxTokens: number): string {
|
|
295
|
-
const
|
|
296
|
-
if (text
|
|
619
|
+
const budget = Math.max(0, Math.floor(maxTokens));
|
|
620
|
+
if (estimateTextTokens(text) <= budget) {
|
|
297
621
|
return text;
|
|
298
622
|
}
|
|
299
|
-
if (
|
|
300
|
-
return USER_MESSAGE_TRUNCATION_MARKER
|
|
623
|
+
if (estimateTextTokens(USER_MESSAGE_TRUNCATION_MARKER) > budget) {
|
|
624
|
+
return tokenBoundedPrefix(USER_MESSAGE_TRUNCATION_MARKER, budget);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let low = 0;
|
|
628
|
+
let high = text.length;
|
|
629
|
+
let best = USER_MESSAGE_TRUNCATION_MARKER;
|
|
630
|
+
while (low <= high) {
|
|
631
|
+
const keepCodeUnits = Math.floor((low + high) / 2);
|
|
632
|
+
const candidate = middleTruncationCandidate(text, keepCodeUnits);
|
|
633
|
+
if (estimateTextTokens(candidate) <= budget) {
|
|
634
|
+
best = candidate;
|
|
635
|
+
low = keepCodeUnits + 1;
|
|
636
|
+
} else {
|
|
637
|
+
high = keepCodeUnits - 1;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return best;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function middleTruncationCandidate(text: string, keepCodeUnits: number): string {
|
|
644
|
+
const headTarget = Math.ceil(keepCodeUnits / 2);
|
|
645
|
+
const tailTarget = Math.floor(keepCodeUnits / 2);
|
|
646
|
+
const headEnd = validPrefixBoundary(text, headTarget);
|
|
647
|
+
const tailStart = validSuffixBoundary(text, text.length - tailTarget);
|
|
648
|
+
return `${text.slice(0, headEnd)}${USER_MESSAGE_TRUNCATION_MARKER}${text.slice(tailStart)}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function tokenBoundedPrefix(text: string, maxTokens: number): string {
|
|
652
|
+
let low = 0;
|
|
653
|
+
let high = text.length;
|
|
654
|
+
let best = "";
|
|
655
|
+
while (low <= high) {
|
|
656
|
+
const target = Math.floor((low + high) / 2);
|
|
657
|
+
const end = validPrefixBoundary(text, target);
|
|
658
|
+
const candidate = text.slice(0, end);
|
|
659
|
+
if (estimateTextTokens(candidate) <= maxTokens) {
|
|
660
|
+
best = candidate;
|
|
661
|
+
low = target + 1;
|
|
662
|
+
} else {
|
|
663
|
+
high = target - 1;
|
|
664
|
+
}
|
|
301
665
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
666
|
+
return best;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Largest boundary at or below target that does not split a surrogate pair. */
|
|
670
|
+
function validPrefixBoundary(text: string, target: number): number {
|
|
671
|
+
let boundary = Math.max(0, Math.min(text.length, target));
|
|
672
|
+
if (
|
|
673
|
+
boundary > 0 &&
|
|
674
|
+
boundary < text.length &&
|
|
675
|
+
isHighSurrogate(text.charCodeAt(boundary - 1)) &&
|
|
676
|
+
isLowSurrogate(text.charCodeAt(boundary))
|
|
677
|
+
) {
|
|
678
|
+
boundary -= 1;
|
|
679
|
+
}
|
|
680
|
+
return boundary;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Smallest boundary at or above target that does not split a surrogate pair. */
|
|
684
|
+
function validSuffixBoundary(text: string, target: number): number {
|
|
685
|
+
let boundary = Math.max(0, Math.min(text.length, target));
|
|
686
|
+
if (
|
|
687
|
+
boundary > 0 &&
|
|
688
|
+
boundary < text.length &&
|
|
689
|
+
isHighSurrogate(text.charCodeAt(boundary - 1)) &&
|
|
690
|
+
isLowSurrogate(text.charCodeAt(boundary))
|
|
691
|
+
) {
|
|
692
|
+
boundary += 1;
|
|
693
|
+
}
|
|
694
|
+
return boundary;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function isHighSurrogate(codeUnit: number): boolean {
|
|
698
|
+
return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function isLowSurrogate(codeUnit: number): boolean {
|
|
702
|
+
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
|
|
306
703
|
}
|
|
307
704
|
|
|
308
705
|
function contentWithoutImages(item: CompactionItem): unknown {
|
package/src/history-sanitizer.ts
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { SCREENSHOT_FAILURE_CARD_IMAGE_URL } from "./screenshot-error-card";
|
|
31
|
+
import {
|
|
32
|
+
boundModelToolOutputItems,
|
|
33
|
+
DEFAULT_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
|
|
34
|
+
} from "@opengeni/codex";
|
|
31
35
|
|
|
32
36
|
/** A history item is any JSON object; we only inspect a few discriminator fields. */
|
|
33
37
|
export type HistoryItem = Record<string, unknown>;
|
|
@@ -224,7 +228,10 @@ export function elideSupersededViewImagePairs<T extends HistoryItem>(items: read
|
|
|
224
228
|
* types exist with that id, the call appearing before the result. Calls and
|
|
225
229
|
* results that satisfy that survive untouched.
|
|
226
230
|
*/
|
|
227
|
-
export function sanitizeHistoryItemsForModel<T extends HistoryItem>(
|
|
231
|
+
export function sanitizeHistoryItemsForModel<T extends HistoryItem>(
|
|
232
|
+
items: readonly T[],
|
|
233
|
+
toolOutputTruncationTokens = DEFAULT_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
|
|
234
|
+
): T[] {
|
|
228
235
|
if (items.length === 0) {
|
|
229
236
|
return [];
|
|
230
237
|
}
|
|
@@ -314,10 +321,12 @@ export function sanitizeHistoryItemsForModel<T extends HistoryItem>(items: reado
|
|
|
314
321
|
}
|
|
315
322
|
}
|
|
316
323
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
324
|
+
const paired =
|
|
325
|
+
dropped.size === 0 ? items.slice() : items.filter((_item, index) => !dropped.has(index));
|
|
326
|
+
return boundModelToolOutputItems(
|
|
327
|
+
paired.map(stripInternalResumeMarker),
|
|
328
|
+
toolOutputTruncationTokens,
|
|
329
|
+
);
|
|
321
330
|
}
|
|
322
331
|
|
|
323
332
|
/**
|