@tangle-network/sandbox 0.10.5 → 0.11.1-develop.20260717054349.4472dd9

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.
@@ -1,5 +1,5 @@
1
- import { n as normalizeSessionInfo, r as normalizeRuntimeBackendConfig, t as SandboxRuntimeApi } from "./runtime-api-CvAUrnHc.js";
2
- import { c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, l as ServerError, n as CapabilityError, p as parseErrorResponse, s as QuotaError, t as AuthError, u as StateError } from "./errors-DSz87Rkk.js";
1
+ import { l as normalizeRuntimeBackendConfig, n as normalizeSessionInfo, o as combineAbortSignals, r as uploadChunked, s as encodePromptForWire, t as SandboxRuntimeApi, u as normalizeRuntimeModelOverride } from "./runtime-api-fqaQ3CN2.js";
2
+ import { c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, l as ServerError, n as CapabilityError, p as parseErrorResponse, s as QuotaError, t as AuthError, u as StateError } from "./errors-Cbs78OlF.js";
3
3
  import { addTokenUsage, readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
4
4
  //#region src/lib/sse-parser.ts
5
5
  /**
@@ -18,6 +18,7 @@ async function* parseSSEStream(body, options) {
18
18
  let currentEvent = "";
19
19
  let dataLines = [];
20
20
  let currentId = "";
21
+ let completed = false;
21
22
  const flush = function* () {
22
23
  if (dataLines.length === 0) {
23
24
  currentEvent = "";
@@ -61,40 +62,13 @@ async function* parseSSEStream(body, options) {
61
62
  buffer += decoder.decode();
62
63
  if (buffer.length > 0) processLine(buffer);
63
64
  yield* flush();
65
+ completed = true;
64
66
  } finally {
67
+ if (!completed) await reader.cancel().catch(() => void 0);
65
68
  reader.releaseLock();
66
69
  }
67
70
  }
68
71
  //#endregion
69
- //#region src/lib/wire-encoding.ts
70
- /**
71
- * Encode a prompt text part for the wire. Server decodes at the route
72
- * boundary; the LLM only ever sees the original UTF-8. Done
73
- * unconditionally because readable bodies false-positive on ingress WAF
74
- * rules that pattern-match shell-injection-shaped substrings (which
75
- * legitimate prompts routinely contain — e.g. Step-0 dev-server
76
- * bootstrap).
77
- */
78
- function encodeTextForWire(text) {
79
- if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
80
- if (typeof btoa === "function") return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, h) => String.fromCharCode(Number.parseInt(h, 16))));
81
- throw new Error("encodeTextForWire: no base64 encoder available (Buffer and btoa both undefined)");
82
- }
83
- /**
84
- * Convert a caller-supplied prompt (string or parts array) into the
85
- * wire-format parts array. Every text part is base64-encoded; non-text
86
- * parts pass through. A bare string is wrapped in a single text part.
87
- */
88
- function encodePromptForWire(message) {
89
- return (typeof message === "string" ? [{
90
- type: "text",
91
- text: message
92
- }] : message).map((part) => part.type === "text" ? {
93
- type: "text",
94
- text: encodeTextForWire(part.text)
95
- } : part);
96
- }
97
- //#endregion
98
72
  //#region src/trace-exporter.ts
99
73
  function buildTraceExportPayload(bundle, format = "tangle", serviceName = "tangle-sandbox") {
100
74
  if (format === "tangle") return bundle;
@@ -635,6 +609,29 @@ var SandboxSession = class SandboxSession {
635
609
  async answer(answers) {
636
610
  return this.box._answerQuestion(this.id, answers);
637
611
  }
612
+ /** Return the newest plan awaiting a decision for this session. */
613
+ async plan() {
614
+ return this.box._currentPlan(this.id);
615
+ }
616
+ /** Approve a durable plan and enqueue its separate execution turn. */
617
+ async approvePlan(planId) {
618
+ const id = planId ?? (await this.requireCurrentPlan()).id;
619
+ return this.box._decidePlan(this.id, id, { outcome: "approved" });
620
+ }
621
+ /** Reject a durable plan with required feedback and enqueue a revision turn. */
622
+ async rejectPlan(feedback, planId) {
623
+ if (!feedback.trim()) throw new Error("Plan rejection feedback is required");
624
+ const id = planId ?? (await this.requireCurrentPlan()).id;
625
+ return this.box._decidePlan(this.id, id, {
626
+ outcome: "rejected",
627
+ feedback: feedback.trim()
628
+ });
629
+ }
630
+ async requireCurrentPlan() {
631
+ const plan = await this.plan();
632
+ if (!plan) throw new Error("No durable plan is awaiting a decision");
633
+ return plan;
634
+ }
638
635
  };
639
636
  //#endregion
640
637
  //#region src/task-session.ts
@@ -738,6 +735,10 @@ const SNAPSHOT_RESTORE_POLL_INTERVAL_MS = 1e3;
738
735
  const SNAPSHOT_RESTORE_TIMEOUT_MS = 600 * 1e3;
739
736
  const SNAPSHOT_VISIBILITY_POLL_INTERVAL_MS = 1e3;
740
737
  const SNAPSHOT_VISIBILITY_TIMEOUT_MS = 120 * 1e3;
738
+ const EXEC_DEFAULT_TIMEOUT_MS = 3e4;
739
+ const EXEC_MIN_TIMEOUT_MS = 100;
740
+ const EXEC_MAX_TIMEOUT_MS = 6e5;
741
+ const EXEC_TRANSPORT_MARGIN_MS = 15e3;
741
742
  const SNAPSHOT_CREATE_REQUEST_TIMEOUT_MS = 600 * 1e3;
742
743
  function toSnapshotResult(data) {
743
744
  return {
@@ -767,6 +768,11 @@ function runtimeFilesystemPath(endpoint, path) {
767
768
  * requirements; a turn whose tool failed (even if narrated around) fails.
768
769
  */
769
770
  function deriveOutcome(inputs) {
771
+ if (inputs.plan) return {
772
+ status: "awaiting_plan_decision",
773
+ success: false,
774
+ error: "Agent is awaiting a durable plan decision"
775
+ };
770
776
  if (inputs.question) return {
771
777
  status: "awaiting_question",
772
778
  success: false,
@@ -799,6 +805,18 @@ function deriveOutcome(inputs) {
799
805
  success: true
800
806
  };
801
807
  }
808
+ function readAwaitingPlan(data) {
809
+ const direct = data.plan;
810
+ const outcome = data.outcome;
811
+ const candidate = direct && typeof direct === "object" ? direct : outcome && typeof outcome === "object" && outcome.type === "awaiting_plan_decision" ? outcome.plan : void 0;
812
+ if (!candidate || typeof candidate !== "object") return void 0;
813
+ const plan = candidate;
814
+ if (typeof plan.id !== "string" || typeof plan.revision !== "number" || typeof plan.body !== "string" || typeof plan.submittedAt !== "string") return;
815
+ return plan;
816
+ }
817
+ function readPlanFromEvent(event) {
818
+ return event.type === "result" || event.type === "done" || event.type === "plan.submitted" ? readAwaitingPlan(event.data) : void 0;
819
+ }
802
820
  /** Read tool invocations off a `result` event payload. */
803
821
  function readToolInvocations(data) {
804
822
  const raw = data.toolInvocations;
@@ -920,6 +938,22 @@ const WRITE_MANY_DEFAULT_PACE_MS = 150;
920
938
  const WRITE_MANY_DEFAULT_MAX_RETRIES = 4;
921
939
  const WRITE_MANY_RETRY_BASE_MS = 250;
922
940
  const WRITE_MANY_RETRY_MAX_MS = 2e3;
941
+ function parseTextFileReadResponse(body) {
942
+ let payload;
943
+ try {
944
+ payload = JSON.parse(body);
945
+ } catch {
946
+ throw new ServerError("Runtime returned invalid JSON for file read", 502, {
947
+ origin: "runtime",
948
+ endpoint: "/files/read"
949
+ });
950
+ }
951
+ if (typeof payload !== "object" || payload === null || !("success" in payload) || payload.success !== true || !("data" in payload) || typeof payload.data !== "object" || payload.data === null || !("content" in payload.data) || typeof payload.data.content !== "string" || !("encoding" in payload.data) || payload.data.encoding !== "utf8") throw new ServerError("Runtime returned a malformed text file response", 502, {
952
+ origin: "runtime",
953
+ endpoint: "/files/read"
954
+ });
955
+ return payload.data.content;
956
+ }
923
957
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
924
958
  /** Transient file-write failures worth retrying: rate-limit (quota), 5xx,
925
959
  * transport (network), and request timeouts. A non-transient error (bad path,
@@ -1240,28 +1274,31 @@ var SandboxInstance = class SandboxInstance {
1240
1274
  */
1241
1275
  async exec(command, options) {
1242
1276
  await this.ensureRunning();
1277
+ const timeoutMs = options?.timeoutMs ?? EXEC_DEFAULT_TIMEOUT_MS;
1278
+ if (!Number.isInteger(timeoutMs) || timeoutMs < EXEC_MIN_TIMEOUT_MS || timeoutMs > EXEC_MAX_TIMEOUT_MS) throw new ValidationError(`exec timeoutMs must be an integer between ${EXEC_MIN_TIMEOUT_MS} and ${EXEC_MAX_TIMEOUT_MS}`);
1243
1279
  const headers = {};
1244
1280
  if (options?.sessionId) headers["X-Session-Id"] = options.sessionId;
1245
- const response = await this.runtimeFetch("/terminals/commands", {
1281
+ const response = await this.runtimeFetch("/process/spawn", {
1246
1282
  method: "POST",
1247
1283
  headers,
1248
1284
  body: JSON.stringify({
1249
1285
  command,
1250
1286
  cwd: options?.cwd,
1251
1287
  env: options?.env,
1252
- timeout: options?.timeoutMs
1288
+ timeoutMs,
1289
+ blocking: true
1253
1290
  })
1254
- });
1291
+ }, { timeoutMs: timeoutMs + EXEC_TRANSPORT_MARGIN_MS });
1255
1292
  if (!response.ok) {
1256
1293
  const body = await response.text();
1257
1294
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1258
1295
  }
1259
- const data = await response.json();
1260
- const result = data.result ?? data;
1296
+ const result = (await response.json()).data;
1297
+ if (!result || typeof result.exitCode !== "number" || typeof result.stdout !== "string" || typeof result.stderr !== "string") throw new ServerError("Sandbox process response was malformed", 502);
1261
1298
  return {
1262
- exitCode: result.exitCode ?? 0,
1263
- stdout: result.stdout ?? "",
1264
- stderr: result.stderr ?? ""
1299
+ exitCode: result.exitCode,
1300
+ stdout: result.stdout,
1301
+ stderr: result.stderr
1265
1302
  };
1266
1303
  }
1267
1304
  /**
@@ -1331,18 +1368,20 @@ var SandboxInstance = class SandboxInstance {
1331
1368
  */
1332
1369
  async read(path, options) {
1333
1370
  await this.ensureRunning();
1334
- const headers = {};
1335
- if (options?.sessionId) headers["X-Session-Id"] = options.sessionId;
1371
+ const headers = options?.sessionId ? { "X-Session-Id": options.sessionId } : void 0;
1336
1372
  const response = await this.runtimeFetch("/files/read", {
1337
1373
  method: "POST",
1338
1374
  headers,
1339
- body: JSON.stringify({ path })
1375
+ body: JSON.stringify({
1376
+ path,
1377
+ encoding: "utf8"
1378
+ })
1340
1379
  });
1341
1380
  if (!response.ok) {
1342
1381
  const body = await response.text();
1343
1382
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1344
1383
  }
1345
- return (await response.json()).data?.content ?? "";
1384
+ return parseTextFileReadResponse(await response.text());
1346
1385
  }
1347
1386
  /**
1348
1387
  * Write content to a file in the sandbox.
@@ -1393,7 +1432,10 @@ var SandboxInstance = class SandboxInstance {
1393
1432
  if (started && paceMs > 0) await delay(paceMs);
1394
1433
  started = true;
1395
1434
  try {
1396
- await this.write(file.path, file.content, { mode: file.mode });
1435
+ await this.write(file.path, file.content, {
1436
+ mode: file.mode,
1437
+ encoding: file.encoding
1438
+ });
1397
1439
  break;
1398
1440
  } catch (err) {
1399
1441
  if (isTransientWriteError(err) && attempt < maxRetries) {
@@ -1420,10 +1462,11 @@ var SandboxInstance = class SandboxInstance {
1420
1462
  let costUsd;
1421
1463
  let toolInvocations;
1422
1464
  let question;
1465
+ let plan;
1423
1466
  let terminalReached = false;
1424
1467
  const controller = new AbortController();
1425
1468
  try {
1426
- const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
1469
+ const signal = options?.signal ? combineAbortSignals([options.signal, controller.signal]) : controller.signal;
1427
1470
  for await (const event of this.streamPrompt(message, {
1428
1471
  ...options,
1429
1472
  signal
@@ -1433,6 +1476,7 @@ var SandboxInstance = class SandboxInstance {
1433
1476
  usage = readTokenUsage(event.data) ?? usage;
1434
1477
  costUsd = readTokenCostUsd(event.data) ?? costUsd;
1435
1478
  }
1479
+ plan = readPlanFromEvent(event) ?? plan;
1436
1480
  if (event.type === "result") toolInvocations = readToolInvocations(event.data) ?? toolInvocations;
1437
1481
  if (event.type === "result" || event.type === "done") terminalReached = true;
1438
1482
  if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
@@ -1449,7 +1493,8 @@ var SandboxInstance = class SandboxInstance {
1449
1493
  runErrorCode,
1450
1494
  toolInvocations,
1451
1495
  approval,
1452
- question
1496
+ question,
1497
+ plan
1453
1498
  });
1454
1499
  return {
1455
1500
  success: outcome.success,
@@ -1460,6 +1505,7 @@ var SandboxInstance = class SandboxInstance {
1460
1505
  toolInvocations,
1461
1506
  approval,
1462
1507
  question,
1508
+ plan,
1463
1509
  traceId,
1464
1510
  durationMs: Date.now() - startTime,
1465
1511
  usage,
@@ -1544,11 +1590,11 @@ var SandboxInstance = class SandboxInstance {
1544
1590
  * `error` + `done` so callers never see a thrown generator.
1545
1591
  */
1546
1592
  async *streamPromptInner(message, options) {
1593
+ if (options?.lastEventId && !options.executionId) throw new ValidationError("lastEventId requires executionId so the SDK can replay the correct execution", { executionId: "Required when lastEventId is set" });
1547
1594
  await this.ensureRunning();
1548
- const parts = encodePromptForWire(message);
1549
1595
  const timeoutMs = typeof options?.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : void 0;
1550
1596
  const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0;
1551
- const streamSignal = timeoutSignal ? options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal : options?.signal;
1597
+ const streamSignal = timeoutSignal ? options?.signal ? combineAbortSignals([options.signal, timeoutSignal]) : timeoutSignal : options?.signal;
1552
1598
  const throwIfTimedOut = () => {
1553
1599
  if (!timeoutSignal?.aborted || !timeoutMs) return;
1554
1600
  throw new TimeoutError(timeoutMs, `Prompt stream timed out after ${timeoutMs}ms`, {
@@ -1563,35 +1609,58 @@ var SandboxInstance = class SandboxInstance {
1563
1609
  ...data
1564
1610
  });
1565
1611
  };
1612
+ const explicitReplay = Boolean(options?.executionId && options.lastEventId);
1613
+ const requestEndpoint = explicitReplay ? `/agents/run/${encodeURIComponent(options?.executionId ?? "")}/events?lastEventId=${encodeURIComponent(options?.lastEventId ?? "")}&format=sse` : "/agents/run/stream";
1614
+ let lastEventId = options?.lastEventId;
1615
+ let executionId = options?.executionId;
1616
+ let sessionId = options?.sessionId;
1617
+ let receivedTerminal = false;
1618
+ const RECONNECT_BACKOFF_MS = [
1619
+ 2e3,
1620
+ 4e3,
1621
+ 8e3,
1622
+ 16e3,
1623
+ 3e4,
1624
+ 3e4
1625
+ ];
1626
+ const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1627
+ let lastReplayStatus;
1628
+ let lastExecutionStatus;
1629
+ let firstSseEventSeen = false;
1566
1630
  let response;
1567
1631
  try {
1568
1632
  logStreamCheckpoint("runtime_post_start", {
1569
- endpoint: "/agents/run/stream",
1633
+ endpoint: requestEndpoint,
1570
1634
  sessionId: options?.sessionId,
1571
1635
  executionId: options?.executionId,
1572
1636
  hasLastEventId: Boolean(options?.lastEventId),
1573
- detach: options?.detach === true
1637
+ detach: options?.detach === true,
1638
+ replayOnly: explicitReplay
1574
1639
  });
1575
- response = await this.runtimeFetch("/agents/run/stream", {
1640
+ const request = explicitReplay ? {
1641
+ method: "GET",
1642
+ signal: streamSignal
1643
+ } : {
1576
1644
  method: "POST",
1577
1645
  signal: streamSignal,
1578
1646
  headers: {
1579
1647
  ...options?.executionId ? { "X-Execution-ID": options.executionId } : {},
1580
- ...options?.lastEventId ? { "Last-Event-ID": options.lastEventId } : {},
1581
1648
  ...options?.detach ? { "x-agent-detach": "true" } : {}
1582
1649
  },
1583
1650
  body: JSON.stringify({
1584
1651
  identifier: "default",
1585
- parts,
1652
+ parts: encodePromptForWire(message),
1586
1653
  sessionId: options?.sessionId,
1587
1654
  turnId: options?.turnId,
1588
1655
  metadata: options?.context,
1589
1656
  requireVisibleAssistantOutput: options?.requireVisibleAssistantOutput,
1590
1657
  ...timeoutMs ? { timeoutMs } : {},
1591
1658
  ...options?.detach ? { detach: true } : {},
1592
- backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend, { model: options?.model })
1659
+ backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend),
1660
+ ...options?.model ? { model: normalizeRuntimeModelOverride(options.model) } : {}
1593
1661
  })
1594
- });
1662
+ };
1663
+ response = await this.runtimeFetch(requestEndpoint, request);
1595
1664
  logStreamCheckpoint("runtime_response_headers", {
1596
1665
  status: response.status,
1597
1666
  ok: response.ok
@@ -1601,27 +1670,20 @@ var SandboxInstance = class SandboxInstance {
1601
1670
  throwIfTimedOut();
1602
1671
  throw err;
1603
1672
  }
1604
- if (!response.ok) {
1673
+ if (!response.ok && !explicitReplay) {
1605
1674
  const body = await response.text();
1606
1675
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1607
1676
  }
1608
- let lastEventId = options?.lastEventId;
1609
- let executionId = options?.executionId;
1610
- let sessionId = options?.sessionId;
1611
- let receivedTerminal = false;
1612
- const RECONNECT_BACKOFF_MS = [
1613
- 2e3,
1614
- 4e3,
1615
- 8e3,
1616
- 16e3,
1617
- 3e4,
1618
- 3e4
1619
- ];
1620
- const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1621
- let lastReplayStatus;
1622
- let lastExecutionStatus;
1623
- let firstSseEventSeen = false;
1624
- for await (const event of this.parseSSEStream(response, streamSignal)) {
1677
+ if (!response.ok) {
1678
+ lastReplayStatus = response.status;
1679
+ await response.text();
1680
+ logStreamCheckpoint("replay_response_status", {
1681
+ attempt: 0,
1682
+ status: response.status,
1683
+ ok: false
1684
+ });
1685
+ } else for await (const event of this.parseSSEStream(response, streamSignal)) {
1686
+ if (event.type === "history.replay.start" || event.type === "history.replay.end") continue;
1625
1687
  if (!firstSseEventSeen) {
1626
1688
  firstSseEventSeen = true;
1627
1689
  logStreamCheckpoint("first_sse_event", {
@@ -1922,6 +1984,7 @@ var SandboxInstance = class SandboxInstance {
1922
1984
  let costUsd;
1923
1985
  let toolInvocations;
1924
1986
  let question;
1987
+ let plan;
1925
1988
  let terminalReached = false;
1926
1989
  try {
1927
1990
  for await (const event of this.streamPrompt(prompt, {
@@ -1938,6 +2001,7 @@ var SandboxInstance = class SandboxInstance {
1938
2001
  }
1939
2002
  if (event.type === "done" && costUsd === void 0) costUsd = readTokenCostUsd(event.data);
1940
2003
  if (event.type === "result" || event.type === "done") terminalReached = true;
2004
+ plan = readPlanFromEvent(event) ?? plan;
1941
2005
  if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
1942
2006
  if (event.type === "trace.id") traceId = event.data.traceId;
1943
2007
  if (event.type === "error") {
@@ -1952,7 +2016,8 @@ var SandboxInstance = class SandboxInstance {
1952
2016
  runErrorCode,
1953
2017
  toolInvocations,
1954
2018
  approval,
1955
- question
2019
+ question,
2020
+ plan
1956
2021
  });
1957
2022
  return {
1958
2023
  success: outcome.success,
@@ -1963,6 +2028,7 @@ var SandboxInstance = class SandboxInstance {
1963
2028
  toolInvocations,
1964
2029
  approval,
1965
2030
  question,
2031
+ plan,
1966
2032
  traceId,
1967
2033
  durationMs: Date.now() - startTime,
1968
2034
  usage,
@@ -2099,11 +2165,19 @@ var SandboxInstance = class SandboxInstance {
2099
2165
  async resourceUsage() {
2100
2166
  await this.ensureRunning();
2101
2167
  const response = await this.runtimeFetch("/health/detailed", { method: "GET" });
2102
- if (!response.ok) {
2103
- const body = await response.text();
2104
- throw parseErrorResponse(response.status, body, void 0, response.headers);
2168
+ const body = await response.text();
2169
+ let data;
2170
+ try {
2171
+ data = JSON.parse(body);
2172
+ } catch (error) {
2173
+ if (response.ok) throw new NetworkError("Detailed health returned a non-JSON response", error instanceof Error ? error : void 0, {
2174
+ endpoint: "/health/detailed",
2175
+ origin: "runtime"
2176
+ });
2105
2177
  }
2106
- return (await response.json()).resources ?? null;
2178
+ if (data?.resources) return data.resources;
2179
+ if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
2180
+ return null;
2107
2181
  }
2108
2182
  /**
2109
2183
  * Git capability object for repository operations.
@@ -2322,6 +2396,7 @@ var SandboxInstance = class SandboxInstance {
2322
2396
  writeMany: (files, options) => this.writeMany(files, options),
2323
2397
  search: (query, options) => this.search(query, options),
2324
2398
  upload: (localPath, remotePath, options) => this.fsUpload(localPath, remotePath, options),
2399
+ uploadData: (remotePath, data, options) => this.fsUploadData(remotePath, data, options),
2325
2400
  download: (remotePath, localPath, options) => this.fsDownload(remotePath, localPath, options),
2326
2401
  uploadDir: (localDir, remoteDir) => this.fsUploadDir(localDir, remoteDir),
2327
2402
  downloadDir: (remoteDir, localDir) => this.fsDownloadDir(remoteDir, localDir),
@@ -2369,6 +2444,14 @@ var SandboxInstance = class SandboxInstance {
2369
2444
  percentage: 100
2370
2445
  });
2371
2446
  }
2447
+ /** See {@link FileSystem.uploadData}. Delegates to the shared, browser-safe
2448
+ * chunked-upload flow in `lib/chunked-upload.ts`, wired to this sandbox's
2449
+ * runtime endpoint (works over the gateway proxy or a direct connection —
2450
+ * both are transparent through `runtimeFetch`). */
2451
+ async fsUploadData(remotePath, data, options) {
2452
+ await this.ensureRunning();
2453
+ return uploadChunked({ fetch: (path, init) => this.runtimeFetch(path, init) }, remotePath, data, options);
2454
+ }
2372
2455
  async fsDownload(remotePath, localPath, options) {
2373
2456
  await this.ensureRunning();
2374
2457
  const fs = await import("node:fs/promises");
@@ -2929,6 +3012,7 @@ var SandboxInstance = class SandboxInstance {
2929
3012
  */
2930
3013
  get process() {
2931
3014
  return {
3015
+ ensureDevServer: (options) => this.processEnsureDevServer(options),
2932
3016
  spawn: (command, options) => this.processSpawn(command, options),
2933
3017
  spawnExact: (executable, args, options) => this.processSpawnExact(executable, args, options),
2934
3018
  runCode: (code, options) => this.processRunCode(code, options),
@@ -2936,17 +3020,35 @@ var SandboxInstance = class SandboxInstance {
2936
3020
  get: (pid) => this.processGet(pid)
2937
3021
  };
2938
3022
  }
2939
- async processSpawnExact(executable, args, options) {
3023
+ async processEnsureDevServer(options) {
3024
+ await this.ensureRunning();
3025
+ if (options?.sessionId !== void 0) assertValidSessionId(options.sessionId);
3026
+ const headers = options?.sessionId ? { "X-Session-Id": options.sessionId } : void 0;
3027
+ const response = await this.runtimeFetch("/process/ensure-dev-server", {
3028
+ method: "POST",
3029
+ headers,
3030
+ body: JSON.stringify({})
3031
+ });
3032
+ if (!response.ok) {
3033
+ const body = await response.text();
3034
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3035
+ }
3036
+ const payload = await response.json();
3037
+ return {
3038
+ ...payload.data,
3039
+ startedAt: new Date(payload.data.startedAt)
3040
+ };
3041
+ }
3042
+ async processSpawn(command, options) {
2940
3043
  await this.ensureRunning();
2941
- const response = await this.runtimeFetch("/process/spawn-exact", {
3044
+ const response = await this.runtimeFetch("/process/spawn", {
2942
3045
  method: "POST",
2943
3046
  body: JSON.stringify({
2944
- executable,
2945
- args,
3047
+ command,
2946
3048
  cwd: options?.cwd,
2947
- env: options?.env ?? {},
2948
- stdin: options?.stdin,
2949
- timeoutMs: options?.timeoutMs
3049
+ env: options?.env,
3050
+ timeoutMs: options?.timeoutMs,
3051
+ blocking: false
2950
3052
  })
2951
3053
  });
2952
3054
  if (!response.ok) {
@@ -2954,17 +3056,19 @@ var SandboxInstance = class SandboxInstance {
2954
3056
  throw parseErrorResponse(response.status, body, void 0, response.headers);
2955
3057
  }
2956
3058
  const data = await response.json();
2957
- return this.createProcessHandle(data.data.pid, executable);
3059
+ return this.createProcessHandle(data.data.pid, command);
2958
3060
  }
2959
- async processSpawn(command, options) {
3061
+ async processSpawnExact(executable, args, options) {
2960
3062
  await this.ensureRunning();
2961
3063
  const response = await this.runtimeFetch("/process/spawn", {
2962
3064
  method: "POST",
2963
3065
  body: JSON.stringify({
2964
- command,
3066
+ executable,
3067
+ args,
2965
3068
  cwd: options?.cwd,
2966
3069
  env: options?.env,
2967
3070
  timeoutMs: options?.timeoutMs,
3071
+ stdin: options?.stdin,
2968
3072
  blocking: false
2969
3073
  })
2970
3074
  });
@@ -2973,7 +3077,7 @@ var SandboxInstance = class SandboxInstance {
2973
3077
  throw parseErrorResponse(response.status, body, void 0, response.headers);
2974
3078
  }
2975
3079
  const data = await response.json();
2976
- return this.createProcessHandle(data.data.pid, command);
3080
+ return this.createProcessHandle(data.data.pid, JSON.stringify([executable, ...args]));
2977
3081
  }
2978
3082
  async processRunCode(code, options) {
2979
3083
  await this.ensureRunning();
@@ -3856,7 +3960,7 @@ var SandboxInstance = class SandboxInstance {
3856
3960
  async resume(options = {}) {
3857
3961
  if (options.timeoutMs !== void 0 && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) throw new ValidationError("resume timeoutMs must be a positive number");
3858
3962
  const timeoutSignal = options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : void 0;
3859
- const signal = timeoutSignal ? options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal : options.signal;
3963
+ const signal = timeoutSignal ? options.signal ? combineAbortSignals([options.signal, timeoutSignal]) : timeoutSignal : options.signal;
3860
3964
  const response = await this.client.fetch(`/v1/sandboxes/${this.id}/resume`, {
3861
3965
  method: "POST",
3862
3966
  ...signal ? { signal } : {}
@@ -4097,9 +4201,10 @@ var SandboxInstance = class SandboxInstance {
4097
4201
  await this.refresh();
4098
4202
  if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
4099
4203
  }
4100
- async runtimeFetch(path, options) {
4204
+ async runtimeFetch(path, options, fetchOptions) {
4101
4205
  const url = `/v1/sandboxes/${encodeURIComponent(this.id)}/runtime${path}`;
4102
4206
  try {
4207
+ if (fetchOptions) return await this.client.fetch(url, options, fetchOptions);
4103
4208
  return options ? await this.client.fetch(url, options) : await this.client.fetch(url);
4104
4209
  } catch (err) {
4105
4210
  throw new NetworkError(`Failed to connect to sandbox: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0, {
@@ -4329,24 +4434,8 @@ var SandboxInstance = class SandboxInstance {
4329
4434
  * abort stamps `interrupted: true` explicitly.
4330
4435
  */
4331
4436
  async messages(opts) {
4332
- await this.ensureRunning();
4333
- const params = new URLSearchParams();
4334
- if (opts.limit !== void 0) params.set("limit", String(opts.limit));
4335
- if (opts.offset !== void 0) params.set("offset", String(opts.offset));
4336
- if (opts.since !== void 0) params.set("since", String(opts.since));
4337
- const query = params.toString() ? `?${params.toString()}` : "";
4338
- const response = await this.client.fetch(`/v1/sessions/${encodeURIComponent(opts.sessionId)}/messages${query}`, { method: "GET" });
4339
- if (!response.ok) {
4340
- const body = await response.text();
4341
- throw parseErrorResponse(response.status, body, void 0, response.headers);
4342
- }
4343
- return (await response.json()).map((m) => ({
4344
- id: m.info.id,
4345
- role: m.info.role,
4346
- timestamp: m.info.timestamp,
4347
- parts: m.parts,
4348
- metadata: m.info.metadata
4349
- }));
4437
+ const { sessionId, ...options } = opts;
4438
+ return this.runtime.messages(sessionId, options);
4350
4439
  }
4351
4440
  /** @internal — invoked by SandboxSession.sendMessage(). */
4352
4441
  async _sessionSendMessage(id, request, options = {}) {
@@ -4455,11 +4544,13 @@ var SandboxInstance = class SandboxInstance {
4455
4544
  }
4456
4545
  if (info) {
4457
4546
  const result = await this._sessionResult(opts.sessionId);
4547
+ const settled = settleTurnDrive({ ...result });
4548
+ if (settled.state === "awaiting_plan_decision") return settled;
4458
4549
  if (!result.success) return {
4459
4550
  state: "failed",
4460
4551
  error: `session ${opts.sessionId} ${info.status}: ${result.error ?? "no error detail"}`
4461
4552
  };
4462
- return settleTurnDrive({ ...result });
4553
+ return settled;
4463
4554
  }
4464
4555
  await this.dispatchPrompt(message, {
4465
4556
  ...opts,
@@ -4468,15 +4559,10 @@ var SandboxInstance = class SandboxInstance {
4468
4559
  return { state: "running" };
4469
4560
  }
4470
4561
  /**
4471
- * Mint a scoped, time-bounded JWT for direct browser access to this
4472
- * sandbox (Issue #913 Gap 1). Authority is the caller's
4473
- * `TANGLE_API_KEY` (sk-tan-*) the Sandbox API mints the token;
4474
- * signing secrets stay server-side.
4475
- *
4476
- * Use this to give a browser direct read access to the sandbox without
4477
- * leaking the full bearer (`box.connection.authToken`). The returned
4478
- * token verifies against the same sidecar middleware that already
4479
- * gates ProductTokenIssuer-issued JWTs — no new sidecar surface.
4562
+ * Mint a scoped, time-bounded JWT for browser access (Issue #913 Gap 1).
4563
+ * Authority is the caller's `TANGLE_API_KEY` (sk-tan-*); signing secrets
4564
+ * stay server-side. Session/project scopes work with SessionGatewayClient,
4565
+ * while session-runtime/read-only scopes work with SandboxRuntimeClient.
4480
4566
  */
4481
4567
  async mintScopedToken(opts) {
4482
4568
  if ((opts.scope === "session" || opts.scope === "session-runtime") && !opts.sessionId) throw new ValidationError(`${opts.scope} scope requires a sessionId`);
@@ -4515,17 +4601,22 @@ var SandboxInstance = class SandboxInstance {
4515
4601
  return normalizeSessionInfo(await response.json());
4516
4602
  }
4517
4603
  /** @internal — invoked by SandboxSession.events(). */
4518
- async *_sessionEvents(id, opts) {
4604
+ async *_sessionEvents(id, opts, stopOnTurnTerminal = false) {
4519
4605
  await this.ensureRunning();
4520
4606
  const search = new URLSearchParams();
4521
4607
  search.set("sessionId", id);
4522
4608
  if (opts?.since) search.set("since", opts.since);
4609
+ if (opts?.executionId) search.set("executionId", opts.executionId);
4523
4610
  const response = await this.runtimeFetch(`/agents/events?${search.toString()}`, { signal: opts?.signal });
4524
4611
  if (!response.ok) {
4525
4612
  const body = await response.text();
4526
4613
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4527
4614
  }
4528
- yield* this.parseSSEStream(response, opts?.signal);
4615
+ for await (const event of this.parseSSEStream(response, opts?.signal)) {
4616
+ yield event;
4617
+ const turnTerminal = event.type === "result" || event.type === "done" || event.type === "error";
4618
+ if ((stopOnTurnTerminal || opts?.executionId !== void 0) && turnTerminal) return;
4619
+ }
4529
4620
  }
4530
4621
  /** @internal — invoked by SandboxSession.result(). */
4531
4622
  async _sessionResult(id) {
@@ -4538,14 +4629,16 @@ var SandboxInstance = class SandboxInstance {
4538
4629
  let costUsd;
4539
4630
  let toolInvocations;
4540
4631
  let question;
4632
+ let plan;
4541
4633
  let terminalReached = false;
4542
- for await (const event of this._sessionEvents(id)) {
4634
+ for await (const event of this._sessionEvents(id, void 0, true)) {
4543
4635
  response = applySandboxEventText(response, event);
4544
4636
  if (event.type === "result" || event.type === "done") {
4545
4637
  const data = event.data;
4546
4638
  usage = readTokenUsage(data) ?? usage;
4547
4639
  costUsd = readTokenCostUsd(data) ?? costUsd;
4548
4640
  }
4641
+ plan = readPlanFromEvent(event) ?? plan;
4549
4642
  if (event.type === "result") {
4550
4643
  const data = event.data;
4551
4644
  toolInvocations = readToolInvocations(data) ?? toolInvocations;
@@ -4569,7 +4662,8 @@ var SandboxInstance = class SandboxInstance {
4569
4662
  runErrorCode,
4570
4663
  toolInvocations,
4571
4664
  approval,
4572
- question
4665
+ question,
4666
+ plan
4573
4667
  });
4574
4668
  return {
4575
4669
  success: outcome.success,
@@ -4580,6 +4674,7 @@ var SandboxInstance = class SandboxInstance {
4580
4674
  toolInvocations,
4581
4675
  approval,
4582
4676
  question,
4677
+ plan,
4583
4678
  traceId,
4584
4679
  durationMs: Date.now() - startTime,
4585
4680
  usage,
@@ -4711,8 +4806,48 @@ var SandboxInstance = class SandboxInstance {
4711
4806
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4712
4807
  }
4713
4808
  }
4809
+ /** @internal — invoked by SandboxSession.plan(). */
4810
+ async _currentPlan(sessionId) {
4811
+ const path = `/v1/sandboxes/${encodeURIComponent(this.id)}/agent-sessions/${encodeURIComponent(sessionId)}/plans/current`;
4812
+ const response = await this.client.fetch(path);
4813
+ if (response.status === 404) return null;
4814
+ if (!response.ok) {
4815
+ const body = await response.text();
4816
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
4817
+ }
4818
+ const decoded = await response.json();
4819
+ if (!decoded.plan) throw new ServerError("Durable plan response is missing plan", 502, {
4820
+ endpoint: path,
4821
+ origin: "control-plane"
4822
+ });
4823
+ return decoded.plan;
4824
+ }
4825
+ /** @internal — invoked by SandboxSession.approvePlan()/rejectPlan(). */
4826
+ async _decidePlan(sessionId, planId, decision) {
4827
+ const path = `/v1/sandboxes/${encodeURIComponent(this.id)}/agent-sessions/${encodeURIComponent(sessionId)}/plans/${encodeURIComponent(planId)}/decision`;
4828
+ const response = await this.client.fetch(path, {
4829
+ method: "POST",
4830
+ body: JSON.stringify(decision)
4831
+ });
4832
+ if (!response.ok) {
4833
+ const body = await response.text();
4834
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
4835
+ }
4836
+ const decoded = await response.json();
4837
+ if (!decoded.plan) throw new ServerError("Plan decision response is missing plan", 502, {
4838
+ endpoint: path,
4839
+ origin: "control-plane"
4840
+ });
4841
+ return decoded.plan;
4842
+ }
4714
4843
  };
4715
4844
  function settleTurnDrive(result) {
4845
+ const plan = readAwaitingPlan(result);
4846
+ if (plan) return {
4847
+ state: "awaiting_plan_decision",
4848
+ plan,
4849
+ result
4850
+ };
4716
4851
  const text = typeof result.text === "string" ? result.text : typeof result.response === "string" ? result.response : void 0;
4717
4852
  const status = typeof result.status === "string" ? result.status : deriveOutcome({
4718
4853
  terminalReached: true,
@@ -4795,4 +4930,4 @@ function quoteForShell(value) {
4795
4930
  return `'${value.replace(/'/g, "'\\''")}'`;
4796
4931
  }
4797
4932
  //#endregion
4798
- export { InteractiveSessionHandle as a, getSandboxEventText as c, exportTraceBundle as d, otelTraceIdForTangleTrace as f, parseSSEStream as g, encodeTextForWire as h, SandboxSession as i, normalizeConnection as l, encodePromptForWire as m, normalizeStartupDiagnostics as n, applySandboxEventText as o, toOtelJson as p, SandboxTaskSession as r, collectAgentResponseText as s, SandboxInstance as t, buildTraceExportPayload as u };
4933
+ export { InteractiveSessionHandle as a, getSandboxEventText as c, exportTraceBundle as d, otelTraceIdForTangleTrace as f, SandboxSession as i, normalizeConnection as l, parseSSEStream as m, normalizeStartupDiagnostics as n, applySandboxEventText as o, toOtelJson as p, SandboxTaskSession as r, collectAgentResponseText as s, SandboxInstance as t, buildTraceExportPayload as u };