• Changelog
  • Introducing Sandbox Agent SDK: One API for Any Coding Agent
Promo Image

Building coding agents is hard. Claude Code has one API. Codex has another. OpenCode and Amp each do things differently. And even if you picked one, you’d face session state disappearing when processes crash, different integration code for every sandbox provider, and no way to stream transcripts back to your application.

Today we’re releasing the Sandbox Agent SDK to solve this.

The Problem

Fragmented APIs. Claude Code uses JSONL over stdout. Codex uses JSON-RPC. OpenCode runs an HTTP server with SSE. Each agent has its own event format, session model, and permission system. Swapping agents meant rewriting your entire integration.

Transient State. Agent transcripts live in the agent process. When it crashes, restarts, or finishes, your conversation history vanishes.

Deployment Chaos. Running agents in E2B required different code than Daytona. Vercel Sandboxes needed yet another approach.

The Solution

Any Coding Agent

Universal API to interact with Claude Code, Codex, OpenCode, and Amp with full feature coverage. Write one integration. Swap agents with a config change.

import { SandboxAgent } from "sandbox-agent";

const client = await SandboxAgent.start();

await client.createSession("my-session", {
  agent: "claude",  // or "codex", "opencode", "amp"
  permissionMode: "auto",
});

for await (const event of client.streamEvents("my-session")) {
  console.log(event.type, event.data);
}
TypeScript

Server or SDK Mode

Run as an HTTP server for language-agnostic access, or use the TypeScript SDK with embedded mode that spawns the daemon as a subprocess.

Universal Session Schema

Every agent event gets normalized into a consistent schema for storing and replaying transcripts:

  • session.started / session.ended - Session lifecycle
  • item.started / item.delta / item.completed - Messages and tool calls with streaming
  • question.requested / question.resolved - Human-in-the-loop questions
  • permission.requested / permission.resolved - Tool execution approvals
  • error - Structured errors

No more parsing five different event formats. No more agent-specific rendering logic.

Supports Your Sandbox Provider

The daemon runs in any environment that can execute a Linux binary: Daytona, E2B, Vercel Sandboxes, Docker, and more. One SDK. Same code. Any provider.

Lightweight, Portable Rust Binary

A ~15MB static binary with no runtime dependencies. Install anywhere with one curl command:

curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
sandbox-agent server --token "$SANDBOX_TOKEN"
Command Line

Automatic Agent Installation

Agents are installed on-demand when first used. No manual setup required.

Primitives That Pair With Rivet Actors

The Sandbox Agent SDK was designed to work seamlessly with Rivet Actors. Stream agent events directly to an actor for persistence, broadcast tool executions to connected clients in real-time, and coordinate multiple agents using actor patterns.

import { actor } from "rivetkit";
import { Daytona } from "@daytonaio/sdk";
import { SandboxAgent, SandboxAgentClient, AgentEvent } from "sandbox-agent";

interface CodingSessionState {
  sandboxId: string;
  baseUrl: string;
  sessionId: string;
  events: AgentEvent[];
}

interface CodingSessionVars {
  client: SandboxAgentClient;
}

const daytona = new Daytona();

const codingSession = actor({
  createState: async (): Promise<CodingSessionState> => {
	// Create sandbox
    const sandbox = await daytona.create({
      snapshot: "sandbox-agent-ready",
      envVars: {
        ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
        OPENAI_API_KEY: process.env.OPENAI_API_KEY,
      },
      autoStopInterval: 0,
    });

    // Start sandbox-agent server
    await sandbox.process.executeCommand(
      "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 &"
    );

    const baseUrl = (await sandbox.getSignedPreviewUrl(3000)).url;
    const sessionId = crypto.randomUUID();

    return {
      sandboxId: sandbox.id,
      baseUrl,
      sessionId,
      events: [],
    };
  },

  createVars: async (c): Promise<CodingSessionVars> => {
    const client = await SandboxAgent.connect({ baseUrl: c.state.baseUrl });
    await client.createSession(c.state.sessionId, { agent: "claude" });
    return { client };
  },

  onDestroy: async (c) => {
    const sandbox = await daytona.get(c.state.sandboxId);
    await sandbox.delete();
  },

  run: async (c) => {
    // Stream events and broadcast to connected clients
    for await (const event of c.vars.client.streamEvents(c.state.sessionId)) {
      c.state.events.push(event);
      c.broadcast("agentEvent", event);
    }
  },

  actions: {
    postMessage: async (c, message: string) => {
      await c.vars.client.postMessage(c.state.sessionId, message);
    },

    getTranscript: (c) => c.state.events,
  },
});
TypeScript

Connect from your frontend using the RivetKit client:

With Rivet Actors, your agent transcripts:

  • Persist automatically - State survives crashes, restarts, and process termination
  • Stream in real-time - Broadcast events to all connected clients as they happen
  • Replay on demand - Retrieve full session history for debugging or analysis
  • Scale horizontally - Run thousands of concurrent agent sessions across your infrastructure