Files
claude-agent-acp/README.md
Ben Brandt ba3c7be05e feat: keep background subagents running after a parent cancel, for clients that can cancel them (#1348)
Stacked on #1347, which gives each running child the `cancel`
capability. Merge #1347 first; this PR's base is its branch.

## What changes

On a client that gets the subagents RFD form (v1 with
`clientCapabilities.subagents` and not AIR, and v2), `session/cancel` on
the parent now spares background subagents. They keep running, as
acp#2308 prescribes ("a background child that the cancelled work did not
wait on may continue"), and the client can stop each one with its
child's `cancel` from #1347. Foreground subagents that the turn waits on
still stop.

Plain v1 clients and AIR are unchanged: the interrupt still stops their
background subagents, since they have no way to stop one.

## How

- **The declaration.** Claude Code's interrupt kills background
subagents and workflows unless the query declares
`perTaskStopAffordance`. The SDK describes it as "this consumer renders
a per-task stop control", and an RFD client now has that control. The
adapter declares the option for those clients. It comes before
`_meta.claudeCode.options`, so a client can turn it off.
- **Workflows.** The declaration also spares background workflows
(`local_workflow`). A workflow's agents are not subagent tasks and get
no child session, so a client can't stop a workflow unless it is AIR
with `asyncTasks`. When a session declares the option and the client has
no such control, `session/cancel` stops each live background workflow
with `Query.stopTask` after the interrupt. Workflows therefore behave as
they did before.
- **Background commands** survive an interrupt with or without the
declaration (checked live and in the CLI code), so nothing changes for
them.

Where I read Claude Code 2.1.296's CLI: without the declaration, the
interrupt runs a kill of every running background `local_agent` and
`local_workflow` task. With it, the interrupt kills only observer tasks.
Adapter interrupts name no prompt, so this runs on every
`session/cancel`, including one with no turn running. The declaration's
only other effect is on requests with `interrupt_if_running`, which the
adapter never sends.

## Checks

**Live, Claude Code 2.1.296, through the built adapter.** Every update
was checked against the schema, and no run had a violation or left a
tool call open. Survival was checked by whether the background command's
process was alive 2 s after the cancel.

| Client | Background work at the cancel | Result |
|---|---|---|
| v1 RFD | subagent running a command | Prompt `cancelled` 9 ms after
the cancel. The child kept running, completed its command and reported
`idle end_turn`. Claude Code's follow-up cycle then streamed to the
root. |
| v1 RFD | subagent with an open permission request | The request stayed
open through the cancel. The client allowed it 5 s later, and the child
ran the command and ended `idle end_turn`. |
| v1 RFD | workflow | The workflow and its command stopped, with no
follow-up cycle. Same as the plain client. |
| v1 plain | subagent | Unchanged: the interrupt killed the child, and
its command's tool call ended `failed`. |
| v1 plain | workflow | Unchanged: stopped, no follow-up. |
| v2 | subagent | Root `idle cancelled` 11 ms after the cancel. The
child kept running and later reported `idle end_turn`, and the root went
`running`, then `idle end_turn`, for the follow-up. |
| v2 | workflow | Stopped, no follow-up. |

**Direct SDK probes** decided the design:
- With the declaration, an interrupt spares a background workflow.
- Without it, the interrupt kills the workflow.
- A `stopTask` on a workflow, before or after the interrupt, stops it
and its command, and Claude Code starts no follow-up cycle, the same as
when the interrupt killed it.

**Tests:**
- Option tests: the declaration for each client form, a caller's
override, and when a cancel stops workflows.
- Cancel tests: only workflows are stopped, after the interrupt and only
when the flag is set.
- Removing any of the eight rules fails a test.
- `npm run check`, build and all 3020 tests pass, with no golden
changes.

## Not covered

- A paused background workflow (one Claude Code restored on a resume)
would also be stopped at a cancel, where the interrupt left it alone. I
didn't check whether such a workflow reports `task_started`.
- Standard ACP has no control to stop background work that is not a
session, such as a workflow or a background command. Only AIR has one,
through its async tasks extension.
2026-10-14 21:56:01 +02:00

114 lines
8.1 KiB
Markdown

# ACP adapter for the Claude Agent SDK
[![npm](https://img.shields.io/npm/v/%40agentclientprotocol%2Fclaude-agent-acp)](https://www.npmjs.com/package/@agentclientprotocol/claude-agent-acp)
Use [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview#branding-guidelines) from [ACP-compatible](https://agentclientprotocol.com) clients!
This tool implements an ACP agent by using the official [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview), supporting:
- Context @-mentions
- Images
- Tool calls (with permission requests)
- Compact file changes through the negotiated [AIR diff patch extension](docs/air-extensions.md#diff-patch)
- Following
- Edit review
- TODO lists
- Nested subagent transcripts
- Interactive (and background) terminals
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
- Client MCP servers
- `/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](docs/air-extensions.md#goal) under `_meta.jetbrains.air.goal`
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/air-extensions.md#session-failure)
- Concrete model and effort defaults through the opt-in [recommended config value extension](docs/air-extensions.md#recommended-config-values)
- Tool permission presentation, editable choices, and durable effects through the [permission extension](docs/air-extensions.md#permission-presentation)
- One fact per field in every tool call report, as the [ACP tool call contract](docs/air-extensions.md#tool-call-contract) defines
- All AIR extensions, capabilities, and `_meta` keys: [AIR extensions](docs/air-extensions.md)
Learn more about the [Agent Client Protocol](https://agentclientprotocol.com/).
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`](docs/RELEASES.md#preview-releases).
```sh
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](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx):
- `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.
- 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_message` in the child's transcript. The tool
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: its prompt and
each SendMessage message are `session_message`s, 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](#experimental-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](docs/air-extensions.md#native-subagent-sessions).
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](https://agentclientprotocol.com/protocol/v2/overview): 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](#subagent-sessions). 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](https://www.apache.org/licenses/LICENSE-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).