@tangle-network/agent-app 0.44.23 → 0.44.25

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.
@@ -93,7 +93,7 @@ import {
93
93
  flattenHistory,
94
94
  readSandboxBinaryBytes,
95
95
  statSandboxFileSize
96
- } from "../chunk-2ZSSOYXP.js";
96
+ } from "../chunk-Q74BS43G.js";
97
97
  import "../chunk-LWSJK546.js";
98
98
  import "../chunk-CQZSAR77.js";
99
99
  import "../chunk-ICOHEZK6.js";
@@ -32,6 +32,75 @@ var fail = (error) => ({
32
32
  error: error instanceof Error ? error : new Error(String(error))
33
33
  });
34
34
 
35
+ // src/sandbox/model.ts
36
+ function resolveModelSelection(config, override) {
37
+ const c = config ?? {};
38
+ const explicitBaseUrl = trimOrNull(c.routerBaseUrl);
39
+ const explicitApiKey = trimOrNull(override?.modelApiKey) ?? trimOrNull(c.apiKey);
40
+ const providerName = trimOrNull(c.providerName);
41
+ const openaiApiKey = trimOrNull(c.openaiApiKey);
42
+ const provider = providerName ?? (explicitApiKey ? "openai-compat" : openaiApiKey ? "openai" : void 0);
43
+ const overrideModel = trimOrNull(override?.model);
44
+ const configModel = trimOrNull(c.modelName);
45
+ const defaultModel = trimOrNull(c.defaultModel);
46
+ const modelName = overrideModel ?? configModel ?? (provider === "openai" || provider === "openai-compat" ? defaultModel : null);
47
+ if (!modelName) return { succeeded: true, value: void 0 };
48
+ const source = overrideModel ? "override" : configModel ? "config" : "default";
49
+ if (!provider) return { succeeded: false, error: "no_provider", model: modelName, source };
50
+ const apiKey = explicitApiKey ?? (provider === "openai" ? openaiApiKey : void 0);
51
+ if (!apiKey && !c.allowKeylessModel) {
52
+ return { succeeded: false, error: "no_api_key", model: modelName, provider, source };
53
+ }
54
+ return {
55
+ succeeded: true,
56
+ value: {
57
+ model: modelName,
58
+ provider,
59
+ ...apiKey ? { apiKey } : {},
60
+ ...explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}
61
+ }
62
+ };
63
+ }
64
+ function resolveModel(config, override) {
65
+ const selection = resolveModelSelection(config, override);
66
+ return selection.succeeded ? selection.value : void 0;
67
+ }
68
+ var SandboxModelResolutionError = class extends Error {
69
+ code;
70
+ model;
71
+ provider;
72
+ source;
73
+ constructor(failure, context) {
74
+ const missing = failure.error === "no_provider" ? "no provider could be resolved for it" : `provider "${failure.provider}" resolved but no API key is configured`;
75
+ const fix = failure.error === "no_provider" ? "set providerName explicitly (provider cannot be inferred without one)" : 'set apiKey (or openaiApiKey when provider is "openai"), or pass allowKeylessModel:true to mint a keyless box';
76
+ super(
77
+ `${context}: model "${failure.model}" (from ${failure.source}) is not transportable \u2014 ${missing}. Fix: ${fix}. The requested model was NOT sent to the box.`
78
+ );
79
+ this.name = "SandboxModelResolutionError";
80
+ this.code = failure.error;
81
+ this.model = failure.model;
82
+ if (failure.error === "no_api_key") this.provider = failure.provider;
83
+ this.source = failure.source;
84
+ }
85
+ };
86
+ function requireTransportableModel(selection, context) {
87
+ if (selection.succeeded) return selection.value;
88
+ if (selection.source === "override") {
89
+ throw new SandboxModelResolutionError(selection, context);
90
+ }
91
+ const reason = selection.error === "no_api_key" ? `provider "${selection.provider}" has no api key` : "no provider resolved";
92
+ if (selection.source === "config") {
93
+ console.error(
94
+ `[sandbox] ${context}: dropping configured provider.modelName "${selection.model}" (${reason}); the box will use its own default model \u2014 set allowKeylessModel:true to bake a keyless model, or configure an api key`
95
+ );
96
+ } else {
97
+ console.error(
98
+ `[sandbox] ${context}: dropping provider.defaultModel "${selection.model}" (${reason}); using the box default`
99
+ );
100
+ }
101
+ return void 0;
102
+ }
103
+
35
104
  // src/sandbox/binary-read.ts
36
105
  function shellQuote(value) {
37
106
  return `'${value.replaceAll("'", `'"'"'`)}'`;
@@ -722,6 +791,21 @@ var SandboxRuntimeAuthRefreshError = class extends Error {
722
791
  this.name = "SandboxRuntimeAuthRefreshError";
723
792
  }
724
793
  };
794
+ var SandboxRecoveryFailedError = class extends Error {
795
+ boxKey;
796
+ stage;
797
+ phase;
798
+ constructor(stage, boxKey, phase, detail, cause) {
799
+ super(
800
+ `${stage} sandbox ${boxKey} failed liveness recovery at ${phase}: ${detail}. The workspace is preserved; pass forceNew to replace the box.`,
801
+ { cause }
802
+ );
803
+ this.name = "SandboxRecoveryFailedError";
804
+ this.boxKey = boxKey;
805
+ this.stage = stage;
806
+ this.phase = phase;
807
+ }
808
+ };
725
809
  function fileApiTarget(mount) {
726
810
  if (mount.resource.kind !== "inline") return null;
727
811
  let rel;
@@ -1081,6 +1165,39 @@ async function resumeStoppedBox(box, timeoutMs, onProgress) {
1081
1165
  return fail(err);
1082
1166
  }
1083
1167
  }
1168
+ async function recoverUnresponsiveBox(client, box, harness, probe, stage, name, resumeTimeout, onProgress) {
1169
+ try {
1170
+ await box.stop();
1171
+ } catch (err) {
1172
+ throw new SandboxRecoveryFailedError(
1173
+ stage,
1174
+ name,
1175
+ "stop",
1176
+ "the platform could not stop the box for a state-preserving restart",
1177
+ err
1178
+ );
1179
+ }
1180
+ const resumed = await resumeStoppedBox(box, resumeTimeout, onProgress);
1181
+ if (!resumed.succeeded) {
1182
+ throw new SandboxRecoveryFailedError(
1183
+ stage,
1184
+ name,
1185
+ "resume",
1186
+ "the box did not reach running after a state-preserving restart",
1187
+ resumed.error
1188
+ );
1189
+ }
1190
+ const recovered = await refreshRuntimeConnection(client, resumed.value);
1191
+ if (!await isReusableBox(recovered, harness, probe)) {
1192
+ throw new SandboxRecoveryFailedError(
1193
+ stage,
1194
+ name,
1195
+ "probe",
1196
+ "the box is still unresponsive after a state-preserving restart"
1197
+ );
1198
+ }
1199
+ return recovered;
1200
+ }
1084
1201
  async function resolveWorkspaceSandboxClient(shell, workspaceId, userId) {
1085
1202
  const scope = { workspaceId, ...userId ? { userId } : {} };
1086
1203
  const creds = await shell.credentials(scope);
@@ -1100,6 +1217,29 @@ async function peekWorkspaceSandbox(shell, options) {
1100
1217
  if (match.status !== "running") return { status: "not-running", state: match.status, box: match };
1101
1218
  return { status: "running", box: match };
1102
1219
  }
1220
+ async function finalizeExistingBox(shell, client, box, stage, name, workspaceId, userId, harness, scope) {
1221
+ const written = await materializeDeferredFilesForExistingBox(
1222
+ shell,
1223
+ client,
1224
+ box,
1225
+ stage,
1226
+ name,
1227
+ workspaceId,
1228
+ userId,
1229
+ harness
1230
+ );
1231
+ if (!written.succeeded) {
1232
+ throw deferredProfileWriteFailed(stage, name, written.error);
1233
+ }
1234
+ const finalBox = written.value;
1235
+ if (shell.bootstrap) {
1236
+ const boot = await shell.bootstrap(finalBox, scope);
1237
+ if (!boot.succeeded) {
1238
+ throw new Error(`bootstrap failed on ${stage} box ${name}`, { cause: boot.error });
1239
+ }
1240
+ }
1241
+ return finalBox;
1242
+ }
1103
1243
  async function ensureWorkspaceSandbox(shell, options) {
1104
1244
  const { workspaceId, userId, harness, forceNew, onProgress, billingOwnerId } = options;
1105
1245
  const resolved = await resolveWorkspaceSandboxClient(shell, workspaceId, userId);
@@ -1119,40 +1259,24 @@ async function ensureWorkspaceSandbox(shell, options) {
1119
1259
  } else if (found.metadata?.harness === harness) {
1120
1260
  const ready = await refreshRuntimeConnection(client, found);
1121
1261
  if (await isReusableBox(ready, harness, shell.livenessProbe)) {
1122
- const written = await materializeDeferredFilesForExistingBox(
1123
- shell,
1124
- client,
1125
- ready,
1126
- "reused",
1127
- name,
1128
- workspaceId,
1129
- userId,
1130
- harness
1131
- );
1132
- if (!written.succeeded) {
1133
- throw deferredProfileWriteFailed("reused", name, written.error);
1134
- }
1135
- const reusedBox = written.value;
1136
- if (shell.bootstrap) {
1137
- const boot = await shell.bootstrap(reusedBox, scope);
1138
- if (!boot.succeeded) {
1139
- throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error });
1140
- }
1141
- }
1142
- return reusedBox;
1143
- }
1144
- const dropped = await deleteBox(ready);
1145
- if (!dropped.succeeded) {
1146
- throw new Error(
1147
- `sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
1148
- { cause: dropped.error }
1149
- );
1262
+ return finalizeExistingBox(shell, client, ready, "reused", name, workspaceId, userId, harness, scope);
1150
1263
  }
1264
+ const recovered = await recoverUnresponsiveBox(
1265
+ client,
1266
+ ready,
1267
+ harness,
1268
+ shell.livenessProbe,
1269
+ "reused",
1270
+ name,
1271
+ resumeTimeout,
1272
+ onProgress
1273
+ );
1274
+ return finalizeExistingBox(shell, client, recovered, "reused", name, workspaceId, userId, harness, scope);
1151
1275
  } else {
1152
1276
  const dropped = await deleteBox(found);
1153
1277
  if (!dropped.succeeded) {
1154
1278
  throw new Error(
1155
- `sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
1279
+ `sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}) could not be deleted`,
1156
1280
  { cause: dropped.error }
1157
1281
  );
1158
1282
  }
@@ -1185,35 +1309,19 @@ async function ensureWorkspaceSandbox(shell, options) {
1185
1309
  } else {
1186
1310
  const box2 = await refreshRuntimeConnection(client, resumed.value);
1187
1311
  if (await isReusableBox(box2, harness, shell.livenessProbe)) {
1188
- const written = await materializeDeferredFilesForExistingBox(
1189
- shell,
1190
- client,
1191
- box2,
1192
- "resumed",
1193
- name,
1194
- workspaceId,
1195
- userId,
1196
- harness
1197
- );
1198
- if (!written.succeeded) {
1199
- throw deferredProfileWriteFailed("resumed", name, written.error);
1200
- }
1201
- const resumedBox = written.value;
1202
- if (shell.bootstrap) {
1203
- const boot = await shell.bootstrap(resumedBox, scope);
1204
- if (!boot.succeeded) {
1205
- throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error });
1206
- }
1207
- }
1208
- return resumedBox;
1209
- }
1210
- const dropped = await deleteBox(box2);
1211
- if (!dropped.succeeded) {
1212
- throw new Error(
1213
- `resumed sandbox ${name} (was ${String(box2.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
1214
- { cause: dropped.error }
1215
- );
1312
+ return finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope);
1216
1313
  }
1314
+ const recovered = await recoverUnresponsiveBox(
1315
+ client,
1316
+ box2,
1317
+ harness,
1318
+ shell.livenessProbe,
1319
+ "resumed",
1320
+ name,
1321
+ resumeTimeout,
1322
+ onProgress
1323
+ );
1324
+ return finalizeExistingBox(shell, client, recovered, "resumed", name, workspaceId, userId, harness, scope);
1217
1325
  }
1218
1326
  }
1219
1327
  }
@@ -1232,7 +1340,7 @@ async function ensureWorkspaceSandbox(shell, options) {
1232
1340
  const { leanProfile, deferredFiles } = shell.deferProfileFiles ? splitDeferredProfileFiles(fullProfile) : { leanProfile: fullProfile, deferredFiles: [] };
1233
1341
  const profile = leanProfile;
1234
1342
  const role = userId && shell.permissionRole ? shell.permissionRole("developer") : void 0;
1235
- let model = shell.backendModelAtCreate ? resolveModel(shell.provider) : void 0;
1343
+ let model = shell.backendModelAtCreate ? requireTransportableModel(resolveModelSelection(shell.provider), `backendModelAtCreate for ${name}`) : void 0;
1236
1344
  if (model && shell.childKeyMint && model.provider === "openai-compat") {
1237
1345
  const minted = await shell.childKeyMint(scope);
1238
1346
  if (minted.succeeded) model = { ...model, apiKey: minted.value };
@@ -1298,22 +1406,6 @@ async function ensureWorkspaceSandbox(shell, options) {
1298
1406
  }
1299
1407
  return box;
1300
1408
  }
1301
- function resolveModel(config, override) {
1302
- const c = config ?? {};
1303
- const explicitBaseUrl = c.routerBaseUrl;
1304
- const explicitApiKey = override?.modelApiKey ?? c.apiKey;
1305
- const provider = c.providerName ?? (explicitApiKey ? "openai-compat" : c.openaiApiKey ? "openai" : void 0);
1306
- const modelName = override?.model ?? c.modelName ?? (provider === "openai" || provider === "openai-compat" ? c.defaultModel : void 0);
1307
- const apiKey = explicitApiKey ?? (provider === "openai" ? c.openaiApiKey : void 0);
1308
- if (!provider || !modelName) return void 0;
1309
- if (!apiKey && !c.allowKeylessModel) return void 0;
1310
- return {
1311
- model: modelName,
1312
- provider,
1313
- ...apiKey ? { apiKey } : {},
1314
- ...explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}
1315
- };
1316
- }
1317
1409
  function historyTranscript(history) {
1318
1410
  return history.map((entry) => `${entry.role === "assistant" ? "Assistant" : "User"}: ${entry.content}`).join("\n\n");
1319
1411
  }
@@ -1359,10 +1451,13 @@ function attachReasoningEffort(profile, harness, effort) {
1359
1451
  }
1360
1452
  async function* streamSandboxPrompt(shell, box, message, options) {
1361
1453
  const harness = options?.harness ?? "opencode";
1362
- const model = resolveModel(shell.provider, {
1363
- model: options?.model,
1364
- modelApiKey: options?.modelApiKey
1365
- });
1454
+ const model = requireTransportableModel(
1455
+ resolveModelSelection(shell.provider, {
1456
+ model: options?.model,
1457
+ modelApiKey: options?.modelApiKey
1458
+ }),
1459
+ "streamSandboxPrompt"
1460
+ );
1366
1461
  if (model?.model) assertHarnessModelCompatible(harness, model.model);
1367
1462
  const prompt = typeof message === "string" ? flattenHistory(message, options?.history) : mergeHistoryIntoParts(message, options?.history);
1368
1463
  const appToolMcp = options?.appToolMcp ?? {};
@@ -1535,10 +1630,13 @@ async function mintSandboxScopedToken(box, options) {
1535
1630
  }
1536
1631
  async function driveSandboxTurn(shell, box, message, options) {
1537
1632
  const harness = options.harness ?? "opencode";
1538
- const model = resolveModel(shell.provider, {
1539
- model: options.model,
1540
- modelApiKey: options.modelApiKey
1541
- });
1633
+ const model = requireTransportableModel(
1634
+ resolveModelSelection(shell.provider, {
1635
+ model: options.model,
1636
+ modelApiKey: options.modelApiKey
1637
+ }),
1638
+ "driveSandboxTurn"
1639
+ );
1542
1640
  if (model?.model) assertHarnessModelCompatible(harness, model.model);
1543
1641
  const prompt = typeof message === "string" ? flattenHistory(message, options.history) : mergeHistoryIntoParts(message, options.history);
1544
1642
  const appToolMcp = options.appToolMcp ?? {};
@@ -1614,6 +1712,10 @@ function firstQuestionText(value) {
1614
1712
  }
1615
1713
 
1616
1714
  export {
1715
+ resolveModelSelection,
1716
+ resolveModel,
1717
+ SandboxModelResolutionError,
1718
+ requireTransportableModel,
1617
1719
  shellQuote,
1618
1720
  statSandboxFileSize,
1619
1721
  readSandboxBinaryBytes,
@@ -1645,6 +1747,7 @@ export {
1645
1747
  runSandboxToolPathSetup,
1646
1748
  splitDeferredProfileFiles,
1647
1749
  SandboxRuntimeAuthRefreshError,
1750
+ SandboxRecoveryFailedError,
1648
1751
  writeProfileFilesToBox,
1649
1752
  deferredCorpusHash,
1650
1753
  PROVISION_PAYLOAD_MAX_BYTES,
@@ -1654,7 +1757,6 @@ export {
1654
1757
  assertEnvWithinLimits,
1655
1758
  peekWorkspaceSandbox,
1656
1759
  ensureWorkspaceSandbox,
1657
- resolveModel,
1658
1760
  flattenHistory,
1659
1761
  mergeHistoryIntoParts,
1660
1762
  mergeExtraMcp,
@@ -1675,4 +1777,4 @@ export {
1675
1777
  isTerminalPromptEvent,
1676
1778
  detectInteractiveQuestion
1677
1779
  };
1678
- //# sourceMappingURL=chunk-2ZSSOYXP.js.map
1780
+ //# sourceMappingURL=chunk-Q74BS43G.js.map