Files
claude-agent-acp/README.md
Ben Brandt cb285cd285 feat: serve ACP v2 with a direct v2 agent and remove the v1 bridge (#1560)
## Summary

ACP v2 was served by a bridge (`src/v2/agent.ts`) that converted each v2
request to v1 and ran it through `ClaudeAcpAgent`. Every v2 session
therefore ran the v1 core's turn machinery: held prompts, echo matching,
trailing-idle debt and the hold for background subagents. v2 asks for
none of it. This PR serves v2 with its own agent in `src/v2/`, which
follows Claude Code's own events. A prompt is answered when Claude Code
takes it in, and `state_update` reports foreground work. Background
subagents and tasks are listed in `background` and do not hold the turn.

- **New v2 agent (`src/v2/`).** `V2Agent` and `V2Session` cover:
  - the prompt lifecycle and cancellation
- subagents as child sessions, following the subagents RFD, plus
background lists
  - replay, restarts for provider and route changes, auth and providers
- elicitation, config options, `/remote-control`, and the
`--hide-claude-auth` guard
- **Bridge removed.** `ClaudeAcpAgent` serves ACP v1 only: its `v2`
option, foreground/background hooks and `this.v2` branches are gone. v1
behavior is unchanged, and no v1 test expectation or golden file
changed. The control flow of `acp-agent.ts` is simpler and the file is
about 450 lines shorter.
- **Shared modules.** Protocol-neutral logic moved out of `acp-agent.ts`
into modules both versions use, including:
- `session-model.ts`, `session-effort.ts`, `session-config-options.ts`
and `session-env.ts`
  - `providers.ts`, `auth-status-reporter.ts` and `context-window.ts`
  - `result-end.ts`: one set of rules for reading a result in both cores
  - `prompt-suggestions.ts` and `mcp-servers.ts`
- **JetBrains AIR on v2.** An AIR v2 client gets the AIR extensions that
ACP v2 does not cover:
  - `recommendedValue`
  - the permission presentation
  - question custom answers
  - prompt suggestions
- `agentFileChangeReport`, sent at the end of the turn, before its idle

ACP v2 itself covers subagent sessions, background subagents, async
tasks, plan files, diff patches, compaction, custom instructions and
session failures. See "ACP v2" in `docs/air-extensions.md`.
- **Ported from main.** Main's v2 changes made on the bridge are ported
to the new agent, and main's v2 tests run against it:
  - workflow cards and phases (#1550, #1552)
  - task tool calls (#1556)
  - plan removal on reset (#1554)
  - subagent checklist plan ids (#1558)
  - the Fable alias (#1548)

## Behavior changes for v2 clients

Each one follows the v2 draft spec, where the bridge did not:

- **Background subagents.** They no longer hold the prompt open. They
are listed in `background`, and their work is reported in their child
session's state.
- **`requires_action`.** While the root waits on foreground subagents,
it reports `requires_action` only when every one of them waits on the
user.
- **Resume of an active session.** Its work keeps running, and the new
MCP servers replace the old ones (`setMcpServers`). The bridge tore the
session down.
- **Cancel.** The idle after a cancel waits for Claude Code's aborted
result and carries its usage. A stopped child's open tool calls end
before its idle.
- **`/remote-control`.** It runs as its own turn. Sent while Claude
works, it waits for that work to end.
- **Accepted URL elicitation.** It keeps the work `requires_action`
until Claude Code reports the elicitation complete.
- **Exited Claude Code.** A session whose Claude Code process exited
stays active and answers -32603 with what happened, until it is resumed.
- **Provider change.** `providers/set`/`disable` restarts idle sessions
before it answers. A busy session restarts at its next turn boundary,
and its prompts are not held meanwhile, which the provider RFD allows.

## Workarounds

- **`/remote-control` waits outside the prompt queue.** The adapter's
own `/clear` and plan prompts, which continue a turn in a fresh context,
use their own push queue (`ownPushes`). They must not wait behind a
`/remote-control` that waits for that same turn to end.
- **Restart on additional directories.** A resume with other additional
directories restarts the query at its next turn boundary. The SDK cannot
change the directories of a running query.

## Rollback

`CLAUDE_AGENT_ACP_PROTOCOL=v1` negotiates every client down to v1. The
bridge and its `CLAUDE_AGENT_ACP_V2_CORE` switch are removed.

## Not served on v2 yet

- AIR's tool call keys (`commandTitle`, `subagent`, `skill`, `mcp`), the
session index, and API retry warnings.
- A `replayFrom` cursor other than `start`, which is rejected with
-32602, and steering, which is v1 only.

## Packaging

- `lib.js` exports are unchanged.
- `acp-agent.js` exports no name fewer than before. It adds
`AUTONOMOUS_RESULT_ORIGINS`, `getAvailableSlashCommands` and
`isAbortedResult`, which the v2 agent uses, and the export pin test is
updated.
- Internal `dist/v2/*` modules changed. They are reachable only through
the `"./*"` export and are not documented.

## Testing

- `npm run format:check`, `lint`, `build` and `test:run` pass: 3935
passed, 33 skipped.
- `npm run test:integration` against the real CLI passes: 3960 passed, 8
skipped. This includes `src/tests/v2-integration.test.ts`.
- The v2 golden scenarios (`acp-scenarios-v2.test.ts`) match without
being regenerated.
- The binary builds and `scripts/smoke-binary.mjs` passes.
- Two tests unchanged from main can time out under heavy load: the
session-index file watcher test and v1's "lagged trailer" test. Both
pass on rerun.

## Follow-up

- Split `src/` into v1, v2 and shared modules.
- Move the SDK-to-ACP mapping that `src/v2/` still imports from
`acp-agent.ts` into a shared module.
- Add a lint rule that keeps the boundary.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 08:34:38 +01:00

11 KiB

ACP adapter for the Claude Agent SDK

npm

Use Claude Agent SDK from ACP-compatible clients!

This tool implements an ACP agent by using the official Claude Agent SDK, supporting:

  • Context @-mentions
  • Images
  • Tool calls (with permission requests)
  • Compact file changes through the negotiated AIR diff patch extension
  • Following
  • Edit review
  • TODO lists
  • Nested subagent transcripts
  • Interactive (and background) terminals
  • Custom Slash commands
  • Client MCP servers
  • An Ultracode session config option, which turns on Claude Code's dynamic workflows for every task. It appears when dynamic workflows are enabled and the model supports ultracode (any model with xhigh effort)
  • /mcp in the chat: the MCP server status as a list. The adapter runs /mcp reconnect, /mcp enable, and /mcp disable through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
  • Session-scoped long-running goals for AIR through the goal extension under _meta.jetbrains.air.goal
  • Structured errors, recovery, and warnings through the opt-in session failure extension
  • Concrete model and effort defaults through the opt-in recommended config value extension
  • Tool permission presentation, editable choices, and durable effects through the permission extension
  • One fact per field in every tool call report, as the ACP tool call contract defines
  • All AIR extensions, capabilities, and _meta keys: AIR extensions

Learn more about the Agent Client Protocol.

To try changes that have landed on main but are not released yet, install from the preview channel — every push to main publishes one. See docs/RELEASES.md.

npm install @agentclientprotocol/claude-agent-acp@preview

Standalone executables

Each release attaches archives for Linux (glibc and musl), macOS and Windows, on x64 and arm64, to its GitHub release. They need no Node.js install. Each holds two files: claude-agent-acp, and the Claude CLI (claude) it runs. Keep them in the same directory; a symlink to claude-agent-acp elsewhere works. To use a Claude CLI you already have instead, set CLAUDE_CODE_EXECUTABLE. On Alpine, install libgcc and libstdc++ first.

To build one for your machine (needs Bun) and check it:

npm run build:binary
npm run smoke:binary            # add `-- --full` to drive a real session; needs a Claude login

Subagent sessions

A client that declares clientCapabilities.subagents, other than JetBrains AIR, gets each Agent or Task subagent as its own child session, following the subagents RFD:

  • subagent_update on the parent announces the child before any of its traffic, with the parent's title and description for it, and reports its work state: running, requires_action while one of its requests is open (and running again when the last one ends, unless a cancel withdrew it: the child is stopping then), and idle when its work ends, with end_turn or cancelled when the SDK gives the reason. A failure is idle with the error stop reason and the SDK's error text as its error. The child keeps its session for every delegation to it.
  • A child that runs outside its parent's working directory, such as an Agent call with isolation: "worktree", gets its cwd on subagent_update before its first tool call. The adapter takes it from the cwd of the child's own hooks: its SubagentStart hook runs before its first model request. It sends cwd: null when the child is back in its parent's directory. A child in its parent's directory gets no cwd, and no child gets additionalDirectories: Claude Code gives a subagent the session's additional directories, so it inherits them. A replay announces a child with the cwd that its transcript recorded, and clears it after the response when that directory no longer exists.
  • Session messages replace the Agent or Task call that launches the child, which its parent is not shown:
    • The launch's prompt is a session_message in both transcripts: the parent's outgoing view and the child's incoming one. Both use the launch's tool use id, and each names the other with the draft RFD's recipientMessageId and senderMessageId.
    • A foreground child's answer, from the launch's result, is the child's session_message to the parent. In Claude Code's hand-back mode, the child's SubagentHandback call is its message to the parent instead, in both transcripts and linked the same way.
    • A launch whose child is never exposed is shown as a tool call after all, with its final status.
  • The message of a SendMessage call that resumes the child is a session_message in the child's transcript. The SendMessage call stays a tool call of the session that made it.
  • session/cancel on the parent interrupts Claude Code, which stops the foreground children that the turn waits on. Background children keep running, as the RFD prefers; a client stops one with its own cancel. Each stopped child reports idle with cancelled from Claude Code's own events, and its open requests are withdrawn. The adapter ends no child at a cancel itself, so a child that keeps running keeps its open requests and its open tool calls. To spare background children, the adapter declares the SDK option perTaskStopAffordance, which a client can turn off in _meta.claudeCode.options. That option spares background workflows too, which a client cannot stop, so the adapter stops them after the interrupt, as Claude Code did without it. Background commands keep running after a cancel either way. A tool call of a stopped child that Claude Code reports no result for, such as the command it was running, ends in the child's session before the cancelled prompt resolves. When Claude Code stops a background child after that, or while no prompt runs, the tool call ends right after the child reports idle. A child that still runs when Claude Code exits is idle with no stop reason.
  • A child whose parent is not yet known when it ends is not exposed, since the RFD forbids guessing its parent.
  • A replay restores each child under its live session id, from its own transcript, with the same session messages as live, and its end is an idle before the response. A launch without Claude Code's metadata file for the child stays an ordinary tool call of the parent, and a notice, for a client that takes notices, says that subagent history is missing.
  • After a load or resume, with or without replay, a SendMessage that resumes an agent from before it runs that agent in its session from before. Before the agent's traffic, each of its ancestors that the client was not told about in this connection is announced, parent before child and with no work state, as the RFD requires. An agent whose launch the session's history does not record stays unexposed.
  • Each running child is announced with the cancel capability. session/cancel with the child's session id stops that child's current work, through the SDK's Query.stopTask, and not its parent's: Claude Code also stops the foreground children it waits on, and the parent's turn goes on. Each stopped child reports idle with cancelled from Claude Code's own events, and its requests that the stop withdraws report no running first. A replayed child, which no longer runs, gets no capabilities. A client cannot prompt or otherwise change a child.

An ACP v2 client gets the same without declaring anything, since subagents are part of v2's baseline. Each child also reports its work state as its own state_update, right after the parent's copy.

JetBrains AIR keeps the earlier draft of the RFD (subagent_spawned, subagent_state_update), which it enables with nativeSubagentSessions in _meta.jetbrains.air.capabilities. See AIR extensions.

Without either signal, Agent and Task stay ordinary tool calls and child interactions stay on the root session. Clients that use the historical _meta["subagent-transcript"] capability or the forwardSubagentText session option retain the flattened child transcript.

Experimental ACP v2

The adapter also serves the draft ACP v2: a client that asks for protocol version 2 or later in initialize gets v2, and every other client gets v1. The draft can still change, and this surface changes with it.

CLAUDE_AGENT_ACP_PROTOCOL limits the adapter to one version. With v1, a client that asks for v2 is negotiated down to v1. With v2, the adapter rejects the initialize of a v1 client. auto, the default, serves both.

On v2 the adapter serves the session baseline (session/new, session/list, session/resume with replay, session/close, session/prompt, session/cancel), session/delete, session/fork, additional directories, config options (the mode is one), plans, notices, compaction, display terminals for commands, permission requests, elicitation, providers, auth, and subagents. Steering is v1 only for now.

v2 is served by the v2 agent in src/v2/, which runs straight on Claude Code instead of translating to v1. A v2 client that declares JetBrains AIR (_meta.jetbrains.air) also gets the AIR extensions that ACP v2 does not cover.

Contribution Policy

This project does not require a Contributor License Agreement (CLA). Instead, contributions are accepted under the following terms:

By contributing to this project, you agree that your contributions will be licensed under the Apache License, Version 2.0. You affirm that you have the legal right to submit your work, that you are not including code you do not have rights to, and that you understand contributions are made without requiring a Contributor License Agreement (CLA).