First part of step 3 of opt-in ACP v2 support (step 1: #1217, step 2: #1219). It's independent of #1219. It changes no ACP v1 behavior, apart from one failure-only case noted below. ## Why ACP v2 changes the prompt lifecycle. `session/prompt` is answered when the prompt is *inserted* into the conversation, and progress and the stop reason arrive as `state_update`s. In v1, the response comes at the end of the turn. The agent only exposed the v1 shape: `prompt()` returns at the end of the turn. So v2 can't be mapped from the outside. Following the discussion on #1219, the adapter gets its own view of a turn, and each ACP version maps to it, instead of v2 being translated into v1. ## What `src/turn-events.ts` defines the adapter's own turn lifecycle: - `inserted(messageId)`: Claude Code took the prompt in as this user message. At most once, and never after the turn ends. - `awaitingUser()` / `resumed()`: a permission request or question is open in the session / none is open any more. - `ended(outcome)`: with a `TurnOutcome` (`stopReason`, `usage`, `_meta`). - `failed(error)`. The agent calls these synchronously while it updates its own state, so handlers must not throw or call back into the agent. In the agent: - `startTurn(params, events)` starts a turn. It resolves once the prompt is queued for Claude Code, and rejects only if the prompt is refused before that. Events can arrive before it resolves. `prompt()` is now the v1 view of it: it awaits the turn's outcome. - **inserted** is reported in `activateTurn`, the single place a queued turn becomes active. That's when Claude Code takes the prompt in: its echo, or for an echo-less command, its result. Two guards: - a clear-context restart re-activates the same turn in the fresh session, so it's reported once; - a turn that a cancel settled during the previous turn's hand-off isn't reported at all. - **awaiting user / resumed** follow one session-level rule: the active turn awaits the user exactly while any request in the session is open, rechecked when a turn activates. A request carries no prompt id, and Claude Code can ask for a queued prompt before the consumer reaches its echo. So tying a request to the turn that was active when it opened would misattribute it. - **ended / failed** come from the existing settle points, each guarded by `settled`, so a turn reports exactly one of them. - `TurnOutcome` and `StopReason` are the adapter's own types. A turn's outcome is field for field a v1 prompt response, so `PromptResponse` now only appears at the v1 boundary. **The one v1 difference:** a prompt that sets a goal publishes it to AIR clients after the prompt is queued. If that notification fails, `startTurn` now logs it rather than rejecting, because the turn is already running. For v1 this only matters on a dead connection: `prompt()` waits for the outcome instead of rejecting with the write error. ## Notes for the v2 step - A prompt sent while another turn is running is inserted only when Claude Code takes it in, after the running turn. The open question about queued prompts is going back to the v2 draft. - An echo-less command (such as `/compact`) is inserted at its result, after its output. On `msg_lifecycle_v1` CLIs, the `command_lifecycle` "started" frame may be an earlier insertion point. - A queued turn that a cancel ends before insertion may still be run by Claude Code (the orphan accounting). - Any open request counts towards `requires_action`, a background task's included; `agentID` could refine that in the permission step. ## Testing `src/tests/turn-events.test.ts` covers: - a normal turn: inserted under the uuid of the message Claude Code received, then ended; - a permission request: awaiting user, then resumed; - a request open from before the turn: the turn awaits the user from insertion, through its own answered request, until the earlier one is answered too; - a cancel during the hand-off from the previous turn: the queued turn ends as cancelled and is never inserted; - cancelling while a second prompt is queued: the active turn is inserted, then ended as cancelled, and the queued turn ends as cancelled without being inserted; - the stream ending with a turn queued: it fails without being inserted; - a prompt for an unknown session: `startTurn` rejects and reports nothing. The existing clear-context restart test also checks that the restarted turn is inserted once. Each guard is checked by mutation: removing the `insertedReported` guard, the `settled` guard, the activation recheck, or the change-only dedupe; tying `resumed` to the request's own turn; or reporting insertion at enqueue each fails at least one test. The existing suite passes unchanged, including the 474 scenario tests that compare outbound v1 traffic with recorded baselines. `npm run build`, `npm run check`, `npm run test:run`.
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
- 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
Subagents are exposed only after bilateral capability negotiation. Until the released ACP SDKs
preserve the draft clientCapabilities.subagents field, a supporting client may advertise
nativeSubagentSessions in _meta.jetbrains.air.capabilities; the adapter mirrors the capability
in its initialize response. The canonical field remains supported and takes precedence once it is
available. Without either client signal, Agent/Task lifecycle keeps its legacy ordinary ACP
tool-call representation and child interactions stay on the root session. Clients that use the
historical _meta["subagent-transcript"] capability or forwardSubagentText session option retain
the flattened child transcript behavior. See AIR extensions.
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).