@opengeni/runtime 0.3.1 → 0.4.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/LICENSE +190 -0
- package/dist/index.d.ts +87 -7
- package/dist/index.js +458 -29
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/context-compaction.ts +164 -16
- package/src/history-sanitizer.ts +2 -1
- package/src/index.ts +519 -13
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
@@ -25,22 +26,19 @@
|
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "tsup",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"@opengeni/agent-proto": "^0.2.1",
|
|
32
|
-
"@opengeni/config": "^0.
|
|
33
|
-
"@opengeni/contracts": "^0.
|
|
34
|
+
"@opengeni/config": "^0.3.0",
|
|
35
|
+
"@opengeni/contracts": "^0.9.0",
|
|
34
36
|
"@openai/agents": "^0.11.6",
|
|
35
37
|
"@openai/agents-extensions": "^0.11.6",
|
|
36
38
|
"modal": "^0.7.4",
|
|
37
39
|
"openai": "6.36.0",
|
|
38
40
|
"@opengeni/codex": "^0.2.1"
|
|
39
41
|
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"tsup": "^8.5.0",
|
|
42
|
-
"typescript": "^6.0.3"
|
|
43
|
-
},
|
|
44
42
|
"repository": {
|
|
45
43
|
"type": "git",
|
|
46
44
|
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* from the active model-facing history; the database audit rows remain.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE } from "./history-sanitizer";
|
|
12
|
+
|
|
11
13
|
export type CompactionItem = Record<string, unknown>;
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -18,7 +20,10 @@ export const COMPACTION_SUMMARY_MARKER = "opengeni_context_summary";
|
|
|
18
20
|
|
|
19
21
|
export const SUMMARY_BUFFER_TOKENS = 20_000;
|
|
20
22
|
export const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
|
|
21
|
-
export const
|
|
23
|
+
export const DEFAULT_COMPACTION_THRESHOLD_RATIO = 0.6;
|
|
24
|
+
export const MIN_COMPACTION_THRESHOLD_RATIO = 0.3;
|
|
25
|
+
export const MAX_COMPACTION_THRESHOLD_RATIO = 0.9;
|
|
26
|
+
export const COMPACTION_FALLBACK_TARGET_RATIO = 0.5;
|
|
22
27
|
|
|
23
28
|
// Verbatim from Codex CLI:
|
|
24
29
|
// codex-rs/prompts/templates/compact/prompt.md
|
|
@@ -42,12 +47,7 @@ export const SUMMARY_PREFIX =
|
|
|
42
47
|
export const USER_MESSAGE_TRUNCATION_MARKER =
|
|
43
48
|
"\n[... middle truncated for context compaction ...]\n";
|
|
44
49
|
|
|
45
|
-
const RESULT_TYPE_BY_CALL_TYPE
|
|
46
|
-
function_call: "function_call_result",
|
|
47
|
-
computer_call: "computer_call_result",
|
|
48
|
-
shell_call: "shell_call_output",
|
|
49
|
-
apply_patch_call: "apply_patch_call_output",
|
|
50
|
-
};
|
|
50
|
+
const RESULT_TYPE_BY_CALL_TYPE = TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE;
|
|
51
51
|
const RESULT_TYPES = new Set(Object.values(RESULT_TYPE_BY_CALL_TYPE));
|
|
52
52
|
|
|
53
53
|
function itemType(item: unknown): string | undefined {
|
|
@@ -101,15 +101,19 @@ export function estimateTokens(items: readonly CompactionItem[]): number {
|
|
|
101
101
|
return total;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
export function clampCompactionThresholdRatio(value: number | undefined | null): number {
|
|
105
|
+
const numeric = typeof value === "number" && Number.isFinite(value)
|
|
106
|
+
? value
|
|
107
|
+
: DEFAULT_COMPACTION_THRESHOLD_RATIO;
|
|
108
|
+
return Math.min(MAX_COMPACTION_THRESHOLD_RATIO, Math.max(MIN_COMPACTION_THRESHOLD_RATIO, numeric));
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
export function clientCompactionThresholdTokens(input: {
|
|
105
112
|
contextWindowTokens: number;
|
|
106
113
|
contextReservedOutputTokens: number;
|
|
114
|
+
contextCompactionThresholdRatio?: number | null;
|
|
107
115
|
}): number {
|
|
108
|
-
|
|
109
|
-
0,
|
|
110
|
-
input.contextWindowTokens - input.contextReservedOutputTokens - SUMMARY_BUFFER_TOKENS,
|
|
111
|
-
);
|
|
112
|
-
return Math.floor(available * CLIENT_COMPACTION_TRIGGER_FRACTION);
|
|
116
|
+
return Math.floor(Math.max(0, input.contextWindowTokens) * clampCompactionThresholdRatio(input.contextCompactionThresholdRatio));
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
export type ClientCompactionDecision = {
|
|
@@ -124,6 +128,7 @@ export function decideClientCompaction(input: {
|
|
|
124
128
|
lastInputTokens?: number | null;
|
|
125
129
|
contextWindowTokens: number;
|
|
126
130
|
contextReservedOutputTokens: number;
|
|
131
|
+
contextCompactionThresholdRatio?: number | null;
|
|
127
132
|
force?: boolean;
|
|
128
133
|
}): ClientCompactionDecision {
|
|
129
134
|
const thresholdTokens = clientCompactionThresholdTokens(input);
|
|
@@ -272,6 +277,70 @@ export function buildCompactionReplacementHistory(
|
|
|
272
277
|
return history;
|
|
273
278
|
}
|
|
274
279
|
|
|
280
|
+
export type DeterministicFallbackCompactionInput = {
|
|
281
|
+
items: readonly CompactionItem[];
|
|
282
|
+
cause?: string;
|
|
283
|
+
targetTokens?: number;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export function buildDeterministicFallbackCompactionHistory(input: DeterministicFallbackCompactionInput): CompactionItem[] {
|
|
287
|
+
const sourceTokens = Math.max(1, estimateTokens(input.items));
|
|
288
|
+
const requestedTarget = typeof input.targetTokens === "number" && input.targetTokens > 0
|
|
289
|
+
? Math.floor(input.targetTokens)
|
|
290
|
+
: Math.floor(sourceTokens * COMPACTION_FALLBACK_TARGET_RATIO);
|
|
291
|
+
const targetTokens = Math.max(1, Math.min(requestedTarget, Math.max(1, sourceTokens - 1)));
|
|
292
|
+
const summaryItem = buildSummaryItem([
|
|
293
|
+
"Non-LLM context compaction fallback.",
|
|
294
|
+
input.cause ? `Summarizer failure: ${input.cause}` : "Summarizer failure: unavailable.",
|
|
295
|
+
"Older assistant, tool, reasoning, and image history was dropped deterministically.",
|
|
296
|
+
"Retained context above is limited to initial instructions and the most recent user messages that fit the fallback budget; oversized retained messages may be middle-truncated.",
|
|
297
|
+
].join("\n"));
|
|
298
|
+
const summaryTokens = estimateItemTokens(summaryItem);
|
|
299
|
+
let remaining = Math.max(0, targetTokens - summaryTokens);
|
|
300
|
+
const retainedInstructions: CompactionItem[] = [];
|
|
301
|
+
|
|
302
|
+
for (const item of input.items) {
|
|
303
|
+
if (!isInitialInstructionMessage(item)) {
|
|
304
|
+
if (isUserMessage(item)) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (remaining <= 0) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
const capped = compactMessageToEstimatedItemBudget(item, Math.min(2_000, remaining));
|
|
313
|
+
if (!capped) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const tokens = estimateItemTokens(capped);
|
|
317
|
+
if (tokens <= remaining) {
|
|
318
|
+
retainedInstructions.push(capped);
|
|
319
|
+
remaining -= tokens;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const retainedUsers: CompactionItem[] = [];
|
|
324
|
+
for (let index = input.items.length - 1; index >= 0 && remaining > 0; index -= 1) {
|
|
325
|
+
const item = input.items[index]!;
|
|
326
|
+
if (!isUserMessage(item) || isCompactionSummary(item)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const capped = compactMessageToEstimatedItemBudget(item, Math.min(COMPACT_USER_MESSAGE_MAX_TOKENS, remaining));
|
|
330
|
+
if (!capped) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const tokens = estimateItemTokens(capped);
|
|
334
|
+
if (tokens <= remaining) {
|
|
335
|
+
retainedUsers.push(capped);
|
|
336
|
+
remaining -= tokens;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
retainedUsers.reverse();
|
|
340
|
+
|
|
341
|
+
return [...retainedInstructions, ...retainedUsers, summaryItem];
|
|
342
|
+
}
|
|
343
|
+
|
|
275
344
|
/**
|
|
276
345
|
* Build the synthetic summary item (a plain user message) appended to the
|
|
277
346
|
* rebuilt active history.
|
|
@@ -287,16 +356,41 @@ export function buildSummaryItem(summaryBody: string): CompactionItem {
|
|
|
287
356
|
}
|
|
288
357
|
|
|
289
358
|
function compactUserMessage(item: CompactionItem): CompactionItem {
|
|
359
|
+
return compactMessageToTokenBudget(item, COMPACT_USER_MESSAGE_MAX_TOKENS);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): CompactionItem {
|
|
290
363
|
const text = messageText(item);
|
|
291
364
|
const next = { ...item };
|
|
292
|
-
if (estimatedTextTokens(text) >
|
|
293
|
-
next.content = truncateMiddleByEstimatedTokens(text,
|
|
365
|
+
if (estimatedTextTokens(text) > maxTokens) {
|
|
366
|
+
next.content = truncateMiddleByEstimatedTokens(text, maxTokens);
|
|
294
367
|
return next;
|
|
295
368
|
}
|
|
296
369
|
next.content = contentWithoutImages(item);
|
|
297
370
|
return next;
|
|
298
371
|
}
|
|
299
372
|
|
|
373
|
+
function compactMessageToEstimatedItemBudget(item: CompactionItem, maxItemTokens: number): CompactionItem | null {
|
|
374
|
+
if (maxItemTokens <= 0) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const overheadTokens = estimateItemTokens({ ...item, content: "" });
|
|
378
|
+
let contentBudget = Math.max(0, maxItemTokens - overheadTokens);
|
|
379
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
380
|
+
const capped = compactMessageToTokenBudget(item, contentBudget);
|
|
381
|
+
const estimated = estimateItemTokens(capped);
|
|
382
|
+
if (estimated <= maxItemTokens) {
|
|
383
|
+
return capped;
|
|
384
|
+
}
|
|
385
|
+
const overage = estimated - maxItemTokens;
|
|
386
|
+
if (contentBudget <= 0) {
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
contentBudget = Math.max(0, contentBudget - Math.max(1, overage));
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
300
394
|
function estimatedTextTokens(text: string): number {
|
|
301
395
|
return Math.ceil(text.length / 4);
|
|
302
396
|
}
|
|
@@ -353,8 +447,42 @@ function messageText(item: CompactionItem): string {
|
|
|
353
447
|
return "";
|
|
354
448
|
}
|
|
355
449
|
|
|
356
|
-
|
|
357
|
-
|
|
450
|
+
function isInitialInstructionMessage(item: unknown): boolean {
|
|
451
|
+
const type = itemType(item);
|
|
452
|
+
const role = itemRole(item);
|
|
453
|
+
return type === "message" && (role === "system" || role === "developer");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export type RenderCompactionTranscriptOptions = {
|
|
457
|
+
/**
|
|
458
|
+
* Hard cap for a rendered transcript. When set, oldest rendered items are
|
|
459
|
+
* dropped first while preserving the final checkpoint prompt; if the retained
|
|
460
|
+
* item itself is too large, it is middle-truncated instead of looping.
|
|
461
|
+
*/
|
|
462
|
+
maxEstimatedTokens?: number;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export function renderCompactionPromptInputForChat(
|
|
466
|
+
input: readonly CompactionItem[],
|
|
467
|
+
options: RenderCompactionTranscriptOptions = {},
|
|
468
|
+
): string {
|
|
469
|
+
const rendered = input.map(renderItem);
|
|
470
|
+
if (typeof options.maxEstimatedTokens !== "number" || options.maxEstimatedTokens <= 0) {
|
|
471
|
+
return rendered.join("\n");
|
|
472
|
+
}
|
|
473
|
+
const maxChars = Math.max(1, Math.floor(options.maxEstimatedTokens * 4));
|
|
474
|
+
const last = rendered.at(-1);
|
|
475
|
+
const prefix = rendered.slice(0, -1);
|
|
476
|
+
let kept = prefix.slice();
|
|
477
|
+
while (kept.length > 0 && transcriptLength([...kept, last].filter((line): line is string => line !== undefined)) > maxChars) {
|
|
478
|
+
kept.shift();
|
|
479
|
+
}
|
|
480
|
+
const lines = [...kept, last].filter((line): line is string => line !== undefined);
|
|
481
|
+
const joined = lines.join("\n");
|
|
482
|
+
if (joined.length <= maxChars) {
|
|
483
|
+
return joined;
|
|
484
|
+
}
|
|
485
|
+
return truncateMiddleByChars(joined, maxChars);
|
|
358
486
|
}
|
|
359
487
|
|
|
360
488
|
function renderItem(item: CompactionItem): string {
|
|
@@ -405,3 +533,23 @@ function truncateForTranscript(text: string, max: number): string {
|
|
|
405
533
|
}
|
|
406
534
|
return `${text.slice(0, max)}... (${text.length - max} more chars)`;
|
|
407
535
|
}
|
|
536
|
+
|
|
537
|
+
function transcriptLength(lines: readonly string[]): number {
|
|
538
|
+
if (lines.length === 0) {
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
return lines.reduce((sum, line) => sum + line.length, 0) + Math.max(0, lines.length - 1);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function truncateMiddleByChars(text: string, maxChars: number): string {
|
|
545
|
+
if (text.length <= maxChars) {
|
|
546
|
+
return text;
|
|
547
|
+
}
|
|
548
|
+
if (maxChars <= USER_MESSAGE_TRUNCATION_MARKER.length) {
|
|
549
|
+
return USER_MESSAGE_TRUNCATION_MARKER.slice(0, maxChars);
|
|
550
|
+
}
|
|
551
|
+
const keepChars = maxChars - USER_MESSAGE_TRUNCATION_MARKER.length;
|
|
552
|
+
const headChars = Math.ceil(keepChars / 2);
|
|
553
|
+
const tailChars = Math.floor(keepChars / 2);
|
|
554
|
+
return `${text.slice(0, headChars)}${USER_MESSAGE_TRUNCATION_MARKER}${text.slice(text.length - tailChars)}`;
|
|
555
|
+
}
|
package/src/history-sanitizer.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type HistoryItem = Record<string, unknown>;
|
|
|
38
38
|
* the one observed live, the rest are included so the same pairing logic holds
|
|
39
39
|
* for every tool-call kind the SDK can emit.
|
|
40
40
|
*/
|
|
41
|
-
const
|
|
41
|
+
export const TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE: Record<string, string> = {
|
|
42
42
|
function_call: "function_call_result",
|
|
43
43
|
computer_call: "computer_call_result",
|
|
44
44
|
shell_call: "shell_call_output",
|
|
@@ -52,6 +52,7 @@ const RESULT_TYPE_BY_CALL_TYPE: Record<string, string> = {
|
|
|
52
52
|
tool_search_call: "tool_search_output",
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
const RESULT_TYPE_BY_CALL_TYPE = TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE;
|
|
55
56
|
const RESULT_TYPES = new Set(Object.values(RESULT_TYPE_BY_CALL_TYPE));
|
|
56
57
|
|
|
57
58
|
function itemType(item: unknown): string | undefined {
|