A modular and extensible ACP (Agent Client Protocol) client library designed to connect standard AI coding agents (Gemini, Claude, Codex) and TUI-based tools (Aider) to upper-layer systems.
Powered by @agentclientprotocol/sdk.
- Builder Pattern: Instantiate and configure client instances via a clean, chainable Builder (
AcpClientBuilder). - State Machine Integration: Expose fine-grained state queries (
disconnected,initializing,authenticated,ready,busy,shutting_down) and state change events. - Config-Driven Extensibility: Define custom client capabilities in plain
YAMLorJSONand register extension method handlers effortlessly. - Isolated Test Layer: Standardised Hello World testing is separated into its own package/layer to allow building the library and tests independently.
npm installcp .env.example .env
# Edit .env with your API keys (e.g., GEMINI_API_KEY)npm run hello -- gemini "Hello World"Instead of configuring child connections manually, use the AcpClientBuilder to set up and construct an AcpClient.
import { AcpClientBuilder } from "acp-client-prototype";
const builder = new AcpClientBuilder()
.withAgent("gemini") // Select agent adapter
.withVerbose(true) // Enable verbose debug logging
.withAutoApprove(true) // Auto-approve agent tool execution
.withSandboxDir("/sandbox") // Enforce FileSystem sandboxing
.withExtensionConfig("extensions.yaml") // Load custom client methods
.registerExtensionHandler("custom/greet", new MyCustomHandler());
const client = builder.build();The client exposes standard execution states and functions as a unified I/O channel for upper-layer orchestrators.
The client transitions through the following states, queryable via client.getState():
disconnected: The child agent process has not been spawned.initializing: The process is spawned and waiting for initialize shake-hands.authenticated: The client has resolved and executed the appropriate credential strategy.ready: Active session created; the client is idle and ready for user prompt instructions.busy: Currently streaming thoughts, chunks, or executing tool calls on behalf of the remote Agent.shutting_down: Process termination and resource cleanups are in progress.
The AcpClient class extends Node's EventEmitter with strictly typed method overrides (on, once, off). Modern IDEs (like VS Code) will automatically provide full autocomplete and type validation for both event names and their payload objects.
Below is the complete registry of typed events emitted by the client:
| Event Name | Parameter Type | Description |
|---|---|---|
stateChange |
(newState: ClientState, oldState: ClientState) |
Triggered on any connection or execution state transition. |
event |
(event: ConnectionEvent) |
Raw, unmodified wrapper for any packet coming from the connection. |
agent_message_chunk |
(payload: any) |
Live textual answer tokens streamed from the Agent. |
agent_thought_chunk |
(payload: any) |
Live thinking process tokens streamed from reasoning-capable Agents. |
tool_call |
(payload: any) |
Signals that the Agent wants to invoke a specific client/client method/tool. |
tool_call_update |
(payload: any) |
Signals the execution result status of a requested tool call. |
stderr |
(payload: any) |
Raw standard error diagnostics emitted by the underlying Agent process. |
permission_request |
(payload: any) |
Raised when an Agent requests permission to run interactive commands. |
// Strict autocompleted subscription
client.on("stateChange", (newState, oldState) => {
// TypeScript knows that newState and oldState are of type ClientState!
});Upper layers can subscribe directly to specific stream events rather than parsing raw text:
// Streamed text response chunks from the Agent
client.on("agent_message_chunk", (payload) => {
process.stdout.write(payload.update.content.text);
});
// Streamed thinking/reasoning chunks from the Agent
client.on("agent_thought_chunk", (payload) => {
console.log(`[Thinking] ${payload.update.content.text}`);
});
// Intercept tool calls
client.on("tool_call", (payload) => {
console.log(`[Tool] Agent requested: ${payload.update.title}`);
});You can easily extend client capabilities without altering the core codebase. This is done by specifying a configuration file and registering a corresponding handler.
methods:
- name: "custom/greet"
description: "Greet a user with a customized styling theme"
params:
name: "string"
style: "string"Write a custom class implementing ClientMethodHandler:
import { ClientMethodHandler } from "acp-client-prototype";
class MyCustomHandler implements ClientMethodHandler {
async handle(method: string, params: any): Promise<any> {
if (method === "custom/greet") {
return {
greeting: `Hello ${params.name || "User"}, styled using: ${params.style || "plain"}`,
};
}
throw new Error(`Unsupported custom method: ${method}`);
}
}const builder = new AcpClientBuilder()
.withAgent("gemini")
.withExtensionConfig("extensions.yaml")
.registerExtensionHandler("custom/greet", new MyCustomHandler());During client initialization, custom capabilities are packed and sent inside clientCapabilities.experimental, telling the AI Agent how to invoke these new capabilities.
| Agent | Connection | Auth Strategy | Description |
|---|---|---|---|
| gemini | acp |
env-auto |
Google Gemini via gemini-cli |
| claude | acp |
none |
Anthropic Claude via claude-agent-acp |
| copilot | acp |
none |
GitHub Copilot via @github/copilot |
| codex | acp |
none |
OpenAI Codex via codex-acp |
| opencode | acp |
pre-configured |
OpenCode AI via opencode-ai |
| goose | acp |
pre-configured |
Block/Square Goose via goose |
| kiro | acp |
pre-configured |
AWS Kiro AI via kiro-cli |
| codebuddy | acp |
env-auto |
Tencent CodeBuddy via codebuddy-code |
| aider | pty |
pre-configured |
AI coding assistant via PTY fallback |
src/
├── adapter/ # Agent definitions, quirks, & extensible registry
├── auth/ # Environment-auto, interactive, pre-configured strategies
├── client/ # Builder pattern, AcpClient lifecycle orchestrator
├── client-methods/ # Standard (FS, Terminal, Session) & Custom Extensions
├── connection/ # Protocol drivers (JSON-RPC / PTY abstraction)
├── core/ # Errors, shared types, ACP schemas
├── driver/ # Direction A Driver Wrapper layer (MockDriver)
├── hook-gate/ # Event schemas & interceptor callbacks (Decoupled)
└── session/ # Session cache & store
tests/
├── driver.test.ts # Direction A Driver contract integration tests
└── hello.ts # Separated testing layer
To support end-to-end multi-agent BCD pipelines, the micro-level AcpClient is wrapped inside the MockDriver adapter which implements DriverRuntimeHandle (from Direction C contract):
sendPrompt(input: DriverPrompt): Promise<DriverRunResult>: High-level execution envelope returning structured patch artifacts and audit logs compatible with SQLite states.
To run the standalone driver test suite:
npm run build && npm run build:test && node dist/tests/driver.test.js| Variable | Description |
|---|---|
VERBOSE=1 |
Enable detailed debug logging and state outputs |
AUTO_APPROVE=1 |
Automatically approve all agent filesystem & terminal requests |
CODEX_HOME |
Point to a custom directory to override global Codex configuration (e.g., ./.codex) |
OPENCODE_CONFIG |
Point to a custom JSON configuration file to override OpenCode config (e.g., ./.opencode.json) |
GOOSE_PATH_ROOT |
Point to a custom folder to sandbox Goose configuration, state, and data (e.g., ./.goose) |
GEMINI_API_KEY |
API key for Gemini adapter |
ANTHROPIC_API_KEY |
API key for Claude adapter |
OPENAI_API_KEY |
API key for Codex/Aider |
COPILOT_GITHUB_TOKEN |
GitHub Token with Copilot access for Copilot adapter (supports GH_TOKEN as fallback) |
By default, the OpenAI Codex adapter (codex-acp) expects its configuration in the global ~/.codex/ directory. If you want to use a project-local configuration (e.g., to override API endpoints or sandbox behavior), you can point CODEX_HOME to a local folder:
- Copy
.env.exampleto.envand setCODEX_HOME=./.codex. - Create local configuration files inside the
.codex/directory of your project:- Copy
.codex/config.toml.exampleto.codex/config.tomland customize it. - Copy
.codex/auth.json.exampleto.codex/auth.jsonand enter your credentials/API keys.
- Copy
These local configuration files are ignored by git to protect your API keys and workspace-specific settings.
By default, the OpenCode adapter (opencode-ai) expects its configuration in global directories like ~/.config/opencode/opencode.json. You can completely override this and use a project-local configuration (e.g., to use local Ollama models) by pointing OPENCODE_CONFIG to a local JSON configuration:
- Copy
.env.exampleto.envand setOPENCODE_CONFIG=./.opencode.json. - Create your project-local configuration file:
- Copy
.opencode.json.exampleto.opencode.jsonand customize your models and providers (e.g., setting up a local Ollama connection).
- Copy
This local configuration file is ignored by git to prevent local environment settings from leaking.
By default, the Goose adapter (goose) expects its data, state, and configuration in standard global directories like ~/Library/Application Support/Block/goose/. You can sandbox its configuration, state, and data to your project workspace by pointing GOOSE_PATH_ROOT to a project-local folder:
- Copy
.env.exampleto.envand setGOOSE_PATH_ROOT=./.goose. Also recommend settingGOOSE_DISABLE_KEYRING=1to store secrets inside your workspace plaintext configuration file instead of the OS system-wide secure keyring. - Run
goose configurein your terminal to automatically generate its complex configuration and file structures locally within the.goose/workspace folder.
Because Goose's config.yaml is highly complex and platform-specific, we do not provide a configuration template. Setting up the local config via goose configure ensures that Goose generates a completely valid schema natively.
The .goose/ workspace folder is ignored by git to prevent secrets and diagnostic caches from being tracked.
- Process Hangs: Ensure you call
client.shutdown()to clean up event loops and terminate child processes. - Sandbox Access Denied: The filesystem handler enforces a strict sandbox. Ensure agents are accessing paths relative to the current working directory.
- Goose/Kiro Driver Fails (ENOENT): If spawning the
gooseorkiroagent fails with anENOENTerror, check if the respective native CLI (gooseorkiro-cli) is installed on your local machine. Both are natively compiled binaries and do not have npm packages.