---
image: true
author: nathan-flurry
published: "2026-01-28"
category: changelog
keywords: ["sandbox", "agent", "sdk", "claude", "codex", "opencode", "amp", "coding-agents", "ai"]
title: "Introducing Sandbox Agent SDK: One API for Any Coding Agent"
description: "A universal SDK that provides a single HTTP/SSE API for Claude Code, Codex, OpenCode, and Amp. Write one integration. Swap agents with a config change."
---

**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.

```typescript
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);
}
```

### 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:

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

### 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.

```typescript
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,
  },
});
```

Connect from your frontend using the RivetKit client:

<Tabs>
<Tab title="JavaScript">

```typescript
import { createClient } from "rivetkit/client";

const client = createClient();
const session = client.codingSession.getOrCreate(["my-session"]);

// Connect for real-time events
const conn = session.connect();
conn.on("agentEvent", (event) => {
  console.log(event.type, event.data);
});

// Send a message to the agent
await conn.postMessage("Create a new React component for user profiles");

// Get full transcript
const transcript = await conn.getTranscript();
```

</Tab>
<Tab title="React">

```tsx
import { createRivetKit } from "@rivetkit/react";

const { useActor } = createRivetKit();

function CodingSession() {
  const [messages, setMessages] = useState<AgentEvent[]>([]);
  const session = useActor({ name: "codingSession", key: ["my-session"] });

  session.useEvent("agentEvent", (event) => {
    setMessages((prev) => [...prev, event]);
  });

  const sendPrompt = async (prompt: string) => {
    await session.connection?.postMessage(prompt);
  };

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{JSON.stringify(msg)}</div>
      ))}
      <button onClick={() => sendPrompt("Build a login page")}>
        Send Prompt
      </button>
    </div>
  );
}
```

</Tab>
</Tabs>

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

