mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 08:03:46 +00:00
docs: documentation overhaul and universal schema reference (#10)
* remove website .astro * fix default origin * docs: comprehensive documentation overhaul - Add quickstart with multi-platform examples (E2B, Daytona, Docker, local) - Add environment variables setup with platform-specific tabs - Add Python SDK page (coming soon) - Add local deployment guide - Update E2B/Daytona/Docker guides with TypeScript examples - Configure OpenAPI auto-generation for API reference - Add CORS configuration guide - Update manage-sessions with Rivet Actors examples - Fix SDK method names and URLs throughout - Add icons to main documentation pages - Remove outdated universal-api and http-api pages * docs: add universal schema and agent compatibility docs - Create universal-schema.mdx with full event/item schema reference - Create agent-compatibility.mdx mirroring README feature matrix - Rename glossary.md to universal-schema.mdx - Update CLAUDE.md with sync requirements for new docs - Add links in README to building-chat-ui, manage-sessions, universal-schema - Fix CLI docs link (rivet.dev -> sandboxagent.dev) * docs: add inspector page and daytona network limits warning
This commit is contained in:
parent
a6f77f3008
commit
08d299a3ef
40 changed files with 1996 additions and 1004 deletions
|
|
@ -1,28 +1,96 @@
|
|||
---
|
||||
title: "Agent Compatibility"
|
||||
description: "Supported agents, install methods, and streaming formats."
|
||||
description: "Feature support across coding agents."
|
||||
icon: "table"
|
||||
---
|
||||
|
||||
## Compatibility matrix
|
||||
The universal API normalizes different coding agents into a consistent interface. Each agent has different native capabilities; the daemon fills gaps with synthetic events where possible.
|
||||
|
||||
| Agent | Provider | Binary | Install method | Session ID | Streaming format |
|
||||
|-------|----------|--------|----------------|------------|------------------|
|
||||
| Claude Code | Anthropic | `claude` | curl raw binary from GCS | `session_id` | JSONL via stdout |
|
||||
| Codex | OpenAI | `codex` | curl tarball from GitHub releases | `thread_id` | JSON-RPC over stdio |
|
||||
| OpenCode | Multi-provider | `opencode` | curl tarball from GitHub releases | `session_id` | SSE or JSONL |
|
||||
| Amp | Sourcegraph | `amp` | curl raw binary from GCS | `session_id` | JSONL via stdout |
|
||||
| Mock | Built-in | — | bundled | `mock-*` | daemon-generated |
|
||||
## Feature Matrix
|
||||
|
||||
## Agent modes
|
||||
| Feature | [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)* | [Codex](https://github.com/openai/codex) | [OpenCode](https://github.com/opencode-ai/opencode) | [Amp](https://ampcode.com) |
|
||||
|---------|:-----------:|:-----:|:--------:|:---:|
|
||||
| Stability | Stable | Stable | Experimental | Experimental |
|
||||
| Text Messages | ✓ | ✓ | ✓ | ✓ |
|
||||
| Tool Calls | —* | ✓ | ✓ | ✓ |
|
||||
| Tool Results | —* | ✓ | ✓ | ✓ |
|
||||
| Questions (HITL) | —* | | ✓ | |
|
||||
| Permissions (HITL) | —* | | ✓ | |
|
||||
| Images | | ✓ | ✓ | |
|
||||
| File Attachments | | ✓ | ✓ | |
|
||||
| Session Lifecycle | | ✓ | ✓ | |
|
||||
| Error Events | | ✓ | ✓ | ✓ |
|
||||
| Reasoning/Thinking | | ✓ | | |
|
||||
| Command Execution | | ✓ | | |
|
||||
| File Changes | | ✓ | | |
|
||||
| MCP Tools | | ✓ | | |
|
||||
| Streaming Deltas | | ✓ | ✓ | |
|
||||
|
||||
- **OpenCode**: discovered via the server API.
|
||||
- **Claude Code / Codex / Amp**: hardcoded modes (typically `build`, `plan`, or `custom`).
|
||||
\* Coming imminently
|
||||
|
||||
## Capability notes
|
||||
## Feature Descriptions
|
||||
|
||||
- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event (tests do not currently exercise Claude question/permission flows).
|
||||
- **Streaming**: all agents stream events; OpenCode uses SSE, Codex uses JSON-RPC over stdio, others use JSONL. Codex is currently normalized to thread/turn starts plus user/assistant completed items (deltas and tool/reasoning items are not emitted yet).
|
||||
- **User messages**: Claude CLI output does not include explicit user-message events in our snapshots, so only assistant messages are surfaced for Claude today.
|
||||
- **Files and images**: normalized via `UniversalMessagePart` with `File` and `Image` parts.
|
||||
### Text Messages
|
||||
|
||||
See [Universal API](/universal-api) for feature coverage details.
|
||||
Basic message exchange between user and assistant.
|
||||
|
||||
### Tool Calls & Results
|
||||
|
||||
Visibility into tool invocations (file reads, command execution, etc.) and their results. When not natively supported, tool activity is embedded in message content.
|
||||
|
||||
### Questions (HITL)
|
||||
|
||||
Interactive questions the agent asks the user. Emits `question.requested` and `question.resolved` events.
|
||||
|
||||
### Permissions (HITL)
|
||||
|
||||
Permission requests for sensitive operations. Emits `permission.requested` and `permission.resolved` events.
|
||||
|
||||
### Images
|
||||
|
||||
Support for image attachments in messages.
|
||||
|
||||
### File Attachments
|
||||
|
||||
Support for file attachments in messages.
|
||||
|
||||
### Session Lifecycle
|
||||
|
||||
Native `session.started` and `session.ended` events. When not supported, the daemon emits synthetic lifecycle events.
|
||||
|
||||
### Error Events
|
||||
|
||||
Structured error events for runtime failures.
|
||||
|
||||
### Reasoning/Thinking
|
||||
|
||||
Extended thinking or reasoning content with visibility controls.
|
||||
|
||||
### Command Execution
|
||||
|
||||
Detailed command execution events with stdout/stderr.
|
||||
|
||||
### File Changes
|
||||
|
||||
Structured file modification events with diffs.
|
||||
|
||||
### MCP Tools
|
||||
|
||||
Model Context Protocol tool support.
|
||||
|
||||
### Streaming Deltas
|
||||
|
||||
Native streaming of content deltas. When not supported, the daemon emits a single synthetic delta before `item.completed`.
|
||||
|
||||
## Synthetic Events
|
||||
|
||||
For features not natively supported, the daemon generates synthetic events to maintain a consistent event stream. Synthetic events have:
|
||||
|
||||
- `source: "daemon"`
|
||||
- `synthetic: true`
|
||||
|
||||
This lets you build UIs that work with any agent without special-casing each provider.
|
||||
|
||||
## Request Support
|
||||
|
||||
Want support for another agent? [Open an issue](https://github.com/rivet-dev/sandbox-agent/issues/new) to request it.
|
||||
|
|
|
|||
|
|
@ -1,350 +0,0 @@
|
|||
---
|
||||
title: "Architecture"
|
||||
description: "How the daemon, schemas, and agents fit together."
|
||||
---
|
||||
|
||||
Sandbox Agent SDK is built around a single daemon that runs inside the sandbox and exposes a universal HTTP API. Clients use the API (or the TypeScript SDK / CLI) to create sessions, send messages, and stream events.
|
||||
|
||||
## Components
|
||||
|
||||
- **Daemon**: Rust HTTP server that manages agent processes and streaming.
|
||||
- **Universal schema**: Shared input/output types for messages and events.
|
||||
- **SDKs & CLI**: Convenience wrappers around the HTTP API.
|
||||
|
||||
## Agent Schema Pipeline
|
||||
|
||||
The schema pipeline extracts type definitions from AI coding agents and converts them to a universal format.
|
||||
|
||||
### Schema Extraction
|
||||
|
||||
TypeScript extractors in `resources/agent-schemas/src/` pull schemas from each agent:
|
||||
|
||||
| Agent | Source | Extractor |
|
||||
|-------|--------|-----------|
|
||||
| Claude | `claude --output-format json --json-schema` | `claude.ts` |
|
||||
| Codex | `codex app-server generate-json-schema` | `codex.ts` |
|
||||
| OpenCode | GitHub OpenAPI spec | `opencode.ts` |
|
||||
| Amp | Scrapes ampcode.com docs | `amp.ts` |
|
||||
|
||||
All extractors include fallback schemas for when CLIs or URLs are unavailable.
|
||||
|
||||
**Output:** JSON schemas written to `resources/agent-schemas/artifacts/json-schema/`
|
||||
|
||||
### Rust Type Generation
|
||||
|
||||
The `server/packages/extracted-agent-schemas/` package generates Rust types at build time:
|
||||
|
||||
- `build.rs` reads JSON schemas and uses the `typify` crate to generate Rust structs
|
||||
- Generated code is written to `$OUT_DIR/{agent}.rs`
|
||||
- Types are exposed via `include!()` macros in `src/lib.rs`
|
||||
|
||||
```
|
||||
resources/agent-schemas/artifacts/json-schema/*.json
|
||||
↓ (build.rs + typify)
|
||||
$OUT_DIR/{claude,codex,opencode,amp}.rs
|
||||
↓ (include!)
|
||||
extracted_agent_schemas::{claude,codex,opencode,amp}::*
|
||||
```
|
||||
|
||||
### Universal Schema
|
||||
|
||||
The `server/packages/universal-agent-schema/` package defines agent-agnostic types:
|
||||
|
||||
**Core types** (`src/lib.rs`):
|
||||
- `UniversalEvent` - Wrapper with id, timestamp, session_id, agent, data
|
||||
- `UniversalEventData` - Enum: Message, Started, Error, QuestionAsked, PermissionAsked, Unknown
|
||||
- `UniversalMessage` - Parsed (role, parts, metadata) or Unparsed (raw JSON)
|
||||
- `UniversalMessagePart` - Text, ToolCall, ToolResult, FunctionCall, FunctionResult, File, Image, Error, Unknown
|
||||
|
||||
**Converters** (`src/agents/{claude,codex,opencode,amp}.rs`):
|
||||
- Each agent has a converter module that transforms native events to universal format
|
||||
- Conversions are best-effort; unparseable data preserved in `Unparsed` or `Unknown` variants
|
||||
|
||||
## Session Management
|
||||
|
||||
Sessions track agent conversations with in-memory state.
|
||||
|
||||
### Session Model
|
||||
|
||||
- **Session ID**: Client-provided primary session identifier.
|
||||
- **Agent session ID**: Underlying ID from the agent (thread/session). This is surfaced in events but is not the primary key.
|
||||
|
||||
### Storage
|
||||
|
||||
Sessions are stored in an in-memory `HashMap<String, SessionState>` inside `SessionManager`:
|
||||
|
||||
```rust
|
||||
struct SessionManager {
|
||||
sessions: Mutex<HashMap<String, SessionState>>,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
There is no disk persistence. Sessions are ephemeral and lost on server restart.
|
||||
|
||||
### SessionState
|
||||
|
||||
Each session tracks:
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `session_id` | Client-provided identifier |
|
||||
| `agent` | Agent type (Claude, Codex, OpenCode, Amp) |
|
||||
| `agent_mode` | Operating mode (build, plan, custom) |
|
||||
| `permission_mode` | Permission handling (default, plan, bypass) |
|
||||
| `model` | Optional model override |
|
||||
| `events: Vec<UniversalEvent>` | Full event history |
|
||||
| `pending_questions` | Question IDs awaiting reply |
|
||||
| `pending_permissions` | Permission IDs awaiting reply |
|
||||
| `broadcaster` | Tokio broadcast channel for SSE streaming |
|
||||
| `ended` | Whether agent process has terminated |
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```
|
||||
POST /v1/sessions/{sessionId} Create session, auto-install agent
|
||||
↓
|
||||
POST /v1/sessions/{id}/messages Spawn agent subprocess, stream output
|
||||
POST /v1/sessions/{id}/messages/stream Post and stream a single turn
|
||||
↓
|
||||
GET /v1/sessions/{id}/events Poll for new events (offset-based)
|
||||
GET /v1/sessions/{id}/events/sse Subscribe to SSE stream
|
||||
↓
|
||||
POST .../questions/{id}/reply Answer agent question
|
||||
POST .../permissions/{id}/reply Grant/deny permission request
|
||||
↓
|
||||
(agent process terminates) Session marked as ended
|
||||
```
|
||||
|
||||
### Event Streaming
|
||||
|
||||
- Events are stored in memory per session and assigned a monotonically increasing `id`.
|
||||
- `/events` returns a slice of events by offset/limit.
|
||||
- `/events/sse` streams new events from the same offset semantics.
|
||||
|
||||
When a message is sent:
|
||||
|
||||
1. `send_message()` spawns the agent CLI as a subprocess
|
||||
2. `consume_spawn()` reads stdout/stderr line by line
|
||||
3. Each JSON line is parsed and converted via `parse_agent_line()`
|
||||
4. Events are recorded via `record_event()` which:
|
||||
- Assigns incrementing event ID
|
||||
- Appends to `events` vector
|
||||
- Broadcasts to SSE subscribers
|
||||
|
||||
## Agent Execution
|
||||
|
||||
Each agent has a different execution model and communication pattern. There are two main architectural patterns:
|
||||
|
||||
### Architecture Patterns
|
||||
|
||||
**Subprocess Model (Claude, Amp):**
|
||||
- New process spawned per message/turn
|
||||
- Process terminates after turn completes
|
||||
- Multi-turn via CLI resume flags (`--resume`, `--continue`)
|
||||
- Simple but has process spawn overhead
|
||||
|
||||
**Client/Server Model (OpenCode, Codex):**
|
||||
- Single long-running server process
|
||||
- Multiple sessions/threads multiplexed via RPC
|
||||
- Multi-turn via server-side thread persistence
|
||||
- More efficient for repeated interactions
|
||||
|
||||
### Overview
|
||||
|
||||
| Agent | Architecture | Binary Source | Multi-Turn Method |
|
||||
|-------|--------------|---------------|-------------------|
|
||||
| Claude Code | Subprocess (per-turn) | GCS (Anthropic) | `--resume` flag |
|
||||
| Codex | **Shared Server (JSON-RPC)** | GitHub releases | **Thread persistence** |
|
||||
| OpenCode | HTTP Server (SSE) | GitHub releases | Server-side sessions |
|
||||
| Amp | Subprocess (per-turn) | GCS (Amp) | `--continue` flag |
|
||||
|
||||
### Claude Code
|
||||
|
||||
Spawned as a subprocess with JSONL streaming:
|
||||
|
||||
```bash
|
||||
claude --print --output-format stream-json --verbose \
|
||||
[--model MODEL] [--resume SESSION_ID] \
|
||||
[--permission-mode plan | --dangerously-skip-permissions] \
|
||||
PROMPT
|
||||
```
|
||||
|
||||
- Streams JSON events to stdout, one per line
|
||||
- Supports session resumption via `--resume`
|
||||
- Permission modes: `--permission-mode plan` for approval workflow, `--dangerously-skip-permissions` for bypass
|
||||
|
||||
### Codex
|
||||
|
||||
Uses a **shared app-server process** that handles multiple sessions via JSON-RPC over stdio:
|
||||
|
||||
```bash
|
||||
codex app-server
|
||||
```
|
||||
|
||||
**Daemon flow:**
|
||||
1. First Codex session triggers `codex app-server` spawn
|
||||
2. Performs `initialize` / `initialized` handshake
|
||||
3. Each session creation sends `thread/start` → receives `thread_id`
|
||||
4. Messages sent via `turn/start` with `thread_id`
|
||||
5. Notifications routed back to session by `thread_id`
|
||||
|
||||
**Key characteristics:**
|
||||
- Single process handles all Codex sessions
|
||||
- JSON-RPC over stdio (JSONL format)
|
||||
- Thread IDs map to daemon session IDs
|
||||
- Approval requests arrive as server-to-client JSON-RPC requests
|
||||
- Process lifetime matches daemon lifetime (not per-turn)
|
||||
|
||||
### OpenCode
|
||||
|
||||
Unique architecture - runs as a **persistent HTTP server** rather than per-message subprocess:
|
||||
|
||||
```bash
|
||||
opencode serve --port {4200-4300}
|
||||
```
|
||||
|
||||
Then communicates via HTTP endpoints:
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `POST /session` | Create new session |
|
||||
| `POST /session/{id}/prompt` | Send message |
|
||||
| `GET /event/subscribe` | SSE event stream |
|
||||
| `POST /question/reply` | Answer HITL question |
|
||||
| `POST /permission/reply` | Grant/deny permission |
|
||||
|
||||
The server is started once and reused across sessions. Events are received via Server-Sent Events (SSE) subscription.
|
||||
|
||||
### Amp
|
||||
|
||||
Spawned as a subprocess with dynamic flag detection:
|
||||
|
||||
```bash
|
||||
amp [--execute|--print] [--output-format stream-json] \
|
||||
[--model MODEL] [--continue SESSION_ID] \
|
||||
[--dangerously-skip-permissions] PROMPT
|
||||
```
|
||||
|
||||
- **Dynamic flag detection**: Probes `--help` output to determine which flags the installed version supports
|
||||
- **Fallback strategy**: If execution fails, retries with progressively simpler flag combinations
|
||||
- Streams JSON events to stdout
|
||||
- Supports session continuation via `--continue`
|
||||
|
||||
### Communication Patterns
|
||||
|
||||
**Per-turn subprocess agents (Claude, Amp):**
|
||||
1. Agent CLI spawned with appropriate flags
|
||||
2. Stdout/stderr read line-by-line
|
||||
3. Each line parsed as JSON
|
||||
4. Events converted via `parse_agent_line()` → agent-specific converter
|
||||
5. Universal events recorded and broadcast to SSE subscribers
|
||||
6. Process terminated on turn completion
|
||||
|
||||
**Shared stdio server agent (Codex):**
|
||||
1. Single `codex app-server` process started on first session
|
||||
2. `initialize`/`initialized` handshake performed once
|
||||
3. New sessions send `thread/start`, receive `thread_id`
|
||||
4. Messages sent via `turn/start` with `thread_id`
|
||||
5. Notifications read from stdout, routed by `thread_id`
|
||||
6. Process persists across sessions and turns
|
||||
|
||||
**HTTP server agent (OpenCode):**
|
||||
1. Server started on available port (if not running)
|
||||
2. Session created via HTTP POST
|
||||
3. Prompts sent via HTTP POST
|
||||
4. Events received via SSE subscription
|
||||
5. HITL responses forwarded via HTTP POST
|
||||
|
||||
### Credential Handling
|
||||
|
||||
All agents receive API keys via environment variables:
|
||||
|
||||
| Agent | Environment Variables |
|
||||
|-------|----------------------|
|
||||
| Claude | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` |
|
||||
| Codex | `OPENAI_API_KEY`, `CODEX_API_KEY` |
|
||||
| OpenCode | `OPENAI_API_KEY` |
|
||||
| Amp | `ANTHROPIC_API_KEY` |
|
||||
|
||||
## Human-in-the-Loop
|
||||
|
||||
Questions and permission prompts are normalized into the universal schema:
|
||||
|
||||
- Question events surface as `questionAsked` with selectable options.
|
||||
- Permission events surface as `permissionAsked` with `reply: once | always | reject`.
|
||||
- Claude plan approval is normalized into a question event (approve/reject).
|
||||
|
||||
## SDK Modes
|
||||
|
||||
The TypeScript SDK supports two connection modes.
|
||||
|
||||
### Embedded Mode
|
||||
|
||||
Defined in `sdks/typescript/src/spawn.ts`:
|
||||
|
||||
1. **Binary resolution**: Checks `SANDBOX_AGENT_BIN` env, then platform-specific npm package, then `PATH`
|
||||
2. **Port selection**: Uses provided port or finds a free one via `net.createServer()`
|
||||
3. **Token generation**: Uses provided token or generates random 24-byte hex string
|
||||
4. **Spawn**: Launches `sandbox-agent server --host <host> --port <port> --token <token>`
|
||||
5. **Health wait**: Polls `GET /v1/health` until server is ready (up to 15s timeout)
|
||||
6. **Cleanup**: On dispose, sends SIGTERM then SIGKILL if needed; also registers process exit handlers
|
||||
|
||||
```typescript
|
||||
const handle = await spawnSandboxAgent({ log: "inherit" });
|
||||
// handle.baseUrl = "http://127.0.0.1:<port>"
|
||||
// handle.token = "<generated>"
|
||||
// handle.dispose() to cleanup
|
||||
```
|
||||
|
||||
### Server Mode
|
||||
|
||||
Defined in `sdks/typescript/src/client.ts`:
|
||||
|
||||
- Direct HTTP client to a remote `sandbox-agent` server
|
||||
- Uses provided `baseUrl` and optional `token`
|
||||
- No subprocess management
|
||||
|
||||
```typescript
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://remote-server:8080",
|
||||
token: "secret",
|
||||
});
|
||||
```
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
`SandboxAgent` provides two factory methods:
|
||||
|
||||
```typescript
|
||||
// Connect to existing server
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://remote:8080",
|
||||
});
|
||||
|
||||
// Start embedded subprocess
|
||||
const client = await SandboxAgent.start();
|
||||
|
||||
// With options
|
||||
const client = await SandboxAgent.start({
|
||||
spawn: { port: 9000 },
|
||||
});
|
||||
```
|
||||
|
||||
The `spawn` option can be:
|
||||
- `true` / `false` - Enable/disable embedded mode
|
||||
- `SandboxAgentSpawnOptions` - Fine-grained control over host, port, token, binary path, timeout, logging
|
||||
|
||||
## Authentication
|
||||
|
||||
The daemon uses a **global token** configured at startup. All HTTP and CLI operations reuse the same token and are validated against the `Authorization` header (`Bearer` or `Token`).
|
||||
|
||||
## Key Files
|
||||
|
||||
| Component | Path |
|
||||
|-----------|------|
|
||||
| Agent spawn/install | `server/packages/agent-management/src/agents.rs` |
|
||||
| Session routing | `server/packages/sandbox-agent/src/router.rs` |
|
||||
| Event converters | `server/packages/universal-agent-schema/src/agents/*.rs` |
|
||||
| Schema extractors | `resources/agent-schemas/src/*.ts` |
|
||||
| TypeScript SDK | `sdks/typescript/src/` |
|
||||
|
|
@ -1,57 +1,76 @@
|
|||
---
|
||||
title: "Building a Chat UI"
|
||||
description: "Design a client that renders universal session events consistently across providers."
|
||||
description: "Build a chat interface using the universal event stream."
|
||||
icon: "comments"
|
||||
---
|
||||
|
||||
This guide explains how to build a chat UI that works across all agents using the universal event
|
||||
stream.
|
||||
## Setup
|
||||
|
||||
## High-level flow
|
||||
### List agents
|
||||
|
||||
1. List agents and read their capabilities.
|
||||
2. Create a session for the selected agent.
|
||||
3. Send user messages.
|
||||
4. Subscribe to events (polling or SSE).
|
||||
5. Render items and deltas into a stable message timeline.
|
||||
```ts
|
||||
const { agents } = await client.listAgents();
|
||||
|
||||
## Use agent capabilities
|
||||
// Each agent has capabilities that determine what UI to show
|
||||
const claude = agents.find((a) => a.id === "claude");
|
||||
if (claude?.capabilities.permissions) {
|
||||
// Show permission approval UI
|
||||
}
|
||||
if (claude?.capabilities.questions) {
|
||||
// Show question response UI
|
||||
}
|
||||
```
|
||||
|
||||
Capabilities tell you which features are supported for the selected agent:
|
||||
### Create a session
|
||||
|
||||
- `tool_calls` and `tool_results` indicate tool execution events.
|
||||
- `questions` and `permissions` indicate HITL flows.
|
||||
- `plan_mode` indicates that the agent supports plan-only execution.
|
||||
- `reasoning` and `status` indicate that the agent can emit reasoning/status content parts.
|
||||
- `item_started` indicates that the agent emits `item.started` on its own; when false the daemon will emit a synthetic `item.started` immediately after sending a user message.
|
||||
```ts
|
||||
const sessionId = `session-${crypto.randomUUID()}`;
|
||||
|
||||
Use these to enable or disable UI affordances (tool panels, approval buttons, etc.).
|
||||
await client.createSession(sessionId, {
|
||||
agent: "claude",
|
||||
agentMode: "code", // Optional: agent-specific mode
|
||||
permissionMode: "default", // Optional: "default" | "plan" | "bypass"
|
||||
model: "claude-sonnet-4", // Optional: model override
|
||||
});
|
||||
```
|
||||
|
||||
## Event model
|
||||
### Send a message
|
||||
|
||||
Every event includes:
|
||||
```ts
|
||||
await client.postMessage(sessionId, { message: "Hello, world!" });
|
||||
```
|
||||
|
||||
- `event_id`, `sequence`, and `time` for ordering.
|
||||
- `session_id` for the universal session.
|
||||
- `native_session_id` for provider-specific debugging.
|
||||
- `type` with one of:
|
||||
- `session.started`, `session.ended`
|
||||
- `item.started`, `item.delta`, `item.completed`
|
||||
- `permission.requested`, `permission.resolved`
|
||||
- `question.requested`, `question.resolved`
|
||||
- `error`, `agent.unparsed`
|
||||
- `data` which holds the payload for the event type.
|
||||
- `synthetic` and `source` to show daemon-generated events.
|
||||
- `raw` (optional) when `include_raw=true`.
|
||||
### Stream events
|
||||
|
||||
## Rendering items
|
||||
Three options for receiving events:
|
||||
|
||||
Items are emitted in three phases:
|
||||
```ts
|
||||
// Option 1: SSE (recommended for real-time UI)
|
||||
const stream = client.streamEvents(sessionId, { offset: 0 });
|
||||
for await (const event of stream) {
|
||||
handleEvent(event);
|
||||
}
|
||||
|
||||
- `item.started`: first snapshot of a message or tool item.
|
||||
- `item.delta`: incremental updates (token streaming or synthetic deltas).
|
||||
- `item.completed`: final snapshot.
|
||||
// Option 2: Polling
|
||||
const { events, hasMore } = await client.getEvents(sessionId, { offset: 0 });
|
||||
events.forEach(handleEvent);
|
||||
|
||||
Recommended render flow:
|
||||
// Option 3: Turn streaming (send + stream in one call)
|
||||
const stream = client.streamTurn(sessionId, { message: "Hello" });
|
||||
for await (const event of stream) {
|
||||
handleEvent(event);
|
||||
}
|
||||
```
|
||||
|
||||
Use `offset` to track the last seen `sequence` number and resume from where you left off.
|
||||
|
||||
---
|
||||
|
||||
## Handling Events
|
||||
|
||||
### Bare minimum
|
||||
|
||||
Handle these three events to render a basic chat:
|
||||
|
||||
```ts
|
||||
type ItemState = {
|
||||
|
|
@ -60,108 +79,278 @@ type ItemState = {
|
|||
};
|
||||
|
||||
const items = new Map<string, ItemState>();
|
||||
const order: string[] = [];
|
||||
|
||||
function applyEvent(event: UniversalEvent) {
|
||||
if (event.type === "item.started") {
|
||||
const item = event.data.item;
|
||||
items.set(item.item_id, { item, deltas: [] });
|
||||
order.push(item.item_id);
|
||||
}
|
||||
|
||||
if (event.type === "item.delta") {
|
||||
const { item_id, delta } = event.data;
|
||||
const state = items.get(item_id);
|
||||
if (state) {
|
||||
state.deltas.push(delta);
|
||||
function handleEvent(event: UniversalEvent) {
|
||||
switch (event.type) {
|
||||
case "item.started": {
|
||||
const { item } = event.data as ItemEventData;
|
||||
items.set(item.item_id, { item, deltas: [] });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "item.completed") {
|
||||
const item = event.data.item;
|
||||
const state = items.get(item.item_id);
|
||||
if (state) {
|
||||
state.item = item;
|
||||
case "item.delta": {
|
||||
const { item_id, delta } = event.data as ItemDeltaData;
|
||||
const state = items.get(item_id);
|
||||
if (state) {
|
||||
state.deltas.push(delta);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "item.completed": {
|
||||
const { item } = event.data as ItemEventData;
|
||||
const state = items.get(item.item_id);
|
||||
if (state) {
|
||||
state.item = item;
|
||||
state.deltas = []; // Clear deltas, use final content
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When rendering, combine the item content with accumulated deltas. If you receive a delta before a
|
||||
started event (should not happen), treat it as an error.
|
||||
When rendering, show a loading indicator while `item.status === "in_progress"`:
|
||||
|
||||
## Content parts
|
||||
```ts
|
||||
function renderItem(state: ItemState) {
|
||||
const { item, deltas } = state;
|
||||
const isLoading = item.status === "in_progress";
|
||||
|
||||
Each `UniversalItem` has `content` parts. Your UI can branch on `part.type`:
|
||||
// For streaming text, combine item content with accumulated deltas
|
||||
const text = item.content
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("");
|
||||
const streamedText = text + deltas.join("");
|
||||
|
||||
- `text` for normal chat text.
|
||||
- `tool_call` and `tool_result` for tool execution.
|
||||
- `file_ref` for file read/write/patch previews.
|
||||
- `reasoning` if you display public reasoning text.
|
||||
- `status` for progress updates.
|
||||
- `image` for image outputs.
|
||||
|
||||
Treat `item.kind` as the primary layout decision (message vs tool call vs system), and use content
|
||||
parts for the detailed rendering.
|
||||
|
||||
## Questions and permissions
|
||||
|
||||
Question and permission events are out-of-band from item flow. Render them as modal or inline UI
|
||||
blocks that must be resolved via:
|
||||
|
||||
- `POST /v1/sessions/{session_id}/questions/{question_id}/reply`
|
||||
- `POST /v1/sessions/{session_id}/questions/{question_id}/reject`
|
||||
- `POST /v1/sessions/{session_id}/permissions/{permission_id}/reply`
|
||||
|
||||
If an agent does not advertise these capabilities, keep those UI controls hidden.
|
||||
|
||||
## Error and unparsed events
|
||||
|
||||
- `error` events are structured failures from the daemon or agent.
|
||||
- `agent.unparsed` indicates the provider emitted something the converter could not parse.
|
||||
|
||||
Treat `agent.unparsed` as a hard failure in development so you can fix converters quickly.
|
||||
|
||||
## Event ordering
|
||||
|
||||
Prefer `sequence` for ordering. It is monotonic for a given session. The `time` field is for
|
||||
timestamps, not ordering.
|
||||
|
||||
## Handling session end
|
||||
|
||||
`session.ended` includes the reason and who terminated it. Disable input after a terminal event.
|
||||
|
||||
## Optional raw payloads
|
||||
|
||||
If you need provider-level debugging, pass `include_raw=true` when streaming or polling events
|
||||
(including one-turn streams) to receive the `raw` payload for each event.
|
||||
|
||||
## SSE vs polling vs turn streaming
|
||||
|
||||
- SSE gives low-latency updates and simplifies streaming UIs.
|
||||
- Polling is simpler to debug and works in any environment.
|
||||
- Turn streaming (`POST /v1/sessions/{session_id}/messages/stream`) is a one-shot stream tied to a
|
||||
single prompt. The stream closes automatically once the turn completes.
|
||||
|
||||
Both yield the same event payloads.
|
||||
|
||||
## Mock agent for UI testing
|
||||
|
||||
Use the built-in `mock` agent to exercise UI behaviors without external credentials:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:2468/v1/sessions/demo-session \
|
||||
-H "content-type: application/json" \
|
||||
-d '{"agent":"mock"}'
|
||||
return {
|
||||
content: streamedText,
|
||||
isLoading,
|
||||
role: item.role,
|
||||
kind: item.kind,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The mock agent sends a prompt telling you what commands it accepts. Send messages like `demo`,
|
||||
`markdown`, or `permission` to emit specific event sequences. Any other text is echoed back as an
|
||||
assistant message so you can test rendering, streaming, and approval flows on demand.
|
||||
### Extra events
|
||||
|
||||
## Reference implementation
|
||||
Handle these for a complete implementation:
|
||||
|
||||
The [Inspector chat UI](https://github.com/rivet-dev/sandbox-agent/blob/main/frontend/packages/inspector/src/App.tsx)
|
||||
is a complete reference implementation showing how to build a chat interface using the universal event
|
||||
stream. It demonstrates session management, event rendering, item lifecycle handling, and HITL approval
|
||||
flows.
|
||||
```ts
|
||||
function handleEvent(event: UniversalEvent) {
|
||||
switch (event.type) {
|
||||
// ... bare minimum events above ...
|
||||
|
||||
case "session.started": {
|
||||
// Session is ready
|
||||
break;
|
||||
}
|
||||
|
||||
case "session.ended": {
|
||||
const { reason, terminated_by } = event.data as SessionEndedData;
|
||||
// Disable input, show end reason
|
||||
// reason: "completed" | "error" | "terminated"
|
||||
// terminated_by: "agent" | "daemon"
|
||||
break;
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const { message, code } = event.data as ErrorData;
|
||||
// Display error to user
|
||||
break;
|
||||
}
|
||||
|
||||
case "agent.unparsed": {
|
||||
const { error, location } = event.data as AgentUnparsedData;
|
||||
// Parsing failure - treat as bug in development
|
||||
console.error(`Parse error at ${location}: ${error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Content parts
|
||||
|
||||
Each item has `content` parts. Render based on `type`:
|
||||
|
||||
```ts
|
||||
function renderContentPart(part: ContentPart) {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return <Markdown>{part.text}</Markdown>;
|
||||
|
||||
case "tool_call":
|
||||
return <ToolCall name={part.name} args={part.arguments} />;
|
||||
|
||||
case "tool_result":
|
||||
return <ToolResult output={part.output} />;
|
||||
|
||||
case "file_ref":
|
||||
return <FileChange path={part.path} action={part.action} diff={part.diff} />;
|
||||
|
||||
case "reasoning":
|
||||
return <Reasoning>{part.text}</Reasoning>;
|
||||
|
||||
case "status":
|
||||
return <Status label={part.label} detail={part.detail} />;
|
||||
|
||||
case "image":
|
||||
return <Image src={part.path} />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handling Permissions
|
||||
|
||||
When `permission.requested` arrives, show an approval UI:
|
||||
|
||||
```ts
|
||||
const pendingPermissions = new Map<string, PermissionEventData>();
|
||||
|
||||
function handleEvent(event: UniversalEvent) {
|
||||
if (event.type === "permission.requested") {
|
||||
const data = event.data as PermissionEventData;
|
||||
pendingPermissions.set(data.permission_id, data);
|
||||
}
|
||||
|
||||
if (event.type === "permission.resolved") {
|
||||
const data = event.data as PermissionEventData;
|
||||
pendingPermissions.delete(data.permission_id);
|
||||
}
|
||||
}
|
||||
|
||||
// User clicks approve/deny
|
||||
async function replyPermission(id: string, reply: "once" | "always" | "reject") {
|
||||
await client.replyPermission(sessionId, id, { reply });
|
||||
pendingPermissions.delete(id);
|
||||
}
|
||||
```
|
||||
|
||||
Render permission requests:
|
||||
|
||||
```ts
|
||||
function PermissionRequest({ data }: { data: PermissionEventData }) {
|
||||
return (
|
||||
<div>
|
||||
<p>Allow: {data.action}</p>
|
||||
<button onClick={() => replyPermission(data.permission_id, "once")}>
|
||||
Allow Once
|
||||
</button>
|
||||
<button onClick={() => replyPermission(data.permission_id, "always")}>
|
||||
Always Allow
|
||||
</button>
|
||||
<button onClick={() => replyPermission(data.permission_id, "reject")}>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handling Questions
|
||||
|
||||
When `question.requested` arrives, show a selection UI:
|
||||
|
||||
```ts
|
||||
const pendingQuestions = new Map<string, QuestionEventData>();
|
||||
|
||||
function handleEvent(event: UniversalEvent) {
|
||||
if (event.type === "question.requested") {
|
||||
const data = event.data as QuestionEventData;
|
||||
pendingQuestions.set(data.question_id, data);
|
||||
}
|
||||
|
||||
if (event.type === "question.resolved") {
|
||||
const data = event.data as QuestionEventData;
|
||||
pendingQuestions.delete(data.question_id);
|
||||
}
|
||||
}
|
||||
|
||||
// User selects answer(s)
|
||||
async function answerQuestion(id: string, answers: string[][]) {
|
||||
await client.replyQuestion(sessionId, id, { answers });
|
||||
pendingQuestions.delete(id);
|
||||
}
|
||||
|
||||
async function rejectQuestion(id: string) {
|
||||
await client.rejectQuestion(sessionId, id);
|
||||
pendingQuestions.delete(id);
|
||||
}
|
||||
```
|
||||
|
||||
Render question requests:
|
||||
|
||||
```ts
|
||||
function QuestionRequest({ data }: { data: QuestionEventData }) {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{data.prompt}</p>
|
||||
{data.options.map((option) => (
|
||||
<label key={option}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(option)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelected([...selected, option]);
|
||||
} else {
|
||||
setSelected(selected.filter((s) => s !== option));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
<button onClick={() => answerQuestion(data.question_id, [selected])}>
|
||||
Submit
|
||||
</button>
|
||||
<button onClick={() => rejectQuestion(data.question_id)}>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing with Mock Agent
|
||||
|
||||
The `mock` agent lets you test UI behaviors without external credentials:
|
||||
|
||||
```ts
|
||||
await client.createSession("test-session", { agent: "mock" });
|
||||
```
|
||||
|
||||
Send `help` to see available commands:
|
||||
|
||||
| Command | Tests |
|
||||
|---------|-------|
|
||||
| `help` | Lists all commands |
|
||||
| `demo` | Full UI coverage sequence with markers |
|
||||
| `markdown` | Streaming markdown rendering |
|
||||
| `tool` | Tool call + result with file refs |
|
||||
| `status` | Status item updates |
|
||||
| `image` | Image content part |
|
||||
| `permission` | Permission request flow |
|
||||
| `question` | Question request flow |
|
||||
| `error` | Error + unparsed events |
|
||||
| `end` | Session ended event |
|
||||
| `echo <text>` | Echo text as assistant message |
|
||||
|
||||
Any unrecognized text is echoed back as an assistant message.
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
The [Inspector UI](https://github.com/rivet-dev/sandbox-agent/blob/main/frontend/packages/inspector/src/App.tsx)
|
||||
is a complete reference showing session management, event rendering, and HITL flows.
|
||||
|
|
|
|||
318
docs/cli.mdx
318
docs/cli.mdx
|
|
@ -1,140 +1,310 @@
|
|||
---
|
||||
title: "CLI"
|
||||
description: "CLI reference and server flags."
|
||||
title: "CLI Reference"
|
||||
description: "Complete CLI reference for sandbox-agent."
|
||||
sidebarTitle: "CLI"
|
||||
icon: "terminal"
|
||||
---
|
||||
|
||||
The `sandbox-agent api` subcommand mirrors the HTTP API so you can script everything without writing client code.
|
||||
## Server
|
||||
|
||||
## Server flags
|
||||
Start the HTTP server:
|
||||
|
||||
```bash
|
||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
sandbox-agent server [OPTIONS]
|
||||
```
|
||||
|
||||
- `--token`: global token for all requests.
|
||||
- `--no-token`: disable auth (local dev only).
|
||||
- `--host`, `--port`: bind address.
|
||||
- `--cors-allow-origin`, `--cors-allow-method`, `--cors-allow-header`, `--cors-allow-credentials`: configure CORS.
|
||||
- `--no-telemetry`: disable anonymous telemetry.
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-t, --token <TOKEN>` | - | Authentication token for all requests |
|
||||
| `-n, --no-token` | - | Disable authentication (local dev only) |
|
||||
| `-H, --host <HOST>` | `127.0.0.1` | Host to bind to |
|
||||
| `-p, --port <PORT>` | `2468` | Port to bind to |
|
||||
| `-O, --cors-allow-origin <ORIGIN>` | - | CORS allowed origin (repeatable) |
|
||||
| `-M, --cors-allow-method <METHOD>` | - | CORS allowed method (repeatable) |
|
||||
| `-A, --cors-allow-header <HEADER>` | - | CORS allowed header (repeatable) |
|
||||
| `-C, --cors-allow-credentials` | - | Enable CORS credentials |
|
||||
| `--no-telemetry` | - | Disable anonymous telemetry |
|
||||
|
||||
## Install agent (no server required)
|
||||
```bash
|
||||
sandbox-agent server --token "$TOKEN" --port 3000
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>install-agent</strong></summary>
|
||||
---
|
||||
|
||||
## Install Agent (Local)
|
||||
|
||||
Install an agent without running the server:
|
||||
|
||||
```bash
|
||||
sandbox-agent install-agent <AGENT> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-r, --reinstall` | Force reinstall even if already installed |
|
||||
|
||||
```bash
|
||||
sandbox-agent install-agent claude --reinstall
|
||||
```
|
||||
</details>
|
||||
|
||||
## API agent commands
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><strong>api agents list</strong></summary>
|
||||
## Credentials
|
||||
|
||||
### Extract
|
||||
|
||||
Extract locally discovered credentials:
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents list --endpoint http://127.0.0.1:2468
|
||||
sandbox-agent credentials extract [OPTIONS]
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api agents install</strong></summary>
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-a, --agent <AGENT>` | Filter by agent (`claude`, `codex`, `opencode`, `amp`) |
|
||||
| `-p, --provider <PROVIDER>` | Filter by provider (`anthropic`, `openai`) |
|
||||
| `-d, --home-dir <DIR>` | Custom home directory for credential search |
|
||||
| `-r, --reveal` | Show full credential values (default: redacted) |
|
||||
| `--no-oauth` | Exclude OAuth credentials |
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents install claude --reinstall --endpoint http://127.0.0.1:2468
|
||||
sandbox-agent credentials extract --agent claude --reveal
|
||||
sandbox-agent credentials extract --provider anthropic
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api agents modes</strong></summary>
|
||||
### Extract as Environment Variables
|
||||
|
||||
Output credentials as shell environment variables:
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents modes claude --endpoint http://127.0.0.1:2468
|
||||
sandbox-agent credentials extract-env [OPTIONS]
|
||||
```
|
||||
</details>
|
||||
|
||||
## API session commands
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions list</strong></summary>
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-e, --export` | Prefix each line with `export` |
|
||||
| `-d, --home-dir <DIR>` | Custom home directory for credential search |
|
||||
| `--no-oauth` | Exclude OAuth credentials |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions list --endpoint http://127.0.0.1:2468
|
||||
# Source directly into shell
|
||||
eval "$(sandbox-agent credentials extract-env --export)"
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions create</strong></summary>
|
||||
---
|
||||
|
||||
## API Commands
|
||||
|
||||
The `sandbox-agent api` subcommand mirrors the HTTP API for scripting without client code.
|
||||
|
||||
All API commands support:
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-e, --endpoint <URL>` | `http://127.0.0.1:2468` | API endpoint |
|
||||
| `-t, --token <TOKEN>` | - | Authentication token |
|
||||
|
||||
---
|
||||
|
||||
### Agents
|
||||
|
||||
#### List Agents
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents list
|
||||
```
|
||||
|
||||
#### Install Agent
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents install <AGENT> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-r, --reinstall` | Force reinstall |
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents install claude --reinstall
|
||||
```
|
||||
|
||||
#### Get Agent Modes
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents modes <AGENT>
|
||||
```
|
||||
|
||||
```bash
|
||||
sandbox-agent api agents modes claude
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sessions
|
||||
|
||||
#### List Sessions
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions list
|
||||
```
|
||||
|
||||
#### Create Session
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions create <SESSION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-a, --agent <AGENT>` | Agent identifier (required) |
|
||||
| `-g, --agent-mode <MODE>` | Agent mode |
|
||||
| `-p, --permission-mode <MODE>` | Permission mode (`default`, `plan`, `bypass`) |
|
||||
| `-m, --model <MODEL>` | Model override |
|
||||
| `-v, --variant <VARIANT>` | Model variant |
|
||||
| `-A, --agent-version <VERSION>` | Agent version |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions create my-session \
|
||||
--agent claude \
|
||||
--agent-mode build \
|
||||
--permission-mode default \
|
||||
--endpoint http://127.0.0.1:2468
|
||||
--agent-mode code \
|
||||
--permission-mode default
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions send-message</strong></summary>
|
||||
#### Send Message
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions send-message <SESSION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-m, --message <TEXT>` | Message text (required) |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions send-message my-session \
|
||||
--message "Summarize the repository" \
|
||||
--endpoint http://127.0.0.1:2468
|
||||
--message "Summarize the repository"
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions send-message-stream</strong></summary>
|
||||
#### Send Message (Streaming)
|
||||
|
||||
Send a message and stream the response:
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions send-message-stream <SESSION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-m, --message <TEXT>` | Message text (required) |
|
||||
| `--include-raw` | Include raw agent data |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions send-message-stream my-session \
|
||||
--message "Summarize the repository" \
|
||||
--endpoint http://127.0.0.1:2468
|
||||
--message "Help me debug this"
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions events</strong></summary>
|
||||
#### Terminate Session
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:2468
|
||||
sandbox-agent api sessions terminate <SESSION_ID>
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions events-sse</strong></summary>
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:2468
|
||||
sandbox-agent api sessions terminate my-session
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions reply-question</strong></summary>
|
||||
#### Get Events
|
||||
|
||||
Fetch session events:
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reply-question my-session QUESTION_ID \
|
||||
--answers "yes" \
|
||||
--endpoint http://127.0.0.1:2468
|
||||
sandbox-agent api sessions events <SESSION_ID> [OPTIONS]
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions reject-question</strong></summary>
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-o, --offset <N>` | Event offset |
|
||||
| `-l, --limit <N>` | Max events to return |
|
||||
| `--include-raw` | Include raw agent data |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:2468
|
||||
sandbox-agent api sessions events my-session --offset 0 --limit 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>api sessions reply-permission</strong></summary>
|
||||
`get-messages` is an alias for `events`.
|
||||
|
||||
#### Stream Events (SSE)
|
||||
|
||||
Stream session events via Server-Sent Events:
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reply-permission my-session PERMISSION_ID \
|
||||
--reply once \
|
||||
--endpoint http://127.0.0.1:2468
|
||||
sandbox-agent api sessions events-sse <SESSION_ID> [OPTIONS]
|
||||
```
|
||||
</details>
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-o, --offset <N>` | Event offset to start from |
|
||||
| `--include-raw` | Include raw agent data |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions events-sse my-session --offset 0
|
||||
```
|
||||
|
||||
#### Reply to Question
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reply-question <SESSION_ID> <QUESTION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-a, --answers <JSON>` | JSON array of answers (required) |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reply-question my-session q1 \
|
||||
--answers '[["yes"]]'
|
||||
```
|
||||
|
||||
#### Reject Question
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reject-question <SESSION_ID> <QUESTION_ID>
|
||||
```
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reject-question my-session q1
|
||||
```
|
||||
|
||||
#### Reply to Permission
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reply-permission <SESSION_ID> <PERMISSION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-r, --reply <REPLY>` | `once`, `always`, or `reject` (required) |
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions reply-permission my-session perm1 --reply once
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI to HTTP Mapping
|
||||
|
||||
| CLI Command | HTTP Endpoint |
|
||||
|-------------|---------------|
|
||||
| `api agents list` | `GET /v1/agents` |
|
||||
| `api agents install` | `POST /v1/agents/{agent}/install` |
|
||||
| `api agents modes` | `GET /v1/agents/{agent}/modes` |
|
||||
| `api sessions list` | `GET /v1/sessions` |
|
||||
| `api sessions create` | `POST /v1/sessions/{sessionId}` |
|
||||
| `api sessions send-message` | `POST /v1/sessions/{sessionId}/messages` |
|
||||
| `api sessions send-message-stream` | `POST /v1/sessions/{sessionId}/messages/stream` |
|
||||
| `api sessions terminate` | `POST /v1/sessions/{sessionId}/terminate` |
|
||||
| `api sessions events` | `GET /v1/sessions/{sessionId}/events` |
|
||||
| `api sessions events-sse` | `GET /v1/sessions/{sessionId}/events/sse` |
|
||||
| `api sessions reply-question` | `POST /v1/sessions/{sessionId}/questions/{questionId}/reply` |
|
||||
| `api sessions reject-question` | `POST /v1/sessions/{sessionId}/questions/{questionId}/reject` |
|
||||
| `api sessions reply-permission` | `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply` |
|
||||
|
|
|
|||
60
docs/cors.mdx
Normal file
60
docs/cors.mdx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
title: "CORS Configuration"
|
||||
description: "Configure CORS for browser-based applications."
|
||||
sidebarTitle: "CORS"
|
||||
icon: "globe"
|
||||
---
|
||||
|
||||
When calling the Sandbox Agent server from a browser, you need to enable CORS (Cross-Origin Resource Sharing) explicitly.
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
```bash
|
||||
sandbox-agent server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--cors-allow-origin "http://localhost:5173" \
|
||||
--cors-allow-method "GET" \
|
||||
--cors-allow-method "POST" \
|
||||
--cors-allow-header "Authorization" \
|
||||
--cors-allow-header "Content-Type" \
|
||||
--cors-allow-credentials
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--cors-allow-origin` | Origins allowed to make requests (e.g., `http://localhost:5173`) |
|
||||
| `--cors-allow-method` | HTTP methods to allow (can be specified multiple times) |
|
||||
| `--cors-allow-header` | Headers to allow (can be specified multiple times) |
|
||||
| `--cors-allow-credentials` | Allow credentials (cookies, authorization headers) |
|
||||
|
||||
## Multiple Origins
|
||||
|
||||
You can allow multiple origins by specifying the flag multiple times:
|
||||
|
||||
```bash
|
||||
sandbox-agent server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--cors-allow-origin "http://localhost:5173" \
|
||||
--cors-allow-origin "http://localhost:3000" \
|
||||
--cors-allow-method "GET" \
|
||||
--cors-allow-method "POST" \
|
||||
--cors-allow-header "Authorization" \
|
||||
--cors-allow-header "Content-Type"
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
In production, replace `localhost` origins with your actual domain:
|
||||
|
||||
```bash
|
||||
sandbox-agent server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--cors-allow-origin "https://your-app.com" \
|
||||
--cors-allow-method "GET" \
|
||||
--cors-allow-method "POST" \
|
||||
--cors-allow-header "Authorization" \
|
||||
--cors-allow-header "Content-Type" \
|
||||
--cors-allow-credentials
|
||||
```
|
||||
|
|
@ -1,21 +1,90 @@
|
|||
---
|
||||
title: "Daytona"
|
||||
description: "Run the daemon in a Daytona workspace."
|
||||
description: "Run the daemon in a Daytona workspace."
|
||||
---
|
||||
|
||||
## Steps
|
||||
<Note>
|
||||
Daytona has [network egress limits](https://www.daytona.io/docs/en/network-limits/) on lower tiers. OpenAI and Anthropic APIs are whitelisted on all tiers, but other external services may be restricted on Tier 1 & 2.
|
||||
</Note>
|
||||
|
||||
1. Create a Daytona workspace with Rust and curl available.
|
||||
2. Install or build the sandbox-agent binary.
|
||||
3. Start the daemon and expose port `2468` (or your preferred port).
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
export SANDBOX_TOKEN="..."
|
||||
- `DAYTONA_API_KEY` environment variable
|
||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
||||
|
||||
cargo run -p sandbox-agent -- server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--host 0.0.0.0 \
|
||||
--port 2468
|
||||
## TypeScript Example
|
||||
|
||||
```typescript
|
||||
import { Daytona, Image } from "@daytonaio/sdk";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const daytona = new Daytona();
|
||||
|
||||
// Pass API keys to the sandbox
|
||||
const envVars: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const sandbox = await daytona.create({ envVars });
|
||||
|
||||
// Install sandbox-agent
|
||||
await sandbox.process.executeCommand(
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"
|
||||
);
|
||||
|
||||
// Start the server in the background
|
||||
await sandbox.process.executeCommand(
|
||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"
|
||||
);
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
// Get the public URL
|
||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||
|
||||
// Connect and use the SDK
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
permissionMode: "default",
|
||||
});
|
||||
|
||||
// Cleanup when done
|
||||
await sandbox.delete();
|
||||
```
|
||||
|
||||
4. Use your Daytona port forwarding to reach the daemon from your client.
|
||||
## Using Snapshots for Faster Startup
|
||||
|
||||
For production, use snapshots with pre-installed binaries:
|
||||
|
||||
```typescript
|
||||
import { Daytona, Image } from "@daytonaio/sdk";
|
||||
|
||||
const daytona = new Daytona();
|
||||
const SNAPSHOT = "sandbox-agent-ready";
|
||||
|
||||
// Create snapshot once (takes 2-3 minutes)
|
||||
const hasSnapshot = await daytona.snapshot.get(SNAPSHOT).then(() => true, () => false);
|
||||
|
||||
if (!hasSnapshot) {
|
||||
await daytona.snapshot.create({
|
||||
name: SNAPSHOT,
|
||||
image: Image.base("ubuntu:22.04").runCommands(
|
||||
"apt-get update && apt-get install -y curl ca-certificates",
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh",
|
||||
"sandbox-agent install-agent claude",
|
||||
"sandbox-agent install-agent codex",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Now sandboxes start instantly
|
||||
const sandbox = await daytona.create({
|
||||
snapshot: SNAPSHOT,
|
||||
envVars,
|
||||
});
|
||||
```
|
||||
|
||||
See [Daytona Snapshots](https://daytona.io/docs/snapshots) for details.
|
||||
|
|
|
|||
|
|
@ -1,27 +1,75 @@
|
|||
---
|
||||
title: "Docker (dev)"
|
||||
title: "Docker"
|
||||
description: "Build and run the daemon in a Docker container."
|
||||
---
|
||||
|
||||
## Build the binary
|
||||
<Warning>
|
||||
Docker is not recommended for production. Standard Docker containers don't provide sufficient isolation for running untrusted code. Use a dedicated sandbox provider like E2B or Daytona for production workloads.
|
||||
</Warning>
|
||||
|
||||
Use the release Dockerfile to build a static binary:
|
||||
## Quick Start
|
||||
|
||||
Run sandbox-agent in a container with agents pre-installed:
|
||||
|
||||
```bash
|
||||
docker run --rm -p 3000:3000 \
|
||||
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
||||
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
|
||||
debian:bookworm-slim bash -lc "\
|
||||
apt-get update && apt-get install -y curl ca-certificates && \
|
||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh && \
|
||||
sandbox-agent install-agent claude && \
|
||||
sandbox-agent install-agent codex && \
|
||||
sandbox-agent server --no-token --host 0.0.0.0 --port 3000"
|
||||
```
|
||||
|
||||
Access the API at `http://localhost:3000`.
|
||||
|
||||
## TypeScript with dockerode
|
||||
|
||||
```typescript
|
||||
import Docker from "dockerode";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const docker = new Docker();
|
||||
const PORT = 3000;
|
||||
|
||||
const container = await docker.createContainer({
|
||||
Image: "debian:bookworm-slim",
|
||||
Cmd: ["bash", "-lc", [
|
||||
"apt-get update && apt-get install -y curl ca-certificates",
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh",
|
||||
"sandbox-agent install-agent claude",
|
||||
"sandbox-agent install-agent codex",
|
||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
||||
].join(" && ")],
|
||||
ExposedPorts: { [`${PORT}/tcp`]: {} },
|
||||
HostConfig: {
|
||||
AutoRemove: true,
|
||||
PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] },
|
||||
},
|
||||
});
|
||||
|
||||
await container.start();
|
||||
|
||||
// Wait for server and connect
|
||||
const baseUrl = `http://127.0.0.1:${PORT}`;
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
// Use the client...
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
permissionMode: "default",
|
||||
});
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
To build a static binary for use in minimal containers:
|
||||
|
||||
```bash
|
||||
docker build -f docker/release/linux-x86_64.Dockerfile -t sandbox-agent-build .
|
||||
|
||||
docker run --rm -v "$PWD/artifacts:/artifacts" sandbox-agent-build
|
||||
```
|
||||
|
||||
The binary will be written to `./artifacts/sandbox-agent-x86_64-unknown-linux-musl`.
|
||||
|
||||
## Run the daemon
|
||||
|
||||
```bash
|
||||
docker run --rm -p 2468:2468 \
|
||||
-v "$PWD/artifacts:/artifacts" \
|
||||
debian:bookworm-slim \
|
||||
/artifacts/sandbox-agent-x86_64-unknown-linux-musl server --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 2468
|
||||
```
|
||||
|
||||
You can now access the API at `http://localhost:2468`.
|
||||
The binary will be at `./artifacts/sandbox-agent-x86_64-unknown-linux-musl`.
|
||||
|
|
|
|||
|
|
@ -3,23 +3,77 @@ title: "E2B"
|
|||
description: "Deploy the daemon inside an E2B sandbox."
|
||||
---
|
||||
|
||||
## Steps
|
||||
## Prerequisites
|
||||
|
||||
1. Start an E2B sandbox with network access.
|
||||
2. Install the agent binaries you need (Claude, Codex, OpenCode, Amp).
|
||||
3. Run the daemon and expose its port.
|
||||
- `E2B_API_KEY` environment variable
|
||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
||||
|
||||
Example startup script:
|
||||
## TypeScript Example
|
||||
|
||||
```bash
|
||||
export SANDBOX_TOKEN="..."
|
||||
```typescript
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
# Install sandbox-agent binary (or build from source)
|
||||
# TODO: replace with release download once published
|
||||
cargo run -p sandbox-agent -- server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--host 0.0.0.0 \
|
||||
--port 2468
|
||||
// Pass API keys to the sandbox
|
||||
const envs: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const sandbox = await Sandbox.create({ envs });
|
||||
|
||||
// Install sandbox-agent
|
||||
await sandbox.commands.run(
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"
|
||||
);
|
||||
|
||||
// Start the server in the background
|
||||
await sandbox.commands.run(
|
||||
"sandbox-agent server --no-token --host 0.0.0.0 --port 3000",
|
||||
{ background: true }
|
||||
);
|
||||
|
||||
// Connect to the server
|
||||
const baseUrl = `https://${sandbox.getHost(3000)}`;
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
// Wait for server to be ready
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
await client.getHealth();
|
||||
break;
|
||||
} catch {
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// Install agents (or pre-install in a custom template)
|
||||
await client.installAgent("claude");
|
||||
await client.installAgent("codex");
|
||||
|
||||
// Create a session and start coding
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
permissionMode: "default",
|
||||
});
|
||||
|
||||
await client.postMessage("my-session", {
|
||||
message: "Summarize this repository",
|
||||
});
|
||||
|
||||
for await (const event of client.streamEvents("my-session")) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await sandbox.kill();
|
||||
```
|
||||
|
||||
4. Configure your client to connect to the sandbox endpoint.
|
||||
## Faster Cold Starts
|
||||
|
||||
For faster startup, create a custom E2B template with sandbox-agent and agents pre-installed:
|
||||
|
||||
1. Create a template with the install script baked in
|
||||
2. Pre-install agents: `sandbox-agent install-agent claude codex`
|
||||
3. Use the template ID when creating sandboxes
|
||||
|
||||
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template) for details.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,21 @@
|
|||
---
|
||||
sidebarTitle: Overview
|
||||
title: "Deploy"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Choose where to run the sandbox-agent server."
|
||||
icon: "server"
|
||||
---
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Local" icon="laptop" href="/deploy/local">
|
||||
Run locally for development. The SDK can auto-spawn the server.
|
||||
</Card>
|
||||
<Card title="E2B" icon="cube" href="/deploy/e2b">
|
||||
Deploy inside an E2B sandbox with network access.
|
||||
</Card>
|
||||
<Card title="Daytona" icon="cloud" href="/deploy/daytona">
|
||||
Run in a Daytona workspace with port forwarding.
|
||||
</Card>
|
||||
<Card title="Docker" icon="docker" href="/deploy/docker">
|
||||
Build and run in a container (development only).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
|
|||
43
docs/deploy/local.mdx
Normal file
43
docs/deploy/local.mdx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: "Local"
|
||||
description: "Run the daemon locally for development."
|
||||
---
|
||||
|
||||
For local development, you can run the daemon directly on your machine.
|
||||
|
||||
## With the CLI
|
||||
|
||||
```bash
|
||||
# Install
|
||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
||||
|
||||
# Run
|
||||
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
||||
```
|
||||
|
||||
Or with npm:
|
||||
|
||||
```bash
|
||||
npx sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
||||
```
|
||||
|
||||
## With the TypeScript SDK
|
||||
|
||||
The SDK can automatically spawn and manage the server as a subprocess:
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
// Spawns sandbox-agent server as a subprocess
|
||||
const client = await SandboxAgent.start();
|
||||
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
permissionMode: "default",
|
||||
});
|
||||
|
||||
// When done
|
||||
await client.dispose();
|
||||
```
|
||||
|
||||
This installs the binary (if needed) and starts the server on a random available port. No manual setup required.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
title: "Vercel Sandboxes"
|
||||
description: "Run the daemon inside Vercel Sandboxes."
|
||||
---
|
||||
|
||||
## Steps
|
||||
|
||||
1. Provision a Vercel Sandbox with network access and storage.
|
||||
2. Install the agent binaries you need.
|
||||
3. Run the daemon and expose the port.
|
||||
|
||||
```bash
|
||||
export SANDBOX_TOKEN="..."
|
||||
|
||||
cargo run -p sandbox-agent -- server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--host 0.0.0.0 \
|
||||
--port 2468
|
||||
```
|
||||
|
||||
4. Configure your client to use the sandbox URL.
|
||||
|
|
@ -34,38 +34,43 @@
|
|||
"pages": [
|
||||
{
|
||||
"group": "Getting started",
|
||||
"pages": [
|
||||
"index",
|
||||
"quickstart",
|
||||
"architecture",
|
||||
"agent-compatibility",
|
||||
"universal-api",
|
||||
"frontend",
|
||||
"building-chat-ui",
|
||||
"manage-session-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "SDKs",
|
||||
"pages": ["sdks/typescript"]
|
||||
},
|
||||
{
|
||||
"group": "AI",
|
||||
"pages": ["ai/skill", "ai/llms-txt"]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": ["cli", "telemetry", "http-api"]
|
||||
"pages": ["quickstart", "building-chat-ui", "manage-sessions"]
|
||||
},
|
||||
{
|
||||
"group": "Deploy",
|
||||
"pages": [
|
||||
"deploy/index",
|
||||
"deploy/docker",
|
||||
"deploy/local",
|
||||
"deploy/e2b",
|
||||
"deploy/daytona",
|
||||
"deploy/vercel-sandboxes"
|
||||
"deploy/docker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "SDKs",
|
||||
"pages": ["sdks/typescript", "sdks/python"]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"cli",
|
||||
"inspector",
|
||||
"universal-schema",
|
||||
"agent-compatibility",
|
||||
"cors",
|
||||
{
|
||||
"group": "AI",
|
||||
"pages": ["ai/skill", "ai/llms-txt"]
|
||||
},
|
||||
{
|
||||
"group": "Advanced",
|
||||
"pages": ["telemetry"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "HTTP API Reference",
|
||||
"openapi": "openapi.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.06145 23.1079C5.26816 22.3769 -3.39077 20.6274 1.4173 5.06384C9.6344 6.09939 16.9728 14.0644 9.06145 23.1079Z" fill="url(#paint0_linear_17557_2021)"/>
|
||||
<path d="M8.91928 23.0939C5.27642 21.2223 0.78371 4.20891 17.0071 0C20.7569 7.19341 19.6212 16.5452 8.91928 23.0939Z" fill="url(#paint1_linear_17557_2021)"/>
|
||||
<path d="M8.91388 23.0788C8.73534 19.8817 10.1585 9.08525 23.5699 13.1107C23.1812 20.1229 18.984 26.4182 8.91388 23.0788Z" fill="url(#paint2_linear_17557_2021)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_17557_2021" x1="3.77557" y1="5.91571" x2="5.23185" y2="21.5589" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#18E299"/>
|
||||
<stop offset="1" stop-color="#15803D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_17557_2021" x1="12.1711" y1="-0.718425" x2="10.1897" y2="22.9832" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#16A34A"/>
|
||||
<stop offset="1" stop-color="#4ADE80"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_17557_2021" x1="23.1327" y1="15.353" x2="9.33841" y2="18.5196" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4ADE80"/>
|
||||
<stop offset="1" stop-color="#0D9373"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="6" fill="url(#bg_gradient)"/>
|
||||
<text x="16" y="22" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="700" fill="white">SA</text>
|
||||
<defs>
|
||||
<linearGradient id="bg_gradient" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#16A34A"/>
|
||||
<stop offset="1" stop-color="#15803D"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 539 B |
|
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
title: "Frontend Demo"
|
||||
description: "Run the Vite + React UI for testing the server."
|
||||
---
|
||||
|
||||
The demo frontend lives at `frontend/packages/inspector`.
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm --filter @sandbox-agent/inspector dev
|
||||
```
|
||||
|
||||
The UI expects:
|
||||
|
||||
- Endpoint (e.g. `http://127.0.0.1:2468`)
|
||||
- Optional token
|
||||
|
||||
When running the server, the inspector is also served automatically at `http://127.0.0.1:2468/ui`.
|
||||
|
||||
If you see CORS errors, enable CORS on the server with `sandbox-agent server --cors-allow-origin` and related flags.
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# Glossary (Universal Schema)
|
||||
|
||||
This glossary defines the universal schema terms used across the daemon, SDK, and tests.
|
||||
|
||||
Session terms
|
||||
- session_id: daemon-generated identifier for a universal session.
|
||||
- native_session_id: provider-native thread/session/run identifier (thread_id merged here).
|
||||
- session.started: event emitted at session start (native or synthetic).
|
||||
- session.ended: event emitted at session end (native or synthetic); includes reason and terminated_by.
|
||||
- terminated_by: who ended the session: agent or daemon.
|
||||
- reason: why the session ended: completed, error, or terminated.
|
||||
|
||||
Event terms
|
||||
- UniversalEvent: envelope that wraps all events; includes source, type, data, raw.
|
||||
- event_id: unique identifier for the event.
|
||||
- sequence: monotonic event sequence number within a session.
|
||||
- time: RFC3339 timestamp for the event.
|
||||
- source: event origin: agent (native) or daemon (synthetic).
|
||||
- raw: original provider payload for native events; optional for synthetic events.
|
||||
|
||||
Item terms
|
||||
- item_id: daemon-generated identifier for a universal item.
|
||||
- native_item_id: provider-native item/message identifier when available; null otherwise.
|
||||
- parent_id: item_id of the parent item (e.g., tool call/result parented to a message).
|
||||
- kind: item category: message, tool_call, tool_result, system, status, unknown.
|
||||
- role: actor role for message items: user, assistant, system, tool (or null).
|
||||
- status: item lifecycle status: in_progress, completed, failed (or null).
|
||||
|
||||
Item event terms
|
||||
- item.started: item creation event (may be synthetic).
|
||||
- item.delta: streaming delta event (native where supported; synthetic otherwise).
|
||||
- item.completed: final item event with complete content.
|
||||
|
||||
Content terms
|
||||
- content: ordered list of parts that make up an item payload.
|
||||
- content part: a typed element inside content (text, json, tool_call, tool_result, file_ref, image, status, reasoning).
|
||||
- text: plain text content part.
|
||||
- json: structured JSON content part.
|
||||
- tool_call: tool invocation content part (name, arguments, call_id).
|
||||
- tool_result: tool result content part (call_id, output).
|
||||
- file_ref: file reference content part (path, action, diff).
|
||||
- image: image content part (path, mime).
|
||||
- status: status content part (label, detail).
|
||||
- reasoning: reasoning content part (text, visibility).
|
||||
- visibility: reasoning visibility: public or private.
|
||||
|
||||
HITL terms
|
||||
- permission.requested / permission.resolved: human-in-the-loop permission flow events.
|
||||
- permission_id: identifier for the permission request.
|
||||
- question.requested / question.resolved: human-in-the-loop question flow events.
|
||||
- question_id: identifier for the question request.
|
||||
- options: question answer options.
|
||||
- response: selected answer for a question.
|
||||
|
||||
Synthetic terms
|
||||
- synthetic event: a daemon-emitted event used to fill gaps in provider-native schemas.
|
||||
- source=daemon: marks synthetic events.
|
||||
- synthetic delta: a single full-content delta emitted for providers without native deltas.
|
||||
|
||||
Provider terms
|
||||
- agent: the native provider (claude, codex, opencode, amp).
|
||||
- native payload: the provider’s original event/message object stored in raw.
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
---
|
||||
title: "HTTP API"
|
||||
description: "Endpoint reference for the sandbox agent daemon."
|
||||
---
|
||||
|
||||
All endpoints are under `/v1`. Authentication uses the daemon-level token via `Authorization: Bearer <token>`.
|
||||
|
||||
## Health
|
||||
|
||||
<details>
|
||||
<summary><strong>GET /v1/health</strong> - Connectivity check</summary>
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
</details>
|
||||
|
||||
## Sessions
|
||||
|
||||
<details>
|
||||
<summary><strong>POST /v1/sessions/{sessionId}</strong> - Create session</summary>
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "claude",
|
||||
"agentMode": "build",
|
||||
"permissionMode": "default",
|
||||
"model": "claude-3-5-sonnet",
|
||||
"variant": "high",
|
||||
"agentVersion": "latest"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"healthy": true,
|
||||
"agentSessionId": "..."
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>POST /v1/sessions/{sessionId}/messages</strong> - Send message</summary>
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Describe the repository."
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>GET /v1/sessions/{sessionId}/events</strong> - Fetch events</summary>
|
||||
|
||||
Query params:
|
||||
|
||||
- `offset`: last-seen event id (exclusive)
|
||||
- `limit`: max number of events
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": 1,
|
||||
"timestamp": "2026-01-25T10:00:00Z",
|
||||
"sessionId": "my-session",
|
||||
"agent": "claude",
|
||||
"agentSessionId": "...",
|
||||
"data": { "message": { "role": "assistant", "parts": [{ "type": "text", "text": "..." }] } }
|
||||
}
|
||||
],
|
||||
"hasMore": false
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>GET /v1/sessions/{sessionId}/events/sse</strong> - Stream events (SSE)</summary>
|
||||
|
||||
Query params:
|
||||
|
||||
- `offset`: last-seen event id (exclusive)
|
||||
|
||||
SSE payloads are `UniversalEvent` JSON.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>POST /v1/sessions/{sessionId}/questions/{questionId}/reply</strong></summary>
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{ "answers": [["Option A"], ["Option B", "Option C"]] }
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>POST /v1/sessions/{sessionId}/questions/{questionId}/reject</strong></summary>
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply</strong></summary>
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{ "reply": "once" }
|
||||
```
|
||||
</details>
|
||||
|
||||
## Agents
|
||||
|
||||
<details>
|
||||
<summary><strong>GET /v1/agents</strong> - List agents</summary>
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": [
|
||||
{ "id": "claude", "installed": true, "version": "...", "path": "/usr/local/bin/claude" }
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>POST /v1/agents/{agentId}/install</strong> - Install agent</summary>
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{ "reinstall": false }
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>GET /v1/agents/{agentId}/modes</strong> - List modes</summary>
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"modes": [
|
||||
{ "id": "build", "name": "Build", "description": "Default coding mode" }
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Error handling
|
||||
|
||||
All errors use RFC 7807 Problem Details and stable `type` strings (e.g. `urn:sandbox-agent:error:session_not_found`).
|
||||
BIN
docs/images/inspector.png
Normal file
BIN
docs/images/inspector.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
title: "Overview"
|
||||
description: "Universal API for running Claude Code, Codex, OpenCode, and Amp inside sandboxes."
|
||||
---
|
||||
|
||||
Sandbox Agent SDK is a universal API and daemon for running coding agents inside sandboxes. It standardizes agent sessions, events, and human-in-the-loop workflows across Claude Code, Codex, OpenCode, and Amp.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Universal HTTP API and TypeScript SDK
|
||||
- Runs inside sandboxes with a lightweight Rust daemon
|
||||
- Streams events in a shared UniversalEvent schema
|
||||
- Supports questions and permission workflows
|
||||
- Designed for multi-provider sandbox environments
|
||||
|
||||
## Quickstart
|
||||
|
||||
Run the daemon locally:
|
||||
|
||||
```bash
|
||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
|
||||
Send a message:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"agent":"claude"}'
|
||||
|
||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message":"Explain the repo structure."}'
|
||||
```
|
||||
|
||||
See the full quickstart in [Quickstart](/quickstart).
|
||||
|
||||
## What this project solves
|
||||
|
||||
- **Universal Coding Agent API**: standardize tool calls, messages, and events across agents.
|
||||
- **Agents in sandboxes**: run a single HTTP daemon inside any sandbox provider.
|
||||
- **Agent transcripts**: stream or persist a universal event log in your own storage.
|
||||
|
||||
## Project scope
|
||||
|
||||
**In scope**
|
||||
|
||||
- Agent session orchestration inside a sandbox
|
||||
- Streaming events in a universal schema
|
||||
- Human-in-the-loop questions and permissions
|
||||
- TypeScript SDK and CLI wrappers
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Persistent storage of sessions on disk
|
||||
- Building custom LLM agents (use Vercel AI SDK for that)
|
||||
- Sandbox provider APIs (use provider SDKs or custom glue)
|
||||
- Git repo management
|
||||
|
||||
## Next steps
|
||||
|
||||
- Read the [Architecture](/architecture) overview
|
||||
- Review [Agent compatibility](/agent-compatibility)
|
||||
- See the [HTTP API](/http-api) and [CLI](/cli)
|
||||
- Run the [Frontend demo](/frontend)
|
||||
- Use the [TypeScript SDK](/typescript-sdk)
|
||||
45
docs/inspector.mdx
Normal file
45
docs/inspector.mdx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: "Inspector"
|
||||
description: "Debug and inspect agent sessions with the Inspector UI."
|
||||
icon: "magnifying-glass"
|
||||
---
|
||||
|
||||
The Inspector is a web-based GUI for debugging and inspecting Sandbox Agent sessions. Use it to view events, send messages, and troubleshoot agent behavior in real-time.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" />
|
||||
</Frame>
|
||||
|
||||
## Open the Inspector
|
||||
|
||||
Visit [inspect.sandboxagent.dev](https://inspect.sandboxagent.dev) and enter your server URL and token to connect.
|
||||
|
||||
You can also generate a pre-filled Inspector URL from the TypeScript SDK:
|
||||
|
||||
```typescript
|
||||
import { buildInspectorUrl } from "sandbox-agent";
|
||||
|
||||
const url = buildInspectorUrl({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
console.log(url);
|
||||
// https://inspect.sandboxagent.dev?url=http%3A%2F%2F127.0.0.1%3A2468&token=...
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Session list**: View all active sessions and their status
|
||||
- **Event stream**: See events in real-time as they arrive (SSE or polling)
|
||||
- **Event details**: Expand any event to see its full JSON payload
|
||||
- **Send messages**: Post messages to a session directly from the UI
|
||||
- **Agent selection**: Switch between agents and modes
|
||||
- **Request log**: View raw HTTP requests and responses for debugging
|
||||
|
||||
## When to Use
|
||||
|
||||
The Inspector is useful for:
|
||||
|
||||
- **Development**: Test your integration without writing client code
|
||||
- **Debugging**: Inspect event payloads and timing issues
|
||||
- **Learning**: Understand how agents respond to different prompts
|
||||
|
|
@ -1,21 +1,261 @@
|
|||
---
|
||||
title: "Manage Session State"
|
||||
description: "TODO"
|
||||
title: "Manage Sessions"
|
||||
description: "Persist and replay agent transcripts across connections."
|
||||
icon: "database"
|
||||
---
|
||||
|
||||
TODO
|
||||
Sandbox Agent stores sessions in memory only. When the server restarts or the sandbox is destroyed, all session data is lost. It's your responsibility to persist events to your own database.
|
||||
|
||||
See the [Building a Chat UI](/building-chat-ui) guide for understanding session lifecycle events like `session.started` and `session.ended`.
|
||||
|
||||
## Recommended approach
|
||||
|
||||
- Store the offset of the last message you have seen (the last event id).
|
||||
- Update your server to stream events from the Events API using that offset.
|
||||
- Write the resulting messages and events to your own database.
|
||||
1. Store events to your database as they arrive
|
||||
2. On reconnect, get the last event's `sequence` and pass it as `offset`
|
||||
3. The API returns events where `sequence > offset`
|
||||
|
||||
This lets you resume from a known offset after a disconnect and prevents duplicate writes.
|
||||
This prevents duplicate writes and lets you recover from disconnects.
|
||||
|
||||
## Recommended: Rivet Actors
|
||||
## Receiving Events
|
||||
|
||||
If you want a managed way to keep long-running streams alive, consider [Rivet Actors](https://rivet.dev).
|
||||
They handle continuous event streaming plus fast reads and writes of data for agents, with built-in
|
||||
realtime support and observability. You can use them to stream `/events/sse` per session and persist
|
||||
each event to your database as it arrives.
|
||||
Two ways to receive events: SSE streaming (recommended) or polling.
|
||||
|
||||
### Streaming
|
||||
|
||||
Use SSE for real-time events with automatic reconnection support.
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
});
|
||||
|
||||
// Get offset from last stored event (0 returns all events)
|
||||
const lastEvent = await db.getLastEvent("my-session");
|
||||
const offset = lastEvent?.sequence ?? 0;
|
||||
|
||||
// Stream from where you left off
|
||||
for await (const event of client.streamEvents("my-session", { offset })) {
|
||||
await db.insertEvent("my-session", event);
|
||||
}
|
||||
```
|
||||
|
||||
### Polling
|
||||
|
||||
If you can't use SSE streaming, poll the events endpoint:
|
||||
|
||||
```typescript
|
||||
const lastEvent = await db.getLastEvent("my-session");
|
||||
let offset = lastEvent?.sequence ?? 0;
|
||||
|
||||
while (true) {
|
||||
const { events } = await client.getEvents("my-session", {
|
||||
offset,
|
||||
limit: 100
|
||||
});
|
||||
|
||||
for (const event of events) {
|
||||
await db.insertEvent("my-session", event);
|
||||
offset = event.sequence;
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
```
|
||||
|
||||
## Database options
|
||||
|
||||
Choose where to persist events based on your requirements. For most use cases, we recommend Rivet Actors.
|
||||
|
||||
| | Durable | Real-time | Multiplayer | Scaling | Throughput | Complexity |
|
||||
|---------|:-------:|:---------:|:-----------:|---------|------------|------------|
|
||||
| Rivet Actors | ✓ | ✓ | ✓ | Auto-sharded, one actor per session | Millions of concurrent sessions | Zero infrastructure |
|
||||
| PostgreSQL | ✓ | | | Manual sharding | Connection pool limited | Connection pools, migrations |
|
||||
| Redis | | ✓ | | Redis Cluster | High, in-memory | Memory management, Sentinel for failover |
|
||||
|
||||
### Rivet Actors
|
||||
|
||||
For production workloads, [Rivet Actors](https://rivet.gg) provide a managed solution for:
|
||||
|
||||
- **Persistent state**: Events survive crashes and restarts
|
||||
- **Real-time streaming**: Built-in WebSocket support for clients
|
||||
- **Horizontal scaling**: Run thousands of concurrent sessions
|
||||
- **Observability**: Built-in logging and metrics
|
||||
|
||||
#### Actor
|
||||
|
||||
```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> => {
|
||||
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,
|
||||
});
|
||||
|
||||
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) => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Client
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript TypeScript
|
||||
import { createClient } from "rivetkit/client";
|
||||
|
||||
const client = createClient();
|
||||
const session = client.codingSession.getOrCreate(["my-session"]);
|
||||
|
||||
const conn = session.connect();
|
||||
conn.on("agentEvent", (event) => {
|
||||
console.log(event.type, event.data);
|
||||
});
|
||||
|
||||
await conn.postMessage("Create a new React component for user profiles");
|
||||
|
||||
const transcript = await conn.getTranscript();
|
||||
```
|
||||
|
||||
```typescript React
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```sql
|
||||
CREATE TABLE agent_events (
|
||||
event_id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
native_session_id TEXT,
|
||||
sequence INTEGER NOT NULL,
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
synthetic BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
data JSONB NOT NULL,
|
||||
UNIQUE(session_id, sequence)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_events_session ON agent_events(session_id, sequence);
|
||||
```
|
||||
|
||||
### Redis
|
||||
|
||||
```typescript
|
||||
// Append event to list
|
||||
await redis.rpush(`session:${sessionId}`, JSON.stringify(event));
|
||||
|
||||
// Get events from offset
|
||||
const events = await redis.lrange(`session:${sessionId}`, offset, -1);
|
||||
```
|
||||
|
||||
## Handling disconnects
|
||||
|
||||
The SSE stream may disconnect due to network issues. Handle reconnection gracefully:
|
||||
|
||||
```typescript
|
||||
async function streamWithRetry(sessionId: string) {
|
||||
while (true) {
|
||||
try {
|
||||
const lastEvent = await db.getLastEvent(sessionId);
|
||||
const offset = lastEvent?.sequence ?? 0;
|
||||
|
||||
for await (const event of client.streamEvents(sessionId, { offset })) {
|
||||
await db.insertEvent(sessionId, event);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Stream disconnected, reconnecting...", error);
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,103 +1,288 @@
|
|||
---
|
||||
title: "Quickstart"
|
||||
description: "Start the server and send your first message."
|
||||
icon: "rocket"
|
||||
---
|
||||
|
||||
## 1. Run the server
|
||||
<Steps>
|
||||
<Step title="Install skill (optional)">
|
||||
```bash
|
||||
npx skills add https://sandboxagent.dev/docs
|
||||
```
|
||||
</Step>
|
||||
|
||||
Use the installed binary, or `cargo run` in development.
|
||||
<Step title="Set environment variables">
|
||||
Each coding agent requires API keys to connect to their respective LLM providers.
|
||||
|
||||
```bash
|
||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Local shell">
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
</Tab>
|
||||
|
||||
If you want to run without auth (local dev only):
|
||||
<Tab title="E2B">
|
||||
```typescript
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
|
||||
```bash
|
||||
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
||||
```
|
||||
const envs: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
If you're running from source instead of the installed CLI:
|
||||
const sandbox = await Sandbox.create({ envs });
|
||||
```
|
||||
</Tab>
|
||||
|
||||
```bash
|
||||
cargo run -p sandbox-agent -- server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
<Tab title="Daytona">
|
||||
```typescript
|
||||
import { Daytona } from "@daytonaio/sdk";
|
||||
|
||||
### CORS (frontend usage)
|
||||
const envVars: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
If you are calling the server from a browser, enable CORS explicitly:
|
||||
const daytona = new Daytona();
|
||||
const sandbox = await daytona.create({
|
||||
snapshot: "sandbox-agent-ready",
|
||||
envVars,
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
||||
```bash
|
||||
sandbox-agent server \
|
||||
--token "$SANDBOX_TOKEN" \
|
||||
--cors-allow-origin "http://localhost:5173" \
|
||||
--cors-allow-method "GET" \
|
||||
--cors-allow-method "POST" \
|
||||
--cors-allow-header "Authorization" \
|
||||
--cors-allow-header "Content-Type" \
|
||||
--cors-allow-credentials
|
||||
```
|
||||
<Tab title="Docker">
|
||||
```bash
|
||||
docker run -e ANTHROPIC_API_KEY="sk-ant-..." \
|
||||
-e OPENAI_API_KEY="sk-..." \
|
||||
your-image
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 2. Install agents (optional)
|
||||
<AccordionGroup>
|
||||
<Accordion title="Extracting API keys from current machine">
|
||||
Use `sandbox-agent credentials extract-env --export` to extract your existing API keys (Anthropic, OpenAI, etc.) from your existing Claude Code or Codex config files on your machine.
|
||||
</Accordion>
|
||||
<Accordion title="Testing without API keys">
|
||||
If you want to test Sandbox Agent without API keys, use the `mock` agent to test the SDK without any credentials. It simulates agent responses for development and testing.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Step>
|
||||
|
||||
Agents install lazily on first use. To preinstall everything up front:
|
||||
<Step title="Run the server">
|
||||
<Tabs>
|
||||
<Tab title="curl">
|
||||
Install and run the binary directly.
|
||||
|
||||
```bash
|
||||
sandbox-agent install-agent claude
|
||||
sandbox-agent install-agent codex
|
||||
sandbox-agent install-agent opencode
|
||||
sandbox-agent install-agent amp
|
||||
```
|
||||
```bash
|
||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
</Tab>
|
||||
|
||||
## 3. Create a session
|
||||
<Tab title="npx">
|
||||
Run without installing globally.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"agent":"claude","agentMode":"build","permissionMode":"default"}'
|
||||
```
|
||||
```bash
|
||||
npx sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
</Tab>
|
||||
|
||||
## 4. Send a message
|
||||
<Tab title="npm i -g">
|
||||
Install globally, then run.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message":"Summarize the repository and suggest next steps."}'
|
||||
```
|
||||
```bash
|
||||
npm install -g @sandbox-agent/cli
|
||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
</Tab>
|
||||
|
||||
## 5. Read events
|
||||
<Tab title="Build from source">
|
||||
If you're running from source instead of the installed CLI.
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:2468/v1/sessions/my-session/events?offset=0&limit=50" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
```
|
||||
```bash
|
||||
cargo run -p sandbox-agent -- server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
```
|
||||
</Tab>
|
||||
|
||||
For streaming output, use SSE:
|
||||
<Tab title="TypeScript (local)">
|
||||
For local development, use `SandboxAgent.start()` to automatically spawn and manage the server as a subprocess.
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:2468/v1/sessions/my-session/events/sse?offset=0" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
```
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
For a single-turn stream (post a message and get one streamed response):
|
||||
const client = await SandboxAgent.start();
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -N -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages/stream" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{"message":"Hello"}'
|
||||
```
|
||||
This installs the binary and starts the server for you. No manual setup required.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 6. CLI shortcuts
|
||||
<AccordionGroup>
|
||||
<Accordion title="Running without tokens">
|
||||
If endpoint is not public, use `--no-token` to disable authentication. Most sandbox providers already secure their networking, so tokens are not required.
|
||||
</Accordion>
|
||||
<Accordion title="CORS">
|
||||
If you're calling the server from a browser, see the [CORS configuration guide](/docs/cors).
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Step>
|
||||
|
||||
The `sandbox-agent api` subcommand mirrors the HTTP API:
|
||||
<Step title="Install agents (optional)">
|
||||
To preinstall agents:
|
||||
|
||||
```bash
|
||||
sandbox-agent api sessions create my-session --agent claude --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
|
||||
```bash
|
||||
sandbox-agent install-agent claude
|
||||
sandbox-agent install-agent codex
|
||||
sandbox-agent install-agent opencode
|
||||
sandbox-agent install-agent amp
|
||||
```
|
||||
|
||||
sandbox-agent api sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
|
||||
If agents are not installed up front, they will be lazily installed when creating a session. It's recommended to pre-install agents then take a snapshot of the sandbox for faster coldstarts.
|
||||
</Step>
|
||||
|
||||
sandbox-agent api sessions send-message-stream my-session --message "Hello" --endpoint http://127.0.0.1:2468 --token "$SANDBOX_TOKEN"
|
||||
```
|
||||
<Step title="Create a session">
|
||||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
agentMode: "build",
|
||||
permissionMode: "default",
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="curl">
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"agent":"claude","agentMode":"build","permissionMode":"default"}'
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="CLI">
|
||||
```bash
|
||||
sandbox-agent api sessions create my-session \
|
||||
--agent claude \
|
||||
--endpoint http://127.0.0.1:2468 \
|
||||
--token "$SANDBOX_TOKEN"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step title="Send a message">
|
||||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
await client.postMessage("my-session", {
|
||||
message: "Summarize the repository and suggest next steps.",
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="curl">
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message":"Summarize the repository and suggest next steps."}'
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="CLI">
|
||||
```bash
|
||||
sandbox-agent api sessions send-message my-session \
|
||||
--message "Summarize the repository and suggest next steps." \
|
||||
--endpoint http://127.0.0.1:2468 \
|
||||
--token "$SANDBOX_TOKEN"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step title="Read events">
|
||||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
// Poll for events
|
||||
const events = await client.getEvents("my-session", { offset: 0, limit: 50 });
|
||||
|
||||
// Or stream events
|
||||
for await (const event of client.streamEvents("my-session", { offset: 0 })) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="curl">
|
||||
```bash
|
||||
# Poll for events
|
||||
curl "http://127.0.0.1:2468/v1/sessions/my-session/events?offset=0&limit=50" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
|
||||
# Stream events via SSE
|
||||
curl "http://127.0.0.1:2468/v1/sessions/my-session/events/sse?offset=0" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
|
||||
# Single-turn stream (post message and get streamed response)
|
||||
curl -N -X POST "http://127.0.0.1:2468/v1/sessions/my-session/messages/stream" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message":"Hello"}'
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="CLI">
|
||||
```bash
|
||||
# Poll for events
|
||||
sandbox-agent api sessions events my-session \
|
||||
--endpoint http://127.0.0.1:2468 \
|
||||
--token "$SANDBOX_TOKEN"
|
||||
|
||||
# Stream events via SSE
|
||||
sandbox-agent api sessions events-sse my-session \
|
||||
--endpoint http://127.0.0.1:2468 \
|
||||
--token "$SANDBOX_TOKEN"
|
||||
|
||||
# Single-turn stream
|
||||
sandbox-agent api sessions send-message-stream my-session \
|
||||
--message "Hello" \
|
||||
--endpoint http://127.0.0.1:2468 \
|
||||
--token "$SANDBOX_TOKEN"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step title="Test with Inspector">
|
||||
Open the [Inspector UI](https://inspect.sandboxagent.dev) to inspect session state using a GUI.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Build a Chat UI" icon="comments" href="/building-chat-ui">
|
||||
Learn how to build a chat interface for your agent.
|
||||
</Card>
|
||||
<Card title="Manage Sessions" icon="database" href="/manage-sessions">
|
||||
Persist and replay agent transcripts.
|
||||
</Card>
|
||||
<Card title="Deploy to a Sandbox" icon="box" href="/deploy">
|
||||
Deploy your agent to E2B, Daytona, or Vercel Sandboxes.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
|
|||
41
docs/sdks/python.mdx
Normal file
41
docs/sdks/python.mdx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: "Python"
|
||||
description: "Python client for managing sessions and streaming events."
|
||||
icon: "python"
|
||||
tag: "Coming Soon"
|
||||
---
|
||||
|
||||
The Python SDK is on our roadmap. It will provide a typed client for managing sessions and streaming events, similar to the TypeScript SDK.
|
||||
|
||||
In the meantime, you can use the [HTTP API](/http-api) directly with any HTTP client like `requests` or `httpx`.
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
base_url = "http://127.0.0.1:2468"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Create a session
|
||||
httpx.post(
|
||||
f"{base_url}/v1/sessions/my-session",
|
||||
headers=headers,
|
||||
json={"agent": "claude", "permissionMode": "default"}
|
||||
)
|
||||
|
||||
# Send a message
|
||||
httpx.post(
|
||||
f"{base_url}/v1/sessions/my-session/messages",
|
||||
headers=headers,
|
||||
json={"message": "Hello from Python"}
|
||||
)
|
||||
|
||||
# Get events
|
||||
response = httpx.get(
|
||||
f"{base_url}/v1/sessions/my-session/events",
|
||||
headers=headers,
|
||||
params={"offset": 0, "limit": 50}
|
||||
)
|
||||
events = response.json()["events"]
|
||||
```
|
||||
|
||||
Want the Python SDK sooner? [Open an issue](https://github.com/rivet-dev/sandbox-agent/issues) to let us know.
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
title: "TypeScript SDK"
|
||||
title: "TypeScript"
|
||||
description: "Use the generated client to manage sessions and stream events."
|
||||
icon: "js"
|
||||
---
|
||||
|
||||
The TypeScript SDK is generated from the OpenAPI spec that ships with the server. It provides a typed
|
||||
|
|
@ -147,4 +148,4 @@ The SDK exports OpenAPI-derived types for events, items, and capabilities:
|
|||
import type { UniversalEvent, UniversalItem, AgentCapabilities } from "sandbox-agent";
|
||||
```
|
||||
|
||||
See `docs/universal-api.mdx` for the universal schema fields and semantics.
|
||||
See the [API Reference](/api) for schema details.
|
||||
|
|
|
|||
|
|
@ -24,8 +24,3 @@ Disable it with:
|
|||
sandbox-agent server --no-telemetry
|
||||
```
|
||||
|
||||
Debug builds disable telemetry automatically. You can opt in with:
|
||||
|
||||
```bash
|
||||
SANDBOX_AGENT_TELEMETRY_DEBUG=1 sandbox-agent server
|
||||
```
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ body {
|
|||
color: var(--sa-text);
|
||||
}
|
||||
|
||||
/*
|
||||
a {
|
||||
color: var(--sa-primary);
|
||||
}
|
||||
|
|
@ -40,13 +41,6 @@ select {
|
|||
color: var(--sa-text);
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
background-color: var(--sa-card);
|
||||
border: 1px solid var(--sa-border);
|
||||
color: var(--sa-text);
|
||||
}
|
||||
|
||||
.card,
|
||||
.mintlify-card,
|
||||
.docs-card {
|
||||
|
|
@ -70,3 +64,4 @@ pre {
|
|||
.alert-danger {
|
||||
border-color: var(--sa-danger);
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: "Universal API"
|
||||
description: "Feature checklist and normalization rules."
|
||||
---
|
||||
|
||||
## Feature checklist
|
||||
|
||||
- [x] Session creation and lifecycle events
|
||||
- [x] Message streaming (assistant and tool messages)
|
||||
- [x] Tool call and tool result normalization
|
||||
- [x] File and image parts
|
||||
- [x] Human-in-the-loop questions
|
||||
- [x] Permission prompts and replies
|
||||
- [x] Plan approval normalization (Claude -> question)
|
||||
- [x] Event streaming over SSE
|
||||
- [ ] Persistent storage (out of scope)
|
||||
|
||||
## Normalization rules
|
||||
|
||||
- **Session ID** is always the client-provided ID.
|
||||
- **Agent session ID** is surfaced in events but never replaces the primary session ID.
|
||||
- **Tool calls** map to `UniversalMessagePart::ToolCall` and results to `ToolResult`.
|
||||
- **File and image parts** map to `AttachmentSource` with `Path`, `Url`, or base64 `Data`.
|
||||
|
||||
## Agent mode vs permission mode
|
||||
|
||||
- **agentMode**: behavior or system prompt strategy (build/plan/custom).
|
||||
- **permissionMode**: capability restrictions (default/plan/bypass).
|
||||
|
||||
These are separate concepts and must be configured independently.
|
||||
291
docs/universal-schema.mdx
Normal file
291
docs/universal-schema.mdx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
---
|
||||
title: "Universal Schema"
|
||||
description: "Reference for the universal event and item schema."
|
||||
icon: "brackets-curly"
|
||||
---
|
||||
|
||||
The universal schema normalizes events from all supported agents (Claude Code, Codex, OpenCode, Amp) into a consistent format. This lets you build UIs and persistence layers that work with any agent without special-casing.
|
||||
|
||||
The schema is defined in [OpenAPI format](https://github.com/rivet-dev/sandbox-agent/blob/main/docs/openapi.json). See the [HTTP API Reference](/api-reference) for endpoint documentation.
|
||||
|
||||
## UniversalEvent
|
||||
|
||||
Every event from the API is wrapped in a `UniversalEvent` envelope.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `event_id` | string | Unique identifier for this event |
|
||||
| `sequence` | integer | Monotonic sequence number within the session (starts at 1) |
|
||||
| `time` | string | RFC3339 timestamp |
|
||||
| `session_id` | string | Daemon-generated session identifier |
|
||||
| `native_session_id` | string? | Provider-native session/thread identifier (e.g., Codex `threadId`, OpenCode `sessionID`) |
|
||||
| `source` | string | Event origin: `agent` (native) or `daemon` (synthetic) |
|
||||
| `synthetic` | boolean | Whether this event was generated by the daemon to fill gaps |
|
||||
| `type` | string | Event type (see [Event Types](#event-types)) |
|
||||
| `data` | object | Event-specific payload |
|
||||
| `raw` | any? | Original provider payload (only when `include_raw=true`) |
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_abc123",
|
||||
"sequence": 1,
|
||||
"time": "2025-01-28T12:00:00Z",
|
||||
"session_id": "my-session",
|
||||
"native_session_id": "thread_xyz",
|
||||
"source": "agent",
|
||||
"synthetic": false,
|
||||
"type": "item.completed",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Event Types
|
||||
|
||||
### Session Lifecycle
|
||||
|
||||
| Type | Description | Data |
|
||||
|------|-------------|------|
|
||||
| `session.started` | Session has started | `{ metadata?: any }` |
|
||||
| `session.ended` | Session has ended | `{ reason, terminated_by }` |
|
||||
|
||||
**SessionEndedData**
|
||||
|
||||
| Field | Type | Values |
|
||||
|-------|------|--------|
|
||||
| `reason` | string | `completed`, `error`, `terminated` |
|
||||
| `terminated_by` | string | `agent`, `daemon` |
|
||||
|
||||
### Item Lifecycle
|
||||
|
||||
| Type | Description | Data |
|
||||
|------|-------------|------|
|
||||
| `item.started` | Item creation | `{ item }` |
|
||||
| `item.delta` | Streaming content delta | `{ item_id, native_item_id?, delta }` |
|
||||
| `item.completed` | Item finalized | `{ item }` |
|
||||
|
||||
Items follow a consistent lifecycle: `item.started` → `item.delta` (0 or more) → `item.completed`.
|
||||
|
||||
### HITL (Human-in-the-Loop)
|
||||
|
||||
| Type | Description | Data |
|
||||
|------|-------------|------|
|
||||
| `permission.requested` | Permission request pending | `{ permission_id, action, status, metadata? }` |
|
||||
| `permission.resolved` | Permission granted or denied | `{ permission_id, action, status, metadata? }` |
|
||||
| `question.requested` | Question pending user input | `{ question_id, prompt, options, status }` |
|
||||
| `question.resolved` | Question answered or rejected | `{ question_id, prompt, options, status, response? }` |
|
||||
|
||||
**PermissionEventData**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `permission_id` | string | Identifier for the permission request |
|
||||
| `action` | string | What the agent wants to do |
|
||||
| `status` | string | `requested`, `approved`, `denied` |
|
||||
| `metadata` | any? | Additional context |
|
||||
|
||||
**QuestionEventData**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `question_id` | string | Identifier for the question |
|
||||
| `prompt` | string | Question text |
|
||||
| `options` | string[] | Available answer options |
|
||||
| `status` | string | `requested`, `answered`, `rejected` |
|
||||
| `response` | string? | Selected answer (when resolved) |
|
||||
|
||||
### Errors
|
||||
|
||||
| Type | Description | Data |
|
||||
|------|-------------|------|
|
||||
| `error` | Runtime error | `{ message, code?, details? }` |
|
||||
| `agent.unparsed` | Parse failure | `{ error, location, raw_hash? }` |
|
||||
|
||||
The `agent.unparsed` event indicates the daemon failed to parse an agent payload. This should be treated as a bug.
|
||||
|
||||
## UniversalItem
|
||||
|
||||
Items represent discrete units of content within a session.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `item_id` | string | Daemon-generated identifier |
|
||||
| `native_item_id` | string? | Provider-native item/message identifier |
|
||||
| `parent_id` | string? | Parent item ID (e.g., tool call/result parented to a message) |
|
||||
| `kind` | string | Item category (see below) |
|
||||
| `role` | string? | Actor role for message items |
|
||||
| `status` | string | Lifecycle status |
|
||||
| `content` | ContentPart[] | Ordered list of content parts |
|
||||
|
||||
### ItemKind
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `message` | User or assistant message |
|
||||
| `tool_call` | Tool invocation |
|
||||
| `tool_result` | Tool execution result |
|
||||
| `system` | System message |
|
||||
| `status` | Status update |
|
||||
| `unknown` | Unrecognized item type |
|
||||
|
||||
### ItemRole
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `user` | User message |
|
||||
| `assistant` | Assistant response |
|
||||
| `system` | System prompt |
|
||||
| `tool` | Tool-related message |
|
||||
|
||||
### ItemStatus
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `in_progress` | Item is streaming or pending |
|
||||
| `completed` | Item is finalized |
|
||||
| `failed` | Item execution failed |
|
||||
|
||||
## Content Parts
|
||||
|
||||
The `content` array contains typed parts that make up an item's payload.
|
||||
|
||||
### text
|
||||
|
||||
Plain text content.
|
||||
|
||||
```json
|
||||
{ "type": "text", "text": "Hello, world!" }
|
||||
```
|
||||
|
||||
### json
|
||||
|
||||
Structured JSON content.
|
||||
|
||||
```json
|
||||
{ "type": "json", "json": { "key": "value" } }
|
||||
```
|
||||
|
||||
### tool_call
|
||||
|
||||
Tool invocation.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Tool name |
|
||||
| `arguments` | string | JSON-encoded arguments |
|
||||
| `call_id` | string | Unique call identifier |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": "read_file",
|
||||
"arguments": "{\"path\": \"/src/main.ts\"}",
|
||||
"call_id": "call_abc123"
|
||||
}
|
||||
```
|
||||
|
||||
### tool_result
|
||||
|
||||
Tool execution result.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `call_id` | string | Matching call identifier |
|
||||
| `output` | string | Tool output |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_result",
|
||||
"call_id": "call_abc123",
|
||||
"output": "File contents here..."
|
||||
}
|
||||
```
|
||||
|
||||
### file_ref
|
||||
|
||||
File reference with optional diff.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `path` | string | File path |
|
||||
| `action` | string | `read`, `write`, `patch` |
|
||||
| `diff` | string? | Unified diff (for patches) |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "file_ref",
|
||||
"path": "/src/main.ts",
|
||||
"action": "write",
|
||||
"diff": "@@ -1,3 +1,4 @@\n+import { foo } from 'bar';"
|
||||
}
|
||||
```
|
||||
|
||||
### image
|
||||
|
||||
Image reference.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `path` | string | Image file path |
|
||||
| `mime` | string? | MIME type |
|
||||
|
||||
```json
|
||||
{ "type": "image", "path": "/tmp/screenshot.png", "mime": "image/png" }
|
||||
```
|
||||
|
||||
### reasoning
|
||||
|
||||
Model reasoning/thinking content.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `text` | string | Reasoning text |
|
||||
| `visibility` | string | `public` or `private` |
|
||||
|
||||
```json
|
||||
{ "type": "reasoning", "text": "Let me think about this...", "visibility": "public" }
|
||||
```
|
||||
|
||||
### status
|
||||
|
||||
Status indicator.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `label` | string | Status label |
|
||||
| `detail` | string? | Additional detail |
|
||||
|
||||
```json
|
||||
{ "type": "status", "label": "Running tests", "detail": "3 of 10 passed" }
|
||||
```
|
||||
|
||||
## Source & Synthetics
|
||||
|
||||
### EventSource
|
||||
|
||||
The `source` field indicates who emitted the event:
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `agent` | Native event from the agent |
|
||||
| `daemon` | Synthetic event generated by the daemon |
|
||||
|
||||
### Synthetic Events
|
||||
|
||||
The daemon emits synthetic events (`synthetic: true`, `source: "daemon"`) to provide a consistent event stream across all agents. Common synthetics:
|
||||
|
||||
| Synthetic | When |
|
||||
|-----------|------|
|
||||
| `session.started` | Agent doesn't emit explicit session start |
|
||||
| `session.ended` | Agent doesn't emit explicit session end |
|
||||
| `item.started` | Agent doesn't emit item start events |
|
||||
| `item.delta` | Agent doesn't stream deltas natively |
|
||||
| `question.*` | Claude Code plan mode (from ExitPlanMode tool) |
|
||||
|
||||
### Raw Payloads
|
||||
|
||||
Pass `include_raw=true` to event endpoints to receive the original agent payload in the `raw` field. Useful for debugging or accessing agent-specific data not in the universal schema.
|
||||
|
||||
```typescript
|
||||
const events = await client.getEvents("my-session", { includeRaw: true });
|
||||
// events[0].raw contains the original agent payload
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue