## Problem For a subagents RFD client, a subagent's child session reports `requires_action` while one of its permission or question requests is open, and `running` again once its last open request ends (#1322, `NativeSubagentRuntime.awaitingUser`). A request that ends because the work was **stopped** is not an answer, but it still reported `running`. On `main`, with Claude Code 2.1.296, a foreground child whose Bash permission request is open, and a cancel on the parent: | What stops the work | States the child reported | | --- | --- | | `session/cancel` (v1, and v2 on the child's own stream) | `running`, `requires_action`, **`running`**, `idle cancelled` | | `session/close` | `running`, `requires_action`, **`running`**, and then nothing: a closed session reports no `idle` | Claude Code's interrupt withdraws the request, so the SDK aborts the signal of the `canUseTool` call, the adapter sends `$/cancel_request`, and the request's `finally` reports `running`. The SDK's own report of the child's end (`task_updated killed`) comes a few ms later (3 to 4 ms in these runs), so the client shows a child that is being stopped as working. After a `session/close` nothing corrects it. ## Cause `awaitingUser` reported `running` whenever the last open request ended, whatever ended it. A request ends in two ways, the user answers it or Claude Code withdraws it, and a withdrawal alone does not say whether the work was stopped. Claude Code also withdraws a request when something else answers it, and then the child runs on. I ran one such case, a `PermissionRequest` hook that allows the command after 3 s: the child worked for 9 more seconds after the withdrawal, and `running` was right. (The CLI also re-checks a pending ask when its permission context changes, and withdraws it if that now allows the call. I did not manage to trigger that: `session/set_mode` to `bypassPermissions` left the request open in my run.) So "a withdrawn request reports no `running`" is wrong: that child would sit on `requires_action` for the rest of its run. Waiting for the child's next update instead does not help either: in that run the child's first update after the withdrawal came 8 s later, with the tool's result. ## Change A request reports no `running` when it was **withdrawn after a cancel that came while it was open**. Then Claude Code's interrupt withdrew it as it stopped the child, and the SDK's events report the child `idle`. - `NativeSubagentRuntime.parentCancelled` (called for `session/cancel`, and by the teardown of `session/close`) counts the cancels in the RFD form. It still reports nothing, so #2308 is unchanged: a child that keeps running keeps its requests and its state. - `awaitingUser` takes the request's `signal`, which the agent passes for permission requests and AskUserQuestion (`withChildAwaitingUser`). A request records the count when it opens. If its signal aborted and the count changed, its end reports no `running`. - Everything else is as before: a request that the user answers, also after the cancel, reports `running`; a withdrawal without a cancel reports `running`; two overlapping requests report once, when the last one ends, by how that one ended. - README: one clause in the work-state bullet of "Subagent sessions". AIR is unchanged: `awaitingUser` only runs in the RFD form, and the AIR golden files do not change. ## Evidence Live runs against Claude Code 2.1.296 (Sonnet), driving `dist/index.js` over stdio with a client that holds the child's permission request, validating every update and request against the SDK's JSON schema (0 violations in all runs): | Run | `main` | This PR | | --- | --- | --- | | v1, `session/cancel` | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | v2, `session/cancel` (child's stream) | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | v1, `session/close` | `running`, `requires_action`, `running` | `running`, `requires_action` | | v1, hook allows after 3 s, no cancel | `running`, `requires_action`, `running`, `idle end_turn` | the same | Tests (all fail without the rule, as checked by removing it): - `native-subagents.test.ts`: the cancel withdrawal (and the child's `idle cancelled` after it); a withdrawal without a cancel (the hook case, which a simpler "abort means stopped" rule fails); a request that outlives a cancel and is answered later; a cancel that came before the request opened; two overlapping requests, in each order. - `acp-agent.test.ts`: through `canUseTool`, with `agent.cancel` and `agent.closeSession` aborting the request as Claude Code does, and the hook case. - `acp-v2.test.ts`: the child's own `state_update` stream and the parent's copy. Removal checks, each run against the three test files: no rule (6 tests fail), the cancel not counted (6), the agent not passing the signal (3: agent cancel and close, v2), and the simple "any withdrawal" rule (3: the hook cases). `npm run check`, `npm run build` and `npm run test:run` pass (2935 tests; the scenario golden files do not change). ## Not covered - **A stream end without a cancel.** If Claude Code exits by itself, the SDK's cleanup aborts every request before the consumer reads the end of the stream, and nothing at that moment tells it from a hook's withdrawal. Live (`kill -9` of the CLI): `running`, then `idle`, 1 ms apart. The session is dead right after. A rule for it would be a timing one: report `running` for a withdrawal with no cancel one event-loop turn later, so that `finishAll` goes first. I tried it (`await new Promise((resolve) => setImmediate(resolve))` before the check in `awaitingUser`'s `finally`): the `kill -9` run then reports `running`, `requires_action`, `idle`, and the hook run still reports `running` 1 ms after the withdrawal. I did not include it. It only works when the end of the stream reaches `finishAll` in the same turn (a worker shutdown publishes its failure notice first, for one), and it hides an ordering assumption about the consumer. Your call. - **Claude Code stopping a child on its own** (the model's TaskStop) while its request is open: the same shape, no cancel. - **The root session on v2** has the same flicker for its own request: `requires_action`, `running`, `idle cancelled` (live, v2). It comes from `syncAwaitingUser`, not from this runtime, and is not changed here. - A background child that outlives a cancel and whose still-open request a hook then withdraws would report no `running`. Claude Code stops background children at an interrupt today, so it does not arise. Related, and independent: a child's AskUserQuestion is not abandoned on an abort when its client ignores `$/cancel_request` (#1338). The two PRs touch the same call site and merge cleanly in either order (checked with `git merge-tree`).
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 (andrunningagain when the last one ends, unless a cancel withdrew it: the child is stopping then), 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).