Skip to content

rodolfochicone/lib-acp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lib-acp

Node.js client library for Claude Code and Codex over the Agent Client Protocol (ACP).

Runtime requirement: Node.js ≥ 20

What this is

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 await async iterators
  • Automatic subprocess cleanup via process exit hooks and Symbol.asyncDispose
  • Typed ACPError hierarchy so every failure has a catchable class
  • Pluggable logger and metrics surface for production observability

What this is not

  • 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

Quickstart

npm install lib-acp
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('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.

API overview

createClient(opts)

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;

client.startSession(opts)

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();

session.prompt(text, opts?)

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() and session.close()

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();

Streamed events

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

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 Codex

Permissions

By 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.

Production checklist

Before deploying a service that uses lib-acp:

  • Set prompt timeouts: wrap session.prompt() with an AbortController and call abort() after your SLA, then catch ACPCancelledError.
  • Call client.closeAll() on shutdown: register a SIGTERM/SIGINT handler and await closeAll() before exiting, or rely on the default exit hooks (installExitHooks: true is the default).
  • Wire a logger: pass a logger in createClient defaults or startSession. The library logs spawn events, handshake errors, and dropped updates at warn or above in production.
  • Limit concurrent sessions: lib-acp does not cap concurrent sessions. Guard startSession with a semaphore or rate limiter in high-throughput paths. Monitor client.metrics.activeSessions.
  • Pre-install the agent in production: the first-run npx --yes fallback downloads the agent binary at request time (~5–20 s). In production, pre-install via npm install -g @agentclientprotocol/claude-agent-acp (Claude) or @zed-industries/codex-acp (Codex), or use the command override to point to a pinned binary.
  • Monitor droppedUpdates: non-zero means a slow consumer is causing event drops. Increase backpressureTimeoutMs in PromptOptions or reduce iteration latency.
  • Check spawnFailuresTotal: a non-zero counter means the agent binary is missing or misconfigured. Alert on first occurrence.

API stability

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.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors