| name | acp-typescript |
| description | Implement or drive the Agent Client Protocol (ACP) in any TypeScript repo via two paths. (1) BUILD a full ACP client/host when you need live streaming into your own UI — the NDJSON/JSON-RPC stdio transport, bidirectional request/notification routing, the session/prompt turn lifecycle, streaming session/update content blocks, client-side handlers (fs, terminal, permissions, elicitation), subprocess spawn plus process-group cleanup plus orphan reaping, per-agent quirks, and forwarding the agent stream to a browser (the SSE streaming problem) — includes a dependency-free runnable reference implementation. (2) SHELL OUT to the acpx CLI when you do not care about live streaming and just want a fast, headless, scriptable, CI agent run (prompt in, result out, no logs, with sessions, permissions, and auth already handled). USE THIS whenever wiring or driving an ACP agent (Claude, Codex, Cursor, OpenCode, Gemini, Qwen, Droid, etc.) from a TS or Node app, building an ACP client/host, running agents headlessly via acpx, or debugging stdio framing, streaming, permission round-trips, subprocess lifecycle, or acpx auth, sessions, and flows. |
Implementing ACP (Agent Client Protocol) in TypeScript
This skill is distilled from a production ACP host that drives Claude Code, Codex,
Cursor, OpenCode, Gemini, Qwen, Junie and arbitrary registry agents, streaming their
output to a browser. Every gotcha below cost someone real debugging time. Read the
mental model, then jump to the reference file you need. There is a runnable,
dependency-free reference implementation in reference-impl/ — start there if you
want working code in one shot.
When to use this skill
- You are wiring an ACP coding agent (Claude/Codex/Cursor/Gemini/OpenCode/Qwen/…) into a TypeScript or Node app.
- You are building an ACP client/host (the side that spawns and talks to agents).
- You are debugging: stdio framing, "messages arrive glued together / split", request/response correlation, notifications not firing, streaming text not appearing, permission prompts hanging, zombie agent processes, or "it works for Claude but not Codex".
- You need to forward an agent's streaming output to a browser/WebSocket/SSE and keep ordering + reconnection sane.
- You want to run an agent headlessly — a script, CI step, cron, or multi-agent flow — and just need the result, not a live stream.
FIRST DECISION: build a client, or shell out to ACPX?
Two paths. Pick before you write anything.
| If you need… | Do this |
|---|
| Live streaming into your own UI — token-by-token text, tool cards updating in place, permission dialogs, a browser transport | BUILD the client. Refs 01–07 + reference-impl/. This is the hard 80%; the skill exists to make it one-shot. |
| A result, headless — "run this task and give me the output", in a script / CI / cron / flow, no streaming UI, no logs | SHELL OUT to acpx. Ref 08-acpx-cli.md. Don't build a transport — acpx already implements all of refs 01–05 as a hardened CLI. |
Rule of thumb: no live-UI-streaming requirement ⇒ use ACPX (acpx <agent> exec '...',
or --format json for machine-readable output). It already handles framing, the turn
lifecycle, permissions, auth, sessions, and subprocess cleanup. You only build the client
when you specifically need to render the stream yourself. (Bonus: even when building, run
acpx --format json codex exec '...' to watch the real ACP NDJSON frames as a working
oracle.) See references/08-acpx-cli.md for the full acpx reference.
The rest of this skill is the build-it path. If you chose ACPX, jump straight to ref 08.
The one-paragraph mental model (read this first)
ACP is newline-delimited JSON-RPC 2.0, spoken bidirectionally over a child process's
stdio. You (the client/host) spawn the agent as a subprocess. You write JSON-RPC to
its stdin, you read JSON-RPC from its stdout, one JSON object per line (\n
terminated, UTF-8). The agent's stderr is logs — never protocol; drain it. The pipe is
symmetric: you call the agent (initialize, session/new, session/prompt), AND the
agent calls you back on the same pipe (fs/read_text_file, session/request_permission,
terminal/create, …) and streams progress to you via session/update notifications.
A "turn" is one session/prompt request that blocks until the agent is done and returns
a stopReason, while session/update notifications stream the actual content
concurrently during that request. There is no per-turn stream close — the turn boundary
is the prompt response.
The hard truths (the things people get wrong)
-
It is NOT SSE. People say "ACP streaming" and reach for Server-Sent Events. ACP
agent→host streaming is session/update JSON-RPC notifications over stdout NDJSON.
SSE/WebSocket only enters the picture if you re-broadcast that stream to a browser —
that is a separate transport you build on top (see references/06-browser-streaming.md).
Conflating the two is the #1 source of "why is ACP streaming so hard" pain. They are
two different streams with two different framings.
-
A notification is "a request with no id". Same wire shape as a request, minus the
id field. Detect it by absence of the id key — NOT by falsiness. id: 0 is a
real request id; if (!msg.id) is a bug that turns request 0 into a notification.
-
One read ≠ one message. A single OS read off stdout can contain 0, 1, or many
lines, and a line can be split across reads. You MUST buffer bytes, split on \n, parse
complete lines, and keep the trailing partial fragment for next time. Hand-rolling this
wrong = "random JSON.parse errors under load".
-
One writer only. Both your outbound calls AND your responses to the agent's
callbacks share the same stdin pipe. If two async tasks write concurrently you get
interleaved/torn JSON lines. Funnel all writes through a single serialized queue/mutex.
-
session/prompt blocks; updates stream during it. Don't expect prompt to return
deltas. It returns { stopReason } at the end. The text/tool/thought content arrives
as session/update notifications before the prompt response resolves.
-
The protocol layer does NOT accumulate content. ACP sends discrete tagged update
events (agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update,
plan, …). Coalescing text deltas, merging tool-call updates, assembling reasoning —
that is 100% your application logic above the wire. Don't look for it in the transport.
-
Agents fork and orphan. Some agents (notably JetBrains Junie via a .app launcher)
detach into a long-lived JVM that survives your SIGKILL. Without a process-group spawn
- a boot-time orphan reaper you accumulate zombie processes at ~150MB/30%CPU each.
-
JSON-RPC ids must fit in a signed 32-bit int. JVM-backed agents parse the id as a
Java/Kotlin Int. Ids > 2,147,483,647 silently fail. Keep your counter well under 2^31.
Implementation roadmap (the "one shot" order)
Build bottom-up, verify each layer before the next (do not write all four then debug the
assembly — test the framer with a fake peer first):
- Transport — NDJSON framer + bidirectional JSON-RPC router + pending-request map +
single-writer queue. →
references/01-transport.md, reference-impl/acp-transport.ts.
Verify: feed it glued/split lines and a notification with id:0; assert correct routing.
- Handshake & methods —
initialize (version + capability negotiation), then
session/new. → references/02-handshake-and-methods.md.
- Turn + streaming —
session/prompt that resolves on stopReason while a
session/update handler accumulates content blocks into your UI model.
→ references/03-streaming-and-turns.md.
- Client handlers — answer the agent's callbacks: permission, fs read/write, terminal,
elicitation. Default unhandled methods to JSON-RPC
methodNotFound (-32601).
→ references/05-client-handlers.md.
- Subprocess lifecycle — spawn detached (process group), drain stderr, three-layer
cleanup, orphan reaper, env/credential passthrough. →
references/04-subprocess-lifecycle.md.
- (Optional) Browser streaming — re-broadcast the normalized event stream over
WebSocket/SSE with snapshot-then-live + reconnect. →
references/06-browser-streaming.md.
- Per-agent hardening — capability probes, auth flows, content-shape differences.
→
references/07-per-agent-quirks.md.
Reference implementation (dependency-free, runnable)
reference-impl/ contains a complete plain-TypeScript ACP client with zero
dependencies (only Node's child_process). It is the fastest path to working code:
acp-transport.ts — the NDJSON + bidirectional JSON-RPC core. The crown jewel; everything
hard lives here (framing, routing, correlation, notification detection, termination).
acp-client.ts — thin host API on top: initialize, newSession, prompt (with a
streaming onUpdate callback), and client-handler registration.
spawn.ts — spawn an agent as a process-group leader + signal-safe cleanup.
example.ts — end-to-end: spawn an agent, initialize, create a session, prompt, print the
streamed updates, answer a permission request.
framer.test.ts — proves the framing/correlation gotchas (glued lines, split lines,
id:0, notification routing). Run with node --test after compiling, or bun test.
Constants you will need (ACP schema v0.11.3 / protocol version 1)
PROTOCOL_VERSION = 1
Agent methods (you → agent):
initialize, authenticate, logout,
session/new, session/load, session/list, session/fork, session/resume, session/close,
session/prompt, session/cancel,
session/set_mode, session/set_model, session/set_config_option
Client methods (agent → you):
fs/read_text_file, fs/write_text_file,
session/request_permission, session/elicitation, session/elicitation/complete,
session/update, // <- the streaming notification
terminal/create, terminal/output, terminal/wait_for_exit, terminal/kill, terminal/release
JSON-RPC error codes:
-32700 parse, -32600 invalid request, -32601 method not found,
-32602 invalid params, -32603 internal,
-32000 auth required, -32002 resource not found, -32800 request cancelled (unstable)
session/cancel, session/update, session/elicitation/complete are notifications
(no id). Everything else in the agent/client method lists is a request (has id).
The killer checklist (paste this into your PR review)
Now open the reference file for the layer you are building. Each one quotes the exact
patterns and the exact bugs to avoid, framework-agnostically (the source host used Effect;
every reference notes what is Effect-specific vs protocol-fundamental so you can port to
plain Promises, an EventEmitter, RxJS, or whatever your repo uses).
ACP Registry - Known Agents and Their Quirks
The ACP Registry provides standardized agent.json configurations for ACP-compliant agents. Below are the most common agents and their specific invocation patterns:
Qwen Code (Alibaba)
- Registry ID:
qwen-code
- Version:
0.17.1
- Invocation:
npx @qwen-code/qwen-code@0.17.1 --acp --experimental-skills
- Local CLI:
qwen --model qwen3.7-max --acp --experimental-skills
- Protocol Quirks:
- protocolVersion: Expects
number (e.g., 1) not string ("1.0")
- session/new: Requires
cwd and mcpServers parameters
- session/prompt: Expects prompt as array of objects:
[{ role: "user", content: "..." }]
- Auth: Uses OpenAI API key (set via
--auth-type=openai or OPENAI_API_KEY)
Claude ACP (Anthropic)
- Registry ID:
claude-acp
- Version:
0.41.0
- Invocation:
npx @agentclientprotocol/claude-agent-acp@0.41.0
- Protocol Quirks: Standard ACP protocol, follows specification closely
Codex ACP (OpenAI)
- Registry ID:
codex-acp
- Version:
0.15.0
- Invocation:
npx @zed-industries/codex-acp@0.15.0 or binary downloads
- Protocol Quirks: Standard ACP protocol, follows specification closely
Junie (JetBrains)
- Registry ID:
junie
- Version:
1831.35.0
- Invocation: Binary with
--acp=true flag
- Protocol Quirks:
- Orphan risk: High - spawns JVM process that survives SIGKILL, requires process-group spawn + orphan reaper
- Binary path: macOS uses
.app bundle, Linux uses junie-app/bin/junie, Windows uses junie/junie.exe
Auggie CLI (Augment Code)
- Registry ID:
auggie
- Version:
0.29.0
- Invocation:
npx @augmentcode/auggie@0.0.29 --acp
- Protocol Quirks:
- Env var: Set
AUGMENT_DISABLE_AUTO_UPDATE=1 to prevent auto-updates
Cortex Code (Snowflake)
- Registry ID:
cortex-code
- Version:
1.0.73
- Invocation: Binary with
acp serve args
- Protocol Quirks: Uses custom binary name (
coco), standard ACP otherwise
Common Patterns Across Agents
- Session Creation Parameters: Most modern agents (Qwen, Devin) require
cwd and mcpServers parameters in session/new
- Protocol Version: Some agents (Qwen) expect numeric protocol version, others accept string
- Prompt Format: Agents may expect string prompt, array of strings, or array of message objects
- Binary vs NPX: Some agents are distributed as binaries, others as npm packages
Usage in TypeScript
const initParams = {
protocolVersion: "1.0",
capabilities: { streaming: false },
clientInfo: { name: "my-app", version: "1.0.0" }
};
const sessionParams = {
cwd: process.cwd(),
mcpServers: []
};
const promptParams = agent === "qwen"
? { sessionId, prompt: [{ role: "user", content: "..." }] }
: { sessionId, prompt: "..." };
Fetching Latest Registry Data
To get the latest agent configurations:
curl https://raw.githubusercontent.com/agentclientprotocol/registry/main/agent-index.json
curl https://raw.githubusercontent.com/agentclientprotocol/registry/main/<agent-id>/agent.json
This section is maintained from the official ACP registry at https://github.com/agentclientprotocol/registry