mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
The first of three stacked PRs that replace #1257, split for review. Together they bring v1 clients the [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx), as revised by agentclientprotocol/agent-client-protocol#2281 and #2308. **AIR is unchanged**: its golden files don't change. 1. **This PR:** the RFD form for a v1 client that declares `subagents` and isn't AIR. 2. #1321: a child keeps its session across SendMessage resumes. 3. #1322: a child reports `requires_action` while its permission or question request is open. Merge them in that order. Each later PR is based on the previous branch, so its diff shows only its own part. ## Who gets what - **AIR** (any client that declares `_meta.jetbrains.air`): the earlier draft it implements, exactly as before. That's `subagent_spawned`, `subagent_state_update`, a session per generation, the hidden Agent/Task tool call, and draft replay. AIR keeps it whichever signal it declares. - **A v1 client with `clientCapabilities.subagents` that isn't AIR:** the RFD form below. SDK 1.8.0 types it (`subagent_update`, `session_message`, the v1 `StateUpdate`). - **Without either signal:** nothing changes. `subagentForm` (`src/acp-subagents.ts`) picks the form. `NativeSubagentRuntime` keeps the routing, buffering and nested lineage, and renders either form. ## The RFD form | RFD | Before | Now | | --- | --- | --- | | Announce the child before any of its traffic | `subagent_spawned { name, task, prompt }` | `subagent_update { sessionId, title, description, state: { state: "running" } }` | | `description` is the role or purpose, "not a transcript of instructions" | `task` was the prompt | `title`: the agent's `name`, with its role (`subagent_type`) after it, else the role. `description`: the Agent input's short `description` | | The parent's message to the child is an incoming `session_message` | `prompt` on the announcement | a `session_message` in the child's transcript, with the Agent/Task tool use's id and the session that made the call as sender | | Delegation tools stay tool calls | the Agent/Task tool call was hidden | it stays a tool call of the session that made it, with the report its model got. Its heartbeats and result stay there too | | Work-state snapshots, not a terminal lifecycle | `completed`, `failed`, `cancelled`, `disconnected` | `running`; `idle` with `end_turn`, `cancelled`, or `error` with the SDK's error text as its JSON-RPC `error`; `idle` with no stop reason for work that ended without a known reason (a worker restart, or still running when the stream ends) | | `idle` isn't terminal | later updates were dropped | an idle child still gets its updates, such as its background work's | | A cancel stops the work it waits on; other descendants are up to the Agent (#2308) | `session/cancel` ended every child as cancelled | the adapter ends no child itself. Claude Code's interrupt stops the foreground children, and currently background ones too; each reports `idle`/`cancelled` from the SDK's events, and the foreground ones' requests are withdrawn | | Never guess a parent | a child that ended before its spawning frame was announced under the root | it stays unexposed. A nested child waits for its parent's announcement | | Never move a tool call | | a tool call that a permission request emitted on the root before its child was announced stays on the root after a cancel. AIR keeps the cancel's cleanup, since its draft ends every child there | | No agent-side capability | `sessionCapabilities.subagents` for every client | for AIR only | ## Not in this PR - **SendMessage resumes** (#1321). Until then a resumed child stays `idle`. Requests find a child by its agent id, so only the permission and question requests of the resumed run appear in its session. A resume never opens a second session for the child, which the RFD forbids, and a test pins that. - **`requires_action`** (#1322). - **Replay** doesn't expose children yet. Today's replay invents `<session>:replay-subagent:<id>` ids, and the RFD requires the original ones. The RFD makes child replay a SHOULD, so an RFD client's replay leaves children out until the replay PR. ## Settled during the review of #1257 - **Cancel cascade:** the RFD leaves background children to the Agent (#2308). An agent-level test pins #2308's validation scenario. - **Session ids across forks:** the raw SDK agent id stays the session id. The SDK's fork copies no subagent history, so a fork can't repeat an id under another root. The reasoning is on `nextChildSessionId`. - **Held turns:** the adapter holds a v1 prompt open while a background subagent the turn started runs. A cancel still settles the prompt. ## Live run Against real Claude Code (Sonnet), with a v1 client that declares `subagents` and checks every update and permission request against the SDK's v1 schema, there were no schema violations: - **Launch:** `running` with the title (`scout (general-purpose)`) and description, the prompt as a `session_message` from the root, and the child's tool calls in its session. Then `idle`/`end_turn` before the Agent call completes on the root. - **A 40-second child:** the Agent call's heartbeat and result stay on the root. Before the fix, they moved to the child, because the heartbeat named the Agent call as its own parent. - **Cancel:** a foreground child is `idle`/`cancelled` 15 ms after `session/cancel`, and the prompt returns `cancelled`. Claude Code stopped a background child too (its metadata says `stoppedByUser`). - **Found on `main`, for every client, and fixed separately:** after a cancel, a tool call that was running never gets a final status. ## Tests - **AIR:** the end-to-end tests of the earlier draft run with an AIR client that declares `nativeSubagentSessions` (`AIR_NATIVE_SUBAGENTS`). The AIR golden files are unchanged. - **RFD, end to end:** - announcement and prompt - a failure with the SDK's error, also when the error arrives after the failure - a stop, and a worker-restart orphan - no draft updates - the cancel per #2308 - the raced tool call after a cancel, kept on the root, with AIR's cleanup kept - the Agent call's heartbeat Every update is checked against the SDK's v1 schema. - **RFD, runtime:** - the visible control tool call, also nested - labels, also from a late frame - an unparented child staying unexposed, and a nested child announced only after its parent - nothing reported at the parent's cancel - the end of the stream, and `unknown` - a resume opening no second session - **Plain/Zed comparison with origin/main:** it now allows the missing agent capability, which `compare.ts` documents. - **Mutation checks during #1257's reviews:** removing any of these rules fails tests. - **Checks:** `npm run check`, build and all 2863 tests pass. ## Release Titled `feat:`, because it changes what a v1 client that declares `subagents` receives.