Node.js client library for Claude Code and Codex over the Agent Client Protocol (ACP).
Runtime requirement: Node.js ≥ 20
A thin TypeScript wrapper that hides subprocess management, ACP JSON-RPC framing, and session lifecycle behind a three-method API: create a client, start a session, stream prompts.
- Session-based multi-turn conversations with Claude Code or Codex agents
- Streaming prompt responses via
for awaitasync iterators - Automatic subprocess cleanup via process exit hooks and
Symbol.asyncDispose - Typed
ACPErrorhierarchy so every failure has a catchable class - Pluggable logger and metrics surface for production observability
- A replacement for
@agentclientprotocol/sdk— this library builds on top of it - A general LLM completion abstraction — for that, see Vercel AI SDK or LangChain
- A browser or edge-runtime library — Node.js ≥ 20 only
- An agent orchestration framework with routing, memory, or multi-agent coordination
- A one-shot completion helper — the session pattern is intentional
npm install lib-acpimport { createClient } from 'lib-acp';
const client = createClient({ agent: 'claude' });
const session = await client.startSession({ cwd: process.cwd() });
for await (const update of session.prompt('Explain ACP in one sentence.')) {
if (update.kind === 'agentMessageChunk') process.stdout.write(update.text);
}
await session.close();
await client.closeAll();Set ANTHROPIC_API_KEY before running. On first run, npx downloads the Claude agent binary automatically; subsequent runs reuse the cached binary.
Creates a long-lived factory. Spawns nothing on construction.
import { createClient } from 'lib-acp';
import type { LoggerLike } from 'lib-acp';
declare const myLogger: LoggerLike;
const client = createClient({
agent: 'claude',
defaults: {
model: 'claude-opus-4-5',
cwd: '/workspace',
logger: myLogger,
},
installExitHooks: true,
});
void client;Spawns the agent subprocess, performs the ACP handshake, and returns a Session.
import { createClient } from 'lib-acp';
const client = createClient({ agent: 'claude' });
const session = await client.startSession({
cwd: '/path/to/workspace',
model: 'claude-sonnet-4-5',
initializeTimeoutMs: 30_000,
});
await session.close();
await client.closeAll();Returns an async iterator of typed SessionUpdateEvent objects.
import { createClient } from 'lib-acp';
const client = createClient({ agent: 'claude' });
const session = await client.startSession({ cwd: process.cwd() });
for await (const update of session.prompt('Refactor this file.')) {
switch (update.kind) {
case 'agentMessageChunk': process.stdout.write(update.text); break;
case 'toolCallStarted': console.log('calling', update.name); break;
case 'promptCompleted': console.log('done:', update.stopReason); break;
}
}
await session.close();
await client.closeAll();Pass an AbortSignal to cancel mid-stream:
import { createClient } from 'lib-acp';
const client = createClient({ agent: 'claude' });
const session = await client.startSession({ cwd: process.cwd() });
const controller = new AbortController();
for await (const update of session.prompt('Long task', { signal: controller.signal })) {
if (update.kind === 'agentMessageChunk') process.stdout.write(update.text);
}
await session.close();
await client.closeAll();session.cancel() sends an ACP cancel notification; the active iterator drains and resolves, and the subprocess remains alive for the next prompt.
session.close() tears down the subprocess. Session implements Symbol.asyncDispose, so await using works in TypeScript 5.2+ with the Disposable lib:
import { createClient } from 'lib-acp';
const client = createClient({ agent: 'claude' });
{
await using session = await client.startSession({ cwd: process.cwd() });
for await (const update of session.prompt('Hello')) {
if (update.kind === 'agentMessageChunk') process.stdout.write(update.text);
}
}
await client.closeAll();session.prompt() yields a discriminated union of SessionUpdateEvent:
kind |
Fields | Description |
|---|---|---|
agentMessageChunk |
text |
Incremental agent response text |
agentThoughtChunk |
text |
Agent reasoning (when available) |
toolCallStarted |
toolCallId, name, input |
Agent is invoking a tool |
toolCallUpdated |
toolCallId, status, output? |
Tool call progress or result |
planUpdated |
entries |
Agent updated its plan |
currentModeUpdated |
modeId |
Session mode changed |
promptCompleted |
stopReason |
Final event — always emitted |
unknown |
raw |
Unrecognized update (forward compat) |
Every event also carries sessionId and the raw SDK value as raw.
Authentication is out of scope at the protocol layer. lib-acp passes the calling process's environment to the agent subprocess unchanged. Set the appropriate key before starting a session:
export ANTHROPIC_API_KEY=sk-ant-... # for Claude
export OPENAI_API_KEY=sk-... # for CodexBy default, lib-acp auto-approves all permission requests from the agent (file read/write, command execution). This is the MVP trade-off for trusted-backend use cases; Phase 2 will introduce configurable permission policies.
Warning: auto-approve grants the agent full access to the filesystem and shell within the working directory. Run in a sandboxed or isolated environment for untrusted workloads.
Before deploying a service that uses lib-acp:
- Set prompt timeouts: wrap
session.prompt()with anAbortControllerand callabort()after your SLA, then catchACPCancelledError. - Call
client.closeAll()on shutdown: register aSIGTERM/SIGINThandler and awaitcloseAll()before exiting, or rely on the default exit hooks (installExitHooks: trueis the default). - Wire a logger: pass a
loggerincreateClientdefaults orstartSession. The library logs spawn events, handshake errors, and dropped updates atwarnor above in production. - Limit concurrent sessions:
lib-acpdoes not cap concurrent sessions. GuardstartSessionwith a semaphore or rate limiter in high-throughput paths. Monitorclient.metrics.activeSessions. - Pre-install the agent in production: the first-run
npx --yesfallback downloads the agent binary at request time (~5–20 s). In production, pre-install vianpm install -g @agentclientprotocol/claude-agent-acp(Claude) or@zed-industries/codex-acp(Codex), or use thecommandoverride to point to a pinned binary. - Monitor
droppedUpdates: non-zero means a slow consumer is causing event drops. IncreasebackpressureTimeoutMsinPromptOptionsor reduce iteration latency. - Check
spawnFailuresTotal: a non-zero counter means the agent binary is missing or misconfigured. Alert on first occurrence.
This library follows semver. The public surface is everything exported from the main entry point (lib-acp). Internal modules (src/internal/*) are not part of the public API and may change in any release.
Current status: 0.x — pre-release. Breaking changes between 0.x releases are possible and documented in the changelog.