@opengeni/runtime 0.6.0 → 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.
Files changed (44) hide show
  1. package/dist/{chunk-BHA2SU3H.js → chunk-RYDCVTMW.js} +1959 -208
  2. package/dist/chunk-RYDCVTMW.js.map +1 -0
  3. package/dist/{index-otZkoLYq.d.ts → index-DIiiwQY2.d.ts} +693 -55
  4. package/dist/index.d.ts +293 -121
  5. package/dist/index.js +1819 -786
  6. package/dist/index.js.map +1 -1
  7. package/dist/sandbox/index.d.ts +1 -1
  8. package/dist/sandbox/index.js +79 -3
  9. package/package.json +21 -20
  10. package/src/codex-tool-search.ts +185 -31
  11. package/src/context-compaction.ts +78 -243
  12. package/src/history-sanitizer.ts +172 -20
  13. package/src/index.ts +2573 -897
  14. package/src/metrics.ts +28 -2
  15. package/src/sandbox/capabilities.ts +12 -3
  16. package/src/sandbox/channel-a.ts +420 -72
  17. package/src/sandbox/display-stack.ts +18 -4
  18. package/src/sandbox/errors.ts +13 -0
  19. package/src/sandbox/index.ts +398 -112
  20. package/src/sandbox/op-correlation.ts +70 -0
  21. package/src/sandbox/providers/index.ts +9 -3
  22. package/src/sandbox/providers/modal.ts +99 -24
  23. package/src/sandbox/providers/selfhosted.ts +1 -4
  24. package/src/sandbox/recording.ts +146 -90
  25. package/src/sandbox/routing/backend-resolver.ts +144 -10
  26. package/src/sandbox/routing/routing-session.ts +129 -25
  27. package/src/sandbox/select.ts +60 -8
  28. package/src/sandbox/selfhosted/capabilities.ts +59 -25
  29. package/src/sandbox/selfhosted/control-rpc.ts +160 -28
  30. package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
  31. package/src/sandbox/selfhosted/op-observer.ts +54 -0
  32. package/src/sandbox/selfhosted/op-stream.ts +801 -0
  33. package/src/sandbox/selfhosted/op-testing.ts +468 -0
  34. package/src/sandbox/selfhosted/op-transport.ts +151 -0
  35. package/src/sandbox/selfhosted/retry-policy.ts +177 -0
  36. package/src/sandbox/selfhosted/session.ts +498 -55
  37. package/src/sandbox/selfhosted/testing.ts +60 -19
  38. package/src/sandbox/stream-port.ts +17 -5
  39. package/src/sandbox/stream-token.ts +8 -2
  40. package/src/sandbox/terminal-server.ts +18 -4
  41. package/src/sandbox-computer.ts +332 -70
  42. package/src/usage-telemetry.ts +73 -0
  43. package/dist/chunk-BHA2SU3H.js.map +0 -1
  44. package/src/image-history.ts +0 -149
@@ -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 { call_id?: unknown; callId?: unknown } | null | undefined;
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.slice();
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 items.filter((_item, index) => !dropped.has(index));
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 = !!providerData && typeof providerData === "object"
267
- && ("encryptedContent" in (providerData as Record<string, unknown>)
268
- || "encrypted_content" in (providerData as Record<string, unknown>));
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 — the run-state REPLAY paths). The approval-decision resume and the
294
- * items-mode run-state fallback replay the serialized RunState blob verbatim. That
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 (wrapper && typeof wrapper === "object" && "rawItem" in (wrapper as Record<string, unknown>)) {
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 (wrapper && typeof wrapper === "object" && "rawItem" in (wrapper as Record<string, unknown>)) {
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.5-2026-04-24): for gpt-5.5 the SDK serializes the GA computer tool as
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.5-2026-04-24 GA computer tool (`{type:"computer"}`):
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 = Array.isArray(record.actions) && (record.actions as unknown[]).length > 0
577
- ? (record.actions as unknown[])
578
- : undefined;
579
- const actions = existingActions ?? (
580
- record.action !== undefined && record.action !== null ? [record.action] : undefined
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("\"computer_call")) {
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);