mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 15:04:55 +00:00
150 lines
5.3 KiB
Text
150 lines
5.3 KiB
Text
---
|
|
title: "Multiplayer"
|
|
description: "Use Rivet Actors to coordinate shared sessions."
|
|
icon: "users"
|
|
---
|
|
|
|
For multiplayer orchestration, use [Rivet Actors](https://rivet.dev/docs/actors).
|
|
|
|
Recommended model:
|
|
|
|
- One actor per collaborative workspace/thread.
|
|
- The actor owns Sandbox Agent session lifecycle and persistence.
|
|
- Clients connect to the actor and receive realtime broadcasts.
|
|
|
|
Use [actor keys](https://rivet.dev/docs/actors/keys) to map each workspace to one actor, [events](https://rivet.dev/docs/actors/events) for realtime updates, and [lifecycle hooks](https://rivet.dev/docs/actors/lifecycle) for cleanup.
|
|
|
|
## Example
|
|
|
|
<CodeGroup>
|
|
|
|
```ts Actor (server)
|
|
import { actor, setup } from "rivetkit";
|
|
import { SandboxAgent, type SessionPersistDriver, type SessionRecord, type SessionEvent, type ListPageRequest, type ListPage, type ListEventsRequest } from "sandbox-agent";
|
|
|
|
// Inline Rivet persist driver — copy into your project.
|
|
// See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-rivet
|
|
interface ActorContextLike { state: Record<string, unknown>; }
|
|
interface RivetPersistData { sessions: Record<string, SessionRecord>; events: Record<string, SessionEvent[]>; }
|
|
type RivetPersistState = { _sandboxAgentPersist: RivetPersistData };
|
|
|
|
class RivetSessionPersistDriver implements SessionPersistDriver {
|
|
private readonly stateKey: string;
|
|
private readonly ctx: ActorContextLike;
|
|
constructor(ctx: ActorContextLike, options: { stateKey?: string } = {}) {
|
|
this.ctx = ctx;
|
|
this.stateKey = options.stateKey ?? "_sandboxAgentPersist";
|
|
if (!this.ctx.state[this.stateKey]) {
|
|
this.ctx.state[this.stateKey] = { sessions: {}, events: {} };
|
|
}
|
|
}
|
|
private get data(): RivetPersistData { return this.ctx.state[this.stateKey] as RivetPersistData; }
|
|
async getSession(id: string) { const s = this.data.sessions[id]; return s ? { ...s } : undefined; }
|
|
async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {
|
|
const sorted = Object.values(this.data.sessions).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
|
|
const offset = Number(request.cursor ?? 0);
|
|
const limit = request.limit ?? 100;
|
|
const slice = sorted.slice(offset, offset + limit);
|
|
return { items: slice, nextCursor: offset + slice.length < sorted.length ? String(offset + slice.length) : undefined };
|
|
}
|
|
async updateSession(session: SessionRecord) { this.data.sessions[session.id] = { ...session }; if (!this.data.events[session.id]) this.data.events[session.id] = []; }
|
|
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
|
|
const all = [...(this.data.events[request.sessionId] ?? [])].sort((a, b) => a.eventIndex - b.eventIndex || a.id.localeCompare(b.id));
|
|
const offset = Number(request.cursor ?? 0);
|
|
const limit = request.limit ?? 100;
|
|
const slice = all.slice(offset, offset + limit);
|
|
return { items: slice, nextCursor: offset + slice.length < all.length ? String(offset + slice.length) : undefined };
|
|
}
|
|
async insertEvent(sessionId: string, event: SessionEvent) { const events = this.data.events[sessionId] ?? []; events.push({ ...event, payload: JSON.parse(JSON.stringify(event.payload)) }); this.data.events[sessionId] = events; }
|
|
}
|
|
|
|
type WorkspaceState = RivetPersistState & {
|
|
sandboxId: string;
|
|
baseUrl: string;
|
|
};
|
|
|
|
export const workspace = actor({
|
|
createState: async () => {
|
|
return {
|
|
sandboxId: "sbx_123",
|
|
baseUrl: "http://127.0.0.1:2468",
|
|
} satisfies Partial<WorkspaceState>;
|
|
},
|
|
|
|
createVars: async (c) => {
|
|
const persist = new RivetSessionPersistDriver(c);
|
|
const sdk = await SandboxAgent.connect({
|
|
baseUrl: c.state.baseUrl,
|
|
persist,
|
|
});
|
|
|
|
const session = await sdk.resumeOrCreateSession({ id: "default", agent: "codex" });
|
|
|
|
const unsubscribe = session.onEvent((event) => {
|
|
c.broadcast("session.event", event);
|
|
});
|
|
|
|
return { sdk, session, unsubscribe };
|
|
},
|
|
|
|
actions: {
|
|
getSessionInfo: (c) => ({
|
|
workspaceId: c.key[0],
|
|
sandboxId: c.state.sandboxId,
|
|
}),
|
|
|
|
prompt: async (c, input: { userId: string; text: string }) => {
|
|
c.broadcast("chat.user", {
|
|
userId: input.userId,
|
|
text: input.text,
|
|
createdAt: Date.now(),
|
|
});
|
|
|
|
await c.vars.session.prompt([{ type: "text", text: input.text }]);
|
|
},
|
|
},
|
|
|
|
onSleep: async (c) => {
|
|
c.vars.unsubscribe?.();
|
|
await c.vars.sdk.dispose();
|
|
},
|
|
});
|
|
|
|
export const registry = setup({
|
|
use: { workspace },
|
|
});
|
|
```
|
|
|
|
```ts Client (browser)
|
|
import { createClient } from "rivetkit/client";
|
|
import type { registry } from "./actors";
|
|
|
|
const client = createClient<typeof registry>({
|
|
endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!,
|
|
});
|
|
|
|
const workspaceId = "workspace-42";
|
|
const room = client.workspace.getOrCreate([workspaceId]);
|
|
const conn = room.connect();
|
|
|
|
conn.on("chat.user", (event) => {
|
|
console.log("user message", event);
|
|
});
|
|
|
|
conn.on("session.event", (event) => {
|
|
console.log("sandbox event", event);
|
|
});
|
|
|
|
await conn.prompt({
|
|
userId: "user-123",
|
|
text: "Propose a refactor plan for auth middleware.",
|
|
});
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
## Notes
|
|
|
|
- Keep sandbox calls actor-only. Browser clients should not call Sandbox Agent directly.
|
|
- Inline the Rivet persist driver (shown above) so session history persists in actor state.
|
|
- For client connection patterns, see [Rivet JavaScript client](https://rivet.dev/docs/clients/javascript).
|