Direct VM API
Use the direct AgentOS VM API or layer it into an AgentOS actor.
@rivet-dev/agentos ships both the direct AgentOs VM API and the agentOS()
actor API.
Direct VM vs actor
| Direct VM | Actor | |
|---|---|---|
| Persistence | In-memory by default (pluggable via mounts) | Persistent filesystem and sessions |
| Distributed state | Manage yourself | Built-in |
| Stateful VMs | Complex to run yourself | Built into Rivet |
| Sleep/wake | Manual dispose() / create() | Automatic |
| Events | Direct callbacks | Broadcast to all clients |
| Preview URLs | None | Built-in signed URL server |
| Multiplayer | N/A | Multiple clients per actor |
| Orchestration | N/A | Workflows, queues, cron |
| Agent-to-agent | Custom | Built into Rivet Actors |
| Authentication | Set up yourself | Docs |
- Use Rivet Actors for persistence, networking, and orchestration.
- Use
AgentOs.create()for direct VM control in a Node.js process. agentOS()returns an ordinary TypeScript Rivet actor definition — VM options plus normal actor state, actions, events, queues, connection types, and lifecycle hooks (onBeforeConnect).- AgentOS actions/events merge in automatically; their names are reserved.
- The VM is created lazily on the first AgentOS action after wake, disposed on
sleep — so a connection can subscribe before
vmBooted. - Creation input flows through
client.vm.create("key", { input })and reachescreateState(c, input)andonCreate(c, input).
Install
npm install @rivet-dev/agentos
Boot a VM
AgentOs.create() boots the VM in-process and returns a handle you call
directly — no actor runtime, no client/server split.
import { mkdirSync } from "node:fs";
import { resolve } from "node:path";
import pi from "@agentos-software/pi";
import { AgentOs } from "@rivet-dev/agentos";
// Create a VM directly with the AgentOS package — no actor runtime, no
// client/server split. `AgentOs.create()` boots the VM in-process.
mkdirSync(".agentos", { recursive: true });
const vm = await AgentOs.create({
database: {
type: "sqlite_file",
path: resolve(".agentos/agentos.sqlite"),
},
software: [pi],
});
const result = await vm.process.exec("echo hello");
console.log(result.stdout); // "hello\n"
Sidecar process
- Every VM runs inside a shared sidecar process, not its own process.
- All VMs default to a single process-global sidecar (the
defaultpool); each extra VM adds only a V8 isolate + its kernel state. - This keeps per-VM memory in the tens of MB and warm creation in single-digit ms (see Performance).
- Automatic for
agentOS(),AgentOs.create(), and Rivet Actors. - Disposing a VM tears down only that VM; the sidecar is reused for the host process lifetime.
- Advanced: the direct VM API exposes explicit sidecar handles to isolate a group of VMs in their own process.
import { AgentOs } from "@rivet-dev/agentos";
// 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 only
await b.dispose();
await sidecar.dispose(); // tears down the shared process
Filesystem
await vm.filesystem.writeFile("/home/agentos/hello.txt", "Hello, world!");
const content = await vm.filesystem.readFile("/home/agentos/hello.txt");
console.log(new TextDecoder().decode(content));
await vm.filesystem.mkdir("/home/agentos/src");
await vm.filesystem.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 vm.filesystem.readdirRecursive("/home/agentos");
for (const entry of entries) {
console.log(entry.type, entry.path);
}
Contexts
Contexts retain JavaScript/TypeScript or Python globals between attached calls.
Create one explicitly, then pass its contextId; unknown IDs fail instead of
silently creating fresh state. A context pins to the first inline language used,
although JavaScript and TypeScript intentionally share one isolate.
await vm.createContext("analysis");
await vm.typescript.execute("const answer: number = 40", {
contextId: "analysis",
});
const result = await vm.javascript.evaluate<number>("answer + 2", {
contextId: "analysis",
});
console.log(result.outcome === "succeeded" ? result.value : result.error);
await vm.contexts.reset("analysis");
await vm.contexts.delete("analysis");
Contexts live for the VM lifetime. In an actor, sleep disposes the VM and its contexts, so create a context lazily on the first stateful action after wake instead of persisting and blindly reusing its ID.
Processes
process.spawn()and languagespawnmethods return a numericpid.onProcessOutput(pid, …)— unified stdout/stderr stream.onProcessExit(pid, …)— completion.
// One-shot execution
const result = await vm.process.exec("ls -la /home/agentos");
console.log(result.stdout);
// Long-running process with portable output and exit subscriptions.
await vm.filesystem.writeFile(
"/tmp/server.mjs",
'import http from "http"; http.createServer((req, res) => res.end("ok")).listen(3000); console.log("listening");',
);
const { pid } = await vm.process.spawn("node", ["/tmp/server.mjs"]);
vm.onProcessOutput(pid, (event) =>
console.log(event.stream, new TextDecoder().decode(event.data)),
);
vm.onProcessExit(pid, (event) => console.log("exited:", event.exitCode));
// Write to stdin
await vm.process.writeStdin(pid, "some input\n");
// Stop or kill
await vm.process.signal(pid, "SIGTERM");
Agent sessions
openSessionnegotiates the adapter and resolves without a value.- Omit
sessionId→main. CallgetSessiononly for durable metadata. - Register
onSessionEventbefore prompting for live deltas. - Native ACP updates and permission request/response share the sequenced
onSessionEventstream. - Durable entries recover via
readHistory; ephemeral agent/thought deltas do not.
// openSession() negotiates ACP and durably records the session in SQLite.
await vm.sessions.open({
agent: "pi",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
permissionPolicy: "ask",
});
// Native ACP updates and permission records share one session event union.
vm.onSessionEvent((event) => {
if (event.type === "permission_request") {
console.log("Permission:", event.requestId, event.toolCall);
} else {
console.log(event.durability, event);
}
});
const result = await vm.sessions.prompt({
content: [{ type: "text", text: "Write a hello world script" }],
});
console.log(result.message?.content ?? []);
// Unload releases the adapter but preserves SQLite history for restoration.
await vm.sessions.unload();
Networking
httpRequest({ port, path, ... }) reaches a server inside the VM and returns a
bounded, serializable response DTO.
// Start a server inside the VM
await vm.filesystem.writeFile(
"/tmp/app.mjs",
'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);',
);
await vm.process.spawn("node", ["/tmp/app.mjs"]);
// httpRequest reaches services running in the VM with serializable DTOs.
const response = await vm.network.httpRequest({ port: 3000, path: "/" });
console.log(new TextDecoder().decode(response.body));
Cron jobs
- Run an
"exec"command or a"session"prompt on a schedule. - Fired jobs surface through
onCronEvent.
const job = vm.cron.schedule({
id: "cleanup",
schedule: "0 * * * *",
action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache"] },
});
console.log("Scheduled:", job.id);
// Run an agent session on a schedule
vm.cron.schedule({
schedule: "0 9 * * *",
action: {
type: "session",
agentType: "pi",
prompt: "Review the logs and summarize any errors",
options: { cwd: "/workspace" },
},
});
vm.onCronEvent((event) => {
console.log("Cron event:", event.type, event.jobId);
});
console.log(vm.cron.list());
Mounts
- Configure filesystem backends at boot.
- Native mount plugins (host directories, S3, etc.) are passed via
plugin, each with anidand aconfig.
import { AgentOs } from "@rivet-dev/agentos";
// Configure filesystem backends at boot. Native mount plugins (host
// directories, S3, etc.) are passed via `plugin`, each identified by an `id`
// and a `config` object.
const vm = await AgentOs.create({
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/" } },
},
],
});
Configuration reference
- All VM config is a single flat object passed to
AgentOs.create(). - The
agentOS()actor accepts the same options and layers persistence, sleep/wake, and preview URLs on top.
import { AgentOs, nodeModulesMount } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";
// The full AgentOs.create() configuration surface. The agentOS() actor accepts
// this same options object and layers persistence, sleep/wake, and preview URLs
// on top.
const vm = await AgentOs.create({
// 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],
// Also install the default software bundle (sh + coreutils). Defaults to true;
// set false for a bare VM with only the software you list.
defaultSoftware: true,
// Ports exempt from SSRF checks (for testing against host-side mock servers)
loopbackExemptPorts: [3000],
// Sidecar placement — defaults to the shared `default` pool
sidecar: { kind: "shared" },
});
Session events
onSessionEventreceives a union of exact native ACPSessionUpdate,RequestPermissionRequest, andRequestPermissionResponsepayloads wrapped with AgentOS durability metadata.- Register before prompting.
- On reconnect, read durable history after your last sequence and dedup by
(sessionId, sequence).
import { AgentOs } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";
// ACP updates and interactive permission records share one durable event union.
const vm = await AgentOs.create({ software: [pi] });
await vm.sessions.open({ agent: "pi", permissionPolicy: "ask" });
// Runs for every event on this session.
vm.onSessionEvent((event) => {
if (event.type === "permission_request") {
console.log("Permission request:", event.requestId, event.toolCall);
} else {
console.log("Session update:", event.durability, event);
}
});
Timeouts and sleep
- Action timeouts and automatic sleep/wake are
agentOS()actor features, not the direct VM API. - A direct VM stays alive until
dispose(). See Persistence & Sleep.