@opengeni/runtime 0.14.1 → 0.14.15

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 (79) hide show
  1. package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
  2. package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
  3. package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
  4. package/dist/chunk-GNEGANUF.js.map +1 -0
  5. package/dist/codex-tool-search.d.ts +100 -0
  6. package/dist/context-compaction.d.ts +236 -0
  7. package/dist/history-sanitizer.d.ts +320 -0
  8. package/dist/index.d.ts +186 -718
  9. package/dist/index.js +612 -126
  10. package/dist/index.js.map +1 -1
  11. package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
  12. package/dist/mcp-network.d.ts +32 -35
  13. package/dist/metrics.d.ts +33 -0
  14. package/dist/sandbox/capabilities.d.ts +11 -0
  15. package/dist/sandbox/channel-a.d.ts +226 -0
  16. package/dist/sandbox/display-stack.d.ts +99 -0
  17. package/dist/sandbox/errors.d.ts +16 -0
  18. package/dist/sandbox/exec-banner.d.ts +19 -0
  19. package/dist/sandbox/index.d.ts +190 -6
  20. package/dist/sandbox/index.js +20 -4
  21. package/dist/sandbox/op-correlation.d.ts +24 -0
  22. package/dist/sandbox/provider-errors.d.ts +32 -0
  23. package/dist/sandbox/provider-operation-gate.d.ts +12 -0
  24. package/dist/sandbox/providers/blaxel.d.ts +2 -0
  25. package/dist/sandbox/providers/cloudflare.d.ts +2 -0
  26. package/dist/sandbox/providers/daytona.d.ts +2 -0
  27. package/dist/sandbox/providers/docker.d.ts +2 -0
  28. package/dist/sandbox/providers/e2b.d.ts +2 -0
  29. package/dist/sandbox/providers/index.d.ts +11 -0
  30. package/dist/sandbox/providers/local.d.ts +2 -0
  31. package/dist/sandbox/providers/modal.d.ts +132 -0
  32. package/dist/sandbox/providers/none.d.ts +2 -0
  33. package/dist/sandbox/providers/runloop.d.ts +2 -0
  34. package/dist/sandbox/providers/selfhosted.d.ts +2 -0
  35. package/dist/sandbox/providers/types.d.ts +30 -0
  36. package/dist/sandbox/providers/vercel.d.ts +2 -0
  37. package/dist/sandbox/recording.d.ts +80 -0
  38. package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
  39. package/dist/sandbox/routing/routing-session.d.ts +401 -0
  40. package/dist/sandbox/run-credentials.d.ts +65 -0
  41. package/dist/sandbox/select.d.ts +120 -0
  42. package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
  43. package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
  44. package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
  45. package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
  46. package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
  47. package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
  48. package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
  49. package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
  50. package/dist/sandbox/selfhosted/session.d.ts +552 -0
  51. package/dist/sandbox/selfhosted/testing.d.ts +48 -0
  52. package/dist/sandbox/stream-port.d.ts +80 -0
  53. package/dist/sandbox/stream-token.d.ts +44 -0
  54. package/dist/sandbox/terminal-server.d.ts +67 -0
  55. package/dist/sandbox/toolspace-token.d.ts +23 -0
  56. package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
  57. package/dist/sandbox/workspace-archive.d.ts +39 -0
  58. package/dist/sandbox-computer.d.ts +227 -0
  59. package/dist/screenshot-error-card.d.ts +1 -0
  60. package/dist/skill-library.d.ts +10 -12
  61. package/dist/usage-telemetry.d.ts +45 -0
  62. package/dist/workspace-skills.d.ts +31 -0
  63. package/package.json +7 -7
  64. package/src/context-compaction.ts +600 -6
  65. package/src/history-sanitizer.ts +10 -92
  66. package/src/index.ts +301 -62
  67. package/src/sandbox/channel-a.ts +267 -81
  68. package/src/sandbox/index.ts +56 -94
  69. package/src/sandbox/provider-operation-gate.ts +110 -0
  70. package/src/sandbox/providers/docker.ts +13 -1
  71. package/src/sandbox/providers/modal.ts +433 -3
  72. package/src/sandbox/routing/routing-session.ts +84 -8
  73. package/src/sandbox/selfhosted/session.ts +7 -6
  74. package/src/sandbox/turn-tool-cancellation.ts +47 -7
  75. package/src/sandbox/workspace-archive.ts +176 -73
  76. package/src/sandbox-computer.ts +25 -75
  77. package/dist/chunk-HHWIZHQM.js.map +0 -1
  78. package/dist/index-BraazrAO.d.ts +0 -2931
  79. /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
@@ -36,6 +36,16 @@ import {
36
36
  /** A history item is any JSON object; we only inspect a few discriminator fields. */
37
37
  export type HistoryItem = Record<string, unknown>;
38
38
 
39
+ /**
40
+ * @deprecated Model-visible history is append-only outside the explicit
41
+ * durable compaction transition. This compatibility identity function performs
42
+ * no elision and returns the exact input array with every item untouched and in
43
+ * its original order.
44
+ */
45
+ export function elideSupersededViewImagePairs<T extends HistoryItem>(items: readonly T[]): T[] {
46
+ return items as T[];
47
+ }
48
+
39
49
  /**
40
50
  * Tool-call item types and the result-item type that settles them. Kept in
41
51
  * sync with the SDK's `TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE`; `function_call` is
@@ -103,98 +113,6 @@ function callIdOf(item: unknown): string | undefined {
103
113
  return undefined;
104
114
  }
105
115
 
106
- /**
107
- * Return the normalized local path from a sandbox `view_image` function call.
108
- * Invalid/partial calls are deliberately ignored: the filter below only
109
- * removes fully-paired calls whose replacement is known to be valid.
110
- */
111
- function viewImagePathOf(item: unknown): string | undefined {
112
- if (!item || typeof item !== "object") {
113
- return undefined;
114
- }
115
- const record = item as { type?: unknown; name?: unknown; arguments?: unknown };
116
- if (
117
- record.type !== "function_call" ||
118
- record.name !== "view_image" ||
119
- typeof record.arguments !== "string"
120
- ) {
121
- return undefined;
122
- }
123
- try {
124
- const parsed = JSON.parse(record.arguments) as { path?: unknown };
125
- return typeof parsed.path === "string" && parsed.path.length > 0 ? parsed.path : undefined;
126
- } catch {
127
- return undefined;
128
- }
129
- }
130
-
131
- /**
132
- * Remove superseded `view_image` call/result pairs for the SAME local path,
133
- * retaining the newest fully-paired observation. A `view_image` result can be
134
- * hundreds of kilobytes of base64. When a model re-opens the same attachment,
135
- * replaying every older copy on each subsequent model call grows input
136
- * quadratically and can trap a turn in context-compaction/requeue recovery.
137
- *
138
- * This is state-based rather than count-based: every distinct path is kept,
139
- * and an older observation is removed only after a newer successful pair for
140
- * that exact path exists. The reasoning items immediately attached to a
141
- * removed call are removed with it, preserving Responses API item validity.
142
- */
143
- export function elideSupersededViewImagePairs<T extends HistoryItem>(items: readonly T[]): T[] {
144
- if (items.length === 0) {
145
- return [];
146
- }
147
-
148
- const callIndexById = new Map<string, number>();
149
- const resultIndexById = new Map<string, number>();
150
- items.forEach((item, index) => {
151
- const callId = callIdOf(item);
152
- if (!callId) {
153
- return;
154
- }
155
- if (itemType(item) === "function_call") {
156
- callIndexById.set(callId, index);
157
- } else if (
158
- itemType(item) === "function_call_result" ||
159
- itemType(item) === "function_call_output"
160
- ) {
161
- resultIndexById.set(callId, index);
162
- }
163
- });
164
-
165
- const pairedByPath = new Map<string, Array<{ callIndex: number; resultIndex: number }>>();
166
- for (const [callId, callIndex] of callIndexById) {
167
- const path = viewImagePathOf(items[callIndex]);
168
- const resultIndex = resultIndexById.get(callId);
169
- if (!path || resultIndex === undefined || resultIndex <= callIndex) {
170
- continue;
171
- }
172
- const pairs = pairedByPath.get(path) ?? [];
173
- pairs.push({ callIndex, resultIndex });
174
- pairedByPath.set(path, pairs);
175
- }
176
-
177
- const dropped = new Set<number>();
178
- for (const pairs of pairedByPath.values()) {
179
- if (pairs.length < 2) {
180
- continue;
181
- }
182
- pairs.sort((left, right) => left.callIndex - right.callIndex);
183
- for (const pair of pairs.slice(0, -1)) {
184
- dropped.add(pair.callIndex);
185
- dropped.add(pair.resultIndex);
186
- for (let index = pair.callIndex - 1; index >= 0; index -= 1) {
187
- if (itemType(items[index]) !== "reasoning") {
188
- break;
189
- }
190
- dropped.add(index);
191
- }
192
- }
193
- }
194
-
195
- return dropped.size === 0 ? items.slice() : items.filter((_item, index) => !dropped.has(index));
196
- }
197
-
198
116
  /**
199
117
  * Sanitize a replayed history item list into a sequence the Responses API
200
118
  * accepts. Pure: returns a new array of the same item references in order,
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  type McpServerConnectionRef,
36
36
  type Permission,
37
37
  type FirstPartyMcpToolName,
38
+ type LatencyMode,
38
39
  type ReasoningEffort,
39
40
  type ResourceRef,
40
41
  type SessionEventType,
@@ -148,7 +149,6 @@ import { fileURLToPath } from "node:url";
148
149
 
149
150
  import {
150
151
  computerCallNormalizingFetch,
151
- elideSupersededViewImagePairs,
152
152
  normalizeComputerCallActions,
153
153
  sanitizeHistoryItemsForModel,
154
154
  } from "./history-sanitizer";
@@ -158,6 +158,8 @@ import {
158
158
  CompactionProviderResponseError,
159
159
  EmptyCompactionSummaryError,
160
160
  SUMMARY_BUFFER_TOKENS,
161
+ buildRemoteCompactionV2PromptInput,
162
+ extractRemoteCompactionV2OutputItem,
161
163
  compactionThresholdTokens,
162
164
  estimateCompleteModelInput,
163
165
  estimateSerializedValueTokens,
@@ -280,13 +282,24 @@ export {
280
282
  clampCompactionThresholdRatio,
281
283
  decideCompaction,
282
284
  buildSummaryItem,
285
+ buildRemoteCompactionV2PromptInput,
286
+ buildRemoteV2ReplacementHistory,
287
+ extractRemoteCompactionV2OutputItem,
283
288
  findCompactionNeededError,
284
289
  isCompactionSummary,
290
+ isRemoteCompactionItem,
291
+ isRetainedRemoteV2Message,
285
292
  latestCompactionReplacementFingerprint,
286
293
  prepareCompactionPromptInput,
287
294
  isUserMessage,
288
295
  estimateTokens,
296
+ estimateTokensBreakdown,
289
297
  estimateItemTokens,
298
+ estimateItemTokenBreakdown,
299
+ estimateOpaqueEncryptedModelVisibleBytes,
300
+ estimateOpaqueEncryptedTokens,
301
+ opaqueEncryptedContentLength,
302
+ estimateNativeImageTokens,
290
303
  estimateCompleteModelInput,
291
304
  estimateSerializedValueTokens,
292
305
  hasModelGeneratedItem,
@@ -294,15 +307,23 @@ export {
294
307
  COMPACTION_SUMMARY_MARKER,
295
308
  COMPACTION_PROMPT,
296
309
  COMPACT_USER_MESSAGE_MAX_TOKENS,
310
+ REMOTE_V2_RETAINED_MESSAGE_TOKEN_BUDGET,
311
+ REMOTE_COMPACTION_V2_IMPLEMENTATION,
312
+ REMOTE_COMPACTION_V2_BETA_FEATURE,
297
313
  DEFAULT_COMPACTION_THRESHOLD_RATIO,
298
314
  MIN_COMPACTION_THRESHOLD_RATIO,
299
315
  MAX_COMPACTION_THRESHOLD_RATIO,
300
316
  SUMMARY_BUFFER_TOKENS,
301
317
  SUMMARY_PREFIX,
302
318
  USER_MESSAGE_TRUNCATION_MARKER,
319
+ UNKNOWN_IMAGE_TOKENS,
320
+ MAX_NATIVE_IMAGE_TOKENS,
303
321
  } from "./context-compaction";
304
322
  export type {
305
323
  CompactionDecision,
324
+ ModelInputTokenBreakdown,
325
+ NativeImageEstimateReason,
326
+ NativeImageTokenEstimate,
306
327
  CompactionItem,
307
328
  PreparedCompactionPromptInput,
308
329
  } from "./context-compaction";
@@ -329,6 +350,7 @@ export type NormalizedRuntimeEvent = {
329
350
 
330
351
  export type ModelResponseUsage = {
331
352
  responseId?: string;
353
+ serviceTier?: string;
332
354
  usage: {
333
355
  inputTokens?: number;
334
356
  outputTokens?: number;
@@ -1005,6 +1027,67 @@ export async function summarizeForCompaction(
1005
1027
  return summary;
1006
1028
  }
1007
1029
 
1030
+ /**
1031
+ * Codex remote compaction v2: send active history + `compaction_trigger`, collect
1032
+ * exactly one `{ type: "compaction", encrypted_content }` output item.
1033
+ * Must run inside Codex ALS with `remote_compaction_v2` beta + turn metadata.
1034
+ */
1035
+ export async function requestRemoteCompactionV2(
1036
+ settings: Settings,
1037
+ input: Array<Record<string, unknown>>,
1038
+ options: {
1039
+ client: OpenAI;
1040
+ model: string;
1041
+ promptCacheKey?: string;
1042
+ onUsage?: (usage: ModelResponseUsage) => void | Promise<void>;
1043
+ },
1044
+ ): Promise<Record<string, unknown>> {
1045
+ const promptInput = buildRemoteCompactionV2PromptInput(input);
1046
+ const request: ModelRequest = {
1047
+ systemInstructions: "",
1048
+ input: promptInput as AgentInputItem[],
1049
+ modelSettings: {
1050
+ // Azure rejects store:false; Codex transport enforces store:false itself.
1051
+ ...(settings.openaiProvider === "azure" ? {} : { store: false }),
1052
+ ...(options.promptCacheKey
1053
+ ? { providerData: { prompt_cache_key: options.promptCacheKey } }
1054
+ : {}),
1055
+ },
1056
+ tools: [],
1057
+ toolsExplicitlyProvided: true,
1058
+ outputType: "text",
1059
+ handoffs: [],
1060
+ tracing: false,
1061
+ };
1062
+ let response: unknown;
1063
+ try {
1064
+ response = await new CompactionResponsesModel(options.client, options.model).fetchResponse(
1065
+ request,
1066
+ );
1067
+ } catch (error) {
1068
+ throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
1069
+ }
1070
+ const usage = modelResponseUsageFromResponse(response);
1071
+ if (usage) {
1072
+ await options.onUsage?.(usage);
1073
+ }
1074
+ if (isFailedCompactionProviderResponse(response)) {
1075
+ throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(response));
1076
+ }
1077
+ try {
1078
+ return extractRemoteCompactionV2OutputItem(response);
1079
+ } catch (error) {
1080
+ if (error instanceof EmptyCompactionSummaryError) {
1081
+ throw new EmptyCompactionSummaryError({
1082
+ ...compactionResponseDiagnostics(response, ""),
1083
+ ...error.diagnostics,
1084
+ stage: "remote_v2_extract",
1085
+ });
1086
+ }
1087
+ throw error;
1088
+ }
1089
+ }
1090
+
1008
1091
  function isFailedCompactionProviderResponse(response: unknown): boolean {
1009
1092
  if (!response || typeof response !== "object") return false;
1010
1093
  const record = response as Record<string, unknown>;
@@ -1293,6 +1376,10 @@ export type BuildAgentOptions = {
1293
1376
  response: HumanInputResponse;
1294
1377
  };
1295
1378
  reasoningEffort?: ReasoningEffort;
1379
+ /** Product latency selection frozen onto this turn. */
1380
+ latencyMode?: LatencyMode;
1381
+ /** Provider-specific wire value resolved by the worker (`fast` or `priority`). */
1382
+ serviceTier?: "fast" | "priority";
1296
1383
  // Per-turn gating overrides for the multi-provider path. Each defaults to
1297
1384
  // today's settings-derived behaviour when omitted, so the legacy
1298
1385
  // global-client callers (no model resolution) are byte-for-byte unchanged.
@@ -1323,8 +1410,7 @@ export type BuildAgentOptions = {
1323
1410
  // EXPLICIT computer-use tool transport, decided where provider identity is
1324
1411
  // authoritative (the worker's model resolution — agent-turn.ts). Threaded into
1325
1412
  // buildAgentCapabilities → computerUse({toolMode}) so tool selection never rests
1326
- // on the SDK's constructor-name sniff. When omitted, the legacy sniff +
1327
- // `structuredToolTransport` neutralize path is preserved byte-for-byte.
1413
+ // on the SDK's constructor-name sniff. Omitted means disabled/fail-closed.
1328
1414
  computerToolMode?: ComputerToolMode;
1329
1415
  /**
1330
1416
  * Invoked once, immediately before the first real computer action and after
@@ -1765,9 +1851,14 @@ export function buildOpenGeniAgent(
1765
1851
  // resolved provider's api/window/web-search instead.
1766
1852
  const hostedWebSearch = options.hostedWebSearch ?? settings.webSearchEnabled;
1767
1853
  const encryptedReasoning = options.encryptedReasoning ?? settings.openaiReasoningEncryptedContent;
1854
+ // Wire value must be provider-mapped by the caller (OpenAI `fast`, Azure/Codex
1855
+ // `priority`). Do not fall back to latencyMode itself — that would send
1856
+ // invalid Azure tiers.
1857
+ const serviceTier = options.serviceTier;
1768
1858
  const providerData = {
1769
1859
  ...(encryptedReasoning ? { include: ["reasoning.encrypted_content"] } : {}),
1770
1860
  ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
1861
+ ...(serviceTier ? { service_tier: serviceTier } : {}),
1771
1862
  };
1772
1863
  // Native hosted tools attached to every constructed agent. webSearchEnabled
1773
1864
  // is ON by default and provider-unconditional on the built-in path (the live
@@ -2252,10 +2343,8 @@ export function buildAgentCapabilities(
2252
2343
  sessionSkills?: PackSkill[];
2253
2344
  workspaceSkillPaths?: readonly WorkspaceSkillSearchPath[];
2254
2345
  structuredToolTransport?: boolean;
2255
- // EXPLICIT computer-use transport (see BuildAgentOptions.computerToolMode). When
2256
- // present, computerUse() is handed the mode directly and its tools() obeys it
2257
- // without the constructor-name sniff. When absent, the legacy neutralize +
2258
- // imageFunctionResults path (driven by structuredToolTransport) is unchanged.
2346
+ // EXPLICIT computer-use transport (see BuildAgentOptions.computerToolMode).
2347
+ // Omitted/unproven transport fails closed with no computer tools.
2259
2348
  computerToolMode?: ComputerToolMode;
2260
2349
  onComputerUseReady?: (session: SandboxSessionLike) => Promise<void>;
2261
2350
  turnCancellationSignal?: AbortSignal;
@@ -2328,37 +2417,14 @@ export function buildAgentCapabilities(
2328
2417
  // FUNCTION `computer_*` tools on the text transport. The ChatGPT/Codex backend
2329
2418
  // rejects hosted tool types (only function/custom/web_search accepted).
2330
2419
  //
2331
- // HARDENING: when the caller declares an EXPLICIT `computerToolMode` (the worker
2332
- // does, from its authoritative model resolution), thread it straight through —
2333
- // tool selection then never depends on the SDK's model-instance constructor-name
2334
- // sniff (which a wrapped/proxied model would defeat, silently 400ing a
2335
- // chat-completions provider handed the hosted tool). When ABSENT, the legacy path
2336
- // is preserved byte-for-byte: on the codex path (structuredToolTransport === false)
2337
- // we set imageFunctionResults and neutralize the capability's model binding — the
2338
- // SAME trick used for filesystem above — so `tools()` sees no model instance and
2339
- // emits the function tools the backend can call, instead of suppressing the tier.
2340
- const explicitMode = options.computerToolMode;
2420
+ // The worker declares an explicit mode from authoritative provider resolution.
2421
+ // Exported/public callers that omit it are unproven and therefore disabled.
2341
2422
  const computerCapability = computerUse({
2342
2423
  dimensions: [settings.streamResolutionWidth, settings.streamResolutionHeight],
2343
2424
  readOnly: settings.computerUseReadOnly,
2344
2425
  ...(options.onComputerUseReady ? { onReady: options.onComputerUseReady } : {}),
2345
- ...(explicitMode
2346
- ? { toolMode: explicitMode }
2347
- : // Legacy (no explicit mode): on the codex path the function tools deliver
2348
- // screenshots as a real image the model can see. The ChatGPT/Codex backend
2349
- // rejects HOSTED tool types but DOES accept `input_image` content items inside a
2350
- // `function_call_output` (proven by openai/codex codex-rs, whose view_image tool
2351
- // ships exactly that shape) — so a structured image tool result is seen, where a
2352
- // text data-URL would be unreadable.
2353
- options.structuredToolTransport === false
2354
- ? { imageFunctionResults: true }
2355
- : {}),
2426
+ toolMode: options.computerToolMode ?? "disabled",
2356
2427
  });
2357
- // Neutralize ONLY on the legacy sniff path. With an explicit toolMode the mode
2358
- // already forces the function tools, so the constructor-name override is moot.
2359
- if (!explicitMode && options.structuredToolTransport === false) {
2360
- neutralizeStructuredToolTransport(computerCapability);
2361
- }
2362
2428
  caps.push(computerCapability as unknown as ReturnType<typeof Capabilities.default>[number]);
2363
2429
  }
2364
2430
  if (toolCancellation) {
@@ -2993,9 +3059,12 @@ function parseWwwAuthenticate(header: string | null): {
2993
3059
  // content, erasing the failure. A JSON-RPC error survives the shim as a thrown
2994
3060
  // McpError, which PrefixedMcpServer.callTool converts back into an MCP-shaped
2995
3061
  // `{ isError: true }` output for the model.
2996
- const MCP_AUTH_NEEDED_ERROR_CODE = -32001;
2997
- const MCP_AUTH_NEEDED_MESSAGE =
2998
- "Authentication required - a connection link was posted to the session.";
3062
+ const MCP_AUTH_NEEDED_ERROR = {
3063
+ // OpenGeni application-defined JSON-RPC code. Keep this positive so it cannot
3064
+ // collide with MCP SDK transport errors such as RequestTimeout (-32001).
3065
+ code: 40_101,
3066
+ message: "Authentication required - a connection link was posted to the session.",
3067
+ } as const;
2999
3068
 
3000
3069
  function mcpToolAuthNeededResponse(id: string | number | null | undefined): Response {
3001
3070
  return new Response(
@@ -3003,8 +3072,8 @@ function mcpToolAuthNeededResponse(id: string | number | null | undefined): Resp
3003
3072
  jsonrpc: "2.0",
3004
3073
  id: id ?? null,
3005
3074
  error: {
3006
- code: MCP_AUTH_NEEDED_ERROR_CODE,
3007
- message: MCP_AUTH_NEEDED_MESSAGE,
3075
+ code: MCP_AUTH_NEEDED_ERROR.code,
3076
+ message: MCP_AUTH_NEEDED_ERROR.message,
3008
3077
  },
3009
3078
  }),
3010
3079
  {
@@ -3019,7 +3088,11 @@ function isAuthNeededMcpError(error: unknown): boolean {
3019
3088
  return false;
3020
3089
  }
3021
3090
  const code = (error as { code?: unknown }).code;
3022
- return code === MCP_AUTH_NEEDED_ERROR_CODE || error.message.includes(MCP_AUTH_NEEDED_MESSAGE);
3091
+ return (
3092
+ code === MCP_AUTH_NEEDED_ERROR.code &&
3093
+ (error.message === MCP_AUTH_NEEDED_ERROR.message ||
3094
+ error.message === `MCP error ${MCP_AUTH_NEEDED_ERROR.code}: ${MCP_AUTH_NEEDED_ERROR.message}`)
3095
+ );
3023
3096
  }
3024
3097
 
3025
3098
  // Model-facing text for a best-effort server whose tool call failed for a
@@ -3053,19 +3126,59 @@ function safeMcpErrorFields(error: unknown): {
3053
3126
  return { errorClass, status };
3054
3127
  }
3055
3128
 
3056
- function safeMcpTransportError(error: unknown): Error & { status?: number; code?: number } {
3129
+ type SafeMcpTransportError = Error & {
3130
+ status?: number;
3131
+ code?: number;
3132
+ mcpTransportFailureKind?: "request_timeout";
3133
+ };
3134
+
3135
+ /**
3136
+ * Preserve the MCP SDK's exact request-timeout meaning without retaining or
3137
+ * exposing a raw HTTP response body. The numeric code is not sufficient:
3138
+ * Streamable HTTP also uses -32001 for "Session not found" and arbitrary
3139
+ * AbortSignal reasons, so only the SDK's two owned timeout messages qualify.
3140
+ */
3141
+ export function isMcpRequestTimeoutError(error: unknown, seen = new WeakSet<object>()): boolean {
3142
+ if (!error || typeof error !== "object") {
3143
+ return false;
3144
+ }
3145
+ if (seen.has(error)) {
3146
+ return false;
3147
+ }
3148
+ seen.add(error);
3149
+ const record = error as Record<string, unknown>;
3150
+ if (record.mcpTransportFailureKind === "request_timeout") {
3151
+ return true;
3152
+ }
3153
+ const code = typeof record.code === "number" ? record.code : record.status;
3154
+ const message = typeof record.message === "string" ? record.message : "";
3155
+ const sdkTimeoutMessages = new Set([
3156
+ "Request timed out",
3157
+ "MCP error -32001: Request timed out",
3158
+ "Maximum total timeout exceeded",
3159
+ "MCP error -32001: Maximum total timeout exceeded",
3160
+ ]);
3161
+ if (code === -32_001 && sdkTimeoutMessages.has(message)) {
3162
+ return true;
3163
+ }
3164
+ return ["error", "cause", "response", "data"].some((key) =>
3165
+ isMcpRequestTimeoutError(record[key], seen),
3166
+ );
3167
+ }
3168
+
3169
+ export function safeMcpTransportError(error: unknown): SafeMcpTransportError {
3057
3170
  const fields = safeMcpErrorFields(error);
3058
3171
  const safeError = new Error(
3059
3172
  `MCP transport operation failed (${mcpErrorReason(fields)})`,
3060
- ) as Error & {
3061
- status?: number;
3062
- code?: number;
3063
- };
3173
+ ) as SafeMcpTransportError;
3064
3174
  safeError.name = "McpTransportError";
3065
3175
  if (fields.status !== undefined) {
3066
3176
  safeError.status = fields.status;
3067
3177
  safeError.code = fields.status;
3068
3178
  }
3179
+ if (isMcpRequestTimeoutError(error)) {
3180
+ safeError.mcpTransportFailureKind = "request_timeout";
3181
+ }
3069
3182
  return safeError;
3070
3183
  }
3071
3184
 
@@ -3162,17 +3275,33 @@ async function signFirstPartyDelegatedBearer(
3162
3275
  if (!settings.delegationSecret || !options.accountId || !options.workspaceId) {
3163
3276
  return null;
3164
3277
  }
3278
+ const attemptClaims = [
3279
+ options.sessionId,
3280
+ options.turnId,
3281
+ options.attemptId,
3282
+ options.executionGeneration,
3283
+ ];
3284
+ const hasAnyAttemptClaim = attemptClaims.some((claim) => claim !== undefined);
3285
+ const hasExactAttemptClaims = attemptClaims.every((claim) => claim !== undefined);
3286
+ if (hasAnyAttemptClaim && !hasExactAttemptClaims) {
3287
+ return null;
3288
+ }
3165
3289
  return await signDelegatedAccessToken(settings.delegationSecret, {
3166
3290
  accountId: options.accountId,
3167
3291
  workspaceId: options.workspaceId,
3168
3292
  subjectId: options.subjectId ?? "worker:first-party-mcp",
3169
3293
  ...(options.subjectLabel ? { subjectLabel: options.subjectLabel } : {}),
3170
3294
  permissions: options.firstPartyPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS],
3295
+ principalKind: hasExactAttemptClaims ? "agent_attempt" : "service",
3171
3296
  firstPartyMcpTools: options.firstPartyTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
3172
- ...(options.sessionId ? { sessionId: options.sessionId } : {}),
3173
- ...(options.turnId ? { turnId: options.turnId } : {}),
3174
- ...(options.attemptId ? { attemptId: options.attemptId } : {}),
3175
- ...(options.executionGeneration ? { executionGeneration: options.executionGeneration } : {}),
3297
+ ...(hasExactAttemptClaims
3298
+ ? {
3299
+ sessionId: options.sessionId!,
3300
+ turnId: options.turnId!,
3301
+ attemptId: options.attemptId!,
3302
+ executionGeneration: options.executionGeneration!,
3303
+ }
3304
+ : {}),
3176
3305
  exp: Math.floor(Date.now() / 1000) + 60 * 60,
3177
3306
  });
3178
3307
  }
@@ -3469,7 +3598,7 @@ class PrefixedMcpServer implements MCPServer {
3469
3598
  if (isAuthNeededMcpError(error)) {
3470
3599
  return {
3471
3600
  isError: true,
3472
- content: [{ type: "text", text: MCP_AUTH_NEEDED_MESSAGE }],
3601
+ content: [{ type: "text", text: MCP_AUTH_NEEDED_ERROR.message }],
3473
3602
  };
3474
3603
  }
3475
3604
  // Best-effort INVOCATION isolation (sibling to the listTools guard). When
@@ -3798,18 +3927,6 @@ export const normalizeComputerCallsFilter: CallModelInputFilter = ({ modelData }
3798
3927
  ) as unknown as AgentInputItem[],
3799
3928
  });
3800
3929
 
3801
- /**
3802
- * Per-call state compaction for local image inspection. Re-opening the same
3803
- * path supersedes its prior base64 result; carrying both provides no newer
3804
- * information and can otherwise balloon every following model request.
3805
- */
3806
- export const elideSupersededViewImagesFilter: CallModelInputFilter = ({ modelData }) => ({
3807
- ...modelData,
3808
- input: elideSupersededViewImagePairs(
3809
- modelData.input as unknown as Array<Record<string, unknown>>,
3810
- ) as unknown as AgentInputItem[],
3811
- });
3812
-
3813
3930
  /**
3814
3931
  * Canonical Codex-style tool-result bound at the final model-input seam. The
3815
3932
  * identical pure normalizer also runs before conversation rows are persisted,
@@ -3936,7 +4053,6 @@ export function callModelInputFilterForSettings(
3936
4053
  ): CallModelInputFilter | undefined {
3937
4054
  const filters: CallModelInputFilter[] = [
3938
4055
  normalizeComputerCallsFilter,
3939
- elideSupersededViewImagesFilter,
3940
4056
  boundModelToolOutputsFilterForSettings(settings),
3941
4057
  ];
3942
4058
  if (settings.openaiProviderItemIds === "strip") {
@@ -5005,6 +5121,72 @@ export function normalizeToolOutputForEvent(output: unknown): unknown {
5005
5121
  return output;
5006
5122
  }
5007
5123
 
5124
+ /**
5125
+ * Hosted web_search progresses on the raw Responses stream
5126
+ * (`response.output_item.added/done` with `web_search_call`) long before the SDK
5127
+ * materializes a `RunToolCallItem` at `response_done`. Without this mapping the
5128
+ * timeline only sees search cards after the whole model round finishes — or
5129
+ * never mid-turn — while assistant prose ("Search 1/5") streams live.
5130
+ */
5131
+ function hostedWebSearchToolCallFromResponsesEvent(raw: unknown): NormalizedRuntimeEvent | null {
5132
+ if (!raw || typeof raw !== "object") {
5133
+ return null;
5134
+ }
5135
+ const event = raw as {
5136
+ type?: unknown;
5137
+ item?: {
5138
+ id?: unknown;
5139
+ type?: unknown;
5140
+ status?: unknown;
5141
+ action?: unknown;
5142
+ [key: string]: unknown;
5143
+ };
5144
+ };
5145
+ const eventType = typeof event.type === "string" ? event.type : "";
5146
+ if (
5147
+ !(
5148
+ (eventType === "response.output_item.added" || eventType === "response.output_item.done") &&
5149
+ event.item?.type === "web_search_call"
5150
+ )
5151
+ ) {
5152
+ // Progress-only `response.web_search_call.*` events lack the action
5153
+ // payload; added/done are enough for a live, query-bearing card.
5154
+ return null;
5155
+ }
5156
+ const item = event.item;
5157
+ const itemId = typeof item.id === "string" ? item.id : null;
5158
+ if (!itemId) {
5159
+ return null;
5160
+ }
5161
+ const status =
5162
+ typeof item.status === "string"
5163
+ ? item.status
5164
+ : eventType === "response.output_item.done"
5165
+ ? "completed"
5166
+ : "in_progress";
5167
+ const action = item.action ?? null;
5168
+ // Codex frequently emits `output_item.added` for web_search_call before the
5169
+ // action payload exists. Persist that so the timeline can show "Searching…",
5170
+ // then the matching `done` (same id) fills in query/queries via merge.
5171
+ const { status: _status, ...providerData } = item;
5172
+
5173
+ return {
5174
+ type: "agent.toolCall.created",
5175
+ payload: {
5176
+ id: itemId,
5177
+ name: "web_search_call",
5178
+ arguments: action,
5179
+ raw: {
5180
+ type: "hosted_tool_call",
5181
+ id: itemId,
5182
+ name: "web_search_call",
5183
+ status,
5184
+ providerData,
5185
+ },
5186
+ },
5187
+ };
5188
+ }
5189
+
5008
5190
  export function normalizeSdkEvent(event: RunStreamEvent): NormalizedRuntimeEvent[] {
5009
5191
  const out: NormalizedRuntimeEvent[] = [];
5010
5192
  if (event.type === "raw_model_stream_event") {
@@ -5022,6 +5204,10 @@ export function normalizeSdkEvent(event: RunStreamEvent): NormalizedRuntimeEvent
5022
5204
  if (raw?.type === "response.reasoning_summary_text.delta" && typeof raw.delta === "string") {
5023
5205
  out.push({ type: "agent.reasoning.delta", payload: { text: raw.delta } });
5024
5206
  }
5207
+ const webSearch = hostedWebSearchToolCallFromResponsesEvent(raw);
5208
+ if (webSearch) {
5209
+ out.push(webSearch);
5210
+ }
5025
5211
  return out;
5026
5212
  }
5027
5213
  if (event.type === "agent_updated_stream_event") {
@@ -5120,12 +5306,65 @@ export function modelResponseUsageFromResponse(response: unknown): ModelResponse
5120
5306
  : typeof (response as { responseId?: unknown } | null)?.responseId === "string"
5121
5307
  ? (response as { responseId: string }).responseId
5122
5308
  : undefined;
5309
+ const serviceTier = modelResponseServiceTierFromResponse(response);
5123
5310
  return {
5124
5311
  ...(responseId ? { responseId } : {}),
5312
+ ...(serviceTier ? { serviceTier } : {}),
5125
5313
  usage,
5126
5314
  };
5127
5315
  }
5128
5316
 
5317
+ export type ModelResponseServiceTierEvent = {
5318
+ source: "normalized" | "provider";
5319
+ serviceTier: string | null;
5320
+ };
5321
+
5322
+ /**
5323
+ * Read the provider's terminal service tier without depending on usage being
5324
+ * present. The normalized terminal can omit provider-only fields, so callers
5325
+ * should treat the raw provider response as the fail-closed authority.
5326
+ */
5327
+ export function modelResponseServiceTierFromSdkEvent(
5328
+ event: RunStreamEvent,
5329
+ ): ModelResponseServiceTierEvent | null {
5330
+ if (event.type === "raw_model_stream_event") {
5331
+ const data = (event as any).data;
5332
+ if (data?.type === "response_done") {
5333
+ return {
5334
+ source: "normalized",
5335
+ serviceTier: modelResponseServiceTierFromResponse(data.response),
5336
+ };
5337
+ }
5338
+ }
5339
+ if (isOpenAIResponsesRawModelStreamEvent(event)) {
5340
+ const raw = (event as any).data?.event;
5341
+ if (raw?.type === "response.completed") {
5342
+ return {
5343
+ source: "provider",
5344
+ serviceTier: modelResponseServiceTierFromResponse(raw.response),
5345
+ };
5346
+ }
5347
+ }
5348
+ return null;
5349
+ }
5350
+
5351
+ function modelResponseServiceTierFromResponse(response: unknown): string | null {
5352
+ if (!response || typeof response !== "object") {
5353
+ return null;
5354
+ }
5355
+ const record = response as Record<string, unknown>;
5356
+ const direct = record.service_tier ?? record.serviceTier;
5357
+ if (typeof direct === "string" && direct.length > 0) {
5358
+ return direct;
5359
+ }
5360
+ const providerData =
5361
+ record.providerData && typeof record.providerData === "object"
5362
+ ? (record.providerData as Record<string, unknown>)
5363
+ : null;
5364
+ const nested = providerData?.service_tier ?? providerData?.serviceTier;
5365
+ return typeof nested === "string" && nested.length > 0 ? nested : null;
5366
+ }
5367
+
5129
5368
  function modelResponseFromSdkEvent(event: RunStreamEvent): any {
5130
5369
  if (event.type === "raw_model_stream_event") {
5131
5370
  const data = (event as any).data;