@opengeni/runtime 0.2.2 → 0.3.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/dist/{chunk-2PO56VAL.js → chunk-D5KU3QUC.js} +240 -23
- package/dist/chunk-D5KU3QUC.js.map +1 -0
- package/dist/index.d.ts +106 -178
- package/dist/index.js +427 -161
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +54 -6
- package/dist/sandbox/index.js +11 -1
- package/package.json +3 -3
- package/src/context-compaction.ts +217 -348
- package/src/image-history.ts +149 -0
- package/src/index.ts +195 -38
- package/src/sandbox/display-stack.ts +96 -12
- package/src/sandbox/index.ts +72 -12
- package/src/sandbox/providers/modal.ts +225 -0
- package/src/sandbox/routing/routing-session.ts +2 -2
- package/src/sandbox/selfhosted/session.ts +21 -5
- package/src/sandbox-computer.ts +88 -26
- package/dist/chunk-2PO56VAL.js.map +0 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { AgentInputItem } from "@openai/agents";
|
|
2
|
+
|
|
3
|
+
export const SCREENSHOT_OMITTED_PLACEHOLDER =
|
|
4
|
+
"[screenshot omitted: an older desktop frame — the full image remains in the session event log]";
|
|
5
|
+
|
|
6
|
+
const DATA_IMAGE_BASE64_PATTERN = /data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=_-]+/i;
|
|
7
|
+
|
|
8
|
+
type PathSegment = string | number;
|
|
9
|
+
|
|
10
|
+
type ImageOccurrence = {
|
|
11
|
+
path: PathSegment[];
|
|
12
|
+
replacement: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ElideStaleScreenshotsResult<T> = {
|
|
16
|
+
items: T[];
|
|
17
|
+
imageCount: number;
|
|
18
|
+
elidedCount: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ElideStaleScreenshotsOptions = {
|
|
22
|
+
keepLast?: number;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function elideStaleScreenshotImages<T extends AgentInputItem>(
|
|
27
|
+
items: readonly T[],
|
|
28
|
+
options: ElideStaleScreenshotsOptions = {},
|
|
29
|
+
): ElideStaleScreenshotsResult<T> {
|
|
30
|
+
const keepLast = Math.max(0, Math.floor(options.keepLast ?? 3));
|
|
31
|
+
const placeholder = options.placeholder ?? SCREENSHOT_OMITTED_PLACEHOLDER;
|
|
32
|
+
const occurrences: ImageOccurrence[] = [];
|
|
33
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
34
|
+
collectItemImageOccurrences(items[i], [i], placeholder, occurrences);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const elidedCount = Math.max(0, occurrences.length - keepLast);
|
|
38
|
+
if (elidedCount === 0) {
|
|
39
|
+
return { items: items.slice(), imageCount: occurrences.length, elidedCount: 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cloned = structuredClone(items) as T[];
|
|
43
|
+
for (const occurrence of occurrences.slice(0, elidedCount)) {
|
|
44
|
+
setPath(cloned, occurrence.path, occurrence.replacement);
|
|
45
|
+
}
|
|
46
|
+
return { items: cloned, imageCount: occurrences.length, elidedCount };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectItemImageOccurrences(
|
|
50
|
+
item: unknown,
|
|
51
|
+
path: PathSegment[],
|
|
52
|
+
placeholder: string,
|
|
53
|
+
out: ImageOccurrence[],
|
|
54
|
+
): void {
|
|
55
|
+
if (!isRecord(item)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (item.type === "message" && (item.role === "user" || item.role === "system")) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (item.type === "computer_call_result" || item.type === "computer_call_output") {
|
|
62
|
+
collectComputerOutputImages(item, path, placeholder, out);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (item.type === "function_call_result" || item.type === "function_call_output") {
|
|
66
|
+
collectToolResultImages(item.output, [...path, "output"], placeholder, out);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectComputerOutputImages(
|
|
71
|
+
item: Record<string, unknown>,
|
|
72
|
+
path: PathSegment[],
|
|
73
|
+
placeholder: string,
|
|
74
|
+
out: ImageOccurrence[],
|
|
75
|
+
): void {
|
|
76
|
+
const output = item.output;
|
|
77
|
+
if (!isRecord(output) || output.type !== "computer_screenshot") {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const key of ["data", "image_url", "imageUrl"]) {
|
|
81
|
+
if (isImageDataUrl(output[key])) {
|
|
82
|
+
out.push({ path: [...path, "output", key], replacement: placeholder });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectToolResultImages(
|
|
89
|
+
value: unknown,
|
|
90
|
+
path: PathSegment[],
|
|
91
|
+
placeholder: string,
|
|
92
|
+
out: ImageOccurrence[],
|
|
93
|
+
): void {
|
|
94
|
+
if (typeof value === "string") {
|
|
95
|
+
if (isImageDataUrl(value)) {
|
|
96
|
+
out.push({ path, replacement: placeholder });
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
102
|
+
collectToolResultImages(value[i], [...path, i], placeholder, out);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(value)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (value.type === "input_image") {
|
|
110
|
+
for (const key of ["image", "imageUrl", "image_url"]) {
|
|
111
|
+
if (isImageDataUrl(value[key])) {
|
|
112
|
+
out.push({ path, replacement: { type: "input_text", text: placeholder } });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const key of ["content", "text", "output"]) {
|
|
118
|
+
if (key in value) {
|
|
119
|
+
collectToolResultImages(value[key], [...path, key], placeholder, out);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isImageDataUrl(value: unknown): value is string {
|
|
125
|
+
return typeof value === "string" && DATA_IMAGE_BASE64_PATTERN.test(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
129
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setPath(root: unknown, path: PathSegment[], value: unknown): void {
|
|
133
|
+
if (path.length === 0) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let cursor = root;
|
|
137
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
138
|
+
const segment = path[i]!;
|
|
139
|
+
cursor = Array.isArray(cursor)
|
|
140
|
+
? cursor[segment as number]
|
|
141
|
+
: (cursor as Record<string, unknown>)[segment as string];
|
|
142
|
+
}
|
|
143
|
+
const last = path[path.length - 1]!;
|
|
144
|
+
if (Array.isArray(cursor)) {
|
|
145
|
+
cursor[last as number] = value;
|
|
146
|
+
} else {
|
|
147
|
+
(cursor as Record<string, unknown>)[last as string] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfiguredModel, ContextCompactionMode, ModelProviderApi, ResolvedModelProvider, Settings } from "@opengeni/config";
|
|
2
|
-
import { AGENT_INSTRUCTIONS_CORE_PLACEHOLDER, collectSandboxEnvironment, contextServerCompactThreshold, firstPartyMcpBaseUrl, parseExposedPorts, resolveContextCompactionMode, resolveModelProvider, sandboxLifecycleHookIds } from "@opengeni/config";
|
|
2
|
+
import { AGENT_INSTRUCTIONS_CORE_PLACEHOLDER, collectSandboxEnvironment, contextInputBudgetTokens, contextServerCompactThreshold, firstPartyMcpBaseUrl, parseExposedPorts, resolveContextCompactionMode, resolveModelProvider, sandboxLifecycleHookIds } from "@opengeni/config";
|
|
3
3
|
import { CAPABILITY_DESCRIPTORS, isClearedRunStateBlob, signDelegatedAccessToken, type Permission, type ReasoningEffort, type ResourceRef, type SessionEventType, type ToolRef } from "@opengeni/contracts";
|
|
4
4
|
import {
|
|
5
5
|
Agent,
|
|
@@ -82,8 +82,17 @@ import { dirname, isAbsolute, join, posix as posixPath, relative } from "node:pa
|
|
|
82
82
|
import { fileURLToPath } from "node:url";
|
|
83
83
|
|
|
84
84
|
import { computerCallNormalizingFetch, normalizeComputerCallActions, sanitizeHistoryItemsForModel } from "./history-sanitizer";
|
|
85
|
+
import { elideStaleScreenshotImages } from "./image-history";
|
|
85
86
|
import { installCodexToolSearch } from "./codex-tool-search";
|
|
86
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
CompactionNeededError,
|
|
89
|
+
SUMMARY_BUFFER_TOKENS,
|
|
90
|
+
clientCompactionThresholdTokens,
|
|
91
|
+
enforceInputBudget,
|
|
92
|
+
estimateItemTokens,
|
|
93
|
+
estimateTokens,
|
|
94
|
+
renderCompactionPromptInputForChat,
|
|
95
|
+
} from "./context-compaction";
|
|
87
96
|
import {
|
|
88
97
|
createSandboxClient,
|
|
89
98
|
deserializeSandboxSessionStateEnvelope,
|
|
@@ -134,22 +143,34 @@ export type { HistoryItem } from "./history-sanitizer";
|
|
|
134
143
|
export { OpenAIChatCompletionsModel, OpenAIResponsesModel } from "@openai/agents";
|
|
135
144
|
|
|
136
145
|
export {
|
|
137
|
-
|
|
146
|
+
CompactionNeededError,
|
|
147
|
+
buildCompactionPromptInput,
|
|
148
|
+
buildCompactionReplacementHistory,
|
|
149
|
+
clientCompactionThresholdTokens,
|
|
150
|
+
decideClientCompaction,
|
|
138
151
|
enforceInputBudget,
|
|
139
152
|
buildSummaryItem,
|
|
140
|
-
|
|
153
|
+
findCompactionNeededError,
|
|
141
154
|
isCompactionSummary,
|
|
142
155
|
isUserMessage,
|
|
143
156
|
findKeepBoundary,
|
|
144
157
|
estimateTokens,
|
|
145
158
|
estimateItemTokens,
|
|
146
|
-
|
|
147
|
-
renderPrefixTranscript,
|
|
159
|
+
renderCompactionPromptInputForChat,
|
|
148
160
|
COMPACTION_SUMMARY_MARKER,
|
|
161
|
+
COMPACTION_PROMPT,
|
|
162
|
+
COMPACT_USER_MESSAGE_MAX_TOKENS,
|
|
163
|
+
CLIENT_COMPACTION_TRIGGER_FRACTION,
|
|
164
|
+
SUMMARY_BUFFER_TOKENS,
|
|
149
165
|
SUMMARY_PREFIX,
|
|
150
|
-
|
|
166
|
+
USER_MESSAGE_TRUNCATION_MARKER,
|
|
151
167
|
} from "./context-compaction";
|
|
152
|
-
export type {
|
|
168
|
+
export type { ClientCompactionDecision, CompactionItem } from "./context-compaction";
|
|
169
|
+
export {
|
|
170
|
+
elideStaleScreenshotImages,
|
|
171
|
+
SCREENSHOT_OMITTED_PLACEHOLDER,
|
|
172
|
+
} from "./image-history";
|
|
173
|
+
export type { ElideStaleScreenshotsOptions, ElideStaleScreenshotsResult } from "./image-history";
|
|
153
174
|
|
|
154
175
|
ensureReadableStreamFrom();
|
|
155
176
|
|
|
@@ -420,7 +441,7 @@ export class MultiProviderModelProvider implements ModelProvider {
|
|
|
420
441
|
|
|
421
442
|
async getModel(modelName?: string): Promise<Model> {
|
|
422
443
|
if (modelName) {
|
|
423
|
-
const resolved = resolveTurnModel(this.settings, modelName);
|
|
444
|
+
const resolved = resolveTurnModel(settingsForRunScopedModelResolution(this.settings, modelName), modelName);
|
|
424
445
|
if (resolved) {
|
|
425
446
|
// Fail-loud floor (defense in depth): a `codex/<slug>` id must only ever
|
|
426
447
|
// resolve through the synthetic codex-subscription provider (which installs
|
|
@@ -458,6 +479,27 @@ export class MultiProviderModelProvider implements ModelProvider {
|
|
|
458
479
|
}
|
|
459
480
|
}
|
|
460
481
|
|
|
482
|
+
function settingsForRunScopedModelResolution(settings: Settings, modelName: string): Settings {
|
|
483
|
+
if (modelName !== settings.openaiModel) {
|
|
484
|
+
return settings;
|
|
485
|
+
}
|
|
486
|
+
const builtinAllowed = splitOpenaiAllowedModels(settings.openaiAllowedModels);
|
|
487
|
+
const fallbackBuiltin = builtinAllowed.find((id) => id !== modelName);
|
|
488
|
+
if (!fallbackBuiltin) {
|
|
489
|
+
return settings;
|
|
490
|
+
}
|
|
491
|
+
// The worker sets runSettings.openaiModel to the turn's model. For namespaced
|
|
492
|
+
// registry ids configuredModels filters the built-in entry out, but a unique
|
|
493
|
+
// bare registry id would otherwise be claimed by the built-in only because of
|
|
494
|
+
// that per-turn override. Resolve the run-scoped router against the deployment
|
|
495
|
+
// allow-list head instead; real built-in models stay in the allow-list.
|
|
496
|
+
return builtinAllowed.includes(modelName) ? settings : { ...settings, openaiModel: fallbackBuiltin };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function splitOpenaiAllowedModels(value: string): string[] {
|
|
500
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
501
|
+
}
|
|
502
|
+
|
|
461
503
|
/**
|
|
462
504
|
* A `codex/<slug>` turn reached the model router but the workspace has no active
|
|
463
505
|
* Codex subscription connected (the worker overlay never injected the synthetic
|
|
@@ -500,10 +542,10 @@ export function configureOpenAI(settings: Settings): void {
|
|
|
500
542
|
|
|
501
543
|
/**
|
|
502
544
|
* Run the compaction summarizer as one plain, tool-less, non-streaming model
|
|
503
|
-
* call against the resolved provider. `
|
|
504
|
-
*
|
|
545
|
+
* call against the resolved provider. `input` is the active history plus
|
|
546
|
+
* Codex's checkpoint prompt. Returns the trimmed summary text, or null on any
|
|
505
547
|
* failure (the caller treats a failed summarize as "skip compaction this turn"
|
|
506
|
-
*
|
|
548
|
+
* - never fatal). The call deliberately does NOT request reasoning encryption,
|
|
507
549
|
* tools, or server-side compaction; it is a self-contained summarize.
|
|
508
550
|
*
|
|
509
551
|
* Provider-aware: the summary always runs on the SAME provider that serves the
|
|
@@ -517,22 +559,19 @@ export function configureOpenAI(settings: Settings): void {
|
|
|
517
559
|
*/
|
|
518
560
|
export async function summarizeForCompaction(
|
|
519
561
|
settings: Settings,
|
|
520
|
-
|
|
562
|
+
input: Array<Record<string, unknown>>,
|
|
521
563
|
options: { client?: OpenAI; api?: ModelProviderApi; maxOutputTokens?: number; model?: string } = {},
|
|
522
564
|
): Promise<string | null> {
|
|
523
565
|
const client = options.client ?? buildOpenAIClientFromSettings(settings);
|
|
524
566
|
const api = options.api ?? "responses";
|
|
525
567
|
const model = options.model ?? settings.openaiModel;
|
|
526
|
-
const maxTokens = options.maxOutputTokens ??
|
|
568
|
+
const maxTokens = options.maxOutputTokens ?? SUMMARY_BUFFER_TOKENS;
|
|
527
569
|
try {
|
|
528
570
|
if (api === "chat") {
|
|
529
571
|
const completion = await client.chat.completions.create({
|
|
530
572
|
model,
|
|
531
573
|
max_tokens: maxTokens,
|
|
532
|
-
messages: [
|
|
533
|
-
{ role: "system", content: messages.system },
|
|
534
|
-
{ role: "user", content: messages.user },
|
|
535
|
-
],
|
|
574
|
+
messages: [{ role: "user", content: renderCompactionPromptInputForChat(input) }],
|
|
536
575
|
} as any);
|
|
537
576
|
const text = (completion as { choices?: Array<{ message?: { content?: unknown } }> }).choices?.[0]?.message?.content;
|
|
538
577
|
const trimmed = typeof text === "string" ? text.trim() : "";
|
|
@@ -545,10 +584,7 @@ export async function summarizeForCompaction(
|
|
|
545
584
|
// built-in path (api "responses"), so gate it on the built-in provider.
|
|
546
585
|
...(settings.openaiProvider === "azure" ? {} : { store: false }),
|
|
547
586
|
max_output_tokens: maxTokens,
|
|
548
|
-
input
|
|
549
|
-
{ role: "system", content: messages.system },
|
|
550
|
-
{ role: "user", content: messages.user },
|
|
551
|
-
],
|
|
587
|
+
input,
|
|
552
588
|
} as any);
|
|
553
589
|
const text = extractResponseOutputText(response);
|
|
554
590
|
const trimmed = text.trim();
|
|
@@ -696,6 +732,14 @@ export type BuildAgentOptions = {
|
|
|
696
732
|
// restyle the persona but never drop the goal-loop contract or environment
|
|
697
733
|
// block.
|
|
698
734
|
instructionsTemplate?: string;
|
|
735
|
+
// Per-SESSION persona/system instructions (the per-agent-type prompt lever an
|
|
736
|
+
// embedding host supplies at session create). Composed AFTER the workspace
|
|
737
|
+
// instructionsTemplate + the non-bypassable CORE, so it refines the workspace
|
|
738
|
+
// persona for this one session without dropping the goal-loop/environment
|
|
739
|
+
// contract. Rides the SAME instructions channel (system-level) — NEVER a user/
|
|
740
|
+
// timeline message. Omitted ⇒ the composed instructions are byte-identical to
|
|
741
|
+
// a workspace-only persona.
|
|
742
|
+
sessionInstructions?: string;
|
|
699
743
|
// Skills delivered by enabled capability packs. They join the bundled
|
|
700
744
|
// skills in the sandbox skill index (mounted under .agents/) so
|
|
701
745
|
// skills/<name> references resolve like any other indexed skill.
|
|
@@ -778,6 +822,27 @@ export function composeAgentInstructions(template: string, workspaceEnvironment?
|
|
|
778
822
|
return core ? `${template} ${core}` : template;
|
|
779
823
|
}
|
|
780
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Appends the per-session persona instructions to the already-composed
|
|
827
|
+
* (workspace + CORE) instructions, joined by " " — exactly the join used
|
|
828
|
+
* throughout the persona composition. The session slice is intentionally LAST
|
|
829
|
+
* (session-specific refinement of the workspace persona). An absent/blank value
|
|
830
|
+
* is a no-op that returns the composed string byte-for-byte.
|
|
831
|
+
*/
|
|
832
|
+
export function appendSessionInstructions(composed: string, sessionInstructions?: string): string {
|
|
833
|
+
const trimmed = sessionInstructions?.trim();
|
|
834
|
+
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Appends the one-shot genesis title directive (genesis turn only), joined by
|
|
839
|
+
* " " and always LAST so a white-label persona template or a per-session
|
|
840
|
+
* instruction can't drop it. A no-op when the hint is absent.
|
|
841
|
+
*/
|
|
842
|
+
export function appendGenesisTitleDirective(instructions: string, genesisTitleHint?: boolean): string {
|
|
843
|
+
return genesisTitleHint ? `${instructions} ${GENESIS_TITLE_DIRECTIVE}` : instructions;
|
|
844
|
+
}
|
|
845
|
+
|
|
781
846
|
const agentFileDownloads = new WeakMap<object, SandboxFileDownload[]>();
|
|
782
847
|
const agentRepositoryCloneHooks = new WeakMap<object, SandboxLifecycleHook[]>();
|
|
783
848
|
// TOKEN-BROKER (B1): the per-turn git token seed, stashed alongside the agent's
|
|
@@ -822,9 +887,21 @@ export function buildOpenGeniAgent(settings: Settings, resources: ResourceRef[],
|
|
|
822
887
|
// ownership + workspace-environment block) at the {{core}} marker, or
|
|
823
888
|
// appends it when the template omits the marker. With the default template
|
|
824
889
|
// and no environment this is byte-identical to the historical preamble.
|
|
825
|
-
instructions:
|
|
826
|
-
|
|
827
|
-
|
|
890
|
+
// Persona composition order (all one system-level instructions string):
|
|
891
|
+
// 1. workspace instructionsTemplate (or deployment default) with the
|
|
892
|
+
// non-bypassable CORE substituted at {{core}} — composeAgentInstructions,
|
|
893
|
+
// 2. + the per-session persona instructions (session-specific, LAST so it
|
|
894
|
+
// refines the workspace persona),
|
|
895
|
+
// 3. + the one-shot genesis title directive (genesis turn only).
|
|
896
|
+
// With no session instructions and no genesis hint this is byte-identical to
|
|
897
|
+
// the historical composed instructions.
|
|
898
|
+
instructions: appendGenesisTitleDirective(
|
|
899
|
+
appendSessionInstructions(
|
|
900
|
+
composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
|
|
901
|
+
options.sessionInstructions,
|
|
902
|
+
),
|
|
903
|
+
options.genesisTitleHint,
|
|
904
|
+
),
|
|
828
905
|
modelSettings: {
|
|
829
906
|
reasoning: { effort: options.reasoningEffort ?? settings.openaiReasoningEffort, summary: "detailed" },
|
|
830
907
|
// Server-side compaction (OpenAI platform) requires store=false: the
|
|
@@ -1573,6 +1650,7 @@ export type RunAgentStreamOptions = {
|
|
|
1573
1650
|
sandboxClient?: unknown;
|
|
1574
1651
|
sandboxEnvironment?: Record<string, string>;
|
|
1575
1652
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
1653
|
+
contextCompactionSignalTokens?: () => number | null | undefined;
|
|
1576
1654
|
// OWNERSHIP INVERSION (P1.2): an externally-owned, already-live sandbox
|
|
1577
1655
|
// session resolved by the per-turn resume-by-id path. When present,
|
|
1578
1656
|
// runAgentStream does NOT build (or resume, or discard) a client — it threads
|
|
@@ -1603,6 +1681,11 @@ export type RunAgentStreamOptions = {
|
|
|
1603
1681
|
callModelInputFilter?: CallModelInputFilter;
|
|
1604
1682
|
};
|
|
1605
1683
|
|
|
1684
|
+
export type ContextRobustnessFilterOptions = {
|
|
1685
|
+
contextCompactionSignalTokens?: () => number | null | undefined;
|
|
1686
|
+
throwOnCompactionNeeded?: boolean;
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1606
1689
|
// One-shot directive appended to the agent's system prompt on the genesis turn
|
|
1607
1690
|
// (see buildOpenGeniAgent's genesisTitleHint). Delivered through the
|
|
1608
1691
|
// authoritative instructions channel so the model reliably obeys; references
|
|
@@ -1656,6 +1739,59 @@ export const normalizeComputerCallsFilter: CallModelInputFilter = ({ modelData }
|
|
|
1656
1739
|
) as unknown as AgentInputItem[],
|
|
1657
1740
|
});
|
|
1658
1741
|
|
|
1742
|
+
export function contextRobustnessFilterForSettings(
|
|
1743
|
+
settings: Settings,
|
|
1744
|
+
options: ContextRobustnessFilterOptions = {},
|
|
1745
|
+
): CallModelInputFilter {
|
|
1746
|
+
const inputBudgetTokens = modelCallBudgetTokens(settings);
|
|
1747
|
+
const clientCompactionMode = resolveContextCompactionMode(settings) === "client";
|
|
1748
|
+
const compactionThresholdTokens = clientCompactionThresholdTokens(settings);
|
|
1749
|
+
return ({ modelData }) => {
|
|
1750
|
+
const images = elideStaleScreenshotImages(modelData.input);
|
|
1751
|
+
if (images.elidedCount > 0) {
|
|
1752
|
+
console.warn(
|
|
1753
|
+
`per-call image history policy elided ${images.elidedCount} older screenshot image(s), keeping the last ${Math.min(3, images.imageCount)} full image(s)`,
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
let input = images.items;
|
|
1757
|
+
if (inputBudgetTokens !== undefined) {
|
|
1758
|
+
const guarded = enforceInputBudget(
|
|
1759
|
+
input as unknown as Array<Record<string, unknown>>,
|
|
1760
|
+
inputBudgetTokens,
|
|
1761
|
+
);
|
|
1762
|
+
if (guarded.trimmed) {
|
|
1763
|
+
console.warn(
|
|
1764
|
+
`per-call budget guard trimmed ${guarded.droppedCount} oldest history item(s) to fit input budget (${inputBudgetTokens} tokens); the over-budget model call was NOT sent`,
|
|
1765
|
+
);
|
|
1766
|
+
input = guarded.items as unknown as AgentInputItem[];
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (clientCompactionMode && options.throwOnCompactionNeeded) {
|
|
1770
|
+
const reported = options.contextCompactionSignalTokens?.();
|
|
1771
|
+
const hasReported = typeof reported === "number" && reported > 0;
|
|
1772
|
+
const signalTokens = hasReported
|
|
1773
|
+
? reported
|
|
1774
|
+
: estimateTokens(input as unknown as Array<Record<string, unknown>>);
|
|
1775
|
+
if (signalTokens > compactionThresholdTokens) {
|
|
1776
|
+
throw new CompactionNeededError({
|
|
1777
|
+
signalTokens,
|
|
1778
|
+
thresholdTokens: compactionThresholdTokens,
|
|
1779
|
+
signalSource: hasReported ? "provider" : "estimate",
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return { ...modelData, input };
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function modelCallBudgetTokens(settings: Settings): number | undefined {
|
|
1788
|
+
if (resolveContextCompactionMode(settings) !== "client") {
|
|
1789
|
+
return undefined;
|
|
1790
|
+
}
|
|
1791
|
+
const budget = contextInputBudgetTokens(settings);
|
|
1792
|
+
return budget > 0 ? budget : undefined;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1659
1795
|
/**
|
|
1660
1796
|
* Compose a list of callModelInputFilters into one, applied left-to-right so
|
|
1661
1797
|
* each sees the prior filter's output.
|
|
@@ -1674,13 +1810,18 @@ function composeCallModelInputFilters(filters: CallModelInputFilter[]): CallMode
|
|
|
1674
1810
|
* The model-input filter applied before every model call. The computer_call
|
|
1675
1811
|
* action/actions normalizer is ALWAYS on (the Azure endpoint 400s without it);
|
|
1676
1812
|
* the provider-item-id strip is layered on top when the configured policy
|
|
1677
|
-
* selects it
|
|
1813
|
+
* selects it; the context-robustness guard then elides stale screenshots on
|
|
1814
|
+
* every mode and applies hard budget trimming only on the client-compaction path.
|
|
1678
1815
|
*/
|
|
1679
|
-
export function callModelInputFilterForSettings(
|
|
1816
|
+
export function callModelInputFilterForSettings(
|
|
1817
|
+
settings: Settings,
|
|
1818
|
+
options: ContextRobustnessFilterOptions = {},
|
|
1819
|
+
): CallModelInputFilter | undefined {
|
|
1680
1820
|
const filters: CallModelInputFilter[] = [normalizeComputerCallsFilter];
|
|
1681
1821
|
if (settings.openaiProviderItemIds === "strip") {
|
|
1682
1822
|
filters.push(stripProviderItemIdsFilter);
|
|
1683
1823
|
}
|
|
1824
|
+
filters.push(contextRobustnessFilterForSettings(settings, options));
|
|
1684
1825
|
return composeCallModelInputFilters(filters);
|
|
1685
1826
|
}
|
|
1686
1827
|
|
|
@@ -1759,7 +1900,15 @@ export async function runAgentStream(agent: Agent<any, any>, input: PreparedAgen
|
|
|
1759
1900
|
// through the client during this run (it is inert for the provided session).
|
|
1760
1901
|
const decoratedClient = withSandboxLifecycleHooks(resourceClient, ownedHooks, ownedHookContext);
|
|
1761
1902
|
const ownedFilter = composeCallModelInputFilters(
|
|
1762
|
-
[
|
|
1903
|
+
[
|
|
1904
|
+
callModelInputFilterForSettings(settings, {
|
|
1905
|
+
throwOnCompactionNeeded: Boolean(overrides.contextCompactionSignalTokens),
|
|
1906
|
+
...(overrides.contextCompactionSignalTokens
|
|
1907
|
+
? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens }
|
|
1908
|
+
: {}),
|
|
1909
|
+
}),
|
|
1910
|
+
overrides.callModelInputFilter,
|
|
1911
|
+
].filter(
|
|
1763
1912
|
(f): f is CallModelInputFilter => Boolean(f),
|
|
1764
1913
|
),
|
|
1765
1914
|
);
|
|
@@ -1806,23 +1955,31 @@ export async function runAgentStream(agent: Agent<any, any>, input: PreparedAgen
|
|
|
1806
1955
|
?? (prepared.serializedRunStateForSandbox && client
|
|
1807
1956
|
? await restoredSandboxSessionState(await RunState.fromString(agent, prepared.serializedRunStateForSandbox), client)
|
|
1808
1957
|
: undefined);
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
// model input
|
|
1958
|
+
// Apply the built-in per-call filters (computer-call normalization, optional
|
|
1959
|
+
// provider-id stripping, image/budget guard), then any per-turn filter
|
|
1960
|
+
// (genesis title directive). A callModelInputFilter only shapes the per-call
|
|
1961
|
+
// model input; the SDK persists filtered clones into its session view, while
|
|
1962
|
+
// OpenGeni's durable conversation truth is still reconciled explicitly below.
|
|
1813
1963
|
const callModelInputFilter = composeCallModelInputFilters(
|
|
1814
|
-
[
|
|
1964
|
+
[
|
|
1965
|
+
callModelInputFilterForSettings(settings, {
|
|
1966
|
+
throwOnCompactionNeeded: Boolean(overrides.contextCompactionSignalTokens),
|
|
1967
|
+
...(overrides.contextCompactionSignalTokens
|
|
1968
|
+
? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens }
|
|
1969
|
+
: {}),
|
|
1970
|
+
}),
|
|
1971
|
+
overrides.callModelInputFilter,
|
|
1972
|
+
].filter(
|
|
1815
1973
|
(f): f is CallModelInputFilter => Boolean(f),
|
|
1816
1974
|
),
|
|
1817
1975
|
);
|
|
1818
1976
|
const runOptions: Parameters<typeof run>[2] = {
|
|
1819
1977
|
stream: true,
|
|
1820
1978
|
maxTurns: settings.agentMaxModelCallsPerTurn,
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
//
|
|
1824
|
-
//
|
|
1825
|
-
// id 'rs_…' not found"; with the ids gone the request is self-contained.
|
|
1979
|
+
// Built-in per-call guard chain: normalize computer calls, optionally strip
|
|
1980
|
+
// provider ids, elide stale screenshots in every mode, and trim to the input
|
|
1981
|
+
// budget on the client-compaction path. This runs for turn-start replay AND
|
|
1982
|
+
// every mid-turn follow-up.
|
|
1826
1983
|
callModelInputFilter,
|
|
1827
1984
|
};
|
|
1828
1985
|
void settings.disableOpenaiTracing;
|