# Crash Course

> **NOTE:** agentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).

## When to Use agentOS

- **Coding agents**: Run any coding agent with full OS access, file editing, shell execution, and tool use.
- **Automated pipelines**: CI-like workflows where agents clone repos, fix bugs, run tests, and open PRs.
- **Multi-agent systems**: Coordinators dispatching to specialized agents, review pipelines, planning chains.
- **Scheduled maintenance**: Cron-based agents that audit code, update dependencies, or generate reports.
- **Collaborative workspaces**: Multiple users observing and interacting with the same agent session in realtime.

## Minimal Project

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

// Subscribe to streaming events
const conn = agent.connect();
conn.on("sessionEvent", (event) => {
	console.log(event);
});

// Create a session and send a prompt
await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
const response = await agent.sessions.prompt({
	content: [
		{ type: "text", text: "Write a hello world script to /workspace/hello.js" },
	],
});
console.log(response.message?.content ?? []);

// Read the file the agent created
const content = await agent.filesystem.readFile("/workspace/hello.js");
console.log(new TextDecoder().decode(content));
```

server.ts:

```ts
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();
```

After the quickstart, customize your agent with the [Registry](/registry).

## Agents

### Sessions & Transcripts

Create agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles.

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

// Stream events as they arrive
const conn = agent.connect();
conn.on("sessionEvent", (event) => {
	console.log(event);
});

// Create a session
await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});

// Send a prompt and wait for the response
const response = await agent.sessions.prompt({
	content: [{ type: "text", text: "List all files in the home directory" }],
});
console.log(response.message?.content ?? []);
```

server.ts:

```ts
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();
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/sessions)*

### Approvals

Approve or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads.

server.ts:

```ts
import pi from "@agentos-software/pi";
import { agentOS, setup } from "@rivet-dev/agentos";

// Observe native ACP permission requests through the ordinary event hook.
const vm = agentOS({
	software: [pi],
	onSessionEvent: async (_c, sessionId, event) => {
		if (event.type === "permission_request") {
			console.log("Permission requested", sessionId, event.requestId);
		}
	},
});

export const registry = setup({ use: { vm } });
registry.start();
```

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const agent = client.vm.getOrCreate("my-agent");

// Handle permission requests through the generic session event stream.
const connection = agent.connect();
connection.on("sessionEvent", (event) => {
	if (event.type !== "permission_request") return;
	const option = event.options.find(
		(candidate) => candidate.kind === "allow_once",
	);
	if (!option) return;
	agent.sessions
		.respondPermission({
			sessionId: event.sessionId,
			requestId: event.requestId,
			optionId: option.optionId,
		})
		.catch((error) => console.error("Permission response failed:", error));
});

await agent.sessions.open({
	agent: "pi",
	// Required for permission_request events; the default is allow_all.
	permissionPolicy: "ask",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agent.sessions.prompt({
	content: [{ type: "text", text: "Create /workspace/output.txt" }],
});
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/approvals)*

### Bindings

Expose your JavaScript functions to agents as CLI commands inside the VM. Each binding group becomes a binary at `/usr/local/bin/agentos-{name}`, and each binding becomes a subcommand with flags auto-generated from its Zod input schema. The server below defines a `weather` binding group with a `forecast` binding; the client opens a session and prompts the agent, which calls the binding itself as a shell command.

server.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import { z } from "zod";

// Define a group of bindings (host functions). Each binding has a Zod input
// schema and an `execute` handler that runs on the host. The group is exposed to
// the agent as a CLI command at /usr/local/bin/agentos-{name} inside the VM.
const weatherBindings = {
	name: "weather",
	description: "Weather data bindings",
	bindings: {
		forecast: {
			description: "Get the weather forecast for a city",
			inputSchema: z.object({
				city: z.string().describe("City name"),
				days: z.number().optional().describe("Number of days"),
			}),
			execute: async (input: { city: string; days?: number }) => {
				const res = await fetch(
					`https://api.weather.example/forecast?city=${input.city}&days=${input.days ?? 3}`,
				);
				return res.json();
			},
			examples: [
				{
					description: "3-day forecast for Paris",
					input: { city: "Paris", days: 3 },
				},
			],
		},
	},
};

const vm = agentOS({
	bindings: [weatherBindings],
});

export const registry = setup({ use: { vm } });
registry.start();
```

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

// The agent invokes the binding itself as a shell command:
//   agentos-weather forecast --city Paris --days 3
await agent.sessions.open({
	agent: "claude",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agent.sessions.prompt({
	content: [{ type: "text", text: "What's the weather in Paris?" }],
});
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/bindings) or [Documentation](/agentos/docs/bindings)*

### Agent-to-Agent

Let one agent call another through a [binding](/agentos/docs/bindings). The coder gets a `review` binding it invokes itself, which bridges into the reviewer's isolated VM.

server.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import { createClient } from "@rivet-dev/agentos/client";
import { z } from "zod";
import pi from "@agentos-software/pi";

// The reviewer is its own isolated agent VM.
const reviewer = agentOS({ software: [pi] });

// The coder gets a `review` binding collection it can call itself: it copies a file from the
// coder's VM into the reviewer's VM and asks the reviewer to review it.
const coder = agentOS({
	software: [pi],
	bindings: [
		{
			name: "review",
			description: "Send a file to the reviewer agent and get back a review.",
			bindings: {
				submit: {
					description: "Submit a file path for review by the reviewer agent.",
					inputSchema: z.object({ path: z.string() }),
					execute: async ({ path }: { path: string }) => {
						const client = createClient<typeof registry>({
							endpoint: "http://localhost:6420",
						});
						const content = await client.coder
							.getOrCreate("feature-auth")
							.filesystem.readFile(path);
						const reviewerHandle = client.reviewer.getOrCreate("feature-auth");
						await reviewerHandle.filesystem.writeFile(path, content);
						await reviewerHandle.sessions.open({
							agent: "pi",
							env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
						});
						const result = await reviewerHandle.sessions.prompt({
							content: [
								{ type: "text", text: `Review ${path} for security issues` },
							],
						});
						return {
							review:
								result.message?.content
									.filter((block) => block.type === "text")
									.map((block) => block.text)
									.join("") ?? "",
						};
					},
				},
			},
		},
	],
});

export const registry = setup({ use: { coder, reviewer } });
registry.start();
```

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./agent-to-agent-server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const coderAgent = client.coder.getOrCreate("feature-auth");
await coderAgent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});

// The coder implements the feature, then calls the `review` binding itself so the
// reviewer agent reviews the code. This is true agent-to-agent: the coder drives it.
await coderAgent.sessions.prompt({
	content: [
		{
			type: "text",
			text: "Implement the login feature in /home/agentos/src/auth.ts, then run `agentos-review submit --path /home/agentos/src/auth.ts` to have it reviewed.",
		},
	],
});
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/agent-to-agent)*

### Multiplayer

Connect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime.

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

// Client A: creates the session and sends prompts
const clientA = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agentA = clientA.vm.getOrCreate("shared-agent");
const connA = agentA.connect();
connA.on("sessionEvent", (event) => {
	console.log("[A]", event);
});

await agentA.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agentA.sessions.prompt({
	content: [{ type: "text", text: "Build a REST API" }],
});

// Client B: observes the same session (separate process)
const clientB = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const connB = clientB.vm.getOrCreate("shared-agent").connect();
connB.on("sessionEvent", (event) => {
	console.log("[B]", event);
});
// Client B sees the same events as Client A
```

server.ts:

```ts
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();
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/multiplayer)*

### Workflows & Graphs

Orchestrate multi-step agent tasks with durable workflows that survive crashes and restarts.

examples/crash-course/workflows.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";
import { actor } from "rivetkit";
import { workflow } from "rivetkit/workflow";

const vm = agentOS({ software: [pi] });

// Each created actor is one durable workflow run. The run input lives in state,
// so no application queue is needed; AgentOS serializes prompts per session.
const bugFixer = actor({
	state: {
		repo: "",
		issue: "",
		status: "running" as "running" | "complete",
	},
	onCreate: (c, input: { repo: string; issue: string }) => {
		c.state.repo = input.repo;
		c.state.issue = input.issue;
	},
	run: workflow(async (ctx) => {
		await ctx.step("clone-repo", (step) =>
			step
				.client<typeof registry>()
				.vm.getOrCreate("bug-fixer")
				.process.exec(`git clone ${step.state.repo} /home/agentos/repo`),
		);

		await ctx.step("fix-bug", async (step) => {
			const agent = step
				.client<typeof registry>()
				.vm.getOrCreate("bug-fixer");
			await agent.sessions.open({
				agent: "pi",
				env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
			});
			await agent.sessions.prompt({
				content: [
					{ type: "text", text: `Fix the bug in issue: ${step.state.issue}` },
				],
			});
		});

		await ctx.step("run-tests", (step) =>
			step
				.client<typeof registry>()
				.vm.getOrCreate("bug-fixer")
				.process.exec("cd /home/agentos/repo && npm test"),
		);
		await ctx.step("complete", async (step) => {
			step.state.status = "complete";
		});
	}),
});

export const registry = setup({ use: { vm, bugFixer } });
registry.start();
```

[Documentation](/agentos/docs/workflows)

## Operating System

### Filesystem

Read, write, and manage files inside the VM. The `/home/agentos` directory is persisted automatically across sleep/wake cycles.

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const agent = client.vm.getOrCreate("my-agent");

// Write a file
await agent.filesystem.writeFile("/home/agentos/config.json", JSON.stringify({ key: "value" }));

// Read a file
const content = await agent.filesystem.readFile("/home/agentos/config.json");
console.log(new TextDecoder().decode(content));

// List directory contents recursively
const files = await agent.filesystem.readdirRecursive("/home/agentos");
for (const entry of files) {
  console.log(entry.type, entry.path);
}
```

server.ts:

```ts
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();
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/filesystem)*

### Processes & Shell

Execute commands, spawn long-running processes, and open interactive shells.

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const agent = client.vm.getOrCreate("my-agent");

// One-shot execution
const result = await agent.process.exec("echo hello && ls /home/agentos");
console.log("stdout:", result.stdout);
console.log("exit code:", result.exitCode);

// Spawn a long-running process
const conn = agent.connect();
const { pid } = await agent.process.spawn("node", ["server.js"]);
conn.on("processOutput", (data) => {
	if (data.pid !== pid) return;
  console.log(`[pid ${data.pid}]`, new TextDecoder().decode(data.data));
});

console.log("Process ID:", pid);
```

server.ts:

```ts
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();
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/processes)*

### Networking & Previews

Proxy HTTP requests into VMs with `httpRequest`. Create actor-namespaced preview URLs for port forwarding VM services to shareable public URLs.

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const agent = client.vm.getOrCreate("my-agent");

// Fetch from a service running inside the VM
const response = await agent.network.httpRequest({ port: 3000, path: "/api/health" });
console.log("Status:", response.status);

// Create a preview path (port forwarding through the actor), valid for 1 hour
const preview = await agent.createPreviewUrl(3000, 3600);
console.log("Preview path:", preview.path);
console.log("Expires at:", new Date(preview.expiresAt));
```

server.ts:

```ts
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();
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/networking)*

### Crons & Loops

Schedule recurring commands and agent sessions with cron expressions.

client.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const agent = client.vm.getOrCreate("my-agent");

// Schedule a command every hour
await agent.cron.schedule({
  schedule: "0 * * * *",
  action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache/*"] },
});

// Schedule an agent session daily at 9 AM
await agent.cron.schedule({
  schedule: "0 9 * * *",
  action: {
    type: "session",
    agentType: "pi",
    prompt: "Review the codebase for security issues and write a report to /home/agentos/audit.md",
  },
});
```

server.ts:

```ts
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();
```

*See [Full Example](https://github.com/rivet-dev/agentos/tree/main/examples/crash-course) or [Documentation](/agentos/docs/cron)*

### External Sandboxes

agentOS uses a hybrid model: agents run in a lightweight VM by default and mount a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. Mount the sandbox as a filesystem and expose its process management as bindings.

examples/crash-course/sandbox.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import {
	createSandboxFs,
	createSandboxBindings,
} from "@rivet-dev/agentos-sandbox";
import { SandboxAgent } from "sandbox-agent";
import { docker } from "sandbox-agent/docker";

const sandbox = await SandboxAgent.start({ sandbox: docker() });

const vm = agentOS({
	// Bindings let the agent control the sandbox
	bindings: [createSandboxBindings({ client: sandbox })],
	// Mounts let the agent read the sandbox filesystem (optional)
	mounts: [
		{
			path: "/home/agentos/sandbox",
			plugin: createSandboxFs({ client: sandbox }),
		},
	],
});

export const registry = setup({ use: { vm } });
registry.start();
```

[Documentation](/agentos/docs/sandboxes)
