# Crash Course

> Source: `src/content/docs/agent-os/crash-course.mdx`
> Canonical URL: https://rivet.dev/docs/agent-os/crash-course
> Description: Run coding agents inside isolated VMs with full filesystem, process, and network control.

---
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).

## Features

- **Isolated VMs**: Each agent gets its own filesystem, processes, and networking. No shared state, no cross-contamination.
- **Multi-Agent Support**: Run Amp, Claude Code, Codex, OpenCode, and PI with a unified API. Swap agents without changing your code.
- **Host Tools**: Expose your JavaScript functions to agents as CLI commands. Direct binding with near-zero latency and automatic code mode for up to 80% token reduction.
- **Persistent State**: Filesystem and transcripts survive sleep/wake cycles automatically. No external database needed.
- **Orchestration**: Workflows, queues, cron jobs, and multi-agent coordination built on Rivet Actors.
- **Hybrid Sandboxes**: Run agents in the lightweight VM by default. Spin up a full sandbox on demand for browsers, compilation, and desktop automation.

## 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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

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

// Create a session and send a prompt
const session = await agent.createSession("pi", {
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
const response = await agent.sendPrompt(
  session.sessionId,
  "Write a hello world script to /home/user/hello.js",
);
console.log(response);

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

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

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

## Quick Reference

### Sessions & Transcripts

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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

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

// Create a session with MCP servers
const session = await agent.createSession("pi", {
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
  mcpServers: [
    {
      type: "local",
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"],
      env: {},
    },
  ],
});

// Send a prompt and wait for the response
const response = await agent.sendPrompt(
  session.sessionId,
  "List all files in the home directory",
);
console.log(response);
```

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/sessions)

### Permissions

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

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

// Auto-approve all permissions server-side
const vm = agentOs({
  onPermissionRequest: async (c, sessionId, request) => {
    await c.respondPermission(sessionId, request.permissionId, "always");
  },
  options: { software: [common, pi] },
});

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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

// Or handle permissions client-side for human-in-the-loop
agent.on("permissionRequest", async (data) => {
  console.log("Permission requested:", data.request);
  // "once" | "always" | "reject"
  await agent.respondPermission(data.sessionId, data.request.permissionId, "once");
});
```

[Documentation](/docs/agent-os/permissions)

### Tools

Expose your JavaScript functions to agents as CLI commands inside the VM. Agents call them as shell commands with auto-generated flags from Zod schemas.

```ts @nocheck
import { toolKit, hostTool } from "@rivet-dev/agent-os-core";
import { z } from "zod";

const myTools = toolKit({
  name: "myapp",
  description: "Application tools",
  tools: {
    createTicket: hostTool({
      description: "Create a ticket in the issue tracker",
      inputSchema: z.object({
        title: z.string().describe("Ticket title"),
        priority: z.enum(["low", "medium", "high"]).describe("Priority level"),
      }),
      execute: async (input) => {
        const ticket = await db.tickets.create(input);
        return { id: ticket.id, url: ticket.url };
      },
    }),
  },
});

// Agent calls: agentos-myapp createTicket --title "Fix login" --priority high
```

[Documentation](/docs/agent-os/tools)

### Filesystem

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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

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

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

// List directory contents recursively
const files = await agent.readdirRecursive("/home/user", { maxDepth: 2 });
console.log(files);
```

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/filesystem)

### Processes & Shell

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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

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

// Spawn a long-running process
agent.on("processOutput", (data) => {
  console.log(`[${data.processId}]`, data.output);
});

const proc = await agent.spawn("node", ["server.js"]);
console.log("Process ID:", proc.processId);
```

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/processes)

### Networking & Previews

Proxy HTTP requests into VMs with `vmFetch`. Create preview URLs for port forwarding VM services to shareable public URLs.

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

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

// Create a preview URL (port forwarding to a public URL)
const preview = await agent.createSignedPreviewUrl(3000);
console.log("Public URL:", preview.path);
console.log("Expires at:", new Date(preview.expiresAt));
```

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/networking)

### Cron Jobs

Schedule recurring commands and agent sessions with cron expressions.

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

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

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

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/cron)

### Sandbox Mounting

agentOS uses a hybrid model: agents run in a lightweight VM by default and spin up a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation.

```ts @nocheck
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi],
    sandbox: {
      enabled: true,
    },
  },
});

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

[Documentation](/docs/agent-os/sandbox)

### Multiplayer & Realtime

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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

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

const session = await agentA.createSession("pi", {
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agentA.sendPrompt(session.sessionId, "Build a REST API");

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

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/multiplayer)

### Agent-to-Agent

Compose specialized agents into pipelines. Each agent gets its own isolated VM and filesystem.

```ts @nocheck server.ts
import { agentOs } from "rivetkit/agent-os";
import { setup } from "rivetkit";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";

const coder = agentOs({
  options: { software: [common, pi] },
});
const reviewer = agentOs({
  options: { software: [common, pi] },
});

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

```ts @nocheck client.ts
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

const client = createClient<typeof registry>("http://localhost:6420");

// Coder writes the feature
const coderAgent = client.coder.getOrCreate(["feature-auth"]);
const coderSession = await coderAgent.createSession("pi", {
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await coderAgent.sendPrompt(coderSession.sessionId, "Implement the login feature");

// Pass files to the reviewer
const src = await coderAgent.readFile("/home/user/src/auth.ts");
const reviewerAgent = client.reviewer.getOrCreate(["feature-auth"]);
await reviewerAgent.writeFile("/home/user/src/auth.ts", src);

// Reviewer checks the code
const reviewSession = await reviewerAgent.createSession("pi", {
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await reviewerAgent.sendPrompt(
  reviewSession.sessionId,
  "Review auth.ts for security issues",
);
```

[Documentation](/docs/agent-os/agent-to-agent)

### Workflows

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

```ts @nocheck
import { agentOs } from "rivetkit/agent-os";
import common from "@rivet-dev/agent-os-common";
import pi from "@rivet-dev/agent-os-pi";
import { actor, setup, workflow } from "rivetkit";

const automator = actor({
  workflows: {
    fixBug: workflow<{ repo: string; issue: string }>(),
  },
  run: async (c) => {
    for await (const message of c.workflow.iter("fixBug")) {
      const { repo, issue } = message.body;
      const agentHandle = c.actors.vm.getOrCreate([`fix-${issue}`]);

      await c.step("clone-repo", async () => {
        return agentHandle.exec(`git clone ${repo} /home/user/repo`);
      });

      await c.step("fix-bug", async () => {
        const session = await agentHandle.createSession("pi", {
          env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
        });
        const response = await agentHandle.sendPrompt(
          session.sessionId,
          `Fix the bug described in issue: ${issue}`,
        );
        await agentHandle.closeSession(session.sessionId);
        return response;
      });

      await c.step("run-tests", async () => {
        return agentHandle.exec("cd /home/user/repo && npm test");
      });

      await message.complete();
    }
  },
});

const vm = agentOs({
  options: { software: [common, pi] },
});

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

[Documentation](/docs/agent-os/workflows)

### SQLite

Use actor-local SQLite as structured long-term memory that persists across sessions and sleep/wake cycles.

```ts @nocheck
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";

const memoryAgent = actor({
  db: db({
    onMigrate: async (db) => {
      await db.execute(`
        CREATE TABLE IF NOT EXISTS memories (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          session_id TEXT NOT NULL,
          category TEXT NOT NULL,
          content TEXT NOT NULL,
          created_at INTEGER NOT NULL
        );
      `);
    },
  }),
  actions: {
    store: async (c, sessionId: string, category: string, content: string) => {
      await c.db.execute(
        "INSERT INTO memories (session_id, category, content, created_at) VALUES (?, ?, ?, ?)",
        sessionId, category, content, Date.now(),
      );
    },
    search: async (c, query: string) => {
      return c.db.execute(
        "SELECT category, content FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT 20",
        `%${query}%`,
      );
    },
  },
});
```

[Documentation](/docs/agent-os/sqlite)

_Source doc path: /docs/agent-os/crash-course_
