Skip to main content
More

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 VMActor
PersistenceIn-memory by default (pluggable via mounts)Persistent filesystem and sessions
Distributed stateManage yourselfBuilt-in
Stateful VMsComplex to run yourselfBuilt into Rivet
Sleep/wakeManual dispose() / create()Automatic
EventsDirect callbacksBroadcast to all clients
Preview URLsNoneBuilt-in signed URL server
MultiplayerN/AMultiple clients per actor
OrchestrationN/AWorkflows, queues, cron
Agent-to-agentCustomBuilt into Rivet Actors
AuthenticationSet up yourselfDocs
  • 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 reaches createState(c, input) and onCreate(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 default pool); 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 language spawn methods return a numeric pid.
  • 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

  • openSession negotiates the adapter and resolves without a value.
  • Omit sessionIdmain. Call getSession only for durable metadata.
  • Register onSessionEvent before prompting for live deltas.
  • Native ACP updates and permission request/response share the sequenced onSessionEvent stream.
  • 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 an id and a config.
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" },
});

See Mounts and Software.

Session events

  • onSessionEvent receives a union of exact native ACP SessionUpdate, RequestPermissionRequest, and RequestPermissionResponse payloads 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