Skip to main content

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.

Overview

The @runloop/agent-axon-client SDK is the TypeScript client for interacting with 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 agents
  • Claude for Claude Code CLI over the claude_json broker protocol

SDK Repository

Source code and examples

Full SDK Documentation

Full method signatures, types, and options
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.

Installation

1

Install the SDK

npm install @runloop/agent-axon-client @runloop/api-client
Requirements:
  • Node.js 22+ or Bun
  • RUNLOOP_API_KEY
  • ANTHROPIC_API_KEY for Claude integrations
2

Install the Claude peer dependency if needed

npm install @anthropic-ai/claude-agent-sdk

Quick Comparison: ACP vs Claude

FeatureACPClaude
Use caseOpenCode, Goose, custom ACP agentsClaude Code CLI
Connection classACPAxonConnectionClaudeAxonConnection
Lifecycleconnect() -> initialize() -> newSession()connect() -> initialize() -> send()
Basic streamingonSessionUpdate()receiveAgentResponse() / receiveAgentEvents()
Recommended app APIonTimelineEvent()onTimelineEvent()
Replay / resumereplay + afterSequencereplay + afterSequence
Custom eventspublish()publish()
Permission customizationrequestPermission / createClient()onControlRequest()
Best fitStructured ACP session workflowsNative Claude Code flows

Pick your consumption pattern

If you want to…Use
Handle ACP session updates onlyonSessionUpdate()
Build a UI over protocol + system + custom eventsonTimelineEvent() / receiveTimelineEvents()
Send one Claude prompt and stop at the end of the turnreceiveAgentResponse()
Consume Claude messages continuouslyreceiveAgentEvents()

ACP Module

The ACP module connects to agents that implement the Agent Client Protocol, including OpenCode and Goose.

1. Create a connection

import { RunloopSDK } from "@runloop/api-client";
import {
  ACPAxonConnection,
  PROTOCOL_VERSION,
} from "@runloop/agent-axon-client/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

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.
import {
  isAgentTextChunk,
  isToolCall,
  isUsageUpdate,
} from "@runloop/agent-axon-client/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}`);
  }
});
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.
import {
  isAgentTextChunk,
  isSessionUpdateEvent,
  isTurnCompletedEvent,
} from "@runloop/agent-axon-client/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:
MethodDescription
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.

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

import { RunloopSDK } from "@runloop/api-client";
import { ClaudeAxonConnection } from "@runloop/agent-axon-client/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

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}`);
  }
}
import {
  isClaudeAssistantTextEvent,
  isClaudeResultEvent,
} from "@runloop/agent-axon-client/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.
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:
MethodDescription
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.

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.

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:
const conn = new ACPAxonConnection(axon, devbox);
await conn.connect();
await conn.initialize({
  protocolVersion: PROTOCOL_VERSION,
  clientInfo: { name: "my-app", version: "1.0.0" },
});
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 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:
FieldDescription
kindOne of acp_protocol, claude_protocol, system, or unknown
dataTyped payload for that event kind
axonEventThe underlying raw Axon event

Custom events

Use publish() to emit custom Axon events and createCustomEventGuard<T>() or tryParseTimelinePayload<T>() to consume them safely.
import { createCustomEventGuard } from "@runloop/agent-axon-client/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:

Combined App

React + Express demo that streams classified timeline events over WebSocket, handles permissions, and demonstrates suspend/resume-aware agent sessions.

ACP Hello World

Small ACP script showing connect, initialize, newSession, and prompt.

Claude Hello World

Small Claude script showing connect, initialize, send, and receiveAgentResponse.

ACP CLI

Interactive ACP REPL with cancellation and richer event handling.

Claude CLI

Interactive Claude REPL with model selection and prompt streaming.

Running the examples

Full-stack app:
git clone https://github.com/runloopai/agent-axon-client-ts
cd agent-axon-client-ts
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:
cd /path/to/agent-axon-client-ts
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

Axons Overview

Learn the raw Axon event model, event structure, and brokered flows.

ACP Protocol

Broker configuration and ACP-specific protocol behavior.

Claude Protocol

Broker configuration and Claude Code event flow.

Broker Overview

Learn how Broker bridges Axons to Devbox-hosted agents.

Axon + ACP Tutorial

Step-by-step walkthrough of an ACP integration over Axon.

Base SDK

Devbox and Axon management in the core Runloop SDKs.