Fixes #1611. ## Problem On ACP v2, a prompt sent while the session works gets its reply twice. The client sees: 1. the reply, streamed under the model's `msg_` id 2. `state_update` `idle` with `stopReason: end_turn` and zero usage 3. `state_update` `running` 4. the same reply again, under a UUID 5. `state_update` `idle` with the real usage This happens whenever Claude Code folds the prompt into running work. The work can be a cycle that Claude Code started on its own, as in the issue, or the turn of an earlier prompt. ## Cause Claude Code sends one result, not two. The order of its frames differs for a folded prompt (CLI 2.1.323): | Prompt | Order | | --- | --- | | started the work | `result`, then `command_lifecycle` `completed` | | folded into the work | `command_lifecycle` `completed`, then `result` | `V2Session.lifecycle` took every `completed` frame of the last started prompt as the end of a turn that had no result, and closed the turn. The result then found no open turn. It opened a new one, saw no streamed text in it, and sent `result.result` as the reply. ## Fix The session remembers which prompt started Claude Code's current cycle (`cycleCommand`). Only that prompt's `completed` frame closes a turn that had no result. A folded prompt's `completed` frame leaves the turn open, and the result ends it. A result ends the cycle, so the next prompt of a turn that stays open (the `/clear` and the plan prompt of a fresh context) starts its own. The `cancelled` frame is unchanged: it still closes the turn for the prompt that started last. ## Tests - `v2.test.ts`: one `it.each` with the frames in the CLI's order, for a prompt folded into another prompt's turn and into a cycle that Claude Code started on its own. Both fail without the fix with `running, idle, running, idle`. - `v2-integration.test.ts`: one case against the bundled CLI. It passes with the fix. - Live, with the issue's steps: one reply and one `idle` with the real usage. ## Not covered - I did not capture what Claude Code sends when a cancel arrives during work with a folded prompt. - I observed the frame order on CLI 2.1.323 only. ACP v1 is not affected: it does not end a turn at a `completed` frame.
ACP adapter for the Claude Agent SDK
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
xhigheffort) /mcpin the chat: the MCP server status as a list. The adapter runs/mcp reconnect,/mcp enable, and/mcp disablethrough 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
_metakeys: 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
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, session/status, session/options (with initial configOptions on session/new and
session/resume), session/set_title, 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).