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"Filesystem
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.