Crash Course
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 or join our 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
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.openSession({
agent: "pi",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
const response = await agent.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.readFile("/workspace/hello.js");
console.log(new TextDecoder().decode(content));
After the quickstart, customize your agent with the Registry.
Agents
Sessions & Transcripts
Create agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles.
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.openSession({
agent: "pi",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
// Send a prompt and wait for the response
const response = await agent.prompt({
content: [{ type: "text", text: "List all files in the home directory" }],
});
console.log(response.message?.content ?? []);
See Full Example or Documentation
Approvals
Approve or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads.
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();
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
.respondPermission({
sessionId: event.sessionId,
requestId: event.requestId,
optionId: option.optionId,
})
.catch((error) => console.error("Permission response failed:", error));
});
await agent.openSession({
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.prompt({
content: [{ type: "text", text: "Create /workspace/output.txt" }],
});
See Full Example or Documentation
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.
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();
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.openSession({
agent: "claude",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agent.prompt({
content: [{ type: "text", text: "What's the weather in Paris?" }],
});
See Full Example or Documentation
Agent-to-Agent
Let one agent call another through a binding. The coder gets a review binding it invokes itself, which bridges into the reviewer’s isolated VM.
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")
.readFile(path);
const reviewerHandle = client.reviewer.getOrCreate("feature-auth");
await reviewerHandle.writeFile(path, content);
await reviewerHandle.openSession({
agent: "pi",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
const result = await reviewerHandle.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();
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.openSession({
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.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 or Documentation
Multiplayer & Realtime
Connect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime.
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.openSession({
agent: "pi",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agentA.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
See Full Example or Documentation
Workflows
Orchestrate multi-step agent tasks with durable workflows that survive crashes and restarts.
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")
.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.openSession({
agent: "pi",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agent.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")
.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();
Operating System
Filesystem
Read, write, and manage files inside the VM. The /home/agentos directory is persisted automatically across sleep/wake cycles.
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.writeFile("/home/agentos/config.json", JSON.stringify({ key: "value" }));
// Read a file
const content = await agent.readFile("/home/agentos/config.json");
console.log(new TextDecoder().decode(content));
// List directory contents recursively
const files = await agent.readdirRecursive("/home/agentos");
for (const entry of files) {
console.log(entry.type, entry.path);
}
See Full Example or Documentation
Processes & Shell
Execute commands, spawn long-running processes, and open interactive shells.
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.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.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);
See Full Example or Documentation
Networking & Previews
Proxy HTTP requests into VMs with httpRequest. Create actor-namespaced preview URLs for port forwarding VM services to shareable public URLs.
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.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));
See Full Example or Documentation
Cron Jobs
Schedule recurring commands and agent sessions with cron expressions.
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.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",
agentType: "pi",
prompt: "Review the codebase for security issues and write a report to /home/agentos/audit.md",
},
});
See Full Example or Documentation
Sandbox Mounting
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, so you can swap providers without changing agent code. Mount the sandbox as a filesystem and expose its process management as bindings.
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();