From 3a768aaddfcc8b8278fe99c83ae2f3f13e9a6e8e Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Tue, 29 Sep 2026 21:14:23 -0600 Subject: [PATCH 1/2] fix(sandbox): correct SDK examples and retry guidance --- pages/sandbox/index.mdx | 4 +- pages/sandbox/quickstart.mdx | 44 ++++++++++++----- pages/sandbox/sdk-reference.mdx | 87 ++++++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 34 deletions(-) diff --git a/pages/sandbox/index.mdx b/pages/sandbox/index.mdx index 75c47ca..4732317 100644 --- a/pages/sandbox/index.mdx +++ b/pages/sandbox/index.mdx @@ -10,8 +10,8 @@ A real isolated computer for every agent. Each sandbox is a dev container or mic ## What you get - **A full machine per agent.** Run shell commands, read and write files, open ports, snapshot state, and attach a GPU only for the command that needs it. -- **Any coding harness.** Each sandbox runs one AI backend: OpenCode (default), Claude Code, Codex, Cursor, and a dozen more. You pick it per sandbox. -- **Durable agent sessions.** Dispatch a prompt and reconnect to the same run after a client crash, a deploy, or a browser reload. Same session ID is idempotent, so a retried request never re-executes. +- **Any coding harness.** Choose the initial harness, such as OpenCode (default), Claude Code, or Codex, when you create a sandbox. Sessions can select different [supported harnesses](/infrastructure/harnesses). +- **Durable agent sessions.** Dispatch a prompt and reconnect to the same run after a client crash, a deploy, or a browser reload. Store session and turn IDs for retries; completed work is deduplicated while its result remains cached. - **One key, metered per second.** The same [`sk-tan-` key](/platform/authentication) authenticates every call; you pay for what you use. ## Why use it diff --git a/pages/sandbox/quickstart.mdx b/pages/sandbox/quickstart.mdx index e0a0b0b..2e8b6dc 100644 --- a/pages/sandbox/quickstart.mdx +++ b/pages/sandbox/quickstart.mdx @@ -23,7 +23,29 @@ export TANGLE_API_KEY=sk-tan-... One key authenticates every Tangle product. Pass it to the client as `apiKey`; the SDK does not read env vars for you. -## 3. Create a sandbox and run a command +## 3. Check your credit and the service + +Free accounts start with no included credit. +Add prepaid credit or select a plan with included credit at [id.tangle.tools](https://id.tangle.tools) before creating a sandbox. +Agent model usage also draws from your balance. +On the Free plan, sandboxes pause when the balance reaches zero. + +Check your key and available credit without provisioning a machine: + +```bash +curl -fsS https://id.tangle.tools/v1/billing/balance \ + -H "Authorization: Bearer $TANGLE_API_KEY" +``` + +The response's `data.balance` is your available credit in US dollars. +You can also check the service and available templates without creating a sandbox: + +```bash +curl -fsS https://sandbox.tangle.tools/health +curl -fsS https://sandbox.tangle.tools/v1/public-templates +``` + +## 4. Create a sandbox and run a command ```typescript import { Sandbox } from "@tangle-network/sandbox"; @@ -45,9 +67,10 @@ try { `create` returns a live machine; `delete` releases it. You never provision infrastructure yourself. -## 4. Run a coding agent +## 5. Run a coding agent -Each sandbox runs one coding harness. Choose it with `backend.type`. `opencode` (OpenCode) is the default and needs no extra key. `claude-code` (Claude Code) runs on Tangle Router by default with no extra key. To bring your own Anthropic credential, pass `backend: { type: 'claude-code', model: { apiKey: process.env.ANTHROPIC_API_KEY } }`. See [supported harnesses](/infrastructure/harnesses) for the full list. +Choose the initial coding harness with `backend.type`. +Sessions can select different [supported harnesses](/infrastructure/harnesses) in the same sandbox. `opencode` (OpenCode) is the default and needs no separate model-provider key. `claude-code` (Claude Code) runs on Tangle Router by default with no separate model-provider key. To bring your own Anthropic credential, pass `backend: { type: 'claude-code', model: { apiKey: process.env.ANTHROPIC_API_KEY } }`. See [supported harnesses](/infrastructure/harnesses) for the full list. ```typescript const box = await client.create({ @@ -63,16 +86,11 @@ try { } ``` -For runs that must survive a client crash or a browser reload, use `box.dispatchPrompt(message, { sessionId })` and reconnect with `box.session(sessionId)`. See [durable sessions in the SDK reference](/sandbox/sdk-reference). - -## 5. Check before you build (optional) - -These calls are safe to run without creating a sandbox: - -```bash -curl -fsS https://sandbox.tangle.tools/health -curl -fsS https://sandbox.tangle.tools/v1/public-templates -``` +For durable runs, use `box.dispatchPrompt(message, { sessionId, turnId })` and reconnect with `box.session(sessionId)`. +Store both IDs before dispatching and reuse them for retries of the same turn. +Completed retries are deduplicated while their results remain cached. +A session ID alone does not deduplicate completed retries. +See [durable sessions in the SDK reference](/sandbox/sdk-reference). ## Next diff --git a/pages/sandbox/sdk-reference.mdx b/pages/sandbox/sdk-reference.mdx index 559711d..1acda08 100644 --- a/pages/sandbox/sdk-reference.mdx +++ b/pages/sandbox/sdk-reference.mdx @@ -54,21 +54,31 @@ const box = await client.get("sandbox_abc123"); const usage = await client.usage(); // activeSandboxes, computeMinutes ``` -### `client.runBatch(tasks, options?)` +### `client.runBatch(request, options?)` Run one-shot tasks across freshly provisioned sandboxes in parallel. For coordinated multi-machine work with shared workspaces and policy caps, use [fleets](https://www.npmjs.com/package/@tangle-network/sandbox) instead. ```typescript const result = await client.runBatch( - [ - { id: "task-1", message: "Analyze code quality" }, - { id: "task-2", message: "Run the security scan" }, - ], - { timeoutMs: 300000, scalingMode: "balanced" }, // fastest | balanced | cheapest + { + tasks: [ + { id: "task-1", message: "Analyze code quality" }, + { id: "task-2", message: "Run the security scan" }, + ], + backends: [{ id: "worker", type: "opencode" }], + }, + { idempotencyKey: "code-review-job-42" }, ); -console.log(result.successRate); +console.log(result.totalSuccess, result.totalFailure); ``` +Select batch workers from the [supported harnesses](/infrastructure/harnesses). +Reuse an `idempotencyKey` only when retrying the same batch with the same request body. +A keyed batch continues after a client disconnect. +While its run record remains retained, a retry joins active work or replays its completed result. +`options.signal` cancels the client stream. +Without an idempotency key, cancellation or disconnection also stops server work. + ## Sandbox instance ### `box.exec(command, options?)` @@ -96,32 +106,71 @@ for await (const event of box.streamPrompt("Fix the failing auth test")) { } ``` -### `box.task(message, options?)` +### `box.createTaskSession(options)` · `box.taskSession(id)` -A higher-level agent task (plan, edit, run, verify) over a single prompt. +Create a background task session with isolated file changes using one of the [supported harnesses](/infrastructure/harnesses). +The sandbox workspace must contain a Git repository. ```typescript -const task = await box.task("Fix any failing tests and commit the changes"); +const { session: task } = await box.createTaskSession({ + sessionId: "test-fix-42", + title: "Fix failing tests", + backend: { type: "opencode" }, + isolateFileWrites: true, +}); +await task.sendMessage({ + parts: [{ type: "text", text: "Fix the failing tests and run them again" }], + turnId: "test-fix-42-turn-1", +}); +await task.result(); +console.log(await task.changes()); ``` +Reconnect with `box.taskSession("test-fix-42")` to inspect the task later. +Review `task.changes()` before applying changes with `task.commit()`. + ## Durable sessions -`prompt` and `streamPrompt` live and die with the call. For a run that must survive a client crash, a redeploy, or a browser reload, dispatch it with a stable `sessionId` and reconnect from a fresh process. +Use `dispatchPrompt` for a turn you must reconnect to after a client crash, redeploy, or browser reload. +`sessionId` identifies the conversation; `turnId` identifies one logical turn. + +Derive both IDs from your trusted job record and store them before dispatching. +Reuse both IDs and the same prompt when retrying that job. ```typescript -const { sessionId, alreadyExisted } = await box.dispatchPrompt(prompt, { - sessionId, // derive this server-side; never accept a caller-chosen id +const sessionId = "code-review-job-42"; +const turnId = "code-review-job-42-turn-1"; + +const receipt = await box.dispatchPrompt("Analyze code quality", { + sessionId, + turnId, }); +console.log(receipt.dispatched); -// From any process, follow the run or wait for the result: -for await (const event of box.session(sessionId).events()) { - console.log(event); +const cached = await box.findCompletedTurn(turnId, { + sessionId: receipt.sessionId, +}); +if (cached) { + console.log(cached.result); +} else { + if (!receipt.executionId) throw new Error("Missing execution ID"); + const final = await box.session(receipt.sessionId).result({ + executionId: receipt.executionId, + }); + console.log(final); } -const final = await box.session(sessionId).result(); -const state = await box.session(sessionId).status(); ``` -The same `sessionId` is idempotent. A duplicate dispatch returns the in-flight or completed session instead of running the work twice, which makes it safe to retry a webhook or payment-triggered run. +Store the sandbox ID and dispatch receipt to reconnect from another process. +Use `client.get(sandboxId)` to obtain the sandbox, then select the receipt's session and execution IDs. +Use `session.events({ executionId })` to follow output while that execution runs. + +The same `sessionId` prevents a second dispatch while its session is in flight. +After completion, that session can receive a new turn. +Pass a stable `turnId` to deduplicate retries while the completed result remains cached. +The completed-turn cache can outlive its session; use `findCompletedTurn` to retrieve that cached result. +`dispatched: false` means the SDK found prior work and did not dispatch another turn. +`alreadyExisted` reports whether the session existed, not whether work ran. ## GPU leases From a07f07329a41cd300a8733360d1ab7b9a634e9b1 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Wed, 30 Sep 2026 17:48:35 -0600 Subject: [PATCH 2/2] fix(sandbox): make each SDK example standalone --- pages/sandbox/index.mdx | 2 +- pages/sandbox/quickstart.mdx | 29 +++- pages/sandbox/sdk-reference.mdx | 246 ++++++++++++++++++++++++++------ 3 files changed, 229 insertions(+), 48 deletions(-) diff --git a/pages/sandbox/index.mdx b/pages/sandbox/index.mdx index 4732317..0527b29 100644 --- a/pages/sandbox/index.mdx +++ b/pages/sandbox/index.mdx @@ -23,7 +23,7 @@ A real isolated computer for every agent. Each sandbox is a dev container or mic ## How it works -Your SDK or CLI call hits the Sandbox API, which routes to a driver (container or microVM), provisions the machine, and relays the agent's output back to you as it streams. You never manage the infrastructure: `create` returns a machine, `delete` releases it. For the execution and orchestration internals, see the [runtime docs](/infrastructure/introduction). +Your SDK and CLI calls hit the Sandbox API, which routes to a driver (container or microVM), provisions the machine, and relays the agent's output back to you as it streams. You never manage the infrastructure: `create` returns a machine, `delete` releases it. For the execution and orchestration internals, see the [runtime docs](/infrastructure/introduction). ## Use cases diff --git a/pages/sandbox/quickstart.mdx b/pages/sandbox/quickstart.mdx index 2e8b6dc..8dbdf18 100644 --- a/pages/sandbox/quickstart.mdx +++ b/pages/sandbox/quickstart.mdx @@ -11,6 +11,7 @@ Create an isolated machine, run a shell command in it, then hand it to a coding ```bash npm install @tangle-network/sandbox +npm install --save-dev tsx ``` ## 2. Get an API key @@ -18,7 +19,9 @@ npm install @tangle-network/sandbox Create an `sk-tan-` key from [Authentication](/platform/authentication) and export it: ```bash -export TANGLE_API_KEY=sk-tan-... +read -rsp "Tangle API key: " TANGLE_API_KEY +printf "\n" +export TANGLE_API_KEY ``` One key authenticates every Tangle product. Pass it to the client as `apiKey`; the SDK does not read env vars for you. @@ -34,7 +37,7 @@ Check your key and available credit without provisioning a machine: ```bash curl -fsS https://id.tangle.tools/v1/billing/balance \ - -H "Authorization: Bearer $TANGLE_API_KEY" + -H "Authorization: Bearer ${TANGLE_API_KEY:?Set TANGLE_API_KEY}" ``` The response's `data.balance` is your available credit in US dollars. @@ -45,13 +48,21 @@ curl -fsS https://sandbox.tangle.tools/health curl -fsS https://sandbox.tangle.tools/v1/public-templates ``` +Each TypeScript block below is a separate program for Node.js 22 or later. +Run it in a project with the SDK installed and `TANGLE_API_KEY` exported. +Save either block as `example.ts` and run it with `npx tsx example.ts`. +Both examples release their machine after completion. + ## 4. Create a sandbox and run a command ```typescript import { Sandbox } from "@tangle-network/sandbox"; +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + const client = new Sandbox({ - apiKey: process.env.TANGLE_API_KEY!, + apiKey, baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", }); @@ -73,9 +84,19 @@ Choose the initial coding harness with `backend.type`. Sessions can select different [supported harnesses](/infrastructure/harnesses) in the same sandbox. `opencode` (OpenCode) is the default and needs no separate model-provider key. `claude-code` (Claude Code) runs on Tangle Router by default with no separate model-provider key. To bring your own Anthropic credential, pass `backend: { type: 'claude-code', model: { apiKey: process.env.ANTHROPIC_API_KEY } }`. See [supported harnesses](/infrastructure/harnesses) for the full list. ```typescript +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + const box = await client.create({ environment: "universal", - backend: { type: "opencode" }, + backend: { type: "opencode" }, // Also supports Claude Code, Codex, and other supported harnesses. }); try { diff --git a/pages/sandbox/sdk-reference.mdx b/pages/sandbox/sdk-reference.mdx index 1acda08..956bcaf 100644 --- a/pages/sandbox/sdk-reference.mdx +++ b/pages/sandbox/sdk-reference.mdx @@ -6,9 +6,15 @@ description: The core @tangle-network/sandbox surface, creating sandboxes, runni # SDK reference The core surface of `@tangle-network/sandbox`. The [npm package](https://www.npmjs.com/package/@tangle-network/sandbox) documents every option; this page covers what most builders reach for. +Each TypeScript block is a separate program for Node.js 22 or later. +Set `TANGLE_API_KEY` before running it; optional `SANDBOX_BASE_URL` selects another deployment. +Examples that create machines or run agents consume account credit. +Creation examples release their machines in `finally`. +Save one block as `example.ts` and run it with `npx tsx example.ts`. ```bash npm install @tangle-network/sandbox +npm install --save-dev tsx ``` ## Client @@ -16,10 +22,13 @@ npm install @tangle-network/sandbox ```typescript import { Sandbox } from "@tangle-network/sandbox"; +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + const client = new Sandbox({ - apiKey: process.env.TANGLE_API_KEY!, + apiKey, baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", - timeoutMs: 30000, // optional + timeoutMs: 30000, }); ``` @@ -28,30 +37,63 @@ const client = new Sandbox({ Create a sandbox and get back a `SandboxInstance`. Common options: ```typescript +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + const box = await client.create({ name: "my-project", - environment: "node:20", - backend: { type: "opencode" }, // coding harness; see supported harnesses below + environment: "universal", + backend: { type: "opencode" }, // Also supports Claude Code, Codex, and other supported harnesses. env: { NODE_ENV: "development" }, resources: { cpuCores: 2, memoryMB: 4096, diskGB: 20 }, maxLifetimeSeconds: 3600, idleTimeoutSeconds: 900, - fromSnapshot: "snap_abc123", // restore a saved workspace (both fields required) - fromSandboxId: "sandbox_abc123", // the sandbox that owns the snapshot }); + +try { + console.log(box.id); +} finally { + await box.delete(); +} ``` `environment` accepts a named environment from `client.environments.list()`, a container image reference, or an SDK-built image ID. Omit it to use the server default. +To restore a snapshot, provide both `fromSnapshot` and its owning `fromSandboxId` when creating the sandbox. `backend.type` picks the coding harness. OpenCode is the default; Claude Code, Codex, and the other [supported harnesses](/infrastructure/harnesses) are selected the same way. ### `client.list(options?)` · `client.get(id)` · `client.usage()` +Set `TANGLE_SANDBOX_ID` to an existing sandbox you own. +This example reads that machine and your account usage without creating a machine. + ```typescript +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + +const sandboxId = process.env.TANGLE_SANDBOX_ID; +if (!sandboxId) throw new Error("Set TANGLE_SANDBOX_ID to an existing sandbox ID"); + const running = await client.list({ status: "running", limit: 10 }); -const box = await client.get("sandbox_abc123"); -const usage = await client.usage(); // activeSandboxes, computeMinutes +const box = await client.get(sandboxId); +if (!box) throw new Error("Sandbox not found"); +const usage = await client.usage(); +console.log(running, box.id, usage); ``` ### `client.runBatch(request, options?)` @@ -59,15 +101,28 @@ const usage = await client.usage(); // activeSandboxes, computeMinutes Run one-shot tasks across freshly provisioned sandboxes in parallel. For coordinated multi-machine work with shared workspaces and policy caps, use [fleets](https://www.npmjs.com/package/@tangle-network/sandbox) instead. ```typescript +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + +const jobId = process.env.TANGLE_BATCH_JOB_ID; +if (!jobId) throw new Error("Set TANGLE_BATCH_JOB_ID to your saved job ID"); + const result = await client.runBatch( { tasks: [ - { id: "task-1", message: "Analyze code quality" }, - { id: "task-2", message: "Run the security scan" }, + { id: "task-1", message: "Create a JavaScript function that adds two numbers and test it." }, + { id: "task-2", message: "Create a JavaScript function that reverses a string and test it." }, ], backends: [{ id: "worker", type: "opencode" }], }, - { idempotencyKey: "code-review-job-42" }, + { idempotencyKey: jobId }, ); console.log(result.totalSuccess, result.totalFailure); ``` @@ -86,12 +141,27 @@ Without an idempotency key, cancellation or disconnection also stops server work Run a shell command. ```typescript -const result = await box.exec("npm install", { - cwd: "/workspace", - env: { CI: "true" }, - timeoutMs: 60000, +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", }); -console.log(result.exitCode, result.stdout); + +const box = await client.create({ environment: "universal" }); +try { + const result = await box.exec("node --version", { + cwd: "/workspace", + env: { CI: "true" }, + timeoutMs: 60000, + }); + console.log(result.exitCode, result.stdout); +} finally { + await box.delete(); +} ``` ### `box.prompt(message, options?)` · `box.streamPrompt(message, options?)` @@ -99,34 +169,75 @@ console.log(result.exitCode, result.stdout); Run one agent turn. `prompt` returns the result; `streamPrompt` yields events as they happen. ```typescript -const result = await box.prompt("List the files and summarize the project"); +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + +const box = await client.create({ environment: "universal" }); +try { + const result = await box.prompt("Create a JavaScript function that adds two numbers."); + console.log(result); -for await (const event of box.streamPrompt("Fix the failing auth test")) { - console.log(event); + for await (const event of box.streamPrompt("Add tests for that function and run them.")) { + console.log(event); + } +} finally { + await box.delete(); } ``` ### `box.createTaskSession(options)` · `box.taskSession(id)` Create a background task session with isolated file changes using one of the [supported harnesses](/infrastructure/harnesses). -The sandbox workspace must contain a Git repository. +Set `TANGLE_REPO_URL` to a public Git repository URL. +The example clones that repository before creating the task session. ```typescript -const { session: task } = await box.createTaskSession({ - sessionId: "test-fix-42", - title: "Fix failing tests", - backend: { type: "opencode" }, - isolateFileWrites: true, +import { randomUUID } from "node:crypto"; +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", }); -await task.sendMessage({ - parts: [{ type: "text", text: "Fix the failing tests and run them again" }], - turnId: "test-fix-42-turn-1", + +const repoUrl = process.env.TANGLE_REPO_URL; +if (!repoUrl) throw new Error("Set TANGLE_REPO_URL to a public Git repository URL"); + +const box = await client.create({ + environment: "universal", + git: { url: repoUrl }, }); -await task.result(); -console.log(await task.changes()); +try { + const { session: task } = await box.createTaskSession({ + sessionId: randomUUID(), + title: "Write a README", + backend: { type: "opencode" }, // Also supports Claude Code, Codex, and other supported harnesses. + isolateFileWrites: true, + }); + await task.sendMessage({ + parts: [{ type: "text", text: "Write a README explaining this workspace." }], + turnId: randomUUID(), + }); + console.log(await task.result()); + console.log(await task.changes()); +} finally { + await box.delete(); +} ``` -Reconnect with `box.taskSession("test-fix-42")` to inspect the task later. +This example clones your repository, inspects isolated changes, then deletes its machine. +For retained tasks, store the sandbox ID and task session ID instead of deleting the machine. +Reconnect with `client.get(savedSandboxId)` and `box.taskSession(savedSessionId)`. Review `task.changes()` before applying changes with `task.commit()`. ## Durable sessions @@ -136,16 +247,35 @@ Use `dispatchPrompt` for a turn you must reconnect to after a client crash, rede Derive both IDs from your trusted job record and store them before dispatching. Reuse both IDs and the same prompt when retrying that job. +Set `TANGLE_SANDBOX_ID`, `TANGLE_SESSION_ID`, and `TANGLE_TURN_ID` from that saved record. +This example retains the existing sandbox so another process can reconnect. +Delete it when the job and any retries finish. ```typescript -const sessionId = "code-review-job-42"; -const turnId = "code-review-job-42-turn-1"; +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + +const sandboxId = process.env.TANGLE_SANDBOX_ID; +const sessionId = process.env.TANGLE_SESSION_ID; +const turnId = process.env.TANGLE_TURN_ID; +if (!sandboxId || !sessionId || !turnId) { + throw new Error("Set TANGLE_SANDBOX_ID, TANGLE_SESSION_ID, and TANGLE_TURN_ID from your saved job record"); +} + +const box = await client.get(sandboxId); +if (!box) throw new Error("Sandbox not found"); const receipt = await box.dispatchPrompt("Analyze code quality", { sessionId, turnId, }); -console.log(receipt.dispatched); +console.log(receipt); const cached = await box.findCompletedTurn(turnId, { sessionId: receipt.sessionId, @@ -177,24 +307,54 @@ The completed-turn cache can outlive its session; use `findCompletedTurn` to ret Keep the base sandbox cheap and attach a GPU only around the step that needs it. Every lease takes a hard spend cap and lifetime. ```typescript -const lease = await box.gpu.attach({ - accelerator: { kind: "nvidia-h100", count: 1 }, - maxSpendUsd: 5, - maxLifetimeSeconds: 600, +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", }); + +const box = await client.create({ environment: "universal" }); try { - await box.gpu.exec(lease.id, { command: "python train.py" }); + const lease = await box.gpu.attach({ + accelerator: { kind: "nvidia-h100", count: 1 }, + maxSpendUsd: 5, + maxLifetimeSeconds: 600, + }); + try { + console.log(await box.gpu.exec(lease.id, { command: "nvidia-smi" })); + } finally { + await box.gpu.detach(lease.id); + } } finally { - await box.gpu.detach(lease.id); + await box.delete(); } ``` ## Lifecycle ```typescript -await box.stop(); // pause; resume later -await box.resume(); -await box.delete(); // release the machine +import { Sandbox } from "@tangle-network/sandbox"; + +const apiKey = process.env.TANGLE_API_KEY; +if (!apiKey) throw new Error("Set TANGLE_API_KEY"); + +const client = new Sandbox({ + apiKey, + baseUrl: process.env.SANDBOX_BASE_URL ?? "https://sandbox.tangle.tools", +}); + +const box = await client.create({ environment: "universal" }); +try { + await box.stop(); + await box.resume(); + console.log((await box.exec("node --version")).stdout); +} finally { + await box.delete(); +} ``` ## Next