> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Remote Agents SDK

> Build CLIs, apps, and integrations with the Remote Agents SDK for TypeScript

## Overview

The `@runloop/remote-agents-sdk` SDK is the TypeScript client for interacting with remote agents over Axon. It wraps the raw event stream in protocol-aware connection classes, typed timeline events, and narrowing guards so you do not have to hand-parse `event.payload` strings.

It supports two protocol modules:

* **ACP** for OpenCode, Goose, and other [Agent Client Protocol](https://agentclientprotocol.com) agents
* **Claude** for Claude Code CLI over the `claude_json` broker protocol

<CardGroup cols={2}>
  <Card title="SDK Repository" icon="github" href="https://github.com/runloopai/remote-agents-sdk">
    Source code and examples
  </Card>

  <Card title="Full SDK Documentation" icon="book" href="https://runloopai.github.io/remote-agents-sdk/">
    Full method signatures, types, and options
  </Card>
</CardGroup>

<Tip>
  For apps and UIs, prefer the **timeline event** APIs: `onTimelineEvent()` and
  `receiveTimelineEvents()`. They give you one typed stream for protocol
  messages, turn boundaries, broker events, and custom Axon events.
</Tip>

## Installation

<Steps>
  <Step title="Install the SDK">
    ```bash theme={null}
    npm install @runloop/remote-agents-sdk @runloop/api-client
    ```

    **Requirements:**

    * Node.js 22+ or Bun
    * `RUNLOOP_API_KEY`
    * `ANTHROPIC_API_KEY` for Claude integrations
  </Step>

  <Step title="Install the Claude peer dependency if needed">
    ```bash theme={null}
    npm install @anthropic-ai/claude-agent-sdk
    ```
  </Step>
</Steps>

## Quick Comparison: ACP vs Claude

| Feature                      | ACP                                             | Claude                                            |
| ---------------------------- | ----------------------------------------------- | ------------------------------------------------- |
| **Use case**                 | OpenCode, Goose, custom ACP agents              | Claude Code CLI                                   |
| **Connection class**         | `ACPAxonConnection`                             | `ClaudeAxonConnection`                            |
| **Lifecycle**                | `connect()` -> `initialize()` -> `newSession()` | `connect()` -> `initialize()` -> `send()`         |
| **Basic streaming**          | `onSessionUpdate()`                             | `receiveAgentResponse()` / `receiveAgentEvents()` |
| **Recommended app API**      | `onTimelineEvent()`                             | `onTimelineEvent()`                               |
| **Replay / resume**          | `replay` + `afterSequence`                      | `replay` + `afterSequence`                        |
| **Custom events**            | `publish()`                                     | `publish()`                                       |
| **Permission customization** | `requestPermission` / `createClient()`          | `onControlRequest()`                              |
| **Best fit**                 | Structured ACP session workflows                | Native Claude Code flows                          |

### Pick your consumption pattern

| If you want to...                                      | Use                                             |
| ------------------------------------------------------ | ----------------------------------------------- |
| Handle ACP session updates only                        | `onSessionUpdate()`                             |
| Build a UI over protocol + system + custom events      | `onTimelineEvent()` / `receiveTimelineEvents()` |
| Send one Claude prompt and stop at the end of the turn | `receiveAgentResponse()`                        |
| Consume Claude messages continuously                   | `receiveAgentEvents()`                          |

***

## ACP Module

The ACP module connects to agents that implement the [Agent Client Protocol](https://agentclientprotocol.com), including OpenCode and Goose.

### 1. Create a connection

```typescript theme={null}
import { RunloopSDK } from "@runloop/api-client";
import {
  ACPAxonConnection,
  PROTOCOL_VERSION,
} from "@runloop/remote-agents-sdk/acp";

const sdk = new RunloopSDK({ bearerToken: process.env.RUNLOOP_API_KEY });
const axon = await sdk.axon.create({ name: "acp-transport" });
const devbox = await sdk.devbox.create({
  mounts: [
    {
      type: "broker_mount",
      axon_id: axon.id,
      protocol: "acp",
      agent_binary: "opencode",
      launch_args: ["acp"],
    },
  ],
});

const conn = new ACPAxonConnection(axon, devbox);

await conn.connect();
await conn.initialize({
  protocolVersion: PROTOCOL_VERSION,
  clientInfo: { name: "my-app", version: "1.0.0" },
});
```

### 2. Start a session and send a prompt

```typescript theme={null}
const session = await conn.newSession({ cwd: "/home/user", mcpServers: [] });

await conn.prompt({
  sessionId: session.sessionId,
  prompt: [{ type: "text", text: "Say hello world" }],
});
```

### 3. Handle basic session updates

Use `onSessionUpdate()` if you only care about ACP session payloads.

```typescript theme={null}
import {
  isAgentTextChunk,
  isToolCall,
  isUsageUpdate,
} from "@runloop/remote-agents-sdk/acp";

conn.onSessionUpdate((_sessionId, update) => {
  if (isAgentTextChunk(update)) {
    process.stdout.write(update.content.text);
  } else if (isToolCall(update)) {
    console.log(`\n[tool] ${update.title}`);
  } else if (isUsageUpdate(update)) {
    console.log(`\n[usage] ${update.used}/${update.size}`);
  }
});
```

### 4. Recommended: use timeline events for apps

Timeline events are the better fit for UIs because they combine protocol events, turn boundaries, broker/system events, and custom Axon events in one ordered stream.

```typescript theme={null}
import {
  isAgentTextChunk,
  isSessionUpdateEvent,
  isTurnCompletedEvent,
} from "@runloop/remote-agents-sdk/acp";

conn.onTimelineEvent((event) => {
  if (isSessionUpdateEvent(event) && isAgentTextChunk(event.data.update)) {
    process.stdout.write(event.data.update.content.text);
  }

  if (isTurnCompletedEvent(event)) {
    console.log("\nTurn complete");
  }
});
```

### ACP Key Methods

The most commonly used methods on `ACPAxonConnection`:

| Method                            | Description                                                              |
| --------------------------------- | ------------------------------------------------------------------------ |
| `connect()`                       | Open the Axon SSE stream and wire the ACP transport.                     |
| `initialize(params)`              | Run the ACP handshake.                                                   |
| `newSession(params)`              | Create a session.                                                        |
| `loadSession(params)`             | Re-open an existing session.                                             |
| `listSessions(params)`            | List sessions known to the agent.                                        |
| `prompt(params)`                  | Start an agent turn.                                                     |
| `cancel(params)`                  | Cancel an in-progress turn.                                              |
| `authenticate(params)`            | Respond to an advertised auth method (differs from agent-to-agent auth). |
| `extMethod(method, params)`       | Send a custom extension request.                                         |
| `extNotification(method, params)` | Send a custom extension notification.                                    |
| `publish(params)`                 | Publish a custom Axon event on the same channel.                         |
| `onTimelineEvent(listener)`       | Subscribe to classified timeline events.                                 |
| `disconnect()`                    | Abort the stream and clean up.                                           |

For the full API surface — all methods, options, and type signatures — see the [full SDK documentation](https://runloopai.github.io/remote-agents-sdk/).

### ACP type guards

For `onSessionUpdate()` you can narrow with:

* `isUserMessageChunk()`
* `isAgentMessageChunk()`
* `isAgentTextChunk()`
* `isAgentThoughtChunk()`
* `isThoughtTextChunk()`
* `isToolCall()`
* `isToolCallProgress()`
* `isPlan()`
* `isUsageUpdate()`
* `isAvailableCommandsUpdate()`
* `isCurrentModeUpdate()`
* `isConfigOptionUpdate()`
* `isSessionInfoUpdate()`

For timeline events you can narrow with:

* `isSessionUpdateEvent()`
* `isInitializeEvent()`
* `isPromptEvent()`
* `isNewSessionEvent()`
* `isTurnStartedEvent()`
* `isTurnCompletedEvent()`
* `isBrokerErrorEvent()`
* `isDevboxLifecycleEvent()`
* `isAgentErrorEvent()`
* `isAgentLogEvent()`
* `isUnknownTimelineEvent()`
* `createCustomEventGuard<T>()`

***

## Claude Module

The Claude module connects to Claude Code CLI running in a Devbox via the `claude_json` broker protocol.

### 1. Create a connection

```typescript theme={null}
import { RunloopSDK } from "@runloop/api-client";
import { ClaudeAxonConnection } from "@runloop/remote-agents-sdk/claude";

const sdk = new RunloopSDK({ bearerToken: process.env.RUNLOOP_API_KEY });
const axon = await sdk.axon.create({ name: "claude-transport" });
const devbox = await sdk.devbox.create({
  mounts: [
    {
      type: "broker_mount",
      axon_id: axon.id,
      protocol: "claude_json",
      agent_binary: "claude",
    },
  ],
  environment_variables: {
    ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? "",
  },
});

const conn = new ClaudeAxonConnection(axon, devbox, {
  model: "claude-sonnet-4-5",
});

await conn.connect();
await conn.initialize();
```

### 2. Send one prompt and iterate the response

```typescript theme={null}
await conn.send("Say hello world");

for await (const msg of conn.receiveAgentResponse()) {
  if (msg.type === "assistant") {
    for (const block of msg.message.content) {
      if (block.type === "text") process.stdout.write(block.text);
    }
  }

  if (msg.type === "result") {
    console.log(`\nTurn complete: ${msg.subtype}`);
  }
}
```

### 3. Recommended: use timeline events for apps

```typescript theme={null}
import {
  isClaudeAssistantTextEvent,
  isClaudeResultEvent,
} from "@runloop/remote-agents-sdk/claude";

conn.onTimelineEvent((event) => {
  if (isClaudeAssistantTextEvent(event)) {
    for (const block of event.data.message.content) {
      if (block.type === "text") process.stdout.write(block.text);
    }
  }

  if (isClaudeResultEvent(event)) {
    console.log(`\nResult: ${event.data.subtype}`);
  }
});
```

### 4. Handle Claude control requests

Use `onControlRequest()` when you want to intercept Claude permission prompts or other control flow.

```typescript theme={null}
conn.onControlRequest("can_use_tool", async (message) => ({
  type: "control_response",
  response: {
    subtype: "success",
    request_id: message.request_id,
    response: {
      behavior: "allow",
      updatedInput: message.request.input,
    },
  },
}));
```

### Claude Key Methods

The most commonly used methods on `ClaudeAxonConnection`:

| Method                      | Description                                           |
| --------------------------- | ----------------------------------------------------- |
| `connect()`                 | Open the transport and start the read loop.           |
| `initialize()`              | Run the Claude handshake.                             |
| `send(prompt)`              | Send a string or `SDKUserMessage`.                    |
| `receiveAgentResponse()`    | Async iterator that stops after the `result` message. |
| `interrupt()`               | Interrupt the current conversation turn.              |
| `publish(params)`           | Publish a custom Axon event on the same channel.      |
| `onTimelineEvent(listener)` | Subscribe to classified timeline events.              |
| `disconnect()`              | Close the transport and clean up.                     |

For the full API surface — all methods, options, and type signatures — see the [full SDK documentation](https://runloopai.github.io/remote-agents-sdk/).

### Claude timeline guards

Useful Claude-side guards include:

* `isClaudeProtocolEvent()`
* `isClaudeAssistantEvent()`
* `isClaudeAssistantTextEvent()`
* `isClaudeResultEvent()`
* `isClaudeQueryEvent()`
* `isClaudeSystemInitEvent()`
* `isClaudeControlRequestEvent()`
* `isClaudeControlResponseEvent()`
* plus the shared guards like `isTurnCompletedEvent()`, `isBrokerErrorEvent()`, and `isDevboxLifecycleEvent()`

***

## Sessions and Replays

Axon is an append-only session log. Reconnecting to the same Axon lets the SDK replay the conversation history so your app can recover the session view instead of starting from scratch.

This page uses **sessions and replays** intentionally. Do not call this a "snapshot" workflow in Runloop docs. "Snapshots" already means [Devbox disk snapshots](/docs/devboxes/snapshots).

### What happens by default

When you call `connect()`, the SDK replays the existing session history first, then resumes live delivery. Conceptually, that means:

* re-open the same Axon
* rebuild the current session state
* continue streaming from where the session left off

That is why refresh-and-recover flows work well with the timeline APIs.

### Recovering a session

If your app reconnects to an existing Axon, the SDK can recover the session state and continue from there:

```typescript theme={null}
const conn = new ACPAxonConnection(axon, devbox);
await conn.connect();
await conn.initialize({
  protocolVersion: PROTOCOL_VERSION,
  clientInfo: { name: "my-app", version: "1.0.0" },
});
```

```typescript theme={null}
const conn = new ClaudeAxonConnection(axon, devbox);
await conn.connect();
await conn.initialize();
```

### Resume after interruptions

If a stream drops or a Devbox resumes, the important mental model is the same: reconnect to the same session and let the SDK rehydrate what happened before live events continue.

Timeline listeners are the best fit for this because they naturally rebuild UI state from the replayed session history.

### Suspend / resume with Devboxes

This model pairs well with Devboxes that suspend on idle and resume on Axon activity. The [`combined-app`](https://github.com/runloopai/remote-agents-sdk/tree/main/examples/combined-app) example uses:

* `lifecycle.after_idle.on_idle: "suspend"`
* `resume_triggers.axon_event: true`

That lets a conversation pause with the Devbox and recover cleanly when activity resumes.

***

## Timeline Events

For apps, **prefer the timeline event APIs**. They give you one ordered stream that includes:

* protocol events (`acp_protocol` or `claude_protocol`)
* broker/system events (`system`)
* custom Axon events (`unknown`)

Every timeline event has:

| Field       | Description                                                      |
| ----------- | ---------------------------------------------------------------- |
| `kind`      | One of `acp_protocol`, `claude_protocol`, `system`, or `unknown` |
| `data`      | Typed payload for that event kind                                |
| `axonEvent` | The underlying raw Axon event                                    |

### Custom events

Use `publish()` to emit custom Axon events and `createCustomEventGuard<T>()` or `tryParseTimelinePayload<T>()` to consume them safely.

```typescript theme={null}
import { createCustomEventGuard } from "@runloop/remote-agents-sdk/acp";

const isBuildStatus = createCustomEventGuard<{
  step: string;
  progress: number;
}>("build_status");

await conn.publish({
  event_type: "build_status",
  origin: "EXTERNAL_EVENT",
  source: "ci-pipeline",
  payload: JSON.stringify({ step: "compile", progress: 75 }),
});

conn.onTimelineEvent((event) => {
  if (isBuildStatus(event)) {
    console.log(`${event.data.step}: ${event.data.progress}%`);
  }
});
```

## Known Limitations

* **Call `connect()` before `initialize()`.** Both modules require an explicit connection step.
* **ACP `prompt()` resolves before trailing `session/update` events are fully delivered.** If you need precise turn boundaries, use `onTimelineEvent()`.
* **Auto-reconnect is single retry only.** If the stream drops twice, create a new connection instance.
* **Claude permission requests auto-approve by default.** Register `onControlRequest("can_use_tool", ...)` if you need custom approval logic.
* **ACP permissions auto-approve by default.** Override `requestPermission` or provide `createClient()` if you need custom handling.
* **Node 22+ is required.**
* **`@runloop/api-client` is a peer dependency.**
* **`@anthropic-ai/claude-agent-sdk` is only required for Claude integrations.**

***

## Examples Repository

If you want a real application to copy from, start with the full-stack demo:

<CardGroup cols={2}>
  <Card title="Combined App" icon="browser" href="https://github.com/runloopai/remote-agents-sdk/tree/main/examples/combined-app">
    React + Express demo that streams classified timeline events over WebSocket,
    handles permissions, and demonstrates suspend/resume-aware agent sessions.
  </Card>

  <Card title="ACP Hello World" icon="code" href="https://github.com/runloopai/remote-agents-sdk/tree/main/examples/acp-hello-world">
    Small ACP script showing connect, initialize, newSession, and prompt.
  </Card>

  <Card title="Claude Hello World" icon="code" href="https://github.com/runloopai/remote-agents-sdk/tree/main/examples/claude-hello-world">
    Small Claude script showing connect, initialize, send, and
    receiveAgentResponse.
  </Card>

  <Card title="ACP CLI" icon="terminal" href="https://github.com/runloopai/remote-agents-sdk/tree/main/examples/acp-cli">
    Interactive ACP REPL with cancellation and richer event handling.
  </Card>

  <Card title="Claude CLI" icon="terminal" href="https://github.com/runloopai/remote-agents-sdk/tree/main/examples/claude-cli">
    Interactive Claude REPL with model selection and prompt streaming.
  </Card>
</CardGroup>

### Running the examples

**Full-stack app:**

```bash theme={null}
git clone https://github.com/runloopai/remote-agents-sdk
cd remote-agents-sdk
bun install && bun run build

export RUNLOOP_API_KEY=your_runloop_api_key
export ANTHROPIC_API_KEY=your_anthropic_api_key

cd examples/combined-app
cp .env.example .env

# Terminal 1
bun run dev

# Terminal 2
bun run dev:client
```

**Hello world scripts:**

```bash theme={null}
cd /path/to/remote-agents-sdk
bun install && bun run build

export RUNLOOP_API_KEY=your_runloop_api_key
export ANTHROPIC_API_KEY=your_anthropic_api_key

cd examples/acp-hello-world
bun run acp-hello-world.ts

cd ../claude-hello-world
bun run claude-hello-world.ts
```

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="Axons Overview" icon="book-open" href="/docs/axons/overview">
    Learn the raw Axon event model, event structure, and brokered flows.
  </Card>

  <Card title="ACP Protocol" icon="plug" href="/docs/axons/broker/acp">
    Broker configuration and ACP-specific protocol behavior.
  </Card>

  <Card title="Claude Protocol" icon="message-bot" href="/docs/axons/broker/claude">
    Broker configuration and Claude Code event flow.
  </Card>

  <Card title="Broker Overview" icon="arrows-turn-right" href="/docs/axons/broker">
    Learn how Broker bridges Axons to Devbox-hosted agents.
  </Card>

  <Card title="Axon + ACP Tutorial" icon="graduation-cap" href="/docs/tutorials/axon-acp-broker">
    Step-by-step walkthrough of an ACP integration over Axon.
  </Card>

  <Card title="Base SDK" icon="code-branch" href="/docs/tools/sdks">
    Devbox and Axon management in the core Runloop SDKs.
  </Card>
</CardGroup>
