## Problem On ACP v2, a model cycle that Claude Code starts on its own, such as its follow-up after a background command finished, is foreground work: `state_update` `running`, then `idle`. When a cancel stops such a cycle, the tool calls that it left open can stay `pending` after `idle` `cancelled`. Claude Code answers each tool of the main conversation with an interrupted result, and the adapter delivers it before the cycle's `idle`. It sends nothing for what a foreground subagent of the cycle ran, nor for a tool call that Claude abandoned before it ran. No turn settles at the cycle's end, so `closeCancelledToolCalls` (#1327, #1221) never runs for it. Example, live (Claude Code 2.1.296, Sonnet): a background `sleep 6` ends its turn. When it finishes, Claude Code's follow-up runs a foreground subagent, which runs a 60 s command, and the client cancels while it runs: ``` root state_update running <- Claude Code's own cycle child tool_call gKT4K4 (the subagent's command) pending ... session/cancel ... root tool_call_update PLh9rB (Agent call) cancelled <- Claude Code's result child state_update idle cancelled root state_update idle cancelled child gKT4K4: still pending, for good ``` ## Change - `closeCancelledCycleToolCalls` ends the open tool calls with the cancelled report before the cycle's end is reported, at the cycle's result and at the idle that ends it when no result comes. The calls are the ones the turn's closing takes: every open call except those of a background subagent that still runs. - It applies when a cancel is in effect (`session.cancelled`) and the cycle is the foreground work (`runsAutonomousCycle`). A turn that took the cycle over closes its own calls when it settles. - The loop of `closeCancelledToolCalls` is now `closeOpenToolCalls`, and its filter is named `closedAtCancel`. These two hunks are the same as in #1336, so that the PRs merge in any order. - The follow-up cycle of a turn that v1 holds for its background subagents is a cycle on v2, and its closing now comes before its `idle`. It came at the held turn's settle, after it. ## Evidence **Tests** (6 new): - `acp-v2.test.ts`: the command of the cycle's foreground subagent ends `cancelled` in the child's session, after the child's idle and before the cycle's, once; a tool call that Claude abandoned in the cycle ends before `idle` `cancelled`; so does one at the idle that ends a cycle without a result; the follow-up cycle of a held turn closes before `idle` `cancelled`, and a background subagent that keeps running keeps its command open; a cycle that the SDK aborted without a cancel is left to Claude Code's result, which still reaches the client. - `incomplete-tools.test.ts`: on v1, which reports no cycle, a cancelled cycle's tool call is not closed (see below). **Removal checks.** Each of these fails at least one test: no closing at the result (3), none at the idle (1), closing a cycle that no cancel stopped (1), without the cycle being the foreground work (2), closing a running background subagent's calls (1), closing after the cycle's end is reported (3), the turn's closing taking those calls too (5). **Live**, every update validated against the SDK's v2 JSON schema (0 violations): | cancel while Claude Code's cycle runs | `main` | this PR | | --- | --- | --- | | a foreground subagent's command | `pending` in the child's session, after the root's `idle cancelled` | `cancelled` in the child's session, after the child's idle and before the root's | | a command of the cycle itself | Claude Code's result, `cancelled`, before `idle` | the same, unchanged | `npm run check`, `npm run build` and `npm run test:run` (2930 tests) pass. No golden file changes. With #1335 and #1336 merged into it, in any of the six orders, the branches merge without conflicts and all 2948 tests pass. ## Not covered, and a decision for the maintainer - **v1.** Nothing tracks a cycle on v1, and with no turn active every result after the cancel is dropped, Claude Code's for the cycle's own commands too. In the same live run on v1 the Agent call and the subagent's command both stay `pending`. Closing at the cycle's cancelled result there would end both, with a made-up status for the Agent call too. I left it: it changes what v1 clients see for a cancel while idle, and one existing test (a held turn that the consumer closes while a cancel settles it inline) fails once the cycle check is removed, so it is not only a gate. It also touches the post-cancel window of #1061, which another change owns. - A cycle that ends without a cancel keeps an abandoned `tool_use` open. `settleActive` closes that for a turn, and no turn owns a cycle. - A session that ends or closes with a cycle running ends the cycle as cancelled without closing its calls.
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
/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
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_updateon the parent announces the child before any of its traffic, with the parent'stitleanddescriptionfor it, and reports its work state:running,requires_actionwhile one of its requests is open, andidlewhen its work ends, withend_turnorcancelledwhen the SDK gives the reason. A failure isidlewith theerrorstop reason and the SDK's error text as itserror. The child keeps its session for every delegation to it.- The prompt of the Agent or Task call that launches the child, and the message of each
SendMessage call that resumes it, is a
session_messagein the child's transcript. The tool call stays a tool call of the session that made it. session/cancelon the parent interrupts Claude Code, which stops the foreground children that the turn waits on and, currently, background children too. Each stopped child reportsidlewithcancelledfrom Claude Code's own events, and the open requests of the foreground ones 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. 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 reportsidle. A child that still runs when Claude Code exits isidlewith 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: its prompt and
each SendMessage message are
session_messages, and its end is anidlebefore 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.
- A client cannot prompt, cancel, or otherwise change a child yet.
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
Started with CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1, 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, as before.
The draft can still change, and this surface changes with it.
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 and the AIR extensions are v1 only for now.
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).