Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 73 additions & 6 deletions sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

## What this package does

Connects applications to Runloop-hosted remote agents (Claude Code, Codex,
OpenCode, etc.) via the Axon event bus. Three protocol modules plus shared
Connects applications to Runloop-hosted remote agents (Claude Code, Codex, Pi,
OpenCode, etc.) via the Axon event bus. Four protocol modules plus shared
utilities.

## Choose your module
Expand All @@ -15,10 +15,12 @@ utilities.
| **ACP** | `@runloop/remote-agents-sdk/acp` | Any ACP-compatible agent (OpenCode, Claude via ACP) |
| **Claude** | `@runloop/remote-agents-sdk/claude` | Claude Code with native SDK message types |
| **Codex** | `@runloop/remote-agents-sdk/codex` | Codex CLI with native app-server message types |
| **Pi** | `@runloop/remote-agents-sdk/pi` | Pi coding agent with native JSONL RPC message types |
| **Shared** | `@runloop/remote-agents-sdk/shared` | Common types (`BaseConnectionOptions`, `AxonEventView`, `AxonEventListener`) and utilities |

Each module pairs with a devbox `broker_mount` `protocol`: `"acp"` for the ACP
module, `"claude_json"` for the Claude module, `"codex_json"` for the Codex module.
module, `"claude_json"` for the Claude module, `"codex_json"` for the Codex
module, `"pi_json"` for the Pi module.

## Required dependencies

Expand All @@ -29,7 +31,7 @@ npm install @runloop/remote-agents-sdk @runloop/api-client
# Only for the Claude module
npm install @anthropic-ai/claude-agent-sdk

# The Codex module needs no extra dependency (protocol types are vendored)
# The Codex and Pi modules need no extra dependency (protocol types ship with the SDK)
```

## ACP module — quick start
Expand Down Expand Up @@ -249,6 +251,67 @@ await conn.disconnect();
| `abortStream()` | Abort the SSE stream without clearing listeners |
| `disconnect()` | Close transport + run `onDisconnect` callback |

## Pi module — quick start

```typescript
import { PiAxonConnection } from "@runloop/remote-agents-sdk/pi";
import { RunloopSDK } from "@runloop/api-client";

const sdk = new RunloopSDK({ bearerToken: process.env.RUNLOOP_API_KEY });

// 1. Provision infrastructure — the broker spawns `pi --mode rpc`. The broker
// owns `--mode rpc` and `--session-dir`; never pass either yourself, or
// session persistence moves out of the durable root and resume breaks.
const axon = await sdk.axon.create({ name: "pi-transport" });
const devbox = await sdk.devbox.create({
mounts: [{
type: "broker_mount",
axon_id: axon.id,
protocol: "pi_json",
agent_binary: "pi",
}],
});

// 2. Connect — there is no handshake, so no initialize() step
const conn = new PiAxonConnection(axon, devbox);
await conn.connect();

// 3. Send and receive. send() resolves when Pi *accepts* the prompt; the turn
// ends later at `agent_settled`.
await conn.send("What files are in this directory?");
for await (const frame of conn.receiveTurn()) {
console.log(frame.type, frame);
}

// 4. Persist the session path if you want to resume it later
const { sessionFile } = await conn.getState();

// 5. Clean up
await conn.disconnect();
```

### Pi — key methods on `PiAxonConnection`

| Method | Purpose |
|--------|---------|
| `connect()` | Open transport and start the read loop; **no `initialize()` follows** |
| `send(message, options?)` | Start a turn with a `prompt`; resolves on *acceptance*, throws `PiCommandError` if Pi rejects it |
| `steer(message, images?)` | Steer the in-flight turn without reopening a broker turn |
| `followUp(message, images?)` | Queue a follow-up message without reopening a broker turn |
| `interrupt()` | Abort the in-flight turn (`abort`) |
| `receiveTurn()` | Async iterator yielding Pi frames until `agent_settled` (**not** `agent_end`) |
| `receiveAgentEvents()` | Async iterator yielding all agent frames indefinitely |
| `getState()` | Read Pi's session state — the supported way to get `sessionFile` |
| `newSession(parentSession?)` | Start a fresh session, optionally branching from an existing one |
| `switchSession(sessionPath)` | Restore a persisted session from its transcript path |
| `command(frame)` | Escape hatch for any unwrapped Pi command (`set_model`, `compact`, `bash`, …) |
| `sessionId` / `sessionFile` | Captured from `get_state` acks, including replayed ones |
| `onAxonEvent(listener)` | Subscribe to all Axon events (returns unsubscribe fn) |
| `onTimelineEvent(listener)` | Subscribe to classified timeline events (returns unsubscribe fn) |
| `receiveTimelineEvents()` | Async generator yielding classified timeline events |
| `abortStream()` | Abort the SSE stream without clearing listeners |
| `disconnect()` | Close transport + run `onDisconnect` callback |

## Timeline Events

All protocol modules provide a unified timeline event stream that classifies
Expand All @@ -263,6 +326,7 @@ events.
| `acp_protocol` | `SessionUpdate \| unknown` | Known ACP protocol event (agent or client method) |
| `claude_protocol` | `SDKMessage` | Known Claude protocol event |
| `codex_protocol` | Typed app-server frame | Known Codex app-server event (narrow with `event.eventType` or the `isCodex*` guards) |
| `pi_protocol` | Typed Pi event or ack | Known Pi RPC event (narrow with `event.eventType` or the `isPi*` guards) |
| `system` | `SystemEvent` | Broker system event (`turn.started`, `turn.completed`, `turn.failed`, `broker.error`) |
| `unknown` | `null` | Anything else — inspect `axonEvent` for details |

Expand Down Expand Up @@ -386,8 +450,11 @@ create a new instance.
- **ACP permissions default to auto-approve** (`allow_always` > `allow_once` > first option). Pass `requestPermission` to customize.
- **Claude permissions also auto-approve** all tool use. Register a `"can_use_tool"` handler via `onControlRequest()` to customize.
- **Codex approvals also auto-approve** by default. Register handlers via `onApprovalRequest()` to customize, or mount with `launch_args: ["-c", "approval_policy=never"]` for headless full-auto (no approval traffic at all).
- **Explicit `connect()` required:** All connections require `await conn.connect()` first, followed by `initialize()` — ACP, Claude, and Codex alike.
- **Explicit `connect()` required:** All connections require `await conn.connect()` first, followed by `initialize()` — ACP, Claude, and Codex alike. **Pi is the exception:** it has no handshake and no `initialize()`; `connect()` is enough.
- **Pi's `send()` resolves on acceptance, not completion.** Pi's ack means only that the prompt was accepted. Await `receiveTurn()` (which ends at `agent_settled`) or the `turn.completed` system event for the response.
- **Pi's `agent_end` is not the end of a turn.** Pi may follow it with an automatic retry (`willRetry: true`). Only `agent_settled` ends an accepted turn.
- **Pi streams one `message_update` per token**, each carrying a full `partial` assistant message. The push surfaces (`onTimelineEvent` / `onAxonEvent`) see every frame. The pull surfaces (`receiveAgentEvents()` / `receiveTurn()`) buffer at most `maxQueuedFrames` (default 1000) and then discard the oldest frame, warning once — so a listener-only application keeps bounded memory, and a pull consumer must keep up.
- **Node >= 22** required.
- **`@runloop/api-client`** is a peer dep — you must install it yourself.
- **`@anthropic-ai/claude-agent-sdk`** is an optional peer dep — only needed for the Claude module. The Codex module has no extra dependency.
- **`@anthropic-ai/claude-agent-sdk`** is an optional peer dep — only needed for the Claude module. The Codex and Pi modules have no extra dependency.
- **`prompt()` resolves before all session updates arrive.** The broker sends the prompt response and `turn.completed` system event *before* flushing thought/message chunks as `session/update` notifications. Use `onAxonEvent` to watch for `turn.started` / `turn.completed` system events to accurately bracket turn content. See the SDK README for details.
Loading
Loading