Closes #1061. ## Where #1061 stands The incident in #1061 was tool starts streamed after `session/cancel` that never got a final status, so a client's ledger of open tool calls kept them forever. Since that report: - **#1313** drops streamed text, thinking and tool calls of the stopped work once the client has been told that the work ended (the cancelled response on v1, `idle cancelled` on v2), while the window after the cancel is open. - **#1327** and **#1221** close the tool calls that streamed before that report, before it is sent. So a late streamed tool start no longer reaches the client. This PR adds regression tests for that, and closes the one path that still produced such a tool call. #1313 named it as not changed: a tool call that the stopped work creates outside the stream. ## The remaining path Example: the user sends a second prompt while the first runs, then cancels. Claude Code had already taken the second prompt off its queue, so it still runs it, and its answer writes a file. The consumer drops the stream and the messages of that answer (#1313). But Claude Code asks for the Write's permission on its control channel: - **`canUseTool`** created the tool call (`ensureToolCallEmitted`) and sent the permission request. AskUserQuestion's elicitation goes the same way. - **The v2 PreToolUse preview hook** created the tool call with the Write's patch. The tool's result is dropped too, so that tool call never ended: the ghost of #1061, now for tools that need a permission. ## The fix - **The consumer records each tool use it reaches,** at its streamed start or, for one that did not stream, at its complete message, with whether it dropped it. The condition is the stream's existing one, now `dropsStoppedWorkOutput`: the client was told that the stopped work ended, and the window after the cancel is open. - **`canUseTool` denies a dropped tool use without showing anything:** no tool call, no permission request, no elicitation. The model gets "The user cancelled this prompt." The **preview hook** sends nothing for it. - **The control channel runs ahead of the consumer,** which is why #1313 left this open. While a cancel's window may be open, `canUseTool` and the hook wait until the consumer reaches the tool use. Claude Code streams the tool use before it asks, so the consumer is usually there within a few ticks. Example: the next prompt's tool asks while the session still looks cancelled, because the consumer has not reached that prompt's echo yet. The wait sees the consumer reach the echo, which closes the window, and then the tool use, which it shows, so the request goes out. - **The wait is bounded:** 2 s, or until Claude Code withdraws the request. The stream never shows some tool uses, such as SandboxNetworkAccess's, and those then go out as before. - **Without a cancel, nothing changes,** not even timing: the check answers synchronously and adds no tick before the permission request. ## Evidence **Tests** (`src/tests/acp-agent.test.ts`, "output after a cancel"; `src/tests/acp-v2.test.ts`): - An abandoned prompt's Write: nothing reaches the client, and the tool is denied. Claude Code asks before the consumer reaches the tool use, between its streamed start and its message, after its message, or for a tool use that did not stream. The same on v2, with Claude Code running the registered PreToolUse hooks and then `canUseTool`. - The next prompt's tool, asked while the window still looks open: its request goes out once the consumer reaches it. - A tool use the stream never shows: the wait ends when the request is withdrawn (before it asked, or while it waits), or after the bound, and then the request goes out. - **Mutation checks:** removing each of the 11 rules fails at least one test. That covers the `canUseTool` check, the hook check, the wait, waiting instead of denying, the stream record, the message record, the stream's dropped flag, the abort exit, the aborted-signal guard, the timeout, and the synchronous answer. **Live, against Claude Code** (v1 and v2, `default` permission mode, every update checked against the schema): - After a cancel, the next prompt's Writes asked for their permissions at once and ran. Nothing was denied, and no tool call stayed open; on v2 the turn went `idle cancelled`, `running`, `requires_action`, `running`, `idle end_turn`. No schema violations. - The abandoned prompt's race did not happen live: Claude Code folded the queued prompt into the cancelled request, and the cancel stopped both. #1313 could not reproduce it either. The tests follow the order a direct CLI probe recorded for #1309. `npm run check`, build and the full suite (2931 tests) pass, with no golden changes. ## Not covered - On CLIs without `request_marker_lists_v1`, the window still lasts until the next prompt's echo, as before. - A tool that runs without a permission, in `acceptEdits` or `bypassPermissions` mode, still runs for an abandoned prompt. Nothing of it reaches the client, since its output is dropped. Stopping such tools would need the adapter to block tools it is not asked about, which this PR does not do.
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. 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).