@tangle-network/sandbox
v0.4.3
Published
Client SDK for the Tangle Sandbox platform - build AI agent applications with dev containers
Downloads
23,302
Readme
@tangle-network/sandbox
TypeScript SDK for the Tangle Sandbox platform. Create isolated dev containers, run AI agents, and build automation workflows.
A separate CLI is published as @tangle-network/sandbox-cli.
Installation
npm install @tangle-network/sandbox
# or
pnpm add @tangle-network/sandbox
# or
yarn add @tangle-network/sandboxQuick Start
import { Sandbox } from "@tangle-network/sandbox";
// Initialize the client
const client = new Sandbox({
apiKey: "sk_sandbox_...",
baseUrl: "https://your-sandbox-api.example.com",
});
// Create a sandbox
const box = await client.create({
name: "my-project",
image: "node:20",
});
// Execute commands
const result = await box.exec("npm install && npm test");
console.log(result.stdout);
// Run an AI agent task
const task = await box.task("Fix any failing tests and commit the changes");
console.log(task.response);
// Clean up
await box.delete();Stream durability is platform-managed — do not build your own
If you are about to add a Cloudflare Durable Object, a KV bucket, an in-Worker ring buffer, or any other state store to "buffer agent stream events so the user survives a reload" — stop. The Tangle orchestrator already buffers every event for every session to a Redis sorted-set keyed by
sessionIdwith TTL, and the SDK ships a browser/Worker-safe client that reconnects, replays missed events, and persistslastEventIdacross tab reloads. The work is done. Use it.
The decision tree:
| You need… | Use |
|---|---|
| Fire-and-forget streaming from a server you control (CLI, cron, batch worker) | box.streamPrompt() / box.streamTask() — internal auto-reconnect handles transient drops within the same call |
| Survive client process death (Worker isolate eviction, laptop crash, deploy) and resume later | box.dispatchPrompt(msg, { sessionId }) then box.session(sessionId).events({ since }) / .result() from a fresh process |
| Survive browser disconnects (wifi flap, tab reload, mobile background) with Last-Event-ID replay | SessionGatewayClient from @tangle-network/sandbox/session-gateway |
| Retry a payment-triggered or webhook-triggered run safely | box.dispatchPrompt(msg, { sessionId: deterministicKeyFromRequest }) — same sessionId is idempotent: a duplicate dispatch returns the in-flight or completed session, never re-executes |
| Inspect what happened to a turn that died mid-stream | box.session(id).status() (terminal state), box.session(id).events({ since }) (replay buffered events) |
Dispatch + reconnect (the "Worker restart" pattern)
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({ apiKey: process.env.TANGLE_API_KEY! });
// In your /chat handler:
const sessionId = req.headers.get("x-turn-id") ?? crypto.randomUUID();
const { sessionId: id, alreadyExisted } = await box.dispatchPrompt(prompt, {
sessionId, // idempotent: a retry with the same id is a lookup, not a re-execute
});
// Stream to the browser; if the client comes back later with the same sessionId
// and a Last-Event-ID, hand them the replay path below.
for await (const event of box.session(id).events({
since: req.headers.get("last-event-id") ?? undefined,
})) {
res.write(`id: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
}Browser-direct streaming (without proxying tokens through your server)
import { SessionGatewayClient } from "@tangle-network/sandbox/session-gateway";
const client = new SessionGatewayClient({
url: "wss://your-sandbox-api.example.com/session",
token: await fetchScopedToken(), // mint via box.mintScopedToken({ scope: 'session', sessionId })
sessionId,
autoReconnect: true,
enableReplayPersistence: true, // remembers lastEventId across reloads
replayStorage: { /* localStorage adapter, see session-gateway docs */ },
handlers: {
onMessage: (event) => render(event),
onReplayStart: ({ since }) => showSpinner(`replaying from ${since}`),
onReplayComplete: () => hideSpinner(),
onBackpressureWarning: ({ droppedCount, suggestReplay }) =>
suggestReplay && client.replay(client.stats.replay.lastEventId),
},
});
client.connect();SessionGatewayClient handles auto-reconnect with exponential backoff, sequence-gap
detection, replay-on-reconnect, and lastEventId persistence. None of this requires a
Durable Object.
See examples/cf-worker-chat.ts, examples/browser-streaming-resume.ts, and
examples/reconnect-from-last-event-id.ts for end-to-end patterns.
Features
- Sandbox Management - Create, list, stop, resume, and delete sandboxes
- Command Execution - Run shell commands in isolated containers
- AI Agent Tasks - Multi-turn agent execution with automatic tool use
- Snapshots - Save and restore sandbox state
- BYOS3 - Bring your own S3 storage for snapshots
- Fleets - Coordinated multi-machine workloads with policy, workspace snapshots, and parallel dispatch
- Intelligence Reports - Deterministic or agentic post-hoc analysis of sandbox or fleet evidence (fleet reports can refine to a single dispatch via
subject.dispatchId) - Event Streaming - Real-time SSE streams for agent events
- Collaboration Foundations - Token issuance and document identity helpers for collaborative editing
- Trace Intelligence - Export raw sandbox and fleet traces, embedded intelligence envelope, timing metrics, and OTEL JSON to customer-owned observability systems
Collaboration Foundations
The SDK now includes the first collaboration primitives for product backends:
- collaboration token issuance from
@tangle-network/sandbox/auth - stable document identity helpers from
@tangle-network/sandbox/collaboration - a collaboration API client for bootstrap / token refresh / snapshot calls
- a headless file bridge for syncing document adapters with sandbox files
Example:
import {
buildCollaborationDocumentId,
} from "@tangle-network/sandbox/collaboration";
import { ProductTokenIssuer } from "@tangle-network/sandbox/auth";
const issuer = new ProductTokenIssuer({
productId: "gtm-agent",
signingSecret: process.env.SANDBOX_SIGNING_SECRET!,
});
const documentId = buildCollaborationDocumentId({
workspaceId: "ws_123",
relativePath: "system/operating-system.md",
});
const { token, expiresAt } = issuer.issueCollaboration({
userId: "user_123",
sessionId: "sess_456",
projectId: "ws_123",
documentId,
access: "write",
});Bootstrap and snapshot helpers:
import { CollaborationClient } from "@tangle-network/sandbox/collaboration";
const collab = new CollaborationClient({
baseUrl: "https://app.example.com",
headers: () => ({
Authorization: `Bearer ${sessionToken}`,
}),
});
const bootstrap = await collab.bootstrap({
workspaceId: "ws_123",
relativePath: "system/operating-system.md",
});The bridge and client are SDK-side primitives. Product/backend endpoints and CRDT runtime integration still need to be wired by the application.
Trace Intelligence
The platform exposes two distinct intelligence primitives. Use the right one for the job.
| Primitive | What you get | Billable | API call |
|---|---|---|---|
| Embedded envelope | Inline summary in a trace/dispatch response: signals, recommended actions, fanout timings, dispatch failure classes | No (billing.billable: false) | trace({ includeIntelligence: true }), intelligence(), dispatch responses |
| Intelligence Report | A pollable job over sandbox or fleet evidence; fleet reports can refine to a single dispatch via subject.dispatchId. Runs in deterministic or agentic mode against a budget. | deterministic: free. agentic: billed against budget.maxUsd | intelligence.createReport(...), see Intelligence Reports |
Embedded envelope (free)
The embedded envelope is opt-in on trace() calls (includeIntelligence: true) and always included on dispatch responses (because dispatch already pays the analysis cost as part of producing the result).
const boxBundle = await box.trace(); // { trace } only
const boxInsights = await box.intelligence(); // envelope only
const boxBundleWithInsights = await box.trace({ includeIntelligence: true });
const run = await fleet.dispatchExecDetailed("pytest -q", {
machines: ["worker-1", "worker-2"],
});
console.log(run.results);
console.log(run.intelligence.signals); // always present on dispatch
console.log(run.intelligence.recommendedActions);
const fleetBundle = await fleet.trace(); // { trace } only
const fleetInsights = await fleet.intelligence(); // envelope only
const fleetBundleWithInsights = await fleet.trace({ includeIntelligence: true });Sandbox traces cover lifecycle, runtime, usage, timing, and current health snapshots. Fleet traces add machine lifecycle, workspace state, dispatch results, fanout timings, and critical path. The embedded envelope tells you what to inspect next: reliability, parallelism efficiency for fleets, dispatch failure classes, resource attribution, timing bottlenecks, and recommended actions.
The embedded envelope is deterministic platform analysis. It reports billing.billable: false and billing.costUsd: 0; customers are not charged for generating these envelopes.
Exporting traces to your observability stack
Use format: "tangle" to preserve the native envelope, or format: "otel-json" for OpenTelemetry-style collectors and platforms that accept OTLP JSON.
await box.exportTrace({
url: "https://collector.example.com/traces",
headers: { Authorization: `Bearer ${process.env.OBSERVABILITY_TOKEN}` },
format: "otel-json",
serviceName: "research-agent",
});
await fleet.exportTrace({
url: "https://collector.example.com/traces",
headers: { Authorization: `Bearer ${process.env.OBSERVABILITY_TOKEN}` },
format: "otel-json",
serviceName: "research-agent",
});For Braintrust, Lemma, Raindrop, Langfuse, Datadog, or a custom warehouse, keep this as a customer-owned sink or webhook. Tangle does not need their vendor credentials: fetch box.trace() or fleet.trace() and send the raw bundle through their SDK/API, or point exportTrace() at a small ingest endpoint that transforms it into the vendor's preferred schema.
Agent tools should expose both trace and intelligence actions on manageSandboxes. trace returns the full raw bundle for downstream analysis; intelligence returns the compact agent-readable next-step summary.
Core Concepts
Sandboxes
A sandbox is an isolated dev container with:
- A programmatic runtime API
- Optional SSH access
- Optional web terminal
- Persistent storage with snapshots
const box = await client.create({
name: "my-sandbox",
image: "python:3.12",
env: { DEBUG: "true" },
sshEnabled: true,
maxLifetimeSeconds: 7200, // 2 hours
idleTimeoutSeconds: 1800, // 30 min idle timeout
resources: {
cpuCores: 2,
memoryMB: 4096,
diskGB: 20,
},
});Status Lifecycle
pending -> provisioning -> running -> stopped -> deleted
|
v
failedAPI Reference
Client
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: "sk_sandbox_...",
baseUrl: "https://your-sandbox-api.example.com", // required
timeoutMs: 30000, // optional
});client.create(options?)
Create a new sandbox.
const box = await client.create({
name: "my-project",
image: "node:20", // or "typescript" for pre-built image
agentIdentifier: "my-agent", // agent to run
env: { NODE_ENV: "development" },
sshEnabled: true,
sshPublicKey: "ssh-ed25519 AAAA...",
webTerminalEnabled: true,
maxLifetimeSeconds: 3600,
idleTimeoutSeconds: 900,
resources: {
cpuCores: 2,
memoryMB: 4096,
diskGB: 20,
},
metadata: { team: "platform" },
// BYOS3: Customer-provided storage
storage: {
type: "s3",
bucket: "my-snapshots",
region: "us-east-1",
credentials: {
accessKeyId: "AKIA...",
secretAccessKey: "...",
},
},
fromSnapshot: "snap_abc123", // restore from snapshot
});client.list(options?)
List all sandboxes.
const sandboxes = await client.list({
status: "running", // filter by status
limit: 10,
offset: 0,
});client.get(id)
Get a sandbox by ID.
const box = await client.get("sandbox_abc123");
if (box) {
console.log(box.status);
}client.usage()
Get account usage information.
const usage = await client.usage();
console.log(`Active: ${usage.activeSandboxes}`);
console.log(`Compute: ${usage.computeMinutes} minutes`);client.runBatch(tasks, options?)
Run ad-hoc tasks across freshly-provisioned sandboxes in parallel. Use this when the work is one-shot and the sandboxes do not need to share a workspace or be addressable by stable machineId. For coordinated multi-machine workloads with policy enforcement, workspace sharing, dispatch buffering, and intelligence reports, see Fleets.
const result = await client.runBatch([
{ id: "task-1", message: "Analyze code quality" },
{ id: "task-2", message: "Run security scan" },
{ id: "task-3", message: "Generate documentation" },
], {
timeoutMs: 300000,
scalingMode: "balanced", // "fastest" | "balanced" | "cheapest"
});
console.log(`Success rate: ${result.successRate}%`);client.fleets
Fleet client — see Fleets for the full surface (create, createWithCoordinator, list, delete, estimateCost, capabilities, operations, reconcile, reapExpired).
client.intelligence
Intelligence report client — see Intelligence Reports for the full surface (createReport, createAgenticReport, getReport, listReports, waitForReport).
Sandbox Instance
After creating or retrieving a sandbox, you get a SandboxInstance with these methods:
box.exec(command, options?)
Execute a shell command.
const result = await box.exec("npm install", {
cwd: "/workspace",
env: { CI: "true" },
timeoutMs: 60000,
});
console.log(result.exitCode); // 0
console.log(result.stdout);
console.log(result.stderr);box.prompt(message, options?)
Send a single prompt to the AI agent.
const result = await box.prompt("What files are in this project?", {
sessionId: "session_123", // for conversation continuity
model: "anthropic/claude-sonnet-4-20250514",
timeoutMs: 120000,
});
console.log(result.response);
console.log(result.usage); // { inputTokens, outputTokens }Backend Selection
Each sandbox runs one AI backend. Pass backend.type to choose it:
| Type | Runtime | When to use |
|------|---------|-------------|
| opencode | OpenCode | Default. Multi-provider, profile system, MCP support |
| claude-code | Claude Code | Anthropic-native. Needs ANTHROPIC_API_KEY |
| codex | Codex CLI | OpenAI-native. Needs OPENAI_API_KEY |
| cursor | Cursor Agent SDK | Cursor-native local/cloud agent. Needs CURSOR_API_KEY |
| amp | AMP | Sourcegraph AMP agent |
| factory-droids | Factory | Factory Droid agent |
// Use Claude Code backend
await box.prompt("Fix the auth bug", {
backend: { type: "claude-code" },
});
// Use Codex with a named profile
await box.prompt("Audit this repo", {
backend: { type: "codex", profile: "browser-codex-fast" },
});
// Use Cursor Agent SDK
await box.prompt("Implement this change", {
backend: {
type: "cursor",
model: {
model: "composer-2",
apiKey: process.env.CURSOR_API_KEY,
},
profile: {
name: "cursor-release-agent",
prompt: {
systemPrompt:
"Use repo rules, configured MCP servers, skills, and subagents when relevant.",
},
mcp: {
docs: { transport: "sse", url: "https://docs.example.com/sse" },
},
subagents: {
reviewer: {
description: "Reviews changes for correctness and missing tests.",
prompt: "Review the current diff. Return only blocking findings.",
model: "composer-2",
},
},
resources: {
instructions: "Run focused tests before reporting completion.",
skills: [
{
kind: "inline",
name: "release-check",
content:
"---\nname: release-check\ndescription: Validate release readiness.\n---\nRun typecheck and focused tests.",
},
],
},
extensions: {
cursor: {
runtime: "local",
local: { settingSources: ["project", "user"] },
force: true,
},
},
},
},
});
// Use OpenCode with an inline profile
await box.prompt("Audit this repo", {
backend: {
type: "opencode",
profile: {
name: "security-auditor",
prompt: {
systemPrompt: "Focus on authorization and sandbox boundary mistakes.",
},
tools: { bash: true },
permissions: { bash: "allow" },
},
},
});
// BYOK (Bring Your Own Key)
await box.prompt("Analyze this", {
backend: {
type: "opencode",
model: {
provider: "anthropic",
model: "claude-sonnet-4-20250514",
apiKey: process.env.MY_ANTHROPIC_KEY,
},
},
});The SDK serializes backend.profile into the required wire format automatically.
Cursor profiles map portable MCP, resources, skills, subagents, hooks, permissions,
and Cursor-native extensions.cursor fields into the Cursor Agent SDK. Local
Cursor runs materialize .cursor/* project files inside the sandbox workspace.
Cloud Cursor runs fail closed when given uncommitted local resources that cannot
be delivered to the remote Cursor workspace.
Provider-native metadata is available through box.backend when the backend
SDK exposes it:
const account = await box.backend.account();
const models = await box.backend.models();
const repositories = await box.backend.repositories();
const agents = await box.backend.agents({ limit: 20 });
const agent = await box.backend.agent(agents.items[0].agentId);
const runs = await box.backend.runs(agent.agentId, { limit: 20 });
const run = await box.backend.run(runs.items[0].id, {
agentId: agent.agentId,
});
const messages = await box.backend.agentMessages(agent.agentId, { limit: 50 });
const artifacts = await box.backend.artifacts("active-session-id");
const bytes = await box.backend.downloadArtifact(
"active-session-id",
artifacts[0].path,
);Unsupported provider-control methods return the backend error; the SDK does not fabricate catalog, run, or artifact data for backends that do not expose it.
box.task(message, options?)
Run a multi-turn agent task. The agent keeps working until completion.
const result = await box.task("Set up a REST API with authentication", {
maxTurns: 20, // limit turns (0 = unlimited)
sessionId: "...", // continue previous session
});
console.log(result.turnsUsed);
console.log(result.response);box.streamPrompt(message, options?)
Stream agent events in real-time.
for await (const event of box.streamPrompt("Explain this codebase")) {
switch (event.type) {
case "message.part.updated": {
const part = event.data.part as { type?: string; text?: string };
if (part.type === "text" && event.data.delta) {
process.stdout.write(String(event.data.delta));
}
if (part.type === "reasoning" && event.data.delta) {
// Reasoning/thinking tokens from extended thinking models
process.stderr.write(`[thinking] ${String(event.data.delta)}`);
}
break;
}
case "result":
console.log("\nFinal:", event.data.finalText);
break;
case "done":
console.log("\nComplete!");
break;
}
}box.streamTask(message, options?)
Stream a multi-turn task with real-time events.
for await (const event of box.streamTask("Build a CLI tool")) {
// Handle events...
}box.direct()
Create an explicit advanced direct-runtime view of the sandbox.
const directBox = box.direct();
const result = await directBox.exec("npm test");box.events(options?)
Subscribe to sandbox lifecycle events.
for await (const event of box.events({ signal: controller.signal })) {
console.log(`Event: ${event.type}`, event.data);
}Snapshots
box.snapshot(options?)
Create a snapshot of the sandbox state.
const snapshot = await box.snapshot({
tags: ["v1.0", "stable"],
paths: ["/workspace"], // specific paths (default: all)
});
console.log(snapshot.snapshotId);
console.log(snapshot.sizeBytes);box.listSnapshots()
List all snapshots for this sandbox.
const snapshots = await box.listSnapshots();
for (const snap of snapshots) {
console.log(`${snap.snapshotId}: ${snap.createdAt}`);
}BYOS3 (Bring Your Own S3)
Store snapshots in your own S3-compatible storage. Supports AWS S3, Google Cloud Storage, and Cloudflare R2.
Creating a sandbox with BYOS3
const box = await client.create({
name: "my-sandbox",
storage: {
type: "s3", // "s3" | "gcs" | "r2"
bucket: "my-snapshots",
region: "us-east-1",
endpoint: "https://s3.us-east-1.amazonaws.com", // optional
credentials: {
accessKeyId: "AKIA...",
secretAccessKey: "...",
},
prefix: "sandbox-snapshots/", // optional path prefix
},
fromSnapshot: "snap_abc123", // restore from your storage
});Snapshots with BYOS3
When storage is configured, snapshots are written directly to your bucket:
// Create snapshot to your S3
const snap = await box.snapshot({
tags: ["production"],
storage: {
type: "s3",
bucket: "my-snapshots",
credentials: { accessKeyId: "...", secretAccessKey: "..." },
},
});
// List snapshots from your S3
const snapshots = await box.listSnapshots({
type: "s3",
bucket: "my-snapshots",
credentials: { ... },
});
// Restore from your S3
await box.restoreFromStorage({
type: "s3",
bucket: "my-snapshots",
credentials: { ... },
});Runtime Routing
By default, SDK runtime calls like exec(), prompt(), task(), file ops, git, and process management go through the Sandbox API, which dispatches them to the correct running sandbox for the authenticated user.
Runtime agent calls can pass a named backend profile or an inline provider-neutral profile object:
const result = await box.prompt("Audit the authentication flow", {
backend: {
profile: {
name: "security-auditor",
prompt: {
systemPrompt: "You are a senior application security auditor.",
instructions: ["Prioritize authz, tenancy, and secret handling."],
},
tools: { bash: true },
permissions: { bash: "allow" },
},
},
});Direct Runtime Access
For advanced use cases, use box.direct() to make runtime calls directly against the sandbox runtime while keeping normal lifecycle methods on the API client:
const directBox = box.direct();
const result = await directBox.exec("npm test");This is the recommended advanced path for power users who want runtime-level access without re-implementing auth/header plumbing themselves.
Lifecycle Methods
// Stop (preserves state)
await box.stop();
// Resume
await box.resume();
// Delete (destroys everything)
await box.delete();
// Refresh status from API
await box.refresh();
// Wait for specific status
await box.waitFor("running", { timeoutMs: 60000 });Properties
box.id // Unique identifier
box.name // Human-readable name
box.status // "pending" | "provisioning" | "running" | "stopped" | "failed"
box.connection // raw direct connection info for advanced use
box.metadata // Custom metadata
box.createdAt // Date
box.startedAt // Date | undefined
box.lastActivityAt // Date | undefined
box.expiresAt // Date | undefined
box.error // Error message if failedFleets
A fleet is a coordinated group of sandboxes that runs one logical workload across many machines. Fleets are the canonical primitive for parallel work, distributed simulation, multi-agent experiments, or any task that needs more than one sandbox under one lifecycle.
Fleets give you:
- A single id (
fleetId) plus a stable machine id (machineId) per member that you choose - Policy enforcement (CPU / memory / storage / accelerator caps, allowed drivers / images / templates, max spend, max concurrent creates) — checked client-side before sandboxes are provisioned
- Per-dispatch parallelism, retries, timeouts, idempotency, cancellation, and result buffering
- Shared workspace modes (
isolated,shared) with cross-machine snapshot, restore, and reconcile - Fleet-scoped telemetry: usage, cost estimate, trace bundle, embedded intelligence envelope, and full intelligence reports
- Scoped tokens for handing a fleet to a downstream service without leaking the parent API key
Create a fleet
There are two creation shapes. Use create when every machine is symmetric, and createWithCoordinator when one machine acts as orchestrator over a pool of workers.
// Symmetric fleet
const fleet = await client.fleets.create({
defaults: {
image: "python:3.12",
maxLifetimeSeconds: 60 * 60,
},
policy: {
maxMachines: 4,
maxConcurrentCreates: 2,
maxTotalCpu: 8,
maxTotalMemoryMb: 16_384,
maxSpendUsd: 5,
allowAccelerators: false,
},
machines: [
{ machineId: "worker-1", resources: { cpuCores: 2, memoryMB: 4096 } },
{ machineId: "worker-2", resources: { cpuCores: 2, memoryMB: 4096 } },
],
workspace: { mode: "isolated" },
metadata: { experiment: "react-19-bump" },
idempotencyKey: "exp-react-19-2025-05-18",
});
// Coordinator + workers
const cluster = await client.fleets.createWithCoordinator({
defaults: { image: "python:3.12" },
coordinator: { resources: { cpuCores: 1, memoryMB: 1024 } },
workers: [
{ machineId: "worker-1", resources: { cpuCores: 2, memoryMB: 4096 } },
{ machineId: "worker-2", resources: { cpuCores: 2, memoryMB: 4096 } },
],
});createWithCoordinator is sugar over create: it injects a coordinator machine with role: "coordinator" and tags the workers role: "worker" in metadata. After creation both shapes return a SandboxFleet instance.
Dispatch across machines
// Fire and collect — returns FleetExecDispatchResult[]
const results = await fleet.dispatchExec("pytest -q", {
machines: fleet.ids, // default: every machine
maxConcurrent: 2,
timeoutMs: 60_000,
retry: { attempts: 2, backoffMs: 1_000 },
dispatchId: "pytest-run-1", // idempotent — same id replays the same dispatch
});
// Detailed — returns the full response including dispatchId, durationMs,
// trace, and the embedded intelligence envelope (always present on dispatch).
const detailed = await fleet.dispatchExecDetailed("pytest -q");
console.log(detailed.intelligence.signals);
// Prompt variant — runs an agent prompt on each selected machine
const promptResults = await fleet.dispatchPrompt(
"Summarize what changed in this branch and why.",
{ machines: ["worker-1"], model: "anthropic/claude-sonnet-4-20250514" },
);
// Stream events as they happen
for await (const event of fleet.dispatchExecStream("npm test", {
machines: fleet.ids,
})) {
if (event.type === "result") console.log(event.data);
}
// Read previously-buffered results by dispatchId
const buffered = await fleet.dispatchResults("pytest-run-1", {
limit: 100,
machines: ["worker-1", "worker-2"],
});
// Cancel an in-flight dispatch
await fleet.cancelDispatch("pytest-run-1", "abandoning experiment");Single-machine helpers
When you want to talk to one machine in the fleet directly:
const result = await fleet.exec("worker-1", "echo hello");
const reply = await fleet.prompt("worker-1", "What is the structure?");
const box = await fleet.sandbox("worker-1"); // full SandboxInstanceWorkspace snapshots (shared workspace mode)
const snap = await fleet.createWorkspaceSnapshot();
await fleet.restoreWorkspaceSnapshot(snap.snapshotId);
await fleet.reconcileWorkspace();Dynamic topology
await fleet.attachMachine({
machineId: "worker-3",
sandboxId: "sbx_abc", // existing sandbox to bind into the fleet
role: "worker",
});
await fleet.detachMachine("worker-3");Artifacts, usage, cost, tokens
const artifacts = await fleet.collectArtifacts([
{ machineId: "worker-1", path: "/workspace/report.json", maxBytes: 1_000_000 },
]);
const usage = await fleet.usage(); // current usage rollup
const estimate = await fleet.cost(); // cost estimate
const manifest = await fleet.manifest(); // machine manifest as persisted
// Scoped token — hand to a downstream service without leaking the parent key
const token = await fleet.createToken({ expiresInSeconds: 3600 });Pre-flight cost estimate (without creating)
const preEstimate = await client.fleets.estimateCost({
defaults: { image: "python:3.12" },
policy: { maxMachines: 4, maxSpendUsd: 5 },
machines: [
{ machineId: "worker-1", resources: { cpuCores: 2, memoryMB: 4096 } },
{ machineId: "worker-2", resources: { cpuCores: 2, memoryMB: 4096 } },
],
});Operations
const caps = await client.fleets.capabilities(); // which drivers / templates / images
const ops = await client.fleets.operations(); // operations summary
const recon = await client.fleets.reconcile(); // reconcile drift
const reaped = await client.fleets.reapExpired(); // sweep expired fleetsLookup and delete
const found = await client.fleets.list({ fleetId: "fleet_abc" });
await client.fleets.delete("fleet_abc", { continueOnError: true });Single-shot batches without fleets
For one-shot, ad-hoc parallel work that does not need fleet-level policy / workspaces / dispatch buffering / intelligence reports, client.runBatch(tasks, options) is the simpler primitive. New code that needs more than one sandbox under one logical lifecycle should reach for fleets.
Intelligence Reports
The Intelligence Reports API generates structured post-hoc analyses over two subject types: a single sandbox or a single fleet. A fleet subject can optionally be narrowed to one dispatch within the fleet via subject.dispatchId. Two modes:
deterministic(default) — platform-side rule-based analysis. Free. Returns immediately or near-immediately. Surfaces lifecycle, runtime, plan-headroom, and resource-density signals derived directly from your trace evidence.agentic— runs the Tangle Trace Analyst, an LLM-driven reasoning loop, over your OTLP trace evidence. Returns findings with evidence references, recommended actions, and a validation plan. Billed againstbudget.maxUsd; the platform never spends past the budget you set. Async (returns a job; poll for terminal state).
The dedicated client lives on client.intelligence:
import { Sandbox } from "@tangle-network/sandbox";
import type { IntelligenceClient } from "@tangle-network/sandbox";
const client = new Sandbox({ apiKey, baseUrl });
const intelligence: IntelligenceClient = client.intelligence;Create a report
// Deterministic — over a single sandbox
const det = await client.intelligence.createReport({
subject: { type: "sandbox", id: box.id },
});
// Deterministic — over a fleet
await client.intelligence.createReport({
subject: { type: "fleet", id: fleet.fleetId },
});
// Deterministic — narrowed to one dispatch within a fleet
await client.intelligence.createReport({
subject: {
type: "fleet",
id: fleet.fleetId,
dispatchId: "pytest-run-1",
},
});
// Agentic — billed against the budget
const agentic = await client.intelligence.createReport({
subject: { type: "fleet", id: fleet.fleetId },
mode: "agentic",
budget: { billTo: "customer", maxUsd: 5 },
acknowledgeCost: true,
metadata: { experiment: "react-19-bump" },
});
// Shorthand for the agentic + budget pattern
const shortcut = await client.intelligence.createAgenticReport({
subject: { type: "sandbox", id: box.id },
maxUsd: 2,
});Poll a report to completion
Agentic reports return status: "pending" immediately. Either poll yourself with getReport, or use the built-in waitForReport:
const job = await client.intelligence.createAgenticReport({
subject: { type: "fleet", id: fleet.fleetId },
maxUsd: 5,
});
const completed = await client.intelligence.waitForReport(job.jobId, {
timeoutMs: 5 * 60 * 1000,
pollMs: 2_000,
});
if (completed.status === "completed") {
console.log(completed.findings);
console.log(completed.recommendedActions);
}List existing reports
const recent = await client.intelligence.listReports({
subjectType: "fleet",
subjectId: fleet.fleetId,
limit: 20,
});Per-subject shortcuts
SandboxInstance and SandboxFleet expose convenience wrappers so you don't have to thread subject manually:
await box.createIntelligenceReport({ mode: "deterministic" });
await box.createAgenticIntelligenceReport({ maxUsd: 2 });
await fleet.createIntelligenceReport({ mode: "deterministic" });
await fleet.createAgenticIntelligenceReport({ maxUsd: 5 });
// Fleet helpers accept the v2 refinement fields directly:
await fleet.createIntelligenceReport({
mode: "deterministic",
dispatchId: "pytest-run-1",
});Both wrappers post to POST /v1/intelligence/reports with the right subject filled in.
Time windows and baselines
Every report can be narrowed by a time window and compared against a same-type baseline. The analyzer rejects mixed-type comparisons because the delta would be meaningless.
// Bound the analysis to a one-hour window.
await fleet.createIntelligenceReport({
window: { since: Date.now() - 60 * 60 * 1000 },
});
// Compare two dispatches of the same fleet against each other.
await fleet.createIntelligenceReport({
dispatchId: "run-after",
compareTo: { type: "fleet", id: fleet.fleetId, dispatchId: "run-before" },
});
// Sandbox baseline.
await box.createIntelligenceReport({
compareTo: { type: "sandbox", id: previousBox.id },
});Cost before commit
Estimate cost without creating a report. Subject ownership is verified the same way as createReport, so the endpoint never becomes an existence oracle for foreign subjects.
const estimate = await client.intelligence.estimateReport({
subject: { type: "fleet", id: fleet.fleetId, dispatchId: "pytest-run-1" },
mode: "agentic",
});
console.log(`Would cost ${estimate.costUsd} USD (${estimate.reason})`);Error Handling
import {
AuthError,
NetworkError,
NotFoundError,
QuotaError,
StateError,
TimeoutError,
ValidationError,
} from "@tangle-network/sandbox";
try {
await box.exec("npm test");
} catch (err) {
if (err instanceof TimeoutError) {
console.log("Command timed out");
} else if (err instanceof StateError) {
console.log(`Invalid state: ${err.currentState}`);
} else if (err instanceof NetworkError) {
console.log("Connection failed");
}
}TypeScript
Full TypeScript support with exported types:
import type {
SandboxClientConfig,
CreateSandboxOptions,
SandboxInfo,
SandboxStatus,
SandboxConnection,
ExecResult,
ExecOptions,
PromptResult,
PromptOptions,
TaskResult,
TaskOptions,
SnapshotResult,
SnapshotOptions,
SnapshotInfo,
StorageConfig,
BatchTask,
BatchResult,
BatchOptions,
UsageInfo,
// Fleets
CreateSandboxFleetOptions,
CreateSandboxFleetWithCoordinatorOptions,
SandboxFleetMachineSpec,
SandboxFleetInfo,
SandboxFleetManifest,
SandboxFleetUsage,
SandboxFleetCostEstimate,
SandboxFleetToken,
SandboxFleetTraceBundle,
SandboxFleetTraceOptions,
SandboxFleetDispatchResponse,
FleetExecDispatchOptions,
FleetExecDispatchResult,
FleetPromptDispatchOptions,
FleetPromptDispatchResult,
FleetDispatchResultBuffer,
FleetDispatchResultBufferOptions,
FleetDispatchStreamOptions,
FleetDispatchCancelResult,
FleetMachineId,
// Intelligence Reports
IntelligenceReport,
IntelligenceReportBudget,
CreateIntelligenceReportOptions,
} from "@tangle-network/sandbox";
// Concrete classes — useful when you need to reference the type itself
import {
IntelligenceClient,
SandboxFleet,
SandboxFleetClient,
} from "@tangle-network/sandbox";License
MIT
