@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
package/dist/index.js CHANGED
@@ -10,21 +10,43 @@ import {
10
10
  DISPLAY_STACK_TIMEOUT_MS,
11
11
  DisplayStackError,
12
12
  DisplayStackUnsupportedError,
13
+ FAULT_FIELD_WHAT_HAPPENED,
14
+ FAULT_FIELD_WHAT_PRESERVED,
15
+ FAULT_FIELD_WHAT_TO_TRY,
16
+ FAULT_FIELD_WHICH_LAYER,
13
17
  MockAgentResponder,
18
+ NEVER_SENT_DETAIL_KEY,
14
19
  NatsControlRpc,
20
+ NatsOpStreamTransport,
21
+ OP_STREAM_ACK_INTERVAL_MS,
22
+ OP_STREAM_DEFAULT_WINDOW_BYTES,
23
+ OP_STREAM_RECONNECT_HOLD_MS,
24
+ OP_STREAM_SILENCE_TIMEOUT_MS,
25
+ OpStreamUnavailableError,
15
26
  PROVIDER_REGISTRY,
27
+ REPOSITORY_DISCOVERY_LIMIT,
16
28
  RecordingError,
17
29
  RecordingUnavailableError,
18
30
  RoutingSandboxSession,
19
31
  RoutingUnsupportedError,
20
32
  SELFHOSTED_DEFAULT_TIMEOUT_MS,
33
+ SELFHOSTED_DRAINING_MAX_RETRIES,
34
+ SELFHOSTED_EXEC_DRAINING_MAX_RETRIES,
35
+ SELFHOSTED_IDEMPOTENT_READONLY_OPS,
36
+ SELFHOSTED_INFRASTRUCTURE_FAULT_CLASSES,
37
+ SELFHOSTED_NEVER_SENT_MAX_RETRIES,
21
38
  SELFHOSTED_RECONNECT_WINDOW_MS,
22
39
  SELFHOSTED_RELAY_STREAM_PATH,
40
+ SELFHOSTED_RETRY_BACKOFF_BASE_MS,
41
+ SELFHOSTED_RETRY_BACKOFF_CAP_MS,
42
+ SELFHOSTED_RETRY_BACKOFF_FACTOR,
43
+ SELFHOSTED_TIMEOUT_MAX_RETRIES,
23
44
  STREAM_PORT,
24
45
  STREAM_TOKEN_DEFAULT_TTL_SECONDS,
25
46
  SandboxChannelAService,
26
47
  SandboxConfigError,
27
48
  SandboxProviderUnavailableError,
49
+ SandboxResumeStateUnavailableError,
28
50
  SelfhostedControlError,
29
51
  SelfhostedSandboxClient,
30
52
  SelfhostedSession,
@@ -47,17 +69,23 @@ import {
47
69
  contentTypeForCodec,
48
70
  createSandboxClient,
49
71
  createSandboxClientForBackend,
72
+ decideSelfhostedRetry,
50
73
  decodeModalSnapshotId,
74
+ defaultSelfhostedRetryClock,
51
75
  deletePriorPersistedSnapshot,
52
76
  deleteRecordingArtifacts,
53
77
  deserializeSandboxSessionStateEnvelope,
54
78
  desktopCapableBackend,
79
+ drainingExhaustedError,
80
+ drainingMessage,
55
81
  ensureDisplayStack,
56
82
  ensureModalRegistryImage,
57
83
  ensureTerminalServer,
58
84
  establishSandboxSessionFromEnvelope,
85
+ execDeadlineHint,
59
86
  exposeStreamPort,
60
87
  extForCodec,
88
+ inspectRecordingArtifact,
61
89
  isExecSessionLostBanner,
62
90
  isProviderSandboxNotFoundError,
63
91
  isSelfhostedProviderNotFoundError,
@@ -68,26 +96,35 @@ import {
68
96
  modalSandboxAttributionTags,
69
97
  negotiateCapabilities,
70
98
  negotiateSelfhostedCapabilities,
99
+ nextDurableOpId,
71
100
  offlineAgentError,
72
101
  offlineControlResponse,
102
+ opAckSubject,
103
+ opFrameSubject,
73
104
  parseExecBannerSessionId,
74
105
  parseExposedPorts,
75
106
  parseNumstatZ,
76
107
  parsePorcelainV2,
77
108
  parseUnifiedPatch,
78
- readRecordingBytes,
109
+ payloadTooLargeMessage,
79
110
  readWorkspaceArchiveFromEnvelopeSessionState,
80
111
  recordingStorageKey,
112
+ renderSelfhostedFault,
81
113
  restoredSandboxSessionStateFromEntry,
114
+ runWithToolCallCorrelation,
82
115
  sandboxStateEntryFromRunState,
116
+ sanitizeOpIdToken,
83
117
  selectBackend,
118
+ selfhostedFaultClass,
84
119
  selfhostedLiveness,
120
+ selfhostedRetryBackoffMs,
85
121
  serializeEstablishedSandboxEnvelope,
86
122
  setSelfhostedApplyDiff,
87
123
  startRecording,
88
124
  stopRecording,
89
125
  stripExecBanner,
90
126
  subjectFor,
127
+ swapTargetEstablishability,
91
128
  sweepModalOrphanSandboxes,
92
129
  tagModalSandbox,
93
130
  tearDownDisplayStack,
@@ -95,12 +132,24 @@ import {
95
132
  terminateModalSandboxById,
96
133
  timeoutAgentError,
97
134
  timeoutControlResponse,
135
+ uploadRecordingArtifact,
98
136
  verifyStreamToken
99
- } from "./chunk-BHA2SU3H.js";
137
+ } from "./chunk-RYDCVTMW.js";
100
138
 
101
139
  // src/index.ts
102
- import { AGENT_INSTRUCTIONS_CORE_PLACEHOLDER, collectSandboxEnvironment as collectSandboxEnvironment2, contextInputBudgetTokens, contextServerCompactThreshold, firstPartyMcpBaseUrl, resolveContextCompactionMode, resolveModelProvider, sandboxLifecycleHookIds } from "@opengeni/config";
103
- import { CAPABILITY_DESCRIPTORS as CAPABILITY_DESCRIPTORS2, isClearedRunStateBlob, prefixedMcpToolName as sharedPrefixedMcpToolName, signDelegatedAccessToken } from "@opengeni/contracts";
140
+ import {
141
+ AGENT_INSTRUCTIONS_CORE_PLACEHOLDER,
142
+ collectSandboxEnvironment as collectSandboxEnvironment2,
143
+ firstPartyMcpBaseUrl,
144
+ resolveModelProvider,
145
+ sandboxLifecycleHookIds
146
+ } from "@opengeni/config";
147
+ import {
148
+ CAPABILITY_DESCRIPTORS as CAPABILITY_DESCRIPTORS2,
149
+ isClearedRunStateBlob,
150
+ prefixedMcpToolName as sharedPrefixedMcpToolName,
151
+ signDelegatedAccessToken
152
+ } from "@opengeni/contracts";
104
153
  import {
105
154
  Agent,
106
155
  AgentsError,
@@ -121,15 +170,11 @@ import {
121
170
  webSearchTool,
122
171
  applyDiff
123
172
  } from "@openai/agents";
124
- import {
125
- localDirLazySkillSource
126
- } from "@openai/agents/sandbox/local";
173
+ import { localDirLazySkillSource } from "@openai/agents/sandbox/local";
127
174
  import {
128
175
  Manifest,
129
176
  SandboxAgent,
130
- StaticCompactionPolicy,
131
177
  azureBlobMount,
132
- compaction,
133
178
  dir,
134
179
  file,
135
180
  filesystem,
@@ -142,7 +187,14 @@ import {
142
187
  } from "@openai/agents/sandbox";
143
188
  import { ModalCloudBucketMountStrategy } from "@openai/agents-extensions/sandbox/modal";
144
189
  import OpenAI from "openai";
145
- import { CODEX_APPS_MCP_SERVER_ID, CODEX_MODEL_ID_PREFIX, CODEX_ORIGINATOR, codexAppsSanitizingFetch, codexRequestStorage, codexSubscriptionFetch } from "@opengeni/codex";
190
+ import {
191
+ CODEX_APPS_MCP_SERVER_ID,
192
+ CODEX_MODEL_ID_PREFIX,
193
+ CODEX_ORIGINATOR,
194
+ codexAppsSanitizingFetch,
195
+ codexRequestStorage,
196
+ codexSubscriptionFetch
197
+ } from "@opengeni/codex";
146
198
  import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs";
147
199
  import { dirname, isAbsolute, join, posix as posixPath, relative } from "path";
148
200
  import { fileURLToPath } from "url";
@@ -195,6 +247,68 @@ function callIdOf(item) {
195
247
  }
196
248
  return void 0;
197
249
  }
250
+ function viewImagePathOf(item) {
251
+ if (!item || typeof item !== "object") {
252
+ return void 0;
253
+ }
254
+ const record = item;
255
+ if (record.type !== "function_call" || record.name !== "view_image" || typeof record.arguments !== "string") {
256
+ return void 0;
257
+ }
258
+ try {
259
+ const parsed = JSON.parse(record.arguments);
260
+ return typeof parsed.path === "string" && parsed.path.length > 0 ? parsed.path : void 0;
261
+ } catch {
262
+ return void 0;
263
+ }
264
+ }
265
+ function elideSupersededViewImagePairs(items) {
266
+ if (items.length === 0) {
267
+ return [];
268
+ }
269
+ const callIndexById = /* @__PURE__ */ new Map();
270
+ const resultIndexById = /* @__PURE__ */ new Map();
271
+ items.forEach((item, index) => {
272
+ const callId = callIdOf(item);
273
+ if (!callId) {
274
+ return;
275
+ }
276
+ if (itemType(item) === "function_call") {
277
+ callIndexById.set(callId, index);
278
+ } else if (itemType(item) === "function_call_result" || itemType(item) === "function_call_output") {
279
+ resultIndexById.set(callId, index);
280
+ }
281
+ });
282
+ const pairedByPath = /* @__PURE__ */ new Map();
283
+ for (const [callId, callIndex] of callIndexById) {
284
+ const path = viewImagePathOf(items[callIndex]);
285
+ const resultIndex = resultIndexById.get(callId);
286
+ if (!path || resultIndex === void 0 || resultIndex <= callIndex) {
287
+ continue;
288
+ }
289
+ const pairs = pairedByPath.get(path) ?? [];
290
+ pairs.push({ callIndex, resultIndex });
291
+ pairedByPath.set(path, pairs);
292
+ }
293
+ const dropped = /* @__PURE__ */ new Set();
294
+ for (const pairs of pairedByPath.values()) {
295
+ if (pairs.length < 2) {
296
+ continue;
297
+ }
298
+ pairs.sort((left, right) => left.callIndex - right.callIndex);
299
+ for (const pair of pairs.slice(0, -1)) {
300
+ dropped.add(pair.callIndex);
301
+ dropped.add(pair.resultIndex);
302
+ for (let index = pair.callIndex - 1; index >= 0; index -= 1) {
303
+ if (itemType(items[index]) !== "reasoning") {
304
+ break;
305
+ }
306
+ dropped.add(index);
307
+ }
308
+ }
309
+ }
310
+ return dropped.size === 0 ? items.slice() : items.filter((_item, index) => !dropped.has(index));
311
+ }
198
312
  function sanitizeHistoryItemsForModel(items) {
199
313
  if (items.length === 0) {
200
314
  return [];
@@ -265,9 +379,24 @@ function sanitizeHistoryItemsForModel(items) {
265
379
  }
266
380
  }
267
381
  if (dropped.size === 0) {
268
- return items.slice();
382
+ return items.map(stripInternalResumeMarker);
383
+ }
384
+ return items.filter((_item, index) => !dropped.has(index)).map(stripInternalResumeMarker);
385
+ }
386
+ var INTERNAL_RESUME_MESSAGE_MARKER = "opengeni_internal_resume";
387
+ function stripInternalResumeMarker(item) {
388
+ const providerData = item.providerData;
389
+ if (!providerData || typeof providerData !== "object" || !(INTERNAL_RESUME_MESSAGE_MARKER in providerData)) {
390
+ return item;
391
+ }
392
+ const { [INTERNAL_RESUME_MESSAGE_MARKER]: _dropped, ...rest } = providerData;
393
+ const next = { ...item };
394
+ if (Object.keys(rest).length > 0) {
395
+ next.providerData = rest;
396
+ } else {
397
+ delete next.providerData;
269
398
  }
270
- return items.filter((_item, index) => !dropped.has(index));
399
+ return next;
271
400
  }
272
401
  function stripReasoningEncryptedContent(item) {
273
402
  const type = itemType(item);
@@ -519,106 +648,6 @@ function computerCallNormalizingFetch(base) {
519
648
  };
520
649
  }
521
650
 
522
- // src/image-history.ts
523
- var SCREENSHOT_OMITTED_PLACEHOLDER = "[screenshot omitted: an older desktop frame \u2014 the full image remains in the session event log]";
524
- var DATA_IMAGE_BASE64_PATTERN = /data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=_-]+/i;
525
- function elideStaleScreenshotImages(items, options = {}) {
526
- const keepLast = Math.max(0, Math.floor(options.keepLast ?? 3));
527
- const placeholder = options.placeholder ?? SCREENSHOT_OMITTED_PLACEHOLDER;
528
- const occurrences = [];
529
- for (let i = 0; i < items.length; i += 1) {
530
- collectItemImageOccurrences(items[i], [i], placeholder, occurrences);
531
- }
532
- const elidedCount = Math.max(0, occurrences.length - keepLast);
533
- if (elidedCount === 0) {
534
- return { items: items.slice(), imageCount: occurrences.length, elidedCount: 0 };
535
- }
536
- const cloned = structuredClone(items);
537
- for (const occurrence of occurrences.slice(0, elidedCount)) {
538
- setPath(cloned, occurrence.path, occurrence.replacement);
539
- }
540
- return { items: cloned, imageCount: occurrences.length, elidedCount };
541
- }
542
- function collectItemImageOccurrences(item, path, placeholder, out) {
543
- if (!isRecord(item)) {
544
- return;
545
- }
546
- if (item.type === "message" && (item.role === "user" || item.role === "system")) {
547
- return;
548
- }
549
- if (item.type === "computer_call_result" || item.type === "computer_call_output") {
550
- collectComputerOutputImages(item, path, placeholder, out);
551
- return;
552
- }
553
- if (item.type === "function_call_result" || item.type === "function_call_output") {
554
- collectToolResultImages(item.output, [...path, "output"], placeholder, out);
555
- }
556
- }
557
- function collectComputerOutputImages(item, path, placeholder, out) {
558
- const output = item.output;
559
- if (!isRecord(output) || output.type !== "computer_screenshot") {
560
- return;
561
- }
562
- for (const key of ["data", "image_url", "imageUrl"]) {
563
- if (isImageDataUrl(output[key])) {
564
- out.push({ path: [...path, "output", key], replacement: placeholder });
565
- return;
566
- }
567
- }
568
- }
569
- function collectToolResultImages(value, path, placeholder, out) {
570
- if (typeof value === "string") {
571
- if (isImageDataUrl(value)) {
572
- out.push({ path, replacement: placeholder });
573
- }
574
- return;
575
- }
576
- if (Array.isArray(value)) {
577
- for (let i = 0; i < value.length; i += 1) {
578
- collectToolResultImages(value[i], [...path, i], placeholder, out);
579
- }
580
- return;
581
- }
582
- if (!isRecord(value)) {
583
- return;
584
- }
585
- if (value.type === "input_image") {
586
- for (const key of ["image", "imageUrl", "image_url"]) {
587
- if (isImageDataUrl(value[key])) {
588
- out.push({ path, replacement: { type: "input_text", text: placeholder } });
589
- return;
590
- }
591
- }
592
- }
593
- for (const key of ["content", "text", "output"]) {
594
- if (key in value) {
595
- collectToolResultImages(value[key], [...path, key], placeholder, out);
596
- }
597
- }
598
- }
599
- function isImageDataUrl(value) {
600
- return typeof value === "string" && DATA_IMAGE_BASE64_PATTERN.test(value);
601
- }
602
- function isRecord(value) {
603
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
604
- }
605
- function setPath(root, path, value) {
606
- if (path.length === 0) {
607
- return;
608
- }
609
- let cursor = root;
610
- for (let i = 0; i < path.length - 1; i += 1) {
611
- const segment = path[i];
612
- cursor = Array.isArray(cursor) ? cursor[segment] : cursor[segment];
613
- }
614
- const last = path[path.length - 1];
615
- if (Array.isArray(cursor)) {
616
- cursor[last] = value;
617
- } else {
618
- cursor[last] = value;
619
- }
620
- }
621
-
622
651
  // src/codex-tool-search.ts
623
652
  import { toolSearchTool } from "@openai/agents";
624
653
  var CODEX_APPS_TOOL_PREFIX = "codex_apps__";
@@ -743,7 +772,10 @@ ${sources.map((s) => `- ${s}`).join("\n")}`;
743
772
  var SEARCH_TOOL_PARAMETERS = {
744
773
  type: "object",
745
774
  properties: {
746
- query: { type: "string", description: "Plain-language description of the capability you need." },
775
+ query: {
776
+ type: "string",
777
+ description: "Plain-language description of the capability you need."
778
+ },
747
779
  limit: { type: "number", description: "Maximum number of tools to return (default 8)." }
748
780
  },
749
781
  required: ["query"],
@@ -756,10 +788,28 @@ function codexToolSearchExecutor(args) {
756
788
  return bm25RankTools(deferred, query, limit);
757
789
  }
758
790
  var NO_NAMESPACES = /* @__PURE__ */ new Set();
759
- function buildCodexToolSearchTool(connectorNamespaces = NO_NAMESPACES) {
791
+ function hasConnectorSources(connectorNamespaces) {
792
+ for (const namespace of connectorNamespaces) {
793
+ if (typeof namespace === "string" && namespace.length > 0) {
794
+ return true;
795
+ }
796
+ }
797
+ return false;
798
+ }
799
+ function resolveSearchToolDescription(connectorNamespaces, freeze) {
800
+ if (freeze?.value != null) {
801
+ return freeze.value;
802
+ }
803
+ const rendered = renderSearchToolDescription(connectorNamespaces);
804
+ if (freeze && hasConnectorSources(connectorNamespaces)) {
805
+ freeze.value = rendered;
806
+ }
807
+ return rendered;
808
+ }
809
+ function buildCodexToolSearchToolFromDescription(description) {
760
810
  return toolSearchTool({
761
811
  execution: "client",
762
- description: renderSearchToolDescription(connectorNamespaces),
812
+ description,
763
813
  parameters: SEARCH_TOOL_PARAMETERS,
764
814
  execute: codexToolSearchExecutor
765
815
  });
@@ -768,7 +818,7 @@ function isToolSearchTool(tool2) {
768
818
  const t = tool2;
769
819
  return !!t && (t.name === "tool_search" || t.providerData?.type === "tool_search");
770
820
  }
771
- function applyCodexToolSearch(tools, connectorNamespaces = NO_NAMESPACES) {
821
+ function applyCodexToolSearch(tools, connectorNamespaces = NO_NAMESPACES, descriptionFreeze) {
772
822
  for (const tool2 of tools) {
773
823
  if (isCodexAppsFunctionTool(tool2) && tool2.deferLoading !== true) {
774
824
  tool2.deferLoading = true;
@@ -777,16 +827,24 @@ function applyCodexToolSearch(tools, connectorNamespaces = NO_NAMESPACES) {
777
827
  if (tools.some(isToolSearchTool)) {
778
828
  return tools;
779
829
  }
780
- return [...tools, buildCodexToolSearchTool(connectorNamespaces)];
830
+ const description = resolveSearchToolDescription(connectorNamespaces, descriptionFreeze);
831
+ return [...tools, buildCodexToolSearchToolFromDescription(description)];
781
832
  }
782
833
  function installCodexToolSearch(agent, connectorNamespaces = NO_NAMESPACES) {
834
+ installCodexToolSearchWithFreeze(agent, connectorNamespaces, { value: null });
835
+ }
836
+ function installCodexToolSearchWithFreeze(agent, connectorNamespaces, descriptionFreeze) {
783
837
  const originalGetAllTools = agent.getAllTools.bind(agent);
784
- agent.getAllTools = (async (runContext) => applyCodexToolSearch(await originalGetAllTools(runContext), connectorNamespaces));
838
+ agent.getAllTools = async (runContext) => applyCodexToolSearch(
839
+ await originalGetAllTools(runContext),
840
+ connectorNamespaces,
841
+ descriptionFreeze
842
+ );
785
843
  const originalClone = agent.clone?.bind(agent);
786
844
  if (originalClone) {
787
845
  const cloneWithToolSearch = (config) => {
788
846
  const cloned = originalClone(config);
789
- installCodexToolSearch(cloned, connectorNamespaces);
847
+ installCodexToolSearchWithFreeze(cloned, connectorNamespaces, descriptionFreeze);
790
848
  return cloned;
791
849
  };
792
850
  agent.clone = cloneWithToolSearch;
@@ -797,10 +855,9 @@ function installCodexToolSearch(agent, connectorNamespaces = NO_NAMESPACES) {
797
855
  var COMPACTION_SUMMARY_MARKER = "opengeni_context_summary";
798
856
  var SUMMARY_BUFFER_TOKENS = 2e4;
799
857
  var COMPACT_USER_MESSAGE_MAX_TOKENS = 2e4;
800
- var DEFAULT_COMPACTION_THRESHOLD_RATIO = 0.6;
858
+ var DEFAULT_COMPACTION_THRESHOLD_RATIO = 0.9;
801
859
  var MIN_COMPACTION_THRESHOLD_RATIO = 0.3;
802
860
  var MAX_COMPACTION_THRESHOLD_RATIO = 0.9;
803
- var COMPACTION_FALLBACK_TARGET_RATIO = 0.5;
804
861
  var COMPACTION_PROMPT = [
805
862
  "You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.",
806
863
  "",
@@ -836,6 +893,9 @@ function isUserMessage(item) {
836
893
  function isCompactionSummary(item) {
837
894
  return isUserMessage(item) && item[COMPACTION_SUMMARY_MARKER] === true;
838
895
  }
896
+ function isEphemeralInternalContext(item) {
897
+ return itemType2(item) === "message" && itemRole(item) === "system";
898
+ }
839
899
  function estimateItemTokens(item) {
840
900
  let text;
841
901
  try {
@@ -854,13 +914,21 @@ function estimateTokens(items) {
854
914
  }
855
915
  function clampCompactionThresholdRatio(value) {
856
916
  const numeric = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_COMPACTION_THRESHOLD_RATIO;
857
- return Math.min(MAX_COMPACTION_THRESHOLD_RATIO, Math.max(MIN_COMPACTION_THRESHOLD_RATIO, numeric));
917
+ return Math.min(
918
+ MAX_COMPACTION_THRESHOLD_RATIO,
919
+ Math.max(MIN_COMPACTION_THRESHOLD_RATIO, numeric)
920
+ );
858
921
  }
859
- function clientCompactionThresholdTokens(input) {
860
- return Math.floor(Math.max(0, input.contextWindowTokens) * clampCompactionThresholdRatio(input.contextCompactionThresholdRatio));
922
+ function compactionThresholdTokens(input) {
923
+ const window = Math.max(0, input.contextWindowTokens);
924
+ const codexMaximum = Math.floor(window * MAX_COMPACTION_THRESHOLD_RATIO);
925
+ if (typeof input.contextAutoCompactThresholdTokens === "number" && Number.isFinite(input.contextAutoCompactThresholdTokens) && input.contextAutoCompactThresholdTokens > 0) {
926
+ return Math.min(Math.floor(input.contextAutoCompactThresholdTokens), codexMaximum);
927
+ }
928
+ return Math.floor(window * clampCompactionThresholdRatio(input.contextCompactionThresholdRatio));
861
929
  }
862
- function decideClientCompaction(input) {
863
- const thresholdTokens = clientCompactionThresholdTokens(input);
930
+ function decideCompaction(input) {
931
+ const thresholdTokens = compactionThresholdTokens(input);
864
932
  const recorded = typeof input.lastInputTokens === "number" && input.lastInputTokens > 0 ? input.lastInputTokens : 0;
865
933
  const signalTokens = recorded > 0 ? recorded : estimateTokens(input.items);
866
934
  if (input.items.length === 0) {
@@ -869,7 +937,7 @@ function decideClientCompaction(input) {
869
937
  if (input.force) {
870
938
  return { shouldCompact: true, reason: "force", signalTokens, thresholdTokens };
871
939
  }
872
- if (signalTokens > thresholdTokens) {
940
+ if (signalTokens >= thresholdTokens) {
873
941
  return { shouldCompact: true, reason: "above_threshold", signalTokens, thresholdTokens };
874
942
  }
875
943
  return { shouldCompact: false, reason: "below_threshold", signalTokens, thresholdTokens };
@@ -878,14 +946,17 @@ var CompactionNeededError = class extends Error {
878
946
  signalTokens;
879
947
  thresholdTokens;
880
948
  signalSource;
949
+ trigger;
881
950
  constructor(input) {
951
+ const trigger = input.trigger ?? "threshold";
882
952
  super(
883
- `Context compaction needed: signal ${input.signalTokens} tokens exceeded threshold ${input.thresholdTokens}`
953
+ trigger === "operator" ? "Context compaction requested by the operator" : `Context compaction needed: signal ${input.signalTokens} tokens exceeded threshold ${input.thresholdTokens}`
884
954
  );
885
955
  this.name = "CompactionNeededError";
886
956
  this.signalTokens = input.signalTokens;
887
957
  this.thresholdTokens = input.thresholdTokens;
888
958
  this.signalSource = input.signalSource;
959
+ this.trigger = trigger;
889
960
  }
890
961
  };
891
962
  function findCompactionNeededError(error, seen = /* @__PURE__ */ new WeakSet()) {
@@ -902,42 +973,6 @@ function findCompactionNeededError(error, seen = /* @__PURE__ */ new WeakSet())
902
973
  const record = error;
903
974
  return findCompactionNeededError(record.cause, seen) ?? findCompactionNeededError(record.error, seen);
904
975
  }
905
- function findKeepBoundary(items, keepRecentTokens) {
906
- const boundaries = [];
907
- for (let i = 0; i < items.length; i += 1) {
908
- if (isUserMessage(items[i])) {
909
- boundaries.push(i);
910
- }
911
- }
912
- if (boundaries.length === 0) {
913
- return 0;
914
- }
915
- for (let b = 0; b < boundaries.length; b += 1) {
916
- const boundary = boundaries[b];
917
- if (estimateTokens(items.slice(boundary)) <= keepRecentTokens) {
918
- return boundary;
919
- }
920
- }
921
- return boundaries[boundaries.length - 1];
922
- }
923
- function enforceInputBudget(items, maxTokens, trailingTokens = 0) {
924
- const total = estimateTokens(items) + Math.max(0, trailingTokens);
925
- if (items.length === 0 || total <= maxTokens) {
926
- return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
927
- }
928
- const historyBudget = Math.max(0, maxTokens - Math.max(0, trailingTokens));
929
- const boundary = findKeepBoundary(items, historyBudget);
930
- if (boundary <= 0) {
931
- return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
932
- }
933
- const kept = items.slice(boundary);
934
- return {
935
- items: kept,
936
- trimmed: true,
937
- droppedCount: boundary,
938
- estimatedTokens: estimateTokens(kept) + Math.max(0, trailingTokens)
939
- };
940
- }
941
976
  function buildCompactionPromptInput(items) {
942
977
  return [
943
978
  ...items,
@@ -949,70 +984,27 @@ function buildCompactionPromptInput(items) {
949
984
  ];
950
985
  }
951
986
  function buildCompactionReplacementHistory(items, summaryBody) {
952
- const history = [];
953
- for (const item of items) {
987
+ const retainedReversed = [];
988
+ let remaining = COMPACT_USER_MESSAGE_MAX_TOKENS;
989
+ for (let index = items.length - 1; index >= 0 && remaining > 0; index -= 1) {
990
+ const item = items[index];
954
991
  if (!isUserMessage(item) || isCompactionSummary(item)) {
955
992
  continue;
956
993
  }
957
- history.push(compactUserMessage(item));
958
- }
959
- history.push(buildSummaryItem(summaryBody));
960
- return history;
961
- }
962
- function buildDeterministicFallbackCompactionHistory(input) {
963
- const sourceTokens = Math.max(1, estimateTokens(input.items));
964
- const requestedTarget = typeof input.targetTokens === "number" && input.targetTokens > 0 ? Math.floor(input.targetTokens) : Math.floor(sourceTokens * COMPACTION_FALLBACK_TARGET_RATIO);
965
- const targetTokens = Math.max(1, Math.min(requestedTarget, Math.max(1, sourceTokens - 1)));
966
- const summaryItem = buildSummaryItem([
967
- "Non-LLM context compaction fallback.",
968
- input.cause ? `Summarizer failure: ${input.cause}` : "Summarizer failure: unavailable.",
969
- "Older assistant, tool, reasoning, and image history was dropped deterministically.",
970
- "Retained context above is limited to initial instructions and the most recent user messages that fit the fallback budget; oversized retained messages may be middle-truncated."
971
- ].join("\n"));
972
- const summaryTokens = estimateItemTokens(summaryItem);
973
- let remaining = Math.max(0, targetTokens - summaryTokens);
974
- const retainedInstructions = [];
975
- for (const item of input.items) {
976
- if (!isInitialInstructionMessage(item)) {
977
- if (isUserMessage(item)) {
978
- break;
979
- }
980
- continue;
981
- }
982
- if (remaining <= 0) {
994
+ const textTokens = estimatedTextTokens(messageText(item));
995
+ retainedReversed.push(compactMessageToTokenBudget(item, remaining));
996
+ if (textTokens > remaining) {
997
+ remaining = 0;
983
998
  break;
984
999
  }
985
- const capped = compactMessageToEstimatedItemBudget(item, Math.min(2e3, remaining));
986
- if (!capped) {
987
- continue;
988
- }
989
- const tokens = estimateItemTokens(capped);
990
- if (tokens <= remaining) {
991
- retainedInstructions.push(capped);
992
- remaining -= tokens;
993
- }
994
- }
995
- const retainedUsers = [];
996
- for (let index = input.items.length - 1; index >= 0 && remaining > 0; index -= 1) {
997
- const item = input.items[index];
998
- if (!isUserMessage(item) || isCompactionSummary(item)) {
999
- continue;
1000
- }
1001
- const capped = compactMessageToEstimatedItemBudget(item, Math.min(COMPACT_USER_MESSAGE_MAX_TOKENS, remaining));
1002
- if (!capped) {
1003
- continue;
1004
- }
1005
- const tokens = estimateItemTokens(capped);
1006
- if (tokens <= remaining) {
1007
- retainedUsers.push(capped);
1008
- remaining -= tokens;
1009
- }
1000
+ remaining -= textTokens;
1010
1001
  }
1011
- retainedUsers.reverse();
1012
- return [...retainedInstructions, ...retainedUsers, summaryItem];
1002
+ const history = retainedReversed.reverse();
1003
+ history.push(buildSummaryItem(summaryBody));
1004
+ return history;
1013
1005
  }
1014
1006
  function buildSummaryItem(summaryBody) {
1015
- const trimmed = summaryBody.trim();
1007
+ const trimmed = summaryBody.trim() || "(no summary available)";
1016
1008
  return {
1017
1009
  type: "message",
1018
1010
  role: "user",
@@ -1021,9 +1013,6 @@ ${trimmed}`,
1021
1013
  [COMPACTION_SUMMARY_MARKER]: true
1022
1014
  };
1023
1015
  }
1024
- function compactUserMessage(item) {
1025
- return compactMessageToTokenBudget(item, COMPACT_USER_MESSAGE_MAX_TOKENS);
1026
- }
1027
1016
  function compactMessageToTokenBudget(item, maxTokens) {
1028
1017
  const text = messageText(item);
1029
1018
  const next = { ...item };
@@ -1034,26 +1023,6 @@ function compactMessageToTokenBudget(item, maxTokens) {
1034
1023
  next.content = contentWithoutImages(item);
1035
1024
  return next;
1036
1025
  }
1037
- function compactMessageToEstimatedItemBudget(item, maxItemTokens) {
1038
- if (maxItemTokens <= 0) {
1039
- return null;
1040
- }
1041
- const overheadTokens = estimateItemTokens({ ...item, content: "" });
1042
- let contentBudget = Math.max(0, maxItemTokens - overheadTokens);
1043
- for (let attempt = 0; attempt < 6; attempt += 1) {
1044
- const capped = compactMessageToTokenBudget(item, contentBudget);
1045
- const estimated = estimateItemTokens(capped);
1046
- if (estimated <= maxItemTokens) {
1047
- return capped;
1048
- }
1049
- const overage = estimated - maxItemTokens;
1050
- if (contentBudget <= 0) {
1051
- break;
1052
- }
1053
- contentBudget = Math.max(0, contentBudget - Math.max(1, overage));
1054
- }
1055
- return null;
1056
- }
1057
1026
  function estimatedTextTokens(text) {
1058
1027
  return Math.ceil(text.length / 4);
1059
1028
  }
@@ -1104,46 +1073,25 @@ function messageText(item) {
1104
1073
  }
1105
1074
  return "";
1106
1075
  }
1107
- function isInitialInstructionMessage(item) {
1108
- const type = itemType2(item);
1109
- const role = itemRole(item);
1110
- return type === "message" && (role === "system" || role === "developer");
1111
- }
1112
- function renderCompactionPromptInputForChat(input, options = {}) {
1113
- const rendered = input.map(renderItem);
1114
- if (typeof options.maxEstimatedTokens !== "number" || options.maxEstimatedTokens <= 0) {
1115
- return rendered.join("\n");
1116
- }
1117
- const maxChars = Math.max(1, Math.floor(options.maxEstimatedTokens * 4));
1118
- const last = rendered.at(-1);
1119
- const prefix = rendered.slice(0, -1);
1120
- let kept = prefix.slice();
1121
- while (kept.length > 0 && transcriptLength([...kept, last].filter((line) => line !== void 0)) > maxChars) {
1122
- kept.shift();
1123
- }
1124
- const lines = [...kept, last].filter((line) => line !== void 0);
1125
- const joined = lines.join("\n");
1126
- if (joined.length <= maxChars) {
1127
- return joined;
1128
- }
1129
- return truncateMiddleByChars(joined, maxChars);
1076
+ function renderCompactionPromptInputForChat(input) {
1077
+ return input.map(renderItem).join("\n");
1130
1078
  }
1131
1079
  function renderItem(item) {
1132
1080
  const type = itemType2(item) ?? "unknown";
1133
1081
  if (type === "message") {
1134
1082
  const role = itemRole(item) ?? "assistant";
1135
- return `[${role}] ${truncateForTranscript(messageText(item), 4e3)}`;
1083
+ return `[${role}] ${messageText(item)}`;
1136
1084
  }
1137
1085
  if (type === "reasoning") {
1138
1086
  return "[reasoning] (omitted)";
1139
1087
  }
1140
1088
  if (RESULT_TYPES2.has(type)) {
1141
- return `[tool_result] ${truncateForTranscript(resultText(item), 2e3)}`;
1089
+ return `[tool_result] ${resultText(item)}`;
1142
1090
  }
1143
1091
  if (RESULT_TYPE_BY_CALL_TYPE2[type]) {
1144
- return `[tool_call ${type}] ${truncateForTranscript(callText(item), 1e3)}`;
1092
+ return `[tool_call ${type}] ${callText(item)}`;
1145
1093
  }
1146
- return `[${type}] ${truncateForTranscript(safeStringify(item), 1e3)}`;
1094
+ return `[${type}] ${safeStringify(item)}`;
1147
1095
  }
1148
1096
  function resultText(item) {
1149
1097
  const output = item.output;
@@ -1166,35 +1114,15 @@ function safeStringify(value) {
1166
1114
  return String(value);
1167
1115
  }
1168
1116
  }
1169
- function truncateForTranscript(text, max) {
1170
- if (text.length <= max) {
1171
- return text;
1172
- }
1173
- return `${text.slice(0, max)}... (${text.length - max} more chars)`;
1174
- }
1175
- function transcriptLength(lines) {
1176
- if (lines.length === 0) {
1177
- return 0;
1178
- }
1179
- return lines.reduce((sum, line) => sum + line.length, 0) + Math.max(0, lines.length - 1);
1180
- }
1181
- function truncateMiddleByChars(text, maxChars) {
1182
- if (text.length <= maxChars) {
1183
- return text;
1184
- }
1185
- if (maxChars <= USER_MESSAGE_TRUNCATION_MARKER.length) {
1186
- return USER_MESSAGE_TRUNCATION_MARKER.slice(0, maxChars);
1187
- }
1188
- const keepChars = maxChars - USER_MESSAGE_TRUNCATION_MARKER.length;
1189
- const headChars = Math.ceil(keepChars / 2);
1190
- const tailChars = Math.floor(keepChars / 2);
1191
- return `${text.slice(0, headChars)}${USER_MESSAGE_TRUNCATION_MARKER}${text.slice(text.length - tailChars)}`;
1192
- }
1193
1117
 
1194
1118
  // src/sandbox-computer.ts
1195
1119
  import { computerTool, tool } from "@openai/agents";
1196
1120
  import { Capability } from "@openai/agents/sandbox";
1197
- import { KeyAction, PointerAction, PointerButton } from "@opengeni/agent-proto";
1121
+ import {
1122
+ KeyAction,
1123
+ PointerAction,
1124
+ PointerButton
1125
+ } from "@opengeni/agent-proto";
1198
1126
  function requireBoundSession(capabilityType, session) {
1199
1127
  if (!session) {
1200
1128
  throw new ComputerUnavailableError(`capability "${capabilityType}" used before bind(session)`);
@@ -1242,7 +1170,13 @@ function toKeysym(k) {
1242
1170
  if (/^f([1-9]|1[0-2])$/.test(low)) return low.toUpperCase();
1243
1171
  return low.length === 1 ? low : k;
1244
1172
  }
1245
- var BUTTON_NUM = { left: 1, wheel: 2, right: 3, back: 8, forward: 9 };
1173
+ var BUTTON_NUM = {
1174
+ left: 1,
1175
+ wheel: 2,
1176
+ right: 3,
1177
+ back: 8,
1178
+ forward: 9
1179
+ };
1246
1180
  var ComputerUnavailableError = class extends Error {
1247
1181
  constructor(message) {
1248
1182
  super(message);
@@ -1270,9 +1204,6 @@ ${stderr}` : ""}`);
1270
1204
  this.stderr = stderr;
1271
1205
  this.name = "ComputerActionError";
1272
1206
  }
1273
- cmd;
1274
- exitCode;
1275
- stderr;
1276
1207
  };
1277
1208
  var SandboxComputer = class {
1278
1209
  environment = "ubuntu";
@@ -1285,6 +1216,9 @@ var SandboxComputer = class {
1285
1216
  tmp;
1286
1217
  screenshotWarmupBudgetMs;
1287
1218
  screenshotRetryDelayMs;
1219
+ prepare;
1220
+ prepared = false;
1221
+ preparation = null;
1288
1222
  constructor(session, opts = {}) {
1289
1223
  this.session = session;
1290
1224
  this.display = opts.display ?? DEFAULT_DISPLAY;
@@ -1297,15 +1231,34 @@ var SandboxComputer = class {
1297
1231
  this.tmp = opts.screenshotTmpDir ?? "/tmp";
1298
1232
  this.screenshotWarmupBudgetMs = opts.screenshotWarmupBudgetMs ?? SCREENSHOT_WARMUP_BUDGET_MS;
1299
1233
  this.screenshotRetryDelayMs = opts.screenshotRetryDelayMs ?? SCREENSHOT_RETRY_DELAY_MS;
1234
+ this.prepare = opts.prepare;
1300
1235
  }
1301
1236
  /** Rebind to a freshly resumed-by-id session after a box rollover / re-establish. */
1302
1237
  rebind(session) {
1303
1238
  this.session = session;
1239
+ this.prepared = false;
1240
+ this.preparation = null;
1241
+ }
1242
+ async prepareOnce() {
1243
+ if (this.prepared) return;
1244
+ if (!this.prepare) {
1245
+ this.prepared = true;
1246
+ return;
1247
+ }
1248
+ this.preparation ??= this.prepare(this.session);
1249
+ try {
1250
+ await this.preparation;
1251
+ this.prepared = true;
1252
+ } catch (error) {
1253
+ this.preparation = null;
1254
+ throw error;
1255
+ }
1304
1256
  }
1305
1257
  // The single command primitive. Dual-paths exec/execCommand (F1), then uses the
1306
1258
  // established string-aware parsers (F2/F3): exitCode from the preamble, and
1307
1259
  // "still running" → a retriable failure. Returns the command OUTPUT body.
1308
1260
  async x(cmd) {
1261
+ await this.prepareOnce();
1309
1262
  const args = {
1310
1263
  cmd: `DISPLAY=${this.display} ${cmd}`,
1311
1264
  ...this.runAs ? { runAs: this.runAs } : {},
@@ -1353,7 +1306,9 @@ var SandboxComputer = class {
1353
1306
  await this.x(`scrot --pointer --overwrite ${f}`);
1354
1307
  const bytes = await this.readScreenshotBytes(f);
1355
1308
  if (bytes.length === 0) {
1356
- throw new ComputerUnavailableError("scrot produced an empty screenshot (display not up?)");
1309
+ throw new ComputerUnavailableError(
1310
+ "scrot produced an empty screenshot (display not up?)"
1311
+ );
1357
1312
  }
1358
1313
  return Buffer.from(bytes).toString("base64");
1359
1314
  } catch (error) {
@@ -1362,7 +1317,9 @@ var SandboxComputer = class {
1362
1317
  throw error;
1363
1318
  }
1364
1319
  } finally {
1365
- await this.x(`rm -f ${f}`).catch(() => void 0);
1320
+ if (this.prepared) {
1321
+ await this.x(`rm -f ${f}`).catch(() => void 0);
1322
+ }
1366
1323
  }
1367
1324
  if (Date.now() + this.screenshotRetryDelayMs >= deadline) {
1368
1325
  break;
@@ -1405,7 +1362,9 @@ var SandboxComputer = class {
1405
1362
  if (typeof this.session.execCommand === "function") {
1406
1363
  return stripExecBanner(await this.session.execCommand(args));
1407
1364
  }
1408
- throw new ComputerUnavailableError("session cannot run commands (no exec/execCommand) \u2014 screenshots unavailable");
1365
+ throw new ComputerUnavailableError(
1366
+ "session cannot run commands (no exec/execCommand) \u2014 screenshots unavailable"
1367
+ );
1409
1368
  }
1410
1369
  // Read the screenshot PNG bytes off the box. A SINGLE `base64 <file>` over Modal's
1411
1370
  // exec silently returns "" once the base64 exceeds the exec-stdout buffer cap (a full
@@ -1542,7 +1501,10 @@ var NativeDesktopComputer = class {
1542
1501
  }
1543
1502
  async pointer(x, y, action, button) {
1544
1503
  const n = this.toNative(x, y);
1545
- await this.session.desktopInput({ $case: "pointer", pointer: { x: n.x, y: n.y, action, button } });
1504
+ await this.session.desktopInput({
1505
+ $case: "pointer",
1506
+ pointer: { x: n.x, y: n.y, action, button }
1507
+ });
1546
1508
  }
1547
1509
  async screenshot() {
1548
1510
  let lastError;
@@ -1556,7 +1518,9 @@ var NativeDesktopComputer = class {
1556
1518
  try {
1557
1519
  const { png, width, height, nativeWidth, nativeHeight } = await this.session.screenshot();
1558
1520
  if (png.length === 0) {
1559
- throw new ComputerUnavailableError("native desktop screenshot returned an empty frame (display not up?)");
1521
+ throw new ComputerUnavailableError(
1522
+ "native desktop screenshot returned an empty frame (display not up?)"
1523
+ );
1560
1524
  }
1561
1525
  this.lastEncoded = [width, height];
1562
1526
  this.lastNative = [nativeWidth || width, nativeHeight || height];
@@ -1570,44 +1534,82 @@ var NativeDesktopComputer = class {
1570
1534
  }
1571
1535
  }
1572
1536
  const reason = lastError instanceof Error ? lastError.message : String(lastError);
1573
- console.warn(`[NativeDesktopComputer] screenshot failed after ${attempt} attempt(s): ${reason}`);
1537
+ console.warn(
1538
+ `[NativeDesktopComputer] screenshot failed after ${attempt} attempt(s): ${reason}`
1539
+ );
1574
1540
  if (lastError instanceof Error) {
1575
1541
  throw lastError;
1576
1542
  }
1577
- throw new ComputerUnavailableError("native desktop screenshot returned an empty frame (display not up?)");
1543
+ throw new ComputerUnavailableError(
1544
+ "native desktop screenshot returned an empty frame (display not up?)"
1545
+ );
1578
1546
  }
1579
1547
  async click(x, y, button) {
1580
1548
  this.guardWrite();
1581
- await this.pointer(x, y, PointerAction.POINTER_ACTION_CLICK, POINTER_BUTTON[button] ?? PointerButton.POINTER_BUTTON_LEFT);
1549
+ await this.pointer(
1550
+ x,
1551
+ y,
1552
+ PointerAction.POINTER_ACTION_CLICK,
1553
+ POINTER_BUTTON[button] ?? PointerButton.POINTER_BUTTON_LEFT
1554
+ );
1582
1555
  }
1583
1556
  async doubleClick(x, y) {
1584
1557
  this.guardWrite();
1585
- await this.pointer(x, y, PointerAction.POINTER_ACTION_DOUBLE_CLICK, PointerButton.POINTER_BUTTON_LEFT);
1558
+ await this.pointer(
1559
+ x,
1560
+ y,
1561
+ PointerAction.POINTER_ACTION_DOUBLE_CLICK,
1562
+ PointerButton.POINTER_BUTTON_LEFT
1563
+ );
1586
1564
  }
1587
1565
  async move(x, y) {
1588
1566
  this.guardWrite();
1589
- await this.pointer(x, y, PointerAction.POINTER_ACTION_MOVE, PointerButton.POINTER_BUTTON_UNSPECIFIED);
1567
+ await this.pointer(
1568
+ x,
1569
+ y,
1570
+ PointerAction.POINTER_ACTION_MOVE,
1571
+ PointerButton.POINTER_BUTTON_UNSPECIFIED
1572
+ );
1590
1573
  }
1591
1574
  async scroll(x, y, sx, sy) {
1592
1575
  this.guardWrite();
1593
1576
  const n = this.toNative(x, y);
1594
- await this.session.desktopInput({ $case: "scroll", scroll: { x: n.x, y: n.y, deltaX: sx, deltaY: sy } });
1577
+ await this.session.desktopInput({
1578
+ $case: "scroll",
1579
+ scroll: { x: n.x, y: n.y, deltaX: sx, deltaY: sy }
1580
+ });
1595
1581
  }
1596
1582
  async type(text) {
1597
1583
  this.guardWrite();
1598
- await this.session.desktopInput({ $case: "key", key: { key: text, isText: true, action: KeyAction.KEY_ACTION_PRESS } });
1584
+ await this.session.desktopInput({
1585
+ $case: "key",
1586
+ key: { key: text, isText: true, action: KeyAction.KEY_ACTION_PRESS }
1587
+ });
1599
1588
  }
1600
1589
  async keypress(keys) {
1601
1590
  this.guardWrite();
1602
- await this.session.desktopInput({ $case: "key", key: { key: keys.join("+"), isText: false, action: KeyAction.KEY_ACTION_PRESS } });
1591
+ await this.session.desktopInput({
1592
+ $case: "key",
1593
+ key: { key: keys.join("+"), isText: false, action: KeyAction.KEY_ACTION_PRESS }
1594
+ });
1603
1595
  }
1604
1596
  async drag(path) {
1605
1597
  this.guardWrite();
1606
1598
  if (path.length === 0) return;
1607
1599
  const [sx, sy] = path[0];
1608
- await this.pointer(sx, sy, PointerAction.POINTER_ACTION_DOWN, PointerButton.POINTER_BUTTON_LEFT);
1600
+ await this.pointer(
1601
+ sx,
1602
+ sy,
1603
+ PointerAction.POINTER_ACTION_DOWN,
1604
+ PointerButton.POINTER_BUTTON_LEFT
1605
+ );
1609
1606
  for (const [px, py] of path.slice(1)) {
1610
- await this.pointer(px, py, PointerAction.POINTER_ACTION_MOVE, PointerButton.POINTER_BUTTON_LEFT);
1607
+ await this.pointer(
1608
+ px,
1609
+ py,
1610
+ PointerAction.POINTER_ACTION_MOVE,
1611
+ PointerButton.POINTER_BUTTON_LEFT
1612
+ );
1611
1613
  }
1612
1614
  const [ex, ey] = path[path.length - 1];
1613
1615
  await this.pointer(ex, ey, PointerAction.POINTER_ACTION_UP, PointerButton.POINTER_BUTTON_LEFT);
@@ -1621,14 +1623,20 @@ function isNativeDesktopSession(session) {
1621
1623
  return typeof s.desktopInput === "function" && typeof s.screenshot === "function";
1622
1624
  }
1623
1625
  function sniffScreenshotMediaType(bytes) {
1624
- if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
1626
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71)
1627
+ return "image/png";
1625
1628
  if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
1626
- if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
1627
- if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
1629
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56)
1630
+ return "image/gif";
1631
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80)
1632
+ return "image/webp";
1628
1633
  return "image/png";
1629
1634
  }
1630
1635
  function imageOutputFromScreenshotBytes(bytes) {
1631
- return { type: "image", image: { data: Uint8Array.from(bytes), mediaType: sniffScreenshotMediaType(bytes) } };
1636
+ return {
1637
+ type: "image",
1638
+ image: { data: Uint8Array.from(bytes), mediaType: sniffScreenshotMediaType(bytes) }
1639
+ };
1632
1640
  }
1633
1641
  function renderImageForTextTransport(output) {
1634
1642
  if (typeof output === "string") return output;
@@ -1643,8 +1651,14 @@ function supportsStructuredToolOutputTransport(modelInstance) {
1643
1651
  }
1644
1652
  var COMPUTER_READ_ONLY_MESSAGE = "computer-use is read-only for this session \u2014 click, double_click, move, scroll, type, keypress, and drag are disabled. Call computer_screenshot to observe the desktop.";
1645
1653
  var COORD_PROPS = {
1646
- x: { type: "integer", description: "X coordinate in the pixels of the most recent computer_screenshot" },
1647
- y: { type: "integer", description: "Y coordinate in the pixels of the most recent computer_screenshot" }
1654
+ x: {
1655
+ type: "integer",
1656
+ description: "X coordinate in the pixels of the most recent computer_screenshot"
1657
+ },
1658
+ y: {
1659
+ type: "integer",
1660
+ description: "Y coordinate in the pixels of the most recent computer_screenshot"
1661
+ }
1648
1662
  };
1649
1663
  function objectSchema(properties, required) {
1650
1664
  return { type: "object", properties, required, additionalProperties: false };
@@ -1678,14 +1692,24 @@ function computerFunctionTools(computer, readOnly, needsApproval, imageFunctionR
1678
1692
  name: "computer_click",
1679
1693
  description: "Click the mouse at (x, y). `button` is one of left|right|wheel|back|forward (default left). Take a computer_screenshot first to find coordinates.",
1680
1694
  parameters: objectSchema(
1681
- { ...COORD_PROPS, button: { type: "string", enum: ["left", "right", "wheel", "back", "forward"], description: "Mouse button; defaults to left" } },
1695
+ {
1696
+ ...COORD_PROPS,
1697
+ button: {
1698
+ type: "string",
1699
+ enum: ["left", "right", "wheel", "back", "forward"],
1700
+ description: "Mouse button; defaults to left"
1701
+ }
1702
+ },
1682
1703
  ["x", "y"]
1683
1704
  ),
1684
1705
  strict: false,
1685
1706
  ...approval,
1686
1707
  execute: async (input) => {
1687
1708
  const { x, y, button } = input;
1688
- return write(`clicked ${button ?? "left"} at (${x}, ${y})`, () => computer.click(x, y, button ?? "left"));
1709
+ return write(
1710
+ `clicked ${button ?? "left"} at (${x}, ${y})`,
1711
+ () => computer.click(x, y, button ?? "left")
1712
+ );
1689
1713
  }
1690
1714
  }),
1691
1715
  tool({
@@ -1716,8 +1740,14 @@ function computerFunctionTools(computer, readOnly, needsApproval, imageFunctionR
1716
1740
  parameters: objectSchema(
1717
1741
  {
1718
1742
  ...COORD_PROPS,
1719
- scroll_x: { type: "integer", description: "Horizontal scroll amount in pixels (positive = right)" },
1720
- scroll_y: { type: "integer", description: "Vertical scroll amount in pixels (positive = down)" }
1743
+ scroll_x: {
1744
+ type: "integer",
1745
+ description: "Horizontal scroll amount in pixels (positive = right)"
1746
+ },
1747
+ scroll_y: {
1748
+ type: "integer",
1749
+ description: "Vertical scroll amount in pixels (positive = down)"
1750
+ }
1721
1751
  },
1722
1752
  ["x", "y", "scroll_x", "scroll_y"]
1723
1753
  ),
@@ -1725,13 +1755,19 @@ function computerFunctionTools(computer, readOnly, needsApproval, imageFunctionR
1725
1755
  ...approval,
1726
1756
  execute: async (input) => {
1727
1757
  const { x, y, scroll_x, scroll_y } = input;
1728
- return write(`scrolled (${scroll_x}, ${scroll_y}) at (${x}, ${y})`, () => computer.scroll(x, y, scroll_x, scroll_y));
1758
+ return write(
1759
+ `scrolled (${scroll_x}, ${scroll_y}) at (${x}, ${y})`,
1760
+ () => computer.scroll(x, y, scroll_x, scroll_y)
1761
+ );
1729
1762
  }
1730
1763
  }),
1731
1764
  tool({
1732
1765
  name: "computer_type",
1733
1766
  description: "Type a literal text string at the current keyboard focus. Click the target field first.",
1734
- parameters: objectSchema({ text: { type: "string", description: "The literal text to type" } }, ["text"]),
1767
+ parameters: objectSchema(
1768
+ { text: { type: "string", description: "The literal text to type" } },
1769
+ ["text"]
1770
+ ),
1735
1771
  strict: false,
1736
1772
  ...approval,
1737
1773
  execute: async (input) => {
@@ -1743,7 +1779,13 @@ function computerFunctionTools(computer, readOnly, needsApproval, imageFunctionR
1743
1779
  name: "computer_keypress",
1744
1780
  description: 'Press a key or chord. `keys` is an ordered list pressed together, e.g. ["ctrl","c"] or ["Enter"]. Use key names (ctrl, alt, shift, cmd, enter, tab, esc, arrows\u2026), not characters.',
1745
1781
  parameters: objectSchema(
1746
- { keys: { type: "array", items: { type: "string" }, description: "Keys pressed together as a chord" } },
1782
+ {
1783
+ keys: {
1784
+ type: "array",
1785
+ items: { type: "string" },
1786
+ description: "Keys pressed together as a chord"
1787
+ }
1788
+ },
1747
1789
  ["keys"]
1748
1790
  ),
1749
1791
  strict: false,
@@ -1789,7 +1831,6 @@ var ComputerUseCapability = class extends Capability {
1789
1831
  super();
1790
1832
  this.args = args;
1791
1833
  }
1792
- args;
1793
1834
  type = "computer-use";
1794
1835
  tools() {
1795
1836
  const session = requireBoundSession("computer-use", this._session);
@@ -1801,22 +1842,45 @@ var ComputerUseCapability = class extends Capability {
1801
1842
  ...this.args.readOnly !== void 0 ? { readOnly: this.args.readOnly } : {},
1802
1843
  ...this.args.display ? { display: this.args.display } : {},
1803
1844
  // The SDK base exposes the bound runAs as a protected field.
1804
- ...typeof this._runAs === "string" ? { runAs: this._runAs } : {}
1845
+ ...typeof this._runAs === "string" ? { runAs: this._runAs } : {},
1846
+ // Crucially lazy: tool registration does no provider work. The first
1847
+ // click/type/screenshot boots the display, then lets the worker start
1848
+ // any computer-use-only recording. One SandboxComputer instance caches
1849
+ // this for the rest of the turn.
1850
+ prepare: async (preparedSession) => {
1851
+ await ensureDisplayStack(preparedSession);
1852
+ await this.args.onReady?.(preparedSession);
1853
+ }
1805
1854
  });
1806
1855
  switch (this.args.toolMode) {
1807
1856
  case "hosted":
1808
1857
  return [this.hostedComputerTool(computer)];
1809
1858
  case "function-image":
1810
- return computerFunctionTools(computer, this.args.readOnly ?? false, this.args.needsApproval, true);
1859
+ return computerFunctionTools(
1860
+ computer,
1861
+ this.args.readOnly ?? false,
1862
+ this.args.needsApproval,
1863
+ true
1864
+ );
1811
1865
  case "function-text":
1812
- return computerFunctionTools(computer, this.args.readOnly ?? false, this.args.needsApproval, false);
1866
+ return computerFunctionTools(
1867
+ computer,
1868
+ this.args.readOnly ?? false,
1869
+ this.args.needsApproval,
1870
+ false
1871
+ );
1813
1872
  case void 0:
1814
1873
  break;
1815
1874
  }
1816
1875
  if (supportsStructuredToolOutputTransport(this._modelInstance)) {
1817
1876
  return [this.hostedComputerTool(computer)];
1818
1877
  }
1819
- return computerFunctionTools(computer, this.args.readOnly ?? false, this.args.needsApproval, this.args.imageFunctionResults ?? false);
1878
+ return computerFunctionTools(
1879
+ computer,
1880
+ this.args.readOnly ?? false,
1881
+ this.args.needsApproval,
1882
+ this.args.imageFunctionResults ?? false
1883
+ );
1820
1884
  }
1821
1885
  /** The single HOSTED `computer_use_preview` tool bound to `computer` — identical
1822
1886
  * construction for the explicit "hosted" mode and the legacy structured-sniff path. */
@@ -1830,8 +1894,52 @@ var ComputerUseCapability = class extends Capability {
1830
1894
 
1831
1895
  // src/index.ts
1832
1896
  import { OpenAIChatCompletionsModel as OpenAIChatCompletionsModel2, OpenAIResponsesModel as OpenAIResponsesModel2 } from "@openai/agents";
1897
+
1898
+ // src/usage-telemetry.ts
1899
+ function modelCallUsageTelemetry(usage) {
1900
+ return {
1901
+ inputTokens: finiteNumberOrNull(usage?.inputTokens),
1902
+ outputTokens: finiteNumberOrNull(usage?.outputTokens),
1903
+ cachedTokens: usage ? firstPositiveDetailNumber(usage.inputTokensDetails, [
1904
+ "cached_tokens",
1905
+ "cachedInputTokens",
1906
+ "cached_input_tokens"
1907
+ ]) : null,
1908
+ reasoningTokens: usage ? firstPositiveDetailNumber(usage.outputTokensDetails, [
1909
+ "reasoning_tokens",
1910
+ "reasoningTokens",
1911
+ "reasoning_output_tokens"
1912
+ ]) : null
1913
+ };
1914
+ }
1915
+ function firstPositiveDetailNumber(details, keys) {
1916
+ if (!details) {
1917
+ return null;
1918
+ }
1919
+ const entries = Array.isArray(details) ? details : [details];
1920
+ let sawZero = false;
1921
+ for (const entry of entries) {
1922
+ for (const key of keys) {
1923
+ const value = finiteNumberOrNull(entry[key]);
1924
+ if (value !== null && value > 0) {
1925
+ return value;
1926
+ }
1927
+ if (value === 0) {
1928
+ sawZero = true;
1929
+ }
1930
+ }
1931
+ }
1932
+ return sawZero ? 0 : null;
1933
+ }
1934
+ function finiteNumberOrNull(value) {
1935
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1936
+ }
1937
+
1938
+ // src/index.ts
1833
1939
  import { MaxTurnsExceededError as MaxTurnsExceededError2 } from "@openai/agents";
1834
- setSelfhostedApplyDiff(applyDiff);
1940
+ setSelfhostedApplyDiff(
1941
+ applyDiff
1942
+ );
1835
1943
  ensureReadableStreamFrom();
1836
1944
  var SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS = 12e4;
1837
1945
  function ensureReadableStreamFrom() {
@@ -1899,7 +2007,7 @@ function buildOpenAIClientFromSettings(settings, providerId = settings.openaiPro
1899
2007
  defaultQuery: azureOpenAIDefaultQuery(settings, baseURL),
1900
2008
  defaultHeaders: settings.azureOpenaiAdToken && !settings.azureOpenaiApiKey ? { Authorization: `Bearer ${settings.azureOpenaiAdToken}` } : void 0,
1901
2009
  // Rewrite every outbound /responses computer_call to the ACTIONS-ONLY shape
1902
- // the GA Azure computer tool (gpt-5.5) accepts. This is the lowest reachable
2010
+ // the GA Azure computer tool accepts. This is the lowest reachable
1903
2011
  // seam — below the SDK responses converter, which always re-synthesizes BOTH
1904
2012
  // `action` and `actions` (rejected 400 "exactly one of action or actions").
1905
2013
  // See computerCallNormalizingFetch / rewriteComputerCallsToActionsOnly.
@@ -1919,19 +2027,35 @@ function buildProviderClient(provider, settings) {
1919
2027
  if (cached) {
1920
2028
  return cached;
1921
2029
  }
1922
- const client = provider.builtin ? buildOpenAIClientFromSettings(settings, provider.id) : provider.kind === "codex-subscription" ? new OpenAI({
1923
- apiKey: provider.apiKey ?? "codex-subscription",
1924
- ...provider.baseUrl ? { baseURL: provider.baseUrl } : {},
1925
- maxRetries: settings.openaiMaxRetries,
1926
- fetch: codexSubscriptionFetch(instrumentedModelFetch(provider.id, globalThis.fetch))
1927
- }) : new OpenAI({
1928
- ...provider.apiKey ? { apiKey: provider.apiKey } : {},
1929
- ...provider.baseUrl ? { baseURL: provider.baseUrl } : {},
1930
- maxRetries: settings.openaiMaxRetries,
1931
- ...provider.defaultQuery ? { defaultQuery: provider.defaultQuery } : {},
1932
- ...provider.defaultHeaders ? { defaultHeaders: provider.defaultHeaders } : {},
1933
- fetch: instrumentedModelFetch(provider.id, globalThis.fetch)
1934
- });
2030
+ const client = provider.builtin ? buildOpenAIClientFromSettings(settings, provider.id) : provider.kind === "codex-subscription" ? (
2031
+ // Codex subscription: the static apiKey is a placeholder — the real per-request
2032
+ // bearer + ChatGPT-Account-ID, the /responses->/codex/responses rewrite, and the
2033
+ // body normalization are all injected by codexSubscriptionFetch, which reads the
2034
+ // per-workspace token from codexRequestStorage (AsyncLocalStorage) at call time.
2035
+ // The provider id is constant ("codex-subscription"), so one cached client serves
2036
+ // every workspace without baking a token into it.
2037
+ new OpenAI({
2038
+ apiKey: provider.apiKey ?? "codex-subscription",
2039
+ ...provider.baseUrl ? { baseURL: provider.baseUrl } : {},
2040
+ // Codex transport owns exactly one explicit 401 refresh/retry. Blind
2041
+ // SDK retries on network/5xx/partial streams can replay provider work
2042
+ // or external tool side effects without a durable checkpoint.
2043
+ maxRetries: 0,
2044
+ fetch: codexSubscriptionFetch(instrumentedModelFetch(provider.id, globalThis.fetch))
2045
+ })
2046
+ ) : (
2047
+ // ResolvedModelProvider.apiKey is already the resolved key (configuredProviders
2048
+ // ran resolveProviderApiKey at config time, collapsing apiKey/apiKeyEnv), so it
2049
+ // is passed straight through here rather than re-resolved.
2050
+ new OpenAI({
2051
+ ...provider.apiKey ? { apiKey: provider.apiKey } : {},
2052
+ ...provider.baseUrl ? { baseURL: provider.baseUrl } : {},
2053
+ maxRetries: settings.openaiMaxRetries,
2054
+ ...provider.defaultQuery ? { defaultQuery: provider.defaultQuery } : {},
2055
+ ...provider.defaultHeaders ? { defaultHeaders: provider.defaultHeaders } : {},
2056
+ fetch: instrumentedModelFetch(provider.id, globalThis.fetch)
2057
+ })
2058
+ );
1935
2059
  providerClientCache.set(provider.id, client);
1936
2060
  return client;
1937
2061
  }
@@ -1955,11 +2079,13 @@ var MultiProviderModelProvider = class {
1955
2079
  constructor(settings) {
1956
2080
  this.settings = settings;
1957
2081
  }
1958
- settings;
1959
2082
  fallback;
1960
2083
  async getModel(modelName) {
1961
2084
  if (modelName) {
1962
- const resolved = resolveTurnModel(settingsForRunScopedModelResolution(this.settings, modelName), modelName);
2085
+ const resolved = resolveTurnModel(
2086
+ settingsForRunScopedModelResolution(this.settings, modelName),
2087
+ modelName
2088
+ );
1963
2089
  if (resolved) {
1964
2090
  if (modelName.startsWith(CODEX_MODEL_ID_PREFIX) && resolved.provider.kind !== "codex-subscription") {
1965
2091
  throw new CodexSubscriptionUnavailableError(modelName);
@@ -1996,6 +2122,14 @@ var CodexSubscriptionUnavailableError = class extends Error {
1996
2122
  this.name = "CodexSubscriptionUnavailableError";
1997
2123
  }
1998
2124
  };
2125
+ var WorkspaceModelPolicyBlockedError = class extends Error {
2126
+ constructor(modelName, providerId, reason) {
2127
+ super(
2128
+ reason === "provider" ? `Model "${modelName}" is not available in this workspace: its provider ("${providerId}") is not in the workspace's allowed providers. Pick an allowed model, or ask a workspace admin to change the workspace model policy.` : `Model "${modelName}" is not in this workspace's allowed models. Pick an allowed model, or ask a workspace admin to change the workspace model policy.`
2129
+ );
2130
+ this.name = "WorkspaceModelPolicyBlockedError";
2131
+ }
2132
+ };
1999
2133
  function configureOpenAI(settings) {
2000
2134
  setOpenAIResponsesTransport(settings.openaiResponsesTransport);
2001
2135
  setTracingDisabled(settings.disableOpenaiTracing || !settings.observabilityOtlpEndpoint);
@@ -2014,7 +2148,7 @@ function configureOpenAI(settings) {
2014
2148
  setDefaultModelProvider(router);
2015
2149
  }
2016
2150
  function instrumentedModelFetch(provider, inner) {
2017
- return (async (input, init) => {
2151
+ return async (input, init) => {
2018
2152
  if (!isModelCallFetch(input)) {
2019
2153
  return await inner(input, init);
2020
2154
  }
@@ -2027,7 +2161,7 @@ function instrumentedModelFetch(provider, inner) {
2027
2161
  recordModelCallMetric(provider, "failed", started);
2028
2162
  throw error;
2029
2163
  }
2030
- });
2164
+ };
2031
2165
  }
2032
2166
  function isModelCallFetch(input) {
2033
2167
  const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -2053,37 +2187,43 @@ async function summarizeForCompaction(settings, input, options = {}) {
2053
2187
  const api = options.api ?? "responses";
2054
2188
  const model = options.model ?? settings.openaiModel;
2055
2189
  const maxTokens = options.maxOutputTokens ?? SUMMARY_BUFFER_TOKENS;
2056
- const transcript = renderCompactionPromptInputForChat(
2057
- input,
2058
- typeof options.maxTranscriptTokens === "number" && options.maxTranscriptTokens > 0 ? { maxEstimatedTokens: options.maxTranscriptTokens } : {}
2059
- );
2060
- try {
2061
- if (api === "chat") {
2062
- const completion = await client.chat.completions.create({
2063
- model,
2064
- max_tokens: maxTokens,
2065
- messages: [{ role: "user", content: transcript }]
2066
- });
2067
- const text2 = completion.choices?.[0]?.message?.content;
2068
- const trimmed2 = typeof text2 === "string" ? text2.trim() : "";
2069
- return trimmed2.length > 0 ? trimmed2 : null;
2070
- }
2071
- const response = await client.responses.create({
2190
+ if (api === "chat") {
2191
+ const transcript = renderCompactionPromptInputForChat(input);
2192
+ const completion = await client.chat.completions.create({
2072
2193
  model,
2073
- // store:false is the OpenAI-platform-only storeless precondition; Azure
2074
- // rejects it. The summarizer's resolved client is OpenAI/Azure on the
2075
- // built-in path (api "responses"), so gate it on the built-in provider.
2076
- ...settings.openaiProvider === "azure" ? {} : { store: false },
2077
- max_output_tokens: maxTokens,
2078
- input: transcript
2194
+ max_tokens: maxTokens,
2195
+ messages: [{ role: "user", content: transcript }],
2196
+ ...options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}
2079
2197
  });
2080
- const text = extractResponseOutputText(response);
2081
- const trimmed = text.trim();
2082
- return trimmed.length > 0 ? trimmed : null;
2083
- } catch (error) {
2084
- console.error("context compaction summarize failed", error);
2085
- return null;
2198
+ const usage2 = modelResponseUsageFromResponse(completion);
2199
+ if (usage2) {
2200
+ await options.onUsage?.(usage2);
2201
+ }
2202
+ const text = completion.choices?.[0]?.message?.content;
2203
+ return typeof text === "string" ? text.trim() : "";
2204
+ }
2205
+ const request = {
2206
+ systemInstructions: options.systemInstructions ?? "",
2207
+ input,
2208
+ modelSettings: {
2209
+ maxTokens,
2210
+ // Azure rejects store:false; the Codex subscription transport enforces
2211
+ // it independently. The OpenAI platform path remains explicitly storeless.
2212
+ ...settings.openaiProvider === "azure" ? {} : { store: false },
2213
+ ...options.promptCacheKey ? { providerData: { prompt_cache_key: options.promptCacheKey } } : {}
2214
+ },
2215
+ tools: [],
2216
+ toolsExplicitlyProvided: true,
2217
+ outputType: "text",
2218
+ handoffs: [],
2219
+ tracing: false
2220
+ };
2221
+ const response = await new CompactionResponsesModel(client, model).fetchResponse(request);
2222
+ const usage = modelResponseUsageFromResponse(response);
2223
+ if (usage) {
2224
+ await options.onUsage?.(usage);
2086
2225
  }
2226
+ return extractResponseOutputText(response).trim();
2087
2227
  }
2088
2228
  function extractResponseOutputText(response) {
2089
2229
  if (!response || typeof response !== "object") {
@@ -2120,6 +2260,19 @@ function extractResponseOutputText(response) {
2120
2260
  }
2121
2261
  return parts.join("");
2122
2262
  }
2263
+ var CompactionResponsesModel = class extends OpenAIResponsesModel {
2264
+ fetchResponse(request) {
2265
+ return this._fetchResponse(request, false);
2266
+ }
2267
+ };
2268
+ function rigInstructions(rig) {
2269
+ return [
2270
+ `This session runs on rig "${rig.name}" (active version v${rig.version}) \u2014 a shared, versioned sandbox machine definition for your workspace.`,
2271
+ "Your sandbox is an EPHEMERAL FORK of that rig: you have root and may install anything freely, but everything you change here is junk that dies with the box and never reaches the rig or other sessions.",
2272
+ "For a DURABLE, team-wide change (tooling every future session on this rig should have), propose it with rig_propose_change, passing the EXACT command that already worked in this box \u2014 never assume an unverified change propagates.",
2273
+ "If tooling you expect is missing, consult rig_get to see the rig's current setup and checks before reinstalling."
2274
+ ];
2275
+ }
2123
2276
  function workspaceEnvironmentInstructions(environment) {
2124
2277
  const lines = [
2125
2278
  `A workspace environment named "${environment.name}" is attached to this session; its variables are exported in the sandbox shell environment.`
@@ -2134,14 +2287,17 @@ function workspaceEnvironmentInstructions(environment) {
2134
2287
  }
2135
2288
  return lines;
2136
2289
  }
2137
- function coreInstructions(workspaceEnvironment) {
2290
+ function coreInstructions(workspaceEnvironment, rig) {
2138
2291
  return [
2139
2292
  "If the session has a goal, you own it: keep working until you call opengeni__goal_complete with concrete evidence or opengeni__goal_pause with a rationale; revise it with opengeni__goal_update; create one with opengeni__goal_set when given a long-running objective.",
2140
- ...workspaceEnvironment ? workspaceEnvironmentInstructions(workspaceEnvironment) : []
2293
+ ...workspaceEnvironment ? workspaceEnvironmentInstructions(workspaceEnvironment) : [],
2294
+ // Rig doctrine (M3): data-conditional, inside the non-bypassable CORE so a
2295
+ // white-label persona template can never drop it. Absent for rig-less sessions.
2296
+ ...rig ? rigInstructions(rig) : []
2141
2297
  ];
2142
2298
  }
2143
- function composeAgentInstructions(template, workspaceEnvironment) {
2144
- const core = coreInstructions(workspaceEnvironment).join(" ");
2299
+ function composeAgentInstructions(template, workspaceEnvironment, rig) {
2300
+ const core = coreInstructions(workspaceEnvironment, rig).join(" ");
2145
2301
  if (template.includes(AGENT_INSTRUCTIONS_CORE_PLACEHOLDER)) {
2146
2302
  return template.split(AGENT_INSTRUCTIONS_CORE_PLACEHOLDER).join(core);
2147
2303
  }
@@ -2151,27 +2307,52 @@ function appendSessionInstructions(composed, sessionInstructions) {
2151
2307
  const trimmed = sessionInstructions?.trim();
2152
2308
  return trimmed ? `${composed} ${trimmed}` : composed;
2153
2309
  }
2154
- function appendToolspaceInstructions(composed, toolspaceAvailable) {
2155
- return toolspaceAvailable ? `${composed} ${TOOLSPACE_PROGRAMMATIC_DIRECTIVE}` : composed;
2310
+ function appendPersistentSessionSettings(composed, settings) {
2311
+ if (!settings) {
2312
+ return composed;
2313
+ }
2314
+ const childState = settings.childNotificationsMode ? `; spawned-worker completion notifications are ${settings.childNotificationsMode}` : "";
2315
+ const persistentTools = settings.childNotificationsMode ? "opengeni__set_session_title or opengeni__set_child_notifications_mode" : "opengeni__set_session_title";
2316
+ return `${composed} Persistent session settings already in effect: the display title is ${settings.titleIsSet ? "set" : "not set"}${childState}. These settings persist across turns, continuations, and interruptions. Do not call ${persistentTools} merely to reassert an unchanged value; call them only when the current value actually needs to change.`;
2317
+ }
2318
+ function appendWorkspaceMemory(composed, workspaceMemory) {
2319
+ const trimmed = workspaceMemory?.trim();
2320
+ return trimmed ? `${composed} ${trimmed}` : composed;
2321
+ }
2322
+ function appendToolspaceInstructions(composed, toolspaceAvailable) {
2323
+ return toolspaceAvailable ? `${composed} ${TOOLSPACE_PROGRAMMATIC_DIRECTIVE}` : composed;
2156
2324
  }
2157
2325
  function appendGenesisTitleDirective(instructions, genesisTitleHint) {
2158
2326
  return genesisTitleHint ? `${instructions} ${GENESIS_TITLE_DIRECTIVE}` : instructions;
2159
2327
  }
2160
2328
  var agentFileDownloads = /* @__PURE__ */ new WeakMap();
2161
2329
  var agentRepositoryCloneHooks = /* @__PURE__ */ new WeakMap();
2162
- var agentGitTokenSeed = /* @__PURE__ */ new WeakMap();
2330
+ var agentGitTokenSeeds = /* @__PURE__ */ new WeakMap();
2163
2331
  var agentToolspaceTokenSeed = /* @__PURE__ */ new WeakMap();
2332
+ var agentsNeedingGenesisTitleDirective = /* @__PURE__ */ new WeakSet();
2164
2333
  var agentActiveSandboxBackend = /* @__PURE__ */ new WeakMap();
2334
+ var agentRigSetup = /* @__PURE__ */ new WeakMap();
2335
+ var agentRigCredentialHooks = /* @__PURE__ */ new WeakMap();
2165
2336
  function mcpToolErrorOutput(error) {
2166
2337
  const details = error instanceof Error ? error.message : String(error);
2167
- return { isError: true, content: [{ type: "text", text: `An error occurred while running the tool. Please try again. Error: ${details}` }] };
2338
+ return {
2339
+ isError: true,
2340
+ content: [
2341
+ {
2342
+ type: "text",
2343
+ text: `An error occurred while running the tool. Please try again. Error: ${details}`
2344
+ }
2345
+ ]
2346
+ };
2168
2347
  }
2169
2348
  var mcpToolErrorFunction = ({ error }) => mcpToolErrorOutput(error);
2170
2349
  function buildOpenGeniAgent(settings, resources, options = {}) {
2171
- const compactionMode = options.compactionMode ?? resolveContextCompactionMode(settings);
2172
2350
  const hostedWebSearch = options.hostedWebSearch ?? settings.webSearchEnabled;
2173
2351
  const encryptedReasoning = options.encryptedReasoning ?? settings.openaiReasoningEncryptedContent;
2174
- const contextWindowTokens = options.contextWindowTokens ?? settings.contextWindowTokens;
2352
+ const providerData = {
2353
+ ...encryptedReasoning ? { include: ["reasoning.encrypted_content"] } : {},
2354
+ ...options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}
2355
+ };
2175
2356
  const hostedTools = hostedWebSearch ? [webSearchTool()] : [];
2176
2357
  const baseConfig = {
2177
2358
  name: "OpenGeni Agent",
@@ -2191,32 +2372,36 @@ function buildOpenGeniAgent(settings, resources, options = {}) {
2191
2372
  // when a toolspace token was minted for this turn (feature enabled + a
2192
2373
  // per-turn seed — the mint gate, which includes selfhosted turns since
2193
2374
  // they now receive the token too) — appendToolspaceInstructions,
2194
- // 3. + the per-session persona instructions (session-specific, so it
2375
+ // 3. + workspace memory working set, ONLY when the workspace setting is on
2376
+ // and the worker resolved a nonblank block — appendWorkspaceMemory,
2377
+ // 4. + the per-session persona instructions (session-specific, so it
2195
2378
  // refines both the workspace persona and the substrate note),
2196
- // 4. + the one-shot genesis title directive (genesis turn only).
2197
- // With no toolspace token, no session instructions, and no genesis hint this
2198
- // is byte-identical to the historical composed instructions.
2199
- instructions: appendGenesisTitleDirective(
2379
+ // 5. + durable session-setting state (title present + child notification
2380
+ // mode), when supplied by the worker,
2381
+ // The genesis title directive is deliberately NOT part of this persistent
2382
+ // string. runAgentStream injects it into the first model call only.
2383
+ instructions: appendPersistentSessionSettings(
2200
2384
  appendSessionInstructions(
2201
- appendToolspaceInstructions(
2202
- composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
2203
- settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed)
2385
+ appendWorkspaceMemory(
2386
+ appendToolspaceInstructions(
2387
+ composeAgentInstructions(
2388
+ options.instructionsTemplate ?? settings.agentInstructionsTemplate,
2389
+ options.workspaceEnvironment,
2390
+ options.rig
2391
+ ),
2392
+ settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed)
2393
+ ),
2394
+ options.workspaceMemory
2204
2395
  ),
2205
2396
  options.sessionInstructions
2206
2397
  ),
2207
- options.genesisTitleHint
2398
+ options.persistentSessionSettings
2208
2399
  ),
2209
2400
  modelSettings: {
2210
- reasoning: { effort: options.reasoningEffort ?? settings.openaiReasoningEffort, summary: "detailed" },
2211
- // Server-side compaction (OpenAI platform) requires store=false: the
2212
- // server emits an opaque ENCRYPTED 'compaction' item that round-trips in
2213
- // the request rather than being anchored to a stored response. OpenGeni
2214
- // already runs storeless (provider item ids stripped, encrypted reasoning
2215
- // round-tripped), so this is consistent with the existing design and
2216
- // only set where the server compaction capability is attached. Gated on
2217
- // the RESOLVED compaction mode (registry providers resolve to "client",
2218
- // so they never carry store:false).
2219
- ...compactionMode === "server" ? { store: false } : {},
2401
+ reasoning: {
2402
+ effort: options.reasoningEffort ?? settings.openaiReasoningEffort,
2403
+ summary: "detailed"
2404
+ },
2220
2405
  // Round-trip the encrypted reasoning payload with every call so chains
2221
2406
  // of thought survive without provider-side response storage (which is
2222
2407
  // what stripped provider item ids opt us out of — see
@@ -2225,7 +2410,7 @@ function buildOpenGeniAgent(settings, resources, options = {}) {
2225
2410
  // function tools, which contribute none. Gated on the resolved
2226
2411
  // encryptedReasoning flag: the chat wire API has no encrypted_content
2227
2412
  // field, so registry "chat" providers turn it off.
2228
- ...encryptedReasoning ? { providerData: { include: ["reasoning.encrypted_content"] } } : {}
2413
+ ...Object.keys(providerData).length > 0 ? { providerData } : {}
2229
2414
  },
2230
2415
  // Explicit hosted tools (web_search when enabled). Threaded into BOTH the
2231
2416
  // `new Agent(baseConfig)` path (sandboxBackend === "none") and the
@@ -2243,6 +2428,9 @@ function buildOpenGeniAgent(settings, resources, options = {}) {
2243
2428
  };
2244
2429
  if (settings.sandboxBackend === "none") {
2245
2430
  const agent2 = new Agent(baseConfig);
2431
+ if (options.genesisTitleHint) {
2432
+ agentsNeedingGenesisTitleDirective.add(agent2);
2433
+ }
2246
2434
  maybeInstallCodexToolSearch(agent2, settings, options);
2247
2435
  applyMcpApprovalPolicy(agent2, settings);
2248
2436
  return agent2;
@@ -2250,26 +2438,51 @@ function buildOpenGeniAgent(settings, resources, options = {}) {
2250
2438
  const runAs = sandboxRunAs(settings);
2251
2439
  const agent = new SandboxAgent({
2252
2440
  ...baseConfig,
2253
- defaultManifest: buildManifest(settings, resources, options.sandboxEnvironment, options.fileResourceDownloads),
2441
+ defaultManifest: buildManifest(
2442
+ settings,
2443
+ resources,
2444
+ options.sandboxEnvironment,
2445
+ options.fileResourceDownloads
2446
+ ),
2254
2447
  ...runAs ? { runAs } : {},
2255
2448
  capabilities: buildAgentCapabilities(settings, options.packSkills ?? [], {
2256
- compactionMode,
2257
- contextWindowTokens,
2258
2449
  ...options.structuredToolTransport !== void 0 ? { structuredToolTransport: options.structuredToolTransport } : {},
2259
- ...options.computerToolMode !== void 0 ? { computerToolMode: options.computerToolMode } : {}
2450
+ ...options.computerToolMode !== void 0 ? { computerToolMode: options.computerToolMode } : {},
2451
+ ...options.onComputerUseReady ? { onComputerUseReady: options.onComputerUseReady } : {}
2260
2452
  })
2261
2453
  });
2262
- agentFileDownloads.set(agent, normalizeSandboxFileDownloads(options.fileResourceDownloads ?? []).filter((download) => !download.content));
2263
- agentRepositoryCloneHooks.set(agent, sandboxRepositoryCloneHooks(settings, resources, options.activeSandboxBackend));
2454
+ if (options.genesisTitleHint) {
2455
+ agentsNeedingGenesisTitleDirective.add(agent);
2456
+ }
2457
+ agentFileDownloads.set(
2458
+ agent,
2459
+ normalizeSandboxFileDownloads(options.fileResourceDownloads ?? []).filter(
2460
+ (download) => !download.content
2461
+ )
2462
+ );
2463
+ agentRepositoryCloneHooks.set(
2464
+ agent,
2465
+ sandboxRepositoryCloneHooks(settings, resources, options.activeSandboxBackend)
2466
+ );
2264
2467
  if (options.activeSandboxBackend) {
2265
2468
  agentActiveSandboxBackend.set(agent, options.activeSandboxBackend);
2266
2469
  }
2267
- if (options.gitTokenSeed) {
2268
- agentGitTokenSeed.set(agent, options.gitTokenSeed);
2470
+ const gitTokenSeeds = {
2471
+ ...options.gitTokenSeeds ?? {},
2472
+ ...options.gitTokenSeed ? { github: options.gitTokenSeed } : {}
2473
+ };
2474
+ if (Object.keys(gitTokenSeeds).length > 0) {
2475
+ agentGitTokenSeeds.set(agent, gitTokenSeeds);
2269
2476
  }
2270
2477
  if (options.toolspaceTokenSeed) {
2271
2478
  agentToolspaceTokenSeed.set(agent, options.toolspaceTokenSeed);
2272
2479
  }
2480
+ if (options.rigSetup) {
2481
+ agentRigSetup.set(agent, options.rigSetup);
2482
+ }
2483
+ if (options.rigCredentialHookIds && options.rigCredentialHookIds.length > 0) {
2484
+ agentRigCredentialHooks.set(agent, sandboxLifecycleHooksForIds(options.rigCredentialHookIds));
2485
+ }
2273
2486
  maybeInstallCodexToolSearch(agent, settings, options);
2274
2487
  applyMcpApprovalPolicy(agent, settings);
2275
2488
  return agent;
@@ -2311,7 +2524,12 @@ function installMcpApprovalPolicy(agent, policies) {
2311
2524
  }
2312
2525
  }
2313
2526
  function applyMcpApprovalPolicy(agent, settings) {
2314
- const policies = settings.mcpServers.filter((server) => server.requireApproval === true || Array.isArray(server.requireApproval) && server.requireApproval.length > 0).map((server) => ({ prefix: prefixedMcpToolName(server.id, ""), requireApproval: server.requireApproval })).sort((a, b) => b.prefix.length - a.prefix.length);
2527
+ const policies = settings.mcpServers.filter(
2528
+ (server) => server.requireApproval === true || Array.isArray(server.requireApproval) && server.requireApproval.length > 0
2529
+ ).map((server) => ({
2530
+ prefix: prefixedMcpToolName(server.id, ""),
2531
+ requireApproval: server.requireApproval
2532
+ })).sort((a, b) => b.prefix.length - a.prefix.length);
2315
2533
  if (policies.length === 0) {
2316
2534
  return;
2317
2535
  }
@@ -2324,24 +2542,67 @@ function neutralizeStructuredToolTransport(capability) {
2324
2542
  };
2325
2543
  capability.bindModel = forceFunctionTransport;
2326
2544
  }
2545
+ function withExecOpCorrelation(tools) {
2546
+ return tools.map((capabilityTool) => {
2547
+ if (capabilityTool.type !== "function" || capabilityTool.name !== "exec_command") {
2548
+ return capabilityTool;
2549
+ }
2550
+ const invoke = capabilityTool.invoke;
2551
+ return {
2552
+ ...capabilityTool,
2553
+ invoke: (runContext, input, details) => {
2554
+ const callId = details?.toolCall?.callId;
2555
+ if (!callId) {
2556
+ return invoke(runContext, input, details);
2557
+ }
2558
+ return runWithToolCallCorrelation(callId, () => invoke(runContext, input, details));
2559
+ }
2560
+ };
2561
+ });
2562
+ }
2563
+ function withStructuredViewImageFunctionResults(tools) {
2564
+ return tools.map((capabilityTool) => {
2565
+ if (capabilityTool.type !== "function" || capabilityTool.name !== "view_image") {
2566
+ return capabilityTool;
2567
+ }
2568
+ const invoke = capabilityTool.invoke;
2569
+ return {
2570
+ ...capabilityTool,
2571
+ invoke: async (runContext, input, details) => {
2572
+ const output = await invoke(runContext, input, details);
2573
+ const dataUrl = typeof output === "string" ? output : output && typeof output === "object" && typeof output.text === "string" ? output.text : null;
2574
+ return dataUrl?.startsWith("data:image/") ? { type: "image", image: { url: dataUrl } } : output;
2575
+ }
2576
+ };
2577
+ });
2578
+ }
2327
2579
  function buildAgentCapabilities(settings, packSkills, options = {}) {
2328
- const mode = options.compactionMode ?? resolveContextCompactionMode(settings);
2329
- const contextWindowTokens = options.contextWindowTokens ?? settings.contextWindowTokens;
2330
- const filesystemCapability = filesystem();
2580
+ const filesystemCapability = filesystem({
2581
+ ...options.structuredToolTransport === false ? { configureTools: withStructuredViewImageFunctionResults } : {}
2582
+ });
2331
2583
  if (options.structuredToolTransport === false) {
2332
2584
  neutralizeStructuredToolTransport(filesystemCapability);
2333
2585
  }
2334
- const caps = [filesystemCapability, shell()];
2335
- if (mode === "server") {
2336
- caps.push(compaction({ policy: new StaticCompactionPolicy(contextServerCompactThreshold({ ...settings, contextWindowTokens })) }));
2337
- }
2586
+ const caps = [
2587
+ filesystemCapability,
2588
+ shell({ configureTools: withExecOpCorrelation })
2589
+ ];
2338
2590
  caps.push(skills({ lazyFrom: lazySkillSourceWithPackSkills(packSkills) }));
2339
2591
  if (settings.computerUseEnabled && settings.sandboxDesktopEnabled && desktopCapableBackend(settings.sandboxBackend)) {
2340
2592
  const explicitMode = options.computerToolMode;
2341
2593
  const computerCapability = computerUse({
2342
2594
  dimensions: [settings.streamResolutionWidth, settings.streamResolutionHeight],
2343
2595
  readOnly: settings.computerUseReadOnly,
2344
- ...explicitMode ? { toolMode: explicitMode } : options.structuredToolTransport === false ? { imageFunctionResults: true } : {}
2596
+ ...options.onComputerUseReady ? { onReady: options.onComputerUseReady } : {},
2597
+ ...explicitMode ? { toolMode: explicitMode } : (
2598
+ // Legacy (no explicit mode): on the codex path the function tools deliver
2599
+ // screenshots as a real image the model can see. The ChatGPT/Codex backend
2600
+ // rejects HOSTED tool types but DOES accept `input_image` content items inside a
2601
+ // `function_call_output` (proven by openai/codex codex-rs, whose view_image tool
2602
+ // ships exactly that shape) — so a structured image tool result is seen, where a
2603
+ // text data-URL would be unreadable.
2604
+ options.structuredToolTransport === false ? { imageFunctionResults: true } : {}
2605
+ )
2345
2606
  });
2346
2607
  if (!explicitMode && options.structuredToolTransport === false) {
2347
2608
  neutralizeStructuredToolTransport(computerCapability);
@@ -2360,32 +2621,44 @@ async function prepareAgentTools(settings, tools, options = {}) {
2360
2621
  }, codexConnectorNamespaces };
2361
2622
  }
2362
2623
  const registry = new Map(settings.mcpServers.map((server) => [server.id, server]));
2363
- const servers = await Promise.all(tools.map(async (tool2) => {
2364
- const config = registry.get(tool2.id);
2365
- if (!config) {
2366
- throw new Error(`Unknown MCP server id: ${tool2.id}`);
2367
- }
2368
- const url = firstPartyMcpServerUrlForRun(settings, config, options.workspaceId) ?? config.url;
2369
- const baseFetch = isCodexAppsMcpServer(config) ? codexAppsSanitizingFetch(globalThis.fetch, codexConnectorNamespaces) : globalThis.fetch;
2370
- const fetchImpl = config.connectionRef ? connectionBrokerFetch(baseFetch, config, options) : baseFetch;
2371
- const server = new PrefixedMcpServer(new MCPServerStreamableHttp({
2372
- url,
2373
- name: config.name ?? config.id,
2374
- cacheToolsList: config.cacheToolsList,
2375
- // codex_apps returns connector tools with empty `outputSchema: {}` that the
2376
- // MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
2377
- // sanitize the response on the wire before validation. The namespace Set
2378
- // also captures each tool's original connector namespace (P4 Part B.1).
2379
- ...fetchImpl !== globalThis.fetch ? { fetch: fetchImpl } : {},
2380
- ...await mcpServerRequestInit(settings, config, options),
2381
- ...config.timeoutMs ? {
2382
- timeout: config.timeoutMs,
2383
- clientSessionTimeoutSeconds: Math.ceil(config.timeoutMs / 1e3)
2384
- } : {}
2385
- }), config.id, config.allowedTools);
2386
- const optional = tool2.optional === true;
2387
- return { server, bestEffort: isCodexAppsMcpServer(config) || optional || !!config.connectionRef, optional };
2388
- }));
2624
+ const servers = await Promise.all(
2625
+ tools.map(async (tool2) => {
2626
+ const config = registry.get(tool2.id);
2627
+ if (!config) {
2628
+ throw new Error(`Unknown MCP server id: ${tool2.id}`);
2629
+ }
2630
+ const url = firstPartyMcpServerUrlForRun(settings, config, options.workspaceId) ?? config.url;
2631
+ const baseFetch = isCodexAppsMcpServer(config) ? codexAppsSanitizingFetch(globalThis.fetch, codexConnectorNamespaces) : globalThis.fetch;
2632
+ const fetchImpl = config.connectionRef ? connectionBrokerFetch(baseFetch, config, options) : isFirstPartyMcpServer(settings, config) ? firstPartyAuthFetch(baseFetch, settings, options) : baseFetch;
2633
+ const optional = tool2.optional === true;
2634
+ const bestEffort = isCodexAppsMcpServer(config) || optional || !!config.connectionRef;
2635
+ const server = new PrefixedMcpServer(
2636
+ new MCPServerStreamableHttp({
2637
+ url,
2638
+ name: config.name ?? config.id,
2639
+ cacheToolsList: config.cacheToolsList,
2640
+ // codex_apps returns connector tools with empty `outputSchema: {}` that the
2641
+ // MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
2642
+ // sanitize the response on the wire before validation. The namespace Set
2643
+ // also captures each tool's original connector namespace (P4 Part B.1).
2644
+ ...fetchImpl !== globalThis.fetch ? { fetch: fetchImpl } : {},
2645
+ ...await mcpServerRequestInit(settings, config),
2646
+ ...config.timeoutMs ? {
2647
+ timeout: config.timeoutMs,
2648
+ clientSessionTimeoutSeconds: Math.ceil(config.timeoutMs / 1e3)
2649
+ } : {}
2650
+ }),
2651
+ config.id,
2652
+ config.allowedTools,
2653
+ bestEffort
2654
+ );
2655
+ return {
2656
+ server,
2657
+ bestEffort,
2658
+ optional
2659
+ };
2660
+ })
2661
+ );
2389
2662
  const requiredServers = servers.filter((entry) => !entry.bestEffort).map((entry) => entry.server);
2390
2663
  const bestEffortServers = servers.filter((entry) => entry.bestEffort).map((entry) => entry.server);
2391
2664
  const optionalServerNames = new Set(
@@ -2429,30 +2702,54 @@ function connectionBrokerFetch(baseFetch, config, options) {
2429
2702
  }
2430
2703
  return async (input, init) => {
2431
2704
  const request = await mcpRequestInfo(input, init);
2432
- const first = await resolveConnectionForRequest(options, config.id, connectionRef, request.toolName, false);
2705
+ const first = await resolveConnectionForRequest(
2706
+ options,
2707
+ config.id,
2708
+ connectionRef,
2709
+ request.toolName,
2710
+ false
2711
+ );
2433
2712
  if (first.status === "auth_needed") {
2434
2713
  return await authNeededFetchResponse(options, config.id, request, first, connectionRef);
2435
2714
  }
2436
- const response = await baseFetch(fetchInputForAttempt(input), withConnectionHeaders(input, init, first.headers));
2715
+ const response = await baseFetch(
2716
+ fetchInputForAttempt(input),
2717
+ withConnectionHeaders(input, init, first.headers)
2718
+ );
2437
2719
  if (response.status === 401) {
2438
- const refreshed = await resolveConnectionForRequest(options, config.id, connectionRef, request.toolName, true);
2720
+ const refreshed = await resolveConnectionForRequest(
2721
+ options,
2722
+ config.id,
2723
+ connectionRef,
2724
+ request.toolName,
2725
+ true
2726
+ );
2439
2727
  if (refreshed.status === "auth_needed") {
2440
2728
  return await authNeededFetchResponse(options, config.id, request, refreshed, connectionRef);
2441
2729
  }
2442
- const retry = await baseFetch(fetchInputForAttempt(input), withConnectionHeaders(input, init, refreshed.headers));
2730
+ const retry = await baseFetch(
2731
+ fetchInputForAttempt(input),
2732
+ withConnectionHeaders(input, init, refreshed.headers)
2733
+ );
2443
2734
  if (retry.status === 403) {
2444
2735
  const auth = insufficientScopeAuth(retry.headers, connectionRef, refreshed.connectionId);
2445
2736
  return auth ? await authNeededFetchResponse(options, config.id, request, auth, connectionRef) : retry;
2446
2737
  }
2447
2738
  if (retry.status === 401) {
2448
- return await authNeededFetchResponse(options, config.id, request, {
2449
- status: "auth_needed",
2450
- reason: "expired",
2451
- providerDomain: connectionRef.providerDomain,
2452
- connectionId: refreshed.connectionId,
2453
- ...connectionRef.scopes ? { scopes: connectionRef.scopes } : {},
2454
- ...connectionRef.resource ? { resource: connectionRef.resource } : {}
2455
- }, connectionRef);
2739
+ return await authNeededFetchResponse(
2740
+ options,
2741
+ config.id,
2742
+ request,
2743
+ {
2744
+ status: "auth_needed",
2745
+ reason: "expired",
2746
+ providerDomain: connectionRef.providerDomain,
2747
+ connectionId: refreshed.connectionId,
2748
+ ...connectionRef.scopes ? { scopes: connectionRef.scopes } : {},
2749
+ ...connectionRef.resource ? { resource: connectionRef.resource } : {}
2750
+ },
2751
+ connectionRef
2752
+ );
2456
2753
  }
2457
2754
  return retry;
2458
2755
  }
@@ -2553,7 +2850,9 @@ async function mcpRequestInfo(input, init) {
2553
2850
  }
2554
2851
  }
2555
2852
  function withConnectionHeaders(input, init, authHeaders) {
2556
- const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
2853
+ const headers = new Headers(
2854
+ init?.headers ?? (input instanceof Request ? input.headers : void 0)
2855
+ );
2557
2856
  for (const [name, value] of Object.entries(authHeaders)) {
2558
2857
  headers.set(name, value);
2559
2858
  }
@@ -2587,17 +2886,20 @@ function parseWwwAuthenticate(header) {
2587
2886
  var MCP_AUTH_NEEDED_ERROR_CODE = -32001;
2588
2887
  var MCP_AUTH_NEEDED_MESSAGE = "Authentication required - a connection link was posted to the session.";
2589
2888
  function mcpToolAuthNeededResponse(id) {
2590
- return new Response(JSON.stringify({
2591
- jsonrpc: "2.0",
2592
- id: id ?? null,
2593
- error: {
2594
- code: MCP_AUTH_NEEDED_ERROR_CODE,
2595
- message: MCP_AUTH_NEEDED_MESSAGE
2596
- }
2597
- }), {
2598
- status: 200,
2599
- headers: { "content-type": "application/json" }
2600
- });
2889
+ return new Response(
2890
+ JSON.stringify({
2891
+ jsonrpc: "2.0",
2892
+ id: id ?? null,
2893
+ error: {
2894
+ code: MCP_AUTH_NEEDED_ERROR_CODE,
2895
+ message: MCP_AUTH_NEEDED_MESSAGE
2896
+ }
2897
+ }),
2898
+ {
2899
+ status: 200,
2900
+ headers: { "content-type": "application/json" }
2901
+ }
2902
+ );
2601
2903
  }
2602
2904
  function isAuthNeededMcpError(error) {
2603
2905
  if (!(error instanceof Error)) {
@@ -2606,19 +2908,35 @@ function isAuthNeededMcpError(error) {
2606
2908
  const code = error.code;
2607
2909
  return code === MCP_AUTH_NEEDED_ERROR_CODE || error.message.includes(MCP_AUTH_NEEDED_MESSAGE);
2608
2910
  }
2609
- async function mcpServerRequestInit(settings, config, options) {
2911
+ function mcpToolUnavailableMessage(reason) {
2912
+ return `This tool is unavailable for the rest of this turn (${reason}). Do not retry it \u2014 continue without it or use another approach.`;
2913
+ }
2914
+ function safeMcpErrorFields(error) {
2915
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
2916
+ const raw = error?.code;
2917
+ const status = typeof raw === "number" ? raw : void 0;
2918
+ if (status === void 0) {
2919
+ const altRaw = error?.status;
2920
+ return typeof altRaw === "number" ? { errorClass, status: altRaw } : { errorClass };
2921
+ }
2922
+ return { errorClass, status };
2923
+ }
2924
+ function mcpErrorReason(fields) {
2925
+ return fields.status === void 0 ? fields.errorClass : `${fields.errorClass} ${fields.status}`;
2926
+ }
2927
+ async function mcpServerRequestInit(settings, config) {
2610
2928
  if (isCodexAppsMcpServer(config)) {
2611
2929
  return await codexAppsMcpRequestInit(settings);
2612
2930
  }
2613
2931
  if (isFirstPartyMcpServer(settings, config)) {
2614
- return await firstPartyMcpRequestInit(settings, config, options);
2932
+ return await firstPartyMcpRequestInit(settings, config);
2615
2933
  }
2616
2934
  if (config.headers && Object.keys(config.headers).length > 0) {
2617
2935
  return { requestInit: { headers: { ...config.headers } } };
2618
2936
  }
2619
2937
  return {};
2620
2938
  }
2621
- async function firstPartyMcpRequestInit(settings, config, options) {
2939
+ async function firstPartyMcpRequestInit(settings, config) {
2622
2940
  if (!isFirstPartyMcpServer(settings, config)) {
2623
2941
  return {};
2624
2942
  }
@@ -2626,17 +2944,6 @@ async function firstPartyMcpRequestInit(settings, config, options) {
2626
2944
  if (settings.authRequired && settings.accessKey) {
2627
2945
  headers["x-opengeni-access-key"] = settings.accessKey;
2628
2946
  }
2629
- if (settings.delegationSecret && options.accountId && options.workspaceId) {
2630
- headers.authorization = `Bearer ${await signDelegatedAccessToken(settings.delegationSecret, {
2631
- accountId: options.accountId,
2632
- workspaceId: options.workspaceId,
2633
- subjectId: options.subjectId ?? "worker:first-party-mcp",
2634
- ...options.subjectLabel ? { subjectLabel: options.subjectLabel } : {},
2635
- permissions: options.firstPartyPermissions ?? firstPartyMcpPermissions,
2636
- ...options.sessionId ? { sessionId: options.sessionId } : {},
2637
- exp: Math.floor(Date.now() / 1e3) + 60 * 60
2638
- })}`;
2639
- }
2640
2947
  if (Object.keys(headers).length === 0) {
2641
2948
  return {};
2642
2949
  }
@@ -2646,6 +2953,33 @@ async function firstPartyMcpRequestInit(settings, config, options) {
2646
2953
  }
2647
2954
  };
2648
2955
  }
2956
+ async function signFirstPartyDelegatedBearer(settings, options) {
2957
+ if (!settings.delegationSecret || !options.accountId || !options.workspaceId) {
2958
+ return null;
2959
+ }
2960
+ return await signDelegatedAccessToken(settings.delegationSecret, {
2961
+ accountId: options.accountId,
2962
+ workspaceId: options.workspaceId,
2963
+ subjectId: options.subjectId ?? "worker:first-party-mcp",
2964
+ ...options.subjectLabel ? { subjectLabel: options.subjectLabel } : {},
2965
+ permissions: options.firstPartyPermissions ?? firstPartyMcpPermissions,
2966
+ ...options.sessionId ? { sessionId: options.sessionId } : {},
2967
+ ...options.turnId ? { turnId: options.turnId } : {},
2968
+ exp: Math.floor(Date.now() / 1e3) + 60 * 60
2969
+ });
2970
+ }
2971
+ function firstPartyAuthFetch(baseFetch, settings, options) {
2972
+ return async (input, init) => {
2973
+ const bearer = await signFirstPartyDelegatedBearer(settings, options);
2974
+ if (!bearer) {
2975
+ return await baseFetch(input, init);
2976
+ }
2977
+ return await baseFetch(
2978
+ fetchInputForAttempt(input),
2979
+ withConnectionHeaders(input, init, { authorization: `Bearer ${bearer}` })
2980
+ );
2981
+ };
2982
+ }
2649
2983
  async function codexAppsMcpRequestInit(settings) {
2650
2984
  const ctx = codexRequestStorage.getStore();
2651
2985
  if (!ctx) {
@@ -2685,8 +3019,9 @@ var firstPartyMcpPermissions = [
2685
3019
  "sessions:read",
2686
3020
  "sessions:create",
2687
3021
  "sessions:control",
2688
- "environments:use",
2689
- "environments:manage",
3022
+ "variable-sets:use",
3023
+ "variable-sets:manage",
3024
+ "rigs:use",
2690
3025
  "github:use"
2691
3026
  ];
2692
3027
  function isCodexAppsMcpServer(config) {
@@ -2715,7 +3050,11 @@ function firstPartyMcpServerUrlForRun(settings, config, workspaceId) {
2715
3050
  if (!isFirstPartyMcpServer(settings, config)) {
2716
3051
  return null;
2717
3052
  }
2718
- const rawBase = settings.opengeniMcpUrl?.includes("{workspaceId}") ? settings.opengeniMcpUrl.replaceAll("{workspaceId}", workspaceId) : settings.opengeniMcpUrl ? scopedMcpUrlFromConfiguredBase(settings.opengeniMcpUrl, workspaceId) : firstPartyMcpBaseUrl(settings).replaceAll("{workspaceId}", workspaceId);
3053
+ const rawBase = settings.opengeniMcpUrl?.includes("{workspaceId}") ? settings.opengeniMcpUrl.replaceAll("{workspaceId}", workspaceId) : settings.opengeniMcpUrl ? scopedMcpUrlFromConfiguredBase(settings.opengeniMcpUrl, workspaceId) : (
3054
+ // unset → the shared loopback default (a `{workspaceId}` template owned by
3055
+ // @opengeni/config's firstPartyMcpBaseUrl), scoped to this run's workspace.
3056
+ firstPartyMcpBaseUrl(settings).replaceAll("{workspaceId}", workspaceId)
3057
+ );
2719
3058
  const url = new URL(rawBase);
2720
3059
  if (config.id === "docs") {
2721
3060
  url.pathname = `${url.pathname.replace(/\/+$/, "")}/docs`;
@@ -2752,18 +3091,24 @@ function prefixedMcpToolName(registryId, toolName) {
2752
3091
  return sharedPrefixedMcpToolName(registryId, toolName);
2753
3092
  }
2754
3093
  var PrefixedMcpServer = class {
2755
- constructor(inner, registryId, allowedTools) {
3094
+ constructor(inner, registryId, allowedTools, bestEffort = false) {
2756
3095
  this.inner = inner;
2757
3096
  this.name = registryId;
2758
3097
  this.prefix = prefixedMcpToolName(registryId, "");
2759
3098
  this.cacheToolsList = inner.cacheToolsList;
2760
3099
  this.allowedTools = allowedTools ? new Set(allowedTools) : void 0;
3100
+ this.bestEffort = bestEffort;
2761
3101
  }
2762
- inner;
2763
3102
  cacheToolsList;
2764
3103
  name;
2765
3104
  prefix;
2766
3105
  allowedTools;
3106
+ // Best-effort servers (optional refs, connectionRef-backed capability MCPs,
3107
+ // codex_apps) must never fail a turn: a tools/list throw degrades to zero
3108
+ // tools instead of propagating. Deduplicate the warn so one degraded server
3109
+ // doesn't spam the log when the SDK re-lists across model turns.
3110
+ bestEffort;
3111
+ loggedListToolsFailure = false;
2767
3112
  connect() {
2768
3113
  return this.inner.connect();
2769
3114
  }
@@ -2771,7 +3116,24 @@ var PrefixedMcpServer = class {
2771
3116
  return this.inner.close();
2772
3117
  }
2773
3118
  async listTools() {
2774
- const tools = await this.inner.listTools();
3119
+ let tools;
3120
+ try {
3121
+ tools = await this.inner.listTools();
3122
+ } catch (error) {
3123
+ if (!this.bestEffort) {
3124
+ throw error;
3125
+ }
3126
+ if (!this.loggedListToolsFailure) {
3127
+ this.loggedListToolsFailure = true;
3128
+ console.warn(
3129
+ "[mcp] best-effort server tools/list failed; its tools are unavailable this turn",
3130
+ // Safe surface only (class + numeric status), never the raw error
3131
+ // message/response body — a broker 401/403 body can echo request detail.
3132
+ { serverId: this.name, ...safeMcpErrorFields(error) }
3133
+ );
3134
+ }
3135
+ return [];
3136
+ }
2775
3137
  return tools.filter((tool2) => this.isAllowed(tool2.name)).map((tool2) => ({ ...tool2, name: prefixedMcpToolName(this.name, tool2.name) }));
2776
3138
  }
2777
3139
  async callTool(toolName, args, meta) {
@@ -2785,6 +3147,17 @@ var PrefixedMcpServer = class {
2785
3147
  if (isAuthNeededMcpError(error)) {
2786
3148
  return { isError: true, content: [{ type: "text", text: MCP_AUTH_NEEDED_MESSAGE }] };
2787
3149
  }
3150
+ if (this.bestEffort) {
3151
+ const fields = safeMcpErrorFields(error);
3152
+ console.warn(
3153
+ "[mcp] best-effort server tool call failed; returning an unavailable result for this turn",
3154
+ { serverId: this.name, toolName: unprefixed, ...fields }
3155
+ );
3156
+ return {
3157
+ isError: true,
3158
+ content: [{ type: "text", text: mcpToolUnavailableMessage(mcpErrorReason(fields)) }]
3159
+ };
3160
+ }
2788
3161
  throw error;
2789
3162
  }
2790
3163
  }
@@ -2822,74 +3195,45 @@ var PrefixedMcpServer = class {
2822
3195
  return toolName.slice(this.prefix.length);
2823
3196
  }
2824
3197
  };
2825
- function guardAssembledInput(history, trailing, inputBudgetTokens) {
2826
- if (typeof inputBudgetTokens !== "number" || inputBudgetTokens <= 0) {
2827
- return [...history, trailing];
2828
- }
2829
- const trailingTokens = estimateItemTokens(trailing);
2830
- const guarded = enforceInputBudget(
2831
- history,
2832
- inputBudgetTokens,
2833
- trailingTokens
2834
- );
2835
- if (guarded.trimmed) {
2836
- console.warn(
2837
- `read-path budget guard trimmed ${guarded.droppedCount} oldest history item(s) to fit input budget (${inputBudgetTokens} tokens); the over-budget input was NOT sent`
2838
- );
2839
- }
2840
- return [...guarded.items, trailing];
2841
- }
2842
3198
  async function prepareRunInput(agent, input, options = {}) {
2843
3199
  if (input.kind === "message") {
2844
- if (input.historyItems && input.historyItems.length > 0) {
2845
- const sandboxSessionState2 = input.sandboxEnvelope ? await restoredSandboxSessionStateFromEntry(input.sandboxEnvelope, options.sandboxClient) : void 0;
2846
- const sanitizedHistory2 = sanitizeHistoryItemsForModel(
2847
- input.historyItems
2848
- );
2849
- return {
2850
- // Read-path budget guard: even after the orphan sanitizer, an assembled
2851
- // input can exceed the model window (pre-turn compaction is best-effort
2852
- // and can no-op). Trim the oldest history at a clean turn boundary so an
2853
- // over-budget request is never sent. No-op when no budget is supplied.
2854
- input: guardAssembledInput(
2855
- sanitizedHistory2,
2856
- {
2857
- type: "message",
2858
- role: "user",
2859
- content: input.text
2860
- },
2861
- options.inputBudgetTokens
2862
- ),
2863
- ...sandboxSessionState2 ? { sandboxSessionState: sandboxSessionState2 } : {}
2864
- };
3200
+ const trailingMessages = [];
3201
+ if (input.internalContext?.trim()) {
3202
+ trailingMessages.push({
3203
+ type: "message",
3204
+ role: "system",
3205
+ content: input.internalContext
3206
+ });
3207
+ }
3208
+ if (input.text?.trim()) {
3209
+ trailingMessages.push({
3210
+ type: "message",
3211
+ role: "user",
3212
+ content: input.text
3213
+ });
2865
3214
  }
2866
- if (!input.serializedRunState || isClearedRunStateBlob(input.serializedRunState)) {
2867
- return { input: input.text };
3215
+ if (trailingMessages.length === 0 && (input.historyItems === void 0 || input.historyItems === null || input.historyItems.length === 0)) {
3216
+ throw new Error("Message input requires a user prompt or internal context");
2868
3217
  }
2869
- const state2 = await RunState.fromString(agent, input.serializedRunState);
2870
- const sandboxSessionState = await restoredSandboxSessionState(state2, options.sandboxClient);
2871
3218
  const sanitizedHistory = sanitizeHistoryItemsForModel(
2872
- state2.history
3219
+ input.historyItems ?? []
2873
3220
  );
3221
+ const sandboxSessionState = input.sandboxEnvelope ? await restoredSandboxSessionStateFromEntry(input.sandboxEnvelope, options.sandboxClient) : void 0;
3222
+ const assembled = [...sanitizedHistory, ...trailingMessages];
3223
+ if (assembled.length === 0) {
3224
+ throw new Error("Message input requires durable history or internal context");
3225
+ }
2874
3226
  return {
2875
- // Read-path budget guard (see the items path above): keep an over-budget
2876
- // resumed history off the wire by trimming the oldest turns when a budget
2877
- // is supplied.
2878
- input: guardAssembledInput(
2879
- sanitizedHistory,
2880
- {
2881
- type: "message",
2882
- role: "user",
2883
- content: input.text
2884
- },
2885
- options.inputBudgetTokens
2886
- ),
2887
- ...sandboxSessionState ? { sandboxSessionState } : {},
2888
- serializedRunStateForSandbox: input.serializedRunState
3227
+ // Preserve the SDK's simple first-message form without creating a second
3228
+ // history path: this is merely the zero-history representation.
3229
+ input: sanitizedHistory.length === 0 && !input.internalContext?.trim() && input.text?.trim() ? input.text : assembled,
3230
+ ...sandboxSessionState ? { sandboxSessionState } : {}
2889
3231
  };
2890
3232
  }
2891
3233
  if (isClearedRunStateBlob(input.serializedRunState)) {
2892
- throw new Error("Cannot resume an approval: the session context was cleared, so the awaiting run state no longer exists.");
3234
+ throw new Error(
3235
+ "Cannot resume an approval: the session context was cleared, so the awaiting run state no longer exists."
3236
+ );
2893
3237
  }
2894
3238
  const state = await RunState.fromString(agent, input.serializedRunState);
2895
3239
  const interruptions = state.getInterruptions();
@@ -2905,6 +3249,26 @@ async function prepareRunInput(agent, input, options = {}) {
2905
3249
  return { input: state };
2906
3250
  }
2907
3251
  var GENESIS_TITLE_DIRECTIVE = "This is the first turn of a new session. Before responding to the user, call the opengeni__set_session_title tool with a concise 3-7 word title that summarizes what this session is about, then address the user's request normally.";
3252
+ function oneShotGenesisTitleInputFilter() {
3253
+ let pending = true;
3254
+ return ({ modelData }) => {
3255
+ if (!pending) {
3256
+ return modelData;
3257
+ }
3258
+ pending = false;
3259
+ return {
3260
+ ...modelData,
3261
+ instructions: modelData.instructions ? `${modelData.instructions} ${GENESIS_TITLE_DIRECTIVE}` : GENESIS_TITLE_DIRECTIVE
3262
+ };
3263
+ };
3264
+ }
3265
+ function takeGenesisTitleInputFilter(agent) {
3266
+ if (!agentsNeedingGenesisTitleDirective.has(agent)) {
3267
+ return void 0;
3268
+ }
3269
+ agentsNeedingGenesisTitleDirective.delete(agent);
3270
+ return oneShotGenesisTitleInputFilter();
3271
+ }
2908
3272
  var TOOLSPACE_PROGRAMMATIC_DIRECTIVE = "Every tool on your MCP surface is also callable programmatically from the sandbox shell, so scripts can invoke tools without a model round trip per call. Run `ogtool list` to see the available tools and their input schemas (from tools/list), then `ogtool call <tool-name> '<json-args>'`; equivalently, POST MCP JSON-RPC to $OPENGENI_TOOLSPACE_URL with the bearer token read from $OPENGENI_TOOLSPACE_TOKEN_FILE. Prefer programmatic calls for loops, polling, and bulk filtering: their results stay in the sandbox and do not consume your context window. Tools that require human approval must still be invoked normally \u2014 called programmatically they return a typed error.";
2909
3273
  var stripProviderItemIdsFilter = ({ modelData }) => ({
2910
3274
  ...modelData,
@@ -2922,38 +3286,32 @@ var normalizeComputerCallsFilter = ({ modelData }) => ({
2922
3286
  modelData.input
2923
3287
  )
2924
3288
  });
3289
+ var elideSupersededViewImagesFilter = ({ modelData }) => ({
3290
+ ...modelData,
3291
+ input: elideSupersededViewImagePairs(
3292
+ modelData.input
3293
+ )
3294
+ });
2925
3295
  function contextRobustnessFilterForSettings(settings, options = {}) {
2926
- const inputBudgetTokens = modelCallBudgetTokens(settings);
2927
- const clientCompactionMode = resolveContextCompactionMode(settings) === "client";
2928
- const compactionThresholdTokens = clientCompactionThresholdTokens(settings);
2929
- return ({ modelData }) => {
2930
- const images = elideStaleScreenshotImages(modelData.input);
2931
- if (images.elidedCount > 0) {
2932
- console.warn(
2933
- `per-call image history policy elided ${images.elidedCount} older screenshot image(s), keeping the last ${Math.min(3, images.imageCount)} full image(s)`
2934
- );
2935
- }
2936
- let input = images.items;
2937
- if (inputBudgetTokens !== void 0) {
2938
- const guarded = enforceInputBudget(
2939
- input,
2940
- inputBudgetTokens
2941
- );
2942
- if (guarded.trimmed) {
2943
- console.warn(
2944
- `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`
2945
- );
2946
- input = guarded.items;
2947
- }
2948
- }
2949
- if (clientCompactionMode && options.throwOnCompactionNeeded) {
3296
+ const thresholdTokens = compactionThresholdTokens(settings);
3297
+ return async ({ modelData }) => {
3298
+ const input = modelData.input;
3299
+ if (options.throwOnCompactionNeeded) {
2950
3300
  const reported = options.contextCompactionSignalTokens?.();
2951
3301
  const hasReported = typeof reported === "number" && reported > 0;
2952
3302
  const signalTokens = hasReported ? reported : estimateTokens(input);
2953
- if (signalTokens > compactionThresholdTokens) {
3303
+ if (await options.contextCompactionRequested?.()) {
3304
+ throw new CompactionNeededError({
3305
+ signalTokens,
3306
+ thresholdTokens,
3307
+ signalSource: hasReported ? "provider" : "estimate",
3308
+ trigger: "operator"
3309
+ });
3310
+ }
3311
+ if (signalTokens >= thresholdTokens) {
2954
3312
  throw new CompactionNeededError({
2955
3313
  signalTokens,
2956
- thresholdTokens: compactionThresholdTokens,
3314
+ thresholdTokens,
2957
3315
  signalSource: hasReported ? "provider" : "estimate"
2958
3316
  });
2959
3317
  }
@@ -2961,13 +3319,6 @@ function contextRobustnessFilterForSettings(settings, options = {}) {
2961
3319
  return { ...modelData, input };
2962
3320
  };
2963
3321
  }
2964
- function modelCallBudgetTokens(settings) {
2965
- if (resolveContextCompactionMode(settings) !== "client") {
2966
- return void 0;
2967
- }
2968
- const budget = contextInputBudgetTokens(settings);
2969
- return budget > 0 ? budget : void 0;
2970
- }
2971
3322
  function composeCallModelInputFilters(filters) {
2972
3323
  return async (args) => {
2973
3324
  let modelData = args.modelData;
@@ -2978,7 +3329,10 @@ function composeCallModelInputFilters(filters) {
2978
3329
  };
2979
3330
  }
2980
3331
  function callModelInputFilterForSettings(settings, options = {}) {
2981
- const filters = [normalizeComputerCallsFilter];
3332
+ const filters = [
3333
+ normalizeComputerCallsFilter,
3334
+ elideSupersededViewImagesFilter
3335
+ ];
2982
3336
  if (settings.openaiProviderItemIds === "strip") {
2983
3337
  filters.push(stripProviderItemIdsFilter);
2984
3338
  }
@@ -2988,54 +3342,63 @@ function callModelInputFilterForSettings(settings, options = {}) {
2988
3342
  async function runAgentStream(agent, input, settings, overrides = {}) {
2989
3343
  const prepared = typeof input === "string" || input instanceof RunState ? { input } : input;
2990
3344
  const environment = overrides.sandboxEnvironment ?? collectSandboxEnvironment2(settings);
3345
+ const genesisTitleInputFilter = takeGenesisTitleInputFilter(agent);
2991
3346
  if (overrides.ownedSandbox) {
2992
3347
  const { client: ownedClient, session, sessionState } = overrides.ownedSandbox;
2993
3348
  const setupSession = overrides.ownedSandbox.setupSession ?? session;
3349
+ if (!overrides.ownedSandbox.deferredSetup) {
3350
+ await runOwnedSandboxSetup(agent, session, setupSession, {
3351
+ settings,
3352
+ environment,
3353
+ preparedInput: prepared,
3354
+ ...overrides.ownedSandbox.fileDownloadsMaterialized ? { fileDownloadsMaterialized: true } : {},
3355
+ ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}
3356
+ });
3357
+ await overrides.onGitCredentialSessionReady?.(setupSession);
3358
+ }
2994
3359
  const runAs2 = sandboxRunAs(settings);
2995
3360
  const fileDownloads2 = sandboxFileDownloadsForAgent(agent);
2996
3361
  const resourceClient2 = fileDownloads2.length > 0 ? withSandboxFileDownloads(ownedClient, fileDownloads2, {
2997
3362
  ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
2998
3363
  ...runAs2 ? { runAs: runAs2 } : {}
2999
3364
  }) : ownedClient;
3000
- const ownedGitTokenSeed = gitTokenSeedForAgent(agent);
3365
+ const ownedGitTokenSeeds = gitTokenSeedsForAgent(agent);
3001
3366
  const ownedToolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
3367
+ const ownedRigSetup = rigSetupDescriptorForAgent(agent);
3002
3368
  const ownedHooks = [
3003
- ...sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
3369
+ // M3: rig setup runs FIRST so any tooling it installs is present for the
3370
+ // credential / repository-clone hooks below. The rig's credential hooks are
3371
+ // unioned into the deployment preparation-profile hooks (deduped by id).
3372
+ ...sandboxRigSetupHooksForAgent(agent),
3373
+ ...unionCredentialHooks(
3374
+ sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
3375
+ rigCredentialHooksForAgent(agent)
3376
+ ),
3004
3377
  ...sandboxToolspaceTokenHooksForAgent(agent),
3005
- ...sandboxRepositoryCloneHooksForAgent(agent)
3378
+ ...sandboxRepositoryCloneHooksForAgent(agent),
3379
+ ...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady)
3006
3380
  ];
3007
3381
  const ownedHookContext = {
3008
3382
  environment,
3009
3383
  ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
3010
3384
  ...runAs2 ? { runAs: runAs2 } : {},
3011
- ...ownedGitTokenSeed ? { gitTokenSeed: ownedGitTokenSeed } : {},
3012
- ...ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}
3385
+ ...ownedGitTokenSeeds ? { gitTokenSeeds: ownedGitTokenSeeds } : {},
3386
+ ...ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {},
3387
+ ...ownedRigSetup ? { rigSetup: ownedRigSetup } : {}
3013
3388
  };
3014
- if (agentActiveSandboxBackend.get(agent) !== "selfhosted") {
3015
- await runBeforeAgentStartHooks(setupSession, ownedHooks, ownedHookContext);
3016
- if (fileDownloads2.length > 0) {
3017
- await materializeSandboxFileDownloads(setupSession, fileDownloads2, {
3018
- ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
3019
- ...runAs2 ? { runAs: runAs2 } : {}
3020
- });
3021
- }
3022
- } else {
3023
- const toolspaceHooks = sandboxToolspaceTokenHooksForAgent(agent);
3024
- if (toolspaceHooks.length > 0) {
3025
- await runBeforeAgentStartHooks(setupSession, toolspaceHooks, ownedHookContext);
3026
- }
3027
- }
3028
3389
  const decoratedClient = withSandboxLifecycleHooks(resourceClient2, ownedHooks, ownedHookContext);
3029
3390
  const ownedFilter = composeCallModelInputFilters(
3030
3391
  [
3031
3392
  callModelInputFilterForSettings(settings, {
3032
- throwOnCompactionNeeded: Boolean(overrides.contextCompactionSignalTokens),
3033
- ...overrides.contextCompactionSignalTokens ? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens } : {}
3393
+ throwOnCompactionNeeded: Boolean(
3394
+ overrides.contextCompactionSignalTokens || overrides.contextCompactionRequested
3395
+ ),
3396
+ ...overrides.contextCompactionSignalTokens ? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens } : {},
3397
+ ...overrides.contextCompactionRequested ? { contextCompactionRequested: overrides.contextCompactionRequested } : {}
3034
3398
  }),
3399
+ genesisTitleInputFilter,
3035
3400
  overrides.callModelInputFilter
3036
- ].filter(
3037
- (f) => Boolean(f)
3038
- )
3401
+ ].filter((f) => Boolean(f))
3039
3402
  );
3040
3403
  const ownedRunOptions = {
3041
3404
  stream: true,
@@ -3050,44 +3413,66 @@ async function runAgentStream(agent, input, settings, overrides = {}) {
3050
3413
  return await runScopedRunner(settings).run(agent, prepared.input, ownedRunOptions);
3051
3414
  }
3052
3415
  const rawClient = overrides.sandboxClient ?? createSandboxClient(settings, environment);
3053
- const refreshedClient = rawClient ? withManifestRefreshOnResume(rawClient, agent.defaultManifest) : void 0;
3416
+ const refreshedClient = rawClient ? withManifestRefreshOnResume(
3417
+ rawClient,
3418
+ agent.defaultManifest,
3419
+ {
3420
+ ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}
3421
+ }
3422
+ ) : void 0;
3054
3423
  const runAs = sandboxRunAs(settings);
3055
3424
  const fileDownloads = sandboxFileDownloadsForAgent(agent);
3056
3425
  const resourceClient = refreshedClient && fileDownloads.length > 0 ? withSandboxFileDownloads(refreshedClient, fileDownloads, {
3057
3426
  ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
3058
3427
  ...runAs ? { runAs } : {}
3059
3428
  }) : refreshedClient;
3060
- const gitTokenSeed = gitTokenSeedForAgent(agent);
3429
+ const gitTokenSeeds = gitTokenSeedsForAgent(agent);
3061
3430
  const toolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
3062
- const client = resourceClient ? withSandboxLifecycleHooks(resourceClient, [
3063
- ...sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
3064
- ...sandboxToolspaceTokenHooksForAgent(agent),
3065
- ...sandboxRepositoryCloneHooksForAgent(agent)
3066
- ], {
3067
- environment,
3068
- ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
3069
- ...runAs ? { runAs } : {},
3070
- ...gitTokenSeed ? { gitTokenSeed } : {},
3071
- ...toolspaceTokenSeed ? { toolspaceTokenSeed } : {}
3072
- }) : void 0;
3073
- const sandboxSessionState = prepared.sandboxSessionState ?? (prepared.serializedRunStateForSandbox && client ? await restoredSandboxSessionState(await RunState.fromString(agent, prepared.serializedRunStateForSandbox), client) : void 0);
3431
+ const legacyRigSetup = rigSetupDescriptorForAgent(agent);
3432
+ const client = resourceClient ? withSandboxLifecycleHooks(
3433
+ resourceClient,
3434
+ [
3435
+ // M3: same rig-setup-first ordering + credential-hook union as the owned
3436
+ // path (this legacy create/resume decoration path is byte-for-byte today
3437
+ // for a rig-less turn — the rig hooks are empty then).
3438
+ ...sandboxRigSetupHooksForAgent(agent),
3439
+ ...unionCredentialHooks(
3440
+ sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
3441
+ rigCredentialHooksForAgent(agent)
3442
+ ),
3443
+ ...sandboxToolspaceTokenHooksForAgent(agent),
3444
+ ...sandboxRepositoryCloneHooksForAgent(agent),
3445
+ ...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady)
3446
+ ],
3447
+ {
3448
+ environment,
3449
+ ...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
3450
+ ...runAs ? { runAs } : {},
3451
+ ...gitTokenSeeds ? { gitTokenSeeds } : {},
3452
+ ...toolspaceTokenSeed ? { toolspaceTokenSeed } : {},
3453
+ ...legacyRigSetup ? { rigSetup: legacyRigSetup } : {}
3454
+ }
3455
+ ) : void 0;
3456
+ const sandboxSessionState = prepared.sandboxSessionState;
3074
3457
  const callModelInputFilter = composeCallModelInputFilters(
3075
3458
  [
3076
3459
  callModelInputFilterForSettings(settings, {
3077
- throwOnCompactionNeeded: Boolean(overrides.contextCompactionSignalTokens),
3078
- ...overrides.contextCompactionSignalTokens ? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens } : {}
3460
+ throwOnCompactionNeeded: Boolean(
3461
+ overrides.contextCompactionSignalTokens || overrides.contextCompactionRequested
3462
+ ),
3463
+ ...overrides.contextCompactionSignalTokens ? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens } : {},
3464
+ ...overrides.contextCompactionRequested ? { contextCompactionRequested: overrides.contextCompactionRequested } : {}
3079
3465
  }),
3466
+ genesisTitleInputFilter,
3080
3467
  overrides.callModelInputFilter
3081
- ].filter(
3082
- (f) => Boolean(f)
3083
- )
3468
+ ].filter((f) => Boolean(f))
3084
3469
  );
3085
3470
  const runOptions = {
3086
3471
  stream: true,
3087
3472
  maxTurns: settings.agentMaxModelCallsPerTurn,
3088
3473
  // Built-in per-call guard chain: normalize computer calls, optionally strip
3089
- // provider ids, elide stale screenshots in every mode, and trim to the input
3090
- // budget on the client-compaction path. This runs for turn-start replay AND
3474
+ // provider ids, trim to the input budget on the client-compaction path, and
3475
+ // raise the proactive compaction signal. This runs for turn-start replay AND
3091
3476
  // every mid-turn follow-up.
3092
3477
  callModelInputFilter
3093
3478
  };
@@ -3099,6 +3484,22 @@ async function runAgentStream(agent, input, settings, overrides = {}) {
3099
3484
  }
3100
3485
  return await runScopedRunner(settings).run(agent, prepared.input, runOptions);
3101
3486
  }
3487
+ function appendSandboxFileDownloadFailureNote(input, failures) {
3488
+ const note = sandboxFileDownloadFailureNote(failures);
3489
+ if (!note) {
3490
+ return;
3491
+ }
3492
+ if (typeof input.input === "string") {
3493
+ input.input = [input.input, "", note].join("\n");
3494
+ return;
3495
+ }
3496
+ if (Array.isArray(input.input)) {
3497
+ input.input = [
3498
+ ...input.input,
3499
+ { type: "message", role: "user", content: note }
3500
+ ];
3501
+ }
3502
+ }
3102
3503
  function runScopedRunner(settings) {
3103
3504
  return new Runner({ modelProvider: new MultiProviderModelProvider(settings) });
3104
3505
  }
@@ -3122,7 +3523,7 @@ function agentsErrorRunState(error) {
3122
3523
  return null;
3123
3524
  }
3124
3525
  }
3125
- function withManifestRefreshOnResume(client, targetManifest) {
3526
+ function withManifestRefreshOnResume(client, targetManifest, context = {}) {
3126
3527
  if (!targetManifest || !client.resume) {
3127
3528
  return client;
3128
3529
  }
@@ -3132,17 +3533,25 @@ function withManifestRefreshOnResume(client, targetManifest) {
3132
3533
  ...client.create ? { create: async (...args) => await client.create(...args) } : {},
3133
3534
  resume: async (state) => {
3134
3535
  const session = await client.resume(state);
3135
- await applyMissingManifestEntries(session, targetManifest);
3536
+ await applyMissingManifestEntries(session, targetManifest, context);
3136
3537
  return session;
3137
3538
  },
3138
3539
  ...client.delete ? { delete: async (state) => await client.delete(state) } : {},
3139
- ...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
3140
- ...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
3141
- ...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
3142
- ...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
3540
+ ...client.serializeSessionState ? {
3541
+ serializeSessionState: async (state, options) => await client.serializeSessionState(state, options)
3542
+ } : {},
3543
+ ...client.canPersistOwnedSessionState ? {
3544
+ canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state)
3545
+ } : {},
3546
+ ...client.canReusePreservedOwnedSession ? {
3547
+ canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state)
3548
+ } : {},
3549
+ ...client.deserializeSessionState ? {
3550
+ deserializeSessionState: async (state) => await client.deserializeSessionState(state)
3551
+ } : {}
3143
3552
  };
3144
3553
  }
3145
- async function applyMissingManifestEntries(session, targetManifest) {
3554
+ async function applyMissingManifestEntries(session, targetManifest, context = {}) {
3146
3555
  const currentManifestValue = session.state?.manifest;
3147
3556
  const currentManifest = currentManifestValue ? ensureManifest(currentManifestValue) : void 0;
3148
3557
  const target = ensureManifest(targetManifest);
@@ -3150,13 +3559,18 @@ async function applyMissingManifestEntries(session, targetManifest) {
3150
3559
  if (Object.keys(target.entries).length === 0) {
3151
3560
  return;
3152
3561
  }
3153
- throw new Error("Resumed sandbox session cannot apply new manifest entries because current manifest state is unavailable");
3562
+ throw new Error(
3563
+ "Resumed sandbox session cannot apply new manifest entries because current manifest state is unavailable"
3564
+ );
3154
3565
  }
3566
+ await reportManifestEnvironmentDrift(currentManifest, target, context);
3155
3567
  if (!session.applyManifest && !session.materializeEntry) {
3156
3568
  if (Object.keys(target.entries).length === 0) {
3157
3569
  return;
3158
3570
  }
3159
- throw new Error("Resumed sandbox session cannot apply new manifest entries because it does not support applyManifest() or materializeEntry()");
3571
+ throw new Error(
3572
+ "Resumed sandbox session cannot apply new manifest entries because it does not support applyManifest() or materializeEntry()"
3573
+ );
3160
3574
  }
3161
3575
  if (Object.keys(target.entries).length === 0) {
3162
3576
  return;
@@ -3177,7 +3591,9 @@ async function applyMissingManifestEntries(session, targetManifest) {
3177
3591
  }
3178
3592
  const environmentChanged = stableJson(currentManifest.environment) !== stableJson(target.environment);
3179
3593
  if (environmentChanged && !session.applyManifest) {
3180
- throw new Error("Resumed sandbox session cannot refresh manifest environment because it does not support applyManifest()");
3594
+ throw new Error(
3595
+ "Resumed sandbox session cannot refresh manifest environment because it does not support applyManifest()"
3596
+ );
3181
3597
  }
3182
3598
  if (Object.keys(entries).length === 0 && !environmentChanged) {
3183
3599
  return;
@@ -3206,6 +3622,114 @@ async function applyMissingManifestEntries(session, targetManifest) {
3206
3622
  ...extraPathGrants.length ? { extraPathGrants } : {}
3207
3623
  });
3208
3624
  }
3625
+ function manifestEnvironmentDrift(current, target) {
3626
+ const currentEnv = current.environment ?? {};
3627
+ const targetEnv = target.environment ?? {};
3628
+ const added = [];
3629
+ const removed = [];
3630
+ const changed = [];
3631
+ for (const key of Object.keys(targetEnv)) {
3632
+ if (!(key in currentEnv)) {
3633
+ added.push(key);
3634
+ } else if (stableJson(currentEnv[key]) !== stableJson(targetEnv[key])) {
3635
+ changed.push(key);
3636
+ }
3637
+ }
3638
+ for (const key of Object.keys(currentEnv)) {
3639
+ if (!(key in targetEnv)) {
3640
+ removed.push(key);
3641
+ }
3642
+ }
3643
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
3644
+ return null;
3645
+ }
3646
+ return { added: added.sort(), removed: removed.sort(), changed: changed.sort() };
3647
+ }
3648
+ async function reportManifestEnvironmentDrift(current, target, context) {
3649
+ const drift = manifestEnvironmentDrift(current, target);
3650
+ if (!drift) {
3651
+ return null;
3652
+ }
3653
+ try {
3654
+ await context.onRuntimeEvent?.({ type: "sandbox.env.drift", payload: drift });
3655
+ } catch {
3656
+ }
3657
+ return drift;
3658
+ }
3659
+ async function pinProvidedSessionManifestEnvironment(agent, session, context = {}) {
3660
+ const holder = agent;
3661
+ const currentManifestValue = session.state?.manifest;
3662
+ if (!holder.defaultManifest || !currentManifestValue) {
3663
+ return;
3664
+ }
3665
+ const current = ensureManifest(currentManifestValue);
3666
+ const target = ensureManifest(holder.defaultManifest);
3667
+ const drift = await reportManifestEnvironmentDrift(current, target, context);
3668
+ if (!drift) {
3669
+ return;
3670
+ }
3671
+ holder.defaultManifest = new Manifest({
3672
+ ...target.root ? { root: target.root } : {},
3673
+ entries: target.entries,
3674
+ environment: current.environment,
3675
+ ...target.extraPathGrants?.length ? { extraPathGrants: target.extraPathGrants } : {}
3676
+ });
3677
+ }
3678
+ async function runOwnedSandboxSetup(agent, session, setupSession, opts) {
3679
+ const { settings, environment } = opts;
3680
+ await pinProvidedSessionManifestEnvironment(agent, session, {
3681
+ ...opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}
3682
+ });
3683
+ const runAs = sandboxRunAs(settings);
3684
+ const fileDownloads = sandboxFileDownloadsForAgent(agent);
3685
+ const ownedGitTokenSeeds = {
3686
+ ...gitTokenSeedsForAgent(agent) ?? {},
3687
+ ...opts.gitTokenSeedOverride ? { github: opts.gitTokenSeedOverride } : {},
3688
+ ...opts.gitTokenSeedsOverride ?? {}
3689
+ };
3690
+ const ownedToolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
3691
+ const ownedRigSetup = rigSetupDescriptorForAgent(agent);
3692
+ const ownedHooks = [
3693
+ // M3: rig setup runs FIRST so any tooling it installs is present for the
3694
+ // credential / repository-clone hooks below; the rig's credential hooks are
3695
+ // unioned into the deployment preparation-profile hooks (deduped by id).
3696
+ // This is the LIVE owned-path execution (the provided session skips the
3697
+ // client create/resume decoration), so the rig hooks MUST be here or a
3698
+ // rig-bound turn would start without ever running the frozen setup script.
3699
+ ...sandboxRigSetupHooksForAgent(agent),
3700
+ ...unionCredentialHooks(
3701
+ sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
3702
+ rigCredentialHooksForAgent(agent)
3703
+ ),
3704
+ ...sandboxToolspaceTokenHooksForAgent(agent),
3705
+ ...sandboxRepositoryCloneHooksForAgent(agent)
3706
+ ];
3707
+ const ownedHookContext = {
3708
+ environment,
3709
+ ...opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {},
3710
+ ...runAs ? { runAs } : {},
3711
+ ...Object.keys(ownedGitTokenSeeds).length > 0 ? { gitTokenSeeds: ownedGitTokenSeeds } : {},
3712
+ ...ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {},
3713
+ ...ownedRigSetup ? { rigSetup: ownedRigSetup } : {}
3714
+ };
3715
+ if (agentActiveSandboxBackend.get(agent) !== "selfhosted") {
3716
+ await runBeforeAgentStartHooks(setupSession, ownedHooks, ownedHookContext);
3717
+ if (fileDownloads.length > 0 && !opts.fileDownloadsMaterialized) {
3718
+ const materialized = await materializeSandboxFileDownloads(setupSession, fileDownloads, {
3719
+ ...opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {},
3720
+ ...runAs ? { runAs } : {}
3721
+ });
3722
+ if (opts.preparedInput) {
3723
+ appendSandboxFileDownloadFailureNote(opts.preparedInput, materialized.failures);
3724
+ }
3725
+ }
3726
+ } else {
3727
+ const toolspaceHooks = sandboxToolspaceTokenHooksForAgent(agent);
3728
+ if (toolspaceHooks.length > 0) {
3729
+ await runBeforeAgentStartHooks(setupSession, toolspaceHooks, ownedHookContext);
3730
+ }
3731
+ }
3732
+ }
3209
3733
  function mergePathGrants(current, target) {
3210
3734
  const merged = /* @__PURE__ */ new Map();
3211
3735
  for (const grant of [...current ?? [], ...target ?? []]) {
@@ -3229,23 +3753,33 @@ function withSandboxFileDownloads(client, downloads, context = {}) {
3229
3753
  return {
3230
3754
  backendId: client.backendId,
3231
3755
  ...client.supportsDefaultOptions !== void 0 ? { supportsDefaultOptions: client.supportsDefaultOptions } : {},
3232
- ...client.create ? { create: async (...args) => await wrapSession(await client.create(...args)) } : {},
3233
- ...client.resume ? { resume: async (state) => await wrapSession(await client.resume(state)) } : {},
3756
+ ...client.create ? {
3757
+ create: async (...args) => await wrapSession(await client.create(...args))
3758
+ } : {},
3759
+ ...client.resume ? {
3760
+ resume: async (state) => await wrapSession(await client.resume(state))
3761
+ } : {},
3234
3762
  ...client.delete ? { delete: async (state) => await client.delete(state) } : {},
3235
- ...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
3236
- ...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
3237
- ...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
3238
- ...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
3763
+ ...client.serializeSessionState ? {
3764
+ serializeSessionState: async (state, options) => await client.serializeSessionState(state, options)
3765
+ } : {},
3766
+ ...client.canPersistOwnedSessionState ? {
3767
+ canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state)
3768
+ } : {},
3769
+ ...client.canReusePreservedOwnedSession ? {
3770
+ canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state)
3771
+ } : {},
3772
+ ...client.deserializeSessionState ? {
3773
+ deserializeSessionState: async (state) => await client.deserializeSessionState(state)
3774
+ } : {}
3239
3775
  };
3240
3776
  }
3241
3777
  async function materializeSandboxFileDownloads(session, downloads, context = {}) {
3242
3778
  const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
3243
3779
  if (normalizedDownloads.length === 0) {
3244
- return;
3245
- }
3246
- if (!session.exec && !session.execCommand) {
3247
- throw new Error("Sandbox file download materialization requires command execution support");
3780
+ return { failures: [] };
3248
3781
  }
3782
+ const failures = [];
3249
3783
  for (const download of normalizedDownloads) {
3250
3784
  const targetPath = sandboxDownloadTargetPath(download);
3251
3785
  const payload = {
@@ -3254,9 +3788,30 @@ async function materializeSandboxFileDownloads(session, downloads, context = {})
3254
3788
  sizeBytes: download.sizeBytes ?? null,
3255
3789
  expiresAt: download.expiresAt ? new Date(download.expiresAt).toISOString() : null
3256
3790
  };
3257
- await context.onRuntimeEvent?.({ type: "sandbox.operation.started", payload: { name: "file-resource-download", ...payload } });
3791
+ await context.onRuntimeEvent?.({
3792
+ type: "sandbox.operation.started",
3793
+ payload: { name: "file-resource-download", ...payload }
3794
+ });
3795
+ if (!session.exec && !session.execCommand) {
3796
+ const failure = sandboxFileDownloadFailure(
3797
+ download,
3798
+ targetPath,
3799
+ "Sandbox file download materialization requires command execution support"
3800
+ );
3801
+ failures.push(failure);
3802
+ await context.onRuntimeEvent?.({
3803
+ type: "sandbox.operation.failed",
3804
+ payload: {
3805
+ name: "file-resource-download",
3806
+ ...payload,
3807
+ error: failure.reason
3808
+ }
3809
+ });
3810
+ continue;
3811
+ }
3812
+ let result;
3258
3813
  try {
3259
- const result = session.exec ? await session.exec({
3814
+ result = session.exec ? await session.exec({
3260
3815
  cmd: sandboxFileDownloadCommand(download, targetPath),
3261
3816
  workdir: "/workspace",
3262
3817
  ...context.runAs ? { runAs: context.runAs } : {},
@@ -3270,19 +3825,48 @@ async function materializeSandboxFileDownloads(session, downloads, context = {})
3270
3825
  maxOutputTokens: 2e4
3271
3826
  });
3272
3827
  assertSandboxCommandSucceeded(result, `Sandbox file resource download ${download.fileId}`);
3273
- await context.onRuntimeEvent?.({ type: "sandbox.operation.completed", payload: { name: "file-resource-download", ...payload } });
3828
+ await context.onRuntimeEvent?.({
3829
+ type: "sandbox.operation.completed",
3830
+ payload: { name: "file-resource-download", ...payload }
3831
+ });
3274
3832
  } catch (error) {
3833
+ const failure = sandboxFileDownloadFailure(download, targetPath, error, result);
3834
+ failures.push(failure);
3275
3835
  await context.onRuntimeEvent?.({
3276
3836
  type: "sandbox.operation.failed",
3277
3837
  payload: {
3278
3838
  name: "file-resource-download",
3279
3839
  ...payload,
3280
- error: error instanceof Error ? error.message : String(error)
3840
+ error: failure.reason,
3841
+ ...failure.exitCode !== void 0 ? { exitCode: failure.exitCode } : {},
3842
+ ...failure.output ? { output: failure.output } : {}
3281
3843
  }
3282
3844
  });
3283
- throw error;
3284
3845
  }
3285
3846
  }
3847
+ return { failures };
3848
+ }
3849
+ function sandboxFileDownloadFailure(download, targetPath, error, result) {
3850
+ const exitCode = sandboxCommandExitCode(result);
3851
+ const output = sandboxCommandOutput(result);
3852
+ return {
3853
+ fileId: download.fileId,
3854
+ filename: download.filename,
3855
+ path: targetPath,
3856
+ reason: error instanceof Error ? error.message : String(error),
3857
+ ...exitCode !== null ? { exitCode } : {},
3858
+ ...output ? { output } : {}
3859
+ };
3860
+ }
3861
+ function sandboxFileDownloadFailureNote(failures) {
3862
+ if (failures.length === 0) {
3863
+ return "";
3864
+ }
3865
+ return [
3866
+ "The following attached files could not be loaded into the sandbox and are unavailable this turn:",
3867
+ ...failures.map((failure) => `- ${failure.filename} (${failure.reason})`),
3868
+ "Continue without them or tell the user."
3869
+ ].join("\n");
3286
3870
  }
3287
3871
  function sandboxFileDownloadsForAgent(agent) {
3288
3872
  return typeof agent === "object" && agent !== null ? [...agentFileDownloads.get(agent) ?? []] : [];
@@ -3357,6 +3941,9 @@ function normalizeSdkEvent(event) {
3357
3941
  out.push({ type: "agent.message.delta", payload: { text: data.delta } });
3358
3942
  return out;
3359
3943
  }
3944
+ if (data?.type === "response_done") {
3945
+ return out;
3946
+ }
3360
3947
  }
3361
3948
  if (isOpenAIResponsesRawModelStreamEvent(event)) {
3362
3949
  const raw = event.data?.event;
@@ -3415,7 +4002,10 @@ function normalizeSdkEvent(event) {
3415
4002
  type: "agent.toolCall.output",
3416
4003
  payload: {
3417
4004
  id: raw.call_id ?? raw.callId ?? item.id ?? null,
3418
- output: { type: "text", text: disclosed.length > 0 ? `Disclosed tools: ${disclosed.join(", ")}` : "No matching tools found." }
4005
+ output: {
4006
+ type: "text",
4007
+ text: disclosed.length > 0 ? `Disclosed tools: ${disclosed.join(", ")}` : "No matching tools found."
4008
+ }
3419
4009
  }
3420
4010
  });
3421
4011
  } else if (item.type === "message_output_item") {
@@ -3428,6 +4018,9 @@ function normalizeSdkEvent(event) {
3428
4018
  }
3429
4019
  function modelResponseUsageFromSdkEvent(event) {
3430
4020
  const response = modelResponseFromSdkEvent(event);
4021
+ return modelResponseUsageFromResponse(response);
4022
+ }
4023
+ function modelResponseUsageFromResponse(response) {
3431
4024
  const usage = usageFromResponse(response);
3432
4025
  if (!usage) {
3433
4026
  return null;
@@ -3458,25 +4051,51 @@ function usageFromResponse(response) {
3458
4051
  if (!raw || typeof raw !== "object") {
3459
4052
  return null;
3460
4053
  }
4054
+ const record = raw;
3461
4055
  const usage = {
3462
- ...numberProp(raw, "inputTokens", "inputTokens", "input_tokens"),
3463
- ...numberProp(raw, "outputTokens", "outputTokens", "output_tokens"),
3464
- ...numberProp(raw, "totalTokens", "totalTokens", "total_tokens"),
3465
- ...inputTokenDetailsProp(raw)
4056
+ ...numberProp(
4057
+ record,
4058
+ "inputTokens",
4059
+ "inputTokens",
4060
+ "input_tokens",
4061
+ "promptTokens",
4062
+ "prompt_tokens"
4063
+ ),
4064
+ ...numberProp(
4065
+ record,
4066
+ "outputTokens",
4067
+ "outputTokens",
4068
+ "output_tokens",
4069
+ "completionTokens",
4070
+ "completion_tokens"
4071
+ ),
4072
+ ...numberProp(record, "totalTokens", "totalTokens", "total_tokens"),
4073
+ ...inputTokenDetailsProp(record),
4074
+ ...outputTokenDetailsProp(record)
3466
4075
  };
3467
4076
  return Object.keys(usage).length > 0 ? usage : null;
3468
4077
  }
3469
- function numberProp(raw, outputKey, camel, snake) {
3470
- const value = raw[camel] ?? raw[snake];
4078
+ function numberProp(raw, outputKey, ...keys) {
4079
+ const value = keys.map((key) => raw[key]).find((candidate) => candidate !== void 0);
3471
4080
  return typeof value === "number" && Number.isFinite(value) ? { [outputKey]: value } : {};
3472
4081
  }
3473
4082
  function inputTokenDetailsProp(raw) {
3474
- const details = raw.inputTokensDetails ?? raw.input_tokens_details;
4083
+ const details = raw.inputTokensDetails ?? raw.input_tokens_details ?? raw.promptTokensDetails ?? raw.prompt_tokens_details;
3475
4084
  if (!details || typeof details !== "object") {
3476
4085
  return {};
3477
4086
  }
3478
4087
  return { inputTokensDetails: details };
3479
4088
  }
4089
+ function outputTokenDetailsProp(raw) {
4090
+ const details = raw.outputTokensDetails ?? raw.output_tokens_details;
4091
+ const normalized = details ?? raw.completionTokensDetails ?? raw.completion_tokens_details;
4092
+ if (!normalized || typeof normalized !== "object") {
4093
+ return {};
4094
+ }
4095
+ return {
4096
+ outputTokensDetails: normalized
4097
+ };
4098
+ }
3480
4099
  function serializeApprovals(interruptions) {
3481
4100
  return interruptions.map((item) => {
3482
4101
  if (typeof item?.toJSON === "function") {
@@ -3492,7 +4111,12 @@ function serializeApprovals(interruptions) {
3492
4111
  }
3493
4112
  function buildManifest(settings, resources, environment = collectSandboxEnvironment2(settings), fileResourceDownloads = []) {
3494
4113
  const entries = {};
3495
- const downloadsByFileId = new Map(normalizeSandboxFileDownloads(fileResourceDownloads).map((download) => [download.fileId, download]));
4114
+ const downloadsByFileId = new Map(
4115
+ normalizeSandboxFileDownloads(fileResourceDownloads).map((download) => [
4116
+ download.fileId,
4117
+ download
4118
+ ])
4119
+ );
3496
4120
  for (const resource of resources) {
3497
4121
  if (resource.kind === "repository") {
3498
4122
  const url = new URL(resource.uri);
@@ -3525,7 +4149,9 @@ function buildManifest(settings, resources, environment = collectSandboxEnvironm
3525
4149
  }
3526
4150
  function sandboxDownloadDirectory(download, mountPath) {
3527
4151
  if (download.mountPath !== mountPath) {
3528
- throw new Error(`File download materialization path mismatch for ${download.fileId}: expected ${mountPath}, got ${download.mountPath}`);
4152
+ throw new Error(
4153
+ `File download materialization path mismatch for ${download.fileId}: expected ${mountPath}, got ${download.mountPath}`
4154
+ );
3529
4155
  }
3530
4156
  assertSafeSandboxFilename(download.filename, download.fileId);
3531
4157
  if (download.content) {
@@ -3541,7 +4167,9 @@ function objectStorageFileMount(settings, prefix) {
3541
4167
  const nativeBucketMount = CAPABILITY_DESCRIPTORS2[settings.sandboxBackend].nativeBucketMount;
3542
4168
  if (settings.objectStorageBackend === "azure-blob") {
3543
4169
  if (nativeBucketMount) {
3544
- throw new Error("Modal sandbox Azure Blob file resources require pre-signed download materialization because the current OpenAI Agents SDK Modal client does not support Azure Blob mount entries.");
4170
+ throw new Error(
4171
+ "Modal sandbox Azure Blob file resources require pre-signed download materialization because the current OpenAI Agents SDK Modal client does not support Azure Blob mount entries."
4172
+ );
3545
4173
  }
3546
4174
  const config2 = azureBlobMountConfig(settings);
3547
4175
  return azureBlobMount({
@@ -3555,7 +4183,9 @@ function objectStorageFileMount(settings, prefix) {
3555
4183
  });
3556
4184
  }
3557
4185
  if (settings.objectStorageBackend === "aws-s3" || settings.objectStorageBackend === "gcs") {
3558
- throw new Error(`${settings.objectStorageBackend} file resources require pre-signed download materialization`);
4186
+ throw new Error(
4187
+ `${settings.objectStorageBackend} file resources require pre-signed download materialization`
4188
+ );
3559
4189
  }
3560
4190
  const config = s3CompatibleMountConfig(settings);
3561
4191
  return s3Mount({
@@ -3591,7 +4221,10 @@ function azureBlobMountConfig(settings) {
3591
4221
  if (!accountName || !accountKey) {
3592
4222
  throw new Error("File resources require Azure Blob account name and account key");
3593
4223
  }
3594
- const endpointUrl = azureBlobManifestEndpoint(settings.objectStorageAzureEndpoint ?? parsed.BlobEndpoint, accountName);
4224
+ const endpointUrl = azureBlobManifestEndpoint(
4225
+ settings.objectStorageAzureEndpoint ?? parsed.BlobEndpoint,
4226
+ accountName
4227
+ );
3595
4228
  return {
3596
4229
  container: settings.objectStorageBucket,
3597
4230
  accountName,
@@ -3608,10 +4241,12 @@ function azureBlobManifestEndpoint(endpoint, accountName) {
3608
4241
  return normalized === standardAccountEndpoint ? void 0 : normalized;
3609
4242
  }
3610
4243
  function parseAzureConnectionString(value) {
3611
- return Object.fromEntries(value.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
3612
- const index = part.indexOf("=");
3613
- return index === -1 ? [part, ""] : [part.slice(0, index), part.slice(index + 1)];
3614
- }));
4244
+ return Object.fromEntries(
4245
+ value.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
4246
+ const index = part.indexOf("=");
4247
+ return index === -1 ? [part, ""] : [part.slice(0, index), part.slice(index + 1)];
4248
+ })
4249
+ );
3615
4250
  }
3616
4251
  function normalizeManifestPath(path) {
3617
4252
  const normalized = path.replace(/^\/+|\/+$/g, "");
@@ -3625,7 +4260,9 @@ function normalizeSandboxFileDownloads(downloads) {
3625
4260
  const mountPath = normalizeManifestPath(download.mountPath);
3626
4261
  assertSafeSandboxFilename(download.filename, download.fileId);
3627
4262
  if (!download.content && !download.url?.trim()) {
3628
- throw new Error(`File download materialization requires content or a URL for ${download.fileId}`);
4263
+ throw new Error(
4264
+ `File download materialization requires content or a URL for ${download.fileId}`
4265
+ );
3629
4266
  }
3630
4267
  return {
3631
4268
  ...download,
@@ -3648,7 +4285,7 @@ function sandboxFileDownloadCommand(download, targetPath) {
3648
4285
  const targetDir = posixPath.dirname(targetPath);
3649
4286
  const tmpPath = `${targetPath}.opengeni-download-$$`;
3650
4287
  return [
3651
- "set -euo pipefail",
4288
+ "set -eu",
3652
4289
  `mkdir -p -- ${shellQuote(targetDir)}`,
3653
4290
  `if [ ! -f ${shellQuote(targetPath)} ]; then`,
3654
4291
  ` tmp=${shellQuote(tmpPath)}`,
@@ -3664,25 +4301,6 @@ function sandboxFileDownloadCommand(download, targetPath) {
3664
4301
  function shellQuote(value) {
3665
4302
  return `'${value.replace(/'/g, "'\\''")}'`;
3666
4303
  }
3667
- async function restoredSandboxSessionState(state, client) {
3668
- if (!client) {
3669
- return void 0;
3670
- }
3671
- const sandboxState = state._sandbox;
3672
- const entry = sandboxState?.sessionsByAgent?.[sandboxState.currentAgentKey] ?? (sandboxState?.currentAgentKey && sandboxState?.sessionState ? {
3673
- backendId: sandboxState.backendId,
3674
- currentAgentKey: sandboxState.currentAgentKey,
3675
- currentAgentName: sandboxState.currentAgentName,
3676
- sessionState: sandboxState.sessionState
3677
- } : void 0);
3678
- if (!entry) {
3679
- return void 0;
3680
- }
3681
- if (client.backendId !== entry.backendId) {
3682
- throw new Error("RunState sandbox backend does not match the configured sandbox client");
3683
- }
3684
- return await deserializeSandboxSessionStateEnvelope(client, entry.sessionState);
3685
- }
3686
4304
  var builtInSandboxLifecycleHooks = {
3687
4305
  "azure-cli-login": {
3688
4306
  id: "azure-cli-login",
@@ -3701,7 +4319,9 @@ function sandboxLifecycleHooksForIds(ids) {
3701
4319
  });
3702
4320
  }
3703
4321
  function applicableBeforeAgentStartHooks(hooks, context) {
3704
- return hooks.filter((hook) => hook.phase === "beforeAgentStart" && (hook.shouldRun?.(context) ?? true));
4322
+ return hooks.filter(
4323
+ (hook) => hook.phase === "beforeAgentStart" && (hook.shouldRun?.(context) ?? true)
4324
+ );
3705
4325
  }
3706
4326
  async function runBeforeAgentStartHooks(session, hooks, context) {
3707
4327
  for (const hook of applicableBeforeAgentStartHooks(hooks, context)) {
@@ -3726,102 +4346,328 @@ function withSandboxLifecycleHooks(client, hooks, context) {
3726
4346
  const wrapped = {
3727
4347
  backendId: client.backendId,
3728
4348
  ...client.supportsDefaultOptions !== void 0 ? { supportsDefaultOptions: client.supportsDefaultOptions } : {},
3729
- ...client.create ? { create: async (...args) => await wrapSession(await client.create(...args)) } : {},
3730
- ...client.resume ? { resume: async (state) => await wrapSession(await client.resume(state)) } : {},
4349
+ ...client.create ? {
4350
+ create: async (...args) => await wrapSession(await client.create(...args))
4351
+ } : {},
4352
+ ...client.resume ? {
4353
+ resume: async (state) => await wrapSession(await client.resume(state))
4354
+ } : {},
3731
4355
  ...client.delete ? { delete: async (state) => await client.delete(state) } : {},
3732
- ...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
3733
- ...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
3734
- ...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
3735
- ...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
4356
+ ...client.serializeSessionState ? {
4357
+ serializeSessionState: async (state, options) => await client.serializeSessionState(state, options)
4358
+ } : {},
4359
+ ...client.canPersistOwnedSessionState ? {
4360
+ canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state)
4361
+ } : {},
4362
+ ...client.canReusePreservedOwnedSession ? {
4363
+ canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state)
4364
+ } : {},
4365
+ ...client.deserializeSessionState ? {
4366
+ deserializeSessionState: async (state) => await client.deserializeSessionState(state)
4367
+ } : {}
3736
4368
  };
3737
4369
  return wrapped;
3738
4370
  }
3739
4371
  function sandboxRepositoryCloneHooksForAgent(agent) {
3740
4372
  return agentRepositoryCloneHooks.get(agent) ?? [];
3741
4373
  }
3742
- function gitTokenSeedForAgent(agent) {
3743
- return agentGitTokenSeed.get(agent);
4374
+ function gitTokenSeedsForAgent(agent) {
4375
+ return agentGitTokenSeeds.get(agent);
3744
4376
  }
3745
4377
  function toolspaceTokenSeedForAgent(agent) {
3746
4378
  return agentToolspaceTokenSeed.get(agent);
3747
4379
  }
3748
4380
  function sandboxToolspaceTokenHooksForAgent(agent) {
3749
- return toolspaceTokenSeedForAgent(agent) ? [{
3750
- id: "toolspace-token",
3751
- phase: "beforeAgentStart",
3752
- run: runToolspaceTokenSeedHook
3753
- }] : [];
4381
+ return toolspaceTokenSeedForAgent(agent) ? [
4382
+ {
4383
+ id: "toolspace-token",
4384
+ phase: "beforeAgentStart",
4385
+ run: runToolspaceTokenSeedHook
4386
+ }
4387
+ ] : [];
4388
+ }
4389
+ function sandboxRigSetupHooksForAgent(agent) {
4390
+ return agentRigSetup.get(agent) ? [
4391
+ {
4392
+ id: "rig-setup",
4393
+ phase: "beforeAgentStart",
4394
+ run: runRigSetupHook
4395
+ }
4396
+ ] : [];
4397
+ }
4398
+ function rigSetupDescriptorForAgent(agent) {
4399
+ return agentRigSetup.get(agent);
4400
+ }
4401
+ function rigCredentialHooksForAgent(agent) {
4402
+ return agentRigCredentialHooks.get(agent) ?? [];
4403
+ }
4404
+ function unionCredentialHooks(deploymentHooks, rigHooks) {
4405
+ if (rigHooks.length === 0) {
4406
+ return deploymentHooks;
4407
+ }
4408
+ const seen = new Set(deploymentHooks.map((hook) => hook.id));
4409
+ return [...deploymentHooks, ...rigHooks.filter((hook) => !seen.has(hook.id))];
4410
+ }
4411
+ function gitCredentialSessionRegistrationHooks(callback) {
4412
+ return callback ? [
4413
+ {
4414
+ id: "git-credential-renewal-registration",
4415
+ phase: "beforeAgentStart",
4416
+ run: async (session) => {
4417
+ await callback(session);
4418
+ }
4419
+ }
4420
+ ] : [];
3754
4421
  }
3755
4422
  function sandboxRepositoryCloneHooks(settings, resources, activeSandboxBackend = settings.sandboxBackend) {
3756
- const repositories = resources.filter((resource) => resource.kind === "repository" && repositoryUsesSandboxClone(settings, resource, activeSandboxBackend));
4423
+ const repositories = resources.filter(
4424
+ (resource) => resource.kind === "repository" && repositoryUsesSandboxClone(settings, resource, activeSandboxBackend)
4425
+ );
3757
4426
  if (repositories.length === 0) {
3758
4427
  return [];
3759
4428
  }
3760
- return [{
3761
- id: "repository-clone",
3762
- phase: "beforeAgentStart",
3763
- run: async (session, context) => {
3764
- await runRepositoryCloneHook(session, repositories, context);
4429
+ return [
4430
+ {
4431
+ id: "repository-clone",
4432
+ phase: "beforeAgentStart",
4433
+ run: async (session, context) => {
4434
+ await runRepositoryCloneHook(session, repositories, context);
4435
+ }
3765
4436
  }
3766
- }];
4437
+ ];
3767
4438
  }
3768
4439
  function repositoryUsesSandboxClone(settings, resource, activeSandboxBackend = settings.sandboxBackend) {
3769
4440
  if (activeSandboxBackend === "selfhosted") {
3770
4441
  return false;
3771
4442
  }
3772
- return settings.sandboxBackend === "modal" || Boolean(resource.githubInstallationId && resource.githubRepositoryId);
4443
+ return settings.sandboxBackend === "modal" || Boolean(resource.githubInstallationId && resource.githubRepositoryId) || Boolean(resource.provider);
3773
4444
  }
3774
- function repositoryCloneCommand(resources) {
3775
- const commands = [
3776
- "set -eu",
3777
- 'export HOME="${HOME:-/workspace}"',
3778
- 'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
3779
- // TOKEN-BROKER (B1/B2): seed the run-scoped GitHub token into the STABLE token FILE
3780
- // AND provision the git-askpass helper into the box AT SETUP (runtime) BEFORE any
3781
- // clone runs, so GIT_ASKPASS points at a per-box, user-writable script that reads
3782
- // that file for the fetch below. Provisioning the askpass here (rather than relying
3783
- // on a baked image script at /usr/local/bin/opengeni-git-askpass) removes the
3784
- // image-rebuild rollout gate: the askpass is correct on ANY box image, including
3785
- // pre-existing warm boxes on their next turn's clone hook, and no product image has
3786
- // to carry it. The seed rides the per-exec env (OPENGENI_GIT_TOKEN_SEED) — NEVER the
3787
- // box/agent manifest (validateNoEnvironmentDelta must not see a rotating value), so
3788
- // this whole block is a no-op when the seed is absent (e.g. the selfhosted path,
3789
- // which uses its own git creds). The token file lives at $OPENGENI_GIT_TOKEN_FILE
3790
- // (stable, from the shared base) with a $HOME/.opengeni/git-token fallback.
3791
- // $GIT_ASKPASS is on the box manifest env (set by
3792
- // sandboxEnvironmentForRun to $HOME/.opengeni/askpass), so it is available to this
3793
- // exec; the askpass script we write is byte-identical to docker/opengeni-git-askpass
3794
- // and is written via a QUOTED heredoc (<<'ASKPASS_EOF') so NOTHING inside it expands
3795
- // ($1, $HOME, ${OPENGENI_GIT_TOKEN_FILE:-...}, and the literal \n in printf all land
3796
- // verbatim), then chmod 0755 so git can exec it.
3797
- //
3798
- // ATOMIC REWRITE: this block now re-runs at the start of EVERY turn on a warm box
3799
- // that other turn holders may be actively using — an in-flight `git fetch` from a
3800
- // concurrent turn can invoke the askpass (which cats the token file) at any moment.
3801
- // Both files are therefore written to a pid-suffixed temp under umask 077 and
3802
- // renamed into place: rename is atomic, concurrent readers keep the old inode, and
3803
- // the token is never observable world-readable (no post-hoc chmod window).
3804
- 'if [ -n "${OPENGENI_GIT_TOKEN_SEED:-}" ]; then',
3805
- ' seed_umask="$(umask)"',
3806
- " umask 077",
3807
- ' git_token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}"',
3808
- ' mkdir -p "$(dirname "$git_token_file")"',
3809
- ` printf '%s' "$OPENGENI_GIT_TOKEN_SEED" > "$git_token_file.tmp.$$"`,
3810
- ' mv -f "$git_token_file.tmp.$$" "$git_token_file"',
3811
- ' git_askpass="${GIT_ASKPASS:-$HOME/.opengeni/askpass}"',
3812
- ' mkdir -p "$(dirname "$git_askpass")"',
3813
- ` cat > "$git_askpass.tmp.$$" <<'ASKPASS_EOF'`,
4445
+ var GIT_CREDENTIAL_PROVIDERS = [
4446
+ "github",
4447
+ "gitlab",
4448
+ "azure_devops"
4449
+ ];
4450
+ function gitProviderSeedEnv(provider) {
4451
+ return `OPENGENI_GIT_${provider.toUpperCase()}_TOKEN_SEED`;
4452
+ }
4453
+ function gitTokenSeedExportPrefix(seeds) {
4454
+ const lines = [];
4455
+ for (const provider of GIT_CREDENTIAL_PROVIDERS) {
4456
+ const token = seeds[provider];
4457
+ if (!token) {
4458
+ continue;
4459
+ }
4460
+ lines.push(`export ${gitProviderSeedEnv(provider)}=${shellQuote(token)}`);
4461
+ if (provider === "github") {
4462
+ lines.push(`export OPENGENI_GIT_TOKEN_SEED=${shellQuote(token)}`);
4463
+ }
4464
+ }
4465
+ return lines.join("\n");
4466
+ }
4467
+ function repositoryCredentialProvider(resource) {
4468
+ return resource.provider ?? "github";
4469
+ }
4470
+ function gitAskpassHostProviderCaseLines(resources) {
4471
+ const hosts = /* @__PURE__ */ new Map();
4472
+ for (const resource of resources) {
4473
+ try {
4474
+ const hostname = new URL(resource.uri).hostname.toLowerCase();
4475
+ if (!hostname) {
4476
+ continue;
4477
+ }
4478
+ hosts.set(hostname, repositoryCredentialProvider(resource));
4479
+ } catch {
4480
+ }
4481
+ }
4482
+ return [...hosts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
4483
+ ([hostname, provider]) => ` ${shellQuote(hostname)}) printf '%s\\n' ${provider}; return 0 ;;`
4484
+ );
4485
+ }
4486
+ function gitCredentialTokenWriterCommandLines() {
4487
+ return [
4488
+ // TOKEN-BROKER (B1/B2): seed run-scoped provider tokens into stable files and
4489
+ // atomically replace each provider file. Token VALUES are supplied only by
4490
+ // the per-exec command prefix
4491
+ // (OPENGENI_GIT_*_TOKEN_SEED), never by the box/agent manifest. Helper paths
4492
+ // are stable manifest values from @opengeni/config.
4493
+ "git_provider_token_file() {",
4494
+ ' provider="$1"',
4495
+ ' case "$provider" in',
4496
+ ` github) printf '%s\\n' "\${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;`,
4497
+ ` *) printf '%s\\n' "\${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token" ;;`,
4498
+ " esac",
4499
+ "}",
4500
+ "write_git_provider_token() {",
4501
+ ' provider="$1"',
4502
+ ' token="$2"',
4503
+ ' [ -n "$token" ] || return 0',
4504
+ ' token_file="$(git_provider_token_file "$provider")"',
4505
+ ' mkdir -p "$(dirname "$token_file")"',
4506
+ ` printf '%s' "$token" > "$token_file.tmp.$$"`,
4507
+ ' mv -f "$token_file.tmp.$$" "$token_file"',
4508
+ ' if [ "$provider" = github ]; then',
4509
+ ' credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
4510
+ ' mkdir -p "$credential_dir"',
4511
+ ` printf '%s' "$token" > "$credential_dir/github-token.tmp.$$"`,
4512
+ ' mv -f "$credential_dir/github-token.tmp.$$" "$credential_dir/github-token"',
4513
+ " fi",
4514
+ "}",
4515
+ 'seed_umask="$(umask)"',
4516
+ "umask 077",
4517
+ 'write_git_provider_token github "${OPENGENI_GIT_GITHUB_TOKEN_SEED:-${OPENGENI_GIT_TOKEN_SEED:-}}"',
4518
+ 'write_git_provider_token gitlab "${OPENGENI_GIT_GITLAB_TOKEN_SEED:-}"',
4519
+ 'write_git_provider_token azure_devops "${OPENGENI_GIT_AZURE_DEVOPS_TOKEN_SEED:-}"',
4520
+ 'umask "$seed_umask"'
4521
+ ];
4522
+ }
4523
+ function gitCredentialHelperCommandLines(resources = []) {
4524
+ const hostProviderCases = gitAskpassHostProviderCaseLines(resources);
4525
+ return [
4526
+ ...gitCredentialTokenWriterCommandLines(),
4527
+ // Provision git/provider-CLI helpers at SETUP (runtime) before any clone
4528
+ // runs. Renewal updates only token files and deliberately leaves these
4529
+ // repository-specific host mappings intact.
4530
+ 'git_askpass="${GIT_ASKPASS:-$HOME/.opengeni/askpass}"',
4531
+ 'mkdir -p "$(dirname "$git_askpass")"',
4532
+ `cat > "$git_askpass.tmp.$$" <<'ASKPASS_EOF'`,
3814
4533
  "#!/usr/bin/env sh",
4534
+ "prompt_host() {",
4535
+ ` prompt_lower="$(printf '%s\\n' "$1" | tr '[:upper:]' '[:lower:]')"`,
4536
+ ' case "$prompt_lower" in',
4537
+ " *://*) ;;",
4538
+ " *) printf '\\n'; return 0 ;;",
4539
+ " esac",
4540
+ ' rest="${prompt_lower#*://}"',
4541
+ ' rest="${rest#*@}"',
4542
+ ' host="${rest%%/*}"',
4543
+ ' host="${host%%:*}"',
4544
+ ` host="$(printf '%s\\n' "$host" | tr -d "'")"`,
4545
+ ` printf '%s\\n' "$host"`,
4546
+ "}",
4547
+ "provider_for_prompt() {",
4548
+ ' host="$(prompt_host "$1")"',
4549
+ ' case "$host" in',
4550
+ ...hostProviderCases.length > 0 ? hostProviderCases : [' "") : ;;'],
4551
+ " esac",
4552
+ ` case "$(printf '%s\\n' "$1" | tr '[:upper:]' '[:lower:]')" in`,
4553
+ " *github.com*|*githubusercontent.com*) printf '%s\\n' github ;;",
4554
+ " *gitlab*) printf '%s\\n' gitlab ;;",
4555
+ " *dev.azure.com*|*.visualstudio.com*) printf '%s\\n' azure_devops ;;",
4556
+ " *) printf '%s\\n' github ;;",
4557
+ " esac",
4558
+ "}",
4559
+ "token_file_for_provider() {",
4560
+ ' case "$1" in',
4561
+ ` github) printf '%s\\n' "\${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;`,
4562
+ ` *) printf '%s\\n' "\${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$1-token" ;;`,
4563
+ " esac",
4564
+ "}",
4565
+ "username_for_provider() {",
4566
+ ' case "$1" in',
4567
+ ` github) printf '%s\\n' "x-access-token" ;;`,
4568
+ ` gitlab) printf '%s\\n' "oauth2" ;;`,
4569
+ ` azure_devops) printf '%s\\n' "opengeni" ;;`,
4570
+ " *) printf '\\n' ;;",
4571
+ " esac",
4572
+ "}",
4573
+ 'provider="$(provider_for_prompt "$1")"',
3815
4574
  'case "$1" in',
3816
- ` *Username*) printf '%s\\n' "x-access-token" ;;`,
3817
- ` *Password*) cat "\${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" 2>/dev/null || printf '\\n' ;;`,
4575
+ ' *Username*) username_for_provider "$provider" ;;',
4576
+ ` *Password*) cat "$(token_file_for_provider "$provider")" 2>/dev/null || printf '\\n' ;;`,
3818
4577
  " *) printf '\\n' ;;",
3819
4578
  "esac",
3820
4579
  "ASKPASS_EOF",
3821
- ' chmod 0755 "$git_askpass.tmp.$$"',
3822
- ' mv -f "$git_askpass.tmp.$$" "$git_askpass"',
3823
- ' umask "$seed_umask"',
4580
+ 'chmod 0755 "$git_askpass.tmp.$$"',
4581
+ 'mv -f "$git_askpass.tmp.$$" "$git_askpass"',
4582
+ 'wrapper_dir="${OPENGENI_GIT_CLI_WRAPPER_DIR:-$HOME/.opengeni/bin}"',
4583
+ 'mkdir -p "$wrapper_dir"',
4584
+ "for opengeni_git_cli_tool in gh glab az; do",
4585
+ ' wrapper="$wrapper_dir/$opengeni_git_cli_tool"',
4586
+ ` cat > "$wrapper.tmp.$$" <<'CLI_WRAPPER_EOF'`,
4587
+ "#!/usr/bin/env sh",
4588
+ "set -eu",
4589
+ 'tool="${0##*/}"',
4590
+ 'case "$tool" in',
4591
+ " gh) provider=github; token_env=GH_TOKEN ;;",
4592
+ " glab) provider=gitlab; token_env=GITLAB_TOKEN ;;",
4593
+ " az) provider=azure_devops; token_env=AZURE_DEVOPS_EXT_PAT ;;",
4594
+ " *) provider=; token_env= ;;",
4595
+ "esac",
4596
+ 'if [ -n "$provider" ]; then',
4597
+ ' case "$provider" in',
4598
+ ' github) token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;',
4599
+ ' *) token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token" ;;',
4600
+ " esac",
4601
+ ' if [ -f "$token_file" ]; then',
4602
+ ' token="$(cat "$token_file" 2>/dev/null || true)"',
4603
+ ' if [ -n "$token" ]; then',
4604
+ ' case "$token_env" in',
4605
+ ' GH_TOKEN) export GH_TOKEN="$token" ;;',
4606
+ ' GITLAB_TOKEN) export GITLAB_TOKEN="$token" ;;',
4607
+ ' AZURE_DEVOPS_EXT_PAT) export AZURE_DEVOPS_EXT_PAT="$token" ;;',
4608
+ " esac",
4609
+ " fi",
4610
+ " fi",
3824
4611
  "fi",
4612
+ `self_real="$(readlink -f "$0" 2>/dev/null || printf '%s\\n' "$0")"`,
4613
+ 'old_ifs="$IFS"',
4614
+ "IFS=:",
4615
+ "for dir in $PATH; do",
4616
+ ' [ -n "$dir" ] || dir=.',
4617
+ ' candidate="$dir/$tool"',
4618
+ ' [ -x "$candidate" ] || continue',
4619
+ ` candidate_real="$(readlink -f "$candidate" 2>/dev/null || printf '%s\\n' "$candidate")"`,
4620
+ ' [ "$candidate_real" = "$self_real" ] && continue',
4621
+ ' IFS="$old_ifs"',
4622
+ ' exec "$candidate" "$@"',
4623
+ "done",
4624
+ 'IFS="$old_ifs"',
4625
+ `printf '%s\\n' "$tool: real command not found on PATH" >&2`,
4626
+ "exit 127",
4627
+ "CLI_WRAPPER_EOF",
4628
+ ' chmod 0755 "$wrapper.tmp.$$"',
4629
+ ' mv -f "$wrapper.tmp.$$" "$wrapper"',
4630
+ "done"
4631
+ ];
4632
+ }
4633
+ function gitProviderTokenRefreshCommand(seeds) {
4634
+ const seedPrefix = gitTokenSeedExportPrefix(seeds);
4635
+ if (!seedPrefix) {
4636
+ return "";
4637
+ }
4638
+ return [
4639
+ seedPrefix,
4640
+ "set -eu",
4641
+ 'export HOME="${HOME:-/workspace}"',
4642
+ ...gitCredentialTokenWriterCommandLines()
4643
+ ].join("\n");
4644
+ }
4645
+ async function refreshGitProviderTokenFiles(session, seeds, options = {}) {
4646
+ const command = gitProviderTokenRefreshCommand(seeds);
4647
+ if (!command) {
4648
+ return;
4649
+ }
4650
+ const args = {
4651
+ cmd: command,
4652
+ workdir: "/workspace",
4653
+ ...options.runAs ? { runAs: options.runAs } : {},
4654
+ yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
4655
+ maxOutputTokens: 4e3
4656
+ };
4657
+ if (session.exec) {
4658
+ assertSandboxCommandSucceeded(await session.exec(args), "Git credential refresh");
4659
+ } else if (session.execCommand) {
4660
+ assertSandboxCommandSucceeded(await session.execCommand(args), "Git credential refresh");
4661
+ } else {
4662
+ throw new Error("Sandbox session does not support command execution");
4663
+ }
4664
+ }
4665
+ function repositoryCloneCommand(resources) {
4666
+ const commands = [
4667
+ "set -eu",
4668
+ 'export HOME="${HOME:-/workspace}"',
4669
+ 'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
4670
+ ...gitCredentialHelperCommandLines(resources),
3825
4671
  "ensure_git() {",
3826
4672
  " if command -v git >/dev/null 2>&1; then",
3827
4673
  " return 0",
@@ -3912,13 +4758,15 @@ function repositoryCloneCommand(resources) {
3912
4758
  const url = new URL(resource.uri);
3913
4759
  const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
3914
4760
  const mountPath = normalizeManifestPath(resource.mountPath ?? `repos/${repo}`);
3915
- commands.push([
3916
- "clone_repository",
3917
- shellQuote(posixPath.join("/workspace", mountPath)),
3918
- shellQuote(resource.uri),
3919
- shellQuote(resource.ref),
3920
- shellQuote(resource.subpath ? normalizeManifestPath(resource.subpath) : "")
3921
- ].join(" "));
4761
+ commands.push(
4762
+ [
4763
+ "clone_repository",
4764
+ shellQuote(posixPath.join("/workspace", mountPath)),
4765
+ shellQuote(resource.uri),
4766
+ shellQuote(resource.ref),
4767
+ shellQuote(resource.subpath ? normalizeManifestPath(resource.subpath) : "")
4768
+ ].join(" ")
4769
+ );
3922
4770
  }
3923
4771
  return commands.join("\n");
3924
4772
  }
@@ -3965,11 +4813,124 @@ ${toolspaceTokenSeedCommand()}`;
3965
4813
  throw new Error("Sandbox session does not support command execution");
3966
4814
  }
3967
4815
  }
4816
+ var RIG_SETUP_OUTPUT_TAIL_LIMIT = 4e3;
4817
+ var RIG_SETUP_SKIPPED_SENTINEL = "__OPENGENI_RIG_SETUP_SKIPPED__";
4818
+ function rigSetupScriptCommand(script, versionId, timeoutMs = 6e5, markerRoot = "/var/opengeni") {
4819
+ const timeoutSecs = Math.max(1, Math.ceil(timeoutMs / 1e3));
4820
+ const lockWaitSecs = timeoutSecs + 6;
4821
+ const marker = `${markerRoot.replace(/\/+$/, "")}/rig-setup-${versionId}.done`;
4822
+ return [
4823
+ "set -u",
4824
+ `mkdir -p ${shellQuote(markerRoot)}`,
4825
+ `__OG_RIG_MARKER=${shellQuote(marker)}`,
4826
+ '__OG_RIG_LOCK="$__OG_RIG_MARKER.lock"',
4827
+ `__OG_RIG_TIMEOUT_SECS=${timeoutSecs}`,
4828
+ `__OG_RIG_LOCK_WAIT_SECS=${lockWaitSecs}`,
4829
+ `if [ -f "$__OG_RIG_MARKER" ]; then printf '%s\\n' ${shellQuote(RIG_SETUP_SKIPPED_SENTINEL)}; exit 0; fi`,
4830
+ "while :; do",
4831
+ ' if mkdir "$__OG_RIG_LOCK" 2>/dev/null; then',
4832
+ ` trap 'rm -rf "$__OG_RIG_LOCK"' EXIT`,
4833
+ ` if [ -f "$__OG_RIG_MARKER" ]; then printf '%s\\n' ${shellQuote(RIG_SETUP_SKIPPED_SENTINEL)}; exit 0; fi`,
4834
+ '__OG_RIG_SCRIPT="$(mktemp)"',
4835
+ `cat > "$__OG_RIG_SCRIPT" <<'__OPENGENI_RIG_SETUP_SCRIPT_EOF__'`,
4836
+ script,
4837
+ "__OPENGENI_RIG_SETUP_SCRIPT_EOF__",
4838
+ ' timeout -k 5s "${__OG_RIG_TIMEOUT_SECS}s" bash "$__OG_RIG_SCRIPT"',
4839
+ "__OG_RIG_RC=$?",
4840
+ ' rm -f "$__OG_RIG_SCRIPT"',
4841
+ ' if [ "$__OG_RIG_RC" -eq 0 ]; then touch "$__OG_RIG_MARKER"; fi',
4842
+ ' exit "$__OG_RIG_RC"',
4843
+ " fi",
4844
+ " __OG_RIG_WAITED=0",
4845
+ ' while [ "$__OG_RIG_WAITED" -lt "$__OG_RIG_LOCK_WAIT_SECS" ]; do',
4846
+ ` if [ -f "$__OG_RIG_MARKER" ]; then printf '%s\\n' ${shellQuote(RIG_SETUP_SKIPPED_SENTINEL)}; exit 0; fi`,
4847
+ ' if [ ! -d "$__OG_RIG_LOCK" ]; then break; fi',
4848
+ " sleep 1",
4849
+ " __OG_RIG_WAITED=$((__OG_RIG_WAITED + 1))",
4850
+ " done",
4851
+ ` if [ -f "$__OG_RIG_MARKER" ]; then printf '%s\\n' ${shellQuote(RIG_SETUP_SKIPPED_SENTINEL)}; exit 0; fi`,
4852
+ ' if [ ! -d "$__OG_RIG_LOCK" ]; then continue; fi',
4853
+ ' rmdir "$__OG_RIG_LOCK" 2>/dev/null || true',
4854
+ "done"
4855
+ ].join("\n");
4856
+ }
4857
+ async function runRigSetupHook(session, context = { environment: {} }) {
4858
+ const rigSetup = context.rigSetup;
4859
+ if (!rigSetup) {
4860
+ return;
4861
+ }
4862
+ const payload = {
4863
+ rigId: rigSetup.rigId,
4864
+ versionId: rigSetup.versionId,
4865
+ rigName: rigSetup.rigName
4866
+ };
4867
+ await context.onRuntimeEvent?.({ type: "rig.setup.started", payload });
4868
+ const command = rigSetupScriptCommand(rigSetup.script, rigSetup.versionId, rigSetup.timeoutMs);
4869
+ const execArgs = {
4870
+ cmd: command,
4871
+ workdir: "/workspace",
4872
+ ...context.runAs ? { runAs: context.runAs } : {},
4873
+ // The in-box coreutils timeout is the hard deadline; the SDK yield waits a
4874
+ // little longer so it observes timeout's non-zero exit instead of a live
4875
+ // still-running process.
4876
+ yieldTimeMs: rigSetup.timeoutMs + 7e3,
4877
+ maxOutputTokens: 2e4
4878
+ };
4879
+ let result;
4880
+ try {
4881
+ if (session.exec) {
4882
+ result = await session.exec(execArgs);
4883
+ } else if (session.execCommand) {
4884
+ result = await session.execCommand(execArgs);
4885
+ } else {
4886
+ throw new Error("Sandbox session does not support command execution");
4887
+ }
4888
+ } catch (error) {
4889
+ const message = error instanceof Error ? error.message : String(error);
4890
+ await context.onRuntimeEvent?.({
4891
+ type: "rig.setup.failed",
4892
+ payload: { ...payload, error: message.slice(-RIG_SETUP_OUTPUT_TAIL_LIMIT) }
4893
+ });
4894
+ throw new Error(
4895
+ `Rig setup failed for rig "${rigSetup.rigName}" (version ${rigSetup.versionId}): ${message}`,
4896
+ { cause: error }
4897
+ );
4898
+ }
4899
+ const output = sandboxCommandOutput(result);
4900
+ if (output.includes(RIG_SETUP_SKIPPED_SENTINEL)) {
4901
+ await context.onRuntimeEvent?.({ type: "rig.setup.skipped", payload });
4902
+ return;
4903
+ }
4904
+ const stillRunning = sandboxCommandStillRunning(result);
4905
+ const exitCode = sandboxCommandExitCode(result);
4906
+ if (stillRunning || exitCode === null || exitCode !== 0) {
4907
+ const tail = output.length > RIG_SETUP_OUTPUT_TAIL_LIMIT ? output.slice(-RIG_SETUP_OUTPUT_TAIL_LIMIT) : output;
4908
+ const reason = stillRunning ? `did not finish within the rig setup timeout (${rigSetup.timeoutMs}ms)` : exitCode === null ? "did not report an exit code" : `exited with code ${exitCode}`;
4909
+ const failure = new Error(
4910
+ `Rig setup failed for rig "${rigSetup.rigName}" (version ${rigSetup.versionId}): the setup script ${reason}${tail ? `:
4911
+ ${tail}` : ""}`
4912
+ );
4913
+ await context.onRuntimeEvent?.({
4914
+ type: "rig.setup.failed",
4915
+ payload: { ...payload, error: failure.message.slice(-RIG_SETUP_OUTPUT_TAIL_LIMIT) }
4916
+ });
4917
+ throw failure;
4918
+ }
4919
+ await context.onRuntimeEvent?.({
4920
+ type: "rig.setup.completed",
4921
+ payload: { ...payload, skipped: false }
4922
+ });
4923
+ }
3968
4924
  async function runRepositoryCloneHook(session, resources, context = { environment: {} }) {
3969
4925
  const payload = { name: "repository-clone", repositoryCount: resources.length };
3970
4926
  await context.onRuntimeEvent?.({ type: "sandbox.operation.started", payload });
3971
4927
  try {
3972
- const command = context.gitTokenSeed ? `export OPENGENI_GIT_TOKEN_SEED=${shellQuote(context.gitTokenSeed)}
4928
+ const gitTokenSeeds = {
4929
+ ...context.gitTokenSeeds ?? {},
4930
+ ...context.gitTokenSeed ? { github: context.gitTokenSeed } : {}
4931
+ };
4932
+ const seedPrefix = gitTokenSeedExportPrefix(gitTokenSeeds);
4933
+ const command = seedPrefix ? `${seedPrefix}
3973
4934
  ${repositoryCloneCommand(resources)}` : repositoryCloneCommand(resources);
3974
4935
  if (session.exec) {
3975
4936
  const result = await session.exec({
@@ -4040,6 +5001,10 @@ function sandboxCommandExitCode(result) {
4040
5001
  return null;
4041
5002
  }
4042
5003
  function sandboxCommandOutput(result) {
5004
+ if (typeof result === "string") {
5005
+ const outputIndex = result.indexOf("Output:");
5006
+ return outputIndex >= 0 ? result.slice(outputIndex + "Output:".length).trim() : result.trim();
5007
+ }
4043
5008
  if (!result || typeof result !== "object") {
4044
5009
  return "";
4045
5010
  }
@@ -4049,12 +5014,17 @@ function sandboxCommandOutput(result) {
4049
5014
  function assertSandboxCommandSucceeded(result, operation) {
4050
5015
  const output = sandboxCommandOutput(result);
4051
5016
  if (sandboxCommandStillRunning(result)) {
4052
- throw new Error(`${operation} did not finish before the lifecycle command timeout${output ? `:
4053
- ${output}` : ""}`);
5017
+ throw new Error(
5018
+ `${operation} did not finish before the lifecycle command timeout${output ? `:
5019
+ ${output}` : ""}`
5020
+ );
4054
5021
  }
4055
5022
  const exitCode = sandboxCommandExitCode(result);
4056
5023
  if (exitCode !== null && exitCode !== 0) {
4057
- throw new Error(output || `${operation} failed with exit code ${exitCode}`);
5024
+ throw new Error(
5025
+ `${operation} failed with exit code ${exitCode}${output ? `:
5026
+ ${output}` : ""}`
5027
+ );
4058
5028
  }
4059
5029
  if (exitCode === null) {
4060
5030
  throw new Error(output || `${operation} did not return a command exit code`);
@@ -4139,7 +5109,10 @@ function bundledSkillsDir() {
4139
5109
  return packaged;
4140
5110
  }
4141
5111
  if (!stagedBundledSkillsDir) {
4142
- stagedBundledSkillsDir = stageBundledSkills(packaged, join(process.cwd(), ".opengeni", "bundled_hashicorp_terraform_skills"));
5112
+ stagedBundledSkillsDir = stageBundledSkills(
5113
+ packaged,
5114
+ join(process.cwd(), ".opengeni", "bundled_hashicorp_terraform_skills")
5115
+ );
4143
5116
  }
4144
5117
  return stagedBundledSkillsDir;
4145
5118
  }
@@ -4184,12 +5157,18 @@ function lazySkillSourceWithPackSkills(packSkills) {
4184
5157
  packNameKeys.add(skill.name.toLowerCase());
4185
5158
  packNames.add(skill.name);
4186
5159
  children[skill.name] = packSkillDirEntry(skill);
4187
- packIndex.push({ name: skill.name, description: packSkillDescription(skill), path: skill.name });
5160
+ packIndex.push({
5161
+ name: skill.name,
5162
+ description: packSkillDescription(skill),
5163
+ path: skill.name
5164
+ });
4188
5165
  }
4189
5166
  return {
4190
5167
  source: dir({ children }),
4191
5168
  getIndex: (manifest, skillsPath) => [
4192
- ...(bundled.getIndex?.(manifest, skillsPath) ?? []).filter((entry) => !packNames.has(entry.path ?? entry.name)),
5169
+ ...(bundled.getIndex?.(manifest, skillsPath) ?? []).filter(
5170
+ (entry) => !packNames.has(entry.path ?? entry.name)
5171
+ ),
4193
5172
  ...packIndex
4194
5173
  ]
4195
5174
  };
@@ -4303,7 +5282,9 @@ function sortJson(value) {
4303
5282
  return value.map(sortJson);
4304
5283
  }
4305
5284
  if (value && typeof value === "object") {
4306
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, sortJson(nested)]));
5285
+ return Object.fromEntries(
5286
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, sortJson(nested)])
5287
+ );
4307
5288
  }
4308
5289
  return value;
4309
5290
  }
@@ -4332,24 +5313,44 @@ export {
4332
5313
  DISPLAY_STACK_TIMEOUT_MS,
4333
5314
  DisplayStackError,
4334
5315
  DisplayStackUnsupportedError,
5316
+ FAULT_FIELD_WHAT_HAPPENED,
5317
+ FAULT_FIELD_WHAT_PRESERVED,
5318
+ FAULT_FIELD_WHAT_TO_TRY,
5319
+ FAULT_FIELD_WHICH_LAYER,
4335
5320
  GENESIS_TITLE_DIRECTIVE,
4336
5321
  MAX_COMPACTION_THRESHOLD_RATIO,
4337
5322
  MIN_COMPACTION_THRESHOLD_RATIO,
4338
5323
  MaxTurnsExceededError2 as MaxTurnsExceededError,
4339
5324
  MockAgentResponder,
4340
5325
  MultiProviderModelProvider,
5326
+ NEVER_SENT_DETAIL_KEY,
4341
5327
  NatsControlRpc,
5328
+ NatsOpStreamTransport,
5329
+ OP_STREAM_ACK_INTERVAL_MS,
5330
+ OP_STREAM_DEFAULT_WINDOW_BYTES,
5331
+ OP_STREAM_RECONNECT_HOLD_MS,
5332
+ OP_STREAM_SILENCE_TIMEOUT_MS,
5333
+ OpStreamUnavailableError,
4342
5334
  OpenAIChatCompletionsModel2 as OpenAIChatCompletionsModel,
4343
5335
  OpenAIResponsesModel2 as OpenAIResponsesModel,
4344
5336
  PROVIDER_REGISTRY,
5337
+ REPOSITORY_DISCOVERY_LIMIT,
4345
5338
  RecordingError,
4346
5339
  RecordingUnavailableError,
4347
5340
  RoutingSandboxSession,
4348
5341
  RoutingUnsupportedError,
4349
- SCREENSHOT_OMITTED_PLACEHOLDER,
4350
5342
  SELFHOSTED_DEFAULT_TIMEOUT_MS,
5343
+ SELFHOSTED_DRAINING_MAX_RETRIES,
5344
+ SELFHOSTED_EXEC_DRAINING_MAX_RETRIES,
5345
+ SELFHOSTED_IDEMPOTENT_READONLY_OPS,
5346
+ SELFHOSTED_INFRASTRUCTURE_FAULT_CLASSES,
5347
+ SELFHOSTED_NEVER_SENT_MAX_RETRIES,
4351
5348
  SELFHOSTED_RECONNECT_WINDOW_MS,
4352
5349
  SELFHOSTED_RELAY_STREAM_PATH,
5350
+ SELFHOSTED_RETRY_BACKOFF_BASE_MS,
5351
+ SELFHOSTED_RETRY_BACKOFF_CAP_MS,
5352
+ SELFHOSTED_RETRY_BACKOFF_FACTOR,
5353
+ SELFHOSTED_TIMEOUT_MAX_RETRIES,
4353
5354
  STREAM_PORT,
4354
5355
  STREAM_TOKEN_DEFAULT_TTL_SECONDS,
4355
5356
  SUMMARY_BUFFER_TOKENS,
@@ -4358,6 +5359,7 @@ export {
4358
5359
  SandboxComputer,
4359
5360
  SandboxConfigError,
4360
5361
  SandboxProviderUnavailableError,
5362
+ SandboxResumeStateUnavailableError,
4361
5363
  SelfhostedControlError,
4362
5364
  SelfhostedSandboxClient,
4363
5365
  SelfhostedSession,
@@ -4369,11 +5371,14 @@ export {
4369
5371
  TerminalServerError,
4370
5372
  TerminalServerUnsupportedError,
4371
5373
  USER_MESSAGE_TRUNCATION_MARKER,
5374
+ WorkspaceModelPolicyBlockedError,
4372
5375
  agentErrorToControlError,
4373
5376
  agentsErrorRunState,
4374
5377
  appendGenesisTitleDirective,
5378
+ appendPersistentSessionSettings,
4375
5379
  appendSessionInstructions,
4376
5380
  appendToolspaceInstructions,
5381
+ appendWorkspaceMemory,
4377
5382
  applyMissingManifestEntries,
4378
5383
  assertDescriptorRegistryInvariants,
4379
5384
  assertProviderRegistryInvariants,
@@ -4384,7 +5389,6 @@ export {
4384
5389
  buildAgentCapabilities,
4385
5390
  buildCompactionPromptInput,
4386
5391
  buildCompactionReplacementHistory,
4387
- buildDeterministicFallbackCompactionHistory,
4388
5392
  buildDisplayStackScript,
4389
5393
  buildManifest,
4390
5394
  buildModelInstance,
@@ -4397,8 +5401,8 @@ export {
4397
5401
  buildTerminalServerScript,
4398
5402
  callModelInputFilterForSettings,
4399
5403
  clampCompactionThresholdRatio,
4400
- clientCompactionThresholdTokens,
4401
5404
  collectSandboxEnvironment,
5405
+ compactionThresholdTokens,
4402
5406
  composeAgentInstructions,
4403
5407
  computerUse,
4404
5408
  configureOpenAI,
@@ -4409,14 +5413,18 @@ export {
4409
5413
  createProductionAgentRuntime,
4410
5414
  createSandboxClient,
4411
5415
  createSandboxClientForBackend,
4412
- decideClientCompaction,
5416
+ decideCompaction,
5417
+ decideSelfhostedRetry,
4413
5418
  decodeModalSnapshotId,
5419
+ defaultSelfhostedRetryClock,
4414
5420
  deletePriorPersistedSnapshot,
4415
5421
  deleteRecordingArtifacts,
4416
5422
  deserializeSandboxSessionStateEnvelope,
4417
5423
  desktopCapableBackend,
4418
- elideStaleScreenshotImages,
4419
- enforceInputBudget,
5424
+ drainingExhaustedError,
5425
+ drainingMessage,
5426
+ elideSupersededViewImagePairs,
5427
+ elideSupersededViewImagesFilter,
4420
5428
  ensureDisplayStack,
4421
5429
  ensureModalRegistryImage,
4422
5430
  ensureReadableStreamFrom,
@@ -4424,12 +5432,15 @@ export {
4424
5432
  establishSandboxSessionFromEnvelope,
4425
5433
  estimateItemTokens,
4426
5434
  estimateTokens,
5435
+ execDeadlineHint,
4427
5436
  exposeStreamPort,
4428
5437
  extForCodec,
4429
5438
  extractResponseOutputText,
4430
5439
  findCompactionNeededError,
4431
- findKeepBoundary,
5440
+ gitProviderTokenRefreshCommand,
5441
+ inspectRecordingArtifact,
4432
5442
  isCompactionSummary,
5443
+ isEphemeralInternalContext,
4433
5444
  isExecSessionLostBanner,
4434
5445
  isProviderSandboxNotFoundError,
4435
5446
  isSelfhostedProviderNotFoundError,
@@ -4437,52 +5448,71 @@ export {
4437
5448
  isWorkspaceEscapeError,
4438
5449
  lazySkillSourceWithPackSkills,
4439
5450
  makeActiveBackendResolver,
5451
+ manifestEnvironmentDrift,
4440
5452
  materializeSandboxFileDownloads,
4441
5453
  maxTurnsExceededRunState,
4442
5454
  mcpToolErrorOutput,
4443
5455
  mintStreamToken,
4444
5456
  modalSandboxAttributionEnvironment,
4445
5457
  modalSandboxAttributionTags,
5458
+ modelCallUsageTelemetry,
5459
+ modelResponseUsageFromResponse,
4446
5460
  modelResponseUsageFromSdkEvent,
4447
5461
  negotiateCapabilities,
4448
5462
  negotiateSelfhostedCapabilities,
4449
5463
  neutralizeToolSearchItemsInSerializedRunState,
5464
+ nextDurableOpId,
4450
5465
  normalizeComputerCallsFilter,
4451
5466
  normalizeSdkEvent,
4452
5467
  normalizeToolOutputForEvent,
4453
5468
  offlineAgentError,
4454
5469
  offlineControlResponse,
5470
+ oneShotGenesisTitleInputFilter,
5471
+ opAckSubject,
5472
+ opFrameSubject,
4455
5473
  parseExecBannerSessionId,
4456
5474
  parseExposedPorts,
4457
5475
  parseNumstatZ,
4458
5476
  parsePorcelainV2,
4459
5477
  parseUnifiedPatch,
5478
+ payloadTooLargeMessage,
5479
+ pinProvidedSessionManifestEnvironment,
4460
5480
  prefixedMcpToolName,
4461
5481
  prepareAgentTools,
4462
5482
  prepareRunInput,
4463
- readRecordingBytes,
4464
5483
  readWorkspaceArchiveFromEnvelopeSessionState,
4465
5484
  recordingStorageKey,
5485
+ refreshGitProviderTokenFiles,
4466
5486
  renderCompactionPromptInputForChat,
5487
+ renderSelfhostedFault,
4467
5488
  repositoryCloneCommand,
4468
5489
  repositoryUsesSandboxClone,
4469
5490
  resolveTurnModel,
4470
5491
  restoredSandboxSessionStateFromEntry,
5492
+ rigInstructions,
5493
+ rigSetupScriptCommand,
4471
5494
  runAgentStream,
4472
5495
  runAzureCliLoginHook,
4473
5496
  runBeforeAgentStartHooks,
5497
+ runOwnedSandboxSetup,
4474
5498
  runRepositoryCloneHook,
5499
+ runRigSetupHook,
4475
5500
  runToolspaceTokenSeedHook,
5501
+ runWithToolCallCorrelation,
4476
5502
  sandboxCommandExitCode,
4477
5503
  sandboxCommandOutput,
4478
5504
  sandboxCommandStillRunning,
5505
+ sandboxFileDownloadFailureNote,
4479
5506
  sandboxFileDownloadsForAgent,
4480
5507
  sandboxLifecycleHooksForIds,
4481
5508
  sandboxRunAs,
4482
5509
  sandboxStateEntryFromRunState,
4483
5510
  sanitizeHistoryItemsForModel,
5511
+ sanitizeOpIdToken,
4484
5512
  selectBackend,
5513
+ selfhostedFaultClass,
4485
5514
  selfhostedLiveness,
5515
+ selfhostedRetryBackoffMs,
4486
5516
  serializeApprovals,
4487
5517
  serializeEstablishedSandboxEnvelope,
4488
5518
  setSelfhostedApplyDiff,
@@ -4494,6 +5524,7 @@ export {
4494
5524
  stripReasoningIdentityFromSerializedRunState,
4495
5525
  subjectFor,
4496
5526
  summarizeForCompaction,
5527
+ swapTargetEstablishability,
4497
5528
  sweepModalOrphanSandboxes,
4498
5529
  tagModalSandbox,
4499
5530
  tearDownDisplayStack,
@@ -4502,10 +5533,12 @@ export {
4502
5533
  timeoutAgentError,
4503
5534
  timeoutControlResponse,
4504
5535
  toolspaceTokenSeedCommand,
5536
+ uploadRecordingArtifact,
4505
5537
  verifyStreamToken,
4506
5538
  withManifestRefreshOnResume,
4507
5539
  withSandboxFileDownloads,
4508
5540
  withSandboxLifecycleHooks,
5541
+ withStructuredViewImageFunctionResults,
4509
5542
  workspaceEnvironmentInstructions
4510
5543
  };
4511
5544
  //# sourceMappingURL=index.js.map