mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
v2 has a `cancelled` tool call status ("the tool call was cancelled
before it completed"), which v1 doesn't. Until now a v2 client got
`failed` for a tool call the user interrupted, the same as a real
failure.
## What changes
Claude Code stamps a tool result that carries no tool output with
`tool_result_meta.non_execution_kind`. For a **v2 client**:
| Kind | Meaning (set in the bundled CLI) | Status |
| --- | --- | --- |
| `interrupted` | the turn was interrupted while the tool ran |
`cancelled` |
| `cancelled` | the call's own run was stopped (aborted, or moved to the
background) | `cancelled` |
| `user-rejected`, `permission-rule`, `automode-*` | the tool was
refused | `failed`, as before |
v1 and AIR clients are unchanged: `failed`, with the kind in
`_meta.claudeCode.nonExecutionKind`. v2 clients still get that `_meta`
too.
## How
- **The renderer decides the status**
(`AcpToolCallRenderer.resultStatus`), from `ClientCapabilities.v2`.
That's the same pattern the agent uses for v2 diffs: the value travels
as a v1 status (`V2_CANCELLED`, with a cast, since v1's `ToolCallStatus`
is closed), and the v2 surface passes it through. The v2 translation
still doesn't read `_meta`. The rule covers the regular result and the
two fallback reports in `toAcpNotifications`: an untracked tool call,
and a TodoWrite/Task tool call that only a permission request surfaced.
- **The agent's own "finished" checks** now use `isFinalToolCallStatus`,
which counts `cancelled`. Two places compared against
`completed`/`failed`: one removes a finished call from the turn's
foreground work, the other drops its eager-session and dispatched-call
entries. Without this, a cancelled call would have leaked those entries.
## Not covered
Replay doesn't have `tool_result_meta`, so a replayed interrupted call
stays `failed`, just as v1 replay has no `nonExecutionKind`.
## Checks
- `npm run check`, `npm run build` and all 2493 tests pass. No golden
file changes: no recorded scenario has an interrupted tool.
- New tests:
- **renderer:** each kind, for v2, v1 and AIR clients, plus the
permission-surfaced fallback;
- **agent:** a cancelled call is released from the dispatched-call
bookkeeping;
- **v2 surface:** a v2 client receives `cancelled`.
- I removed each part of the change in turn, and each removal fails a
test: never cancelled, cancelled for v1 too, refusals cancelled too, the
bookkeeping ignoring `cancelled`, and the fallback branches keeping
`failed`.