Core Package
agentOS vs agentOS Core
Section titled “agentOS vs agentOS Core”The agentOS() actor (from @rivet-dev/agentos) wraps the core package and adds:
Core (@rivet-dev/agentos-core) | Actor (@rivet-dev/agentos) | |
|---|---|---|
| Persistence | In-memory by default (pluggable via mounts) | Persistent filesystem and sessions |
| Distributed state | Manage yourself | Built-in distributed statefulness |
| Stateful VMs | Complex to run yourself | Built into Rivet |
| Sleep/wake | Manual dispose() / create() | Automatic |
| Events | Direct callbacks | Broadcasted to all connected clients |
| Preview URLs | None | Built-in signed URL server |
| Multiplayer | N/A | Multiple clients on same actor |
| Orchestration | N/A | Workflows, queues, cron |
| Agent-to-agent communication | Custom | Built into Rivet Actors |
| Authentication | Set up yourself | Documentation |
We recommend using Rivet Actors because they provide a portable way to run agentOS() on any infrastructure with built-in persistence, networking, and orchestration. Use the core package if you need the most bare-bones implementation possible.
Install
Section titled “Install”npm install @rivet-dev/agentos-coreBoot a VM
Section titled “Boot a VM”Define the actor on the server:
import { agentOS, setup } from "@rivet-dev/agentos";import pi from "@agentos-software/pi";
const vm = agentOS({ software: [pi],});
export const registry = setup({ use: { vm } });registry.start();Then drive it from a typed client:
import { createClient } from "@rivet-dev/agentos/client";import type { registry } from "./server";
const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });const handle = client.vm.getOrCreate("my-agent");
const result = await handle.exec("echo hello");console.log(result.stdout); // "hello\n"Sidecar process
Section titled “Sidecar process”Every VM runs inside a shared sidecar process rather than a process of its own. By default all VMs are tenants of a single, process-global sidecar (the default pool), so each additional VM only adds its marginal cost — a V8 isolate plus its kernel state — instead of a whole OS process. This is what keeps per-VM memory in the tens of MB and warm VM creation in the single-digit milliseconds (see Benchmarks).
This is automatic — agentOS() and AgentOs.create() use the shared default sidecar with no configuration, and the same applies to Rivet Actors (each actor’s VM is a tenant of the shared process). Disposing a VM tears down only that VM; the shared sidecar process is reused across VMs and stays alive for the lifetime of the host process.
For advanced cases the core package exposes explicit sidecar handles so you can isolate a group of VMs in their own process:
import { AgentOs } from "@rivet-dev/agentos-core";
// One dedicated sidecar process hosting multiple VMs.const sidecar = await AgentOs.createSidecar();const a = await AgentOs.create({ sidecar: { kind: "explicit", handle: sidecar } });const b = await AgentOs.create({ sidecar: { kind: "explicit", handle: sidecar } });
await a.dispose(); // tears down VM a onlyawait b.dispose();await sidecar.dispose(); // tears down the shared processFilesystem
Section titled “Filesystem”await handle.writeFile("/home/agentos/hello.txt", "Hello, world!");const content = await handle.readFile("/home/agentos/hello.txt");console.log(new TextDecoder().decode(content));
await handle.mkdir("/home/agentos/src");await handle.writeFiles([ { path: "/home/agentos/src/index.ts", content: "console.log('hi');" }, { path: "/home/agentos/src/utils.ts", content: "export const add = (a: number, b: number) => a + b;" },]);
const entries = await handle.readdirRecursive("/home/agentos");for (const entry of entries) { console.log(entry.type, entry.path);}Processes
Section titled “Processes”Long-running process output is delivered over the live processOutput / processExit events on a connection rather than per-pid callbacks:
// One-shot executionconst result = await handle.exec("ls -la /home/agentos");console.log(result.stdout);
// Long-running process with streaming outputawait handle.writeFile( "/tmp/server.mjs", 'import http from "http"; http.createServer((req, res) => res.end("ok")).listen(3000); console.log("listening");',);const { pid } = await handle.spawn("node", ["/tmp/server.mjs"]);
const conn = handle.connect();conn.on("processOutput", (data) => { if (data.pid === pid && data.stream === "stdout") { console.log("stdout:", new TextDecoder().decode(data.data)); }});conn.on("processExit", (data) => { if (data.pid === pid) console.log("exited:", data.exitCode);});
// Write to stdinawait handle.writeProcessStdin(pid, "some input\n");
// Stop or killawait handle.stopProcess(pid);Agent sessions
Section titled “Agent sessions”createSession returns a session record. All session operations take its sessionId. Session events and permission requests are delivered over the live connection (sessionEvent / permissionRequest):
const conn = handle.connect();
// Stream events (each event is a JSON-RPC notification)conn.on("sessionEvent", (data) => { console.log(data.event.method, data.event.params);});
// Handle permissionsconn.on("permissionRequest", (data) => { console.log("Permission:", data.request.description); // Reply with "once", "always", or "reject" void handle.respondPermission(data.sessionId, data.request.permissionId, "once");});
const session = await handle.createSession("pi", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },});
// Send a prompt. sendPrompt() resolves to { response, text }, where `text` is// the accumulated agent message text and `response` is the raw JSON-RPC response.const { text } = await handle.sendPrompt(session.sessionId, "Write a hello world script");console.log(text);
// Configure the sessionawait handle.setModel(session.sessionId, "claude-sonnet-4-6");await handle.setMode(session.sessionId, "plan");
await handle.closeSession(session.sessionId);Subscribe to sessionEvent before sending a prompt so you do not miss the live stream. Persisted history can be read back later with getSessionEvents().
Interactive shell
Section titled “Interactive shell”const { shellId } = await handle.openShell();
const conn = handle.connect();conn.on("shellData", (data) => { if (data.shellId === shellId) { process.stdout.write(new TextDecoder().decode(data.data)); }});
await handle.writeShell(shellId, "echo hello from shell\n");
// Resize terminalawait handle.resizeShell(shellId, 120, 40);
await handle.closeShell(shellId);Networking
Section titled “Networking”// Start a server inside the VMawait handle.writeFile( "/tmp/app.mjs", 'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);',);await handle.spawn("node", ["/tmp/app.mjs"]);
// Fetch from itconst response = await handle.vmFetch(3000, "/");console.log(new TextDecoder().decode(response.body));Cron jobs
Section titled “Cron jobs”Cron jobs run an "exec" command or a "session" prompt on a schedule. Fired jobs are surfaced over the live cronEvent connection:
const { id } = await handle.scheduleCron({ id: "cleanup", schedule: "0 * * * *", action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache"] },});console.log("Scheduled:", id);
// Run an agent session on a scheduleawait handle.scheduleCron({ schedule: "0 9 * * *", action: { type: "session", agentType: "pi", prompt: "Review the logs and summarize any errors", options: { cwd: "/workspace" }, },});
const conn = handle.connect();conn.on("cronEvent", (data) => { console.log("Cron event:", data.event.id, data.event.schedule);});
console.log(await handle.listCronJobs());Mounts
Section titled “Mounts”Configure filesystem backends at boot time.
Native mount plugins (host directories, S3, etc.) are passed via plugin, each
identified by an id and a config object.
import { agentOS, setup } from "@rivet-dev/agentos";
const vm = agentOS({ mounts: [ // Host directory (read-only) { path: "/mnt/code", plugin: { id: "host_dir", config: { hostPath: "/path/to/repo" } }, readOnly: true, }, // S3 bucket { path: "/mnt/data", plugin: { id: "s3", config: { bucket: "my-bucket", prefix: "agent/" } }, }, ],});
export const registry = setup({ use: { vm } });registry.start();agentOS() configuration reference
Section titled “agentOS() configuration reference”When you use the agentOS() actor, all VM configuration is passed to the factory as a single flat object. This is the consolidated config block to copy and adapt:
import { agentOS, nodeModulesMount, setup } from "@rivet-dev/agentos";import pi from "@agentos-software/pi";
const vm = agentOS({ // Filesystems to mount at boot. Use nodeModulesMount() to expose a host // node_modules tree at /root/node_modules. mounts: [nodeModulesMount("/path/to/project/node_modules")], // Software packages to install in the VM (see /docs/software) software: [pi], // Ports exempt from SSRF checks loopbackExemptPorts: [3000], // Extra instructions appended to agent system prompts additionalInstructions: "Always write tests first.",
// Preview URL token lifetimes preview: { defaultExpiresInSeconds: 3600, // 1 hour (default) maxExpiresInSeconds: 86400, // 24 hours (default) },
// Lifecycle hooks (see below) onSessionEvent: async (sessionId, event) => { console.log("Session event:", sessionId, event.method); }, onPermissionRequest: async (sessionId, request) => { console.log("Permission request:", sessionId, request.permissionId); },});
export const registry = setup({ use: { vm } });registry.start();The top-level fields are documented inline above. See Mounts, Software, and (for the hooks) Approvals.
Lifecycle hooks
Section titled “Lifecycle hooks”onPermissionRequest(sessionId, request) fires when an agent requests permission. onSessionEvent(sessionId, event) is a server-side hook called once for every session event: unlike the client-side sessionEvent connection subscription, it runs in the actor for every event regardless of connected clients, making it the place for server-side logging, persistence, or side effects.
import { agentOS } from "@rivet-dev/agentos";
export const vm = agentOS({ // Runs once per session event, server-side, for every session. onSessionEvent: async (sessionId, event) => { console.log("Session event:", sessionId, event.method); },});Timeouts
Section titled “Timeouts”| Setting | Default | Description |
|---|---|---|
| Action timeout | 15 minutes | Maximum time for any single action |
| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |
These are set internally by the agentOS() factory and cannot be overridden per-call. See Persistence & Sleep for details on the sleep lifecycle.