npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@runloop/agent-axon-client

v0.1.2

Published

Axon-backed ACP and Claude agent clients for Runloop broker agents

Readme

@runloop/agent-axon-client

Alpha — subject to change. This SDK is in early development. APIs, interfaces, and behavior may change without notice between versions.

TypeScript client for connecting to coding agents running inside Runloop devboxes via the Axon event bus.

This package provides two independent modules:

| Module | Import path | Protocol | Use case | |--------|-------------|----------|----------| | ACP | @runloop/agent-axon-client/acp | Agent Client Protocol (JSON-RPC 2.0) | Any ACP-compatible agent (OpenCode, Claude via ACP, etc.) | | Claude | @runloop/agent-axon-client/claude | Claude Code SDK wire format | Claude Code with native SDK message types |

Both modules communicate over Runloop Axon channels. Pick the one that matches your agent's protocol.

Installation

npm install @runloop/agent-axon-client @runloop/api-client

@runloop/api-client is a peer dependency — you provide the Runloop SDK instance.

If using the Claude module, you also need:

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

Imports

// Subpath imports (recommended — tree-shakeable)
import { ACPAxonConnection, PROTOCOL_VERSION } from "@runloop/agent-axon-client/acp";
import { ClaudeAxonConnection } from "@runloop/agent-axon-client/claude";

// Namespaced root import (both modules at once)
import { acp, claude } from "@runloop/agent-axon-client";

Getting Started

ACP Agent

import { ACPAxonConnection, PROTOCOL_VERSION } from "@runloop/agent-axon-client/acp";
import { RunloopSDK } from "@runloop/api-client";

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 agent = new ACPAxonConnection({
  axon,
  devboxId: devbox.id,
  shutdown: async () => {
    await devbox.shutdown();
  },
});

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

agent.onSessionUpdate((sessionId, update) => console.log(sessionId, update));

const session = await agent.newSession({ cwd: "/home/user", mcpServers: [] });
await agent.prompt({
  sessionId: session.sessionId,
  prompt: [{ type: "text", text: "Hello!" }],
});

await agent.shutdown();

Claude Code Agent

import { ClaudeAxonConnection } from "@runloop/agent-axon-client/claude";
import { RunloopSDK } from "@runloop/api-client";

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",
  }],
});

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

await conn.send("What files are in this directory?");

for await (const msg of conn.receiveResponse()) {
  console.log(msg.type, msg);
}

await conn.disconnect();

ACP Module

ACPAxonConnection

Higher-level wrapper that manages an axonStream, an AbortController, and the ACP ClientSideConnection.

Constructor (ACPAxonConnectionOptions):

| Field | Type | Required | Description | |-------|------|----------|-------------| | axon | Axon | Yes | Axon channel from @runloop/api-client | | devboxId | string | No | Devbox ID for logging / observability | | shutdown | () => Promise<void> | No | Called from shutdown() (e.g. devbox teardown) | | requestPermission | (params) => Promise<Response> | No | Custom permission handler (defaults to auto-approve) | | onError | (error: unknown) => void | No | Error callback | | onDisconnect | () => void | No | Called when the SSE stream disconnects |

ACP Methods (proxied from ClientSideConnection):

| Method | Description | |--------|-------------| | initialize(params) | Establishes the connection and negotiates capabilities | | newSession(params) | Creates a new conversation session | | loadSession(params) | Loads an existing session | | listSessions(params) | Lists existing sessions | | prompt(params) | Sends a prompt and processes the agent's turn | | cancel(params) | Cancels an ongoing prompt turn | | authenticate(params) | Authenticates using an advertised method | | setSessionMode(params) | Sets session mode (e.g. "ask", "code") | | setSessionConfigOption(params) | Sets a session config option | | extMethod(method, params) | Extension request | | extNotification(method, params) | Extension notification |

Listeners & Lifecycle:

| Property / Method | Description | |---|---| | protocol: ClientSideConnection | Escape hatch for experimental/unstable ACP methods | | axonId: string | The Axon channel ID | | devboxId: string \| undefined | The Runloop devbox ID (if passed in options) | | signal: AbortSignal | Fires when the connection closes | | closed: Promise<void> | Resolves when the connection closes | | onSessionUpdate(listener) | Register a session update listener. Returns unsubscribe function. | | onRawEvent(listener) | Register a raw Axon event listener. Returns unsubscribe function. | | disconnect() | Abort the stream and clear all listeners | | shutdown() | Disconnect and run the teardown callback (e.g. devbox shutdown) |

Provisioning Axon + devbox

Create an Axon channel, attach a devbox broker_mount with protocol: "acp", then pass the Axon (and optional devboxId / shutdown) into ACPAxonConnection:

import { ACPAxonConnection, PROTOCOL_VERSION } from "@runloop/agent-axon-client/acp";
import { RunloopSDK } from "@runloop/api-client";

const sdk = new RunloopSDK({ bearerToken: process.env.RUNLOOP_API_KEY });
const axon = await sdk.axon.create({ name: "my-channel" });
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,
  devboxId: devbox.id,
  shutdown: async () => {
    await devbox.shutdown();
  },
  requestPermission: async (params) => {
    const option = params.options[0];
    return { outcome: { outcome: "selected", optionId: option.optionId } };
  },
  onError: (err) => console.warn("transport error:", err),
});

conn.onSessionUpdate((sessionId, update) => {
  console.log(sessionId, update);
});

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

axonStream(options): Stream

Low-level function that creates an ACP-compatible duplex stream backed by an Axon channel from @runloop/api-client. Uses axon.subscribeSse() for inbound events and axon.publish() for outbound messages.

Parameters (AxonStreamOptions):

| Field | Type | Required | Description | |-------|------|----------|-------------| | axon | Axon | Yes | Axon channel from @runloop/api-client | | signal | AbortSignal | No | Cancellation signal | | onRawEvent | (event: AxonEventView) => void | No | Callback for every raw Axon event | | onError | (error: unknown) => void | No | Callback for swallowed parse errors | | onDisconnect | () => void | No | Called when the SSE stream disconnects |

Returns: { readable: ReadableStream<AnyMessage>; writable: WritableStream<AnyMessage> }

The stream handles JSON-RPC ID correlation internally — Axon's wire format doesn't carry IDs, so the transport layer maintains mapping tables to synthesize and restore them.

Session Update Type Guards

Narrowing helpers for discriminating SessionUpdate variants:

import {
  isUserMessageChunk,
  isAgentMessageChunk,
  isToolCall,
  isUsageUpdate,
  // ...
} from "@runloop/agent-axon-client/acp";

agent.onSessionUpdate((sessionId, update) => {
  if (isAgentMessageChunk(update)) {
    process.stdout.write(update.message);
  } else if (isToolCall(update)) {
    console.log(`Tool: ${update.toolName}`);
  }
});

Available guards: isUserMessageChunk, isAgentMessageChunk, isAgentThoughtChunk, isToolCall, isToolCallProgress, isPlan, isAvailableCommandsUpdate, isCurrentModeUpdate, isConfigOptionUpdate, isSessionInfoUpdate, isUsageUpdate.

Re-exported ACP Types

All types from @agentclientprotocol/sdk are re-exported for convenience:

import type {
  SessionUpdate,
  SessionNotification,
  ToolCall,
  ContentBlock,
  // ... etc.
} from "@runloop/agent-axon-client/acp";

Claude Module

ClaudeAxonConnection

Bidirectional, interactive client for Claude Code via Axon. Messages are yielded as SDKMessage from @anthropic-ai/claude-agent-sdk — the exact types the Claude Code CLI emits.

Constructor:

new ClaudeAxonConnection(axon: Axon, devbox?: Devbox, options?: ClaudeAxonConnectionOptions)

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | axon | Axon | Yes | The Axon channel (from @runloop/api-client) | | devbox | Devbox | No | If provided, shut down automatically on disconnect() | | options | ClaudeAxonConnectionOptions | No | See below |

ClaudeAxonConnectionOptions:

| Field | Type | Description | |-------|------|-------------| | verbose | boolean | Emit verbose logs to stderr | | systemPrompt | string | Override the system prompt | | appendSystemPrompt | string | Append to the default system prompt | | model | string | Model ID (e.g. "claude-sonnet-4-5") — set after initialization |

Lifecycle:

| Method | Description | |--------|-------------| | connect() | Connect to Claude Code, initialize the control protocol, and set model if configured | | disconnect() | Close the transport, fail pending requests, and shut down the devbox if provided |

Messaging:

| Method | Description | |--------|-------------| | send(prompt) | Send a user message. Accepts a string or SDKUserMessage. | | receiveMessages() | Async iterator yielding all SDKMessages indefinitely | | receiveResponse() | Async iterator yielding messages until (and including) a result message |

Control:

| Method | Description | |--------|-------------| | interrupt() | Interrupt the current conversation turn | | setPermissionMode(mode) | Change the permission mode | | setModel(model) | Change the AI model |

AxonTransport

Lower-level transport that implements the Transport interface using Runloop Axon. Used internally by ClaudeAxonConnection but available for custom integrations.

import { AxonTransport, type Transport } from "@runloop/agent-axon-client/claude";

const transport = new AxonTransport(axon, { verbose: true });
await transport.connect();

await transport.write(JSON.stringify({ type: "user", message: { role: "user", content: "Hello" } }));

for await (const msg of transport.readMessages()) {
  console.log(msg);
}

await transport.close();

Transport interface:

| Method | Description | |--------|-------------| | connect() | Open the underlying connection | | write(data: string) | Send a JSON message string | | readMessages() | Async iterable of parsed inbound messages | | close() | Close the transport | | isReady() | Whether the transport is connected and not closed |


Architecture

Both modules communicate over Runloop Axon channels but use different wire formats:

ACP Module                                    Claude Module

┌─────────────────┐                           ┌─────────────────┐
│  axonStream()   │                           │  AxonTransport   │
│  (Axon SDK)     │                           │  (Axon SDK)      │
│       ↕         │                           │       ↕          │
│  JSON-RPC 2.0   │         Axon Bus          │  Claude SDK      │
│  translation    │◄───────────────────────►  │  wire format     │
│       ↕         │       (SSE + publish)     │       ↕          │
│  ACPAxon        │                           │  ClaudeAxon      │
│  Connection     │                           │  Connection      │
└─────────────────┘                           └─────────────────┘
        ↕                                             ↕
   ACP Agent                                   Claude Code
   (in devbox)                                 (in devbox)

| | ACP Module | Claude Module | |---|---|---| | Wire format | JSON-RPC 2.0 via Axon events | Claude SDK messages via Axon events | | Transport | @runloop/api-client Axon SDK | @runloop/api-client Axon SDK | | Agent protocol | @agentclientprotocol/sdk | @anthropic-ai/claude-agent-sdk | | ID tracking | Synthetic (transport maps IDs) | Native (SDK handles correlation) |

Types

AxonEventView (ACP module)

Raw event from the Axon event bus:

interface AxonEventView {
  axon_id: string;
  event_type: string;
  origin: "EXTERNAL_EVENT" | "AGENT_EVENT" | "USER_EVENT" | "SYSTEM_EVENT";
  payload: string;
  sequence: number;
  source: string;
  timestamp_ms: number;
}

WireData (Claude module)

Generic JSON wire format used by the Claude transport:

type WireData = Record<string, any>;

Known Limitations

  • Eager SSE connection (ACP): The ACPAxonConnection constructor immediately opens an SSE subscription via axon.subscribeSse(). Connection errors surface on the first awaited method call, not at construction time.
  • No automatic reconnection: If an SSE stream drops, the connection is dead. Create a new instance to reconnect.
  • Permission handling (Claude): The ClaudeAxonConnection auto-approves all tool use by default. Override via incoming control request handling is not yet exposed as a configuration option.

License

MIT