
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);
}
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 lifecycleitem.started/item.delta/item.completed- Messages and tool calls with streamingquestion.requested/question.resolved- Human-in-the-loop questionspermission.requested/permission.resolved- Tool execution approvalserror- 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"
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,
},
});
Connect from your frontend using the RivetKit client:
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();
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>
);
}
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