npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/sandbox

Quick 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 sessionId with TTL, and the SDK ships a browser/Worker-safe client that reconnects, replays missed events, and persists lastEventId across 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
                           failed

API 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 failed

Fleets

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 SandboxInstance

Workspace 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 fleets

Lookup 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 against budget.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