# Custom Agents

A custom agent is a program that runs **inside the VM** to drive a coding agent. AgentOS spawns it when you call `openSession({ sessionId, agent })` and talks to it over the Agent Client Protocol. You ship it as a software package, exactly like the built-in agents.

## Agent Client Protocol (ACP)

agentOS speaks the [Agent Client Protocol (ACP)](https://agentclientprotocol.com) to every agent: JSON-RPC over stdio. The agent reads protocol messages on **stdin** and writes them on **stdout**, so stdout is reserved for ACP and **stderr is used for logs**. Your program only needs to speak ACP; how it runs the underlying model is up to you. See the [ACP documentation](https://agentclientprotocol.com) for the full protocol.

## Two ways to build an agent

There are two shapes, depending on whether the agent runs in the ACP process or in its own.

### Single process (embedded)

The ACP adapter **embeds the agent SDK** and runs it in the same process. One process inside the VM, lower memory footprint.

<svg viewBox="0 0 340 246" role="img" aria-label="Single embedded process: one ACP adapter that embeds the agent, running inside the VM" style="max-width:360px;width:100%;height:auto;display:block;margin:1.25rem auto;font-family:ui-sans-serif,system-ui,sans-serif;">
  <defs>
    <marker id="ca-arrow-1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="#1b1916" />
    </marker>
  </defs>
  <rect x="130" y="18" width="80" height="28" rx="6" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
  <text x="170" y="36" text-anchor="middle" font-size="11" fill="#1b1916">Host</text>
  <line x1="170" y1="46" x2="170" y2="86" stroke="#1b1916" stroke-width="1.5" marker-end="url(#ca-arrow-1)" />
  <text x="184" y="70" font-size="10" fill="#56524a">ACP</text>
  <rect x="50" y="90" width="240" height="140" rx="10" fill="#faf8f3" stroke="#1b1916" stroke-width="1.5" stroke-dasharray="4 3" />
  <text x="64" y="110" font-size="11" fill="#56524a">VM</text>
  <rect x="80" y="135" width="180" height="64" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
  <text x="170" y="163" text-anchor="middle" font-size="11" fill="#1b1916">ACP adapter +</text>
  <text x="170" y="180" text-anchor="middle" font-size="11" fill="#1b1916">agent (embedded)</text>
</svg>

For example, an adapter to run **OpenCode**, which speaks ACP natively. One package is both the ACP process and the agent, so there's no separate adapter and nothing else is spawned.

examples/custom/opencode.ts:

```ts
import opencode from "@agentos-software/opencode";

// OpenCode *is* the ACP process: it speaks ACP on stdio itself, so there is no
// separate adapter to spawn the agent. The published @agentos-software/opencode
// descriptor already encodes the right entrypoint and env, so use it as-is.
export default opencode;
```

### ACP adapter (separate agent)

The ACP adapter is a thin **bridge** that spawns the real agent as its **own process** (a CLI or SDK) and translates between it and ACP. Full agent feature set, higher memory.

<svg viewBox="0 0 340 276" role="img" aria-label="ACP adapter: a thin adapter inside the VM that spawns the agent as a separate process" style="max-width:360px;width:100%;height:auto;display:block;margin:1.25rem auto;font-family:ui-sans-serif,system-ui,sans-serif;">
  <defs>
    <marker id="ca-arrow-2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="#1b1916" />
    </marker>
  </defs>
  <rect x="130" y="18" width="80" height="28" rx="6" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
  <text x="170" y="36" text-anchor="middle" font-size="11" fill="#1b1916">Host</text>
  <line x1="170" y1="46" x2="170" y2="86" stroke="#1b1916" stroke-width="1.5" marker-end="url(#ca-arrow-2)" />
  <text x="184" y="70" font-size="10" fill="#56524a">ACP</text>
  <rect x="50" y="90" width="240" height="170" rx="10" fill="#faf8f3" stroke="#1b1916" stroke-width="1.5" stroke-dasharray="4 3" />
  <text x="64" y="110" font-size="11" fill="#56524a">VM</text>
  <rect x="90" y="118" width="160" height="42" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
  <text x="170" y="144" text-anchor="middle" font-size="11" fill="#1b1916">ACP adapter</text>
  <line x1="170" y1="160" x2="170" y2="180" stroke="#1b1916" stroke-width="1.5" marker-end="url(#ca-arrow-2)" />
  <text x="184" y="174" font-size="10" fill="#56524a">spawns</text>
  <rect x="90" y="182" width="160" height="46" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
  <text x="170" y="202" text-anchor="middle" font-size="11" fill="#1b1916">Agent process</text>
  <text x="170" y="217" text-anchor="middle" font-size="10" fill="#56524a">(CLI / SDK)</text>
</svg>

For example, an adapter to run **Pi**: the `pi` CLI doesn't speak ACP, so `pi-acp` speaks ACP and spawns the CLI as a separate process. The packaged agent bundles both — its `agentos-package.json` names `pi-acp` as the `acpEntrypoint` and points it at the `pi` CLI via `agent.env`.

examples/custom/pi-cli.ts:

```ts
import { defineSoftware } from "@rivet-dev/agentos";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const packagePath = resolve(dirname(fileURLToPath(import.meta.url)), "..");

// Example: a pi-acp adapter that runs the Pi CLI.
// The agent block lives in the package's agentos-package.json, generated by `agentos-toolchain pack --agent`.
export default defineSoftware({ packagePath });
```

## Use your agent

Register the package on the server with `software`. Sessions are then created from the client by `id`, exactly like any built-in agent.

```ts title="server.ts"
import { agentOS, setup, defineSoftware } from "@rivet-dev/agentos";

const myAgent = defineSoftware({
  packagePath,  // the packed agent .aospkg; its embedded manifest carries the agent block
});

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

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

See [Sessions](/agentos/docs/sessions) for creating and driving sessions. Package your adapter with `agentos-toolchain pack --agent my-agent-acp` so its dependencies are bundled into the self-contained package directory and the `agent` block (naming the `bin/` ACP entrypoint) is written into the package's `agentos-package.json`, rather than shipping it as a loose file.

All built-in agents are defined exactly this way. Browse them for reference on [GitHub](https://github.com/rivet-dev/agentos/tree/main/software).

## Read more

- [Defining software packages](/agentos/docs/custom-software/definition): the full descriptor reference, including the `agentos-package.json` schema and every `agent` field (`acpEntrypoint`, `env`, `launchArgs`, `snapshot`).
- [Building binaries](/agentos/docs/custom-software/building-wasm): compile WASM command binaries and use the registry.

## Debugging

When a custom agent exits mid-turn or a tool call fails, capture the agent's stderr with the `onAgentStderr` hook on `AgentOs.create()`. The agent uses stdout for ACP, so stderr carries its logs and crash output. See [Debugging](/agentos/docs/debugging) for that hook and the runtime (sidecar) logs.
