Skip to main content
More

Core Package

Use @rivet-dev/agentos-core standalone for direct VM control without the Rivet Actor runtime.

agentOS vs agentOS Core

The agentOS() actor (from @rivet-dev/agentos) wraps the core package and adds:

Core (@rivet-dev/agentos-core)Actor (@rivet-dev/agentos)
PersistenceIn-memory by default (pluggable via mounts)Persistent filesystem and sessions
Distributed stateManage yourselfBuilt-in distributed statefulness
Stateful VMsComplex to run yourselfBuilt into Rivet
Sleep/wakeManual dispose() / create()Automatic
EventsDirect callbacksBroadcasted to all connected clients
Preview URLsNoneBuilt-in signed URL server
MultiplayerN/AMultiple clients on same actor
OrchestrationN/AWorkflows, queues, cron
Agent-to-agent communicationCustomBuilt into Rivet Actors
AuthenticationSet up yourselfDocumentation

We recommend using Rivet Actors because they provide a portable way to run agentOS() on any infrastructure with built-in persistence, networking, and orchestration. Use the core package if you need the most bare-bones implementation possible.

Install

npm install @rivet-dev/agentos-core

Boot a VM

Create a VM and drive it directly — no actor runtime, no client/server split. AgentOs.create() boots the VM in-process and returns a handle you call directly:

import { AgentOs } from "@rivet-dev/agentos-core";
import pi from "@agentos-software/pi";

// Create a VM directly with the core package — no actor runtime, no
// client/server split. `AgentOs.create()` boots the VM in-process.
const vm = await AgentOs.create({ software: [pi] });

const result = await vm.exec("echo hello");
console.log(result.stdout); // "hello\n"

Sidecar process

Every VM runs inside a shared sidecar process rather than a process of its own. By default all VMs are tenants of a single, process-global sidecar (the default pool), so each additional VM only adds its marginal cost — a V8 isolate plus its kernel state — instead of a whole OS process. This is what keeps per-VM memory in the tens of MB and warm VM creation in the single-digit milliseconds (see Benchmarks).

This is automatic — agentOS() and AgentOs.create() use the shared default sidecar with no configuration, and the same applies to Rivet Actors (each actor’s VM is a tenant of the shared process). Disposing a VM tears down only that VM; the shared sidecar process is reused across VMs and stays alive for the lifetime of the host process.

For advanced cases the core package exposes explicit sidecar handles so you can isolate a group of VMs in their own process:

import { AgentOs } from "@rivet-dev/agentos-core";

// 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.writeFile("/home/agentos/hello.txt", "Hello, world!");
const content = await vm.readFile("/home/agentos/hello.txt");
console.log(new TextDecoder().decode(content));

await vm.mkdir("/home/agentos/src");
await vm.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.readdirRecursive("/home/agentos");
for (const entry of entries) {
  console.log(entry.type, entry.path);
}

Processes

Long-running process output is delivered through the onStdout/onStderr callbacks you pass to spawn, and exit through onProcessExit(pid, …):

// One-shot execution
const result = await vm.exec("ls -la /home/agentos");
console.log(result.stdout);

// Long-running process with streaming output. spawn() returns synchronously;
// stdout/stderr are delivered through the callbacks you pass in.
await vm.writeFile(
  "/tmp/server.mjs",
  'import http from "http"; http.createServer((req, res) => res.end("ok")).listen(3000); console.log("listening");',
);
const { pid } = vm.spawn("node", ["/tmp/server.mjs"], {
  onStdout: (data) => console.log("stdout:", new TextDecoder().decode(data)),
});

vm.onProcessExit(pid, (exitCode) => console.log("exited:", exitCode));

// Write to stdin
await vm.writeProcessStdin(pid, "some input\n");

// Stop or kill
vm.stopProcess(pid);

Agent sessions

createSession resolves to a session record; all session operations take its sessionId. Session events and permission requests are delivered through per-session callbacks (onSessionEvent / onPermissionRequest):

// createSession() resolves to a session record. All session operations take
// its `sessionId`.
const { sessionId } = await vm.createSession("pi", {
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});

// Stream session events (each event is a JSON-RPC notification).
vm.onSessionEvent(sessionId, (event) => {
  console.log(event.method, event.params);
});

// Observe permission requests from the agent.
vm.onPermissionRequest(sessionId, (request) => {
  console.log("Permission:", request.description ?? request.permissionId);
});

// Send a prompt. prompt() resolves to { response, text }, where `text` is the
// accumulated agent message text and `response` is the raw JSON-RPC response.
const { text } = await vm.prompt(sessionId, "Write a hello world script");
console.log(text);

vm.closeSession(sessionId);

Register onSessionEvent right after createSession so you do not miss the live stream — core session events are live-only and are not replayed.

Networking

fetch(port, request) reaches a server running inside the VM:

// Start a server inside the VM
await vm.writeFile(
  "/tmp/app.mjs",
  'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);',
);
vm.spawn("node", ["/tmp/app.mjs"]);

// Fetch from it — fetch(port, Request) reaches services running in the VM.
const response = await vm.fetch(3000, new Request("http://vm/"));
console.log(await response.text());

Cron jobs

Cron jobs run an "exec" command or a "session" prompt on a schedule. Fired jobs are surfaced through the onCronEvent callback:

const job = vm.scheduleCron({
  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.scheduleCron({
  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.listCronJobs());

Mounts

Configure filesystem backends at boot time.

Native mount plugins (host directories, S3, etc.) are passed via plugin, each identified by an id and a config object.

import { AgentOs } from "@rivet-dev/agentos-core";

// 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 configuration is passed to AgentOs.create() as a single flat object. This is the consolidated config block to copy and adapt. The agentOS() actor accepts the same options and layers persistence, sleep/wake, and preview URLs on top:

import { AgentOs, nodeModulesMount } from "@rivet-dev/agentos-core";
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],
  // Extra instructions appended to agent system prompts
  additionalInstructions: "Always write tests first.",
  // Sidecar placement — defaults to the shared `default` pool
  sidecar: { kind: "shared" },
});

The top-level fields are documented inline above. See Mounts and Software.

Session events

With the core package, session events and permission requests are observed per-session on the AgentOs instance. onSessionEvent(sessionId, event) fires once for every session event; onPermissionRequest(sessionId, request) fires when an agent requests permission. Both are live-only callbacks — register them right after createSession:

import { AgentOs } from "@rivet-dev/agentos-core";
import pi from "@agentos-software/pi";

// With the core package, session events and permission requests are observed
// per-session on the AgentOs instance (there is no actor-factory hook).
const vm = await AgentOs.create({ software: [pi] });
const { sessionId } = await vm.createSession("pi");

// Runs for every event on this session.
vm.onSessionEvent(sessionId, (event) => {
  console.log("Session event:", sessionId, event.method);
});

// Fires when the agent requests permission.
vm.onPermissionRequest(sessionId, (request) => {
  console.log("Permission request:", sessionId, request.permissionId);
});

Timeouts and sleep

Action timeouts and automatic sleep/wake are features of the agentOS() actor, not the core package. A core VM stays alive until you call dispose(). See Persistence & Sleep for the actor’s sleep lifecycle.