# Direct VM API

`@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](#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](/agentos/docs/agent-to-agent) |
| Authentication | Set up yourself | [Docs](/agentos/docs/authentication) |

- Use [Rivet Actors](https://rivet.dev/docs/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

```bash
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.

vm.ts:

```ts
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](/agentos/docs/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.

advanced.ts:

```ts
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

vm.ts:

```ts
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.

vm.ts:

```ts
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.

vm.ts:

```ts
// 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 `sessionId` → `main`. 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.

vm.ts:

```ts
// 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.

vm.ts:

```ts
// 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`.

vm.ts:

```ts
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`.

mounts.ts:

```ts
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](/agentos/docs/quickstart) accepts the same options and layers
  persistence, sleep/wake, and preview URLs on top.

config-reference.ts:

```ts
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](#mounts) and [Software](/agentos/docs/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)`.

hooks.ts:

```ts
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](/agentos/docs/quickstart)
  features, not the direct VM API.
- A direct VM stays alive until `dispose()`. See
  [Persistence & Sleep](/agentos/docs/persistence).
