Core Package
Use @rivet-dev/agentos-core standalone for direct VM control without the Rivet Actor runtime.
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.
agentOS() returns an ordinary TypeScript Rivet actor definition. Its config accepts the core VM options together with normal actor state, actions, events, queues, connection types, and lifecycle hooks such as onBeforeConnect. AgentOS actions and events are merged in automatically; their names are reserved so they cannot be accidentally shadowed. After a wake, the actor creates the core SDK VM lazily on the first AgentOS action and disposes it on sleep. This lets a connection subscribe before the vmBooted event is emitted.
Creation input is inferred from the actor definition and is passed through normal client creation options: client.vm.create("key", { input }). The same input reaches createState(c, input) and onCreate(c, input).
Install
npm install @rivet-dev/agentos-core
Boot a VM
Create a VM and drive it directly — no actor runtime, no client/server split. AgentOs.create() boots the VM in-process and returns a handle you call directly:
import { AgentOs } from "@rivet-dev/agentos-core";
import pi from "@agentos-software/pi";
// Create a VM directly with the core package — no actor runtime, no
// client/server split. `AgentOs.create()` boots the VM in-process.
const vm = await AgentOs.create({ software: [pi] });
const result = await vm.exec("echo hello");
console.log(result.stdout); // "hello\n"
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 only
await b.dispose();
await sidecar.dispose(); // tears down the shared process
Filesystem
await vm.writeFile("/home/agentos/hello.txt", "Hello, world!");
const content = await vm.readFile("/home/agentos/hello.txt");
console.log(new TextDecoder().decode(content));
await vm.mkdir("/home/agentos/src");
await vm.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.readdirRecursive("/home/agentos");
for (const entry of entries) {
console.log(entry.type, entry.path);
}
Processes
Portable spawn() is callback-free. Subscribe to its unified stdout/stderr stream with onProcessOutput(pid, …) and to completion with onProcessExit(pid, …):
// One-shot execution
const result = await vm.exec("ls -la /home/agentos");
console.log(result.stdout);
// Long-running process with portable output and exit subscriptions.
await vm.writeFile(
"/tmp/server.mjs",
'import http from "http"; http.createServer((req, res) => res.end("ok")).listen(3000); console.log("listening");',
);
const { pid } = vm.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.writeProcessStdin(pid, "some input\n");
// Stop or kill
vm.stopProcess(pid);
Agent sessions
openSession negotiates the adapter and resolves without a value. Omit sessionId to use main; call getSession separately only when you need durable metadata. Native ACP updates and interactive permission request/response variants share the sequenced onSessionEvent stream:
// openSession() negotiates ACP and durably records the session in SQLite.
await vm.openSession({
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.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.unloadSession();
Register onSessionEvent before prompting to receive live deltas. Durable entries can be recovered with readHistory; ephemeral agent/thought deltas cannot.
Networking
httpRequest({ port, path, ... }) reaches a server running inside the VM and returns a bounded, serializable response DTO:
// Start a server inside the VM
await vm.writeFile(
"/tmp/app.mjs",
'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);',
);
vm.spawn("node", ["/tmp/app.mjs"]);
// httpRequest reaches services running in the VM with serializable DTOs.
const response = await vm.httpRequest({ port: 3000, path: "/" });
console.log(new TextDecoder().decode(response.body));
Cron jobs
Cron jobs run an "exec" command or a "session" prompt on a schedule. Fired jobs are surfaced through the onCronEvent callback:
const job = vm.scheduleCron({
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.scheduleCron({
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.listCronJobs());
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 } from "@rivet-dev/agentos-core";
// 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 configuration is passed to AgentOs.create() as a single flat object. This is the consolidated config block to copy and adapt. The agentOS() actor accepts the same options and layers persistence, sleep/wake, and preview URLs on top:
import { AgentOs, nodeModulesMount } from "@rivet-dev/agentos-core";
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" },
});
The top-level fields are documented inline above. See Mounts and Software.
Session events
With the core package, onSessionEvent receives a generic union containing exact native ACP SessionUpdate, RequestPermissionRequest, and RequestPermissionResponse payloads wrapped with AgentOS durability metadata. Register it before prompting. On reconnect, also read durable history after your last sequence and deduplicate by (sessionId, sequence):
import { AgentOs } from "@rivet-dev/agentos-core";
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.openSession({ 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 features of the agentOS() actor, not the core package. A core VM stays alive until you call dispose(). See Persistence & Sleep for the actor’s sleep lifecycle.