@tangle-network/sandbox 0.9.7 → 0.10.1

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;
@@ -32,7 +34,7 @@ function normalizeRuntimeBackendConfig(backend, options = {}) {
32
34
  ...backend?.model ? { model: backend.model } : options.model ? { model: parseModelString(options.model) } : {},
33
35
  ...backend?.server ? { server: backend.server } : {},
34
36
  ...backend?.interactions !== void 0 ? { interactions: backend.interactions } : {},
35
- ...backend?.questionChannel !== void 0 ? { questionChannel: backend.questionChannel } : {}
37
+ ...backend?.metadata !== void 0 ? { metadata: backend.metadata } : {}
36
38
  };
37
39
  }
38
40
  /**
@@ -99,19 +101,42 @@ function toBackendProfile(profile) {
99
101
  ...timeout !== void 0 ? { timeout } : {}
100
102
  }];
101
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;
102
120
  return {
103
121
  name: profile.name,
122
+ description: profile.description,
123
+ version: profile.version,
124
+ tags: profile.tags,
125
+ ...profileExtends ? { extends: profileExtends } : {},
104
126
  model: profile.model?.default,
127
+ small_model: profile.model?.small,
105
128
  ...profile.prompt?.systemPrompt ? { systemPrompt: profile.prompt.systemPrompt } : {},
106
129
  ...Object.keys(tools).length > 0 ? { tools } : {},
107
130
  ...profile.prompt?.instructions?.length ? { instructions: profile.prompt.instructions } : {},
108
131
  ...Object.keys(permission).length > 0 ? { permission } : {},
109
132
  ...mcp ? { mcp } : {},
110
133
  ...profile.connections?.length ? { connections: profile.connections } : {},
111
- ...profile.subagents ? { subagents: profile.subagents } : {},
134
+ ...agents ? { agent: agents } : {},
112
135
  ...profile.resources && hasAnyProfileResource(profile.resources) ? { resources: profile.resources } : {},
113
136
  ...profile.hooks ? { hooks: profile.hooks } : {},
114
137
  ...profile.modes ? { modes: profile.modes } : {},
138
+ ...commands ? { command: commands } : {},
139
+ ...plugins?.length ? { plugin: plugins } : {},
115
140
  ...profile.extensions ? { extensions: profile.extensions } : {}
116
141
  };
117
142
  }
@@ -574,6 +599,244 @@ function collectAgentResponseText(events) {
574
599
  return responseText && responseText.trim().length > 0 ? responseText : void 0;
575
600
  }
576
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
577
840
  //#region src/interactive.ts
578
841
  /**
579
842
  * Handle for one session's interactive harness. Obtained via
@@ -631,7 +894,7 @@ var InteractiveSessionHandle = class {
631
894
  * A single agent session inside a sandbox. Created via
632
895
  * `box.session(id)` — does not hit the network until a method is called.
633
896
  */
634
- var SandboxSession = class {
897
+ var SandboxSession = class SandboxSession {
635
898
  /**
636
899
  * @internal SDK-internal constructor — apps should call `box.session(id)`.
637
900
  */
@@ -674,6 +937,16 @@ var SandboxSession = class {
674
937
  return this.box._sessionResult(this.id);
675
938
  }
676
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
+ /**
677
950
  * Continue this session with an additional prompt. Equivalent to
678
951
  * `box.prompt(message, { ...opts, sessionId: this.id })` but reads
679
952
  * naturally on a Session reference.
@@ -685,6 +958,13 @@ var SandboxSession = class {
685
958
  });
686
959
  }
687
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
+ /**
688
968
  * Abort the session's in-flight execution; the session and its messages
689
969
  * are preserved (this does not delete the session). Void-returning alias
690
970
  * of `interrupt()` — use `interrupt()` when you need to know whether an
@@ -695,6 +975,22 @@ var SandboxSession = class {
695
975
  async cancel() {
696
976
  return this.box._sessionCancel(this.id);
697
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
+ }
698
994
  /**
699
995
  * Drive this session's harness in INTERACTIVE mode: spawn its native TUI,
700
996
  * stream the live framebuffer, and inject prompts as keystrokes. Distinct
@@ -732,6 +1028,24 @@ var SandboxSession = class {
732
1028
  }
733
1029
  };
734
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
735
1049
  //#region src/sandbox.ts
736
1050
  /**
737
1051
  * Sandbox Instance
@@ -1004,10 +1318,15 @@ var SandboxInstance = class SandboxInstance {
1004
1318
  client;
1005
1319
  info;
1006
1320
  defaultRuntimeBackend;
1321
+ runtime;
1007
1322
  constructor(client, info, defaultRuntimeBackend) {
1008
1323
  this.client = client;
1009
1324
  this.info = info;
1010
1325
  this.defaultRuntimeBackend = defaultRuntimeBackend;
1326
+ this.runtime = new SandboxRuntimeApi({
1327
+ ensureRunning: () => this.ensureRunning(),
1328
+ fetch: (path, init) => this.runtimeFetch(path, init)
1329
+ });
1011
1330
  }
1012
1331
  /** Unique sandbox identifier */
1013
1332
  get id() {
@@ -1049,6 +1368,10 @@ var SandboxInstance = class SandboxInstance {
1049
1368
  get error() {
1050
1369
  return this.info.error;
1051
1370
  }
1371
+ /** GPU lease attached during sandbox creation, when requested. */
1372
+ get gpuLease() {
1373
+ return this.info.gpuLease;
1374
+ }
1052
1375
  /** Web terminal URL for browser-based access */
1053
1376
  get url() {
1054
1377
  return this.info.connection?.webTerminalUrl;
@@ -1089,7 +1412,8 @@ var SandboxInstance = class SandboxInstance {
1089
1412
  startedAt: this.startedAt,
1090
1413
  lastActivityAt: this.lastActivityAt,
1091
1414
  expiresAt: this.expiresAt,
1092
- error: this.error
1415
+ error: this.error,
1416
+ gpuLease: this.gpuLease
1093
1417
  };
1094
1418
  }
1095
1419
  /**
@@ -1419,11 +1743,15 @@ var SandboxInstance = class SandboxInstance {
1419
1743
  */
1420
1744
  async write(path, content, options) {
1421
1745
  await this.ensureRunning();
1422
- 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}` : ""}`, {
1423
1750
  method: "POST",
1424
1751
  body: JSON.stringify({
1425
1752
  path,
1426
1753
  content,
1754
+ encoding: options?.encoding,
1427
1755
  mode: options?.mode
1428
1756
  })
1429
1757
  });
@@ -1431,6 +1759,7 @@ var SandboxInstance = class SandboxInstance {
1431
1759
  const body = await response.text();
1432
1760
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1433
1761
  }
1762
+ return (await response.json()).data;
1434
1763
  }
1435
1764
  /**
1436
1765
  * Write many files in one paced, retry-aware batch — see
@@ -1531,6 +1860,13 @@ var SandboxInstance = class SandboxInstance {
1531
1860
  controller.abort();
1532
1861
  }
1533
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
+ }
1534
1870
  /**
1535
1871
  * Stream events from an agent prompt.
1536
1872
  * Use this for real-time updates during agent execution.
@@ -1562,16 +1898,27 @@ var SandboxInstance = class SandboxInstance {
1562
1898
  }
1563
1899
  } catch (err) {
1564
1900
  if (isCallerAbort()) return;
1901
+ this.logStreamCheckpoint("synthetic_error", {
1902
+ sessionId,
1903
+ executionId,
1904
+ error: err instanceof Error ? err.message : String(err)
1905
+ });
1565
1906
  yield toStreamErrorEvent(err);
1566
1907
  } finally {
1567
- if (!receivedTerminal && !isCallerAbort()) yield {
1568
- type: "done",
1569
- data: {
1570
- status: "failed",
1571
- ...sessionId ? { sessionId } : {},
1572
- ...executionId ? { executionId } : {}
1573
- }
1574
- };
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
+ }
1575
1922
  }
1576
1923
  }
1577
1924
  /**
@@ -1593,8 +1940,22 @@ var SandboxInstance = class SandboxInstance {
1593
1940
  origin: "runtime"
1594
1941
  });
1595
1942
  };
1943
+ const streamStartedAt = Date.now();
1944
+ const logStreamCheckpoint = (checkpoint, data = {}) => {
1945
+ this.logStreamCheckpoint(checkpoint, {
1946
+ elapsedMs: Date.now() - streamStartedAt,
1947
+ ...data
1948
+ });
1949
+ };
1596
1950
  let response;
1597
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
+ });
1598
1959
  response = await this.runtimeFetch("/agents/run/stream", {
1599
1960
  method: "POST",
1600
1961
  signal: streamSignal,
@@ -1610,11 +1971,17 @@ var SandboxInstance = class SandboxInstance {
1610
1971
  turnId: options?.turnId,
1611
1972
  metadata: options?.context,
1612
1973
  requireVisibleAssistantOutput: options?.requireVisibleAssistantOutput,
1974
+ ...timeoutMs ? { timeoutMs } : {},
1613
1975
  ...options?.detach ? { detach: true } : {},
1614
1976
  backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend, { model: options?.model })
1615
1977
  })
1616
1978
  });
1979
+ logStreamCheckpoint("runtime_response_headers", {
1980
+ status: response.status,
1981
+ ok: response.ok
1982
+ });
1617
1983
  } catch (err) {
1984
+ logStreamCheckpoint("runtime_post_failed", { error: err instanceof Error ? err.message : String(err) });
1618
1985
  throwIfTimedOut();
1619
1986
  throw err;
1620
1987
  }
@@ -1637,17 +2004,39 @@ var SandboxInstance = class SandboxInstance {
1637
2004
  const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1638
2005
  let lastReplayStatus;
1639
2006
  let lastExecutionStatus;
2007
+ let firstSseEventSeen = false;
1640
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
+ }
1641
2016
  if (event.id) lastEventId = event.id;
1642
2017
  if (event.type === "execution.started" && event.data?.executionId) executionId = event.data.executionId;
1643
2018
  const eventSessionId = event.data?.sessionId;
1644
2019
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
1645
- 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
+ }
1646
2029
  yield event;
1647
2030
  }
1648
2031
  if (receivedTerminal) return;
1649
2032
  throwIfTimedOut();
1650
2033
  if (options?.signal?.aborted || streamSignal?.aborted) return;
2034
+ logStreamCheckpoint("stream_drop", {
2035
+ executionId,
2036
+ sessionId,
2037
+ lastEventId,
2038
+ receivedTerminal
2039
+ });
1651
2040
  if (!executionId) throw new NetworkError("Prompt stream dropped before execution.started; cannot reconnect", {
1652
2041
  endpoint: "/agents/run/stream",
1653
2042
  origin: "runtime"
@@ -1656,6 +2045,12 @@ var SandboxInstance = class SandboxInstance {
1656
2045
  throwIfTimedOut();
1657
2046
  if (options?.signal?.aborted || streamSignal?.aborted) return;
1658
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
+ });
1659
2054
  await new Promise((resolve) => setTimeout(resolve, RECONNECT_BACKOFF_MS[attempt - 1]));
1660
2055
  throwIfTimedOut();
1661
2056
  if (options?.signal?.aborted || streamSignal?.aborted) return;
@@ -1665,6 +2060,11 @@ var SandboxInstance = class SandboxInstance {
1665
2060
  method: "GET",
1666
2061
  signal: streamSignal
1667
2062
  });
2063
+ logStreamCheckpoint("replay_response_status", {
2064
+ attempt,
2065
+ status: replayResponse.status,
2066
+ ok: replayResponse.ok
2067
+ });
1668
2068
  if (!replayResponse.ok) {
1669
2069
  lastReplayStatus = replayResponse.status;
1670
2070
  console.warn(`[sandbox-sdk] Replay endpoint returned ${replayResponse.status}, attempt ${attempt}`);
@@ -1674,6 +2074,13 @@ var SandboxInstance = class SandboxInstance {
1674
2074
  lastExecutionStatus = info.status;
1675
2075
  if (info.status === "completed" || info.status === "failed" || info.status === "cancelled") {
1676
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
+ });
1677
2084
  yield {
1678
2085
  type: "done",
1679
2086
  data: {
@@ -1692,18 +2099,46 @@ var SandboxInstance = class SandboxInstance {
1692
2099
  continue;
1693
2100
  }
1694
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
+ }
1695
2110
  if (event.type === "history.replay.start" || event.type === "history.replay.end") continue;
1696
2111
  if (event.id) lastEventId = event.id;
1697
2112
  const eventSessionId = event.data?.sessionId;
1698
2113
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
1699
- 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
+ }
1700
2124
  yield event;
1701
2125
  }
1702
- if (receivedTerminal) return;
2126
+ if (receivedTerminal) {
2127
+ logStreamCheckpoint("reconnect_attempt_end", {
2128
+ attempt,
2129
+ status: "terminal_received"
2130
+ });
2131
+ return;
2132
+ }
1703
2133
  throwIfTimedOut();
1704
2134
  if (options?.signal?.aborted || streamSignal?.aborted) return;
1705
2135
  } catch (err) {
1706
2136
  throwIfTimedOut();
2137
+ logStreamCheckpoint("reconnect_attempt_end", {
2138
+ attempt,
2139
+ status: "failed",
2140
+ error: err instanceof Error ? err.message : String(err)
2141
+ });
1707
2142
  console.warn(`[sandbox-sdk] Reconnection attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
1708
2143
  }
1709
2144
  }
@@ -2015,6 +2450,22 @@ var SandboxInstance = class SandboxInstance {
2015
2450
  after: match.after
2016
2451
  };
2017
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
+ }
2018
2469
  /**
2019
2470
  * Live whole-sandbox resource usage (memory + CPU) from the container cgroup.
2020
2471
  *
@@ -2249,6 +2700,8 @@ var SandboxInstance = class SandboxInstance {
2249
2700
  return {
2250
2701
  supportsWriteMode: true,
2251
2702
  read: (path) => this.read(path),
2703
+ readBatch: (paths, options) => this.fsReadBatch(paths, options),
2704
+ tree: (path, options) => this.fsTree(path, options),
2252
2705
  write: (path, content, options) => this.write(path, content, options),
2253
2706
  writeMany: (files, options) => this.writeMany(files, options),
2254
2707
  search: (query, options) => this.search(query, options),
@@ -2263,6 +2716,12 @@ var SandboxInstance = class SandboxInstance {
2263
2716
  exists: (path) => this.fsExists(path)
2264
2717
  };
2265
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
+ }
2266
2725
  async fsUpload(localPath, remotePath, options) {
2267
2726
  await this.ensureRunning();
2268
2727
  const fs = await import("node:fs/promises");
@@ -2369,6 +2828,7 @@ var SandboxInstance = class SandboxInstance {
2369
2828
  const params = new URLSearchParams();
2370
2829
  if (options?.all) params.set("all", "true");
2371
2830
  if (options?.long) params.set("long", "true");
2831
+ if (options?.sessionId) params.set("sessionId", options.sessionId);
2372
2832
  const query = params.toString();
2373
2833
  const url = query ? `/fs/list${path}?${query}` : `/fs/list${path}`;
2374
2834
  const response = await this.runtimeFetch(url, { method: "GET" });
@@ -2414,8 +2874,21 @@ var SandboxInstance = class SandboxInstance {
2414
2874
  }
2415
2875
  async fsDelete(path, options) {
2416
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
+ }
2417
2889
  const params = new URLSearchParams();
2418
2890
  if (options?.recursive) params.set("recursive", "true");
2891
+ if (options?.sessionId) params.set("sessionId", options.sessionId);
2419
2892
  const query = params.toString();
2420
2893
  const url = query ? `/fs${path}?${query}` : `/fs${path}`;
2421
2894
  const response = await this.runtimeFetch(url, { method: "DELETE" });
@@ -3046,6 +3519,20 @@ var SandboxInstance = class SandboxInstance {
3046
3519
  update: (policy) => this.egressUpdate(policy)
3047
3520
  };
3048
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
+ }
3049
3536
  async networkUpdate(config) {
3050
3537
  if (config.blockOutbound !== void 0 && config.allowList !== void 0) {
3051
3538
  if (config.blockOutbound && config.allowList.length > 0) throw new Error("blockOutbound and allowList are mutually exclusive");
@@ -3123,6 +3610,44 @@ var SandboxInstance = class SandboxInstance {
3123
3610
  source: data.source
3124
3611
  };
3125
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
+ }
3126
3651
  /**
3127
3652
  * Validate CIDR notation (IPv4 and IPv6)
3128
3653
  */
@@ -3523,132 +4048,60 @@ var SandboxInstance = class SandboxInstance {
3523
4048
  };
3524
4049
  }
3525
4050
  /**
3526
- * Create a CRIU checkpoint of the sandbox's memory state.
3527
- *
3528
- * Checkpoints capture the complete memory state of the running sandbox,
3529
- * enabling true pause/resume and fork operations. Unlike snapshots which
3530
- * only preserve filesystem state, checkpoints preserve process memory,
3531
- * open file descriptors, and execution state.
3532
- *
3533
- * **Requirements:** CRIU must be available on the host. Check availability
3534
- * with `client.criuStatus()` before calling.
3535
- *
3536
- * **Note:** By default, checkpoint stops the sandbox. Use `leaveRunning: true`
3537
- * to keep it running (creates a copy-on-write checkpoint).
3538
- *
3539
- * @param options - Checkpoint options
3540
- * @returns Checkpoint result with ID and metadata
3541
- *
3542
- * @example Basic checkpoint (stops sandbox)
3543
- * ```typescript
3544
- * const checkpoint = await box.checkpoint();
3545
- * console.log(`Checkpoint: ${checkpoint.checkpointId}`);
3546
- * // Sandbox is now stopped, resume with box.resume()
3547
- * ```
3548
- *
3549
- * @example Checkpoint without stopping
3550
- * ```typescript
3551
- * const checkpoint = await box.checkpoint({
3552
- * tags: ["before-deploy"],
3553
- * leaveRunning: true,
3554
- * });
3555
- * // Sandbox continues running
3556
- * ```
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.
3557
4054
  */
3558
- async checkpoint(options) {
3559
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints`, {
3560
- method: "POST",
3561
- body: JSON.stringify({
3562
- tags: options?.tags,
3563
- leaveRunning: options?.leaveRunning,
3564
- includeSnapshot: options?.includeSnapshot
3565
- })
3566
- });
3567
- if (!response.ok) {
3568
- const body = await response.text();
3569
- throw parseErrorResponse(response.status, body, void 0, response.headers);
3570
- }
3571
- const data = await response.json();
3572
- return {
3573
- checkpointId: data.checkpointId ?? data.id,
3574
- createdAt: new Date(data.createdAt ?? ""),
3575
- sizeBytes: data.sizeBytes,
3576
- tags: data.tags ?? []
3577
- };
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);
3578
4057
  }
3579
4058
  /**
3580
- * List all checkpoints for this sandbox.
3581
- *
3582
- * @returns Array of checkpoint metadata
3583
- *
3584
- * @example
3585
- * ```typescript
3586
- * const checkpoints = await box.listCheckpoints();
3587
- * for (const cp of checkpoints) {
3588
- * console.log(`${cp.checkpointId}: ${cp.createdAt}`);
3589
- * }
3590
- * ```
4059
+ * @deprecated CRIU checkpoints were removed. List durable filesystem state
4060
+ * with {@link listSnapshots}. This method now throws.
3591
4061
  */
3592
- async listCheckpoints() {
3593
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints`);
3594
- if (!response.ok) {
3595
- const body = await response.text();
3596
- throw parseErrorResponse(response.status, body, void 0, response.headers);
3597
- }
3598
- const data = await response.json();
3599
- 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);
3600
4064
  }
3601
4065
  /**
3602
- * Delete a checkpoint.
3603
- *
3604
- * @param checkpointId - ID of the checkpoint to delete
4066
+ * @deprecated CRIU checkpoints were removed. This method now throws.
3605
4067
  */
3606
- async deleteCheckpoint(checkpointId) {
3607
- const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints/${encodeURIComponent(checkpointId)}`, { method: "DELETE" });
3608
- if (!response.ok) {
3609
- const body = await response.text();
3610
- throw parseErrorResponse(response.status, body, void 0, response.headers);
3611
- }
4068
+ deleteCheckpoint() {
4069
+ throw new CapabilityError("deleteCheckpoint() was removed: checkpoints no longer exist", "CHECKPOINT_REMOVED", 410);
3612
4070
  }
3613
4071
  /**
3614
- * Fork a new sandbox from a checkpoint.
3615
- *
3616
- * Creates a new sandbox with the same memory state as this sandbox
3617
- * at the time of the checkpoint. The fork has a new identity but
3618
- * preserves the execution state.
3619
- *
3620
- * **Use cases:**
3621
- * - Branch workflows: Create parallel execution paths
3622
- * - A/B testing: Run same state with different configurations
3623
- * - 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.
3624
4087
  *
3625
- * @param checkpointId - ID of the checkpoint to fork from
3626
- * @param options - Fork configuration
3627
- * @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.
3628
4091
  *
3629
- * @example Basic fork
3630
- * ```typescript
3631
- * const checkpoint = await box.checkpoint({ leaveRunning: true });
3632
- * const forked = await box.fork(checkpoint.checkpointId);
3633
- * // forked has same memory state as box at checkpoint time
3634
- * ```
3635
- *
3636
- * @example Fork with custom config
4092
+ * @example
3637
4093
  * ```typescript
3638
- * const forked = await box.fork(checkpointId, {
3639
- * name: "experiment-branch",
3640
- * env: { EXPERIMENT: "true" },
3641
- * });
4094
+ * const [a, b, c] = await box.branch(3);
4095
+ * // a, b, c each share box's memory state at branch time
3642
4096
  * ```
3643
4097
  */
3644
- async fork(checkpointId, options) {
3645
- 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`, {
3646
4101
  method: "POST",
3647
4102
  body: JSON.stringify({
3648
- checkpointId,
3649
- name: options?.name,
4103
+ count,
3650
4104
  env: options?.env,
3651
- resources: options?.resources,
3652
4105
  metadata: options?.metadata
3653
4106
  })
3654
4107
  });
@@ -3656,8 +4109,7 @@ var SandboxInstance = class SandboxInstance {
3656
4109
  const body = await response.text();
3657
4110
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3658
4111
  }
3659
- const data = await response.json();
3660
- 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));
3661
4113
  }
3662
4114
  /**
3663
4115
  * Stop the sandbox (keeps state for resume).
@@ -3894,17 +4346,6 @@ var SandboxInstance = class SandboxInstance {
3894
4346
  abortController.abort();
3895
4347
  }
3896
4348
  }
3897
- parseCheckpointInfo(cp) {
3898
- return {
3899
- checkpointId: cp.checkpointId ?? cp.id,
3900
- sandboxId: cp.sandboxId ?? cp.projectRef ?? this.id,
3901
- createdAt: new Date(cp.createdAt),
3902
- tags: cp.tags ?? [],
3903
- sizeBytes: cp.sizeBytes,
3904
- hasMemoryState: cp.hasMemoryState ?? true,
3905
- hasFilesystemSnapshot: cp.hasFilesystemSnapshot ?? false
3906
- };
3907
- }
3908
4349
  async ensureRunning() {
3909
4350
  await this.refresh();
3910
4351
  if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
@@ -3956,15 +4397,51 @@ var SandboxInstance = class SandboxInstance {
3956
4397
  async registerSessionMapping(opts) {
3957
4398
  const response = await this.client.fetch(`/v1/session/${encodeURIComponent(opts.sessionId)}/mapping`, {
3958
4399
  method: "PUT",
4400
+ headers: { "x-user-id": opts.userId },
3959
4401
  body: JSON.stringify({
3960
- userId: opts.userId,
3961
- sandboxId: this.id
4402
+ sidecarSessionId: opts.runtimeSessionId,
4403
+ ...opts.sidecarId ? { sidecarId: opts.sidecarId } : {},
4404
+ ...opts.projectRef ? { projectRef: opts.projectRef } : {}
3962
4405
  })
3963
4406
  });
4407
+ if (response.status === 410) return {
4408
+ success: false,
4409
+ sessionId: opts.sessionId,
4410
+ reprovisionRequired: true,
4411
+ code: "STALE_SIDECAR"
4412
+ };
3964
4413
  if (!response.ok) {
3965
4414
  const body = await response.text();
3966
4415
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3967
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
+ };
3968
4445
  }
3969
4446
  parseInfo(data) {
3970
4447
  return {
@@ -3978,6 +4455,7 @@ var SandboxInstance = class SandboxInstance {
3978
4455
  lastActivityAt: data.lastActivityAt ? new Date(data.lastActivityAt) : void 0,
3979
4456
  expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0,
3980
4457
  error: data.error,
4458
+ gpuLease: data.gpuLease ?? this.info.gpuLease,
3981
4459
  startupDiagnostics: normalizeStartupDiagnostics(data.startupDiagnostics) ?? this.info?.startupDiagnostics
3982
4460
  };
3983
4461
  }
@@ -3992,6 +4470,30 @@ var SandboxInstance = class SandboxInstance {
3992
4470
  session(id) {
3993
4471
  return new SandboxSession(this, id);
3994
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
+ }
3995
4497
  /**
3996
4498
  * List sessions on this sandbox, optionally filtering by status. Returns
3997
4499
  * `SandboxSession` instances paired with their last-known
@@ -4099,6 +4601,44 @@ var SandboxInstance = class SandboxInstance {
4099
4601
  metadata: m.info.metadata
4100
4602
  }));
4101
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
+ }
4102
4642
  /**
4103
4643
  * Look up a cached turn result by idempotency key. Returns the cached
4104
4644
  * payload if a turn with this `turnId` previously completed on the
@@ -4441,28 +4981,6 @@ function settleTurnDrive(result) {
4441
4981
  result
4442
4982
  };
4443
4983
  }
4444
- function normalizeSessionInfo(raw) {
4445
- const id = raw.id;
4446
- if (typeof id !== "string") return null;
4447
- const status = raw.status || "running";
4448
- return {
4449
- id,
4450
- status: [
4451
- "queued",
4452
- "running",
4453
- "completed",
4454
- "failed",
4455
- "cancelled"
4456
- ].includes(status) ? status : "running",
4457
- backend: typeof raw.backendType === "string" ? raw.backendType : typeof raw.backend === "string" ? raw.backend : void 0,
4458
- model: typeof raw.model === "string" ? raw.model : void 0,
4459
- promptCount: typeof raw.promptCount === "number" ? raw.promptCount : void 0,
4460
- createdAt: raw.createdAt ? new Date(raw.createdAt) : void 0,
4461
- startedAt: raw.startedAt ? new Date(raw.startedAt) : void 0,
4462
- endedAt: raw.endedAt ? new Date(raw.endedAt) : void 0,
4463
- raw
4464
- };
4465
- }
4466
4984
  var DirectRuntimeHttpClient = class {
4467
4985
  baseClient;
4468
4986
  sandboxId;
@@ -4529,4 +5047,4 @@ function quoteForShell(value) {
4529
5047
  return `'${value.replace(/'/g, "'\\''")}'`;
4530
5048
  }
4531
5049
  //#endregion
4532
- 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 };