mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
6a33dedcbb90e7bf9db91207f0edbf19ca6d705e
37 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9b3a5c891c |
feat: end a turn at its result for AIR clients with backgroundSubagents (#1262)
## What This PR adds the AIR client capability `backgroundSubagents`. A client declares it in `initialize.clientCapabilities._meta.jetbrains.air.capabilities`. With the capability, an ACP v1 prompt turn ends at its result. This is also true while background subagents of the turn still run, including a subagent that SendMessage resumed. Subagent updates, permission requests and elicitations then go out after the prompt response. Their routing does not change. The agent advertises the capability only to ACP v1 AIR clients. ## Why The hold of #864/#866 keeps `session/prompt` open until the subagents finish. The Claude CLI ends the model turn at `result`, and only `idle` waits for the background agents. In AIR the hold hides the end of the answer, and the next user message steers into an old turn. One SendMessage-resumed subagent kept a turn open for about two hours. ## Unchanged - A client without the capability keeps the hold exactly. So #866 stays fixed for clients that stop listening at the prompt response. - ACP v2 keeps the hold too. ## Related changes - Out of a turn, the ExitPlanMode request of such a client does not offer the clear-context option. The fresh query must continue an ACP turn, and there is none. - An ExitPlanMode interruption of an autonomous cycle with no held turn now clears its markers. ## Docs `docs/air-extensions.md` has a new "Background subagents" section. ## Tests New capability cases: - a fresh subagent; - a SendMessage-resumed subagent; - a permission request after the prompt response, with the `parentToolUseId` attribution; - an autonomous followup that does not end the queued next prompt; - ExitPlanMode out of a turn, with and without a reset; - the exclusion of a running subagent from the incomplete-tool check. The existing tests cover the path without the capability. When the gate is disabled, 7 of the 8 new cases fail. The eighth is the in-turn control case and passes in both configurations. `npm run build`, `npm run check` and `npm run test:run` pass. The exception is 4 `providers.test.ts` cases. They depend on the environment (a local proxy `baseUrl`) and fail on `main` too. |
||
|
|
e9f986b8a9 |
fix: IJAI-3744 keep a session running while its background tasks are open (#1399)
## Summary A Claude Code session can start a background shell command or a background sub-agent, and then end its turn. When a task ends, Claude Code wakes the session for a short turn. The session index reported `idle` at each turn end, so a client saw one completion for each task. Now the index finds the open background tasks in the transcript. A turn end with an open task stays `running`, also after 10 minutes. The session gets one `idle`, when the last task ends. The rules are the same as in the native Claude provider of AIR (IJAI-3744). ## Rules - A task opens with a tool result that has `backgroundTaskId`, or `isAsync` with an `agentId`. - A task closes with its `<task-notification>` (a user record or a queue `remove`), or with a `TaskStop` result. - The wait ends when no live process holds the session, or when the registry status is `idle`. - A task can start before the tail window. A later scan applies the tail on top of the earlier task set. When it cannot, the index reads the whole transcript, but only for a finished turn of a live session. ## Not in this PR - A background task that a sub-agent starts. Its start record is only in the transcript of the sub-agent. - A session that this connection runs. Its state comes from the SDK. ## Tests - `background-tasks.test.ts`: the record orders of the native provider tests, in a small transcript and in one longer than the tail window. The tests also cover a task before the tail window, the end of the wait, and the count of whole-file reads. - A real check with Claude Code 2.1.306 in a terminal. With two background shells or two background sub-agents, `main` gave three `idle` and this change gave one. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a1d0eeeaad |
fix: publish turns that Claude Code starts after end_turn as an async task in v1 (#1390)
## Motivation
Claude Code can start a turn of its own after a prompt has ended, for
example to answer the notification of a finished background task. ACP v1
has no session state, and the client already got `end_turn` for its last
prompt. The output of that turn just keeps arriving as session updates,
and the client has no signal that the agent is working again, or when it
stops.
## Change
The adapter now publishes such a turn as an async task (AIR async tasks
extension) with the id `autonomous-turn-<uuid>`, the name `Claude is
working`, `taskType: "turn"` and `showInTranscript: false`. Its
description is the summary of the last task notification, when there is
one.
- **Start:** the SDK moves to `running` while no prompt is active or
queued. From CLI 2.1.270 the state stays `running` while background
agents run, so the task also starts at the first root output with no
prompt open.
- **End:** the task ends `completed` at the turn's result or at `idle`,
and `failed` at an error result. A coalesced placeholder result
(`num_turns: 0`) does not end it.
- **Folded prompt:** a prompt that the turn takes in ends the task,
because that prompt owns the turn now.
- **Stop:** `_session/async_task/stop` on this task interrupts the turn,
since the SDK has no task to stop. It returns `{ "stopped": false }`
while a prompt waits for its turn, because the interrupt would drop that
prompt.
- **Lifecycle:** the SDK task list does not end the task, because the
SDK does not know about it. Shutdown fails it like other open tasks.
Clients without the async tasks capability get nothing new. The behavior
is documented in `docs/air-extensions.md` under "Autonomous turn".
## Tests
`acp-agent.test.ts` covers the start at `running` and at the first
output, the end at the result, the folded prompt, stop as an interrupt,
and a client without async tasks. `async-tasks.test.ts` covers the SDK
task list reconciliation and shutdown.
|
||
|
|
1420994ced |
feat: send one end event after the session/load history replay for AIR (#1361)
## What
A client that declares the AIR capability `loadReplayEnd` gets one extra
`session/update` after the history replay of `session/load`:
```json
{
"sessionId": "<loaded session>",
"update": {
"sessionUpdate": "session_info_update",
"_meta": { "jetbrains": { "air": { "version": 1, "loadReplayEnd": true } } }
}
}
```
The client declares the capability in
`clientCapabilities._meta.jetbrains.air.capabilities`, as it declares
the other AIR extensions. The agent does not advertise it.
## Why
ACP has no signal that the history replay of `session/load` is over,
other than the response. This adapter replays the transcript while the
Claude Code CLI starts, and the CLI start takes most of the load time.
So the history is whole long before the response. With this event, the
client can show the history at once.
The protocol side is the RFD
https://github.com/agentclientprotocol/agent-client-protocol/pull/2407.
## Design change
The first version of this PR marked each replayed notification with an
index (`loadSessionReplayIndex`) and the last one with
`loadSessionEndOfRecordedHistory`. To know the last one, it held the
latest update and queued every other outbound message of the session
behind it. That design is replaced with a capability-gated single end
event. The replay sends as before. Only one event is added at its end.
There are no indexes, no held update and no queue.
## When the event is sent
- After every replayed update, the subagent history that the parent
replay sends to the child sessions included, and before the
`session/load` response.
- Also for an empty history.
- For `session/load` of a session that starts while the replay runs, for
`session/load` of a session that is already live, and for the ACP v2
`session/resume` with `replayFrom: {type: "start"}`.
It is not sent:
- When the replay stops (the CLI start failed during the replay) or
fails.
- For a live update.
- To a client without the capability.
The update has no `title` and no `updatedAt`. A `null` there would clear
the value. The load can still fail after the event, for example when the
CLI start fails after the replay. The event means that the recorded
history arrived whole.
## Client status
JetBrains Air shows the history before the load answer only after the
Kotlin SDK delivers the updates of a loading session before the answer.
An SDK PR for this is in progress.
## Tests
- `create-session-options.test.ts`: the event comes once after the
replay and before the response; before the CLI start finishes; for an
empty history; for an already live session; not when the CLI start fails
part way; not when the replay fails; not without the capability. The
event has no `title` and no `updatedAt` key.
- `acp-agent.test.ts`: the subagent history from its own transcript file
comes before the event.
- `acp-v2.test.ts`: v2 `session/resume` with a replay ends with the
event before the response; a resume without a replay sends none.
- `docs/air-extensions.md`: a new "Load replay end" section, and the
capability and key rows.
|
||
|
|
872e93f11f |
feat: add the MCP server and tool to AIR tool calls (#1359)
AIR gets `_meta.jetbrains.air.mcp = {server, tool}` on MCP tool calls,
so the MCP card does not depend on the title format.
The adapter splits the `mcp__<server>__<tool>` tool name the way Claude
Code does. `server` is the normalized name. Clients other than AIR get
nothing new.
Split from IJAI-872. The SDK does not expose MCP progress yet, so this
PR does not forward it.
|
||
|
|
78a29f58ba |
feat: publish the predicted follow-up prompt as _meta.promptSuggestions (#1356)
## What
The Claude Code CLI can predict the prompt the user is likely to send
next. This adapter asked for nothing and dropped the `prompt_suggestion`
message. It now asks for the prediction (`promptSuggestions: true`) and
forwards the one prediction of a turn.
ACP has no field for a composer prefill, so the value travels as
```json
{
"sessionUpdate": "session_info_update",
"_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } }
}
```
The key names no vendor, so another client can read it. AIR already
renders the first non-blank text as gray text in an empty composer,
where Tab fills the composer and sends nothing.
## Behavior
- `suggestions: []` clears the prediction the client holds: at the next
`session/prompt`, because that turn's prediction is spent, and on a
`conversation_reset`, because the transcript it was drawn from is gone.
- A prediction the next prompt outran — the user sent the next prompt
before the adapter read the prediction off the SDK stream — is dropped
instead of published mid-turn.
- A blank prediction never goes on the wire.
- The update is sent only to an AIR client, like the other extension
keys, so non-AIR traffic is unchanged (the key is added to
`AIR_ONLY_META_KEYS`; the `origin/main` scenario baselines stay
byte-identical).
- The CLI predicts nothing on the first turn, after an API error, in
plan mode, and at the usage limit, so a turn without a prediction is
normal. A caller can opt out with
`_meta.claudeCode.options.promptSuggestions: false`.
Documented in `docs/air-extensions.md`.
## Tests
`src/tests/prompt-suggestions.test.ts` covers the wire shape, the
publish after the turn's result, the clear at the next prompt, the
dropped stale prediction, the blank prediction, and a non-AIR client.
`src/tests/create-session-options.test.ts` covers the SDK option and the
caller opt-out.
`npm run check`, `npm run build` and the touched suites are green; the
full `test:run` has pre-existing flakiness on this machine
(session-index watchers, providers) unrelated to this change.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
|
||
|
|
1a1f1a0e80 |
feat: mark session list rows of worktrees Claude Code made (agentWorktree) (#1352)
## What Marks `session/list` and `_session/list/changes` rows whose session runs in a linked Git worktree that Claude Code itself created (`claude --worktree`, the `EnterWorktree` tool), as `_meta.jetbrains.air.agentWorktree: true`. A new agent capability `sessionWorktrees`, advertised together with `sessionIndex`, tells the client that rows carry the field, so a missing field means "not made by the agent" rather than "unknown". ## Why With `includeWorktrees`, a client gets sessions from every linked worktree with nothing to tell them apart. A worktree Claude Code made for a session belongs to the project the user started it from; a worktree the user made by hand is a separate checkout. Only the agent knows which is which. ## How - The rule reads the transcript's `worktree-state` records: the latest one wins; the session counts only when `worktreeSession` is non-null and `enteredExisting` is not `true`. `environment.isWorktree` is not used, since plain git detection is also true for user-made worktrees. - The record is parsed in the existing head/tail transcript scan, so no extra file reads. The row is marked only when its `cwd` is in a linked worktree (`<common-dir>/worktrees/<name>`), never for the main checkout or a submodule. - The row's `cwd` stays the session's own directory. The field is `true` or absent. Proposed as `SessionInfo.agentWorktree` plus a `agentWorktrees` capability marker in the session list extensions RFD: agentclientprotocol/agent-client-protocol#2323. Docs: `docs/air-extensions.md`. ## Tests `src/tests/session-index.test.ts`: marked worktree, `enteredExisting`, no record (user worktree), a later `null` record, main checkout, `_session/list/changes` rows, capability negotiation, and scan windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fadbe1881c |
fix: let session/delete succeed for a session without a transcript (#1351)
Found by running the ACP TCK's v2 suite against the adapter
(ACP-DELETE-201 and ACP-DELETE-203). The code is shared, so v1 clients
get the fix too.
## The bug
ACP says: "Deleting an already-deleted session, or a session that never
existed, **SHOULD** succeed silently." For a client that is neither AIR
nor a `sessionIndex` client, `session/delete` instead failed with
`-32603` "Internal error" in three cases:
- **A session this connection ran but never prompted.** Claude Code
writes the transcript at the first prompt. So the adapter closed the
live session, then the SDK delete threw "not found in any project
directory".
- **An unknown session.** The SDK delete throws the same "not found"
error.
- **An id that isn't a UUID.** The SDK delete throws "Invalid
sessionId".
## The fix
A new `deleteSessionIfStored` in `src/session-index/service.ts` handles
every other client's delete:
- An id that isn't a session id can't name a transcript, so it returns
`{}` without calling the SDK.
- The SDK's own "not found" means nothing is on disk, so that also
returns `{}`. It's recognised by the existing `isSdkNotFound`.
- Any other SDK failure, such as a permission error, still fails the
request.
The two AIR paths are unchanged, because `docs/air-extensions.md`
documents their errors: `-32002` for an unknown session on a
`sessionIndex` client, and the SDK's error for AIR without
`sessionIndex`. The doc now describes the plain client's behavior.
## Checks
- **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against
this build: ACP-DELETE-201 and ACP-DELETE-203 now pass, where both
failed before. ACP-DELETE-202 is still skipped, because `session/list`
doesn't show a session that was never prompted.
- **Tests:** three new tests: a never-prompted live session, an unknown
session, and an id that isn't a UUID. Removing either rule, or
swallowing every error, fails a test. The existing delete tests now use
UUID ids, which is what real sessions have.
- **Suite:** `npm run check`, build and all 3068 tests pass, with no
golden changes.
|
||
|
|
e8eb9d340f |
feat: narrow models to what the providers/set gateway lists (#1340)
### Summary Addresses [IJAI-1783](https://youtrack.jetbrains.com/issue/IJAI-1783). An organization can restrict the models its JetBrains gateway serves, but a session routed through `providers/set` offered the whole Claude Code catalogue, so the picker showed models the gateway then refused. `providers/set` now lists the gateway's models before it responds, and the model picker offers only the SDK models the gateway lists. - `anthropic`: `GET {baseUrl}/v1/models?limit=1000`, paginated by `has_more`/`last_id`, with the provider headers and `anthropic-version`. - `bedrock`: `GET {baseUrl}/foundation-models` (`ListFoundationModels`, `modelSummaries[].modelId`), the listing the JetBrains gateway answers on its Bedrock route. Only `anthropic.` ids count. The cross-region prefix `global` when listed, else that of the first listed id, becomes the session's `ANTHROPIC_BEDROCK_REGION_PREFIX`, so the CLI calls the models under the prefix the gateway serves; ids of other prefixes, and SDK rows resolved to another prefix, are dropped. - Rows match by `resolvedModel` (or `value`) without dates, `-vN:M` suffixes, `[1m]` hints and provider prefixes; the narrowing runs after the `availableModels` allowlist. When the gateway does not list the model a session runs (the dropped default, or a resumed or recreated session's last model), the session switches to the first listed one with `setModel`, so the CLI does not keep running an unlisted model. - The listing runs in the provider-update queue, so `providers/set` and `providers/disable` apply in the order they arrive; logout also clears a `providers/set` that is still listing. - A 4xx answer means the gateway has no listing: `providers/set` succeeds and nothing is narrowed. Any other failed listing (network error, 5xx, unexpected body, 10 s timeout) fails `providers/set` with `internal_error`, and the previous provider stays in effect. An invalid header or a base URL with credentials fails without repeating the value. A listing that matches no SDK model leaves the session with no model to offer: there is no model config option. - `vertex`, gateways set through `authenticate`, and native routing are not listed. Setting the same gateway again reuses its listing; `providers/disable` and logout drop it. - `docs/model-configuration.md` describes the narrowing. ### Not covered Background haiku requests, subagents, and a `/model` typed into the prompt choose their own model and are not checked against the listing. ### Verification ```bash npm run check npm run build npm run test:run -- --no-file-parallelism # 2964 passed, 5 failed, 30 skipped; all 5 in session-index.test.ts, flaky on main too ``` Tests stub `fetch` (`src/tests/setup-no-network.ts` answers an empty listing by default) and cover the listing requests and pagination, the Bedrock region prefix and its env, region-aware matching, the dropped default, the failing `providers/set`, and the empty picker. The JetBrains Bedrock gateway's `/foundation-models` answers `global.anthropic.claude-{haiku,opus,sonnet}-5-5` (seen through Junie, which lists the same route); this adapter has not yet been run end to end against it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
764f339c3e |
fix: end the terminal of a command whose tool call is closed without a result (#1335)
## Problem A tool call that ends without a tool result is closed by a report that only sets its status (`AcpToolCallRenderer.unfinished`). That happens after `session/cancel`, for a subagent's command (Claude Code sends no result for it) or a call that Claude abandoned, and when a turn ends with a call that has no result (`closeUnfinished` in `settleActive`). A client that renders terminals (the `terminal_output` / `terminal_output_delta` extension, and the display terminals of ACP v2) holds a terminal for a Bash command, which its `tool_call` started with `_meta.terminal_info`. The closing report did not end it, so the call shows as ended next to a terminal that never exits. Example, live (Claude Code 2.1.296, Sonnet): a foreground Agent call whose subagent runs `python3 -c "import time; time.sleep(45)"`, and `session/cancel` while it runs. Claude Code answers the Agent call and sends nothing for the subagent's command, so the adapter closes that call itself: - v1 client with `_meta.terminal_output`: `tool_call_update` `failed`, with no `terminal_exit`. - v2 client: `terminal_update` with the command, then `tool_call_update` `cancelled`, and no `terminal_update` that carries an exit. ## Change - `closeToolCall` is the one function that every closing report goes through (the cancelled-turn closing and the end-of-turn closing today, and any closing added later). It now also ends the call's terminal, in the same update, as the result of a command does. It reads the tool from the tool use cache before it drops the entry. - The closings that #1336 and #1339 add go through `closeToolCall` too, so they end the terminal as well once merged. - `AcpToolCallRenderer.terminalExit` decides whether the call has a terminal. It uses the rule that `toolCall` uses to start one, a command on a client that renders terminals, which is now one private method (`hasTerminal`) that `toolCall`, `toolUseContent` and `terminalExit` share. - The exit code is unknown, since the command ended without a result: `exit_code: null`, as `endBackgroundTerminal` sends for a background command. AIR always gets a number and reads 1 for a command that did not succeed (#1252), so it gets 1. A v2 client gets `terminal_update` with an empty `exitStatus`. - `docs/air-extensions.md`: one line in the terminal conventions. ## Evidence **Tests** (8 new): - `incomplete-tools.test.ts`: the cancel's closing of a subagent's command ends its terminal, still names the subagent, and sends `exit_code: null`; AIR gets `exit_code: 1`; a client without terminals gets no exit; a subagent's `Read` gets none; a command that ran but got no result by the end of the turn gets the exit; so does one that Claude abandoned before it ran. - `tools.test.ts`: `terminalExit` for Bash, PowerShell, v2, AIR, a client without terminals, and tools that are not commands. - `acp-v2.test.ts`: the stopped subagent's terminal exits in the child's session, after the command's terminal started and before the root's `idle cancelled`, and the call ends `cancelled`. **Removal checks.** Each of these fails at least one test: no exit in `closeToolCall` (5), AIR gets null (2), an exit for a client without terminals (2), an exit for a tool that is not a command (3), another terminal id (5), the exit replacing the parent meta (1), the tool read after the cache entry is dropped (5). **Live**, each update validated against the SDK's JSON schema (v1 `SessionNotification`, v2 `session/update`), 0 violations. The Agent call and the subagent's command from the example, cancelled 4 s after the command's permission request was answered: | client | `main` | this PR | | --- | --- | --- | | v1, `_meta.terminal_output` | closing report `failed`, no `terminal_exit` | the same update carries `terminal_exit {exit_code: null, signal: null}` | | v1, AIR with `terminal_output` | not run on `main` (same code path) | `terminal_exit` with `exit_code: 1` | | v2 | `cancelled`, no terminal update | `terminal_update` with `exitStatus: {}`, then `cancelled`, before the root's `idle cancelled` | `npm run check`, `npm run build` and `npm run test:run` (2932 tests) pass. No golden file changes. With #1336 and #1339 merged into it, in any of the six orders, the branch merges without conflicts and all 2948 tests pass. ## Not covered - A call that only a permission request announced, whose tool use never streamed before it was closed, has no entry in the tool use cache. Its tool is unknown here, so its terminal stays open. This is the lookup that `AsyncTaskRuntime` uses too (`toolUseCache`, then `resolvedToolNames`). Covering it needs the tool name where the permission request emits the call. - The fallback for a tool result without a tool use (`toAcpNotifications`) still reports the status only, with no exit. - The end-of-turn closing is covered by a test only: Claude Code does not end a turn with an unanswered call on demand. |
||
|
|
5758ac84a4 |
feat: report subagents as the subagents RFD defines, for clients that are not AIR (#1320)
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. |
||
|
|
d6c371704f |
fix: display active Central helpers as JetBrains Air Gateway (#1266)
### Summary Addresses [IJAI-2956](https://youtrack.jetbrains.com/issue/IJAI-2956). - Recognize Central only when the active key source is `apiKeyHelper` and its effective helper command contains the whole word `central` and the word sequence `proxy start`, case-insensitively, allowing multiple whitespace characters. - Read the effective command through the existing `SettingsManager`/SDK settings resolution, including project/managed overrides, rather than reimplementing configuration merging or executing helpers. - Return `kind: gateway` and the exact label `JetBrains Air Gateway`, without `detail` or invented account identity. URL, flags, routes, and endpoint ownership are intentionally not checked: this is an agreed weak display heuristic, not proof of provenance or entitlement. - Render ordinary helpers as `API key helper` instead of raw `apiKeyHelper`; do not expose unknown key-source identifiers. Preserve external backend and client-owned configuration priority, CLI/SDK merging, stale-probe protection, and duplicate suppression. - Add mapper and ACP-agent regression coverage for positive/negative commands, overrides, CLI/SDK results, and source changes; update `docs/air-extensions.md`. Examples matching the heuristic include `central proxy start` and `/opt/bin/CeNtRaL PrOxY StArT --arbitrary-flag`. `decentralized proxy start`, `central start`, and an inactive helper do not match. ### Sanitized auth status ```json { "kind": "gateway", "label": "JetBrains Air Gateway" } ``` No helper command, key, header, private route, or invented account information is sent. ### Verification Based on fresh upstream `main` at `0724b17`, rechecked immediately before publication. Changes and rebuilt `dist` are available in the main local clone; the user confirmed the local adapters work. ```bash npm run build npm run lint npm run test:run ``` - Build and lint passed. - Full test suite: **2484 passed, 30 existing skipped** across 44 passed test files and 4 skipped files. - No new skip flags or disabled tests were used. ### Scope No AIR UI, provisioning, personal configuration migration, effort/mode changes, or new ACP protocol. Plan files and personal configurations are not committed. --------- Co-authored-by: Junie <junie@jetbrains.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Andrey Bragin <andrey.bragin@jetbrains.com> |
||
|
|
383bc7dcf0 |
fix: start a forked session so the client can prompt it at once (#1317)
## Problem The v1 schema says a forked session is active once the agent responds, and can be prompted without `session/load` or `session/resume` (agentclientprotocol/agent-client-protocol#2359). `session/fork` only copied the transcript, so prompting the new id failed with "Session not found" until the client loaded or resumed it. ## Change - **The fork starts the new session the way a resume does.** It starts from the copied transcript, with the model and the permission mode that transcript records, and with the request's `cwd`, MCP servers, additional directories and `_meta`. The response carries the session's `modes` and `configOptions`, and the available commands follow it, as for `session/new`. - **A client that loads or resumes after a fork keeps working.** A later `session/load` replays the history into the running session, and a later `session/resume` returns it. Neither starts a second Claude Code process. A load or resume with other settings restarts the session, as for any loaded session. The fingerprint that decides this ignores the AIR fork point in `_meta`. - **Cost:** each fork now starts a Claude Code process. That added about 0.65 s to the fork response in the live run. - **v2:** the v2 fork used to resume the forked session itself. It now uses the session that the v1 fork starts, so the v2 bridge drops its own resume. - **AIR docs:** `docs/air-extensions.md` now says, under the session fork point, what a load or resume after a fork does. ## Evidence **Live, CLI 2.1.293 on Haiku, over v1:** 1. A new session answered "Remember the codeword PELICAN" with `OK`. 2. `session/fork` returned the fork with mode `auto` and the `mode`, `model` and `effort` config options. 3. Prompting the fork right away returned `PELICAN`, with no load or resume in between. 4. A `session/load` of the fork then replayed its history and was ready in 4 ms. The timing log shows one session start for the fork and none for the load, and a further prompt still answered. **Tests:** - New: the fork can be prompted at once, and its response carries modes and config options; a later resume and load reuse the session; and the fork fails when its session can't start. - The existing fork tests now also check that the session starts from the fork, with the request's additional directories. - The v2 fork tests pass unchanged. - **Mutation checks:** removing the start, the response's `modes`, or the additional directories each fails a test. - `npm run check`, build and all 2813 tests pass, with no golden changes. |
||
|
|
fe8b69edd4 |
feat: accept IDE session hooks without dropping model settings (#1265)
IDEs can pass their own Claude hooks through `_meta.claudeCode.options.settings.hooks` on `session/new` and `session/load`. Before this change, any session settings replaced `CLAUDE_MODEL_CONFIG` entirely, so adding a single hook dropped the deployment's `modelOverrides` / `availableModels`. The two are now merged, and the session settings win per key. The wrapper does not merge hooks itself: Claude Code already runs the hooks of every settings tier, so user and project hooks keep running next to the session ones. Hooks stay scoped to their session, and a `session/load` with different settings recreates the query, which replaces or removes them. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
03d84f6663 |
feat: add the AIR sessionIndex extension for fast session lists (#1268)
## Summary
Adds the opt-in AIR `sessionIndex` extension. With it, an AIR client can
show the latest sessions of a project quickly, with their state, and can
rename, archive and unarchive them over ACP. The full contract is in
`docs/air-extensions.md` ("Session index").
The extension follows ACP RFDs. The transport differs: it uses
`_meta.jetbrains.air` request options and row fields, `_session/*`
methods, and AIR capabilities that are not advertised under ACP v2. The
`archived` list filter also differs (see below).
- **Session list extensions**
(agentclientprotocol/agent-client-protocol#2323):
- `limit`, ordered by last user activity (`lastPromptAt ?? updatedAt`),
and `includeWorktrees`.
- Row fields `lastPromptAt`, `model` and `forkedFrom`.
- `state`: `running`, `idle`, `requires_action`, and `error`, which the
RFD lacks.
- `lastTurnEndedAt`.
- `cost`: the SDK's `total_cost_usd` for a session that runs on this
connection only. No cost is read from transcripts.
- Live updates come from an AIR list subscription (below).
- **Session archive** (agentclientprotocol/agent-client-protocol#2161):
- `archived` is on every row, `session_info_update` is sent on a change,
and an unknown session is `-32002`.
- The `archived` list filter has three values: `"unarchived"` (the
default), `"archived"` (archived sessions only) and `"all"` (both, in
one order). #2161 has a boolean here. Its `false` is `"unarchived"`, its
`true` is `"all"`, and `"archived"` has no counterpart.
- **Capabilities**: an AIR client that declares `sessionIndex` gets
these in `_meta.jetbrains.air.capabilities`. The client declares only
`sessionIndex`.
- `sessionIndex`.
- `sessionArchive`: `_session/archive` and `_session/unarchive`. The RFD
capability is `archive` (#2161).
- `sessionRename`: `_session/rename`. The RFD has capability `setTitle`
and method `session/set_title`
(agentclientprotocol/agent-client-protocol#1987).
- `sessionListSubscribe`: `_session/list/subscribe`,
`_session/list/unsubscribe` and `_session/list/changes`. No RFD
counterpart.
### Clients without `sessionIndex`
Clients that don't declare `sessionIndex`, which means non-AIR clients
and released AIR builds, keep the current behaviour. They get the same
SDK `listSessions` path, page size, cursor and initialize capabilities,
with no watchers and no notifications. There are two deliberate
deviations, both strictly safer:
1. **`session/delete` from an AIR client without `sessionIndex`.**
Released AIR builds use delete as "Done". Instead of deleting the
transcript (data loss), the adapter archives the session in AIR's own
Claude format (below). Archived sessions are hidden from that client's
list, so "Done" looks the same and nothing is lost. An unknown session
keeps the SDK's error. Other clients get the SDK delete, as before.
2. **`session/close`** of a session that is not loaded here returns `{}`
instead of throwing.
### Sessions open in another process
`session/load`, `session/resume` and `session/prompt` work on a session
that another live Claude Code process has open, for every client, as the
Claude CLI does. Rename, archive, unarchive and delete also go ahead,
writing records or deleting transcripts as the CLI's `/rename` and
delete do. The other process keeps running.
### Archive format and rule
The archive state uses AIR's native Claude archive format
(`ClaudeStoreThreadBackend.appendTitleRecords`, ADR 0140), so AIR's
native path and the adapter read and write the same state.
- **Archive** appends `{"type":"custom-title","customTitle":"[archived]
<title>",…}` and `{"type":"agent-name","agentName":"[archived]
<title>",…}` to every transcript copy. **Unarchive** appends the same
pair without the prefix. Titles are cut to the CLI's 200 characters.
- **Archived is decided by the agent name first**, as in AIR's native
Claude provider. A session's name is its last top-level `agentName`,
else the custom title the SDK reports. The session is archived exactly
when that name starts with `[archived] `. The listed title comes from
the same source, shown without the prefix.
- Title records are metadata: `updatedAt` and the list order do not
move.
### Rename, archive and delete
- **Rename** writes the same title pair. An archived session keeps its
prefix. A session running here is renamed through its CLI's
`rename_session` control request, serialised with auto-titling. Other
sessions get the records plus the `custom-title.json` sidecar the CLI
writes. No generated title replaces a client title, but a title stored
later by someone else (a `/rename`) is still adopted.
- **Archiving stops the running session.** `_session/archive` of a
session loaded on this connection does four things:
1. It interrupts the running turn.
2. It retitles the session through its CLI. The adapter also appends the
matching `agent-name` record to the CLI's transcript, because the CLI
writes that record only later.
3. It closes the session as `session/close` does.
4. It archives the transcripts once more if the closed CLI left them
unarchived.
The client then gets `session_info_update` with `archived: true`.
Unarchive does not reopen the session. A session that is not loaded here
is archived by writing the title records.
- **Delete** for a `sessionIndex` client deletes every copy of the
session and its `<sessionId>/` directories. A session running here is
closed first.
### Implementation (`src/session-index/`)
- **Index**:
- Reads the project dirs with `readdir` + `stat`, and keeps a 2000-entry
metadata LRU keyed by (path, mtime, size).
- Cache misses are read in parallel through `getSessionInfo`, plus a
growing tail read (64 KB → 4 MB) for the last prompt, last message, turn
end and agent name.
- SDK semantics are kept: sidecar titles, `continued-in`, long-path
hashes, darwin case, and first-prompt extraction (ported, and
fuzz-compared with the SDK).
- The `cwd` scope follows the SDK `listSessions`. Worktrees expand to
the same relative subdirectory, and a session copied to several project
directories is listed from its newest copy.
- The keyset cursor is bound to `cwd`, `archived` and
`includeWorktrees`. Session ids match exactly.
- **State**:
- For sessions of this process, the SDK state.
- For other sessions, the Claude Code live registry
(`~/.claude/sessions/<pid>.json`, with pid + procStart liveness and
Claude Desktop's rules) or the transcript tail. The registry is read
only for the row state.
- `error` means the session is not running and its last turn ended with
an error, not a cancel.
Documented in `docs/air-extensions.md`, with an extension → RFD mapping
table and the remaining differences.
### Measured on a real `~/.claude` (233 transcripts, read-only)
| Case | New path | Old SDK path |
|---|---|---|
| cwd, limit 50, cold process | ~80 ms | 78–156 ms (all rows) |
| cwd, limit 50, warm | 1.3–4 ms | — |
## Session list subscription
Replaces polling the first page. It is only for a client that declared
`sessionIndex`; the agent advertises `sessionListSubscribe` with it,
never under ACP v2.
- **Subscribe**: `_session/list/subscribe { cwd }` returns `{
subscriptionId }`. `cwd` must be absolute, else `-32602`; other params
are ignored.
- The scope is `cwd` and the same subdirectory in every linked worktree,
in any archive state, without sidechains.
- **Changes**: `_session/list/changes { subscriptionId, sessions,
removed }`.
- `sessions` holds the full `session/list` row of each session that
appeared, or whose title, `lastPromptAt`, `state`, `lastTurnEndedAt`,
`cost`, `model`, `forkedFrom` or `archived` changed since the last row
sent.
- The first change of a session after subscribe is sent in full. After
that, a change of `updatedAt` alone sends nothing.
- `removed` lists sessions that were deleted or moved out of scope.
- Archive and unarchive are row changes.
- A session is sent at most once a second, with one notification per
subscription per pass.
- **Unsubscribe**: `_session/list/unsubscribe { subscriptionId }`
returns `{}` and is idempotent.
- A subscription lives until unsubscribe or until the connection closes.
- A connection has at most 128 subscriptions. The 129th is `-32602` with
`data.reason: "too_many_subscriptions"`.
- Subscriptions of the same `cwd` share one watch and are notified
independently.
- **Delivery** is best effort, with no resync and no sequence numbers. A
rescan every 10 s (and 1 s after a project directory watch opens) sends
whatever differs.
- **Implementation** (`list-subscriptions.ts`, `session-info.ts`):
- A non-recursive `fs.watch` on the scope's project dirs, the registry
and the projects root.
- A transcript event re-reads only that transcript, and a registry event
reads only that `<pid>.json`. Sessions running here push their SDK state
at once.
- Subscribe reads no session row. The rescan reads only transcripts
whose stat changed or whose read failed.
### Measured: one subscription on the 5,200-session project (Docker
`node:22`, 2 CPU, 2 GB, shared host)
300 s of churn: 20 sessions appending 10/s each (updatedAt-only),
registry status flips every 200 ms, and a session created every 2 s and
deleted 10 s later.
| | Old `list_changed` watcher | Subscription |
|---|---|---|
| Agent CPU p50 / max | 19% / — | 12.9% / 49% |
| Event loop p99 (p50 / max of 1 s windows) | — | 16 / 31 ms; worst
single delay 87 ms |
| Change latency p50 / p95 | 950 ms (hint) | create 291 / 570 ms, delete
279 / 506 ms; registry state 902 / 990 ms* |
| Notifications / min | — | 90 (896 rows, 0 rows for updatedAt-only
appends) |
| RSS | flat ~160 MB | 159 → 198 MB (heap flat ~50 MB) |
\* The registry flips the same 5 sessions every 200 ms, so each is held
to the once-a-second limit. Latency is measured from the first unsent
change.
These numbers were measured with an earlier subscribe that read every
row up front. Subscribe now reads no rows, so that one-off cost no
longer occurs. The lazy subscription has not been re-measured yet.
## Load test: 10,000 sessions (Docker, 2 CPU, 2 GB)
Calibrated data: 10k transcripts, 15 GB, 36k subagent files, and one
5,200-session project. Sizes match a real profile (p50 311K / p90 2.67M
/ p99 20.5M).
| Scenario (limit 50) | Before | After |
|---|---|---|
| Big project, first page cold / warm p50 | out of memory | 225 / 19 ms
|
| No cwd, cold / warm p50 | 1,722 / 178 ms | 209 / 45 ms |
| Page through 4,637 rows | 457 s | 11.4 s cold, 2.9 s warm |
| Delete p50 / 100 concurrent | 23.8 ms / 4.4 s | 7.8 ms / 0.8 s |
| 300 s of churn (60k changes) | out of memory after 3 s | list p50 54
ms, 0 stalls >50 ms, RSS flat ~160 MB |
## Streaming: keep the event loop responsive under a backlog
Under a backlog of buffered SDK messages, the adapter could freeze its
event loop for seconds. Every `query.next()` on a non-empty SDK queue
resolves in a microtask, so `runConsumer` forwarded the whole backlog
without reaching a macrotask boundary. Incoming JSON-RPC, including
`session/cancel`, waited until the backlog drained.
1. **Consumer yields to the event loop.** A shared `EventLoopYielder`
(`src/utils.ts`) gives the event loop a turn via `setImmediate` once the
current iteration has run for 8 ms. Order, content and cancel semantics
are unchanged.
2. **No per-message copy in `nodeToWebWritable`** for byte streams.
Object-mode streams keep the copy.
3. **Diff memo.** The approval preview and the PostToolUse patch no
longer diff the same texts twice. Timed-out diffs are not remembered.
4. **Prefix guards** in `parseMcpCommand` and `sanitizeTitle`. Both give
identical output.
5. **Replay yields between history messages**, with the same budget.
With 10 sessions × 1 MB, the max event-loop delay went from 4205 ms to
1626 ms, and ping p95 from 2793 ms to 262 ms (interleaved A/B, Docker
`node:22`, 2 CPU). Single-stream throughput is unchanged. Remaining
stalls are inside the Claude SDK and GC.
## Test plan
- [x] Rebased on `origin/main`. `npm run check`, `npm run build` and
`npm run test:run` pass.
- [x] Regression tests for the unchanged non-`sessionIndex` paths.
- [x] Regression tests for every review finding.
- [x] Reviews: Codex (gpt-6-astra) and a Claude review subagent, run as
independent passes against `origin/main` until a round found nothing of
normal or higher severity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0e83a06c4a |
fix: async task routing, Monitor tasks, structured task ids, and abandoned tool calls (#1249)
It affects AIR clients with the `asyncTasks` capability.
## Problem
- A subagent can start a background Bash or Monitor and then end its
turn. The task ends after the subagent finished. The route of the task
dropped every update of a finished child generation, so the client never
got the terminal `async_task_state_update`. The task stayed running in
the client forever. One AIR log held 440 such dropped updates.
- The `Monitor` tool streams the stdout of a background command to the
model only. The SDK reports its task as `local_bash`, so the client
showed it as a background shell. In one real session, 18 of 18 Monitor
tasks had no output file, no progress and no result. The Stop button
only broke the agent's wait.
- The adapter read a task id from the text of a Bash tool result. A
foreground `sed`, `strings` or `python3` that printed adapter source,
test fixtures or CLI strings became a background task, for example with
the id `";\n const start = text` or `${e}`. The client got a spawn and a
`backgrounded` marker on a foreground call, and the task never ended.
- Claude Code 2.1.287 reports a background command in four texts, and
only one gave an output path. A subagent tool result almost never has
`tool_use_result` (0 of 48 in one session). So a subagent command that
hit its timeout got no output path (9 of 10).
## Fix
- `NativeSubagentRuntime.routeOfToolCall` still sends
`async_task_spawned`, `async_task_progress` and
`async_task_state_update` to the child generation that owned the tool
call after that child finished. It never sends them to a later
generation. Every other late update is dropped as before. The spawn is
in the list because a held task can get its tool call id after the
subagent finished.
- `AsyncTaskRuntime` takes a `toolNameOf` lookup. A task of a `Monitor`
tool call is ignored, like a subagent task: no spawn, no progress, no
state, and no stop. A held task that gets its tool call id later goes
through the same check. A `local_monitor` task is ignored too. The
`Monitor` tool call stays in the transcript.
- A task comes only from structured data:
`tool_use_result.backgroundTaskId` or the SDK task events. The text id
parser is gone.
- The output path of a known task can come from the result of its own
tool call. The path ends with `tasks/<id>.output` and starts after the
nearest `": "` on the same line, so a path with spaces works, for
example under `C:\Users\John Doe\AppData\Local\Temp`. It must be
absolute (POSIX, a Windows drive or UNC), and it never holds the end of
another path. The parse is linear in the text length. All four texts
give the path in both event orders: the tool result before or after the
SDK names the task. The memory is bounded. A structured path still wins,
and the terminal `output_file` reaches the client also after a level
close.
- The `backgrounded` marker comes only from structured data: the
structured task, a known background task of the tool call, or a
`run_in_background` input. The "both event orders" above holds for the
path, not for this marker. The marker rides the tool result update, so a
command that the SDK names only after its tool result gets the marker
only from a `run_in_background` input. A command that the user moved to
the background by hand gets no marker in that order.
- The replay behaves the same in each case.
## Tests
- Native subagent and agent tests: a task update and a late spawn after
the child finished go to `worker-1`; a held task of a subagent gets its
spawn and state in the child session after the subagent finished; a late
message chunk is still dropped; a subagent's background Bash ends after
the subagent and the client gets the terminal state in the child
session.
- Monitor: runtime, live and replay tests, also a task that names its
tool call after the tool result. A Monitor task cannot be stopped.
- Structured ids: foreground output with each marker text creates no
task; each of the four texts gives the path in both orders, also a
Windows and a POSIX path with spaces and a UNC path; no path of another
task, another tool call, `.outputs`, a relative path, a path without a
`": "` boundary, or a path across a line; two paths on one line are not
glued; a 2 MB text without `": "` parses in milliseconds; a structured
path wins. An agent test covers a subagent without `tool_use_result`,
and a replay test takes the path from the tool result.
- The new tests fail without the change. Build, lint, format and all
tests pass.
## Abandoned streamed tool calls
This part affects every client, Zed included.
**Problem.** The adapter opens a tool call at the `content_block_start`
of a streamed `tool_use`. Claude can abandon that block before it is
complete, for example for a steering message. The tool then never runs.
At end_turn the settle check reported the session failure "Claude ended
the turn without returning results for tool calls"
(`incomplete_tool_call`). A real AIR session showed a red service error
after 27 minutes of normal work, for a call that never had input.
**Fix.**
- The session records the tool calls that Claude Code sent to the tool
runner: the `tool_use` blocks of a complete assistant message, and the
tool calls of a permission request. The record is bounded and is dropped
when the call ends.
- At end_turn, an open foreground call without that record is abandoned.
The adapter closes it as failed with "Claude stopped this tool call
before it ran." and reports no session failure. The turn ends as
`end_turn`.
- An open call with that record keeps the old behaviour: the old failure
text and the `incomplete_tool_call` session failure.
- The cleanup and the cancellation checks are the same for both groups.
- The replay does not change, because the history holds no partial
`tool_use`. No scenario golden file changes, for AIR, plain or Zed.
**Tests.** A partial `tool_use` and then text ends as `end_turn` with
the neutral close. A mixed turn still fails for the unfinished call. A
steering message during a partial `tool_use` gives no failure. The
existing incomplete-tool tests now send the complete `tool_use` and keep
their failure. The new tests fail without the change.
|
||
|
|
190a00f68f |
feat: answer /mcp in chat with the MCP server status and a reconnect (#1218)
## Summary In an ACP client, `/mcp` showed the CLI text "… Use `/mcp` in the terminal for details." The user of an ACP client has no terminal. In SDK mode, Claude Code also refuses `/mcp reconnect|enable|disable` with "Reconnect, enable, and disable aren't available in this session." and changes nothing. - `/mcp` and `/mcp reconnect|enable|disable [<server>|all]` still run as a normal Claude Code turn. The adapter replaces the text of the local command, through the same hook that `/usage` uses. Prompt order, cancel, and the provider update stay with the turn. - For an action, the adapter runs it in that turn through the SDK control API (`reconnectMcpServer`, `toggleMcpServer`). It shows one result line for each server, then the status list. - The status list comes from `query.mcpServerStatus()`. It has a summary line and groups by status: failed, needs authentication, connecting, connected, and disabled on one line. A failed server shows a short cleaned error. A list is used and not a table, because a table breaks words in narrow columns. - Useful CLI sentences stay, for example "There's no MCP server named …". Sentences that point to the terminal are removed. - After an explicit reconnect, a server from the ACP session that needs authentication starts the existing MCP OAuth flow, when the client supports URL elicitation. - `/mcp` is advertised with the input hint `[reconnect|enable|disable [<server>|all]]` and is hidden on history replay. ## Other commits in this branch This branch also carries commits of other work: skill command metadata for AIR, a faster `session/load` replay, and the resumed permission mode. They can move to separate PRs if preferred. ## Tests - `src/tests/mcp-command.test.ts` (61 tests). The fake CLI answers with the real SDK-mode texts and message shapes (synthetic assistant + result) and does not change any status. - A live run through the adapter with a broken and a working stdio server: `/mcp`, `/mcp disable good`, `/mcp enable good`, `/mcp reconnect broken`, `/mcp reconnect nosuch`. - `compare.ts` allows one new difference against the origin/main baseline: the `mcp` command in `available_commands_update`. The baseline files are unchanged. - `npm run check` and `npm run test:run` pass. |
||
|
|
af0b43de94 |
fix: send the legacy custom answer key to AIR (#1210)
Released AIR versions, such as 262.air/release-09, read the custom answer companion only from the root key _meta._askUserQuestionCustomAnswer. The adapter sent only _meta.jetbrains.air.customAnswer, so these versions showed the companion as a separate question. For an AIR client the companion now carries both keys with the same value. Other clients get no key, as before. Remove the root key when the ACP registry no longer serves AIR versions that read only that key. IJAI-2892 |
||
|
|
875d75f2a4 |
feat: AIR tool call contract, exact git patches, plan file, and fixes for every client (#1153)
## Summary One PR with separate commits (the earlier stack #1168 and #1169 is folded back in). ### Exact git patches - `src/diff.ts` builds exact git patches for Edit and Write: real line numbers, file modes, CRLF, the final-newline marker, a size limit (including a bound for `replace_all`), binary detection, and an asynchronous diff with a time budget. The approval preview mirrors the input normalisation of the bundled CLI. - No creation patch over an existing file or for an Edit of an existing empty file, and no patch for a Write that changed nothing. ### For every client Clients that do not declare `_meta.jetbrains.air` (Zed, plain ACP clients) get the same information as on `main`. - A `tool_call_update` omits top-level fields that did not change since the last report of the same tool call (ACP merge semantics). The `_meta` of each report stays complete. - Bug fixes: a failed approval preview no longer blanks the approval diff; a permission denial replaces a pinned approval patch; the compaction summary is not repeated when the streamed text is identical; streamed subagent text is not sent twice; async task fields are recorded only after a successful send; late updates of a finished native subagent are dropped; the updates of a fresh Claude context after a clear-context restart reach the ACP session. ### For AIR only - One fact in one field: input in `rawInput`, the file text of an edit only in the diff (`diffPatch`), output once, no display copies of the input (`rawInputRendering`), `_meta.claudeCode` reduced to `toolName`, `parentToolUseId` and the `toolResponse` markers, per-key `_meta` filtering. - Plan file mode (`planFile`): ExitPlanMode carries only `rawInput.planFilePath`; AIR follows the file that Claude writes. - AIR-only keys move under `_meta.jetbrains.air.*` and are no longer sent to other clients. ### Structure and docs - `src/tool-calls/`: one reporter per tool, one `ClientCapabilities`, one `AcpToolCallRenderer`, the field tracker and the changed-meta filter. - `docs/air-extensions.md` replaces the per-extension AIR docs. ## Tests - `npm run build`, `npm run check`, `npm run test:run`: 1995 passed. - Scenario tests for plain, Zed, and AIR clients: the SDK mock honours the adapter's session id, every update must reach its ACP session, every message is validated against the ACP JSON schema with all formats checked, plain and Zed are compared with a recorded `main` baseline, AIR keeps compact golden recordings. |
||
|
|
79851b8668 | feat: Send experimental session notices for live advisories (#1155) | ||
|
|
bbae5c2c09 |
fix: show shell commands in permission prompts (#1070)
## Summary - use the canonical tool-call title for permission prompts, so shell approvals show the exact command instead of the model-authored description - map `PowerShell` through the same shell presentation path as `Bash` - complete the PowerShell terminal lifecycle with matching `terminal_info`, `terminal_output`, and `terminal_exit` metadata, including failed commands - add focused regression coverage without changing the existing Bash lifecycle tests Fixes #1068 ## PowerShell input and result contract `PowerShell` is a distinct tool name, but its documented input fields match `Bash`: the executable command is in `command`, while `description` is an optional human-readable summary. The Claude Code hooks reference documents the shared `command`, `description`, `timeout`, and `run_in_background` fields, and the tools reference likewise states that PowerShell hooks receive the command in `tool_input.command`. Anthropic closed [claude-code#83647](https://github.com/anthropics/claude-code/issues/83647#issuecomment-5310033500) after documenting this equivalence. Reusing the SDK's exported `BashInput` shape for the common input fields is therefore intentional; the SDK currently does not export a separate `PowerShellInput` type. PowerShell results do not need a PowerShell-specific output assumption here. The result mapper first shape-checks structured stdout/stderr and otherwise falls back to the standard raw `tool_result` string or content-block array. Routing PowerShell through that existing mapper preserves this fallback while completing the adapter's capability-gated terminal sequence: 1. initial `tool_call` with `terminal_info` 2. `tool_call_update` with `terminal_output` 3. final `tool_call_update` with `terminal_exit` All three events use the original tool-use ID, so the client can reconcile and close the terminal. Failed PowerShell results also bypass the generic error renderer and finish the terminal with a non-zero display exit code. The permission presentation introduced in #1004 already handled `Bash` and `PowerShell` together when choosing a description-based permission title, but the canonical tool presentation and terminal result paths only handled `Bash`. This PR completes both paths. References: - [Claude Code hooks: PowerShell input](https://code.claude.com/docs/en/hooks#powershell) - [Claude Code tools reference: PowerShell tool](https://code.claude.com/docs/en/tools-reference#powershell-tool) - [Anthropic schema confirmation](https://github.com/anthropics/claude-code/issues/83647#issuecomment-5310033500) ## Verification - `npm run check` - `npm run build` - `npm run test:run -- src/tests/tools.test.ts -t "PowerShell terminal output"` (1 passed) - full suite with an empty temporary Claude config: `CLAUDE_CONFIG_DIR=<empty temporary directory> npm run test:run` (1070 passed, 27 skipped) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> |
||
|
|
735950a83e |
perf: supply AIR diff counts from Claude structured patches (#1122)
Claude supplies structured patches for Edit and Write, but ACP diff blocks previously carried only the reconstructed texts. AIR had to compare those texts again to display added and removed counts. Count patch operations in the existing conversion loop and publish `_meta.jetbrains.air.diffStats` on each diff block. This uses the same contract as https://github.com/agentclientprotocol/codex-acp/pull/501. Each block has its own counts. No extra traversal, full file comparison, or navigation calculation is needed. Omit statistics when patches are absent or inconsistent, so clients retain their existing fallback. Exclude EOF markers from the displayed texts. Document the extension and cover per-block counts, Edit and Write hooks, EOF changes, and fallback behavior. Validation: - `npm run build` - `npm run check` - `env -u ANTHROPIC_BASE_URL npm run test:run` (the local endpoint override affects unrelated provider tests) |
||
|
|
3d99cafe4a |
fix: honor host opt-out for allowDangerouslySkipPermissions (#1129)
## Summary Closes #1119. Supersedes #1120 by @amansk (their commits are included on this branch; the fork does not allow maintainer edits, so review fixes landed here instead). Hosts that never use `bypassPermissions` can opt a session out by sending `_meta.claudeCode.options.allowDangerouslySkipPermissions: false` on `session/new` (and again on `session/load`). When omitted or `true`, behavior is unchanged: bypass is available on non-root sessions. ## Design Bypass availability is decided once per session in `createSession`: ```ts const allowBypass = ALLOW_BYPASS && sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false; ``` and threaded to the three places that must agree: - the SDK `allowDangerouslySkipPermissions` flag (the actual enforcement; the CLI refuses bypass without it) - the spawn-time mode clamp in `resolvePermissionMode`, so a `permissions.defaultMode` of `bypassPermissions` (or a mode carried across a clear-context restart) becomes `default` before `query()` rather than failing at spawn - the session mode catalog in `SessionModeManager.initialize`, so the client is never offered a mode the CLI would reject ## Changes from #1120 - Fold the host opt-out into `resolvePermissionMode` via an `allowBypass` parameter instead of a second inline clamp; drop the separate `resolveAllowDangerouslySkipPermissions` helper - Make `allowBypass` a required `initialize` parameter and remove the module-global `ALLOW_BYPASS` fallback from `session-mode.ts` - Fix `tsc` errors in the new tests (`modes` is optional on `NewSessionResponse`), which broke the CI build job - Move the settings-driven clamp test onto the shared `acp-agent-settings` fixture (isolated cwd, no double module reload, passes as root) - Document the opt-out in `docs/permission-extension.md` ## Testing - `npm run check`, `npm run build` — pass - `npm run test:run` — pass (the branch is based before #1123, so the SDK version guard in `model-presentation.test.ts` only passes with the pinned 0.3.257; it clears on rebase) Co-authored-by: Cursor Agent <cursoragent@cursor.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Amandeep Khurana <amansk@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d3205ab11f |
feat(deps): Bump @anthropic-ai/claude-agent-sdk to 0.3.270 (#1123)
Bumps the minor group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.257` | `0.3.263` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.122.0` | `0.124.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.5.0` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.69.0` | `8.70.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.69.0` | `8.70.0` | | [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` | | [hono](https://github.com/honojs/hono) | `4.13.5` | `4.13.7` | | [jose](https://github.com/panva/jose) | `6.2.10` | `6.2.12` | Updates `@anthropic-ai/claude-agent-sdk` from 0.3.257 to 0.3.263 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/anthropics/claude-agent-sdk-typescript/releases">@anthropic-ai/claude-agent-sdk's releases</a>.</em></p> <blockquote> <h2>v0.3.263</h2> <h2>What's changed</h2> <ul> <li>Updated to parity with Claude Code v2.1.263</li> </ul> <h2>Update</h2> <pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.263 # or yarn add @anthropic-ai/claude-agent-sdk@0.3.263 # or pnpm add @anthropic-ai/claude-agent-sdk@0.3.263 # or bun add @anthropic-ai/claude-agent-sdk@0.3.263 </code></pre> <h2>v0.3.261</h2> <h2>What's changed</h2> <ul> <li>Added <code>pluginDelivery: 'initialize'</code> to send <code>plugins</code> over stdin so the launch command line no longer grows with the plugin count (fixes Windows start failures with many plugins)</li> <li>Fixed <code>query()</code> throwing "Object not disposable" in runtimes without a native <code>Symbol.dispose</code>, such as Node ≤22 <code>vm</code> contexts (Jest's <code>node</code> environment, vitest <code>vmThreads</code>/<code>vmForks</code>) and Node <18.18</li> <li>Updated to parity with Claude Code v2.1.261</li> </ul> <h2>Update</h2> <pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.261 # or yarn add @anthropic-ai/claude-agent-sdk@0.3.261 # or pnpm add @anthropic-ai/claude-agent-sdk@0.3.261 # or bun add @anthropic-ai/claude-agent-sdk@0.3.261 </code></pre> <h2>v0.3.260</h2> <h2>What's changed</h2> <ul> <li>Added optional <code>user_message_uuid</code> to <code>thinking_tokens</code> system messages, linking thinking progress to the user message that triggered the turn</li> <li>Added optional <code>first_content_frame_ms</code>, <code>first_stream_post_ms</code>, <code>first_stream_post_ack_ms</code> and <code>first_stream_post_wall_ms</code> fields to the success result message for remote-session latency breakdowns</li> <li>Fixed <code>managedSettings</code> <code>disableAutoMode: "disable"</code> (either spelling) being dropped by the restrictive-only filter instead of turning auto mode off for the spawned session</li> <li>Fixed <code>rewindFiles()</code> reporting success when no files could be restored (for example when checkpoint backups are missing); it now fails</li> <li>Changed <code>error_max_structured_output_retries</code> results to append the last StructuredOutput tool error; validation errors now name the offending key, allowed values, and actual length or count</li> <li>Changed <code>rate_limit_event</code> to also re-emit during an exceeded window on repeat 429s (about once per 30 seconds per limit window), so stream consumers can refresh stale rate-limit state</li> <li>Updated to parity with Claude Code v2.1.260</li> </ul> <h2>Update</h2> <pre lang="sh"><code></tr></table> </code></pre> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md">@anthropic-ai/claude-agent-sdk's changelog</a>.</em></p> <blockquote> <h2>0.3.263</h2> <ul> <li>Updated to parity with Claude Code v2.1.263</li> </ul> <h2>0.3.262</h2> <ul> <li>Updated to parity with Claude Code v2.1.262</li> </ul> <h2>0.3.261</h2> <ul> <li>Added <code>pluginDelivery: 'initialize'</code> to send <code>plugins</code> over stdin so the launch command line no longer grows with the plugin count (fixes Windows start failures with many plugins)</li> <li>Fixed <code>query()</code> throwing "Object not disposable" in runtimes without a native <code>Symbol.dispose</code>, such as Node ≤22 <code>vm</code> contexts (Jest's <code>node</code> environment, vitest <code>vmThreads</code>/<code>vmForks</code>) and Node <18.18</li> <li>Updated to parity with Claude Code v2.1.261</li> </ul> <h2>0.3.260</h2> <ul> <li>Added optional <code>user_message_uuid</code> to <code>thinking_tokens</code> system messages, linking thinking progress to the user message that triggered the turn</li> <li>Added optional <code>first_content_frame_ms</code>, <code>first_stream_post_ms</code>, <code>first_stream_post_ack_ms</code> and <code>first_stream_post_wall_ms</code> fields to the success result message for remote-session latency breakdowns</li> <li>Fixed <code>managedSettings</code> <code>disableAutoMode: "disable"</code> (either spelling) being dropped by the restrictive-only filter instead of turning auto mode off for the spawned session</li> <li>Fixed <code>rewindFiles()</code> reporting success when no files could be restored (for example when checkpoint backups are missing); it now fails</li> <li>Changed <code>error_max_structured_output_retries</code> results to append the last StructuredOutput tool error; validation errors now name the offending key, allowed values, and actual length or count</li> <li>Changed <code>rate_limit_event</code> to also re-emit during an exceeded window on repeat 429s (about once per 30 seconds per limit window), so stream consumers can refresh stale rate-limit state</li> <li>Updated to parity with Claude Code v2.1.260</li> </ul> <h2>0.3.259</h2> <ul> <li>Added <code>user_message_uuids</code> beside <code>user_message_uuid</code> on a turn's first reply frame and result: every user message the turn answered, so a reply to several merged messages can be matched to each</li> <li>Added <code>permissionPrompts: 'none'</code> option to auto-deny permission prompts in sessions with nobody to answer them, without disabling auto mode's classifier</li> <li>Updated to parity with Claude Code v2.1.259</li> </ul> <h2>0.3.258</h2> <ul> <li>Updated to parity with Claude Code v2.1.258</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
67c7c9b117 |
feat: Add tool names to tool calls (#1128)
Supports the new tool call name RFD |
||
|
|
4deace4037 | ci: poll for package availability after publish before triggering registry update (#1121) | ||
|
|
2b666c3d35 |
feat: advertise recommended config values (#1111)
Clients that negotiate the AIR `recommendedValue` capability receive concrete model and effort choices instead of ambiguous default rows. The SDK's default model is mapped only to an exact selectable model (including equivalent context suffix spellings); unavailable recommendations retain the legacy default. Recommendations remain independent of explicit selections. The adapter applies the displayed effort to the SDK at startup and on model switches, preserving supported user pins and re-reading per-model settings for automatic choices. Permission modes and clients without the capability retain their existing configuration behavior. Model and effort configuration logic is extracted into dedicated modules. Terse SDK model labels are enriched with concrete versions from model metadata: for example, `Fable` becomes `Fable 5.1`, `Sonnet` becomes `Sonnet 5`, and `Opus (1M context)` becomes `Opus 5`. Custom names are preserved, and collisions fall back to the original labels. An SDK version guard requires review on upgrades, and an opt-in live initialization test verifies the upstream model contract without sending a prompt. Validation: - `npm run check` - `npm run build` - `env -u ANTHROPIC_BASE_URL npm run test:run` — 1253 passed, 28 skipped - `RUN_INTEGRATION_TESTS=true npx vitest run src/tests/model-presentation.test.ts` — 4 passed - Regression coverage for unavailable model generations/context windows, startup effort, per-model settings, unsupported effort pins, and duplicate Opus labels |
||
|
|
45f8b34020 | ci: don't wait for CI workflow when publishing preview releases, replace --dry-run with real publication (#1113) | ||
|
|
0dd58685b1 |
ci: set up preview publishing (#1084)
Today nothing reaches npm between two release-please releases, so trying a merged but unreleased change means building from source. This adds a continuously published preview channel. Every CI-green push to `main` that is not a release merge publishes `<package.json version, patch bumped>-preview.N` under the `preview` dist-tag, tags the commit `v<version>`, and dispatches the same agent registry update a stable release does. With `main` at 0.73.0 that gives `0.73.1-preview.1`, `0.73.1-preview.2`, and so on; `N` restarts at 1 whenever a release moves the base. `latest` is untouched. ## Approach - **Added to `publish.yml` rather than a new workflow.** npm binds a trusted publisher to one repo + workflow filename, and a package gets only one. A separate file would fail OIDC and cost the stable path its publisher. - **Gated on the `CI` workflow succeeding** (`workflow_run`), so a red commit is never published. release-please's release merge is excluded by the head commit's author and subject — its `releases_created` output is not reachable from a different workflow run. - **`N` is the max of the npm registry and local git tags.** npm reserves versions forever, even after `unpublish`, so a reused `N` is a hard failure; but the registry is CDN-served and lags, which the tags cover. A registry read failure aborts rather than guessing. - **Base is patch+1, not release-please's prediction**, which moves mid-flight and would reset `N` under already-published previews. - **The version is applied to the CI working tree only**, never committed — `package.json` and the release-please manifest stay release-please's to own. - **`--tag preview` is mandatory:** `npm publish` defaults to `latest` even for a semver prerelease. - **Three jobs.** `publish-npm-preview` mirrors `publish-npm` and only publishes (no repo write access); `publish-tag-preview` only tags, so a failed tag is retryable with _Re-run failed jobs_ without re-publishing an immutable version; `trigger-registry-update` is shared with the stable path. Both downstream jobs gate on a `published` output the publish step sets, not on job status — the tag push happens after the publish, so a red job can still mean npm has the version. ## Next step The publish is `npm publish --dry-run` for now. The `workflow_run` trigger and the `release` environment's protected-branch policy mean this job cannot run off the default branch at all, so merging this is the only way to exercise the trigger, the gate and the version calculation — and with `published` unset, the rehearsal neither tags nor touches the registry. Once one run looks right, a follow-up drops `--dry-run` and adds `echo "published=true" >> "$GITHUB_OUTPUT"` to that step. Nothing else changes. |
||
|
|
996d488589 |
feat: align Claude modes and clear-context planning (#1004)
## Summary - translate Claude SDK permission suggestions and tool-specific decisions into ACP v1 presentation and editable option metadata - align Claude session permission modes and clear-context plan handling - split permission effects, modes, normalization, option construction, presentation, and response handling into focused modules - document the permission extension and add unit plus live-audit coverage for native edge cases ## Tests - `npm run check` - `npm run build` - `env -u ANTHROPIC_BASE_URL npm run test:run` (931 passed, 27 skipped) |
||
|
|
0581b9cf39 |
feat: align typed session failures with AIR protocol (#992)
## Summary - align `_meta.jetbrains.air.sessionFailure` with AIR's transcript-record protocol: `id`, `revision`, broad `category`, explicit `severity`, `title`, optional `details`, and `actions` - forward Claude's actual user-facing assistant/result text as `title`; use adapter-authored fallback text only when Claude provides no message (transport failure, worker EOF, or an internal invariant) - reduce categories to `connection`, `access`, `limit`, `request`, `service`, and `unknown` - limit suggested actions to `retry`, `login`, and `new_session` - update one transcript record through `id + revision`, keep resolved entries in history, and remove clear/tombstone updates - suppress duplicate assistant failure prose only for clients that negotiated typed failures; preserve legacy ACP behavior otherwise - restore synthetic usage-limit failures from session history with their exact stored Claude text - document negotiation, delivery carriers, identity/revision behavior, mappings, recovery, and compatibility in the extension spec ## Validation - `npm run check` - `npm run build` - `npm run test:run` — 777 passed, 26 skipped - `git diff --check` ## Tracking [IJAI-993](https://youtrack.jetbrains.com/issue/IJAI-993) |
||
|
|
8aaf608b4e | ci: fix release flow (#971) | ||
|
|
f8fd3ab822 |
fix: publish and replace Claude goals reliably (#967)
## Summary - publish optimistic provider-neutral goal snapshots for submitted and steered goal commands - advertise and implement `set`, using steering for an active turn and an ordinary prompt while idle - suppress stale old-goal runtime updates while replacement/clear is pending and roll back failed commands - keep cancellation turn-scoped and orphan-account pending steers so stale results cannot settle the next prompt - reject unsupported pause/resume controls without host-side fallbacks ## Validation - `npm test -- --run` (702 passed, 20 skipped) - `npm run check` - `npm run build` - regression coverage for idle/active set and clear, stale runtime updates, command failure rollback, cancellation, and stale steer isolation |
||
|
|
133337ffe5 | ci: simplify the release flow and make it agent-friendly (#965) | ||
|
|
8b31dea11b |
feat: expose provider-neutral ACP goal extension (#964)
## Summary
- advertise a provider-neutral goal extension in
`InitializeResponse._meta.goal`
- map Claude SDK `active_goal` messages to
`session_info_update._meta.goal`
- expose the shared `_session/goal` control method with the truthfully
supported `clear` action
- document the extension as a candidate shape for a future first-class
ACP goal API
## Architecture
A goal is persistent **session state**, not the lifecycle of one
`session/prompt` request. `goal.status === "active"` says that the
objective may drive more work; it does not say that a prompt is
executing now.
This keeps two independent axes:
1. **Goal state** is published asynchronously as session metadata and
may survive multiple prompt turns.
2. **Prompt activity** follows the backend's actual running/quiescent
boundaries. While a turn runs, clients use advertised steering or prompt
queueing. Once the backend is quiescent, an ordinary `session/prompt` is
valid even if the goal remains active.
The metadata is deliberately provider-neutral:
```json
{
"_meta": {
"goal": {
"version": 1,
"controlMethod": "_session/goal",
"actions": ["clear"]
}
}
}
```
State is emitted as `session_info_update._meta.goal`; `null` clears it.
The common snapshot vocabulary supports `objective`, common statuses,
timestamps in Unix milliseconds, budgets/usage, iteration count, and a
continuation reason. Each implementation emits only fields it actually
knows. This shape is intentionally close to a future typed ACP
capability/update so promotion out of `_meta` would not require a
provider-specific migration.
Control actions are capability-negotiated. The shared vocabulary
reserves `set`, `pause`, `resume`, and `clear`, but an agent advertises
only the subset it can implement with correct semantics.
## Claude mapping
The Claude runtime already emits internal `active_goal` messages,
although that message is not currently included in the public
`SDKMessage` union. The adapter handles it before the exhaustive switch:
- `condition` → `objective`
- `iterations` → `iterations`
- `set_at` → `createdAt`
- `last_reason` → `lastReason`
- `value: null` → `goal: null`
Provider bookkeeping such as `tokens_at_start` is intentionally not
exposed. Goal updates do not settle, extend, or otherwise mutate ACP
prompt lifecycle. Claude's existing Stop hook remains authoritative for
whether the backend itself continues working.
Only `clear` is advertised for out-of-band control. It submits `/goal
clear` through the existing session prompt queue, preserving the
adapter's established ordering and turn ownership. Setting a goal
remains available through `/goal <condition>` but is not advertised as a
control action because it starts the long-running Stop-hook workflow.
The companion Codex implementation is agentclientprotocol/codex-acp#371.
## Verification
- `npm run test:run` — 692 passed, 20 skipped
- `npm run build`
- `npm run check`
|
||
|
|
1e15f3d8b0 |
feat: allow configuring models through env vars (#586)
Allow providing Claude model settings via env var on startup. Providing model settings is required when using Claude with alternative llm provider, e.g. Bedrock as models have different IDs there |
||
|
|
9e18a4e6fd | Add docs for release process (#135) |