@opengeni/runtime 0.6.1 → 0.7.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-DYBF7ZEB.js → chunk-RYDCVTMW.js} +1958 -207
- package/dist/chunk-RYDCVTMW.js.map +1 -0
- package/dist/{index-BTo_bZvN.d.ts → index-DIiiwQY2.d.ts} +689 -52
- package/dist/index.d.ts +293 -123
- package/dist/index.js +1819 -790
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +79 -3
- package/package.json +21 -20
- package/src/codex-tool-search.ts +185 -31
- package/src/context-compaction.ts +78 -243
- package/src/history-sanitizer.ts +172 -20
- package/src/index.ts +2573 -898
- package/src/metrics.ts +28 -2
- package/src/sandbox/capabilities.ts +12 -3
- package/src/sandbox/channel-a.ts +420 -72
- package/src/sandbox/display-stack.ts +18 -4
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +398 -112
- package/src/sandbox/op-correlation.ts +70 -0
- package/src/sandbox/providers/index.ts +9 -3
- package/src/sandbox/providers/modal.ts +84 -14
- package/src/sandbox/providers/selfhosted.ts +1 -4
- package/src/sandbox/recording.ts +146 -90
- package/src/sandbox/routing/backend-resolver.ts +144 -10
- package/src/sandbox/routing/routing-session.ts +129 -25
- package/src/sandbox/select.ts +60 -8
- package/src/sandbox/selfhosted/capabilities.ts +59 -25
- package/src/sandbox/selfhosted/control-rpc.ts +160 -28
- package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
- package/src/sandbox/selfhosted/op-observer.ts +54 -0
- package/src/sandbox/selfhosted/op-stream.ts +801 -0
- package/src/sandbox/selfhosted/op-testing.ts +468 -0
- package/src/sandbox/selfhosted/op-transport.ts +151 -0
- package/src/sandbox/selfhosted/retry-policy.ts +177 -0
- package/src/sandbox/selfhosted/session.ts +498 -55
- package/src/sandbox/selfhosted/testing.ts +60 -19
- package/src/sandbox/stream-port.ts +17 -5
- package/src/sandbox/stream-token.ts +8 -2
- package/src/sandbox/terminal-server.ts +18 -4
- package/src/sandbox-computer.ts +332 -70
- package/src/usage-telemetry.ts +73 -0
- package/dist/chunk-DYBF7ZEB.js.map +0 -1
- package/src/image-history.ts +0 -149
- package/src/screenshot-omitted-card.ts +0 -15
package/src/history-sanitizer.ts
CHANGED
|
@@ -84,7 +84,10 @@ function callIdOf(item: unknown): string | undefined {
|
|
|
84
84
|
// (mirrors the SDK's getToolSearchProviderCallId: providerData.call_id ??
|
|
85
85
|
// providerData.callId ?? call_id ?? callId). Harmless for other item kinds —
|
|
86
86
|
// their ids never live there.
|
|
87
|
-
const provider = record.providerData as
|
|
87
|
+
const provider = record.providerData as
|
|
88
|
+
| { call_id?: unknown; callId?: unknown }
|
|
89
|
+
| null
|
|
90
|
+
| undefined;
|
|
88
91
|
if (provider && typeof provider === "object") {
|
|
89
92
|
if (typeof provider.call_id === "string" && provider.call_id.length > 0) {
|
|
90
93
|
return provider.call_id;
|
|
@@ -96,6 +99,98 @@ function callIdOf(item: unknown): string | undefined {
|
|
|
96
99
|
return undefined;
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Return the normalized local path from a sandbox `view_image` function call.
|
|
104
|
+
* Invalid/partial calls are deliberately ignored: the filter below only
|
|
105
|
+
* removes fully-paired calls whose replacement is known to be valid.
|
|
106
|
+
*/
|
|
107
|
+
function viewImagePathOf(item: unknown): string | undefined {
|
|
108
|
+
if (!item || typeof item !== "object") {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const record = item as { type?: unknown; name?: unknown; arguments?: unknown };
|
|
112
|
+
if (
|
|
113
|
+
record.type !== "function_call" ||
|
|
114
|
+
record.name !== "view_image" ||
|
|
115
|
+
typeof record.arguments !== "string"
|
|
116
|
+
) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(record.arguments) as { path?: unknown };
|
|
121
|
+
return typeof parsed.path === "string" && parsed.path.length > 0 ? parsed.path : undefined;
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remove superseded `view_image` call/result pairs for the SAME local path,
|
|
129
|
+
* retaining the newest fully-paired observation. A `view_image` result can be
|
|
130
|
+
* hundreds of kilobytes of base64. When a model re-opens the same attachment,
|
|
131
|
+
* replaying every older copy on each subsequent model call grows input
|
|
132
|
+
* quadratically and can trap a turn in context-compaction/requeue recovery.
|
|
133
|
+
*
|
|
134
|
+
* This is state-based rather than count-based: every distinct path is kept,
|
|
135
|
+
* and an older observation is removed only after a newer successful pair for
|
|
136
|
+
* that exact path exists. The reasoning items immediately attached to a
|
|
137
|
+
* removed call are removed with it, preserving Responses API item validity.
|
|
138
|
+
*/
|
|
139
|
+
export function elideSupersededViewImagePairs<T extends HistoryItem>(items: readonly T[]): T[] {
|
|
140
|
+
if (items.length === 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const callIndexById = new Map<string, number>();
|
|
145
|
+
const resultIndexById = new Map<string, number>();
|
|
146
|
+
items.forEach((item, index) => {
|
|
147
|
+
const callId = callIdOf(item);
|
|
148
|
+
if (!callId) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (itemType(item) === "function_call") {
|
|
152
|
+
callIndexById.set(callId, index);
|
|
153
|
+
} else if (
|
|
154
|
+
itemType(item) === "function_call_result" ||
|
|
155
|
+
itemType(item) === "function_call_output"
|
|
156
|
+
) {
|
|
157
|
+
resultIndexById.set(callId, index);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const pairedByPath = new Map<string, Array<{ callIndex: number; resultIndex: number }>>();
|
|
162
|
+
for (const [callId, callIndex] of callIndexById) {
|
|
163
|
+
const path = viewImagePathOf(items[callIndex]);
|
|
164
|
+
const resultIndex = resultIndexById.get(callId);
|
|
165
|
+
if (!path || resultIndex === undefined || resultIndex <= callIndex) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const pairs = pairedByPath.get(path) ?? [];
|
|
169
|
+
pairs.push({ callIndex, resultIndex });
|
|
170
|
+
pairedByPath.set(path, pairs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const dropped = new Set<number>();
|
|
174
|
+
for (const pairs of pairedByPath.values()) {
|
|
175
|
+
if (pairs.length < 2) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
pairs.sort((left, right) => left.callIndex - right.callIndex);
|
|
179
|
+
for (const pair of pairs.slice(0, -1)) {
|
|
180
|
+
dropped.add(pair.callIndex);
|
|
181
|
+
dropped.add(pair.resultIndex);
|
|
182
|
+
for (let index = pair.callIndex - 1; index >= 0; index -= 1) {
|
|
183
|
+
if (itemType(items[index]) !== "reasoning") {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
dropped.add(index);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return dropped.size === 0 ? items.slice() : items.filter((_item, index) => !dropped.has(index));
|
|
192
|
+
}
|
|
193
|
+
|
|
99
194
|
/**
|
|
100
195
|
* Sanitize a replayed history item list into a sequence the Responses API
|
|
101
196
|
* accepts. Pure: returns a new array of the same item references in order,
|
|
@@ -220,9 +315,56 @@ export function sanitizeHistoryItemsForModel<T extends HistoryItem>(items: reado
|
|
|
220
315
|
}
|
|
221
316
|
|
|
222
317
|
if (dropped.size === 0) {
|
|
223
|
-
return items.
|
|
318
|
+
return items.map(stripInternalResumeMarker);
|
|
319
|
+
}
|
|
320
|
+
return items.filter((_item, index) => !dropped.has(index)).map(stripInternalResumeMarker);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Internal marker stamping machine-generated resume/continuation messages
|
|
325
|
+
* (turn-resumed notices, goal continuations) so compaction housekeeping can
|
|
326
|
+
* recognize them in STORED history. Lives here (not context-compaction, which
|
|
327
|
+
* imports from this module) so the wire sanitizer below can strip it without
|
|
328
|
+
* an import cycle; context-compaction re-exports it.
|
|
329
|
+
*/
|
|
330
|
+
export const INTERNAL_RESUME_MESSAGE_MARKER = "opengeni_internal_resume";
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Remove the {@link INTERNAL_RESUME_MESSAGE_MARKER} providerData key from a
|
|
334
|
+
* single item before it reaches ANY model wire. Pure + non-mutating; returns
|
|
335
|
+
* the SAME reference when there is nothing to strip (keeping the common path
|
|
336
|
+
* byte-identical, mirroring {@link stripReasoningEncryptedContent}).
|
|
337
|
+
*
|
|
338
|
+
* WHY. The @openai/agents SDK serializes providerData keys verbatim into the
|
|
339
|
+
* request item, and strict Responses backends reject unknown per-item fields —
|
|
340
|
+
* observed in production as `400 Unknown parameter:
|
|
341
|
+
* 'input[N].opengeni_internal_resume'`, which deterministically failed every
|
|
342
|
+
* turn whose replayed history contained a marked resume message (the marker is
|
|
343
|
+
* durable in conversation truth, so retries could never succeed). Detection
|
|
344
|
+
* (isInternalResumeMessage) reads STORED items and also keeps a text-prefix
|
|
345
|
+
* fallback, so stripping at the wire degrades housekeeping gracefully and
|
|
346
|
+
* never the turn.
|
|
347
|
+
*/
|
|
348
|
+
export function stripInternalResumeMarker<T extends HistoryItem>(item: T): T {
|
|
349
|
+
const providerData = (item as Record<string, unknown>).providerData;
|
|
350
|
+
if (
|
|
351
|
+
!providerData ||
|
|
352
|
+
typeof providerData !== "object" ||
|
|
353
|
+
!(INTERNAL_RESUME_MESSAGE_MARKER in (providerData as Record<string, unknown>))
|
|
354
|
+
) {
|
|
355
|
+
return item;
|
|
356
|
+
}
|
|
357
|
+
const { [INTERNAL_RESUME_MESSAGE_MARKER]: _dropped, ...rest } = providerData as Record<
|
|
358
|
+
string,
|
|
359
|
+
unknown
|
|
360
|
+
>;
|
|
361
|
+
const next: Record<string, unknown> = { ...(item as Record<string, unknown>) };
|
|
362
|
+
if (Object.keys(rest).length > 0) {
|
|
363
|
+
next.providerData = rest;
|
|
364
|
+
} else {
|
|
365
|
+
delete next.providerData;
|
|
224
366
|
}
|
|
225
|
-
return
|
|
367
|
+
return next as T;
|
|
226
368
|
}
|
|
227
369
|
|
|
228
370
|
/**
|
|
@@ -263,9 +405,11 @@ export function stripReasoningEncryptedContent<T extends HistoryItem>(item: T):
|
|
|
263
405
|
}
|
|
264
406
|
const record = item as Record<string, unknown>;
|
|
265
407
|
const providerData = record.providerData;
|
|
266
|
-
const providerHasBlob =
|
|
267
|
-
|
|
268
|
-
|
|
408
|
+
const providerHasBlob =
|
|
409
|
+
!!providerData &&
|
|
410
|
+
typeof providerData === "object" &&
|
|
411
|
+
("encryptedContent" in (providerData as Record<string, unknown>) ||
|
|
412
|
+
"encrypted_content" in (providerData as Record<string, unknown>));
|
|
269
413
|
const topLevelHasBlob = "encrypted_content" in record;
|
|
270
414
|
if (!providerHasBlob && !topLevelHasBlob) {
|
|
271
415
|
// Nothing encrypted to strip — return the same reference (byte-identical).
|
|
@@ -290,9 +434,8 @@ export function stripReasoningEncryptedContent<T extends HistoryItem>(item: T):
|
|
|
290
434
|
* a parse failure or a no-op returns the SAME string reference (so an unchanged
|
|
291
435
|
* or non-codex run-state replays byte-for-byte).
|
|
292
436
|
*
|
|
293
|
-
* WHY (HOLE C —
|
|
294
|
-
*
|
|
295
|
-
* blob round-trips `reasoning.encrypted_content` minted by the ChatGPT/Codex
|
|
437
|
+
* WHY (HOLE C — approval replay). An approval decision resumes the serialized
|
|
438
|
+
* RunState blob. That blob round-trips `reasoning.encrypted_content` minted by the ChatGPT/Codex
|
|
296
439
|
* backend (bound to the freezing account/org — a foreign account 400s it) AND the
|
|
297
440
|
* foreign `rs_…` reasoning ids the Responses backend validates (rejected once the
|
|
298
441
|
* blob is gone). Unlike `session_history_items`, the blob carries NO per-item
|
|
@@ -368,7 +511,11 @@ export function stripReasoningIdentityFromSerializedRunState(serialized: string)
|
|
|
368
511
|
// protocol reasoning shape under `rawItem`.
|
|
369
512
|
if (Array.isArray(root.generatedItems)) {
|
|
370
513
|
for (const wrapper of root.generatedItems) {
|
|
371
|
-
if (
|
|
514
|
+
if (
|
|
515
|
+
wrapper &&
|
|
516
|
+
typeof wrapper === "object" &&
|
|
517
|
+
"rawItem" in (wrapper as Record<string, unknown>)
|
|
518
|
+
) {
|
|
372
519
|
scrubReasoning((wrapper as Record<string, unknown>).rawItem);
|
|
373
520
|
}
|
|
374
521
|
}
|
|
@@ -452,7 +599,11 @@ export function neutralizeToolSearchItemsInSerializedRunState(serialized: string
|
|
|
452
599
|
neutralizeArray(root.originalInput);
|
|
453
600
|
if (Array.isArray(root.generatedItems)) {
|
|
454
601
|
for (const wrapper of root.generatedItems) {
|
|
455
|
-
if (
|
|
602
|
+
if (
|
|
603
|
+
wrapper &&
|
|
604
|
+
typeof wrapper === "object" &&
|
|
605
|
+
"rawItem" in (wrapper as Record<string, unknown>)
|
|
606
|
+
) {
|
|
456
607
|
neutralize((wrapper as Record<string, unknown>).rawItem);
|
|
457
608
|
}
|
|
458
609
|
}
|
|
@@ -490,7 +641,7 @@ export function neutralizeToolSearchItemsInSerializedRunState(serialized: string
|
|
|
490
641
|
* call carrying `action:{type:"screenshot"}` AND `actions:[{type:"screenshot"}]`).
|
|
491
642
|
*
|
|
492
643
|
* Which singular do we keep? LIVE-PROVEN against the deployed Azure deployment
|
|
493
|
-
* (gpt-5.
|
|
644
|
+
* (gpt-5.6-sol-2026-04-24): for gpt-5.6-sol the SDK serializes the GA computer tool as
|
|
494
645
|
* `{type:"computer"}` (not the legacy `computer_use_preview`), and that GA tool
|
|
495
646
|
* accepts ONLY the batched plural `actions`. Probing all three shapes:
|
|
496
647
|
* - `action`-only -> 400 "exactly one of action or actions" (STILL rejected)
|
|
@@ -538,7 +689,7 @@ export function normalizeComputerCallActions<T extends HistoryItem>(items: reado
|
|
|
538
689
|
* payload from the item: when `actions` is present it emits BOTH
|
|
539
690
|
* `{action: ..., actions: [...]}`, and when only `action` is present it emits
|
|
540
691
|
* `action`-only. It can NEVER emit actions-only. Probed live against the
|
|
541
|
-
* deployed Azure gpt-5.
|
|
692
|
+
* deployed Azure gpt-5.6-sol-2026-04-24 GA computer tool (`{type:"computer"}`):
|
|
542
693
|
* - `action`-only -> 400 "Computer call input must include exactly one of
|
|
543
694
|
* `action` or `actions`." (rejected)
|
|
544
695
|
* - both -> 400 same message (rejected)
|
|
@@ -573,12 +724,13 @@ export function rewriteComputerCallsToActionsOnly(body: unknown): boolean {
|
|
|
573
724
|
if (record.type !== "computer_call") {
|
|
574
725
|
continue;
|
|
575
726
|
}
|
|
576
|
-
const existingActions =
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
727
|
+
const existingActions =
|
|
728
|
+
Array.isArray(record.actions) && (record.actions as unknown[]).length > 0
|
|
729
|
+
? (record.actions as unknown[])
|
|
730
|
+
: undefined;
|
|
731
|
+
const actions =
|
|
732
|
+
existingActions ??
|
|
733
|
+
(record.action !== undefined && record.action !== null ? [record.action] : undefined);
|
|
582
734
|
if (actions === undefined) {
|
|
583
735
|
// Neither action nor actions present: nothing to normalize.
|
|
584
736
|
continue;
|
|
@@ -700,7 +852,7 @@ export function computerCallNormalizingFetch(base: FetchLike): FetchLike {
|
|
|
700
852
|
// Match any request that mentions either `computer_call` (the action call) or
|
|
701
853
|
// `computer_call_output` (the output/result item). Both strings begin with
|
|
702
854
|
// `"computer_call` so a single prefix-substring check covers both.
|
|
703
|
-
if (init && typeof init.body === "string" && init.body.includes("
|
|
855
|
+
if (init && typeof init.body === "string" && init.body.includes('"computer_call')) {
|
|
704
856
|
try {
|
|
705
857
|
const parsed = JSON.parse(init.body) as unknown;
|
|
706
858
|
const changed1 = rewriteComputerCallsToActionsOnly(parsed);
|