@tangle-network/sandbox 0.10.5 → 0.11.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.
@@ -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 } from "./runtime-api-kukaWPtF.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
@@ -767,6 +764,11 @@ function runtimeFilesystemPath(endpoint, path) {
767
764
  * requirements; a turn whose tool failed (even if narrated around) fails.
768
765
  */
769
766
  function deriveOutcome(inputs) {
767
+ if (inputs.plan) return {
768
+ status: "awaiting_plan_decision",
769
+ success: false,
770
+ error: "Agent is awaiting a durable plan decision"
771
+ };
770
772
  if (inputs.question) return {
771
773
  status: "awaiting_question",
772
774
  success: false,
@@ -799,6 +801,18 @@ function deriveOutcome(inputs) {
799
801
  success: true
800
802
  };
801
803
  }
804
+ function readAwaitingPlan(data) {
805
+ const direct = data.plan;
806
+ const outcome = data.outcome;
807
+ const candidate = direct && typeof direct === "object" ? direct : outcome && typeof outcome === "object" && outcome.type === "awaiting_plan_decision" ? outcome.plan : void 0;
808
+ if (!candidate || typeof candidate !== "object") return void 0;
809
+ const plan = candidate;
810
+ if (typeof plan.id !== "string" || typeof plan.revision !== "number" || typeof plan.body !== "string" || typeof plan.submittedAt !== "string") return;
811
+ return plan;
812
+ }
813
+ function readPlanFromEvent(event) {
814
+ return event.type === "result" || event.type === "done" || event.type === "plan.submitted" ? readAwaitingPlan(event.data) : void 0;
815
+ }
802
816
  /** Read tool invocations off a `result` event payload. */
803
817
  function readToolInvocations(data) {
804
818
  const raw = data.toolInvocations;
@@ -920,6 +934,22 @@ const WRITE_MANY_DEFAULT_PACE_MS = 150;
920
934
  const WRITE_MANY_DEFAULT_MAX_RETRIES = 4;
921
935
  const WRITE_MANY_RETRY_BASE_MS = 250;
922
936
  const WRITE_MANY_RETRY_MAX_MS = 2e3;
937
+ function parseTextFileReadResponse(body) {
938
+ let payload;
939
+ try {
940
+ payload = JSON.parse(body);
941
+ } catch {
942
+ throw new ServerError("Runtime returned invalid JSON for file read", 502, {
943
+ origin: "runtime",
944
+ endpoint: "/files/read"
945
+ });
946
+ }
947
+ 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, {
948
+ origin: "runtime",
949
+ endpoint: "/files/read"
950
+ });
951
+ return payload.data.content;
952
+ }
923
953
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
924
954
  /** Transient file-write failures worth retrying: rate-limit (quota), 5xx,
925
955
  * transport (network), and request timeouts. A non-transient error (bad path,
@@ -1331,18 +1361,20 @@ var SandboxInstance = class SandboxInstance {
1331
1361
  */
1332
1362
  async read(path, options) {
1333
1363
  await this.ensureRunning();
1334
- const headers = {};
1335
- if (options?.sessionId) headers["X-Session-Id"] = options.sessionId;
1364
+ const headers = options?.sessionId ? { "X-Session-Id": options.sessionId } : void 0;
1336
1365
  const response = await this.runtimeFetch("/files/read", {
1337
1366
  method: "POST",
1338
1367
  headers,
1339
- body: JSON.stringify({ path })
1368
+ body: JSON.stringify({
1369
+ path,
1370
+ encoding: "utf8"
1371
+ })
1340
1372
  });
1341
1373
  if (!response.ok) {
1342
1374
  const body = await response.text();
1343
1375
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1344
1376
  }
1345
- return (await response.json()).data?.content ?? "";
1377
+ return parseTextFileReadResponse(await response.text());
1346
1378
  }
1347
1379
  /**
1348
1380
  * Write content to a file in the sandbox.
@@ -1393,7 +1425,10 @@ var SandboxInstance = class SandboxInstance {
1393
1425
  if (started && paceMs > 0) await delay(paceMs);
1394
1426
  started = true;
1395
1427
  try {
1396
- await this.write(file.path, file.content, { mode: file.mode });
1428
+ await this.write(file.path, file.content, {
1429
+ mode: file.mode,
1430
+ encoding: file.encoding
1431
+ });
1397
1432
  break;
1398
1433
  } catch (err) {
1399
1434
  if (isTransientWriteError(err) && attempt < maxRetries) {
@@ -1420,10 +1455,11 @@ var SandboxInstance = class SandboxInstance {
1420
1455
  let costUsd;
1421
1456
  let toolInvocations;
1422
1457
  let question;
1458
+ let plan;
1423
1459
  let terminalReached = false;
1424
1460
  const controller = new AbortController();
1425
1461
  try {
1426
- const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
1462
+ const signal = options?.signal ? combineAbortSignals([options.signal, controller.signal]) : controller.signal;
1427
1463
  for await (const event of this.streamPrompt(message, {
1428
1464
  ...options,
1429
1465
  signal
@@ -1433,6 +1469,7 @@ var SandboxInstance = class SandboxInstance {
1433
1469
  usage = readTokenUsage(event.data) ?? usage;
1434
1470
  costUsd = readTokenCostUsd(event.data) ?? costUsd;
1435
1471
  }
1472
+ plan = readPlanFromEvent(event) ?? plan;
1436
1473
  if (event.type === "result") toolInvocations = readToolInvocations(event.data) ?? toolInvocations;
1437
1474
  if (event.type === "result" || event.type === "done") terminalReached = true;
1438
1475
  if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
@@ -1449,7 +1486,8 @@ var SandboxInstance = class SandboxInstance {
1449
1486
  runErrorCode,
1450
1487
  toolInvocations,
1451
1488
  approval,
1452
- question
1489
+ question,
1490
+ plan
1453
1491
  });
1454
1492
  return {
1455
1493
  success: outcome.success,
@@ -1460,6 +1498,7 @@ var SandboxInstance = class SandboxInstance {
1460
1498
  toolInvocations,
1461
1499
  approval,
1462
1500
  question,
1501
+ plan,
1463
1502
  traceId,
1464
1503
  durationMs: Date.now() - startTime,
1465
1504
  usage,
@@ -1544,11 +1583,11 @@ var SandboxInstance = class SandboxInstance {
1544
1583
  * `error` + `done` so callers never see a thrown generator.
1545
1584
  */
1546
1585
  async *streamPromptInner(message, options) {
1586
+ 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
1587
  await this.ensureRunning();
1548
- const parts = encodePromptForWire(message);
1549
1588
  const timeoutMs = typeof options?.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : void 0;
1550
1589
  const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0;
1551
- const streamSignal = timeoutSignal ? options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal : options?.signal;
1590
+ const streamSignal = timeoutSignal ? options?.signal ? combineAbortSignals([options.signal, timeoutSignal]) : timeoutSignal : options?.signal;
1552
1591
  const throwIfTimedOut = () => {
1553
1592
  if (!timeoutSignal?.aborted || !timeoutMs) return;
1554
1593
  throw new TimeoutError(timeoutMs, `Prompt stream timed out after ${timeoutMs}ms`, {
@@ -1563,26 +1602,47 @@ var SandboxInstance = class SandboxInstance {
1563
1602
  ...data
1564
1603
  });
1565
1604
  };
1605
+ const explicitReplay = Boolean(options?.executionId && options.lastEventId);
1606
+ const requestEndpoint = explicitReplay ? `/agents/run/${encodeURIComponent(options?.executionId ?? "")}/events?lastEventId=${encodeURIComponent(options?.lastEventId ?? "")}&format=sse` : "/agents/run/stream";
1607
+ let lastEventId = options?.lastEventId;
1608
+ let executionId = options?.executionId;
1609
+ let sessionId = options?.sessionId;
1610
+ let receivedTerminal = false;
1611
+ const RECONNECT_BACKOFF_MS = [
1612
+ 2e3,
1613
+ 4e3,
1614
+ 8e3,
1615
+ 16e3,
1616
+ 3e4,
1617
+ 3e4
1618
+ ];
1619
+ const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1620
+ let lastReplayStatus;
1621
+ let lastExecutionStatus;
1622
+ let firstSseEventSeen = false;
1566
1623
  let response;
1567
1624
  try {
1568
1625
  logStreamCheckpoint("runtime_post_start", {
1569
- endpoint: "/agents/run/stream",
1626
+ endpoint: requestEndpoint,
1570
1627
  sessionId: options?.sessionId,
1571
1628
  executionId: options?.executionId,
1572
1629
  hasLastEventId: Boolean(options?.lastEventId),
1573
- detach: options?.detach === true
1630
+ detach: options?.detach === true,
1631
+ replayOnly: explicitReplay
1574
1632
  });
1575
- response = await this.runtimeFetch("/agents/run/stream", {
1633
+ const request = explicitReplay ? {
1634
+ method: "GET",
1635
+ signal: streamSignal
1636
+ } : {
1576
1637
  method: "POST",
1577
1638
  signal: streamSignal,
1578
1639
  headers: {
1579
1640
  ...options?.executionId ? { "X-Execution-ID": options.executionId } : {},
1580
- ...options?.lastEventId ? { "Last-Event-ID": options.lastEventId } : {},
1581
1641
  ...options?.detach ? { "x-agent-detach": "true" } : {}
1582
1642
  },
1583
1643
  body: JSON.stringify({
1584
1644
  identifier: "default",
1585
- parts,
1645
+ parts: encodePromptForWire(message),
1586
1646
  sessionId: options?.sessionId,
1587
1647
  turnId: options?.turnId,
1588
1648
  metadata: options?.context,
@@ -1591,7 +1651,8 @@ var SandboxInstance = class SandboxInstance {
1591
1651
  ...options?.detach ? { detach: true } : {},
1592
1652
  backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend, { model: options?.model })
1593
1653
  })
1594
- });
1654
+ };
1655
+ response = await this.runtimeFetch(requestEndpoint, request);
1595
1656
  logStreamCheckpoint("runtime_response_headers", {
1596
1657
  status: response.status,
1597
1658
  ok: response.ok
@@ -1601,27 +1662,20 @@ var SandboxInstance = class SandboxInstance {
1601
1662
  throwIfTimedOut();
1602
1663
  throw err;
1603
1664
  }
1604
- if (!response.ok) {
1665
+ if (!response.ok && !explicitReplay) {
1605
1666
  const body = await response.text();
1606
1667
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1607
1668
  }
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)) {
1669
+ if (!response.ok) {
1670
+ lastReplayStatus = response.status;
1671
+ await response.text();
1672
+ logStreamCheckpoint("replay_response_status", {
1673
+ attempt: 0,
1674
+ status: response.status,
1675
+ ok: false
1676
+ });
1677
+ } else for await (const event of this.parseSSEStream(response, streamSignal)) {
1678
+ if (event.type === "history.replay.start" || event.type === "history.replay.end") continue;
1625
1679
  if (!firstSseEventSeen) {
1626
1680
  firstSseEventSeen = true;
1627
1681
  logStreamCheckpoint("first_sse_event", {
@@ -1922,6 +1976,7 @@ var SandboxInstance = class SandboxInstance {
1922
1976
  let costUsd;
1923
1977
  let toolInvocations;
1924
1978
  let question;
1979
+ let plan;
1925
1980
  let terminalReached = false;
1926
1981
  try {
1927
1982
  for await (const event of this.streamPrompt(prompt, {
@@ -1938,6 +1993,7 @@ var SandboxInstance = class SandboxInstance {
1938
1993
  }
1939
1994
  if (event.type === "done" && costUsd === void 0) costUsd = readTokenCostUsd(event.data);
1940
1995
  if (event.type === "result" || event.type === "done") terminalReached = true;
1996
+ plan = readPlanFromEvent(event) ?? plan;
1941
1997
  if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
1942
1998
  if (event.type === "trace.id") traceId = event.data.traceId;
1943
1999
  if (event.type === "error") {
@@ -1952,7 +2008,8 @@ var SandboxInstance = class SandboxInstance {
1952
2008
  runErrorCode,
1953
2009
  toolInvocations,
1954
2010
  approval,
1955
- question
2011
+ question,
2012
+ plan
1956
2013
  });
1957
2014
  return {
1958
2015
  success: outcome.success,
@@ -1963,6 +2020,7 @@ var SandboxInstance = class SandboxInstance {
1963
2020
  toolInvocations,
1964
2021
  approval,
1965
2022
  question,
2023
+ plan,
1966
2024
  traceId,
1967
2025
  durationMs: Date.now() - startTime,
1968
2026
  usage,
@@ -2099,11 +2157,19 @@ var SandboxInstance = class SandboxInstance {
2099
2157
  async resourceUsage() {
2100
2158
  await this.ensureRunning();
2101
2159
  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);
2160
+ const body = await response.text();
2161
+ let data;
2162
+ try {
2163
+ data = JSON.parse(body);
2164
+ } catch (error) {
2165
+ if (response.ok) throw new NetworkError("Detailed health returned a non-JSON response", error instanceof Error ? error : void 0, {
2166
+ endpoint: "/health/detailed",
2167
+ origin: "runtime"
2168
+ });
2105
2169
  }
2106
- return (await response.json()).resources ?? null;
2170
+ if (data?.resources) return data.resources;
2171
+ if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
2172
+ return null;
2107
2173
  }
2108
2174
  /**
2109
2175
  * Git capability object for repository operations.
@@ -2322,6 +2388,7 @@ var SandboxInstance = class SandboxInstance {
2322
2388
  writeMany: (files, options) => this.writeMany(files, options),
2323
2389
  search: (query, options) => this.search(query, options),
2324
2390
  upload: (localPath, remotePath, options) => this.fsUpload(localPath, remotePath, options),
2391
+ uploadData: (remotePath, data, options) => this.fsUploadData(remotePath, data, options),
2325
2392
  download: (remotePath, localPath, options) => this.fsDownload(remotePath, localPath, options),
2326
2393
  uploadDir: (localDir, remoteDir) => this.fsUploadDir(localDir, remoteDir),
2327
2394
  downloadDir: (remoteDir, localDir) => this.fsDownloadDir(remoteDir, localDir),
@@ -2369,6 +2436,14 @@ var SandboxInstance = class SandboxInstance {
2369
2436
  percentage: 100
2370
2437
  });
2371
2438
  }
2439
+ /** See {@link FileSystem.uploadData}. Delegates to the shared, browser-safe
2440
+ * chunked-upload flow in `lib/chunked-upload.ts`, wired to this sandbox's
2441
+ * runtime endpoint (works over the gateway proxy or a direct connection —
2442
+ * both are transparent through `runtimeFetch`). */
2443
+ async fsUploadData(remotePath, data, options) {
2444
+ await this.ensureRunning();
2445
+ return uploadChunked({ fetch: (path, init) => this.runtimeFetch(path, init) }, remotePath, data, options);
2446
+ }
2372
2447
  async fsDownload(remotePath, localPath, options) {
2373
2448
  await this.ensureRunning();
2374
2449
  const fs = await import("node:fs/promises");
@@ -2930,32 +3005,11 @@ var SandboxInstance = class SandboxInstance {
2930
3005
  get process() {
2931
3006
  return {
2932
3007
  spawn: (command, options) => this.processSpawn(command, options),
2933
- spawnExact: (executable, args, options) => this.processSpawnExact(executable, args, options),
2934
3008
  runCode: (code, options) => this.processRunCode(code, options),
2935
3009
  list: () => this.processList(),
2936
3010
  get: (pid) => this.processGet(pid)
2937
3011
  };
2938
3012
  }
2939
- async processSpawnExact(executable, args, options) {
2940
- await this.ensureRunning();
2941
- const response = await this.runtimeFetch("/process/spawn-exact", {
2942
- method: "POST",
2943
- body: JSON.stringify({
2944
- executable,
2945
- args,
2946
- cwd: options?.cwd,
2947
- env: options?.env ?? {},
2948
- stdin: options?.stdin,
2949
- timeoutMs: options?.timeoutMs
2950
- })
2951
- });
2952
- if (!response.ok) {
2953
- const body = await response.text();
2954
- throw parseErrorResponse(response.status, body, void 0, response.headers);
2955
- }
2956
- const data = await response.json();
2957
- return this.createProcessHandle(data.data.pid, executable);
2958
- }
2959
3013
  async processSpawn(command, options) {
2960
3014
  await this.ensureRunning();
2961
3015
  const response = await this.runtimeFetch("/process/spawn", {
@@ -3856,7 +3910,7 @@ var SandboxInstance = class SandboxInstance {
3856
3910
  async resume(options = {}) {
3857
3911
  if (options.timeoutMs !== void 0 && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) throw new ValidationError("resume timeoutMs must be a positive number");
3858
3912
  const timeoutSignal = options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : void 0;
3859
- const signal = timeoutSignal ? options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal : options.signal;
3913
+ const signal = timeoutSignal ? options.signal ? combineAbortSignals([options.signal, timeoutSignal]) : timeoutSignal : options.signal;
3860
3914
  const response = await this.client.fetch(`/v1/sandboxes/${this.id}/resume`, {
3861
3915
  method: "POST",
3862
3916
  ...signal ? { signal } : {}
@@ -4329,24 +4383,8 @@ var SandboxInstance = class SandboxInstance {
4329
4383
  * abort stamps `interrupted: true` explicitly.
4330
4384
  */
4331
4385
  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
- }));
4386
+ const { sessionId, ...options } = opts;
4387
+ return this.runtime.messages(sessionId, options);
4350
4388
  }
4351
4389
  /** @internal — invoked by SandboxSession.sendMessage(). */
4352
4390
  async _sessionSendMessage(id, request, options = {}) {
@@ -4455,11 +4493,13 @@ var SandboxInstance = class SandboxInstance {
4455
4493
  }
4456
4494
  if (info) {
4457
4495
  const result = await this._sessionResult(opts.sessionId);
4496
+ const settled = settleTurnDrive({ ...result });
4497
+ if (settled.state === "awaiting_plan_decision") return settled;
4458
4498
  if (!result.success) return {
4459
4499
  state: "failed",
4460
4500
  error: `session ${opts.sessionId} ${info.status}: ${result.error ?? "no error detail"}`
4461
4501
  };
4462
- return settleTurnDrive({ ...result });
4502
+ return settled;
4463
4503
  }
4464
4504
  await this.dispatchPrompt(message, {
4465
4505
  ...opts,
@@ -4468,15 +4508,10 @@ var SandboxInstance = class SandboxInstance {
4468
4508
  return { state: "running" };
4469
4509
  }
4470
4510
  /**
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.
4511
+ * Mint a scoped, time-bounded JWT for browser access (Issue #913 Gap 1).
4512
+ * Authority is the caller's `TANGLE_API_KEY` (sk-tan-*); signing secrets
4513
+ * stay server-side. Session/project scopes work with SessionGatewayClient,
4514
+ * while session-runtime/read-only scopes work with SandboxRuntimeClient.
4480
4515
  */
4481
4516
  async mintScopedToken(opts) {
4482
4517
  if ((opts.scope === "session" || opts.scope === "session-runtime") && !opts.sessionId) throw new ValidationError(`${opts.scope} scope requires a sessionId`);
@@ -4515,17 +4550,22 @@ var SandboxInstance = class SandboxInstance {
4515
4550
  return normalizeSessionInfo(await response.json());
4516
4551
  }
4517
4552
  /** @internal — invoked by SandboxSession.events(). */
4518
- async *_sessionEvents(id, opts) {
4553
+ async *_sessionEvents(id, opts, stopOnTurnTerminal = false) {
4519
4554
  await this.ensureRunning();
4520
4555
  const search = new URLSearchParams();
4521
4556
  search.set("sessionId", id);
4522
4557
  if (opts?.since) search.set("since", opts.since);
4558
+ if (opts?.executionId) search.set("executionId", opts.executionId);
4523
4559
  const response = await this.runtimeFetch(`/agents/events?${search.toString()}`, { signal: opts?.signal });
4524
4560
  if (!response.ok) {
4525
4561
  const body = await response.text();
4526
4562
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4527
4563
  }
4528
- yield* this.parseSSEStream(response, opts?.signal);
4564
+ for await (const event of this.parseSSEStream(response, opts?.signal)) {
4565
+ yield event;
4566
+ const turnTerminal = event.type === "result" || event.type === "done" || event.type === "error";
4567
+ if ((stopOnTurnTerminal || opts?.executionId !== void 0) && turnTerminal) return;
4568
+ }
4529
4569
  }
4530
4570
  /** @internal — invoked by SandboxSession.result(). */
4531
4571
  async _sessionResult(id) {
@@ -4538,14 +4578,16 @@ var SandboxInstance = class SandboxInstance {
4538
4578
  let costUsd;
4539
4579
  let toolInvocations;
4540
4580
  let question;
4581
+ let plan;
4541
4582
  let terminalReached = false;
4542
- for await (const event of this._sessionEvents(id)) {
4583
+ for await (const event of this._sessionEvents(id, void 0, true)) {
4543
4584
  response = applySandboxEventText(response, event);
4544
4585
  if (event.type === "result" || event.type === "done") {
4545
4586
  const data = event.data;
4546
4587
  usage = readTokenUsage(data) ?? usage;
4547
4588
  costUsd = readTokenCostUsd(data) ?? costUsd;
4548
4589
  }
4590
+ plan = readPlanFromEvent(event) ?? plan;
4549
4591
  if (event.type === "result") {
4550
4592
  const data = event.data;
4551
4593
  toolInvocations = readToolInvocations(data) ?? toolInvocations;
@@ -4569,7 +4611,8 @@ var SandboxInstance = class SandboxInstance {
4569
4611
  runErrorCode,
4570
4612
  toolInvocations,
4571
4613
  approval,
4572
- question
4614
+ question,
4615
+ plan
4573
4616
  });
4574
4617
  return {
4575
4618
  success: outcome.success,
@@ -4580,6 +4623,7 @@ var SandboxInstance = class SandboxInstance {
4580
4623
  toolInvocations,
4581
4624
  approval,
4582
4625
  question,
4626
+ plan,
4583
4627
  traceId,
4584
4628
  durationMs: Date.now() - startTime,
4585
4629
  usage,
@@ -4711,8 +4755,48 @@ var SandboxInstance = class SandboxInstance {
4711
4755
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4712
4756
  }
4713
4757
  }
4758
+ /** @internal — invoked by SandboxSession.plan(). */
4759
+ async _currentPlan(sessionId) {
4760
+ const path = `/v1/sandboxes/${encodeURIComponent(this.id)}/agent-sessions/${encodeURIComponent(sessionId)}/plans/current`;
4761
+ const response = await this.client.fetch(path);
4762
+ if (response.status === 404) return null;
4763
+ if (!response.ok) {
4764
+ const body = await response.text();
4765
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
4766
+ }
4767
+ const decoded = await response.json();
4768
+ if (!decoded.plan) throw new ServerError("Durable plan response is missing plan", 502, {
4769
+ endpoint: path,
4770
+ origin: "control-plane"
4771
+ });
4772
+ return decoded.plan;
4773
+ }
4774
+ /** @internal — invoked by SandboxSession.approvePlan()/rejectPlan(). */
4775
+ async _decidePlan(sessionId, planId, decision) {
4776
+ const path = `/v1/sandboxes/${encodeURIComponent(this.id)}/agent-sessions/${encodeURIComponent(sessionId)}/plans/${encodeURIComponent(planId)}/decision`;
4777
+ const response = await this.client.fetch(path, {
4778
+ method: "POST",
4779
+ body: JSON.stringify(decision)
4780
+ });
4781
+ if (!response.ok) {
4782
+ const body = await response.text();
4783
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
4784
+ }
4785
+ const decoded = await response.json();
4786
+ if (!decoded.plan) throw new ServerError("Plan decision response is missing plan", 502, {
4787
+ endpoint: path,
4788
+ origin: "control-plane"
4789
+ });
4790
+ return decoded.plan;
4791
+ }
4714
4792
  };
4715
4793
  function settleTurnDrive(result) {
4794
+ const plan = readAwaitingPlan(result);
4795
+ if (plan) return {
4796
+ state: "awaiting_plan_decision",
4797
+ plan,
4798
+ result
4799
+ };
4716
4800
  const text = typeof result.text === "string" ? result.text : typeof result.response === "string" ? result.response : void 0;
4717
4801
  const status = typeof result.status === "string" ? result.status : deriveOutcome({
4718
4802
  terminalReached: true,
@@ -4795,4 +4879,4 @@ function quoteForShell(value) {
4795
4879
  return `'${value.replace(/'/g, "'\\''")}'`;
4796
4880
  }
4797
4881
  //#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 };
4882
+ 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 };
@@ -1,2 +1,2 @@
1
- import { a as TANGLE_JOBS_CONTRACT, c as AgentSandboxBlueprintAbi, d as SandboxCreateParamTypes, f as SandboxCreateResponseParamTypes, i as TANGLE_CHAIN_ID, l as ITangleJobsAbi, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, p as SandboxIdParamTypes, r as JOB_SANDBOX_DELETE, s as TangleSandboxClientConfig, t as TangleSandboxClient, u as JsonResponseParamTypes } from "../index-DF36P3Ew.js";
1
+ import { a as TANGLE_JOBS_CONTRACT, c as AgentSandboxBlueprintAbi, d as SandboxCreateParamTypes, f as SandboxCreateResponseParamTypes, i as TANGLE_CHAIN_ID, l as ITangleJobsAbi, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, p as SandboxIdParamTypes, r as JOB_SANDBOX_DELETE, s as TangleSandboxClientConfig, t as TangleSandboxClient, u as JsonResponseParamTypes } from "../index-V2vjBfHU.js";
2
2
  export { AgentSandboxBlueprintAbi, ITangleJobsAbi, JOB_SANDBOX_CREATE, JOB_SANDBOX_DELETE, JsonResponseParamTypes, SandboxCreateParamTypes, SandboxCreateResponseParamTypes, SandboxIdParamTypes, TANGLE_CHAIN_ID, TANGLE_JOBS_CONTRACT, TANGLE_MAINNET_RPC, TangleSandboxClient, TangleSandboxClientConfig };
@@ -1,2 +1,2 @@
1
- import { a as TANGLE_JOBS_CONTRACT, c as ITangleJobsAbi, d as SandboxCreateResponseParamTypes, f as SandboxIdParamTypes, i as TANGLE_CHAIN_ID, l as JsonResponseParamTypes, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, r as JOB_SANDBOX_DELETE, s as AgentSandboxBlueprintAbi, t as TangleSandboxClient, u as SandboxCreateParamTypes } from "../tangle-Bqbf3dA8.js";
1
+ import { a as TANGLE_JOBS_CONTRACT, c as ITangleJobsAbi, d as SandboxCreateResponseParamTypes, f as SandboxIdParamTypes, i as TANGLE_CHAIN_ID, l as JsonResponseParamTypes, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, r as JOB_SANDBOX_DELETE, s as AgentSandboxBlueprintAbi, t as TangleSandboxClient, u as SandboxCreateParamTypes } from "../tangle-051CtV2b.js";
2
2
  export { AgentSandboxBlueprintAbi, ITangleJobsAbi, JOB_SANDBOX_CREATE, JOB_SANDBOX_DELETE, JsonResponseParamTypes, SandboxCreateParamTypes, SandboxCreateResponseParamTypes, SandboxIdParamTypes, TANGLE_CHAIN_ID, TANGLE_JOBS_CONTRACT, TANGLE_MAINNET_RPC, TangleSandboxClient };
@@ -1,5 +1,5 @@
1
- import { p as parseErrorResponse } from "./errors-DSz87Rkk.js";
2
- import { t as SandboxInstance } from "./sandbox-Vh7Aog8b.js";
1
+ import { p as parseErrorResponse } from "./errors-Cbs78OlF.js";
2
+ import { t as SandboxInstance } from "./sandbox-r2Lxbw8z.js";
3
3
  //#region src/tangle/abi.ts
4
4
  /**
5
5
  * Tangle Contract ABI Definitions