Ben Brandt 6b69852cfc refactor: report each prompt turn through internal turn events (#1222)
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`.
2026-10-02 15:01:27 +02:00
2026-07-21 18:53:56 +02:00
2025-09-02 19:06:32 -03:00
2026-03-26 12:19:06 +01:00

ACP adapter for the Claude Agent SDK

npm

Use Claude Agent SDK from ACP-compatible clients!

This tool implements an ACP agent by using the official Claude Agent SDK, supporting:

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).

Description
Pull mirror of agentclientprotocol/claude-agent-acp (Apache-2.0): the ACP bridge that lets Buzz drive Claude Code
Readme 107 MiB
Languages
TypeScript 99.2%
JavaScript 0.6%
Shell 0.2%