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:
Nathan Flurry 2026-01-28 05:07:15 -08:00 committed by GitHub
parent a6f77f3008
commit 08d299a3ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1996 additions and 1004 deletions

View file

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