@tangle-network/sandbox 0.9.6 → 0.10.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,6 +1,7 @@
1
- import { c as ServerError, d as ValidationError, f as parseErrorResponse, l as StateError, o as QuotaError, r as NetworkError, s as SandboxError, t as AuthError, u as TimeoutError } from "./errors-DZsfJUuc.js";
1
+ 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";
2
2
  import { addTokenUsage, readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
3
3
  //#region src/backend-config.ts
4
+ const LEGACY_QUESTION_ALIAS_KEY = ["question", "Channel"].join("");
4
5
  function parseModelString(model) {
5
6
  const parts = model.split("/");
6
7
  if (parts.length >= 2) return {
@@ -17,6 +18,7 @@ function parseModelString(model) {
17
18
  */
18
19
  function normalizeRuntimeBackendConfig(backend, options = {}) {
19
20
  if (!backend && !options.model) return void 0;
21
+ if (backend && LEGACY_QUESTION_ALIAS_KEY in backend) throw new Error(`[sandbox-sdk] backend.${LEGACY_QUESTION_ALIAS_KEY} was removed. Use backend.interactions.question instead.`);
20
22
  const portableProfile = backend?.profile && typeof backend.profile !== "string" ? backend.profile : void 0;
21
23
  const inlineProfile = portableProfile ? toBackendProfile(portableProfile) : void 0;
22
24
  const callerInlineProfile = backend?.inlineProfile;
@@ -30,7 +32,9 @@ function normalizeRuntimeBackendConfig(backend, options = {}) {
30
32
  ...typeof backend?.profile === "string" ? { profile: backend.profile } : {},
31
33
  ...inlineProfile ? { inlineProfile } : callerInlineProfile ? { inlineProfile: callerInlineProfile } : {},
32
34
  ...backend?.model ? { model: backend.model } : options.model ? { model: parseModelString(options.model) } : {},
33
- ...backend?.server ? { server: backend.server } : {}
35
+ ...backend?.server ? { server: backend.server } : {},
36
+ ...backend?.interactions !== void 0 ? { interactions: backend.interactions } : {},
37
+ ...backend?.metadata !== void 0 ? { metadata: backend.metadata } : {}
34
38
  };
35
39
  }
36
40
  /**
@@ -97,19 +101,42 @@ function toBackendProfile(profile) {
97
101
  ...timeout !== void 0 ? { timeout } : {}
98
102
  }];
99
103
  })) : void 0;
104
+ const openCodeExtension = profile.extensions?.opencode;
105
+ const plugins = openCodeExtension?.plugins;
106
+ if (plugins !== void 0 && (!Array.isArray(plugins) || plugins.some((plugin) => typeof plugin !== "string" || !plugin.trim()))) throw new Error("backend.profile.extensions.opencode.plugins must be an array of non-empty package names");
107
+ const commands = openCodeExtension?.commands;
108
+ if (commands !== void 0 && (commands === null || typeof commands !== "object" || Array.isArray(commands))) throw new Error("backend.profile.extensions.opencode.commands must be an object");
109
+ const profileExtends = openCodeExtension?.extends;
110
+ if (profileExtends !== void 0 && typeof profileExtends !== "string" && (!Array.isArray(profileExtends) || profileExtends.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error("backend.profile.extensions.opencode.extends must be a profile name or array of profile names");
111
+ const agents = profile.subagents ? Object.fromEntries(Object.entries(profile.subagents).map(([name, subagent]) => [name, {
112
+ description: subagent.description,
113
+ prompt: subagent.prompt,
114
+ model: subagent.model,
115
+ tools: subagent.tools,
116
+ permission: subagent.permissions,
117
+ maxSteps: subagent.maxSteps,
118
+ ...subagent.metadata ?? {}
119
+ }])) : void 0;
100
120
  return {
101
121
  name: profile.name,
122
+ description: profile.description,
123
+ version: profile.version,
124
+ tags: profile.tags,
125
+ ...profileExtends ? { extends: profileExtends } : {},
102
126
  model: profile.model?.default,
127
+ small_model: profile.model?.small,
103
128
  ...profile.prompt?.systemPrompt ? { systemPrompt: profile.prompt.systemPrompt } : {},
104
129
  ...Object.keys(tools).length > 0 ? { tools } : {},
105
130
  ...profile.prompt?.instructions?.length ? { instructions: profile.prompt.instructions } : {},
106
131
  ...Object.keys(permission).length > 0 ? { permission } : {},
107
132
  ...mcp ? { mcp } : {},
108
133
  ...profile.connections?.length ? { connections: profile.connections } : {},
109
- ...profile.subagents ? { subagents: profile.subagents } : {},
134
+ ...agents ? { agent: agents } : {},
110
135
  ...profile.resources && hasAnyProfileResource(profile.resources) ? { resources: profile.resources } : {},
111
136
  ...profile.hooks ? { hooks: profile.hooks } : {},
112
137
  ...profile.modes ? { modes: profile.modes } : {},
138
+ ...commands ? { command: commands } : {},
139
+ ...plugins?.length ? { plugin: plugins } : {},
113
140
  ...profile.extensions ? { extensions: profile.extensions } : {}
114
141
  };
115
142
  }
@@ -572,6 +599,244 @@ function collectAgentResponseText(events) {
572
599
  return responseText && responseText.trim().length > 0 ? responseText : void 0;
573
600
  }
574
601
  //#endregion
602
+ //#region src/runtime-api.ts
603
+ /** Internal typed client for routes served by a sandbox runtime. */
604
+ var SandboxRuntimeApi = class {
605
+ terminals;
606
+ constructor(transport) {
607
+ this.transport = transport;
608
+ this.terminals = {
609
+ create: (options, request) => this.createTerminal(options, request),
610
+ input: (terminalId, data, request) => this.writeTerminal(terminalId, data, request),
611
+ list: () => this.listTerminals(),
612
+ get: (terminalId) => this.getTerminal(terminalId)
613
+ };
614
+ }
615
+ async health() {
616
+ await this.transport.ensureRunning();
617
+ return this.json("/health", { method: "GET" });
618
+ }
619
+ async ports() {
620
+ await this.transport.ensureRunning();
621
+ return (await this.json("/ports", { method: "GET" })).data?.ports ?? [];
622
+ }
623
+ async profiles() {
624
+ await this.transport.ensureRunning();
625
+ return this.json("/profiles", { method: "GET" });
626
+ }
627
+ async readFiles(paths, options = {}) {
628
+ if (paths.length === 0) return {
629
+ files: [],
630
+ errors: [],
631
+ stats: {
632
+ requested: 0,
633
+ succeeded: 0,
634
+ failed: 0
635
+ }
636
+ };
637
+ await this.transport.ensureRunning();
638
+ const files = [];
639
+ const errors = [];
640
+ let requested = 0;
641
+ let succeeded = 0;
642
+ let failed = 0;
643
+ for (let offset = 0; offset < paths.length; offset += 100) {
644
+ const chunk = paths.slice(offset, offset + 100);
645
+ const params = new URLSearchParams();
646
+ if (options.sessionId) params.set("sessionId", options.sessionId);
647
+ const query = params.toString();
648
+ const payload = await this.json(`/files/read-batch${query ? `?${query}` : ""}`, {
649
+ method: "POST",
650
+ body: JSON.stringify({
651
+ paths: chunk,
652
+ ...options.encoding ? { encoding: options.encoding } : {}
653
+ })
654
+ });
655
+ for (const [path, result] of Object.entries(payload.data.files)) if (result.success) files.push({
656
+ path,
657
+ content: result.content,
658
+ encoding: result.encoding,
659
+ hash: result.hash,
660
+ size: result.size,
661
+ mtime: result.mtime
662
+ });
663
+ else errors.push({
664
+ path,
665
+ error: result.error,
666
+ code: result.code
667
+ });
668
+ const results = Object.values(payload.data.files);
669
+ requested += payload.data.stats?.requested ?? chunk.length;
670
+ succeeded += payload.data.stats?.succeeded ?? results.filter((entry) => entry.success).length;
671
+ failed += payload.data.stats?.failed ?? results.filter((entry) => !entry.success).length;
672
+ }
673
+ return {
674
+ files,
675
+ errors,
676
+ stats: {
677
+ requested,
678
+ succeeded,
679
+ failed
680
+ }
681
+ };
682
+ }
683
+ async fileTree(path, options = {}) {
684
+ if (options.maxDepth !== void 0 && (!Number.isInteger(options.maxDepth) || options.maxDepth < 1 || options.maxDepth > 20)) throw new ValidationError("maxDepth must be an integer between 1 and 20");
685
+ await this.transport.ensureRunning();
686
+ const params = new URLSearchParams({ path });
687
+ if (options.maxDepth !== void 0) params.set("maxDepth", String(options.maxDepth));
688
+ if (options.sessionId) params.set("sessionId", options.sessionId);
689
+ return (await this.json(`/files/tree?${params}`, { method: "GET" })).data;
690
+ }
691
+ async createSession(options) {
692
+ await this.transport.ensureRunning();
693
+ const info = normalizeSessionInfo(await this.json("/agents/sessions", {
694
+ method: "POST",
695
+ body: JSON.stringify({
696
+ ...options.title !== void 0 ? { title: options.title } : {},
697
+ ...options.parentId !== void 0 ? { parentID: options.parentId } : {},
698
+ backend: normalizeRuntimeBackendConfig(options.backend)
699
+ })
700
+ }));
701
+ if (!info) throw new ServerError("Session creation response missing session id", 502, {
702
+ endpoint: "/agents/sessions",
703
+ origin: "runtime"
704
+ });
705
+ return info;
706
+ }
707
+ async createTaskSession(options) {
708
+ await this.transport.ensureRunning();
709
+ const info = await this.json("/tasks", {
710
+ method: "POST",
711
+ body: JSON.stringify({
712
+ sessionId: options.sessionId,
713
+ backend: normalizeRuntimeBackendConfig(options.backend),
714
+ title: options.title,
715
+ ...options.parentSessionId !== void 0 ? { parentSessionId: options.parentSessionId } : {},
716
+ ...options.profile !== void 0 ? { profile: options.profile } : {},
717
+ ...options.isolateFileWrites !== void 0 ? { isolateFileWrites: options.isolateFileWrites } : {}
718
+ })
719
+ });
720
+ if (!info.id) throw new ServerError("Task session creation response missing session id", 502, {
721
+ endpoint: "/tasks",
722
+ origin: "runtime"
723
+ });
724
+ return info;
725
+ }
726
+ async sendSessionMessage(id, request, options = {}) {
727
+ if (options.timeoutMs !== void 0 && options.timeoutMs <= 0) throw new ValidationError("timeoutMs must be greater than zero");
728
+ await this.transport.ensureRunning();
729
+ const headers = new Headers({ "x-no-retry": "true" });
730
+ if (options.credentials?.apiKey) headers.set("X-Backend-Api-Key", options.credentials.apiKey);
731
+ if (options.credentials?.baseUrl) headers.set("X-Backend-Base-Url", options.credentials.baseUrl);
732
+ const timeoutSignal = options.timeoutMs !== void 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
733
+ const signal = options.signal && timeoutSignal ? AbortSignal.any([options.signal, timeoutSignal]) : options.signal ?? timeoutSignal;
734
+ return this.json(`/agents/sessions/${encodeURIComponent(id)}/messages`, {
735
+ method: "POST",
736
+ headers,
737
+ signal,
738
+ body: JSON.stringify({
739
+ ...request.messageId !== void 0 ? { messageID: request.messageId } : {},
740
+ parts: request.parts,
741
+ ...request.system !== void 0 ? { system: request.system } : {},
742
+ ...request.agent !== void 0 ? { agent: request.agent } : {},
743
+ ...request.model !== void 0 ? { model: {
744
+ providerID: request.model.providerId,
745
+ modelID: request.model.modelId
746
+ } } : {},
747
+ ...request.reasoningEffort !== void 0 ? { reasoningEffort: request.reasoningEffort } : {},
748
+ ...request.turnId !== void 0 ? { turnId: request.turnId } : {},
749
+ ...request.interactions !== void 0 ? { interactions: request.interactions } : {}
750
+ })
751
+ });
752
+ }
753
+ async deleteSession(id) {
754
+ await this.transport.ensureRunning();
755
+ await this.request(`/agents/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
756
+ }
757
+ async taskSessionChanges(id) {
758
+ await this.transport.ensureRunning();
759
+ return this.json(`/tasks/${encodeURIComponent(id)}/changes`, { method: "GET" });
760
+ }
761
+ async commitTaskSession(id, options = {}) {
762
+ await this.transport.ensureRunning();
763
+ return this.json(`/tasks/${encodeURIComponent(id)}/changes`, {
764
+ method: "POST",
765
+ body: JSON.stringify({
766
+ ...options.message !== void 0 ? { message: options.message } : {},
767
+ ...options.files !== void 0 ? { files: options.files } : {}
768
+ })
769
+ });
770
+ }
771
+ async createTerminal(options = {}, request = {}) {
772
+ await this.transport.ensureRunning();
773
+ const params = new URLSearchParams();
774
+ if (request.sessionId) params.set("sessionId", request.sessionId);
775
+ const query = params.toString();
776
+ return (await this.json(`/terminals${query ? `?${query}` : ""}`, {
777
+ method: "POST",
778
+ body: JSON.stringify(options)
779
+ })).data;
780
+ }
781
+ async writeTerminal(terminalId, data, request = {}) {
782
+ await this.transport.ensureRunning();
783
+ const params = new URLSearchParams();
784
+ if (request.sessionId) params.set("sessionId", request.sessionId);
785
+ const query = params.toString();
786
+ await this.request(`/terminals/${encodeURIComponent(terminalId)}/input${query ? `?${query}` : ""}`, {
787
+ method: "POST",
788
+ body: JSON.stringify({ data })
789
+ });
790
+ }
791
+ async listTerminals() {
792
+ await this.transport.ensureRunning();
793
+ return (await this.json("/terminals", { method: "GET" })).data ?? [];
794
+ }
795
+ async getTerminal(terminalId) {
796
+ await this.transport.ensureRunning();
797
+ const response = await this.transport.fetch(`/terminals/${encodeURIComponent(terminalId)}`, { method: "GET" });
798
+ if (response.status === 404) return null;
799
+ await this.assertOk(response);
800
+ return (await response.json()).data;
801
+ }
802
+ async json(path, init) {
803
+ return await (await this.request(path, init)).json();
804
+ }
805
+ async request(path, init) {
806
+ const response = await this.transport.fetch(path, init);
807
+ await this.assertOk(response);
808
+ return response;
809
+ }
810
+ async assertOk(response) {
811
+ if (response.ok) return;
812
+ const body = await response.text();
813
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
814
+ }
815
+ };
816
+ function normalizeSessionInfo(raw) {
817
+ const id = raw.id;
818
+ if (typeof id !== "string") return null;
819
+ const status = raw.status || "running";
820
+ return {
821
+ id,
822
+ parentId: typeof raw.parentID === "string" ? raw.parentID : typeof raw.parentId === "string" ? raw.parentId : void 0,
823
+ status: [
824
+ "queued",
825
+ "running",
826
+ "completed",
827
+ "failed",
828
+ "cancelled"
829
+ ].includes(status) ? status : "running",
830
+ backend: typeof raw.backendType === "string" ? raw.backendType : typeof raw.backend === "string" ? raw.backend : void 0,
831
+ model: typeof raw.model === "string" ? raw.model : void 0,
832
+ promptCount: typeof raw.promptCount === "number" ? raw.promptCount : void 0,
833
+ createdAt: raw.createdAt ? new Date(raw.createdAt) : void 0,
834
+ startedAt: raw.startedAt ? new Date(raw.startedAt) : void 0,
835
+ endedAt: raw.endedAt ? new Date(raw.endedAt) : void 0,
836
+ raw
837
+ };
838
+ }
839
+ //#endregion
575
840
  //#region src/interactive.ts
576
841
  /**
577
842
  * Handle for one session's interactive harness. Obtained via
@@ -629,7 +894,7 @@ var InteractiveSessionHandle = class {
629
894
  * A single agent session inside a sandbox. Created via
630
895
  * `box.session(id)` — does not hit the network until a method is called.
631
896
  */
632
- var SandboxSession = class {
897
+ var SandboxSession = class SandboxSession {
633
898
  /**
634
899
  * @internal SDK-internal constructor — apps should call `box.session(id)`.
635
900
  */
@@ -672,6 +937,16 @@ var SandboxSession = class {
672
937
  return this.box._sessionResult(this.id);
673
938
  }
674
939
  /**
940
+ * List persisted messages for this session, including in-flight assistant
941
+ * content when the runtime has flushed partial output.
942
+ */
943
+ async messages(opts = {}) {
944
+ return this.box.messages({
945
+ ...opts,
946
+ sessionId: this.id
947
+ });
948
+ }
949
+ /**
675
950
  * Continue this session with an additional prompt. Equivalent to
676
951
  * `box.prompt(message, { ...opts, sessionId: this.id })` but reads
677
952
  * naturally on a Session reference.
@@ -683,6 +958,13 @@ var SandboxSession = class {
683
958
  });
684
959
  }
685
960
  /**
961
+ * Send a structured message through the session message endpoint. Model and
962
+ * message identifiers are translated to the runtime's wire names by the SDK.
963
+ */
964
+ async sendMessage(request, options) {
965
+ return this.box._sessionSendMessage(this.id, request, options);
966
+ }
967
+ /**
686
968
  * Abort the session's in-flight execution; the session and its messages
687
969
  * are preserved (this does not delete the session). Void-returning alias
688
970
  * of `interrupt()` — use `interrupt()` when you need to know whether an
@@ -693,6 +975,22 @@ var SandboxSession = class {
693
975
  async cancel() {
694
976
  return this.box._sessionCancel(this.id);
695
977
  }
978
+ /** Delete this session and its persisted runtime state. */
979
+ async delete() {
980
+ return this.box._sessionDelete(this.id);
981
+ }
982
+ /**
983
+ * Fork this session into a new queued session. The fork shares the
984
+ * parent workspace and copies conversation history up to `messageId`
985
+ * when supplied.
986
+ */
987
+ async fork(opts) {
988
+ const info = await this.box._sessionFork(this.id, opts);
989
+ return {
990
+ session: new SandboxSession(this.box, info.id),
991
+ info
992
+ };
993
+ }
696
994
  /**
697
995
  * Drive this session's harness in INTERACTIVE mode: spawn its native TUI,
698
996
  * stream the live framebuffer, and inject prompts as keystrokes. Distinct
@@ -720,14 +1018,34 @@ var SandboxSession = class {
720
1018
  }
721
1019
  /**
722
1020
  * Answer a question the agent asked via a question tool invocation. `answers`
723
- * maps each question id to the selected option(s). OpenCode-only; other
724
- * backends reject loudly.
1021
+ * maps each question id to the selected option(s). Backend-agnostic: works
1022
+ * with any backend that raises kind:"question" interactions — OpenCode
1023
+ * natively, or CLI backends with `BackendConfig.interactions.question`
1024
+ * enabled. Backends with no question support reject loudly.
725
1025
  */
726
1026
  async answer(answers) {
727
1027
  return this.box._answerQuestion(this.id, answers);
728
1028
  }
729
1029
  };
730
1030
  //#endregion
1031
+ //#region src/task-session.ts
1032
+ /** A background task session with isolated changeset operations. */
1033
+ var SandboxTaskSession = class extends SandboxSession {
1034
+ /** @internal Apps should call `box.taskSession(id)`. */
1035
+ constructor(taskBox, id) {
1036
+ super(taskBox, id);
1037
+ this.taskBox = taskBox;
1038
+ }
1039
+ /** Read the task's isolated file changes. */
1040
+ async changes() {
1041
+ return this.taskBox._taskSessionChanges(this.id);
1042
+ }
1043
+ /** Apply all or a selected subset of the task's isolated file changes. */
1044
+ async commit(options) {
1045
+ return this.taskBox._taskSessionCommit(this.id, options);
1046
+ }
1047
+ };
1048
+ //#endregion
731
1049
  //#region src/sandbox.ts
732
1050
  /**
733
1051
  * Sandbox Instance
@@ -1000,10 +1318,15 @@ var SandboxInstance = class SandboxInstance {
1000
1318
  client;
1001
1319
  info;
1002
1320
  defaultRuntimeBackend;
1321
+ runtime;
1003
1322
  constructor(client, info, defaultRuntimeBackend) {
1004
1323
  this.client = client;
1005
1324
  this.info = info;
1006
1325
  this.defaultRuntimeBackend = defaultRuntimeBackend;
1326
+ this.runtime = new SandboxRuntimeApi({
1327
+ ensureRunning: () => this.ensureRunning(),
1328
+ fetch: (path, init) => this.runtimeFetch(path, init)
1329
+ });
1007
1330
  }
1008
1331
  /** Unique sandbox identifier */
1009
1332
  get id() {
@@ -1045,6 +1368,10 @@ var SandboxInstance = class SandboxInstance {
1045
1368
  get error() {
1046
1369
  return this.info.error;
1047
1370
  }
1371
+ /** GPU lease attached during sandbox creation, when requested. */
1372
+ get gpuLease() {
1373
+ return this.info.gpuLease;
1374
+ }
1048
1375
  /** Web terminal URL for browser-based access */
1049
1376
  get url() {
1050
1377
  return this.info.connection?.webTerminalUrl;
@@ -1085,7 +1412,8 @@ var SandboxInstance = class SandboxInstance {
1085
1412
  startedAt: this.startedAt,
1086
1413
  lastActivityAt: this.lastActivityAt,
1087
1414
  expiresAt: this.expiresAt,
1088
- error: this.error
1415
+ error: this.error,
1416
+ gpuLease: this.gpuLease
1089
1417
  };
1090
1418
  }
1091
1419
  /**
@@ -1415,11 +1743,15 @@ var SandboxInstance = class SandboxInstance {
1415
1743
  */
1416
1744
  async write(path, content, options) {
1417
1745
  await this.ensureRunning();
1418
- const response = await this.runtimeFetch("/files/write", {
1746
+ const params = new URLSearchParams();
1747
+ if (options?.sessionId) params.set("sessionId", options.sessionId);
1748
+ const query = params.toString();
1749
+ const response = await this.runtimeFetch(`/files/write${query ? `?${query}` : ""}`, {
1419
1750
  method: "POST",
1420
1751
  body: JSON.stringify({
1421
1752
  path,
1422
1753
  content,
1754
+ encoding: options?.encoding,
1423
1755
  mode: options?.mode
1424
1756
  })
1425
1757
  });
@@ -1427,6 +1759,7 @@ var SandboxInstance = class SandboxInstance {
1427
1759
  const body = await response.text();
1428
1760
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1429
1761
  }
1762
+ return (await response.json()).data;
1430
1763
  }
1431
1764
  /**
1432
1765
  * Write many files in one paced, retry-aware batch — see
@@ -1527,6 +1860,13 @@ var SandboxInstance = class SandboxInstance {
1527
1860
  controller.abort();
1528
1861
  }
1529
1862
  }
1863
+ logStreamCheckpoint(checkpoint, data = {}) {
1864
+ if (globalThis.process?.env?.TANGLE_SANDBOX_SDK_STREAM_DEBUG !== "1") return;
1865
+ console.debug("[sandbox-sdk] stream checkpoint", {
1866
+ checkpoint,
1867
+ ...data
1868
+ });
1869
+ }
1530
1870
  /**
1531
1871
  * Stream events from an agent prompt.
1532
1872
  * Use this for real-time updates during agent execution.
@@ -1558,16 +1898,27 @@ var SandboxInstance = class SandboxInstance {
1558
1898
  }
1559
1899
  } catch (err) {
1560
1900
  if (isCallerAbort()) return;
1901
+ this.logStreamCheckpoint("synthetic_error", {
1902
+ sessionId,
1903
+ executionId,
1904
+ error: err instanceof Error ? err.message : String(err)
1905
+ });
1561
1906
  yield toStreamErrorEvent(err);
1562
1907
  } finally {
1563
- if (!receivedTerminal && !isCallerAbort()) yield {
1564
- type: "done",
1565
- data: {
1566
- status: "failed",
1567
- ...sessionId ? { sessionId } : {},
1568
- ...executionId ? { executionId } : {}
1569
- }
1570
- };
1908
+ if (!receivedTerminal && !isCallerAbort()) {
1909
+ this.logStreamCheckpoint("synthetic_terminal", {
1910
+ sessionId,
1911
+ executionId
1912
+ });
1913
+ yield {
1914
+ type: "done",
1915
+ data: {
1916
+ status: "failed",
1917
+ ...sessionId ? { sessionId } : {},
1918
+ ...executionId ? { executionId } : {}
1919
+ }
1920
+ };
1921
+ }
1571
1922
  }
1572
1923
  }
1573
1924
  /**
@@ -1589,8 +1940,22 @@ var SandboxInstance = class SandboxInstance {
1589
1940
  origin: "runtime"
1590
1941
  });
1591
1942
  };
1943
+ const streamStartedAt = Date.now();
1944
+ const logStreamCheckpoint = (checkpoint, data = {}) => {
1945
+ this.logStreamCheckpoint(checkpoint, {
1946
+ elapsedMs: Date.now() - streamStartedAt,
1947
+ ...data
1948
+ });
1949
+ };
1592
1950
  let response;
1593
1951
  try {
1952
+ logStreamCheckpoint("runtime_post_start", {
1953
+ endpoint: "/agents/run/stream",
1954
+ sessionId: options?.sessionId,
1955
+ executionId: options?.executionId,
1956
+ hasLastEventId: Boolean(options?.lastEventId),
1957
+ detach: options?.detach === true
1958
+ });
1594
1959
  response = await this.runtimeFetch("/agents/run/stream", {
1595
1960
  method: "POST",
1596
1961
  signal: streamSignal,
@@ -1606,11 +1971,17 @@ var SandboxInstance = class SandboxInstance {
1606
1971
  turnId: options?.turnId,
1607
1972
  metadata: options?.context,
1608
1973
  requireVisibleAssistantOutput: options?.requireVisibleAssistantOutput,
1974
+ ...timeoutMs ? { timeoutMs } : {},
1609
1975
  ...options?.detach ? { detach: true } : {},
1610
1976
  backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend, { model: options?.model })
1611
1977
  })
1612
1978
  });
1979
+ logStreamCheckpoint("runtime_response_headers", {
1980
+ status: response.status,
1981
+ ok: response.ok
1982
+ });
1613
1983
  } catch (err) {
1984
+ logStreamCheckpoint("runtime_post_failed", { error: err instanceof Error ? err.message : String(err) });
1614
1985
  throwIfTimedOut();
1615
1986
  throw err;
1616
1987
  }
@@ -1633,17 +2004,39 @@ var SandboxInstance = class SandboxInstance {
1633
2004
  const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1634
2005
  let lastReplayStatus;
1635
2006
  let lastExecutionStatus;
2007
+ let firstSseEventSeen = false;
1636
2008
  for await (const event of this.parseSSEStream(response, streamSignal)) {
2009
+ if (!firstSseEventSeen) {
2010
+ firstSseEventSeen = true;
2011
+ logStreamCheckpoint("first_sse_event", {
2012
+ eventType: event.type,
2013
+ eventId: event.id
2014
+ });
2015
+ }
1637
2016
  if (event.id) lastEventId = event.id;
1638
2017
  if (event.type === "execution.started" && event.data?.executionId) executionId = event.data.executionId;
1639
2018
  const eventSessionId = event.data?.sessionId;
1640
2019
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
1641
- if (event.type === "result" || event.type === "done") receivedTerminal = true;
2020
+ if (event.type === "result" || event.type === "done") {
2021
+ receivedTerminal = true;
2022
+ logStreamCheckpoint("terminal_event", {
2023
+ eventType: event.type,
2024
+ eventId: event.id,
2025
+ sessionId,
2026
+ executionId
2027
+ });
2028
+ }
1642
2029
  yield event;
1643
2030
  }
1644
2031
  if (receivedTerminal) return;
1645
2032
  throwIfTimedOut();
1646
2033
  if (options?.signal?.aborted || streamSignal?.aborted) return;
2034
+ logStreamCheckpoint("stream_drop", {
2035
+ executionId,
2036
+ sessionId,
2037
+ lastEventId,
2038
+ receivedTerminal
2039
+ });
1647
2040
  if (!executionId) throw new NetworkError("Prompt stream dropped before execution.started; cannot reconnect", {
1648
2041
  endpoint: "/agents/run/stream",
1649
2042
  origin: "runtime"
@@ -1652,6 +2045,12 @@ var SandboxInstance = class SandboxInstance {
1652
2045
  throwIfTimedOut();
1653
2046
  if (options?.signal?.aborted || streamSignal?.aborted) return;
1654
2047
  console.warn(`[sandbox-sdk] Stream dropped without terminal event, reconnecting (attempt ${attempt}/${MAX_RECONNECT_ATTEMPTS})`);
2048
+ logStreamCheckpoint("reconnect_attempt_start", {
2049
+ attempt,
2050
+ executionId,
2051
+ sessionId,
2052
+ lastEventId
2053
+ });
1655
2054
  await new Promise((resolve) => setTimeout(resolve, RECONNECT_BACKOFF_MS[attempt - 1]));
1656
2055
  throwIfTimedOut();
1657
2056
  if (options?.signal?.aborted || streamSignal?.aborted) return;
@@ -1661,6 +2060,11 @@ var SandboxInstance = class SandboxInstance {
1661
2060
  method: "GET",
1662
2061
  signal: streamSignal
1663
2062
  });
2063
+ logStreamCheckpoint("replay_response_status", {
2064
+ attempt,
2065
+ status: replayResponse.status,
2066
+ ok: replayResponse.ok
2067
+ });
1664
2068
  if (!replayResponse.ok) {
1665
2069
  lastReplayStatus = replayResponse.status;
1666
2070
  console.warn(`[sandbox-sdk] Replay endpoint returned ${replayResponse.status}, attempt ${attempt}`);
@@ -1670,6 +2074,13 @@ var SandboxInstance = class SandboxInstance {
1670
2074
  lastExecutionStatus = info.status;
1671
2075
  if (info.status === "completed" || info.status === "failed" || info.status === "cancelled") {
1672
2076
  console.warn(`[sandbox-sdk] Replay 404 but session ${sessionId} is terminal (${info.status}); ending stream gracefully`);
2077
+ logStreamCheckpoint("terminal_event", {
2078
+ eventType: "done",
2079
+ synthetic: true,
2080
+ reason: "replay_unavailable_terminal_session",
2081
+ sessionId,
2082
+ executionId
2083
+ });
1673
2084
  yield {
1674
2085
  type: "done",
1675
2086
  data: {
@@ -1688,18 +2099,46 @@ var SandboxInstance = class SandboxInstance {
1688
2099
  continue;
1689
2100
  }
1690
2101
  for await (const event of this.parseSSEStream(replayResponse, streamSignal)) {
2102
+ if (!firstSseEventSeen) {
2103
+ firstSseEventSeen = true;
2104
+ logStreamCheckpoint("first_sse_event", {
2105
+ eventType: event.type,
2106
+ eventId: event.id,
2107
+ replay: true
2108
+ });
2109
+ }
1691
2110
  if (event.type === "history.replay.start" || event.type === "history.replay.end") continue;
1692
2111
  if (event.id) lastEventId = event.id;
1693
2112
  const eventSessionId = event.data?.sessionId;
1694
2113
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
1695
- if (event.type === "result" || event.type === "done") receivedTerminal = true;
2114
+ if (event.type === "result" || event.type === "done") {
2115
+ receivedTerminal = true;
2116
+ logStreamCheckpoint("terminal_event", {
2117
+ eventType: event.type,
2118
+ eventId: event.id,
2119
+ replay: true,
2120
+ sessionId,
2121
+ executionId
2122
+ });
2123
+ }
1696
2124
  yield event;
1697
2125
  }
1698
- if (receivedTerminal) return;
2126
+ if (receivedTerminal) {
2127
+ logStreamCheckpoint("reconnect_attempt_end", {
2128
+ attempt,
2129
+ status: "terminal_received"
2130
+ });
2131
+ return;
2132
+ }
1699
2133
  throwIfTimedOut();
1700
2134
  if (options?.signal?.aborted || streamSignal?.aborted) return;
1701
2135
  } catch (err) {
1702
2136
  throwIfTimedOut();
2137
+ logStreamCheckpoint("reconnect_attempt_end", {
2138
+ attempt,
2139
+ status: "failed",
2140
+ error: err instanceof Error ? err.message : String(err)
2141
+ });
1703
2142
  console.warn(`[sandbox-sdk] Reconnection attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
1704
2143
  }
1705
2144
  }
@@ -2011,6 +2450,22 @@ var SandboxInstance = class SandboxInstance {
2011
2450
  after: match.after
2012
2451
  };
2013
2452
  }
2453
+ /** Basic health reported by the runtime inside this sandbox. */
2454
+ async runtimeHealth() {
2455
+ return this.runtime.health();
2456
+ }
2457
+ /** Return the runtime's current listening-port snapshot. */
2458
+ async ports() {
2459
+ return this.runtime.ports();
2460
+ }
2461
+ /** Interactive terminal operations for this runtime. */
2462
+ get terminals() {
2463
+ return this.runtime.terminals;
2464
+ }
2465
+ /** List profiles installed in this sandbox runtime. */
2466
+ async listProfiles() {
2467
+ return this.runtime.profiles();
2468
+ }
2014
2469
  /**
2015
2470
  * Live whole-sandbox resource usage (memory + CPU) from the container cgroup.
2016
2471
  *
@@ -2245,6 +2700,8 @@ var SandboxInstance = class SandboxInstance {
2245
2700
  return {
2246
2701
  supportsWriteMode: true,
2247
2702
  read: (path) => this.read(path),
2703
+ readBatch: (paths, options) => this.fsReadBatch(paths, options),
2704
+ tree: (path, options) => this.fsTree(path, options),
2248
2705
  write: (path, content, options) => this.write(path, content, options),
2249
2706
  writeMany: (files, options) => this.writeMany(files, options),
2250
2707
  search: (query, options) => this.search(query, options),
@@ -2259,6 +2716,12 @@ var SandboxInstance = class SandboxInstance {
2259
2716
  exists: (path) => this.fsExists(path)
2260
2717
  };
2261
2718
  }
2719
+ async fsReadBatch(paths, options = {}) {
2720
+ return this.runtime.readFiles(paths, options);
2721
+ }
2722
+ async fsTree(path, options = {}) {
2723
+ return this.runtime.fileTree(path, options);
2724
+ }
2262
2725
  async fsUpload(localPath, remotePath, options) {
2263
2726
  await this.ensureRunning();
2264
2727
  const fs = await import("node:fs/promises");
@@ -2365,6 +2828,7 @@ var SandboxInstance = class SandboxInstance {
2365
2828
  const params = new URLSearchParams();
2366
2829
  if (options?.all) params.set("all", "true");
2367
2830
  if (options?.long) params.set("long", "true");
2831
+ if (options?.sessionId) params.set("sessionId", options.sessionId);
2368
2832
  const query = params.toString();
2369
2833
  const url = query ? `/fs/list${path}?${query}` : `/fs/list${path}`;
2370
2834
  const response = await this.runtimeFetch(url, { method: "GET" });
@@ -2410,8 +2874,21 @@ var SandboxInstance = class SandboxInstance {
2410
2874
  }
2411
2875
  async fsDelete(path, options) {
2412
2876
  await this.ensureRunning();
2877
+ if (options?.sessionId && !options.recursive) {
2878
+ const params = new URLSearchParams({ sessionId: options.sessionId });
2879
+ const response = await this.runtimeFetch(`/files/delete?${params}`, {
2880
+ method: "POST",
2881
+ body: JSON.stringify({ path })
2882
+ });
2883
+ if (!response.ok) {
2884
+ const body = await response.text();
2885
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2886
+ }
2887
+ return;
2888
+ }
2413
2889
  const params = new URLSearchParams();
2414
2890
  if (options?.recursive) params.set("recursive", "true");
2891
+ if (options?.sessionId) params.set("sessionId", options.sessionId);
2415
2892
  const query = params.toString();
2416
2893
  const url = query ? `/fs${path}?${query}` : `/fs${path}`;
2417
2894
  const response = await this.runtimeFetch(url, { method: "DELETE" });
@@ -3042,6 +3519,20 @@ var SandboxInstance = class SandboxInstance {
3042
3519
  update: (policy) => this.egressUpdate(policy)
3043
3520
  };
3044
3521
  }
3522
+ /**
3523
+ * On-demand GPU leases attached to this sandbox.
3524
+ *
3525
+ * The base sandbox stays cheap; `attach` starts a temporary external GPU
3526
+ * worker with an explicit spend cap, and `detach` destroys it.
3527
+ */
3528
+ get gpu() {
3529
+ return {
3530
+ attach: (options) => this.gpuAttach(options),
3531
+ list: () => this.gpuList(),
3532
+ exec: (leaseId, options) => this.gpuExec(leaseId, options),
3533
+ detach: (leaseId) => this.gpuDetach(leaseId)
3534
+ };
3535
+ }
3045
3536
  async networkUpdate(config) {
3046
3537
  if (config.blockOutbound !== void 0 && config.allowList !== void 0) {
3047
3538
  if (config.blockOutbound && config.allowList.length > 0) throw new Error("blockOutbound and allowList are mutually exclusive");
@@ -3119,6 +3610,44 @@ var SandboxInstance = class SandboxInstance {
3119
3610
  source: data.source
3120
3611
  };
3121
3612
  }
3613
+ async gpuAttach(options) {
3614
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/gpu/leases`, {
3615
+ method: "POST",
3616
+ body: JSON.stringify(options)
3617
+ });
3618
+ if (!response.ok) {
3619
+ const body = await response.text();
3620
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3621
+ }
3622
+ return (await response.json()).lease;
3623
+ }
3624
+ async gpuList() {
3625
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/gpu/leases`);
3626
+ if (!response.ok) {
3627
+ const body = await response.text();
3628
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3629
+ }
3630
+ return (await response.json()).leases ?? [];
3631
+ }
3632
+ async gpuExec(leaseId, options) {
3633
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/gpu/leases/${encodeURIComponent(leaseId)}/exec`, {
3634
+ method: "POST",
3635
+ body: JSON.stringify(options)
3636
+ });
3637
+ if (!response.ok) {
3638
+ const body = await response.text();
3639
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3640
+ }
3641
+ return await response.json();
3642
+ }
3643
+ async gpuDetach(leaseId) {
3644
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/gpu/leases/${encodeURIComponent(leaseId)}`, { method: "DELETE" });
3645
+ if (!response.ok) {
3646
+ const body = await response.text();
3647
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3648
+ }
3649
+ return (await response.json()).lease;
3650
+ }
3122
3651
  /**
3123
3652
  * Validate CIDR notation (IPv4 and IPv6)
3124
3653
  */
@@ -3519,132 +4048,60 @@ var SandboxInstance = class SandboxInstance {
3519
4048
  };
3520
4049
  }
3521
4050
  /**
3522
- * Create a CRIU checkpoint of the sandbox's memory state.
3523
- *
3524
- * Checkpoints capture the complete memory state of the running sandbox,
3525
- * enabling true pause/resume and fork operations. Unlike snapshots which
3526
- * only preserve filesystem state, checkpoints preserve process memory,
3527
- * open file descriptors, and execution state.
3528
- *
3529
- * **Requirements:** CRIU must be available on the host. Check availability
3530
- * with `client.criuStatus()` before calling.
3531
- *
3532
- * **Note:** By default, checkpoint stops the sandbox. Use `leaveRunning: true`
3533
- * to keep it running (creates a copy-on-write checkpoint).
3534
- *
3535
- * @param options - Checkpoint options
3536
- * @returns Checkpoint result with ID and metadata
3537
- *
3538
- * @example Basic checkpoint (stops sandbox)
3539
- * ```typescript
3540
- * const checkpoint = await box.checkpoint();
3541
- * console.log(`Checkpoint: ${checkpoint.checkpointId}`);
3542
- * // Sandbox is now stopped, resume with box.resume()
3543
- * ```
3544
- *
3545
- * @example Checkpoint without stopping
3546
- * ```typescript
3547
- * const checkpoint = await box.checkpoint({
3548
- * tags: ["before-deploy"],
3549
- * leaveRunning: true,
3550
- * });
3551
- * // Sandbox continues running
3552
- * ```
4051
+ * @deprecated CRIU checkpoints were removed. To branch live memory into
4052
+ * copy-on-write children of a running sandbox use {@link branch}; for durable
4053
+ * filesystem state use {@link snapshot}. This method now throws.
3553
4054
  */
3554
- async checkpoint(options) {
3555
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints`, {
3556
- method: "POST",
3557
- body: JSON.stringify({
3558
- tags: options?.tags,
3559
- leaveRunning: options?.leaveRunning,
3560
- includeSnapshot: options?.includeSnapshot
3561
- })
3562
- });
3563
- if (!response.ok) {
3564
- const body = await response.text();
3565
- throw parseErrorResponse(response.status, body, void 0, response.headers);
3566
- }
3567
- const data = await response.json();
3568
- return {
3569
- checkpointId: data.checkpointId ?? data.id,
3570
- createdAt: new Date(data.createdAt ?? ""),
3571
- sizeBytes: data.sizeBytes,
3572
- tags: data.tags ?? []
3573
- };
4055
+ checkpoint() {
4056
+ throw new CapabilityError("checkpoint() was removed: use branch() to fork live memory into copy-on-write children, or snapshot() for durable disk state", "CHECKPOINT_REMOVED", 410);
3574
4057
  }
3575
4058
  /**
3576
- * List all checkpoints for this sandbox.
3577
- *
3578
- * @returns Array of checkpoint metadata
3579
- *
3580
- * @example
3581
- * ```typescript
3582
- * const checkpoints = await box.listCheckpoints();
3583
- * for (const cp of checkpoints) {
3584
- * console.log(`${cp.checkpointId}: ${cp.createdAt}`);
3585
- * }
3586
- * ```
4059
+ * @deprecated CRIU checkpoints were removed. List durable filesystem state
4060
+ * with {@link listSnapshots}. This method now throws.
3587
4061
  */
3588
- async listCheckpoints() {
3589
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints`);
3590
- if (!response.ok) {
3591
- const body = await response.text();
3592
- throw parseErrorResponse(response.status, body, void 0, response.headers);
3593
- }
3594
- const data = await response.json();
3595
- return (Array.isArray(data) ? data : data.checkpoints ?? []).map((cp) => this.parseCheckpointInfo(cp));
4062
+ listCheckpoints() {
4063
+ throw new CapabilityError("listCheckpoints() was removed: checkpoints no longer exist; use listSnapshots() for durable disk state", "CHECKPOINT_REMOVED", 410);
3596
4064
  }
3597
4065
  /**
3598
- * Delete a checkpoint.
3599
- *
3600
- * @param checkpointId - ID of the checkpoint to delete
4066
+ * @deprecated CRIU checkpoints were removed. This method now throws.
3601
4067
  */
3602
- async deleteCheckpoint(checkpointId) {
3603
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints/${encodeURIComponent(checkpointId)}`, { method: "DELETE" });
3604
- if (!response.ok) {
3605
- const body = await response.text();
3606
- throw parseErrorResponse(response.status, body, void 0, response.headers);
3607
- }
4068
+ deleteCheckpoint() {
4069
+ throw new CapabilityError("deleteCheckpoint() was removed: checkpoints no longer exist", "CHECKPOINT_REMOVED", 410);
3608
4070
  }
3609
4071
  /**
3610
- * Fork a new sandbox from a checkpoint.
3611
- *
3612
- * Creates a new sandbox with the same memory state as this sandbox
3613
- * at the time of the checkpoint. The fork has a new identity but
3614
- * preserves the execution state.
3615
- *
3616
- * **Use cases:**
3617
- * - Branch workflows: Create parallel execution paths
3618
- * - A/B testing: Run same state with different configurations
3619
- * - Debugging: Fork at a specific point to investigate
4072
+ * @deprecated Forking from a CRIU checkpoint was removed. Use {@link branch}
4073
+ * to fork a running sandbox's live memory into copy-on-write children, or
4074
+ * `client.create({ fromSnapshot })` to provision from durable disk state.
4075
+ * This method now throws.
4076
+ */
4077
+ fork() {
4078
+ throw new CapabilityError("fork() was removed: use branch() to fork live memory into copy-on-write children, or create({ fromSnapshot }) for durable disk state", "FORK_REMOVED", 410);
4079
+ }
4080
+ /**
4081
+ * Branch this RUNNING sandbox into `count` copy-on-write children
4082
+ * (Morph Infinibranch parity). Unlike {@link fork} — which rehydrates one
4083
+ * sandbox from a CRIU checkpoint — branch forks the live VM's memory into
4084
+ * many children at once via Firecracker UFFD copy-on-write: the children
4085
+ * share the parent's clean pages instead of each copying full guest
4086
+ * memory. The parent stays running.
3620
4087
  *
3621
- * @param checkpointId - ID of the checkpoint to fork from
3622
- * @param options - Fork configuration
3623
- * @returns The new sandbox instance
4088
+ * @param count - Number of children to create (must be >= 1).
4089
+ * @param options - Per-child overrides.
4090
+ * @returns Exactly `count` new sandbox instances.
3624
4091
  *
3625
- * @example Basic fork
3626
- * ```typescript
3627
- * const checkpoint = await box.checkpoint({ leaveRunning: true });
3628
- * const forked = await box.fork(checkpoint.checkpointId);
3629
- * // forked has same memory state as box at checkpoint time
3630
- * ```
3631
- *
3632
- * @example Fork with custom config
4092
+ * @example
3633
4093
  * ```typescript
3634
- * const forked = await box.fork(checkpointId, {
3635
- * name: "experiment-branch",
3636
- * env: { EXPERIMENT: "true" },
3637
- * });
4094
+ * const [a, b, c] = await box.branch(3);
4095
+ * // a, b, c each share box's memory state at branch time
3638
4096
  * ```
3639
4097
  */
3640
- async fork(checkpointId, options) {
3641
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/fork`, {
4098
+ async branch(count, options) {
4099
+ if (!Number.isInteger(count) || count < 1) throw new ValidationError(`branch count must be an integer >= 1 (got ${count})`);
4100
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/branch`, {
3642
4101
  method: "POST",
3643
4102
  body: JSON.stringify({
3644
- checkpointId,
3645
- name: options?.name,
4103
+ count,
3646
4104
  env: options?.env,
3647
- resources: options?.resources,
3648
4105
  metadata: options?.metadata
3649
4106
  })
3650
4107
  });
@@ -3652,8 +4109,7 @@ var SandboxInstance = class SandboxInstance {
3652
4109
  const body = await response.text();
3653
4110
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3654
4111
  }
3655
- const data = await response.json();
3656
- return new SandboxInstance(this.client, this.parseInfo(data.sandbox ?? data), this.defaultRuntimeBackend);
4112
+ return ((await response.json()).children ?? []).map((child) => new SandboxInstance(this.client, this.parseInfo(child), this.defaultRuntimeBackend));
3657
4113
  }
3658
4114
  /**
3659
4115
  * Stop the sandbox (keeps state for resume).
@@ -3890,17 +4346,6 @@ var SandboxInstance = class SandboxInstance {
3890
4346
  abortController.abort();
3891
4347
  }
3892
4348
  }
3893
- parseCheckpointInfo(cp) {
3894
- return {
3895
- checkpointId: cp.checkpointId ?? cp.id,
3896
- sandboxId: cp.sandboxId ?? cp.projectRef ?? this.id,
3897
- createdAt: new Date(cp.createdAt),
3898
- tags: cp.tags ?? [],
3899
- sizeBytes: cp.sizeBytes,
3900
- hasMemoryState: cp.hasMemoryState ?? true,
3901
- hasFilesystemSnapshot: cp.hasFilesystemSnapshot ?? false
3902
- };
3903
- }
3904
4349
  async ensureRunning() {
3905
4350
  await this.refresh();
3906
4351
  if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
@@ -3952,15 +4397,51 @@ var SandboxInstance = class SandboxInstance {
3952
4397
  async registerSessionMapping(opts) {
3953
4398
  const response = await this.client.fetch(`/v1/session/${encodeURIComponent(opts.sessionId)}/mapping`, {
3954
4399
  method: "PUT",
4400
+ headers: { "x-user-id": opts.userId },
3955
4401
  body: JSON.stringify({
3956
- userId: opts.userId,
3957
- sandboxId: this.id
4402
+ sidecarSessionId: opts.runtimeSessionId,
4403
+ ...opts.sidecarId ? { sidecarId: opts.sidecarId } : {},
4404
+ ...opts.projectRef ? { projectRef: opts.projectRef } : {}
3958
4405
  })
3959
4406
  });
4407
+ if (response.status === 410) return {
4408
+ success: false,
4409
+ sessionId: opts.sessionId,
4410
+ reprovisionRequired: true,
4411
+ code: "STALE_SIDECAR"
4412
+ };
3960
4413
  if (!response.ok) {
3961
4414
  const body = await response.text();
3962
4415
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3963
4416
  }
4417
+ const data = await response.json().catch(() => ({}));
4418
+ return {
4419
+ success: data.success ?? true,
4420
+ sessionId: data.sessionId ?? opts.sessionId,
4421
+ sidecarSessionId: data.sidecarSessionId,
4422
+ reprovisionRequired: data.reprovisionRequired ?? false,
4423
+ code: data.code
4424
+ };
4425
+ }
4426
+ /**
4427
+ * Remove the sidecar session mapping for an agent session. Sandbox deletion
4428
+ * also cleans mappings, so this is only needed to release a mapping while the
4429
+ * sandbox stays alive.
4430
+ */
4431
+ async unregisterSessionMapping(opts) {
4432
+ const response = await this.client.fetch(`/v1/session/${encodeURIComponent(opts.sessionId)}/mapping`, {
4433
+ method: "DELETE",
4434
+ headers: { "x-user-id": opts.userId }
4435
+ });
4436
+ if (!response.ok) {
4437
+ const body = await response.text();
4438
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
4439
+ }
4440
+ const data = await response.json().catch(() => ({}));
4441
+ return {
4442
+ success: data.success ?? true,
4443
+ sessionId: data.sessionId ?? opts.sessionId
4444
+ };
3964
4445
  }
3965
4446
  parseInfo(data) {
3966
4447
  return {
@@ -3974,6 +4455,7 @@ var SandboxInstance = class SandboxInstance {
3974
4455
  lastActivityAt: data.lastActivityAt ? new Date(data.lastActivityAt) : void 0,
3975
4456
  expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0,
3976
4457
  error: data.error,
4458
+ gpuLease: data.gpuLease ?? this.info.gpuLease,
3977
4459
  startupDiagnostics: normalizeStartupDiagnostics(data.startupDiagnostics) ?? this.info?.startupDiagnostics
3978
4460
  };
3979
4461
  }
@@ -3988,6 +4470,30 @@ var SandboxInstance = class SandboxInstance {
3988
4470
  session(id) {
3989
4471
  return new SandboxSession(this, id);
3990
4472
  }
4473
+ /** Create an agent session and return both its handle and initial metadata. */
4474
+ async createSession(options) {
4475
+ const info = await this.runtime.createSession(options);
4476
+ return {
4477
+ session: this.session(info.id),
4478
+ info
4479
+ };
4480
+ }
4481
+ /** Delete an agent session by id. */
4482
+ async deleteSession(id) {
4483
+ return this._sessionDelete(id);
4484
+ }
4485
+ /** Get a lazy background-task session reference. */
4486
+ taskSession(id) {
4487
+ return new SandboxTaskSession(this, id);
4488
+ }
4489
+ /** Create a background task session. Dispatch work with `session.sendMessage()`. */
4490
+ async createTaskSession(options) {
4491
+ const info = await this.runtime.createTaskSession(options);
4492
+ return {
4493
+ session: this.taskSession(info.id),
4494
+ info
4495
+ };
4496
+ }
3991
4497
  /**
3992
4498
  * List sessions on this sandbox, optionally filtering by status. Returns
3993
4499
  * `SandboxSession` instances paired with their last-known
@@ -4095,6 +4601,44 @@ var SandboxInstance = class SandboxInstance {
4095
4601
  metadata: m.info.metadata
4096
4602
  }));
4097
4603
  }
4604
+ /** @internal — invoked by SandboxSession.sendMessage(). */
4605
+ async _sessionSendMessage(id, request, options = {}) {
4606
+ return this.runtime.sendSessionMessage(id, request, options);
4607
+ }
4608
+ /** @internal — invoked by SandboxSession.delete(). */
4609
+ async _sessionDelete(id) {
4610
+ return this.runtime.deleteSession(id);
4611
+ }
4612
+ /** @internal — invoked by SandboxTaskSession.changes(). */
4613
+ async _taskSessionChanges(id) {
4614
+ return this.runtime.taskSessionChanges(id);
4615
+ }
4616
+ /** @internal — invoked by SandboxTaskSession.commit(). */
4617
+ async _taskSessionCommit(id, options = {}) {
4618
+ return this.runtime.commitTaskSession(id, options);
4619
+ }
4620
+ async _sessionFork(id, opts = {}) {
4621
+ await this.ensureRunning();
4622
+ const body = {};
4623
+ if (opts.messageId !== void 0) body.messageID = opts.messageId;
4624
+ if (opts.targetBackend !== void 0) body.targetBackend = opts.targetBackend;
4625
+ if (opts.targetModel !== void 0) body.targetModel = opts.targetModel;
4626
+ if (opts.files !== void 0) body.files = opts.files;
4627
+ const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/forks`, {
4628
+ method: "POST",
4629
+ body: JSON.stringify(body)
4630
+ });
4631
+ if (!response.ok) {
4632
+ const responseBody = await response.text();
4633
+ throw parseErrorResponse(response.status, responseBody, void 0, response.headers);
4634
+ }
4635
+ const info = normalizeSessionInfo(await response.json());
4636
+ if (!info) throw new ServerError("Session fork response missing session id", 502, {
4637
+ origin: "runtime",
4638
+ endpoint: `/agents/sessions/${encodeURIComponent(id)}/forks`
4639
+ });
4640
+ return info;
4641
+ }
4098
4642
  /**
4099
4643
  * Look up a cached turn result by idempotency key. Returns the cached
4100
4644
  * payload if a turn with this `turnId` previously completed on the
@@ -4437,28 +4981,6 @@ function settleTurnDrive(result) {
4437
4981
  result
4438
4982
  };
4439
4983
  }
4440
- function normalizeSessionInfo(raw) {
4441
- const id = raw.id;
4442
- if (typeof id !== "string") return null;
4443
- const status = raw.status || "running";
4444
- return {
4445
- id,
4446
- status: [
4447
- "queued",
4448
- "running",
4449
- "completed",
4450
- "failed",
4451
- "cancelled"
4452
- ].includes(status) ? status : "running",
4453
- backend: typeof raw.backendType === "string" ? raw.backendType : typeof raw.backend === "string" ? raw.backend : void 0,
4454
- model: typeof raw.model === "string" ? raw.model : void 0,
4455
- promptCount: typeof raw.promptCount === "number" ? raw.promptCount : void 0,
4456
- createdAt: raw.createdAt ? new Date(raw.createdAt) : void 0,
4457
- startedAt: raw.startedAt ? new Date(raw.startedAt) : void 0,
4458
- endedAt: raw.endedAt ? new Date(raw.endedAt) : void 0,
4459
- raw
4460
- };
4461
- }
4462
4984
  var DirectRuntimeHttpClient = class {
4463
4985
  baseClient;
4464
4986
  sandboxId;
@@ -4525,4 +5047,4 @@ function quoteForShell(value) {
4525
5047
  return `'${value.replace(/'/g, "'\\''")}'`;
4526
5048
  }
4527
5049
  //#endregion
4528
- export { collectAgentResponseText as a, buildTraceExportPayload as c, toOtelJson as d, encodePromptForWire as f, serializeForSidecar as h, applySandboxEventText as i, exportTraceBundle as l, normalizeRuntimeBackendConfig as m, SandboxSession as n, getSandboxEventText as o, parseSSEStream as p, InteractiveSessionHandle as r, normalizeConnection as s, SandboxInstance as t, otelTraceIdForTangleTrace as u };
5050
+ export { normalizeRuntimeBackendConfig as _, 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, serializeForSidecar as v };