Compare commits

..

12 Commits

Author SHA1 Message Date
Ben Brandt
7184fd3cfe fix: keep a long Agent call's heartbeat on the session that made the call
A heartbeat of a running Agent call reports under a synthetic id, so the
adapter falls back to parent_tool_use_id, which is the Agent call itself, and
also stamped that id as the beat's parentToolUseId. For a client on the
subagents RFD, which sees the Agent call, the beat and every later update of
the call then went to the child session. Found in a live run with a 40-second
subagent.

The same run showed that Claude Code's interrupt currently stops background
subagents too, so the README and comments no longer say they keep running.
2026-10-13 14:59:44 +02:00
Ben Brandt
9382e10f2c fix: keep the cancel's tool-call cleanup for AIR, whose draft ends every child at the cancel
The previous commit stopped a cancel from clearing each tool call's state, so
that a raced tool call of an RFD client's background child stays on the root.
For AIR that let a later update of an ended child's permission tool call reach
the child's session after subagent_state_update cancelled. Only the RFD form
keeps the state now.
2026-10-13 09:52:54 +02:00
Ben Brandt
4b67682ff9 fix: never announce a nested subagent early or under the root, and keep a raced tool call where it was emitted after a cancel
Two MUSTs of the subagents RFD that rare event orders broke:

- A child is announced before any traffic that names it, and never under a
  guessed parent. An Agent call made inside a child that was known but not
  yet announced registered that child as the parent, so the nested child
  was announced under it before the child itself was; and one made inside a
  child not known yet fell back to the root. In the RFD form, such a call
  now establishes no parent: its update is buffered under the child's tool
  use, as before, and routed again once the child is announced, which
  registers the parent and announces the nested child after it.
- An interaction already issued never moves to another session. A cancel
  cleared the session of every eagerly emitted tool call, a relic of when a
  cancel ended every subagent. A background child keeps running after a
  cancel (agent-client-protocol#2308), so a tool call of its that a
  permission request emitted on the root before the child was announced
  then moved into the child, for example with its next progress beat. A
  cancel now keeps the per-tool-call state (the session, the fields the
  client holds, the hook callbacks); each tool call drops it when it ends,
  and the cancelled turn's own tool calls end through their results or
  closeUnfinished.
2026-10-13 08:28:34 +02:00
Ben Brandt
529718af76 fix: report a failed subagent with the error stop reason
The subagents RFD reports a failure that ends a child's foreground work as
`idle` with `stopReason: "error"` and, when available, the failure as a
JSON-RPC `error` (agent-client-protocol#2281). SDK 1.8.0's v1
`IdleStateUpdate` has that variant, so the stand-in
`_meta.claudeCode.error` goes: a failed child is now idle with the `error`
stop reason, and with the SDK's error text as an internal error, the code a
failed turn uses on the v2 surface.
2026-10-13 08:11:40 +02:00
Ben Brandt
69cd49afeb Merge remote-tracking branch 'origin/main' into feat/subagent-update
# Conflicts:
#	src/acp-agent.ts
2026-10-13 08:02:17 +02:00
Ben Brandt
764c5abdf1 fix: report no cancellation for a subagent that only outlived a cancel
From a third review of the RFD form, against the subagents RFD as
revised by agentclientprotocol/agent-client-protocol#2308 (a cancel stops
the descendant work the cancelled work waits on; the rest is up to the
Agent; the Client cleans up a descendant's requests once it reports a
cancelled idle):

- At the end of the stream, or a conversation reset, a child that still
  runs is idle with no stop reason. After a cancel it is a background
  child that outlived it, so reporting `cancelled` invented a
  cancellation, which under #2308 also tells the client to clean up its
  requests. Foreground children report their cancellation from the SDK's
  task events, before the cancelled prompt resolves.
- A SendMessage result whose call has no message text still names the
  call, so a late result does not resume a run that already ended.
- An agent-level test pins #2308's validation scenario: at a root
  cancel, the foreground child's request is withdrawn and it reports a
  cancelled idle before the prompt response; the background child keeps
  running with its request open, and ends idle with no reason.
- README says what `session/cancel` does to children.

AIR's path and golden files are unchanged.
2026-10-05 23:06:55 +02:00
Ben Brandt
5588538fe2 docs: note why a subagent's agent id is unique enough as its session id
The RFD needs a child session id unique within the connection and never
under two parents. Agent ids are random and an agent keeps its parent, so
only a fork could repeat one under another root, which the SDK's
forkSession cannot do today because it copies no subagents/ history.
2026-10-05 19:29:55 +02:00
Ben Brandt
fd41b69e05 Merge remote-tracking branch 'origin/main' into feat/subagent-update
# Conflicts:
#	src/native-subagents.ts
2026-10-05 17:02:56 +02:00
Ben Brandt
bce759a258 fix: order a resumed subagent's delegations by the call that made each one
Fixes from a second review of the RFD form, for resume and end events
that arrive out of their usual order:

- Each delegation is the call the SDK runs it under. A run's end records
  its call; a SendMessage result for a call whose run already ended
  resumes nothing (`isEndedDelegation`), so a late result no longer sets
  an idle child running for good or re-arms the turn's hold.
- The end of an earlier delegation ends nothing (`isStaleEnd`), now also
  in the agent: a stale task_notification no longer settles the resumed
  run's live task, which permission attribution and the turn hold use.
- A run under an id the child used before becomes the current delegation
  again: the check compares with the current call, not the map.
- A child that was never announced does not take a resume's call, so a
  late frame cannot announce a withdrawn child.
- A running task_updated patch reports only that the child runs: its
  prompt is a guess, and the task_started or SendMessage result carries
  the delegation (`sendMessageResumed`). `taskResumed` keeps its
  signature from main.
- A resume while a request of the child is open reports requires_action.
- The end-to-end stale-end test now checks that the child stays running
  until the real end.

AIR's path and golden files are unchanged.
2026-10-05 17:00:24 +02:00
Ben Brandt
f826b6025e Merge remote-tracking branch 'origin/main' into feat/subagent-update 2026-10-05 16:29:22 +02:00
Ben Brandt
020dab8dce fix: follow a resumed subagent under the call that resumed it, and other RFD review fixes
Fixes from a close review of the RFD form, against the subagents RFD and
the bundled Claude Code CLI:

- The CLI runs a resumed agent under the id of the call that resumed it
  (`toolUseContext.toolUseId`, the SendMessage call). The child now takes
  over that id, so the resumed run's output and its task_notification
  reach the child instead of being buffered and dropped. A task_started
  of a known child resumes it even without a subagent type.
- A prompt message's id is the id of the call that delegated, and a
  prompt whose id is already in the child's transcript is not sent
  again, so a repeated resume signal does not overwrite or repeat it.
- The end of an earlier delegation no longer ends the current one.
- A resume's `senderSessionId` is the session that made the SendMessage
  call, and is left out when unknown, instead of always the parent.
- A failure reported first without the SDK's error text gets it when a
  later event of the same failure carries it.
- A task that a worker restart orphaned is idle with no stop reason, not
  cancelled: the RFD forbids inventing a cancellation.
- A child that ends before its parent is known stays unexposed, rather
  than being announced under the root: the RFD forbids guessing parentage.
- A late frame that names a child keeps its role in the title.
- `announceNativeSubagent` takes its form explicitly.
- README, docs/air-extensions.md and doc comments say what the code does.

AIR's path and golden files are unchanged.
2026-10-05 16:29:21 +02:00
Ben Brandt
e34c447e96 feat: report subagents as the subagents RFD defines, for clients that are not AIR
A client that declares `clientCapabilities.subagents` and is not AIR now
gets native subagent sessions in the form of the revised subagents RFD
(agentclientprotocol/agent-client-protocol#1992, docs/rfds/subagents.mdx),
which SDK 1.7.0 types. AIR keeps the earlier draft it implements
(`subagent_spawned`, `subagent_state_update`), whichever signal it
declares; its golden files do not change.

In the RFD form (`SubagentForm`, chosen by `subagentForm`):

- The child's session id is the SDK agent id, for every delegation to
  it: a SendMessage resume no longer opens a new generation.
- `subagent_update` announces the child on its immediate parent before
  any child traffic, with `title` (the agent's name, with its role after
  it, else the role), `description` (the Agent input's short
  description, never the prompt), and the `running` state.
- The prompt of each delegation is a `session_message` in the child's
  transcript, from the parent, with the id of the tool use that sent it.
  The Agent, Task, or SendMessage tool call stays a tool call of the
  session that made it.
- The work state follows the SDK: `requires_action` while a permission
  or question request of the child is open, `idle` with `end_turn`,
  `cancelled` when the SDK stopped or killed the task, and a failure as
  `idle` with the SDK's error in `_meta.claudeCode.error`. (The RFD's
  `error` stop reason is newer than SDK 1.7.0's closed v1 StopReason.)
  `session/cancel` no longer ends the children: a background subagent
  outlives the interrupt, and the RFD reports a cancellation only from
  evidence. At the end of the stream a running child gets `idle`,
  `cancelled` after a cancel.
- An idle child is reusable, so its later updates still reach it.
- No per-child capabilities, so a client may not change a child.
- Replay does not expose children yet, until it keeps their live ids.

The agent-side `sessionCapabilities.subagents` goes to AIR alone: the RFD
has only the client capability. compare.ts allows it against the
origin/main baseline.

Tests: the end-to-end tests of the earlier draft now run with an AIR
client; new runtime and end-to-end tests cover the RFD form, and check
every update against the v1 schema.

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-10-05 13:27:21 +02:00
188 changed files with 1571 additions and 22446 deletions

View File

@@ -24,10 +24,9 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Use Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "lts/*"
check-latest: true
cache: "npm"
- name: Install dependencies

View File

@@ -36,7 +36,7 @@ jobs:
# Generating a GitHub token, so that PRs and tags created by
# the release-please-action can trigger actions workflows.
- name: Generate GitHub token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
id: generate-token
with:
# GitHub App ID secret name
@@ -74,10 +74,9 @@ jobs:
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "lts/*"
check-latest: true
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build
@@ -130,10 +129,9 @@ jobs:
# few megabytes packed, so a full fetch is cheap and sidesteps every
# shallow-clone tag caveat.
fetch-depth: 0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "lts/*"
check-latest: true
registry-url: "https://registry.npmjs.org"
# Ahead of the version bump, so this still validates the committed lockfile.
- run: npm ci
@@ -256,7 +254,7 @@ jobs:
exit 1
- name: Generate token scoped to the registry repo
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
id: registry-token
with:
app-id: ${{ secrets.REGISTRY_UPDATER_APP_ID }}

View File

@@ -1,3 +1,3 @@
{
".": "0.98.0"
".": "0.89.1"
}

View File

@@ -1,169 +1,5 @@
# Changelog
## [0.98.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.97.0...v0.98.0) (2026-10-27)
### Features
* add batch session metadata lookup ([#1408](https://github.com/agentclientprotocol/claude-agent-acp/issues/1408)) ([0ce42f2](https://github.com/agentclientprotocol/claude-agent-acp/commit/0ce42f2c312d190f78024d8b4b1ae5f00d9e5939))
### Bug Fixes
* publish Monitor async tasks as monitors ([#1452](https://github.com/agentclientprotocol/claude-agent-acp/issues/1452)) ([4324dce](https://github.com/agentclientprotocol/claude-agent-acp/commit/4324dce4be877c55385b3389cebac1c63d08f456))
* settle the tool call of a SandboxNetworkAccess permission request ([#1448](https://github.com/agentclientprotocol/claude-agent-acp/issues/1448)) ([8f43ac8](https://github.com/agentclientprotocol/claude-agent-acp/commit/8f43ac808931a3f14719bbed37b9b153c9933139))
* **v2:** leave the tool call status out of permission subjects ([#1447](https://github.com/agentclientprotocol/claude-agent-acp/issues/1447)) ([c7c80f7](https://github.com/agentclientprotocol/claude-agent-acp/commit/c7c80f7b26f8d90f7f1c828036db64bc46dddddf))
## [0.97.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.96.0...v0.97.0) (2026-10-26)
### Features
* add ultracode session config option ([#1438](https://github.com/agentclientprotocol/claude-agent-acp/issues/1438)) ([de96cef](https://github.com/agentclientprotocol/claude-agent-acp/commit/de96cef382d8daf27301f1f90ae4b5e7f665f73a))
### Bug Fixes
* bump body-parser from 2.3.0 to 2.4.0 ([#1436](https://github.com/agentclientprotocol/claude-agent-acp/issues/1436)) ([c26e640](https://github.com/agentclientprotocol/claude-agent-acp/commit/c26e640915739dc7a3cd0664f8f5879098e0fd83))
* distinguish model options with duplicate labels ([#1286](https://github.com/agentclientprotocol/claude-agent-acp/issues/1286)) ([434be13](https://github.com/agentclientprotocol/claude-agent-acp/commit/434be13a33141af8655c6f7c30f574841bf2279c))
* keep the usage-limit error when it arrives after compaction ([#1377](https://github.com/agentclientprotocol/claude-agent-acp/issues/1377)) ([2a1525e](https://github.com/agentclientprotocol/claude-agent-acp/commit/2a1525e57cd89246d1a50993fd6ba335c4e634ca))
* pass --settings before the auth status subcommand ([#1392](https://github.com/agentclientprotocol/claude-agent-acp/issues/1392)) ([6a33ded](https://github.com/agentclientprotocol/claude-agent-acp/commit/6a33dedcbb90e7bf9db91207f0edbf19ca6d705e))
* preserve foreground fork reports until launch completes ([#1431](https://github.com/agentclientprotocol/claude-agent-acp/issues/1431)) ([e369479](https://github.com/agentclientprotocol/claude-agent-acp/commit/e369479f93b34f84a11103323d097c44df42e02e))
* settle advisor tool results before completing turns ([#1247](https://github.com/agentclientprotocol/claude-agent-acp/issues/1247)) ([bed795d](https://github.com/agentclientprotocol/claude-agent-acp/commit/bed795d261620e6d5113e427a7196d94e66e7034))
* settle live steers folded into autonomous cycles ([#1185](https://github.com/agentclientprotocol/claude-agent-acp/issues/1185)) ([a4957d1](https://github.com/agentclientprotocol/claude-agent-acp/commit/a4957d17c1e1a925ba3b72b29c5dcde4faabbe17))
## [0.96.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.95.0...v0.96.0) (2026-10-26)
### Features
* report SendMessage delegations as linked session messages ([#1424](https://github.com/agentclientprotocol/claude-agent-acp/issues/1424)) ([739dde3](https://github.com/agentclientprotocol/claude-agent-acp/commit/739dde3ae5bb848bdb90e2c9150e2cc4687d480c))
* report the model of each native subagent ([#1432](https://github.com/agentclientprotocol/claude-agent-acp/issues/1432)) ([c319e5e](https://github.com/agentclientprotocol/claude-agent-acp/commit/c319e5e542aabe353bb2c5e177e4c5853392b8c4))
### Bug Fixes
* do not hold an AIR load replay until the CLI starts ([#1433](https://github.com/agentclientprotocol/claude-agent-acp/issues/1433)) ([eb9d63e](https://github.com/agentclientprotocol/claude-agent-acp/commit/eb9d63e5fde9b8fd8fe692dfb3280faba6626a42))
* refine RFD child models after a load, and retry a failed model update ([#1434](https://github.com/agentclientprotocol/claude-agent-acp/issues/1434)) ([9a8bf71](https://github.com/agentclientprotocol/claude-agent-acp/commit/9a8bf71d454fa85d03f55d4101a77e3ea7a149d0))
* send subagents' current state after a load response ([#1423](https://github.com/agentclientprotocol/claude-agent-acp/issues/1423)) ([17a967b](https://github.com/agentclientprotocol/claude-agent-acp/commit/17a967b4dab78bbb9cf9fedae60e6821578dc5cb))
* update to Claude Code 2.1.310 ([#1435](https://github.com/agentclientprotocol/claude-agent-acp/issues/1435)) ([2f3c6bb](https://github.com/agentclientprotocol/claude-agent-acp/commit/2f3c6bb7b3451a00cd77d7bb31c09e690ca220b3))
## [0.95.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.94.0...v0.95.0) (2026-10-23)
### Features
* end a turn at its result for AIR clients with backgroundSubagents ([#1262](https://github.com/agentclientprotocol/claude-agent-acp/issues/1262)) ([9b3a5c8](https://github.com/agentclientprotocol/claude-agent-acp/commit/9b3a5c891c75e78b985adb31f4e4b59a16ad2ebc))
* report read-only subagent configuration ([#1417](https://github.com/agentclientprotocol/claude-agent-acp/issues/1417)) ([6ab670b](https://github.com/agentclientprotocol/claude-agent-acp/commit/6ab670baaefb30aaac2606d8ee6524e08243e5b5))
* report RFD delegations as linked session messages ([#1405](https://github.com/agentclientprotocol/claude-agent-acp/issues/1405)) ([59e292e](https://github.com/agentclientprotocol/claude-agent-acp/commit/59e292e29f773cc6f03c0999993cbe8a2d97a7e3))
* serve experimental ACP v2 ([#1400](https://github.com/agentclientprotocol/claude-agent-acp/issues/1400)) ([4d378f9](https://github.com/agentclientprotocol/claude-agent-acp/commit/4d378f93f36d37ba5ef64af87b617706dd9c6e82))
* show messages typed in other Remote Control clients ([#1371](https://github.com/agentclientprotocol/claude-agent-acp/issues/1371)) ([5b11f47](https://github.com/agentclientprotocol/claude-agent-acp/commit/5b11f470b1c05b114c876b3b6e5b4df9441b1087))
* update to Claude Code 2.1.309 ([#1413](https://github.com/agentclientprotocol/claude-agent-acp/issues/1413)) ([a1521b6](https://github.com/agentclientprotocol/claude-agent-acp/commit/a1521b6196c58dbc5065f9c19fc9694273d3d435))
### Bug Fixes
* bump fast-uri from 3.1.8 to 3.1.9 ([#1410](https://github.com/agentclientprotocol/claude-agent-acp/issues/1410)) ([0fe22ae](https://github.com/agentclientprotocol/claude-agent-acp/commit/0fe22ae5a172b228a574665bb7136b900746041d))
* bump postcss from 8.5.30 to 8.5.31 in the minor group ([#1403](https://github.com/agentclientprotocol/claude-agent-acp/issues/1403)) ([ecaa2c9](https://github.com/agentclientprotocol/claude-agent-acp/commit/ecaa2c935b9f2b63c1846ca17c6c3398390fb0ef))
* bump the minor group across 1 directory with 2 updates ([#1416](https://github.com/agentclientprotocol/claude-agent-acp/issues/1416)) ([f7d6030](https://github.com/agentclientprotocol/claude-agent-acp/commit/f7d6030c04af23171058772480d55f6d2aee5298))
* IJAI-3744 keep a session running while its background tasks are open ([#1399](https://github.com/agentclientprotocol/claude-agent-acp/issues/1399)) ([e9f986b](https://github.com/agentclientprotocol/claude-agent-acp/commit/e9f986b8a941f5c1789078a39e474f247bd0db60))
* publish the session title as soon as the CLI has it ([#1406](https://github.com/agentclientprotocol/claude-agent-acp/issues/1406)) ([50cd76e](https://github.com/agentclientprotocol/claude-agent-acp/commit/50cd76e78dd9bb1ab0e4850a8d95431175ab5947))
* publish turns that Claude Code starts after end_turn as an async task in v1 ([#1390](https://github.com/agentclientprotocol/claude-agent-acp/issues/1390)) ([a1d0eee](https://github.com/agentclientprotocol/claude-agent-acp/commit/a1d0eeeaad3d67a0765f4b89e3d81c1af6ca0dbb))
## [0.94.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.93.0...v0.94.0) (2026-10-19)
### Features
* send one end event after the session/load history replay for AIR ([#1361](https://github.com/agentclientprotocol/claude-agent-acp/issues/1361)) ([1420994](https://github.com/agentclientprotocol/claude-agent-acp/commit/1420994ced20919aad127a8bd0294de022449e35))
### Bug Fixes
* match OpenRouter model ids in the gateway model listing ([#1365](https://github.com/agentclientprotocol/claude-agent-acp/issues/1365)) ([bcf74c2](https://github.com/agentclientprotocol/claude-agent-acp/commit/bcf74c2fe8e32bd24714d45122529dcf2f99a279))
* resolve a skill's SKILL.md by its declared name, not by its directory name ([#1372](https://github.com/agentclientprotocol/claude-agent-acp/issues/1372)) ([424db87](https://github.com/agentclientprotocol/claude-agent-acp/commit/424db8753581bfaf4c5bf278f8df8c089eec35da))
* update to ACP SDK 2.0.0 ([#1386](https://github.com/agentclientprotocol/claude-agent-acp/issues/1386)) ([ff01e06](https://github.com/agentclientprotocol/claude-agent-acp/commit/ff01e06579225e00643914692994cd60da4d6225))
* update to Claude Code 2.1.303 ([#1384](https://github.com/agentclientprotocol/claude-agent-acp/issues/1384)) ([2dab922](https://github.com/agentclientprotocol/claude-agent-acp/commit/2dab92264414d133fd6774f7a90241ea5192ba32))
## [0.93.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.92.0...v0.93.0) (2026-10-16)
### Features
* add /remote-control command to start Remote Control for a session ([#1197](https://github.com/agentclientprotocol/claude-agent-acp/issues/1197)) ([db9f50f](https://github.com/agentclientprotocol/claude-agent-acp/commit/db9f50f02145d088c0f6080672969a0673ba6359))
### Bug Fixes
* hold a fork back from the session list until its session/fork response is out ([#1368](https://github.com/agentclientprotocol/claude-agent-acp/issues/1368)) ([f5967fd](https://github.com/agentclientprotocol/claude-agent-acp/commit/f5967fd6b49ce4821cc829c26902ccf46f671992))
## [0.92.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.91.0...v0.92.0) (2026-10-16)
### Features
* add the MCP server and tool to AIR tool calls ([#1359](https://github.com/agentclientprotocol/claude-agent-acp/issues/1359)) ([872e93f](https://github.com/agentclientprotocol/claude-agent-acp/commit/872e93f11fde8abc528822d29cebcc542b05e9e8))
* publish the predicted follow-up prompt as _meta.promptSuggestions ([#1356](https://github.com/agentclientprotocol/claude-agent-acp/issues/1356)) ([78a29f5](https://github.com/agentclientprotocol/claude-agent-acp/commit/78a29f58ba1eb1b54896d8d73f49402ee50aa087))
* update ACP SDK to 1.9.0 and adopt the v2 spec clarifications ([#1366](https://github.com/agentclientprotocol/claude-agent-acp/issues/1366)) ([68bf74f](https://github.com/agentclientprotocol/claude-agent-acp/commit/68bf74f58ee378f21502ac93c44878b712905af2))
### Bug Fixes
* include subagent file edits in the file-change report ([#1357](https://github.com/agentclientprotocol/claude-agent-acp/issues/1357)) ([910d83c](https://github.com/agentclientprotocol/claude-agent-acp/commit/910d83ceea78a03b39002cf2dcc275442838e2db))
* restore resumed subagents after an adapter restart ([#1364](https://github.com/agentclientprotocol/claude-agent-acp/issues/1364)) ([26f144a](https://github.com/agentclientprotocol/claude-agent-acp/commit/26f144a2a5c60452805d11c87433963318ef4e65))
## [0.91.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.90.0...v0.91.0) (2026-10-15)
### Features
* keep background subagents running after a parent cancel, for clients that can cancel them ([#1348](https://github.com/agentclientprotocol/claude-agent-acp/issues/1348)) ([ba3c7be](https://github.com/agentclientprotocol/claude-agent-acp/commit/ba3c7be05e9af297fe95b0ac647da2340dd9e15b))
* let a client cancel one subagent with session/cancel on its child session ([#1347](https://github.com/agentclientprotocol/claude-agent-acp/issues/1347)) ([aeb69e7](https://github.com/agentclientprotocol/claude-agent-acp/commit/aeb69e7ed1170e8bce236378432ef8b83cdf2554))
* mark session list rows of worktrees Claude Code made (agentWorktree) ([#1352](https://github.com/agentclientprotocol/claude-agent-acp/issues/1352)) ([1a1f1a0](https://github.com/agentclientprotocol/claude-agent-acp/commit/1a1f1a0e802cc2ee097d9c78c1f7bafccef7c3df))
* narrow models to what the providers/set gateway lists ([#1340](https://github.com/agentclientprotocol/claude-agent-acp/issues/1340)) ([e8eb9d3](https://github.com/agentclientprotocol/claude-agent-acp/commit/e8eb9d340fd4f6dfd9826cf58461edf5ee165f44))
* restore a subagent from before a reload when SendMessage resumes it, for subagents RFD clients ([#1332](https://github.com/agentclientprotocol/claude-agent-acp/issues/1332)) ([df21b3f](https://github.com/agentclientprotocol/claude-agent-acp/commit/df21b3fb56c3449b6f86c9c9df558be351815ed3))
### Bug Fixes
* close the tool calls of a background subagent that the SDK reports stopped after a cancel ([#1336](https://github.com/agentclientprotocol/claude-agent-acp/issues/1336)) ([9a1b90f](https://github.com/agentclientprotocol/claude-agent-acp/commit/9a1b90fb9db8b04ef6756cbcdc8df22afc82a2ba))
* close the tool calls of a cycle that Claude Code started and a cancel stopped ([#1339](https://github.com/agentclientprotocol/claude-agent-acp/issues/1339)) ([1fd217f](https://github.com/agentclientprotocol/claude-agent-acp/commit/1fd217f3e77d0b7883ebd220bc19c7899d3af77f))
* close the tool calls that a cycle Claude Code started abandoned, at its end ([#1344](https://github.com/agentclientprotocol/claude-agent-acp/issues/1344)) ([594aa8c](https://github.com/agentclientprotocol/claude-agent-acp/commit/594aa8c9556609c16946c889e9c8f1f76337b203))
* deliver the output of a background subagent that a cancel did not stop ([#1345](https://github.com/agentclientprotocol/claude-agent-acp/issues/1345)) ([bcc024d](https://github.com/agentclientprotocol/claude-agent-acp/commit/bcc024d2c9a34d62e19999bc6cfc393ad56fbcde))
* end the terminal of a command whose tool call is closed without a result ([#1335](https://github.com/agentclientprotocol/claude-agent-acp/issues/1335)) ([764f339](https://github.com/agentclientprotocol/claude-agent-acp/commit/764f339c3e83eac8ca34f4ddc8ed65f8199b651d))
* end the tool calls of a cycle that Claude Code started and a cancel stopped, on v1 ([#1342](https://github.com/agentclientprotocol/claude-agent-acp/issues/1342)) ([b4788d6](https://github.com/agentclientprotocol/claude-agent-acp/commit/b4788d61eaf9372b376d56c0f6fb7c6156822ae6))
* keep a live session that was never prompted when a resume or load changes its settings ([#1353](https://github.com/agentclientprotocol/claude-agent-acp/issues/1353)) ([0c8de13](https://github.com/agentclientprotocol/claude-agent-acp/commit/0c8de1380fd990291fc51cf17c6742402efb5c94))
* let session/delete succeed for a session without a transcript ([#1351](https://github.com/agentclientprotocol/claude-agent-acp/issues/1351)) ([fadbe18](https://github.com/agentclientprotocol/claude-agent-acp/commit/fadbe1881cd441609a1fde8c43286d9a404dc425))
* report no running for the session's own request that a cancel withdraws, on ACP v2 ([#1346](https://github.com/agentclientprotocol/claude-agent-acp/issues/1346)) ([d528647](https://github.com/agentclientprotocol/claude-agent-acp/commit/d528647162ba6747654125cf6ebc2297ced02ed0))
* show nothing of a tool that cancelled work asks to run after the cancel was reported ([#1334](https://github.com/agentclientprotocol/claude-agent-acp/issues/1334)) ([ebf8b49](https://github.com/agentclientprotocol/claude-agent-acp/commit/ebf8b49fd68924f2c50680e7e73241f625801980))
* stop waiting for a question the client leaves open after Claude Code cancels it ([#1338](https://github.com/agentclientprotocol/claude-agent-acp/issues/1338)) ([c1e83b4](https://github.com/agentclientprotocol/claude-agent-acp/commit/c1e83b46b717230a5d623c3ac591d915a776e0ae))
* update Claude Agent SDK to 0.3.300 ([#1355](https://github.com/agentclientprotocol/claude-agent-acp/issues/1355)) ([761b4c8](https://github.com/agentclientprotocol/claude-agent-acp/commit/761b4c835985ff265d35338df35eee6e6861d8f3))
## [0.90.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.89.1...v0.90.0) (2026-10-13)
### Features
* accept IDE session hooks without dropping model settings ([#1265](https://github.com/agentclientprotocol/claude-agent-acp/issues/1265)) ([fe8b69e](https://github.com/agentclientprotocol/claude-agent-acp/commit/fe8b69edd438ee1bc4206752b029dd45e71975fe))
* **deps:** Bump actions/setup-node from 7.0.0 to 7.1.0 ([#1329](https://github.com/agentclientprotocol/claude-agent-acp/issues/1329)) ([c6510c2](https://github.com/agentclientprotocol/claude-agent-acp/commit/c6510c27c11bb59ad10090c8c18ba60c0fa1c3fa))
* keep a subagent's session across SendMessage resumes for subagents RFD clients ([#1321](https://github.com/agentclientprotocol/claude-agent-acp/issues/1321)) ([bd06804](https://github.com/agentclientprotocol/claude-agent-acp/commit/bd06804b67d7b4efa8180db2f36203a5d34c3ac4))
* replay each subagent under its live session id for subagents RFD clients ([#1328](https://github.com/agentclientprotocol/claude-agent-acp/issues/1328)) ([21eef57](https://github.com/agentclientprotocol/claude-agent-acp/commit/21eef576557c845bfe9acc66499dab347f813a98))
* report a subagent as requires_action while its request is open for subagents RFD clients ([#1322](https://github.com/agentclientprotocol/claude-agent-acp/issues/1322)) ([252126f](https://github.com/agentclientprotocol/claude-agent-acp/commit/252126f61654cf7ac2cad74be53b4c6add569e1f))
* report subagents as the subagents RFD defines, for clients that are not AIR ([#1320](https://github.com/agentclientprotocol/claude-agent-acp/issues/1320)) ([5758ac8](https://github.com/agentclientprotocol/claude-agent-acp/commit/5758ac84a4a3b95ace7542a7e71fd58d93a581b3))
### Bug Fixes
* close the open tool calls of a held or settling turn that a cancel ends ([#1221](https://github.com/agentclientprotocol/claude-agent-acp/issues/1221)) ([32c23db](https://github.com/agentclientprotocol/claude-agent-acp/commit/32c23db06d53d7d4e1295d78fc0129f8278d75df))
* display active Central helpers as JetBrains Air Gateway ([#1266](https://github.com/agentclientprotocol/claude-agent-acp/issues/1266)) ([d6c3717](https://github.com/agentclientprotocol/claude-agent-acp/commit/d6c371704fb5fd74c3b329a3d756486975e3f180))
* drop streamed output of prompts a cancel abandoned ([#1313](https://github.com/agentclientprotocol/claude-agent-acp/issues/1313)) ([1137df3](https://github.com/agentclientprotocol/claude-agent-acp/commit/1137df33aa4fe387916596ff489c02ee48ccbba3))
* end a cancelled turn at its interrupted result and deliver Claude Code's follow-up after a cancel ([#1309](https://github.com/agentclientprotocol/claude-agent-acp/issues/1309)) ([b007ac7](https://github.com/agentclientprotocol/claude-agent-acp/commit/b007ac7c45b85755877fee802c2c901a0e3057c6))
* end the tool calls of a cancelled turn before it reports the cancel ([#1327](https://github.com/agentclientprotocol/claude-agent-acp/issues/1327)) ([53df1c7](https://github.com/agentclientprotocol/claude-agent-acp/commit/53df1c7b22bf08f9a2866389be812be80f34c8a3))
* keep the terminal of a command in the background running until its task ends ([#1312](https://github.com/agentclientprotocol/claude-agent-acp/issues/1312)) ([d074558](https://github.com/agentclientprotocol/claude-agent-acp/commit/d074558e4a2ef4ee6f4b39aecb5ebf36d8c0f84b))
* report a command whose result says interrupted as cancelled on the experimental ACP v2 surface ([#1301](https://github.com/agentclientprotocol/claude-agent-acp/issues/1301)) ([b3bed2f](https://github.com/agentclientprotocol/claude-agent-acp/commit/b3bed2f7630f4ddb97978246255ba89ca6e4613e))
* route a subagent's PostToolUse hook update to its child session ([#1324](https://github.com/agentclientprotocol/claude-agent-acp/issues/1324)) ([4615a91](https://github.com/agentclientprotocol/claude-agent-acp/commit/4615a9141c12e8cd9093a5bbc0e62a7d778cf0cf))
* start a forked session so the client can prompt it at once ([#1317](https://github.com/agentclientprotocol/claude-agent-acp/issues/1317)) ([383bc7d](https://github.com/agentclientprotocol/claude-agent-acp/commit/383bc7dcf0d4d624d2d74405eb3cee6453eb9b11))
## [0.89.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.89.0...v0.89.1) (2026-10-10)

View File

@@ -17,7 +17,6 @@ This tool implements an ACP agent by using the official [Claude Agent SDK](https
- Interactive (and background) terminals
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
- Client MCP servers
- An Ultracode session config option, which turns on Claude Code's dynamic workflows for every task. It appears when dynamic workflows are enabled and the model supports ultracode (any model with `xhigh` effort)
- `/mcp` in the chat: the MCP server status as a list. The adapter runs `/mcp reconnect`, `/mcp enable`, and `/mcp disable` through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
- Session-scoped long-running goals for AIR through the [goal extension](docs/air-extensions.md#goal) under `_meta.jetbrains.air.goal`
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/air-extensions.md#session-failure)
@@ -43,57 +42,22 @@ Task subagent as its own child session, following the [subagents RFD](https://gi
- `subagent_update` on the parent announces the child before any of its traffic, with the
parent's `title` and `description` for it, and reports its work state: `running`,
`requires_action` while one of its requests is open (and `running` again when the last one ends,
unless a cancel withdrew it: the child is stopping then), and `idle` when its work ends, with
`requires_action` while one of its requests is open, and `idle` when its work ends, with
`end_turn` or `cancelled` when the SDK gives the reason. A failure is `idle` with the `error`
stop reason and the SDK's error text as its `error`. The child keeps its session for every
delegation to it.
- Session messages replace the Agent or Task call that launches the child, which its parent is
not shown:
- The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and
the child's incoming one. Both use the launch's tool use id, and each names the other with the
draft RFD's `recipientMessageId` and `senderMessageId`.
- A foreground child's answer, from the launch's result, is the child's `session_message` to
the parent. In Claude Code's hand-back mode, the child's `SubagentHandback` call is its
message to the parent instead, in both transcripts and linked the same way.
- A launch whose child is never exposed is shown as a tool call after all, with its final
status.
- The message of a SendMessage call that resumes the child is a `session_message` in the child's
transcript. The SendMessage call stays a tool call of the session that made it.
- The prompt of the Agent or Task call that launches the child, and the message of each
SendMessage call that resumes it, is a `session_message` in the child's transcript. The tool
call stays a tool call of the session that made it.
- `session/cancel` on the parent interrupts Claude Code, which stops the foreground children that
the turn waits on. Background children keep running, as the RFD prefers; a client stops one with
its own `cancel`. Each stopped child reports `idle` with `cancelled` from Claude Code's own
events, and its open requests are withdrawn. The adapter ends no child at a cancel itself, so a
child that keeps running keeps its open requests and its open tool calls. To spare background
children, the adapter declares the SDK option `perTaskStopAffordance`, which a client can turn
off in `_meta.claudeCode.options`. That option spares background workflows too, which a client
cannot stop, so the adapter stops them after the interrupt, as Claude Code did without it.
Background commands keep running after a cancel either way. A tool call of a stopped child that Claude Code reports no
result for, such as the command it was running, ends in the child's session before the cancelled
prompt resolves. When Claude Code stops a background child after that, or while no prompt runs,
the tool call ends right after the child reports `idle`. A child that still runs when Claude Code
exits is `idle` with no stop reason.
the turn waits on and, currently, background children too. Each stopped child reports `idle`
with `cancelled` from Claude Code's own events, and the open requests of the foreground ones are
withdrawn. The adapter ends no child at a cancel itself, so a child that keeps running keeps its
open requests. A child that still runs when Claude Code exits is `idle` with no stop reason.
- A child whose parent is not yet known when it ends is not exposed, since the RFD forbids
guessing its parent.
- A replay restores each child under its live session id, from its own transcript, with the same
session messages as live, and its end is an `idle` before the response.
A launch without Claude Code's metadata file for the child stays an ordinary tool call of the
parent, and a notice, for a client that takes notices, says that subagent history is missing.
- After a load or resume, with or without replay, a SendMessage that resumes an agent from before
it runs that agent in its session from before. Before the agent's traffic, each of its ancestors
that the client was not told about in this connection is announced, parent before child and
with no work state, as the RFD requires. An agent whose launch the session's history does not
record stays unexposed.
- Each running child is announced with the `cancel` capability. `session/cancel` with the child's
session id stops that child's current work, through the SDK's `Query.stopTask`, and not its
parent's: Claude Code also stops the foreground children it waits on, and the parent's turn goes
on. Each stopped child reports `idle` with `cancelled` from Claude Code's own events, and its
requests that the stop withdraws report no `running` first. A replayed child, which no longer
runs, gets no capabilities. A client cannot prompt or otherwise change a child.
An [ACP v2](#experimental-acp-v2) client gets the same without declaring anything, since subagents
are part of v2's baseline. Each child also reports its work state as its own `state_update`, right
after the parent's copy.
- A client cannot prompt, cancel, or otherwise change a child yet, and a replay does not restore
children yet.
JetBrains AIR keeps the earlier draft of the RFD (`subagent_spawned`, `subagent_state_update`),
which it enables with `nativeSubagentSessions` in `_meta.jetbrains.air.capabilities`. See
@@ -103,22 +67,6 @@ Without either signal, Agent and Task stay ordinary tool calls and child interac
root session. Clients that use the historical `_meta["subagent-transcript"]` capability or the
`forwardSubagentText` session option retain the flattened child transcript.
### Experimental ACP v2
The adapter also serves the [draft ACP v2](https://agentclientprotocol.com/protocol/v2/overview):
a client that asks for protocol version 2 or later in `initialize` gets v2, and every other client
gets v1. The draft can still change, and this surface changes with it.
`CLAUDE_AGENT_ACP_PROTOCOL` limits the adapter to one version. With `v1`, a client that asks
for v2 is negotiated down to v1. With `v2`, the adapter rejects the `initialize` of a v1 client.
`auto`, the default, serves both.
On v2 the adapter serves the session baseline (`session/new`, `session/list`, `session/resume`
with replay, `session/close`, `session/prompt`, `session/cancel`), `session/delete`,
`session/fork`, additional directories, config options (the mode is one), plans, notices,
compaction, display terminals for commands, permission requests, elicitation, providers, auth, and
[subagents](#subagent-sessions). Steering and the AIR extensions are v1 only for now.
## Contribution Policy
This project does not require a Contributor License Agreement (CLA). Instead, contributions are accepted under the following terms:

View File

@@ -20,17 +20,14 @@ It describes only this adapter.
- [Session hooks](#session-hooks)
- [Permission presentation](#permission-presentation)
- [Goal](#goal)
- [Predicted follow-up prompt](#predicted-follow-up-prompt)
- [Recommended config values](#recommended-config-values)
- [Async tasks](#async-tasks)
- [Agent file-change report](#agent-file-change-report)
- [Session failure](#session-failure)
- [Native subagent sessions](#native-subagent-sessions)
- [Session index](#session-index)
- [Background subagents](#background-subagents)
- [Context compaction](#context-compaction)
- [Question custom answers](#question-custom-answers)
- [Load replay end](#load-replay-end)
- [Session fork point](#session-fork-point)
- [Presentation hints](#presentation-hints)
- [Removed keys](#removed-keys)
@@ -126,8 +123,7 @@ The `initialize` response of an AIR client carries the agent side of the extensi
"asyncTasks",
"recommendedValue",
"diffPatch",
"planFile",
"backgroundSubagents"
"planFile"
],
"goal": {
"version": 1,
@@ -141,30 +137,26 @@ The `initialize` response of an AIR client carries the agent side of the extensi
}
```
The agent list does not depend on the capabilities that AIR declares, except `sessionIndex`: the agent lists it, together with `sessionBatchGet`, `sessionArchive`, `sessionRename`, `sessionListSubscribe` and `sessionWorktrees`, only for a client that declared `sessionIndex`.
An extension is active only when the client declared its capability. The client does not declare `sessionBatchGet`, `sessionArchive`, `sessionRename`, `sessionListSubscribe` and `sessionWorktrees`.
The agent list does not depend on the capabilities that AIR declares, except `sessionIndex`: the agent lists it, together with `sessionArchive`, `sessionRename` and `sessionListSubscribe`, only for a client that declared `sessionIndex`.
An extension is active only when the client declared its capability. The client does not declare `sessionArchive`, `sessionRename` and `sessionListSubscribe`.
The response to a client that is not AIR has no `_meta.jetbrains` key.
### Capabilities
| Capability | Advertised | What the adapter does when the client declares it | Section |
| ------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `diffPatch` | yes | Sends an exact Git patch in the diff block of an Edit or a Write. | [Diff patch](#diff-patch) |
| `sessionFailure` | yes | Sends warnings and errors as typed transcript records. | [Session failure](#session-failure) |
| `recommendedValue` | yes | Replaces the `default` model and effort rows with concrete values and a recommendation. | [Recommended config values](#recommended-config-values) |
| `asyncTasks` | yes | Publishes background work that is not a subagent as async tasks. | [Async tasks](#async-tasks) |
| `agentFileChangeReport` | yes | Accepts a report request on `session/prompt` and sends the changed file list. | [Agent file-change report](#agent-file-change-report) |
| `nativeSubagentSessions` | yes | Reports an Agent or Task subagent as a native ACP child session. | [Native subagent sessions](#native-subagent-sessions) |
| `planFile` | yes | Sends the path of the plan file in place of the plan text of an ExitPlanMode. | [Plan file](#plan-file) |
| `backgroundSubagents` | yes | Ends a prompt turn at its result while background subagents still run. | [Background subagents](#background-subagents) |
| `sessionIndex` | on request | Pages, orders and annotates `session/list`; adds rename, archive and a list subscription. | [Session index](#session-index) |
| `sessionBatchGet` | with `sessionIndex` | Supports `_session/batch_get` for metadata of known session IDs. | [Batch get](#batch-get) |
| `sessionArchive` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/archive` and `_session/unarchive`. | [Archive](#archive) |
| `sessionRename` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/rename`. | [Rename](#rename) |
| `sessionListSubscribe` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/list/subscribe`. | [List subscription](#list-subscription) |
| `sessionWorktrees` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that rows carry `agentWorktree` for a worktree Claude Code made. | [List response](#list-response) |
| `loadReplayEnd` | no | Sends one `session_info_update` after the whole history replay of a loaded session. | [Load replay end](#load-replay-end) |
| `rawInputRendering` | no | Sends no display copy of readable input in `content`. The client renders `rawInput`. | [Tool call contract](#tool-call-contract) |
| Capability | Advertised | What the adapter does when the client declares it | Section |
| ------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `diffPatch` | yes | Sends an exact Git patch in the diff block of an Edit or a Write. | [Diff patch](#diff-patch) |
| `sessionFailure` | yes | Sends warnings and errors as typed transcript records. | [Session failure](#session-failure) |
| `recommendedValue` | yes | Replaces the `default` model and effort rows with concrete values and a recommendation. | [Recommended config values](#recommended-config-values) |
| `asyncTasks` | yes | Publishes background work that is not a subagent as async tasks. | [Async tasks](#async-tasks) |
| `agentFileChangeReport` | yes | Accepts a report request on `session/prompt` and sends the changed file list. | [Agent file-change report](#agent-file-change-report) |
| `nativeSubagentSessions` | yes | Reports an Agent or Task subagent as a native ACP child session. | [Native subagent sessions](#native-subagent-sessions) |
| `planFile` | yes | Sends the path of the plan file in place of the plan text of an ExitPlanMode. | [Plan file](#plan-file) |
| `sessionIndex` | on request | Pages, orders and annotates `session/list`; adds rename, archive and a list subscription. | [Session index](#session-index) |
| `sessionArchive` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/archive` and `_session/unarchive`. | [Archive](#archive) |
| `sessionRename` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/rename`. | [Rename](#rename) |
| `sessionListSubscribe` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/list/subscribe`. | [List subscription](#list-subscription) |
| `rawInputRendering` | no | Sends no display copy of readable input in `content`. The client renders `rawInput`. | [Tool call contract](#tool-call-contract) |
The adapter ignores `planContentDelta`.
Claude does not stream a plan, so the adapter never sends `contentDelta` (see [Plan file](#plan-file)).
@@ -193,13 +185,11 @@ The adapter sends these keys only to an AIR client. "AIR" in the Gate column mea
| `commandTitle` | Bash and PowerShell tool call `_meta.jetbrains.air` | the `description` input string | AIR |
| `subagent` | Agent and Task tool call `_meta.jetbrains.air` | `true` | AIR |
| `skill` | Skill tool call `_meta.jetbrains.air` | `{name, path?}` | AIR |
| `mcp` | MCP tool call `_meta.jetbrains.air` | `{server, tool}` | AIR |
| `contextCompaction` | compaction tool call or `compaction_update`, `_meta.jetbrains.air` | `{version: 1, trigger?, preTokens?, postTokens?, durationMs?, error?}` | AIR |
| `customAnswer` | elicitation schema property `_meta.jetbrains.air` | `{questionId, isCustomAnswer: true}` | AIR, when the client supports form elicitation |
| `kind` | session mode `_meta.jetbrains.air` and mode config option value `_meta.jetbrains.air` | `standard`, `plan`, `auto_review`, or `full_access` | AIR |
| `kind` | available command `_meta.jetbrains.air` of a skill | `skill` | AIR, when the skill's `SKILL.md` is found; not for a built-in command or an MCP prompt |
| `skillPath` | available command `_meta.jetbrains.air` of a skill | absolute path of the skill's `SKILL.md` | AIR, together with `kind: "skill"` |
| `loadReplayEnd` | `session_info_update._meta.jetbrains.air` | `true` | `loadReplayEnd` |
| `fork` | `session/fork` request `_meta.jetbrains.air` (client to agent) | `{version: 1, messageId, messageFingerprint?, messageOccurrence?}` | none |
## JetBrains shared keys
@@ -215,21 +205,6 @@ The adapter keeps them where they are.
| `steering` | `initialize` response `_meta.steering = {supported: true}` | The agent accepts `_session/steering` for a running turn. |
| `quota` | `PromptResponse._meta.quota` | Token usage of the turn, also split by model. |
| `authStatus` | `initialize` response `agentCapabilities._meta.authStatus` | The agent pushes `_auth/status_update`. The object carries no payload. |
| `promptSuggestions` | `session_info_update._meta.promptSuggestions = {suggestions: [{text}]}`, AIR only | The prompt the agent predicts for the next turn. An empty list clears it. |
## Auth source display
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
A command containing the whole word `central` and the word sequence `proxy start` is displayed as:
```json
{ "authStatus": { "kind": "gateway", "label": "JetBrains Air Gateway" } }
```
Matching is case-insensitive and accepts multiple whitespace characters between `proxy` and `start`. The command's flags, URL, host and route are not checked. This is a weak display heuristic, not proof of configuration provenance, account ownership or entitlement. Inactive helpers and external cloud backends retain their existing classification. No helper command, key, headers, private route or inferred account is included in this payload.
Other active helpers use `label: "Anthropic API key"` and `detail: "API key helper"`; unknown key-source identifiers are not exposed as display text. CLI and SDK reads share this presentation, retain stale-probe protection, and suppress unchanged notifications.
## Zed conventions
@@ -240,7 +215,6 @@ An AIR client that declares `terminal_output_delta` gets appends instead of `ter
A command tool call then has `content: [{type: "terminal", terminalId}]` and `_meta.terminal_info = {terminal_id}`.
The output goes in `_meta.terminal_output = {terminal_id, data}`.
The end sends `_meta.terminal_exit = {terminal_id, exit_code, signal: null}`.
A command that ends without a result (a cancel stopped its subagent, say) gets the exit on the report that closes its tool call, with no exit code (`null`; AIR gets `1`).
- A client that declares neither `terminal_output` nor `terminal_output_delta` gets no terminal.
The command output is a `console` code block in `content`.
- A client that declares `clientCapabilities._meta["terminal-auth"]: true` gets `_meta["terminal-auth"] = {command, args, label}` on the terminal login methods.
@@ -333,14 +307,10 @@ For every tool, a client that is not AIR also gets these upstream fields:
| `Skill` | `kind: other`, `title: "Load skill: <name>"`. | `_meta.jetbrains.air.skill`. | none |
| `TodoWrite`, `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet` | The stream reports them as a standard `plan` with entries. Only a permission request shows them as a tool call. | A plan that repeats the previous plan is not sent. | Every plan goes out. |
| `ReportFindings` | `kind: think`, `title: "Report <n> findings"`. | With `rawInputRendering`, no copy of the findings in `content`. | none |
| MCP tools (`mcp__*`) and other tools | `kind: other`, `title` is the tool name. The result text goes to `content`. | `_meta.is_mcp_tool_call` for MCP, and `_meta.jetbrains.air.mcp = {server, tool}`. | none |
| MCP tools (`mcp__*`) and other tools | `kind: other`, `title` is the tool name. The result text goes to `content`. | `_meta.is_mcp_tool_call` for MCP. | none |
| Context compaction | `compaction_update` when the client declares `session.compaction`. Otherwise a synthetic tool call. | Both forms carry `_meta.jetbrains.air.contextCompaction`. | See [Context compaction](#context-compaction). |
| Background work that is not a subagent | none | With `asyncTasks`, async task updates. | none |
`_meta.jetbrains.air.mcp` comes from the tool name `mcp__<server>__<tool>`, on `tool_call` and on a `tool_call_update` that changes it.
`server` is the part up to the next `__`. `tool` is the whole rest and can contain `__`, as in Claude Code.
A name that does not split (`mcp__x`) gets no `mcp` key, and `_meta.is_mcp_tool_call` still marks it.
## Diff patch
The diff patch extension lets the adapter send one compact Git patch instead of file text snapshots.
@@ -615,7 +585,6 @@ The client already has the rest of the tool call, because the adapter sends the
- `content` is present only with an exact preview patch for a `diffPatch` client.
- `locations` is present only when a valid `blockedPath` is not a location of the tool call yet.
- `_meta.claudeCode.mcpServer` names the MCP server of an `mcp__*` tool.
It is the server name as authored in the config. `_meta.jetbrains.air.mcp.server` on the tool call is the normalized name (characters outside `[a-zA-Z0-9_-]` replaced by `_`), so the two can differ.
A client that is not AIR gets the upstream request `toolCall`: `toolCallId`, `name`, `status: pending`, the whole `rawInput`, `title`, `kind`, `content`, and `locations`.
Its `_meta.claudeCode` carries `toolName`, and `parentToolUseId` for a subagent tool call, next to `mcpServer`.
@@ -708,7 +677,7 @@ A non-empty plan also gets one clear-context choice with the same priority.
After that choice, the adapter stops the old private Claude query and hides that internal rejection.
It starts a new private query under the same ACP session.
It continues the turn with `Implement the following plan:` and the complete plan.
The mode, model, agent, effort, Fast mode, ultracode, and turn usage stay the same.
The mode, model, agent, effort, Fast mode, and turn usage stay the same.
The SDK cannot apply `allowedPrompts` again, so those hints do not survive the handoff.
The adapter does not allow a callback only because the session mode is `bypassPermissions`.
@@ -831,37 +800,6 @@ While that command is pending, the adapter ignores late updates for the previous
A matching runtime `active_goal` stays authoritative and adds the iteration, reason, and timestamp.
A failed command restores the previous state.
## Predicted follow-up prompt
After a turn, the Claude Code CLI can predict the prompt the user is likely to send next.
The adapter asks for it with the SDK option `promptSuggestions: true` and forwards the one prediction of the turn.
ACP has no field for a composer prefill, so the prediction travels outside the AIR namespace:
```json
{
"sessionUpdate": "session_info_update",
"_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } }
}
```
The key names no vendor, so another client can read it.
The adapter sends it only to an AIR client; see the [compatibility rule](#compatibility-rule).
AIR shows the first non-blank `suggestions[].text` as gray text in an empty composer.
Tab fills the composer with it and sends nothing.
`suggestions: []` clears the prediction the client holds. The adapter clears it:
- when the next `session/prompt` arrives, because that turn's prediction is spent;
- on a `conversation_reset`, because the prediction was drawn from the transcript the reset dropped.
A prediction the next prompt outran — the user sent it before the adapter read the prediction off the SDK stream — is dropped instead of published.
The CLI predicts nothing on the first turn of a session, after an API error, in plan mode, and at the account's usage limit, so many turns end with no prediction.
A client must treat a turn without one as normal.
A caller can turn the whole feature off with `_meta.claudeCode.options.promptSuggestions: false` on `session/new`; a user can turn it off in Claude Code settings or with `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false`.
## Recommended config values
The `recommendedValue` extension lets a client show concrete model and effort choices without an ambiguous `default` row.
@@ -998,20 +936,6 @@ When an announced task leaves that list, the adapter reports `stopped`.
A later terminal event can correct that state to `completed` or `failed`.
At shutdown, the adapter finishes each open task as `failed` or `stopped`.
### Autonomous turn
Claude Code can start a turn of its own after the prompt ended, for example to answer the notification of a finished background task.
ACP v1 has no session state, so the adapter publishes such a turn as an async task.
The task has 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.
The task starts at the SDK `running` state while no prompt is active or queued.
When the state stays `running`, as it does while background agents run, it starts at the first root output with no prompt open.
It ends `completed` at the result of the turn or at the SDK `idle` state, and `failed` at an error result.
A coalesced placeholder result (`num_turns: 0`) does not end it.
A prompt that the turn takes in ends the task, because that prompt owns the turn now.
A stop request interrupts the turn instead of stopping an SDK task.
It returns `{ "stopped": false }` while a prompt waits for its turn, because the interrupt would drop that prompt.
### Stop request
The client sends `_session/async_task/stop`:
@@ -1283,8 +1207,8 @@ The session index makes `session/list` a bounded, ordered page with row metadata
AIR declares `sessionIndex` in `_meta.jetbrains.air.capabilities`.
Unlike the other capabilities, the agent advertises `sessionIndex` only to a client that declared it, and never under ACP v2.
Exactly when it advertises `sessionIndex`, the agent also advertises `sessionBatchGet` (it supports `_session/batch_get`), `sessionArchive` (it supports `_session/archive` and `_session/unarchive`), `sessionRename` (it supports `_session/rename`), `sessionListSubscribe` (it supports `_session/list/subscribe` and `_session/list/unsubscribe`) and `sessionWorktrees` (rows carry `agentWorktree`, see [List response](#list-response)).
The client does not declare these five; declaring them without `sessionIndex` enables nothing.
Exactly when it advertises `sessionIndex`, the agent also advertises `sessionArchive` (it supports `_session/archive` and `_session/unarchive`), `sessionRename` (it supports `_session/rename`) and `sessionListSubscribe` (it supports `_session/list/subscribe` and `_session/list/unsubscribe`).
The client does not declare these three; declaring them without `sessionIndex` enables nothing.
Everything in this section applies only to such a client, except where [Delete and close](#delete-and-close) says otherwise.
A client without the capability gets the `session/list` of before: the SDK `listSessions` call, pages of 1000 by file mtime, `offset:N` cursors, and no row `_meta`.
It gets no new notification, and the adapter starts no watcher for it.
@@ -1352,7 +1276,6 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
"lastPromptAt": "2026-10-06T09:57:40.000Z",
"model": "claude-opus-5-5",
"forkedFrom": "8f0c1d2e-0000-4000-8000-000000000000",
"agentWorktree": true,
"state": "idle",
"lastTurnEndedAt": "2026-10-06T09:58:10.000Z",
"cost": { "amount": 1.23, "currency": "USD" }
@@ -1362,19 +1285,10 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
- `lastPromptAt` is the time of the last real user prompt in the transcript tail: prompt text (the predicate of the SDK title extractor) or an image or a document, but not a tool result, a meta or compact summary record, a slash command or an interrupt. When a long answer or tool output follows the last prompt, the tail window grows (256 KB, 1 MB, 4 MB) to find it; it is omitted only when the last 4 MB hold no prompt.
- `model` is the model of the last assistant message in the tail; messages the CLI makes up (`<synthetic>`) do not count.
- `forkedFrom` is the parent session id that the SDK `forkSession` and the CLI fork write on every copied record (`forkedFrom.sessionId`). Sessions started any other way have none.
- `agentWorktree` is `true` when the session runs in a linked git worktree that Claude Code made for it (`claude --worktree`, or the worktree tools), and omitted otherwise, never `false`. The transcript rule is the one of AIR's native Claude provider; the adapter adds the git check of the row's `cwd`:
- the transcript holds `worktree-state` records, `{"type":"worktree-state","worktreeSession":{originalCwd, worktreePath, worktreeName, worktreeBranch, originalBranch, originalHeadCommit, sessionId, hookBased?, enteredExisting?}}`, which the CLI writes when the session enters or leaves a worktree;
- the last line that parses as a `worktree-state` record decides, whatever it holds: the session is marked only when its `worktreeSession` is an object without `enteredExisting: true` (`enteredExisting` means a worktree that existed before, so not made for the session). Any other `worktreeSession` (`null`, which the CLI writes when the session leaves the worktree, a missing key, or a value that is no object) means not marked, and an older record does not count then. Only lines that are not JSON, such as the cut first line of a window, are skipped;
- the row's `cwd` must lie in a linked worktree: the adapter reads the git files, without running git. It takes the nearest `.git` above `cwd`, follows a `.git` file to its git dir, reads `commondir`, and requires the git dir to be `<git-common-dir>/worktrees/<name>`. This is checked on every `session/list` and on every row a subscription reads, not cached. The main checkout, a submodule and a directory outside a repository never get the field, and neither does a worktree the user made (it has no record). The adapter does not use `environment.isWorktree` or `git worktree list`;
- `hookBased` is not read: a worktree a `WorktreeCreate` hook made is marked only when its directory is a linked git worktree as above, so a hook that makes a plain directory or a checkout of another VCS gives no field. The repository is the nearest one to `cwd`: a session in a repository nested in a linked worktree is not marked, and one in a linked worktree of a nested repository is;
- the record is read from the 64 KB tail, else from the 64 KB head, and from the wider tail when the window grows to find the last prompt (see `lastPromptAt`). `claude --worktree` writes it at the start of the session, and the CLI writes it again with the other session metadata at the end of the transcript (on exit and as the transcript grows), so one of these windows almost always holds the latest one. A session that entered or left a worktree in the middle of a long transcript, with no metadata written since, keeps its old value until the CLI writes the metadata again;
- a session that made a worktree mid-session (EnterWorktree) is marked too: the CLI moves its transcript to the worktree (a `relocated` record), so the row's `cwd` is the worktree path, not the directory the session started in. The row's `cwd` is always the session's own directory, so the worktree path;
- a subscription does not send a row again when only git metadata changes, such as a manual `git worktree remove` or `git worktree move`: the field is checked again only when the transcript changes, or when the rescan finds that the worktrees of the scope changed and resolves the rows again. `session/list` checks it on every read.
- `state` is `running`, `idle`, `requires_action`, or `error`, never `unknown`. `error` means the session is not running and its last turn ended with an error; a user cancel or interrupt is no error, and a new turn clears it:
- a session this connection runs: the SDK `session_state_changed` state; `error` instead of `idle` when its last turn failed (an error result, `is_error` or an `error_*` subtype, that was not a cancel). A session whose query failed here (not a cancel) is reported as any other session, but `error` until its transcript shows a prompt or a turn end after the failure, unless a busy or waiting interactive CLI holds it;
- a session no live Claude Code process holds: `idle`, or `error` when its last turn ended with an API error (an assistant record with `isApiErrorMessage: true`, also when the CLI's turn-end records follow it, with no prompt after it);
- a session that an interactive CLI holds (not an `sdk-*` entrypoint) whose registry status is newer than the transcript: `busy` and `shell` are `running`, `waiting` is `requires_action`, `idle` is `idle`, or `error` after such an API error;
- any other held session whose last turn ended with an open background task: `running`, also after 10 minutes, until the registry status is `idle`. Claude Code wakes the session again when the task ends, so the session gets one `idle` at the last turn end, not one for each task. A background shell command (`toolUseResult.backgroundTaskId`) or an async sub-agent (`isAsync` with an `agentId`) opens a task. Its `<task-notification>` with a `<status>`, as a user record or a `queue-operation` `remove`, or the result of a `TaskStop` call closes it. A notification without a `<status>`, such as the check-in of a quiet shell or a `Monitor` event, keeps it open. Sidechain records open no task. The tail gives the tasks on top of those of an earlier scan when the file only grew within the tail; else the whole transcript is read, and only for such a held session. A background task that a sub-agent starts is not seen: it is only in the transcript of the sub-agent;
- any other held session: `idle` after a finished turn (an assistant `end_turn`, a user interrupt, or the CLI turn-end records), `error` after a turn that ended with an API error, `running` for an unfinished turn written in the last 10 minutes, and no state otherwise.
- The live processes come from `<config>/sessions/<pid>.json`, with the liveness rules of Claude Desktop: `kill(pid, 0)` (or `EPERM`), a matching `procStart`, no foreign `pidDomain`, and records older than 24 hours only with a matching `procStart`.
The adapter never reads the `.key` files there and never changes the directory.
@@ -1443,71 +1357,6 @@ In all of the session index methods and in `session/delete`, a session id matche
Loading an archived session does not unarchive it.
The CLI cleanup still deletes old transcripts (`cleanupPeriodDays`), archived or not.
### Batch get
`_session/batch_get` refreshes metadata for retained session IDs, such as pinned
sessions and folder members outside the recent list page. Check for
`sessionBatchGet` in `initialize.result._meta.jetbrains.air.capabilities` before
calling it.
The params object must contain only `sessionIds`: an array of distinct,
non-blank strings. IDs match exactly, without trimming or case normalization.
Malformed params, duplicates or extra fields return `Invalid params` (`-32602`).
The agent sets no limit on the number of IDs; the client chooses the batch size
for the latency it accepts. Archived sessions and sessions in other projects
or worktrees are eligible.
```json
{
"jsonrpc": "2.0",
"id": 7,
"method": "_session/batch_get",
"params": { "sessionIds": ["oldA", "oldB", "oldC"] }
}
```
```json
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"sessions": [
{
"sessionId": "oldA",
"cwd": "/project",
"title": "Fix the build",
"updatedAt": "2026-01-01T00:00:00.000Z",
"_meta": { "jetbrains": { "air": { "version": 1, "archived": false } } }
}
]
}
}
```
The example IDs are placeholders for stored Claude session UUIDs. `sessions`
contains the same full rows as `session/list` without a project filter, including
archived sessions and `_meta.jetbrains.air`. Copies use the same
newest-readable-copy selection as the list. Returned sessions preserve their
relative request order. Non-UUID IDs and IDs without a listable transcript are
omitted: absent, empty, unreadable or malformed transcripts, sidechains, sessions
whose continuation has history, and transcripts whose cwd cannot be recovered.
A sidecar alone does not supply a row.
`{ "sessionIds": [] }` returns `{ "sessions": [] }` without reading the store.
A store-wide failure, such as an unreadable projects root, rejects the request.
Individual results reflect the store as each ID is read.
Lookup normally addresses only the requested transcript filenames in each project
and reads their metadata. A transcript without a cwd can use a sibling's cwd,
as the list does. This fallback enumerates and stats sibling transcripts only
in the directories of rows still missing a cwd, then reads at most 64 siblings
per directory, newest mtime first. A failed recovery is cached for 60 seconds
unless the directory mtime changes. Other project histories are not enumerated.
The agent looks up 100 IDs at a time; the client may request any number of IDs.
Merge returned rows with the recent page by `sessionId`. A batch read does not
expand an existing list subscription's scope; refresh retained IDs outside that
scope explicitly.
### List subscription
`_session/list/subscribe { "cwd": "…" }` returns `{ "subscriptionId": "…" }`.
@@ -1531,8 +1380,7 @@ From then on the agent sends `_session/list/changes` notifications for the sessi
- `cwd` is required and must be an absolute path; a missing, non-string or relative `cwd` is `-32602`. Other parameters are ignored.
- The scope is the sessions of `cwd` and of the same subdirectory in every existing linked worktree of its repository, as a list with `includeWorktrees: true` resolves it, in any archive state. Sidechain and subagent transcripts are left out, as in the list.
- There is no page or recent-session limit: changes to older pinned or folder sessions within that scope are reported even outside the page AIR loaded. Pinned IDs outside the subscribed project and worktrees are not tracked; `_session/batch_get` does not add them to the subscription. AIR must subscribe to their project scope or refresh them explicitly.
- `sessions` holds the full current row of each session in scope that appeared, or whose `title`, `lastPromptAt`, `state`, `lastTurnEndedAt`, `cost`, `model`, `forkedFrom`, `agentWorktree` or `archived` changed since the last row sent for it on this subscription. A row is exactly the row of `session/list`, `_meta.jetbrains.air` included.
- `sessions` holds the full current row of each session in scope that appeared, or whose `title`, `lastPromptAt`, `state`, `lastTurnEndedAt`, `cost`, `model`, `forkedFrom` or `archived` changed since the last row sent for it on this subscription. A row is exactly the row of `session/list`, `_meta.jetbrains.air` included.
- After subscribe, the first change of each session sends its row in full, even when only `updatedAt` changed. From then on, a change of `updatedAt` alone sends nothing; the new `updatedAt` comes with the next change.
- `removed` holds the ids of sessions that left the scope: deleted (the transcript is gone), or moved to a path outside it. It may name a session the client does not list (one it never listed, one the list hides, such as a session continued in another one when that one changes); the client ignores such an id.
- Archive and unarchive are row changes (`archived`): their title records are a transcript change. The agent applies no archive filter: the client filters.
@@ -1560,8 +1408,7 @@ It is idempotent, and an unknown id also returns `{}`.
- a `sessionIndex` client: deletes every transcript of the session (empty ones too) and every `<sessionId>/` directory, also one left without a transcript by an earlier failed delete. Anything left behind fails the request. A session that runs here is closed first. As with the CLI, the session is deleted also when another Claude Code process has it open. An unknown session is `-32002`.
- an AIR client without `sessionIndex`: archives the session instead of deleting it (the title records of [Archive](#archive)), because AIR uses delete for "Done" and can reopen the session. A session that runs here is closed first. The list hides archived sessions from such a client too: those whose custom title, as the SDK list reports it, is archived. A session without a transcript fails with the error of the SDK delete, as before.
A `sessionIndex` client sees such a session as archived (`archived: true`), as AIR's native Claude path does.
- any other client: the SDK delete. A session without a transcript (unknown, deleted before, or
never prompted) succeeds, as ACP asks.
- any other client: the SDK delete, as before.
`session/close` of a session that is not loaded returns `{}` for every client.
@@ -1579,23 +1426,21 @@ It is idempotent, and an unknown id also returns `{}`.
The session index is the `_meta` form of two ACP RFDs: "Session list extensions: limit, order, row metadata and change hints" and "Session Archive and Unarchive" (#2161).
Names and semantics follow them; the transport differs, and so do the `archived` list filter and the archive of a running session (see [Relation to ACP RFD #2161](#relation-to-acp-rfd-2161)):
| Extension | RFD |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| capability `sessionIndex` (`_meta.jetbrains.air`) | `sessionCapabilities.list.limit` |
| capability `sessionArchive` (`_meta.jetbrains.air`) | `sessionCapabilities.archive` (#2161) |
| capability `sessionRename` (`_meta.jetbrains.air`) | `sessionCapabilities.setTitle` (#1987) |
| capability `sessionBatchGet`, `_session/batch_get` | none |
| `_meta.jetbrains.air.list.limit` | `session/list` `limit` |
| `_meta.jetbrains.air.list.includeWorktrees` | `session/list` `includeWorktrees` |
| `_meta.jetbrains.air.list.archived` (`"unarchived"`, `"archived"`, `"all"`) | `session/list` `archived` (#2161), a boolean there |
| row `_meta.jetbrains.air.lastPromptAt`, `model`, `forkedFrom`, `state`, `lastTurnEndedAt`, `cost` | `SessionInfo` fields of the same names; the RFD `state` has no `error` |
| row `_meta.jetbrains.air.archived` | `SessionInfo.archived` (#2161) |
| `session_info_update` `_meta.jetbrains.air.archived` | `SessionInfoUpdate.archived` (#2161) |
| capability `sessionWorktrees`, row `_meta.jetbrains.air.agentWorktree` | none yet: proposed in the session list extensions RFD (agentclientprotocol/agent-client-protocol#2323) as `SessionInfo.agentWorktree`, with the capability named `agentWorktrees` there, not `sessionWorktrees` |
| capability `sessionListSubscribe`, `_session/list/subscribe`, `_session/list/unsubscribe` | none |
| `_session/list/changes { subscriptionId, sessions, removed }` | none |
| `_session/archive`, `_session/unarchive` | `session/archive`, `session/unarchive` (#2161) |
| `_session/rename` | `session/set_title` (#1987) |
| Extension | RFD |
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| capability `sessionIndex` (`_meta.jetbrains.air`) | `sessionCapabilities.list.limit` |
| capability `sessionArchive` (`_meta.jetbrains.air`) | `sessionCapabilities.archive` (#2161) |
| capability `sessionRename` (`_meta.jetbrains.air`) | `sessionCapabilities.setTitle` (#1987) |
| `_meta.jetbrains.air.list.limit` | `session/list` `limit` |
| `_meta.jetbrains.air.list.includeWorktrees` | `session/list` `includeWorktrees` |
| `_meta.jetbrains.air.list.archived` (`"unarchived"`, `"archived"`, `"all"`) | `session/list` `archived` (#2161), a boolean there |
| row `_meta.jetbrains.air.lastPromptAt`, `model`, `forkedFrom`, `state`, `lastTurnEndedAt`, `cost` | `SessionInfo` fields of the same names; the RFD `state` has no `error` |
| row `_meta.jetbrains.air.archived` | `SessionInfo.archived` (#2161) |
| `session_info_update` `_meta.jetbrains.air.archived` | `SessionInfoUpdate.archived` (#2161) |
| capability `sessionListSubscribe`, `_session/list/subscribe`, `_session/list/unsubscribe` | none |
| `_session/list/changes { subscriptionId, sessions, removed }` | none |
| `_session/archive`, `_session/unarchive` | `session/archive`, `session/unarchive` (#2161) |
| `_session/rename` | `session/set_title` (#1987) |
### Relation to ACP RFD #2161
@@ -1610,45 +1455,6 @@ It differs in transport, until the RFD lands in the SDK, in the list filter, and
- the list parameter is `_meta.jetbrains.air.list.archived`, and the state is `_meta.jetbrains.air.archived` on rows and in `session_info_update`, not the `archived` fields;
- an AIR client without `sessionIndex` still archives with `session/delete` (see [Delete and close](#delete-and-close)), which the RFD asks clients not to do.
## Background subagents
A prompt turn can start a background Agent or Task subagent.
A SendMessage tool call can also resume a subagent that ended.
Claude Code sends the result of the turn when the main agent stops, also while such a subagent still runs.
### Hold without the capability
A client that does not declare `backgroundSubagents` gets the hold of issues #864 and #866.
The adapter keeps the `session/prompt` request open while a subagent that the turn started or resumed runs.
The subagent output, its permission requests and the followup summary of the model then go out inside the turn.
The turn ends at the followup result, at an `idle` with no live subagent, at `session/cancel`, or when the next prompt starts.
The hold protects a client that stops listening at the prompt response.
### Behavior with the capability
AIR declares `backgroundSubagents` in `initialize.clientCapabilities._meta.jetbrains.air.capabilities`.
The capability applies to ACP v1 only. ACP v2 keeps the hold, and the agent does not advertise the capability to ACP v2.
- The turn ends at its result with the stop reason of the result, usually `end_turn`.
- The subagent updates, permission requests and elicitations go out after the prompt response.
- The routing does not change: the native child session, else the root session with the parent tool call in `_meta.claudeCode.parentToolUseId`.
- The followup cycle after a subagent ends is an autonomous cycle. Its updates go out of a turn. Its result does not end a turn.
- A steer with no turn in flight starts a new turn, or returns `promptRequired` when the client asks for it.
- `session/cancel` with no turn works as before: the adapter interrupts the SDK query and reports each native subagent session as cancelled.
- The tool call of a running subagent does not fail the incomplete tool call check of the turn.
- The [agent file-change report](#agent-file-change-report) of the turn does not include an edit that a subagent makes after the result.
- An open permission request of a subagent counts as an open request for the next turn and for the steer priority.
### Plan approval out of a turn
The followup cycle can call ExitPlanMode. AIR then gets the permission request out of a turn.
The request has no clear-context option, because the fresh query must continue an ACP turn.
"No, keep planning" stops only the followup cycle. There is no turn to end.
A prompt turn owns the request when the turn is active, or when Claude Code asks before the echo of its prompt.
Then the request has the clear-context option.
A turn that took over a followup cycle also owns the request of that cycle, and the accepted plan continues in that turn.
## Context compaction
The adapter implements the ACP session compaction RFD for a client that declares `clientCapabilities.session.compaction`.
@@ -1704,32 +1510,6 @@ For AIR, the companion field also carries the same value under the root key `_me
Released AIR versions read only that key.
A client that is not AIR gets neither key.
## Load replay end
ACP has no signal that the history replay of `session/load` is over, other than the response.
The adapter replays the transcript while the Claude Code CLI starts, so the history can arrive long before the response.
The client declares `loadReplayEnd` in its capabilities. The agent does not advertise it.
After the history replay of the loaded session, the agent then sends one more update for that session:
```json
{
"sessionId": "<loaded session>",
"update": {
"sessionUpdate": "session_info_update",
"_meta": { "jetbrains": { "air": { "version": 1, "loadReplayEnd": true } } }
}
}
```
- The update has no `title` and no `updatedAt`. A `null` there would clear the value.
- It comes after every replayed update, the subagent history that the replay sends to the child sessions included, and before the `session/load` response.
- It comes also for an empty history.
- It comes for `session/load` of a new or an already live session, and for the ACP v2 `session/resume` with `replayFrom: {type: "start"}`.
- It does not come when the replay stops or fails. The replay stops when the CLI start fails during the replay.
- It does not come for a live update, and it does not come to a client without the capability.
- The `session/load` can still fail after it, for example when the CLI start fails after the replay. The event means that the recorded history arrived whole. The client decides what to do with the failed load.
## Session fork point
AIR can fork a session at one agent message.

View File

@@ -54,14 +54,3 @@ An ACP caller can provide `settings` via `_meta.claudeCode.options.settings` in
- The value must be valid JSON. Invalid JSON will cause session creation to fail with a parse error.
- Only `modelOverrides` and `availableModels` keys are read; other keys in the JSON are ignored.
- Both fields map directly to the Claude Agent SDK's `Settings` type.
## Models of a `providers/set` gateway
When an ACP client routes the agent through a gateway with `providers/set`, the adapter lists the gateway's models before `providers/set` returns, with the provider headers:
- `anthropic`: `GET {baseUrl}/v1/models`, paginated by `has_more`/`last_id`.
- `bedrock`: `GET {baseUrl}/foundation-models` (Bedrock `ListFoundationModels`, `modelSummaries[].modelId`). Only `anthropic.` ids count. The cross-region prefix `global` when listed, else that of the first listed id with one (`eu` for `eu.anthropic.claude-sonnet-5`), becomes the session's `ANTHROPIC_BEDROCK_REGION_PREFIX`, and ids with other prefixes, or without one, are dropped.
The model picker then offers only the SDK models whose resolved model the gateway lists, matched without dates, version suffixes (`-v1:0`), `-fast`, `@` suffixes, context hints (`[1m]`) and provider prefixes, after the `availableModels` allowlist above. On Bedrock, a model the CLI resolved to another region prefix is dropped as well. When the gateway does not list the model a session runs, that is the default model or a resumed session's last model, the session switches to the first remaining picker row.
A 4xx answer means the gateway has no listing: `providers/set` succeeds and nothing is narrowed. A failed listing (network error, a status other than 2xx or 4xx, unexpected body, the 10 s timeout, an invalid header, or a base URL with credentials) fails `providers/set` with a JSON-RPC internal error, and the previous provider stays in effect. A listing that matches no SDK model leaves the session with no model to offer: there is no model config option. Gateways set through `authenticate`, `vertex` providers, and native routing are not listed. Setting the same gateway again (the same protocol, base URL and headers) reuses its listing, unless it answered 4xx; `providers/set` and `providers/disable` apply in the order they arrive, and logout clears the routing after `claude auth logout`, including a `providers/set` still listing; `providers/disable` and logout drop it.

View File

@@ -82,10 +82,10 @@ import {
client as acpClient,
methods,
ndJsonStream,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
// Type-only: Node's type stripping does not elide value imports, and these two
// exist only in the .d.ts — importing them normally is a runtime SyntaxError.
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk/unstable";
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk";
// ---------------------------------------------------------------- steering

View File

@@ -46,7 +46,7 @@ import {
ndJsonStream,
type PromptRequest,
type PromptResponse,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
/** The steering extension method, per the ACP steering wire protocol. */
const STEERING_METHOD = "_session/steering";

750
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.98.0",
"version": "0.89.1",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -65,25 +65,26 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.0.2",
"@anthropic-ai/claude-agent-sdk": "0.3.310",
"@agentclientprotocol/sdk": "1.8.0",
"@anthropic-ai/claude-agent-sdk": "0.3.293",
"diff": "9.0.0",
"zod": "4.6.7"
"zod": "4.6.5"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.3.0",
"@anthropic-ai/sdk": "0.132.1",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.72.0",
"@typescript-eslint/parser": "8.72.0",
"@types/node": "26.6.4",
"@typescript-eslint/eslint-plugin": "8.71.1",
"@typescript-eslint/parser": "8.71.1",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",
"eslint": "10.12.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"prettier": "3.9.10",
"prettier": "3.9.9",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "5.0.5"
"vitest": "5.0.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import type {
ClientCapabilities,
SessionCapabilities,
SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
import {
AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY,
clientSupportsAirCapability,
@@ -17,8 +17,7 @@ export { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY } from "./air-extension.js";
* - `rfd`: the subagents RFD (agentclientprotocol/agent-client-protocol#1992,
* as revised): `subagent_update` and `session_message`, which the SDK types.
* A client that declares `clientCapabilities.subagents` and is not AIR gets
* it. (An ACP v2 client gets it without declaring anything, since subagents
* are part of v2's baseline.)
* it. (ACP v2 clients do not get subagents yet.)
* - `air`: the earlier draft of that RFD, which AIR implements:
* `subagent_spawned` and `subagent_state_update`, typed below. AIR gets it,
* whichever capability it declares (`docs/air-extensions.md`).

View File

@@ -1,10 +1,5 @@
export const AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY = "nativeSubagentSessions";
export const AIR_ASYNC_TASKS_CAPABILITY = "asyncTasks";
/**
* A prompt turn ends at its result also while its background subagents run.
* The subagent updates and requests then go out after the prompt response.
*/
export const AIR_BACKGROUND_SUBAGENTS_CAPABILITY = "backgroundSubagents";
export const AIR_SESSION_FAILURE_CAPABILITY = "sessionFailure";
export const AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY = "recommendedValue";
export const AIR_CUSTOM_INSTRUCTIONS_CAPABILITY = "customInstructions";
@@ -22,9 +17,6 @@ export const AIR_PLAN_FILE_CAPABILITY = "planFile";
* subscription. Advertised only to a client that declares it.
*/
export const AIR_SESSION_INDEX_CAPABILITY = "sessionIndex";
/** Metadata lookup of known ids with `_session/batch_get`. Advertised
* exactly when {@link AIR_SESSION_INDEX_CAPABILITY} is negotiated. */
export const AIR_SESSION_BATCH_GET_CAPABILITY = "sessionBatchGet";
/**
* The agent supports `_session/archive` and `_session/unarchive`. Advertised
* exactly when {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not
@@ -42,26 +34,11 @@ export const AIR_SESSION_RENAME_CAPABILITY = "sessionRename";
* {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
*/
export const AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY = "sessionListSubscribe";
/**
* Session index rows carry `agentWorktree: true` when the session runs in a
* linked git worktree that Claude Code made for it. Advertised exactly when
* {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
*/
export const AIR_SESSION_WORKTREES_CAPABILITY = "sessionWorktrees";
/**
* The client wants to know when the history replay of a loaded session is
* whole. After the last replayed update, the agent sends one
* `session_info_update` with `_meta.jetbrains.air.loadReplayEnd: true` (see
* {@link AIR_LOAD_REPLAY_END_KEY}). The client declares it; the agent does not
* advertise it.
*/
export const AIR_LOAD_REPLAY_END_CAPABILITY = "loadReplayEnd";
/** The `_meta.jetbrains.air` keys that the ACP tool call contract defines. */
export const AIR_COMMAND_TITLE_KEY = "commandTitle";
export const AIR_SUBAGENT_KEY = "subagent";
export const AIR_SKILL_KEY = "skill";
export const AIR_MCP_KEY = "mcp";
export const AIR_CONTEXT_COMPACTION_KEY = "contextCompaction";
export const AIR_GOAL_KEY = "goal";
export const AIR_KIND_KEY = "kind";
@@ -69,8 +46,6 @@ export const AIR_KIND_KEY = "kind";
export const AIR_SKILL_PATH_KEY = "skillPath";
export const AIR_PERMISSION_KEY = "permission";
export const AIR_CUSTOM_ANSWER_KEY = "customAnswer";
/** The `_meta.jetbrains.air` key of the update that ends a whole history replay. */
export const AIR_LOAD_REPLAY_END_KEY = "loadReplayEnd";
/**
* The root `_meta` spelling of {@link AIR_CUSTOM_ANSWER_KEY} that released
* AIR versions read. Remove it when the ACP registry no longer serves AIR

View File

@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import type { ClientCapabilities } from "@agentclientprotocol/sdk";
import type { AcpSessionNotification, AsyncTaskState } from "./acp-subagents.js";
import { AIR_ASYNC_TASKS_CAPABILITY, clientSupportsAirCapability } from "./air-extension.js";
import { noticeOrTranscriptUpdate } from "./session-notices.js";
@@ -15,8 +14,6 @@ type AsyncTask = {
taskType: string;
description: string;
showInTranscript: boolean;
/** A Monitor tool call or an SDK `local_monitor` task: published as a monitor the client cannot stop. */
monitor: boolean;
outputFilePath?: string;
toolCallId?: string;
/**
@@ -42,12 +39,6 @@ type AsyncTask = {
terminalSource?: TerminalSource;
/** The optional fields that the client holds now, as JSON. */
published: Map<string, string>;
/**
* Whether the task stands for a model turn that Claude Code started on its
* own, see {@link AsyncTaskRuntime.autonomousTurnStarted}. The SDK knows no
* such task, so its background task list never ends it.
*/
autonomousTurn: boolean;
};
type TaskIdentity = { taskId?: unknown; task_id?: unknown };
@@ -130,10 +121,6 @@ export class AsyncTaskRuntime {
* The oldest entries are dropped.
*/
private readonly outputPathsByToolCall = new Map<string, string[]>();
/** The task of the autonomous turn that runs now. */
private autonomousTurn?: AsyncTask;
/** The summary of the last task notification, which usually starts the next autonomous turn. */
private lastNotificationSummary?: string;
constructor(
readonly enabled: boolean,
@@ -144,8 +131,7 @@ export class AsyncTaskRuntime {
* the tasks that a tool call in another session started, for example in
* a native subagent session. `toolNameOf`: the name of the tool of a tool
* call, or `undefined` when the tool call is unknown. A task that a
* `Monitor` tool call started is published as a monitor: the SDK reports
* it as `local_bash`, so the adapter names it a monitor. */
* `Monitor` tool call started is ignored. */
private readonly options: {
notices?: boolean;
routeOf?: (toolCallId: string) => Route | undefined;
@@ -158,7 +144,7 @@ export class AsyncTaskRuntime {
const taskId = taskIdOf(message);
if (!taskId) return;
const task = this.task(taskId);
if (isSubagentTask(message)) {
if (isSubagentTask(message) || isMonitorTask(message)) {
this.ignore(task);
return;
}
@@ -189,7 +175,7 @@ export class AsyncTaskRuntime {
const taskId = taskIdOf(message);
if (!taskId) return;
const task = this.task(taskId);
if (isSubagentTask(message)) {
if (isSubagentTask(message) || isMonitorTask(message)) {
this.ignore(task);
return;
}
@@ -314,7 +300,6 @@ export class AsyncTaskRuntime {
const state = taskState(message.status);
if (!taskId || !state || state === "running" || state === "paused") return;
this.lastNotificationSummary = nonBlankString(message.summary) ?? this.lastNotificationSummary;
const task = this.task(taskId);
const wasTerminal = isTerminal(task.state);
const correctsLevelState = wasTerminal && task.terminalSource === "level";
@@ -358,7 +343,7 @@ export class AsyncTaskRuntime {
const task = this.task(taskId);
if (task.ignored || isTerminal(task.state)) continue;
const levelTaskType = field(item, "taskType", "task_type");
if (levelTaskType === "local_agent") {
if (levelTaskType === "local_agent" || levelTaskType === "local_monitor") {
this.ignore(task);
continue;
}
@@ -378,7 +363,6 @@ export class AsyncTaskRuntime {
if (
(task.announced || task.held) &&
!task.ignored &&
!task.autonomousTurn &&
!isTerminal(task.state) &&
!live.has(task.id)
) {
@@ -394,7 +378,6 @@ export class AsyncTaskRuntime {
}
async finishAll(state: Extract<AsyncTaskState, "failed" | "stopped">): Promise<void> {
this.autonomousTurn = undefined;
const errors: unknown[] = [];
for (const task of this.tasks.values()) {
if ((task.announced || task.held) && !task.ignored && !isTerminal(task.state)) {
@@ -411,13 +394,7 @@ export class AsyncTaskRuntime {
canStop(taskId: string): boolean {
const task = this.tasks.get(taskId);
return (
task?.announced === true &&
!task.monitor &&
!task.ignored &&
!task.stopping &&
!isTerminal(task.state)
);
return task?.announced === true && !task.ignored && !task.stopping && !isTerminal(task.state);
}
claimStop(taskId: string): boolean {
@@ -435,7 +412,6 @@ export class AsyncTaskRuntime {
const task = this.tasks.get(taskId);
if (!task || !task.announced || task.ignored || task.stopAnnounced) return;
task.stopAnnounced = true;
if (this.autonomousTurn === task) this.autonomousTurn = undefined;
// No terminal summary: a stopped task leaves the Async Tasks panel at once,
// so anything said there is said to nobody.
await this.finish(task, "stopped", undefined, "event");
@@ -462,54 +438,6 @@ export class AsyncTaskRuntime {
);
}
/**
* Publishes a model turn that Claude Code started with no prompt open, for
* example to answer a task notification. ACP v1 has no session state, and
* the client already got the stop reason of its last prompt, so this task is
* the only sign that the agent works. The tool calls of the turn are in the
* transcript already, so the task is not shown there.
*
* [cause] names what started the turn when Claude Code says so, for example
* "A scheduled wakeup fired." It replaces the description of a task that is
* already open; without it, an open task is left as it is.
*/
async autonomousTurnStarted(cause?: string): Promise<void> {
if (!this.enabled) return;
if (this.autonomousTurn) {
if (cause) await this.publishProgress(this.autonomousTurn, { description: cause });
return;
}
const task = this.task(`autonomous-turn-${randomUUID()}`);
task.autonomousTurn = true;
task.startedObserved = true;
task.name = "Claude is working";
task.taskType = "turn";
task.description =
cause ?? this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
task.showInTranscript = false;
this.lastNotificationSummary = undefined;
this.autonomousTurn = task;
await this.announce(task, { withoutToolCall: true });
}
/** Whether the task of an autonomous turn is open. */
get autonomousTurnOpen(): boolean {
return this.autonomousTurn !== undefined;
}
/** Ends the task of the autonomous turn that runs now, if there is one. */
async autonomousTurnEnded(state: "completed" | "failed"): Promise<void> {
const task = this.autonomousTurn;
if (!task) return;
this.autonomousTurn = undefined;
await this.finish(task, state, undefined, "event");
}
/** Whether the task stands for an autonomous turn, which a stop interrupts. */
isAutonomousTurn(taskId: string): boolean {
return this.tasks.get(taskId)?.autonomousTurn === true;
}
/** Whether a task of the tool call went to the background, so the client gets it. */
isBackgroundedToolCall(toolCallId: string): boolean {
for (const task of this.tasks.values()) {
@@ -560,8 +488,6 @@ export class AsyncTaskRuntime {
clear(): void {
this.tasks.clear();
this.outputPathsByToolCall.clear();
this.autonomousTurn = undefined;
this.lastNotificationSummary = undefined;
}
/**
@@ -584,7 +510,6 @@ export class AsyncTaskRuntime {
taskType: "task",
description: "Background task",
showInTranscript: true,
monitor: false,
announced: false,
held: false,
deferred: [],
@@ -595,7 +520,6 @@ export class AsyncTaskRuntime {
stopAnnounced: false,
state: "running",
published: new Map(),
autonomousTurn: false,
};
this.tasks.set(taskId, task);
return task;
@@ -605,7 +529,6 @@ export class AsyncTaskRuntime {
task.startedObserved = true;
const rawTaskType = field(message, "taskType", "task_type");
if (rawTaskType !== undefined) task.taskType = friendlyTaskType(rawTaskType);
if (rawTaskType === "local_monitor") task.monitor = true;
const description = nonBlankString(message.description);
if (description) task.description = description;
else if (task.description === "Background task")
@@ -663,7 +586,6 @@ export class AsyncTaskRuntime {
private mergeLevel(task: AsyncTask, level: BackgroundTaskLevel): void {
const rawTaskType = field(level, "taskType", "task_type");
if (rawTaskType !== undefined) task.taskType = friendlyTaskType(rawTaskType);
if (rawTaskType === "local_monitor") task.monitor = true;
const description = nonBlankString(level.description);
if (description) {
task.description = description;
@@ -688,29 +610,25 @@ export class AsyncTaskRuntime {
* ends first, or when the prompt result ends the model turn
* ({@link releaseHeld}). No guess by the command text binds a task.
*
* A task that a `Monitor` tool call started is published as a monitor. The
* SDK reports it as `local_bash`, so the adapter names it a monitor instead of
* a shell. It has no transcript card because Monitor streams its output to
* the model only, and no Stop because stopping it only ends the agent's wait
* (the agent's own TaskStop still works). The tool_use streams before the
* tool runs, so the tool name is known when the spawn has the tool call id.
* A task that a `Monitor` tool call started is ignored: Monitor streams its
* output to the model only, so the task has nothing for the client. The
* tool_use streams before the tool runs, so the tool name is known when the
* spawn has the tool call id.
*/
private async announce(
task: AsyncTask,
options: { withoutToolCall?: boolean } = {},
): Promise<void> {
if (task.announced || task.ignored) return;
const toolName = task.toolCallId ? this.options.toolNameOf?.(task.toolCallId) : undefined;
if (toolName !== undefined && HIDDEN_TASK_TOOLS.has(toolName)) {
this.ignore(task);
return;
}
if (!task.toolCallId && !options.withoutToolCall && !isTerminal(task.state)) {
task.held = true;
return;
}
if (task.toolCallId && this.options.toolNameOf?.(task.toolCallId) === "Monitor") {
task.monitor = true;
}
if (task.monitor) {
task.taskType = "monitor";
task.showInTranscript = false;
}
if (task.toolCallId) task.route ??= this.options.routeOf?.(task.toolCallId);
await this.send(task, {
sessionUpdate: "async_task_spawned",
@@ -719,7 +637,7 @@ export class AsyncTaskRuntime {
taskType: task.taskType,
description: task.description,
showInTranscript: task.showInTranscript,
canStop: !task.monitor,
canStop: true,
...(task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
...(task.toolCallId ? { toolCallId: task.toolCallId } : {}),
});
@@ -1067,6 +985,13 @@ function isBackgroundTask(isBackgrounded: unknown, taskType: unknown): boolean {
return taskType !== "local_bash" && taskType !== "local_agent";
}
/** The tools whose async tasks the client never sees. */
const HIDDEN_TASK_TOOLS = new Set(["Monitor"]);
function isMonitorTask(message: AsyncTaskStarted): boolean {
return field(message, "taskType", "task_type") === "local_monitor";
}
function isSubagentTask(message: AsyncTaskStarted): boolean {
return (
field(message, "subagentType", "subagent_type") !== undefined ||

View File

@@ -103,13 +103,6 @@ const NOT_LOGGED_IN: AuthStatus = {
label: "Not logged in",
};
const API_KEY_SOURCE_DETAILS = new Map([
["apiKeyHelper", "API key helper"],
["env", "env"],
["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"],
["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN"],
]);
/** Display names for the non-firstParty backends, where auth lives outside the
* agent (AWS credentials, gcloud ADC, …). */
const EXTERNAL_PROVIDER_LABELS: Record<string, string> = {
@@ -169,10 +162,7 @@ export function accountInfoHasIdentitySignal(account: AccountInfo | undefined):
/** Session-time mapping from the SDK's `AccountInfo`, the richest source when
* it is populated. Returns `undefined` when the account carries no identity
* signal, so callers keep the status they already know. */
export function fromAccountInfo(
account: AccountInfo | undefined,
apiKeyHelper?: string,
): AuthStatus | undefined {
export function fromAccountInfo(account: AccountInfo | undefined): AuthStatus | undefined {
if (!account || !accountInfoHasIdentitySignal(account)) {
return undefined;
}
@@ -182,14 +172,13 @@ export function fromAccountInfo(
apiKeySource: account.apiKeySource,
email: account.email,
organization: account.organization,
apiKeyHelper,
});
}
/** Pre-session mapping from `claude auth status --json` stdout. Returns
* `undefined` when the output is not the expected JSON object, so callers can
* report "not known" instead of guessing. */
export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus | undefined {
export function fromCliStatus(stdout: string): AuthStatus | undefined {
let parsed: unknown;
try {
parsed = JSON.parse(stdout);
@@ -216,7 +205,6 @@ export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus
apiKeySource: status.apiKeySource,
email: status.email,
organization: status.orgName,
apiKeyHelper,
});
}
@@ -236,7 +224,6 @@ function mapAuthFields(fields: {
apiKeySource?: string;
email?: string;
organization?: string;
apiKeyHelper?: string;
}): AuthStatus {
const { apiProvider, subscriptionType, apiKeySource, email, organization } = fields;
@@ -250,19 +237,11 @@ function mapAuthFields(fields: {
};
}
if (apiKeySource) {
if (
apiKeySource === "apiKeyHelper" &&
typeof fields.apiKeyHelper === "string" &&
/\bcentral\b/i.test(fields.apiKeyHelper) &&
/\bproxy\s+start\b/i.test(fields.apiKeyHelper)
) {
return { kind: "gateway", label: "JetBrains Air Gateway" };
}
const detail = API_KEY_SOURCE_DETAILS.get(apiKeySource);
return {
kind: "api_key",
label: "Anthropic API key",
...(detail ? { detail } : {}),
// The source ("apiKeyHelper", "env", …) is the specifics line.
detail: apiKeySource,
};
}
if (subscriptionType) {

View File

@@ -1,4 +1,4 @@
import type { NewSessionRequest, SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { NewSessionRequest, SessionConfigOption } from "@agentclientprotocol/sdk";
import type {
EffortLevel,
Options,
@@ -39,13 +39,11 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
configOptions: SessionConfigOption[];
currentAgent: string;
fastModeEnabled: boolean;
ultracodeEnabled: boolean;
effortPinnedLevel?: string;
activeTurn?: Turn | null;
turnQueue?: Turn[];
pendingExitPlanContextReset?: ClearContextReset;
contextUsedTokens?: number;
sentUserUuids?: Set<string>;
input: { push(message: SDKUserMessage): unknown };
};
@@ -60,7 +58,6 @@ export type ClearContextCoordinatorHost<
options: { publicSessionId: string; permissionMode: PermissionMode },
): Promise<Session>;
applyFastMode(session: Session, enabled: boolean): Promise<void>;
applyUltracode(session: Session, enabled: boolean): Promise<void>;
publishSessionState(
sessionId: string,
mode: PermissionMode,
@@ -148,16 +145,6 @@ export async function continuePlanInFreshContext<
}
assertRestartActive(freshSession);
}
// The fresh query seeds ultracode from settings only, so a toggle made
// during the old session would otherwise be lost.
if (oldSession.ultracodeEnabled !== freshSession.ultracodeEnabled) {
try {
await host.applyUltracode(freshSession, oldSession.ultracodeEnabled);
} catch (error) {
host.logError("Failed to restore ultracode after clearing context:", error);
}
assertRestartActive(freshSession);
}
oldSession.activeTurn = null;
oldSession.turnQueue = [];
@@ -171,7 +158,6 @@ export async function continuePlanInFreshContext<
}
assertRestartActive(freshSession);
(freshSession.sentUserUuids ??= new Set()).add(turn.promptUuid);
freshSession.input.push(host.continuationMessage(sessionId, reset.plan, turn.promptUuid));
assertRestartActive(freshSession);
host.ensureConsumer(freshSession, sessionId);

View File

@@ -1,74 +0,0 @@
/**
* The client-provided system prompt of the unstable `systemPrompt` field on
* `session/new`, `session/load`, `session/resume`, and `session/fork`.
*
* - `append` keeps the Claude Code preset and adds the text after it, the
* SDK's `{ type: "preset", preset: "claude_code", append }`.
* - `override` replaces the Claude Code preset with the text, the SDK's custom
* string prompt. Tools, MCP servers, permissions, and settings (including the
* CLAUDE.md files that `settingSources` loads) are unchanged.
*
* The CLI records the system prompt of a conversation on its first request
* and reuses it on resume, so a different `systemPrompt` on a later
* `session/resume` takes effect at the next compaction. The RFD allows this
* deferral.
*/
import type { Options } from "@anthropic-ai/claude-agent-sdk";
import {
RequestError,
type SessionCapabilities,
type SystemPrompt,
} from "@agentclientprotocol/sdk/unstable";
/** The `sessionCapabilities.systemPrompt` the agent advertises: both modes. */
export const SYSTEM_PROMPT_CAPABILITY = {
mode: { override: {} },
} satisfies NonNullable<SessionCapabilities["systemPrompt"]>;
export type ClientSystemPrompt = { mode: "append" | "override"; text: string };
/**
* The mode and text of a `systemPrompt`, or `undefined` when the request has
* none. Throws `invalid_params` for a mode other than `append` and `override`
* and for a content block other than text: the RFD requires an error, and no
* session, for a prompt the agent cannot apply.
*/
export function clientSystemPrompt(
systemPrompt: SystemPrompt | null | undefined,
): ClientSystemPrompt | undefined {
if (systemPrompt == null) return undefined;
const { mode, content } = systemPrompt;
if (mode !== "append" && mode !== "override") {
throw RequestError.invalidParams(
{ systemPrompt: { mode } },
`Unsupported systemPrompt mode: ${mode}`,
);
}
const text: string[] = [];
for (const block of content) {
if (block.type !== "text") {
throw RequestError.invalidParams(
{ systemPrompt: { content: { type: block.type } } },
`systemPrompt content must be text, but received a ${block.type} block`,
);
}
text.push(block.text);
}
return { mode, text: text.join("\n\n") };
}
/**
* The SDK `systemPrompt` option for a client system prompt. `customInstructions`
* (AIR's `customInstructions` on `session/new` metadata) still follows the
* client text in both modes.
*/
export function sdkSystemPrompt(
prompt: ClientSystemPrompt,
customInstructions: string | undefined,
): NonNullable<Options["systemPrompt"]> {
const text = [prompt.text, customInstructions].filter((part) => part).join("\n\n");
if (prompt.mode === "override") return text;
return text
? { type: "preset", preset: "claude_code", append: text }
: { type: "preset", preset: "claude_code" };
}

View File

@@ -1,4 +1,4 @@
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
import {
ContextCompactionMetadata,
createContextCompactionMeta,

View File

@@ -1,5 +1,5 @@
import { ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk/unstable";
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
import { ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk";
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { structuredPatch } from "diff";
import { constants } from "node:fs";
import { type FileHandle, open } from "node:fs/promises";

View File

@@ -1,12 +1,12 @@
import { randomUUID } from "node:crypto";
import { CreateElicitationResponse } from "@agentclientprotocol/sdk/unstable";
import { CreateElicitationResponse } from "@agentclientprotocol/sdk";
import type {
CreateElicitationRequest,
ElicitationAcceptAction,
ElicitationPropertySchema,
ElicitationSchema,
EnumOption,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
import type { ElicitationRequest, ElicitationResult } from "@anthropic-ai/claude-agent-sdk";
import type {
AskUserQuestionInput,

View File

@@ -1,4 +1,4 @@
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { SessionNotification } from "@agentclientprotocol/sdk";
import type { SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
import {
type ClearContextCoordinatorHost,

View File

@@ -19,10 +19,6 @@ export type FileChangeReportTurnState = {
export type NativeFileChangeReportTurn = {
promptUuid: string;
fileChangeReport?: FileChangeReportTurnState;
/** Raw `file_path`/`notebook_path` values of the file-tool edits that
* subagents of this turn completed. Checkpoints do not track files that
* only a subagent edited. */
editedPaths?: Set<string>;
};
export type FileChangeReportWorkspace = {
@@ -98,41 +94,6 @@ export function agentFileChangeReportMeta(
return withAirMeta(undefined, AGENT_FILE_CHANGE_REPORT_CAPABILITY, result);
}
const FILE_EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
/**
* Adds to `turn.editedPaths` the `file_path` (`notebook_path` for NotebookEdit)
* of each successful file-tool edit among the tool results of a subagent
* message. Root-agent messages (`parentToolUseId === null`) are skipped: the
* checkpoint already tracks their files. Must run before `toAcpNotifications`
* drops the tool use cache entries.
*/
export function recordSubagentFileEdits(
turn: (NativeFileChangeReportTurn & { settled: boolean }) | null | undefined,
parentToolUseId: string | null,
content: unknown,
toolUses: Record<string, { name: string; input: unknown } | undefined>,
): void {
if (parentToolUseId === null || !turn || turn.settled || !turn.fileChangeReport) return;
if (!Array.isArray(content)) return;
for (const block of content) {
if (
block?.type !== "tool_result" ||
block.is_error === true ||
typeof block.tool_use_id !== "string"
) {
continue;
}
const toolUse = toolUses[block.tool_use_id];
if (!toolUse || !FILE_EDIT_TOOLS.has(toolUse.name)) continue;
const input = toolUse.input as Record<string, unknown> | undefined;
const editedPath = toolUse.name === "NotebookEdit" ? input?.notebook_path : input?.file_path;
if (typeof editedPath === "string" && editedPath.length > 0) {
(turn.editedPaths ??= new Set()).add(editedPath);
}
}
}
export function createNativeFileChangeReporter(
options: NativeFileChangeReporterOptions,
): NativeFileChangeReporter {
@@ -140,7 +101,7 @@ export function createNativeFileChangeReporter(
const timeoutMs = options.timeoutMs ?? AGENT_FILE_CHANGE_REPORT_TIMEOUT_MS;
const requestIds = new Set<string>();
async function publish(state: FileChangeReportTurnState, result: AgentFileChangeReportResult) {
const publish = async (state: FileChangeReportTurnState, result: AgentFileChangeReportResult) => {
if (state.phase === "finished") return;
state.phase = "finished";
try {
@@ -148,55 +109,24 @@ export function createNativeFileChangeReporter(
} catch (error) {
options.logError(`Failed to publish file-change report ${state.requestId}: ${error}`);
}
}
};
async function finishUnavailable(
const finishUnavailable = async (
state: FileChangeReportTurnState,
reason: FileChangeReportUnavailableReason,
) {
) => {
await publish(state, {
version: 1,
requestId: state.requestId,
status: "unavailable",
reason,
});
}
};
async function publishReported(state: FileChangeReportTurnState, reportedPaths: string[]) {
const normalized = normalizeReportedPaths(reportedPaths, workspace);
await publish(
state,
fitReportedAgentFileChangeReport({
version: 1,
requestId: state.requestId,
status: "reported",
paths: normalized.paths,
// Checkpoints miss Bash mutations. Subagent file-tool edits are added
// from the message stream, but the list is still not exhaustive.
declaredComplete: false,
truncated: normalized.truncated,
}),
);
}
// Used when the checkpoint preview fails: the subagent edits seen in the
// stream are still worth reporting.
async function finishWithoutPreview(
state: FileChangeReportTurnState,
editedPaths: Set<string> | undefined,
reason: FileChangeReportUnavailableReason,
) {
if (editedPaths && editedPaths.size > 0) {
await publishReported(state, [...editedPaths]);
} else {
await finishUnavailable(state, reason);
}
}
async function report(
const report = async (
turn: NativeFileChangeReportTurn | null | undefined,
query: Pick<Query, "rewindFiles">,
) {
) => {
const state = turn?.fileChangeReport;
if (!state || state.phase !== "requested") return;
state.phase = "collecting";
@@ -215,22 +145,35 @@ export function createNativeFileChangeReporter(
}),
]);
if (preview === null) {
await finishWithoutPreview(state, turn.editedPaths, "timeout");
await finishUnavailable(state, "timeout");
return;
}
if (!preview.canRewind || !Array.isArray(preview.filesChanged)) {
await finishWithoutPreview(state, turn.editedPaths, "invalidOutput");
await finishUnavailable(state, "invalidOutput");
return;
}
await publishReported(state, [...preview.filesChanged, ...(turn.editedPaths ?? [])]);
const normalized = normalizeReportedPaths(preview.filesChanged, workspace);
await publish(
state,
fitReportedAgentFileChangeReport({
version: 1,
requestId: state.requestId,
status: "reported",
paths: normalized.paths,
// Checkpoints cover Claude file tools, but not every mutation source
// (notably Bash and most subagents), so this list is never exhaustive.
declaredComplete: false,
truncated: normalized.truncated,
}),
);
} catch (error) {
options.logError(`Failed to inspect Claude file checkpoint ${turn.promptUuid}: ${error}`);
await finishWithoutPreview(state, turn.editedPaths, "providerError");
await finishUnavailable(state, "providerError");
} finally {
if (timer) clearTimeout(timer);
}
}
};
return {
request(meta) {

View File

@@ -1,8 +1,4 @@
import {
ForkSessionRequest,
ForkSessionResponse,
RequestError,
} from "@agentclientprotocol/sdk/unstable";
import { ForkSessionRequest, ForkSessionResponse, RequestError } from "@agentclientprotocol/sdk";
import {
forkSession as forkClaudeSession,
getSessionMessages,

View File

@@ -1,223 +0,0 @@
/**
* Model discovery for a gateway set via `providers/set`.
*
* An organization can restrict which models its gateway serves, and the
* gateway's listing returns only those: `GET /v1/models` for an `anthropic`
* gateway, `GET /foundation-models` (Bedrock `ListFoundationModels`) for a
* `bedrock` one. The adapter lists them once when the provider is set, and the
* model picker then offers only the SDK models the gateway listed (see
* `narrowToGatewayModels` in `session-model.ts`). A listing that fails fails
* `providers/set` with a {@link GatewayListingError}.
*/
const LISTING_TIMEOUT_MS = 10_000;
const ANTHROPIC_PAGE_LIMIT = "1000";
const MAX_LISTING_PAGES = 20;
const MAX_LOGGED_BODY_CHARS = 500;
const ANTHROPIC_VERSION = "2023-06-01";
/**
* The bearer token `createEnvForProvider` sets as `ANTHROPIC_AUTH_TOKEN` and
* `AWS_BEARER_TOKEN_BEDROCK`, so the CLI sends `Authorization: Bearer acp-proxy`
* unless the provider headers replace it. The listing sends the same, so the
* gateway sees the same auth as on chat requests.
*/
export const PROVIDER_AUTH_TOKEN_PLACEHOLDER = "acp-proxy";
/**
* The cross-region inference profile prefixes the CLI knows:
* `eu.anthropic.claude-sonnet-5`. `ANTHROPIC_BEDROCK_REGION_PREFIX` accepts all
* of them but `us-gov`, which the CLI takes only from a `us-gov-*` AWS region;
* a gateway listing only `us-gov.*` ids would then get an empty picker. That
* case is theoretical, so it is left as is.
*/
const BEDROCK_REGION_PREFIXES = ["us", "eu", "apac", "jp", "au", "us-gov", "global"];
type ListingLogger = { log: (...args: any[]) => void; error: (...args: any[]) => void };
type ListingPage = { ids: string[]; nextQuery?: Record<string, string> };
/**
* A gateway listing that failed: a network error, a status other than 2xx or
* 4xx, a body that is not the expected listing, or the 10 s timeout. The
* message names the request path and the reason, never a header value.
*/
export class GatewayListingError extends Error {
override name = "GatewayListingError";
}
/**
* Lists the model ids an `anthropic` gateway at `baseUrl` serves, following
* `has_more` / `last_id` pagination. Returns `undefined` when the gateway
* answers 4xx: it has no listing, and nothing is narrowed. Throws a
* {@link GatewayListingError} when the listing fails. Header values are never
* logged.
*/
export async function listGatewayModels(
baseUrl: string,
providerHeaders: Record<string, string>,
logger: ListingLogger,
): Promise<string[] | undefined> {
return listPages(
`${baseUrl.replace(/\/+$/, "")}/v1/models`,
{ limit: ANTHROPIC_PAGE_LIMIT },
listingHeaders(providerHeaders, { "anthropic-version": ANTHROPIC_VERSION }),
logger,
(body) => {
const page = body as { data?: unknown; has_more?: unknown; last_id?: unknown };
if (!Array.isArray(page.data)) return undefined;
return {
ids: stringField(page.data, "id"),
nextQuery:
page.has_more === true && typeof page.last_id === "string"
? { limit: ANTHROPIC_PAGE_LIMIT, after_id: page.last_id }
: undefined,
};
},
);
}
/**
* Lists the Anthropic models a `bedrock` gateway at `baseUrl` serves
* (`GET /foundation-models`, Bedrock `ListFoundationModels`, one page), the
* listing AI Router answers on its Bedrock route. Returns the listed ids, such
* as `global.anthropic.claude-sonnet-5-5`; `undefined` and the errors are
* those of {@link listGatewayModels}.
*/
export async function listBedrockModels(
baseUrl: string,
providerHeaders: Record<string, string>,
logger: ListingLogger,
): Promise<string[] | undefined> {
const ids = await listPages(
`${baseUrl.replace(/\/+$/, "")}/foundation-models`,
{},
listingHeaders(providerHeaders, {}),
logger,
(body) => {
const page = body as { modelSummaries?: unknown };
if (!Array.isArray(page.modelSummaries)) return undefined;
return { ids: stringField(page.modelSummaries, "modelId") };
},
);
return ids?.filter((id) => id.includes("anthropic."));
}
/** The cross-region prefix of a Bedrock model id or ARN, if it has one. */
export function bedrockRegionPrefix(modelId: string): string | undefined {
const id = modelId.slice(modelId.lastIndexOf("/") + 1).toLowerCase();
return BEDROCK_REGION_PREFIXES.find((prefix) => id.startsWith(`${prefix}.anthropic.`));
}
/**
* Picks the region of the listed Bedrock models: `global` when the gateway
* lists it, else the prefix of the first id that has one. A gateway that lists
* `eu.*` and `us.*` ids gets the prefix that comes first, and the ids of the
* other prefixes are dropped, so the picker offers only models the CLI can
* call under that prefix. Ids without a prefix (`anthropic.claude-…`) are
* dropped too once a prefix is picked; without any prefix every id is kept.
*/
export function selectBedrockRegion(modelIds: readonly string[]): {
regionPrefix?: string;
modelIds: string[];
} {
const prefixes = modelIds.map(bedrockRegionPrefix);
const regionPrefix = prefixes.includes("global")
? "global"
: prefixes.find((prefix) => prefix !== undefined);
if (!regionPrefix) return { modelIds: [...modelIds] };
return {
regionPrefix,
modelIds: modelIds.filter((id) => bedrockRegionPrefix(id) === regionPrefix),
};
}
// Provider headers win over the defaults, matched case-insensitively, the way
// `ANTHROPIC_CUSTOM_HEADERS` overrides the CLI's own headers.
function listingHeaders(
providerHeaders: Record<string, string>,
protocolHeaders: Record<string, string>,
): Headers {
const headers = new Headers({
authorization: `Bearer ${PROVIDER_AUTH_TOKEN_PLACEHOLDER}`,
accept: "application/json",
...protocolHeaders,
});
for (const [name, value] of Object.entries(providerHeaders)) {
// `Headers.set` puts an invalid value into its error message, so name only the header.
try {
headers.set(name, value);
} catch {
throw new GatewayListingError(`Model listing cannot send the invalid header "${name}"`);
}
}
return headers;
}
function stringField(entries: unknown[], field: string): string[] {
return entries.flatMap((entry) => {
const value = (entry as Record<string, unknown> | null)?.[field];
return typeof value === "string" && value.trim() !== "" ? [value] : [];
});
}
/**
* Reads `endpoint` page by page; `parse` returns `undefined` for a body it does
* not understand. A 4xx answer on any page means the gateway has no listing:
* the result is `undefined`, and nothing is narrowed.
*/
async function listPages(
endpoint: string,
firstQuery: Record<string, string>,
headers: Headers,
logger: ListingLogger,
parse: (body: unknown) => ListingPage | undefined,
): Promise<string[] | undefined> {
const { pathname: path, username, password } = new URL(endpoint);
const failure = (reason: string): GatewayListingError => {
const error = new GatewayListingError(`Model listing GET ${path} ${reason}`);
logger.error(`[providers/set] ${error.message}`);
return error;
};
// fetch refuses such a URL and repeats it, credentials included, in its error.
if (username || password) throw failure("cannot be sent: the base URL carries credentials");
const signal = AbortSignal.timeout(LISTING_TIMEOUT_MS);
const ids: string[] = [];
let query: Record<string, string> | undefined = firstQuery;
for (let page = 0; page < MAX_LISTING_PAGES && query; page++) {
const url = new URL(endpoint);
for (const [name, value] of Object.entries(query)) url.searchParams.set(name, value);
let response: Response;
let body: string;
try {
response = await fetch(url, { headers, signal });
body = await response.text();
} catch (error) {
throw failure(`failed: ${error instanceof Error ? error.message : String(error)}`);
}
if (response.status >= 400 && response.status < 500) {
logger.log(
`[providers/set] Model listing GET ${path} returned ${response.status}: the gateway has no listing, models are not narrowed`,
);
return undefined;
}
if (!response.ok) {
throw failure(`returned ${response.status}: ${body.slice(0, MAX_LOGGED_BODY_CHARS)}`);
}
let parsed: ListingPage | undefined;
try {
parsed = parse(JSON.parse(body));
} catch {
parsed = undefined;
}
if (!parsed) throw failure("returned an unexpected body");
ids.push(...parsed.ids);
query = parsed.nextQuery;
}
if (query) {
logger.error(
`[providers/set] Model listing GET ${path} still had more pages after ${MAX_LISTING_PAGES}; using what was read`,
);
}
return ids;
}

View File

@@ -1,4 +1,4 @@
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import { RequestError } from "@agentclientprotocol/sdk";
import type { SDKActiveGoalMessage } from "@anthropic-ai/claude-agent-sdk";
export const GOAL_EXTENSION_VERSION = 1 as const;

View File

@@ -32,7 +32,7 @@
* until the next prompt. Awaiting it would add a CLI run to every turn.
*/
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import { RequestError } from "@agentclientprotocol/sdk";
import type { AccountInfo, Query } from "@anthropic-ai/claude-agent-sdk";
export function shouldHideClaudeAuth(): boolean {

View File

@@ -2,7 +2,7 @@
import { claudeCliPath } from "./acp-agent.js";
import { applyManagedPolicyEnv } from "./managed-policy.js";
import { acpProtocolsFromEnv, serveAcp } from "./serve.js";
import { serveAcp } from "./serve.js";
import packageJson from "../package.json" with { type: "json" };
import { appendFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
@@ -83,8 +83,9 @@ if (process.argv.includes("--cli")) {
: undefined;
logger?.log("Claude ACP started");
// The draft ACP v2 is opt-in while it is a draft.
const acp = serveAcp(logger, {
protocols: acpProtocolsFromEnv(process.env.CLAUDE_AGENT_ACP_PROTOCOL),
experimentalV2: process.env.CLAUDE_AGENT_ACP_EXPERIMENTAL_V2 === "1",
});
async function shutdown() {

View File

@@ -1,4 +1,4 @@
import type { AvailableCommand } from "@agentclientprotocol/sdk/unstable";
import type { AvailableCommand } from "@agentclientprotocol/sdk";
import type { McpServerStatus, Query } from "@anthropic-ai/claude-agent-sdk";
import { escapeMarkdown } from "./usage-markdown.js";
import { raceTimeoutAndAbort } from "./utils.js";

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk/unstable";
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk";
import type {
PermissionMode,
PermissionResult,

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import {
buildEditPermissionOptions,
buildGlobPermissionOptions,

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import os from "node:os";
import path from "node:path";
import type { DurablePermissionChangeSet } from "../normalization.js";

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { DurablePermissionChangeSet } from "../normalization.js";
export const PERMISSION_OPTION_ID = {
@@ -30,9 +30,6 @@ export interface PermissionOptionContext {
/** The mode the session left when it entered plan mode. */
prePlanMode?: string;
contextUsedPercent?: number;
/** False when no ACP turn can continue an accepted plan in a fresh
* context. Then an ExitPlanMode request offers no clear-context option. */
allowContextReset?: boolean;
}
export function allowOnce(name = "Yes"): PermissionOption {

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import path from "node:path";
import type { DurablePermissionChangeSet } from "../normalization.js";
import { plainString, type PermissionOptionContext, withOptionalUpdate } from "./shared.js";

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import {
allowOnce,
exactLocalAllowRule,
@@ -70,7 +70,7 @@ export function buildExitPlanModePermissionOptions(
: (["auto", "bypassPermissions"] as const)
).filter((mode) => modes.has(mode));
const preferredMode = elevatedModes[0] ?? "acceptEdits";
if (context.allowContextReset !== false && plainString(context.input.plan)) {
if (plainString(context.input.plan)) {
const usage =
context.contextUsedPercent === undefined ? "" : ` (${context.contextUsedPercent}% used)`;
options.push(

View File

@@ -1,4 +1,4 @@
import type { RequestPermissionRequest, ToolCallContent } from "@agentclientprotocol/sdk/unstable";
import type { RequestPermissionRequest, ToolCallContent } from "@agentclientprotocol/sdk";
import { AIR_PERMISSION_KEY, withAirMeta } from "../air-extension.js";
import { ClientCapabilities } from "../tool-calls/client-capabilities.js";
import { AcpToolCallRenderer } from "../tool-calls/renderer.js";

View File

@@ -1,5 +1,5 @@
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { PermissionMode, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
import type { DurablePermissionChangeSet } from "./normalization.js";
import { applyClaudePermissionSelection, parseClaudePermissionSelection } from "./effects.js";

View File

@@ -1,44 +0,0 @@
/**
* The follow-up prompt the SDK predicts for the next turn.
*
* With `promptSuggestions` on, the CLI emits one `prompt_suggestion` message
* after a turn's `result` holding the prompt it expects the user to send next.
* ACP has no field for it, so it travels as
* `session_info_update._meta.promptSuggestions`: a client that does not read
* the key sees a session info update that says nothing and ignores it.
*
* AIR renders the first non-blank `suggestions[].text` as gray text in an empty
* composer, where Tab fills the composer without sending.
*
* The CLI sends no suggestion on the first turn of a session, after an API
* error, in plan mode, and at the account's usage limit, so most sessions have
* turns that end with no prediction at all.
*/
/** The `_meta` key the suggestion travels under. It names no vendor. */
export const PROMPT_SUGGESTIONS_META_KEY = "promptSuggestions";
/** One predicted prompt, as the `_meta` value carries it. */
export type PromptSuggestionsMetaValue = {
suggestions: { text: string }[];
};
/**
* The suggestion as it goes on the wire, or `null` for a suggestion that says
* nothing: the CLI can predict whitespace, and an empty text would otherwise
* reach the composer as a hint with no content.
*/
export function promptSuggestionText(raw: string | null | undefined): string | null {
const text = raw?.trim();
return text ? text : null;
}
/**
* The `_meta` of the session info update that publishes `text`. `null` sends an
* empty `suggestions` list, which clears the prompt the client holds — the turn
* predicted nothing, so the composer must not keep the previous prediction.
*/
export function promptSuggestionsMeta(text: string | null): Record<string, unknown> {
const value: PromptSuggestionsMetaValue = { suggestions: text === null ? [] : [{ text }] };
return { [PROMPT_SUGGESTIONS_META_KEY]: value };
}

View File

@@ -1,32 +1,19 @@
import { ndJsonStream } from "@agentclientprotocol/sdk";
import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { v1AgentApp, type ClaudeAcpAgent, type Logger } from "./acp-agent.js";
import { nodeToWebReadable, nodeToWebWritable } from "./utils.js";
import { v2AgentApp } from "./v2/agent.js";
/**
* The ACP versions that the adapter serves: `v1` or `v2` alone, or both with
* `auto`, where the client's `initialize` picks the version.
* Serves ACP on stdio.
*
* With `experimentalV2`, the connection goes through {@link acpProtocolRouter}.
* Without it, the adapter serves ACP v1 alone and never routes by protocol
* version, exactly as before.
*/
export type AcpProtocols = "auto" | "v1" | "v2";
/**
* Reads `CLAUDE_AGENT_ACP_PROTOCOL`: unset, empty, or `auto` serves both
* versions, and `v1` or `v2` serves that version alone. Throws on any other
* value, so that a typo such as `CLAUDE_AGENT_ACP_PROTOCOL=1` fails at startup
* instead of silently serving both.
*/
export function acpProtocolsFromEnv(value: string | undefined): AcpProtocols {
if (value === undefined || value === "") return "auto";
if (value === "auto" || value === "v1" || value === "v2") return value;
throw new Error(
`CLAUDE_AGENT_ACP_PROTOCOL must be "auto", "v1", or "v2", not ${JSON.stringify(value)}`,
);
}
/** Serves ACP on stdio through {@link acpProtocolRouter}. */
export function serveAcp(
logger: Logger | undefined,
options: { protocols: AcpProtocols },
options: { experimentalV2: boolean },
): { closed: Promise<void>; dispose(): Promise<void> } {
const output = nodeToWebWritable(process.stdout);
const input = nodeToWebReadable(process.stdin);
@@ -36,9 +23,9 @@ export function serveAcp(
const onAgent = (created: ClaudeAcpAgent) => {
agent = created;
};
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols).connect(
v2.ndJsonStream(output, input),
);
const { closed } = options.experimentalV2
? acpProtocolRouter(logger, onAgent).connect(v2.ndJsonStream(output, input))
: v1AgentApp(logger, onAgent).connect(ndJsonStream(output, input));
return {
closed,
dispose: async () => {
@@ -49,19 +36,16 @@ export function serveAcp(
/**
* Routes a connection to the protocol version that the client negotiates in
* `initialize`. With `auto`, a client that requests version 2 or later gets
* the draft ACP v2, and every other client gets ACP v1. With `v1`, a v2
* client is negotiated down to v1. With `v2`, the router rejects the
* `initialize` of a v1 client as an unsupported protocol version. `onAgent`
* receives the agent of the connection once the router has opened it.
* `initialize`: the experimental draft ACP v2 for a client that requests
* version 2 or later, and ACP v1 otherwise. `onAgent` receives the agent of
* the connection once the router has opened it.
*/
export function acpProtocolRouter(
logger: Logger | undefined,
onAgent: (agent: ClaudeAcpAgent) => void,
protocols: AcpProtocols = "auto",
): v2.AgentProtocolRouter {
const router = v2.agentProtocolRouter();
if (protocols !== "v2") router.withV1(v1AgentApp(logger, onAgent));
if (protocols !== "v1") router.withV2(v2AgentApp(logger, onAgent));
return router;
return v2
.agentProtocolRouter()
.withV1(v1AgentApp(logger, onAgent))
.withV2(v2AgentApp(logger, onAgent));
}

View File

@@ -2,4 +2,3 @@
export const DEFAULT_MODEL_ID = "default";
export const DEFAULT_AGENT_ID = "default";
export const EFFORT_CONFIG_ID = "effort";
export const ULTRACODE_CONFIG_ID = "ultracode";

View File

@@ -1,4 +1,4 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import type { EffortLevel, ModelInfo, Settings } from "@anthropic-ai/claude-agent-sdk";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, withAirMeta } from "./air-extension.js";
import { EFFORT_CONFIG_ID } from "./session-config-ids.js";
@@ -14,15 +14,14 @@ export function toSdkEffortLevel(value: string | undefined): EffortLevel | null
// Since CLI 2.1.284 an `effortLevel` that changes the level, sent without an
// `ultracode` key, turns ultracode off. The CLI's own effort control keeps
// ultracode on at any level, so send `ultracode: true` with every effort apply
// while the session has ultracode on. When it is off, the key is omitted: an
// effort change then leaves ultracode off, as it already was.
// ultracode on at any level, so carry a settings-requested ultracode along
// with every effort apply.
export function effortFlagSettings(
value: string | undefined,
ultracode: boolean,
settings: Settings,
): { effortLevel: EffortLevel | null; ultracode?: true } {
const effortLevel = toSdkEffortLevel(value);
return ultracode ? { effortLevel, ultracode: true } : { effortLevel };
return settings.ultracode === true ? { effortLevel, ultracode: true } : { effortLevel };
}
function canonicalizeModelSettingsKey(value: string): string {
@@ -80,13 +79,6 @@ export function mergeEffortSettings(base: Settings, override: Settings | undefin
};
}
export function effortLevelName(level: string): string {
return level
.split(/[_-]/)
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
.join(" ");
}
export function buildEffortConfigOption(
modelInfos: ModelInfo[],
currentModelId: string,
@@ -106,7 +98,10 @@ export function buildEffortConfigOption(
...(useRecommendedValue ? [] : [{ value: "default", name: "Default" }]),
...supportedLevels.map((level) => ({
value: level,
name: effortLevelName(level),
name: level
.split(/[_-]/)
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
.join(" "),
})),
];
const includes = (level: string) =>

View File

@@ -1,4 +1,4 @@
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
import {
getSessionMessages,
type SDKAssistantMessageError,

View File

@@ -11,10 +11,6 @@
* - an interactive CLI that is not driven by an SDK holds it, and its registry
* status is newer than the transcript: the registry status (an idle one is
* `error` after such a turn);
* - a turn that ended with an open background task is `running`, also after
* 10 minutes: Claude Code wakes the session again when the task ends. The
* wait ends when the registry status is `idle`, because a task can end
* without a transcript record (a stop in the task UI);
* - otherwise the transcript tail: a finished turn is `idle` (`error` after
* an API error), an unfinished turn written in the last 10 minutes is
* `running`, and anything else has no known state.
@@ -76,14 +72,6 @@ function usesRegistryStatus(record: LiveRecord, transcriptMtimeMs: number): bool
);
}
/** Whether the last turn ended while a background task of the session is
* open. */
function waitsForBackgroundTasks(facts: TranscriptFacts): boolean {
if (facts.turnState !== "finished") return false;
const openTasks = facts.backgroundTasks?.open ?? [];
return openTasks.length > 0;
}
function iso(ms: number | undefined): string | undefined {
return ms === undefined || !Number.isFinite(ms) ? undefined : new Date(ms).toISOString();
}
@@ -115,9 +103,6 @@ export function deriveActivity(input: {
// A busy or waiting CLI wins; an idle one shows how its turn ended.
state = registryState(live.status);
if (state === "idle" && failed) state = "error";
} else if (waitsForBackgroundTasks(facts) && live.status !== "idle") {
// Claude Code wakes the session again when an open task ends.
state = "running";
} else if (failed) {
state = "error";
} else if (facts.turnState === "finished") {

View File

@@ -1,223 +0,0 @@
/**
* The background tasks of a session that did not end yet, from its transcript.
*
* A Claude Code session can start a background shell command or a background
* sub-agent and end its turn at once. When the task ends, Claude Code wakes
* the session with a `<task-notification>` input for a short turn. So a turn
* end with an open task is no end of the work: the session list keeps such a
* session `running` (see `activity.ts`).
*
* The records, as Claude Code 2.1.299 writes them, in the rules of AIR's
* native Claude provider:
*
* - a tool result opens a task when its `toolUseResult` has a
* `backgroundTaskId` (a shell command), or `isAsync: true` with an
* `agentId` (a sub-agent);
* - a task closes when its notification reaches the session: as a user
* record, or as a `queue-operation` `remove` when a running turn takes it
* in. A notification that waits in the queue (`enqueue`) keeps the task
* open, because the session still runs a turn for it;
* - only a notification with a `<status>` closes a task. A notification
* without one comes while the task still runs: the check-in of a quiet
* shell ("is still running after …", since Claude Code 2.1.303) or a
* `Monitor` event;
* - a `TaskStop` result closes the task of its `toolUseResult.task_id`:
* Claude Code writes no notification for it. The result does not name its
* tool, so its `tool_use_id` must match a `TaskStop` call. A `task_id` in
* the result of another tool keeps the task open;
* - a resumed CLI writes a `stopped` notification for each task of the
* previous process;
* - a sidechain record belongs to a sub-agent and opens or closes no task of
* the session.
*
* A background shell that does not end, for example a dev server, keeps its
* task open, so its session stays `running` while the CLI holds it.
*
* A task that a sub-agent starts is not seen. Its start record is only in the
* transcript of the sub-agent, so the session can be `idle` while the
* sub-agent waits for it.
*/
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import { TASK_NOTIFICATION_TAG, taskNotificationsOf } from "../task-notifications.js";
export type BackgroundTasks = {
/** The ids of the tasks that started and did not end, in start order. */
open: string[];
/** The `tool_use` ids of the `TaskStop` calls without a result. */
stopCalls: string[];
/** Whether the sets hold the records of the whole file. A set from the
* tail window only does not know a task that started before it. */
complete: boolean;
};
/** The fields of a transcript record that bear on a task. */
type TranscriptRecord = {
type?: string;
isSidechain?: boolean;
/** Of a `queue-operation` record: `enqueue`, `remove`, ... */
operation?: string;
/** Of a `queue-operation` record: the queued input. */
content?: string;
message?: { content?: string | ContentBlock[] };
toolUseResult?: ToolUseResult;
};
type ContentBlock = {
type?: string;
text?: string;
/** Of a `tool_use` block. */
id?: string;
name?: string;
/** Of a `tool_result` block. */
tool_use_id?: string;
};
type ToolUseResult = {
/** The task of a background shell command. */
backgroundTaskId?: string;
/** With `agentId`: an async sub-agent. */
isAsync?: boolean;
agentId?: string;
/** The task that a `TaskStop` call stopped. Another tool can give one. */
task_id?: string;
};
const NOTIFICATION_TAG = `<${TASK_NOTIFICATION_TAG}>`;
/** Text that a line must hold to bear on a task, so that the other lines
* are not parsed. */
const TASK_MARKERS = ["backgroundTaskId", '"isAsync"', NOTIFICATION_TAG, "TaskStop", "task_id"];
function parseRecord(line: string): TranscriptRecord | undefined {
try {
return JSON.parse(line) as TranscriptRecord;
} catch {
return undefined;
}
}
function blocksOf(record: TranscriptRecord): ContentBlock[] {
const content = record.message?.content;
return Array.isArray(content) ? content : [];
}
/** The input of a user record: its string content, else its first text
* block. */
function inputTextOf(record: TranscriptRecord): string | undefined {
const content = record.message?.content;
if (typeof content === "string") return content;
return blocksOf(record).find((block) => block.type === "text")?.text;
}
/** The task that a `<task-notification>` input ends: only an input that
* starts with the tag, after whitespace, as AIR's `TaskNotification` reads
* it. Only a notification with a `<status>` ends a task (see
* `taskNotificationsOf`). */
export function endedTaskId(input: string | undefined): string | undefined {
if (!input?.trimStart().startsWith(NOTIFICATION_TAG)) return undefined;
return taskNotificationsOf(input)[0]?.task_id;
}
/** The task that a tool result started: a background shell command, or an
* async sub-agent. */
function startedTaskId(record: TranscriptRecord): string | undefined {
if (record.type !== "user") return undefined;
const result = record.toolUseResult;
if (result?.backgroundTaskId) return result.backgroundTaskId;
if (result?.isAsync) return result.agentId;
return undefined;
}
/** The task whose notification reached the session: as a user record, or
* as a queue `remove` when a running turn takes it in. */
function deliveredTaskId(record: TranscriptRecord): string | undefined {
if (record.type === "user") return endedTaskId(inputTextOf(record));
if (record.type === "queue-operation" && record.operation === "remove") {
return endedTaskId(record.content);
}
return undefined;
}
/** Collects the tasks of the lines it is given, in file order. */
class TaskTracker {
private readonly open: Set<string>;
private readonly stopCalls: Set<string>;
constructor(after?: BackgroundTasks) {
this.open = new Set(after?.open);
this.stopCalls = new Set(after?.stopCalls);
}
add(line: string): void {
const mayBearOnTask = TASK_MARKERS.some((marker) => line.includes(marker));
if (!mayBearOnTask) return;
const record = parseRecord(line);
// A sidechain record belongs to a sub-agent, which receives the
// notifications of its own tasks.
if (!record || record.isSidechain) return;
this.apply(record);
}
result({ complete }: { complete: boolean }): BackgroundTasks {
return { open: [...this.open], stopCalls: [...this.stopCalls], complete };
}
private apply(record: TranscriptRecord): void {
const started = startedTaskId(record);
if (started) this.open.add(started);
const delivered = deliveredTaskId(record);
if (delivered) this.open.delete(delivered);
this.applyTaskStop(record);
}
/** A `task_id` stops its task only in the result of a `TaskStop` call:
* another tool can give a `task_id` too. */
private applyTaskStop(record: TranscriptRecord): void {
let isTaskStopResult = false;
for (const block of blocksOf(record)) {
if (block.type === "tool_use" && block.name === "TaskStop" && block.id) {
this.stopCalls.add(block.id);
}
const resultId = block.tool_use_id;
if (resultId && this.stopCalls.has(resultId)) {
this.stopCalls.delete(resultId);
isTaskStopResult = true;
}
}
const stopped = record.toolUseResult?.task_id;
if (isTaskStopResult && stopped) this.open.delete(stopped);
}
}
/**
* The tasks of the lines of `text`, applied on the tasks `after`.
*
* `after` can be what an earlier scan found for a shorter copy of the file,
* and `text` can repeat records of that copy. The result is still the same:
* the last record that opens or closes a task decides, and `text` holds the
* last records.
*/
export function backgroundTasksOf(
text: string,
options: { complete: boolean; after?: BackgroundTasks },
): BackgroundTasks {
const tracker = new TaskTracker(options.after);
for (const line of text.split("\n")) tracker.add(line);
return tracker.result({ complete: options.complete });
}
/** The tasks of the first `size` bytes of `filePath`, from all its records.
* Reads the whole file, so only for a session that a live process holds. */
export async function searchBackgroundTasks(
filePath: string,
size: number,
): Promise<BackgroundTasks> {
const tracker = new TaskTracker();
if (size === 0) return tracker.result({ complete: true });
const stream = createReadStream(filePath, { start: 0, end: size - 1, encoding: "utf8" });
const lines = createInterface({ input: stream, crlfDelay: Infinity });
for await (const line of lines) tracker.add(line);
return tracker.result({ complete: true });
}

View File

@@ -15,16 +15,15 @@ import {
type InitializeRequest,
type ListSessionsRequest,
type ListSessionsResponse,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
import { deleteSession as sdkDeleteSession } from "@anthropic-ai/claude-agent-sdk";
import type { Query } from "@anthropic-ai/claude-agent-sdk";
import type { AcpClient, Logger, Session } from "../acp-agent.js";
import {
AIR_SESSION_ARCHIVE_CAPABILITY,
AIR_SESSION_BATCH_GET_CAPABILITY,
AIR_SESSION_INDEX_CAPABILITY,
AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY,
AIR_SESSION_RENAME_CAPABILITY,
AIR_SESSION_WORKTREES_CAPABILITY,
clientSupportsAirCapability,
withAirMeta,
} from "../air-extension.js";
@@ -42,13 +41,10 @@ import {
} from "./list-subscriptions.js";
import {
archiveInsteadOfDelete,
deleteSessionIfStored,
archiveTo,
renameTo,
parseRenameSessionRequest,
parseSessionIdRequest,
parseBatchGetSessionsRequest,
SESSION_BATCH_GET_METHOD,
SESSION_ARCHIVE_METHOD,
SESSION_RENAME_METHOD,
SESSION_UNARCHIVE_METHOD,
@@ -57,8 +53,6 @@ import {
type RetitleOptions,
type SessionIdRequest,
type TitleChange,
type BatchGetSessionsRequest,
type BatchGetSessionsResponse,
} from "./service.js";
/** What the session index keeps on each `Session`. */
@@ -149,17 +143,15 @@ export class SessionIndexConnection {
}
/** The AIR capabilities the agent advertises for the index: the index
* itself, batch get, archive, rename, the list subscription and the `agentWorktree`
* row field, all only to a client that declared `sessionIndex`. */
* itself, archive, rename and the list subscription, all only to a client
* that declared `sessionIndex`. */
capabilities(): string[] {
return this.service
? [
AIR_SESSION_INDEX_CAPABILITY,
AIR_SESSION_BATCH_GET_CAPABILITY,
AIR_SESSION_ARCHIVE_CAPABILITY,
AIR_SESSION_RENAME_CAPABILITY,
AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY,
AIR_SESSION_WORKTREES_CAPABILITY,
]
: [];
}
@@ -195,31 +187,6 @@ export class SessionIndexConnection {
this.service?.ownSessionChanged(sessionId);
}
/**
* Runs `fork`, the handler of a `session/fork` request of `sourceId`. The
* session list holds back the forks of the source until the response is
* out. The client then knows a fork from the response before a list shows
* it.
*/
async forking<T>(sourceId: string, fork: () => Promise<T>): Promise<T> {
// The forks of this connection: the request handler reads the service
// once, so a later `initialize` does not split the start and the answer.
const service = this.service;
service?.forkStarted(sourceId);
try {
return await fork();
} finally {
// The SDK writes the response in the microtasks after the handler
// resolves, so a `setTimeout(0)` callback runs after it.
setTimeout(() => service?.forkAnswered(sourceId), 0);
}
}
/** The `session/fork` of `sourceId` copied the transcript into `forkId`. */
onForkCreated(sourceId: string, forkId: string): void {
this.service?.forkCreated(sourceId, forkId);
}
/** `_session/list/subscribe`. */
subscribeList(params: ListSubscribeRequest): Promise<ListSubscribeResponse> {
return this.requireService(LIST_SUBSCRIBE_METHOD).subscribeList(params.cwd);
@@ -236,12 +203,6 @@ export class SessionIndexConnection {
return this.service?.list(params, (sessionId) => this.ownSessionState(sessionId));
}
batchGet(params: BatchGetSessionsRequest): Promise<BatchGetSessionsResponse> {
return this.requireService(SESSION_BATCH_GET_METHOD).batchGet(params, (sessionId) =>
this.ownSessionState(sessionId),
);
}
/** An AIR client archives with session/delete (see {@link deleteSession}):
* its archived sessions (an archived custom title) stay hidden from the
* old list, as when the delete removed them. */
@@ -375,8 +336,7 @@ export class SessionIndexConnection {
* - Another AIR client uses delete to mark a session done, and may reopen
* it later: the adapter archives it instead (as `_session/archive` does,
* after the CLI closed), so the transcript survives.
* - Every other client: the SDK delete. A session without a transcript
* succeeds (see {@link deleteSessionIfStored}).
* - Every other client: the SDK delete, as before.
*/
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
const loaded = this.host.agent.sessions[params.sessionId] !== undefined;
@@ -390,7 +350,7 @@ export class SessionIndexConnection {
} else if (this.host.isAirClient()) {
await archiveInsteadOfDelete(params.sessionId, this.airArchiver());
} else {
await deleteSessionIfStored(params.sessionId);
await sdkDeleteSession(params.sessionId);
}
return {};
}
@@ -460,7 +420,6 @@ export class SessionIndexConnection {
/** The agent methods behind the session index extension methods. */
type SessionIndexMethods = {
batchGetSessions(params: BatchGetSessionsRequest): Promise<BatchGetSessionsResponse>;
subscribeSessionList(params: ListSubscribeRequest): Promise<ListSubscribeResponse>;
unsubscribeSessionList(params: ListUnsubscribeRequest): Promise<EmptyResponse>;
renameSessionTitle(params: RenameSessionRequest): Promise<EmptyResponse>;
@@ -468,15 +427,10 @@ type SessionIndexMethods = {
unarchiveSession(params: SessionIdRequest): Promise<EmptyResponse>;
};
/** Routes `_session/batch_get`, `_session/list/subscribe`, `_session/list/unsubscribe`,
/** Routes `_session/list/subscribe`, `_session/list/unsubscribe`,
* `_session/rename`, `_session/archive` and `_session/unarchive`. */
export function onSessionIndexRequests(app: AgentApp, agent: () => SessionIndexMethods): AgentApp {
return app
.onRequest<BatchGetSessionsRequest, BatchGetSessionsResponse>(
SESSION_BATCH_GET_METHOD,
{ parse: parseBatchGetSessionsRequest },
(ctx) => agent().batchGetSessions(ctx.params),
)
.onRequest<ListSubscribeRequest, ListSubscribeResponse>(
LIST_SUBSCRIBE_METHOD,
{ parse: parseListSubscribeRequest },

View File

@@ -40,7 +40,7 @@ import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as path from "node:path";
import { RequestError, type SessionInfo } from "@agentclientprotocol/sdk/unstable";
import { RequestError, type SessionInfo } from "@agentclientprotocol/sdk";
import type { OwnSessionState } from "./activity.js";
import {
bySession,
@@ -111,8 +111,6 @@ export type ListSubscriptionDeps = {
/** What this connection knows of a session it runs. */
own: (sessionId: string) => OwnSessionState | undefined;
notify: (changes: ListChanges) => Promise<void>;
/** The forks whose rows wait for their fork response. */
forkHolds?: ForkHolds;
logError: (message: string, error: unknown) => void;
now?: () => number;
debounceMs?: number;
@@ -244,54 +242,6 @@ function transcriptId(filename: string): string | undefined {
return isSessionId(id) ? id : undefined;
}
/**
* The `session/fork` requests in flight. A client must get the id of a fork
* from the fork response before a list shows the fork. Otherwise the client
* sees an unknown session and lists it as a session of its own.
*/
export class ForkHolds {
/** Source session id → the number of its forks in flight, and the forks
* they copied the transcript into. */
private readonly sources = new Map<string, { count: number; forks: Set<string> }>();
started(sourceId: string): void {
const source = this.sources.get(sourceId);
if (source) source.count++;
else this.sources.set(sourceId, { count: 1, forks: new Set() });
}
/** A fork of `sourceId` copied the transcript into `forkId`. */
created(sourceId: string, forkId: string): void {
this.sources.get(sourceId)?.forks.add(forkId);
}
/** The response of a fork of `sourceId` is out. Returns whether that
* freed rows: the last fork of the source answered. */
answered(sourceId: string): boolean {
const source = this.sources.get(sourceId);
if (!source) return false;
if (--source.count > 0) return false;
this.sources.delete(sourceId);
return true;
}
/** Whether `id` is a fork whose fork response is not out yet. */
isHeld(id: string): boolean {
for (const { forks } of this.sources.values()) if (forks.has(id)) return true;
return false;
}
/**
* Whether the row of `id` waits. A fork that a fork in flight created
* waits. Before the copy returns its id, an unsent row of a fork of a
* source in flight waits too, by its `forkedFrom`.
*/
holds(id: string, sent: boolean, forkedFrom: string | undefined): boolean {
if (this.isHeld(id)) return true;
return !sent && forkedFrom !== undefined && this.sources.has(forkedFrom);
}
}
export class ListSubscriptions {
private readonly subscriptions = new Map<string, Subscription>();
/** Sessions whose row shows this connection's SDK state: the rescan
@@ -398,13 +348,6 @@ export class ListSubscriptions {
}
}
/** Sends the pending sessions now: {@link ForkHolds} held some back. */
deliverPending(): void {
for (const subscription of this.subscriptions.values()) {
if (subscription.pending.size > 0) this.deliver(subscription);
}
}
/**
* This connection wrote the transcripts of a session: a rename, archive,
* unarchive or delete. Its transcripts are stat'ed and its row read again
@@ -1256,10 +1199,7 @@ export class ListSubscriptions {
if (!watch.rows.has(id)) candidates.push(...(priorFiles.get(id) ?? []));
}
if (candidates.length === 0) return new Set();
// Only the ids count: no worktree check.
const { rows, unknown } = await this.deps.index.cachedRowsOf(paths, candidates, {
mark: false,
});
const { rows, unknown } = await this.deps.index.cachedRowsOf(paths, candidates);
for (const row of rows) unknown.add(row.sessionId);
return unknown;
}
@@ -1284,12 +1224,6 @@ export class ListSubscriptions {
subscription.pending.delete(id);
continue;
}
// A fork whose fork response is not out yet stays pending.
if (
this.deps.forkHolds?.holds(id, subscription.sent.has(id), current?.row.facts.forkedFrom)
) {
continue;
}
const dueAt = (subscription.sentAt.get(id) ?? -Infinity) + this.minIntervalMs;
if (dueAt > now) {
nextAt = Math.min(nextAt, dueAt);

View File

@@ -104,17 +104,6 @@ export async function canonicalPath(cwd: string): Promise<string> {
}
}
/** Whether the path exists, including a symlink whose target is missing. */
export async function exists(target: string): Promise<boolean> {
try {
await fs.lstat(target);
return true;
} catch (error) {
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") return false;
throw error;
}
}
/** The names of the project directories of `cwd` among `rootEntries`. */
export function projectDirsOf(cwd: string, rootEntries: readonly string[]): string[] {
const exact = encodeProjectPath(cwd);

View File

@@ -15,8 +15,7 @@ import {
RequestError,
type ListSessionsRequest,
type ListSessionsResponse,
type SessionInfo,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
import { deleteSession as sdkDeleteSession } from "@anthropic-ai/claude-agent-sdk";
import { airExtensionMeta } from "../air-extension.js";
import { sanitizeTitle } from "../session-titles.js";
@@ -29,7 +28,6 @@ import {
visibleTitle,
} from "./archive-title.js";
import {
ForkHolds,
ListSubscriptions,
type ListChanges,
type ListSubscribeResponse,
@@ -38,7 +36,7 @@ import {
import { LiveSessionRegistry } from "./live-registry.js";
import {
canonicalPath,
exists,
errorCode,
isExactProjectDir,
isSessionId,
projectDirMatches,
@@ -66,7 +64,6 @@ import { sessionInfoOf } from "./session-info.js";
export const SESSION_RENAME_METHOD = "_session/rename";
export const SESSION_ARCHIVE_METHOD = "_session/archive";
export const SESSION_UNARCHIVE_METHOD = "_session/unarchive";
export const SESSION_BATCH_GET_METHOD = "_session/batch_get";
/** The JSON-RPC code of an unknown session (ACP `ResourceNotFound`). */
const RESOURCE_NOT_FOUND = -32002;
@@ -75,27 +72,6 @@ const CURSOR_VERSION = 5;
export type SessionIdRequest = { sessionId: string };
export type RenameSessionRequest = { sessionId: string; title: string };
export type BatchGetSessionsRequest = { sessionIds: string[] };
export type BatchGetSessionsResponse = {
sessions: SessionInfo[];
};
export function parseBatchGetSessionsRequest(value: unknown): BatchGetSessionsRequest {
const record = asRecord(value);
const ids = record.sessionIds;
if (
Object.keys(record).length !== 1 ||
!Array.isArray(ids) ||
ids.some((id) => typeof id !== "string" || !id.trim()) ||
new Set(ids).size !== ids.length
) {
throw RequestError.invalidParams(
undefined,
"params require only sessionIds: distinct, non-blank strings",
);
}
return { sessionIds: ids };
}
export function sessionNotFound(sessionId: string): RequestError {
return new RequestError(RESOURCE_NOT_FOUND, `Session not found: ${sessionId}`, { sessionId });
@@ -387,6 +363,16 @@ function isSdkNotFound(error: unknown): boolean {
return error instanceof Error && /^Session \S+ not found in /.test(error.message);
}
async function exists(target: string): Promise<boolean> {
try {
await fs.lstat(target);
return true;
} catch (error) {
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") return false;
throw error;
}
}
/** The error the SDK `deleteSession` gives for a session id it rejects or
* does not find, which a client without `sessionIndex` got before. */
function sdkDeleteError(sessionId: string): Error {
@@ -397,28 +383,6 @@ function sdkDeleteError(sessionId: string): Error {
);
}
/**
* `session/delete` of a client that is neither AIR nor a `sessionIndex` client:
* the SDK delete of the session's transcript. A session without one succeeds,
* as ACP asks ("Deleting an already-deleted session, or a session that never
* existed, SHOULD succeed silently"): an id that is not a session id, a
* session deleted before, or a session this connection ran but never prompted,
* since Claude Code writes the transcript at the first prompt. Other failures
* still fail the request.
*/
export async function deleteSessionIfStored(
sessionId: string,
deleteSession: (sessionId: string) => Promise<void> = sdkDeleteSession,
): Promise<void> {
if (!isSessionId(sessionId)) return;
try {
await deleteSession(sessionId);
} catch (error) {
if (isSdkNotFound(error)) return;
throw error;
}
}
/**
* `session/delete` of an AIR client without `sessionIndex`, which uses delete
* to mark a session done: archives it instead, so the transcript survives. A
@@ -478,9 +442,6 @@ export class SessionIndexService {
readonly index: SessionIndex;
readonly registry: LiveSessionRegistry;
private subscriptions?: ListSubscriptions;
/** Here and not in the subscriptions, which start with the first subscribe:
* a subscribe during a fork holds that fork back too. */
private readonly forkHolds = new ForkHolds();
/** Set for good by {@link dispose}: no subscription is made after it. */
private disposed = false;
/** The mutation in flight per session, which the next one waits for. */
@@ -489,18 +450,12 @@ export class SessionIndexService {
private readonly deleteSession: (sessionId: string) => Promise<void>;
constructor(private readonly deps: SessionIndexDeps) {
this.index = new SessionIndex();
this.registry = deps.registry ?? new LiveSessionRegistry();
this.index = new SessionIndex({ isLive: (sessionId) => this.isLive(sessionId) });
this.now = deps.now ?? Date.now;
this.deleteSession = deps.deleteSession ?? ((id) => sdkDeleteSession(id));
}
/** Whether a live process holds the session, by the live registry. */
private async isLive(sessionId: string): Promise<boolean> {
const live = await this.registry.snapshot();
return live.has(sessionId);
}
/** `session/list` of a `sessionIndex` client. `own` reports the sessions
* that this connection runs. */
async list(
@@ -525,30 +480,13 @@ export class SessionIndexService {
});
const live = await livePromise;
const now = this.now();
const sessions = rows
// A fork whose fork response is not out yet is left out, as in the list subscriptions.
.filter((row) => !this.forkHolds.isHeld(row.sessionId))
.map((row) => sessionInfoOf(row, own(row.sessionId), live.get(row.sessionId), now));
const sessions = rows.map((row) =>
sessionInfoOf(row, own(row.sessionId), live.get(row.sessionId), now),
);
const last = rows[rows.length - 1];
return hasMore && last ? { sessions, nextCursor: encodeListCursor(last, scope) } : { sessions };
}
/** Metadata for known ids, independent of list pages and project filters. */
async batchGet(
{ sessionIds }: BatchGetSessionsRequest,
own: (sessionId: string) => OwnSessionState | undefined,
): Promise<BatchGetSessionsResponse> {
if (sessionIds.length === 0) return { sessions: [] };
const live = await this.registry.snapshot();
const now = this.now();
const rows = await this.index.getSessions(sessionIds);
return {
sessions: rows.map((row) =>
sessionInfoOf(row, own(row.sessionId), live.get(row.sessionId), now),
),
};
}
/** Runs `mutation` after the previous mutation of the session ended. */
private exclusive<T>(sessionId: string, mutation: () => Promise<T>): Promise<T> {
const previous = this.mutations.get(sessionId) ?? Promise.resolve();
@@ -748,7 +686,6 @@ export class SessionIndexService {
registry: this.registry,
own: (sessionId) => this.deps.ownSessionState?.(sessionId),
notify: (changes) => this.deps.notifyListChanges?.(changes) ?? Promise.resolve(),
forkHolds: this.forkHolds,
logError: this.deps.logError,
...this.deps.listSubscriptionTiming,
});
@@ -765,22 +702,6 @@ export class SessionIndexService {
this.subscriptions?.ownSessionChanged(sessionId);
}
/** A `session/fork` of `sourceId` started (see {@link ForkHolds}). */
forkStarted(sourceId: string): void {
this.forkHolds.started(sourceId);
}
/** The `session/fork` of `sourceId` copied the transcript into `forkId`. */
forkCreated(sourceId: string, forkId: string): void {
this.forkHolds.created(sourceId, forkId);
}
/** The response of a `session/fork` of `sourceId` is out: the list
* subscriptions send the rows they held back. */
forkAnswered(sourceId: string): void {
if (this.forkHolds.answered(sourceId)) this.subscriptions?.deliverPending();
}
dispose(): void {
this.disposed = true;
this.subscriptions?.dispose();

View File

@@ -26,8 +26,6 @@ import { effectiveTitle, visibleTitle } from "./archive-title.js";
import {
canonicalPath,
encodeProjectPath,
errorCode,
exists,
isExactProjectDir,
isSessionId,
normalizePath,
@@ -54,21 +52,16 @@ import {
type SdkTitles,
type TranscriptFacts,
} from "./transcript-scan.js";
import { searchBackgroundTasks } from "./background-tasks.js";
import { DirListings, statFiles } from "./dir-listing.js";
import { isInLinkedWorktree, worktreeCounterparts } from "./worktrees.js";
import { worktreeCounterparts } from "./worktrees.js";
export const DEFAULT_LIST_LIMIT = 50;
export const MAX_LIST_LIMIT = 200;
/** Discovery could not establish which sessions the store contains. */
export class SessionStoreUnavailableError extends Error {}
/** Entries of the metadata cache: a few hundred bytes each, so the
* transcripts of a large history (tens of thousands) stay cached and deep
* pages do not read them again. */
const METADATA_CACHE_SIZE = 50_000;
const READ_BATCH_SIZE = 16;
/** Session ids looked up together by {@link SessionIndex.getSessions}. */
const LOOKUP_CHUNK_SIZE = 100;
/** Transcripts read at most per directory to recover a sibling's cwd. */
const MAX_CWD_PROBES_PER_DIR = 64;
/** How long a directory that gave no cwd is not probed again. */
@@ -103,24 +96,6 @@ export type TranscriptCandidate = {
ino: number;
};
type TranscriptFile = Pick<
TranscriptCandidate,
"sessionId" | "filePath" | "dirName" | "projectPath"
>;
/** Non-empty transcript files, with bounded stats shared by lists and id lookups. */
async function transcriptCandidates(
files: readonly TranscriptFile[],
): Promise<TranscriptCandidate[]> {
const stats = await statFiles(files.map(({ filePath }) => filePath));
return files.flatMap((file, i) => {
const current = stats[i];
return current?.isFile() && current.size > 0
? [{ ...file, mtimeMs: current.mtimeMs, size: current.size, ino: current.ino }]
: [];
});
}
/** The cached metadata of one transcript. */
type TranscriptMetadata = {
/** The visible title: without the archive prefix. */
@@ -150,10 +125,6 @@ export type IndexRow = {
facts: TranscriptFacts;
mtimeMs: number;
archived: boolean;
/** The session runs in a linked git worktree that Claude Code made for
* it: its transcript says so (`facts.agentWorktree`) and `cwd` lies in
* a linked worktree. See {@link markAgentWorktrees}. */
agentWorktree?: true;
};
/** A keyset position: the order key and the session id of the last row. */
@@ -278,22 +249,6 @@ function toRow({ candidate, metadata }: Resolved, cwd: string): IndexRow {
};
}
/**
* Sets `agentWorktree` on the rows whose transcript says Claude Code made
* their worktree and whose cwd lies in a linked worktree now. Only those rows
* are checked; the check is not cached, so a removed worktree shows at once.
*/
async function markAgentWorktrees(rows: IndexRow[]): Promise<IndexRow[]> {
await Promise.all(
rows.map(async (row) => {
if (row.facts.agentWorktree === true && (await isInLinkedWorktree(row.cwd))) {
row.agentWorktree = true;
}
}),
);
return rows;
}
function isAfter(row: { orderAtMs: number; sessionId: string }, cursor: ListCursor): boolean {
return compareRows(row, cursor) > 0;
}
@@ -425,15 +380,7 @@ function scopeOf(
};
}
export type SessionIndexOptions = {
/** Whether a live process holds the session. Only for such a session the
* whole transcript is searched for its background tasks. */
isLive?: (sessionId: string) => Promise<boolean>;
};
export class SessionIndex {
constructor(private readonly options: SessionIndexOptions = {}) {}
private readonly listings = new DirListings();
private readonly metadata = new Lru<string, CacheEntry>(METADATA_CACHE_SIZE);
/** The metadata reads in flight, by path: a list and a subscription that
@@ -550,16 +497,25 @@ export class SessionIndex {
const listed = await this.listings.namesOfAll(dirPaths);
const files = dirs.flatMap(({ dirName, projectPath }, i) =>
listed[i]!.filter((name) => name.endsWith(".jsonl") && isSessionId(name.slice(0, -6))).map(
(name) => ({
sessionId: name.slice(0, -6),
filePath: path.join(root, dirName, name),
dirName,
projectPath,
}),
(name) => ({ name, filePath: path.join(root, dirName, name), dirName, projectPath }),
),
);
const candidates = await transcriptCandidates(files);
return { dirs: [root, ...dirPaths], names: [rootNames, ...listed], candidates };
const stats = await statFiles(files.map(({ filePath }) => filePath));
const all: TranscriptCandidate[] = [];
files.forEach(({ name, filePath, dirName, projectPath }, i) => {
const fileStats = stats[i];
if (!fileStats?.isFile() || fileStats.size === 0) return;
all.push({
sessionId: name.slice(0, -6),
filePath,
dirName,
projectPath,
mtimeMs: fileStats.mtimeMs,
size: fileStats.size,
ino: fileStats.ino,
});
});
return { dirs: [root, ...dirPaths], names: [rootNames, ...listed], candidates: all };
}
/**
@@ -595,90 +551,6 @@ export class SessionIndex {
};
}
/** Known ids in request order, using the same rows as the list.
* Addresses exact filenames; only rows without a cwd need sibling discovery. */
async getSessions(sessionIds: readonly string[]): Promise<IndexRow[]> {
if (sessionIds.length === 0) return [];
const root = projectsRoot();
let projectEntries: string[];
try {
projectEntries = await fs.readdir(root);
} catch (cause) {
if (errorCode(cause) === "ENOENT" && !(await exists(root))) {
return [];
}
throw new SessionStoreUnavailableError("Session store could not be read", { cause });
}
const ids = sessionIds.filter(isSessionId);
const byId = new Map<string, IndexRow>();
// Paths are project directories × ids: bound them per chunk, not per request.
for (let start = 0; start < ids.length; start += LOOKUP_CHUNK_SIZE) {
const chunk = ids.slice(start, start + LOOKUP_CHUNK_SIZE);
for (const row of await this.lookupRows(root, projectEntries, chunk)) {
byId.set(row.sessionId, row);
}
}
return sessionIds.flatMap((id) => {
const row = byId.get(id);
return row ? [row] : [];
});
}
/** The rows of `ids` found under `projectEntries`. All copies of an id are
* in the same call, so copy selection is unchanged by chunking. */
private async lookupRows(
root: string,
projectEntries: readonly string[],
ids: readonly string[],
): Promise<IndexRow[]> {
let candidates = await transcriptCandidates(
projectEntries.flatMap((dirName) =>
ids.map((sessionId) => ({
sessionId,
filePath: path.join(root, dirName, `${sessionId}.jsonl`),
dirName,
})),
),
);
if (CASE_INSENSITIVE_PATHS) {
// On a case-insensitive file system, stat of `ABC.jsonl` also finds
// `abc.jsonl`. Keep a file only when its real name is the requested one,
// or differs by more than case (a symlink, as the list would read it).
// A failed realpath means the file is unreadable, so the id is omitted.
const exact = await Promise.all(
candidates.map(async ({ filePath }) => {
try {
const actual = path.basename(await fs.realpath(filePath));
const expected = path.basename(filePath);
return actual === expected || actual.toLowerCase() !== expected.toLowerCase();
} catch {
return false;
}
}),
);
candidates = candidates.filter((_, i) => exact[i]);
}
return this.collectRows(candidates.sort(byMtimeDescending), {
inScope: scopeOf(undefined),
archived: "all",
wanted: Infinity,
unread: async (read, dirNames) => {
const dirs = [...dirNames];
const listed = await this.listings.namesOfAll(dirs.map((dir) => path.join(root, dir)));
const siblings = dirs.flatMap((dirName, i) =>
listed[i]!.filter((name) => name.endsWith(".jsonl") && isSessionId(name.slice(0, -6)))
.map((name) => ({
sessionId: name.slice(0, -6),
filePath: path.join(root, dirName, name),
dirName,
}))
.filter(({ filePath }) => !read.has(filePath)),
);
return (await transcriptCandidates(siblings)).sort(byMtimeDescending);
},
});
}
/** Every transcript file of `sessionId` (see {@link scanSession}). Empty
* files only with `includeEmpty`. */
async findTranscripts(
@@ -814,14 +686,11 @@ export class SessionIndex {
* alone gives, read from it at once, and the ids of the sessions it gives
* no answer for: a transcript of it has nothing cached at its stat, no cwd
* known, or is continued in another session (whether the successor hides
* it depends on the successor now). Reads no transcript. The rows carry
* `agentWorktree` ({@link markAgentWorktrees}, which reads git files)
* unless `mark` is false, for a caller that uses only their ids.
* it depends on the successor now). Reads no transcript.
*/
async cachedRowsOf(
paths: readonly string[],
candidates: readonly TranscriptCandidate[],
{ mark = true }: { mark?: boolean } = {},
): Promise<{ rows: IndexRow[]; unknown: Set<string> }> {
const unknown = new Set<string>();
const resolved: { item: Resolved; cwd: string }[] = [];
@@ -852,8 +721,7 @@ export class SessionIndex {
if (previous && previous.mtimeMs >= item.candidate.mtimeMs) continue;
newest.set(id, { row: toRow(item, cwd), mtimeMs: item.candidate.mtimeMs });
}
const rows = [...newest.values()].map(({ row }) => row);
return { rows: mark ? await markAgentWorktrees(rows) : rows, unknown };
return { rows: [...newest.values()].map(({ row }) => row), unknown };
}
/**
@@ -877,12 +745,9 @@ export class SessionIndex {
archived: ArchivedFilter;
after?: ListCursor;
wanted: number;
/** The unread transcripts of the directories still eligible for cwd
* recovery. Called only after checking the negative cache. */
unread?: (
read: ReadonlySet<string>,
dirNames: ReadonlySet<string>,
) => readonly TranscriptCandidate[] | Promise<readonly TranscriptCandidate[]>;
/** The transcripts not read (by path), which may supply a sibling's
* cwd. */
unread: (read: ReadonlySet<string>) => readonly TranscriptCandidate[];
restat?: boolean;
},
): Promise<IndexRow[]> {
@@ -954,20 +819,19 @@ export class SessionIndex {
}
await settlePending();
}
const unread = options.unread;
if (pending.length > 0 && unread) {
if (pending.length > 0) {
// A row recovered after the scan stopped still sorts into the page: it
// was read before the stop, and more rows only raise the bound. A
// sibling that the archive filter or the cursor leaves out may still
// supply the cwd.
await this.learnDirCwds(
new Set(pending.map(({ candidate }) => candidate.dirName)),
(dirNames) => unread(read, dirNames),
options.unread(read),
options.restat ?? false,
);
await settlePending();
}
return markAgentWorktrees(rows());
return rows();
}
/** Whether the cached metadata of `candidate`, still current, places it
@@ -996,9 +860,7 @@ export class SessionIndex {
* changes. `restat` as in {@link collectRows}. */
private async learnDirCwds(
dirNames: ReadonlySet<string>,
unread: (
dirNames: ReadonlySet<string>,
) => readonly TranscriptCandidate[] | Promise<readonly TranscriptCandidate[]>,
unread: readonly TranscriptCandidate[],
restatFirst: boolean,
): Promise<void> {
const root = projectsRoot();
@@ -1014,9 +876,8 @@ export class SessionIndex {
if (known && known.mtimeMs === mtimeMs && now - known.at < NO_CWD_RETRY_MS) continue;
mtimes.set(dirName, mtimeMs);
}
if (mtimes.size === 0) return;
const probes = new Map<string, number>();
let remaining = (await unread(new Set(mtimes.keys()))).filter((candidate) => {
let remaining = unread.filter((candidate) => {
if (!mtimes.has(candidate.dirName)) return false;
const count = probes.get(candidate.dirName) ?? 0;
if (count >= MAX_CWD_PROBES_PER_DIR) return false;
@@ -1107,15 +968,14 @@ export class SessionIndex {
this.reads++;
const headTail = await readHeadTail(candidate.filePath, candidate.size);
if (isSidechainTranscript(headTail.head)) return null;
const scanned = await scanTranscriptFile(
const facts = await scanTranscriptFile(
candidate.filePath,
candidate.size,
headTail,
previous,
candidate.ino,
);
if (!scanned.hasMessages) return null;
const facts = await this.withAllBackgroundTasks(candidate, scanned);
if (!facts.hasMessages) return null;
// The last relocation names the session's cwd, as the SDK reads it; the
// messages before it keep the old one.
const fileCwd = await this.recoverCwd(candidate.dirName, [
@@ -1148,24 +1008,6 @@ export class SessionIndex {
};
}
/**
* `facts` with the background tasks of the whole transcript, when the
* tasks of its tail are not complete: a task can start before the tail.
* Only a finished turn of a live session waits for the tasks, so only
* then the whole transcript is read.
*/
private async withAllBackgroundTasks(
candidate: TranscriptCandidate,
facts: TranscriptFacts,
): Promise<TranscriptFacts> {
if (facts.turnState !== "finished") return facts;
if (facts.backgroundTasks?.complete) return facts;
const isLive = await this.options.isLive?.(candidate.sessionId);
if (!isLive) return facts;
const backgroundTasks = await searchBackgroundTasks(candidate.filePath, candidate.size);
return { ...facts, backgroundTasks };
}
/**
* The titles the SDK gives the listed transcript (`customTitle` and
* `summary` of its `getSessionInfo`), by the SDK's rule from the head and

View File

@@ -4,7 +4,7 @@
* the RFDs in `_meta.jetbrains.air`.
*/
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import type { SessionInfo } from "@agentclientprotocol/sdk";
import { withAirMeta } from "../air-extension.js";
import { deriveActivity, selectCost, type OwnSessionState } from "./activity.js";
import type { LiveRecord } from "./live-registry.js";
@@ -38,7 +38,6 @@ export function sessionInfoOf(
lastPromptAt: iso(facts.lastPromptAt),
model: facts.model,
forkedFrom: facts.forkedFrom,
agentWorktree: row.agentWorktree,
state: activity?.state,
lastTurnEndedAt: activity?.lastTurnEndedAt,
cost: cost === undefined ? undefined : { amount: cost, currency: "USD" },

View File

@@ -7,13 +7,9 @@
*
* A last message longer than the tail window leaves no message in it: then
* the tail window grows, up to {@link MAX_TAIL_SIZE}, until it holds one.
*
* The open background tasks come from the tail, on top of the tasks of an
* earlier scan (see `background-tasks.ts`).
*/
import * as fs from "node:fs/promises";
import { backgroundTasksOf, type BackgroundTasks } from "./background-tasks.js";
import { firstPrompt, isUserPrompt, mediaPrompt, promptOf } from "./first-prompt.js";
const CHUNK_SIZE = 64 * 1024;
@@ -55,14 +51,6 @@ export type TranscriptFacts = {
/** The session this one was forked from: the `forkedFrom.sessionId` that
* the SDK and CLI fork write on every copied record. */
forkedFrom?: string;
/** What the last `worktree-state` record says: true when its
* `worktreeSession` is set and not `enteredExisting` (Claude Code made the
* worktree and the session is in it), false when it says otherwise.
* Undefined when no such record was read. See {@link agentWorktreeOf}. */
agentWorktree?: boolean;
/** The background tasks that did not end. From the tail, on top of what
* an earlier scan found when the file only grew within the tail. */
backgroundTasks?: BackgroundTasks;
/** Whether the head or the tail holds a user or an assistant message. A
* transcript without one is a metadata-only stub. */
hasMessages: boolean;
@@ -79,12 +67,12 @@ export type HeadTail = {
};
/** Facts that an earlier scan may have found before the tail window. */
const INHERITED_FACTS = ["model", "tailCwd", "lastTurnEndedAt", "agentWorktree"] as const;
const INHERITED_FACTS = ["model", "tailCwd", "lastTurnEndedAt"] as const;
/** What an earlier scan of the same file found, at `size`. */
export type PreviousScan = { size: number } & Pick<
TranscriptFacts,
"lastPromptAt" | "promptSearched" | "backgroundTasks" | (typeof INHERITED_FACTS)[number]
"lastPromptAt" | "promptSearched" | (typeof INHERITED_FACTS)[number]
>;
/** The last `window` bytes of a file of `size` bytes, without the first,
@@ -164,14 +152,6 @@ export async function scanTranscriptFile(
}
}
}
const previousTasks = grownWithinTail ? previous.backgroundTasks : undefined;
facts.backgroundTasks = tailBackgroundTasks(read.tail, size, previousTasks);
// No worktree record in the tail (or before it, by an earlier scan): the
// one `claude --worktree` writes at the start may still be in the head.
if (facts.agentWorktree === undefined) {
const agentWorktree = agentWorktreeOf(read.head);
if (agentWorktree !== undefined) facts.agentWorktree = agentWorktree;
}
if (facts.lastPromptAt !== undefined || size <= CHUNK_SIZE) {
facts.promptSearched = true;
} else if (grownWithinTail && (previous.lastPromptAt !== undefined || previous.promptSearched)) {
@@ -183,8 +163,7 @@ export async function scanTranscriptFile(
if (!facts.hasMessages || complete(facts) || size <= CHUNK_SIZE) return facts;
const key = `${filePath}\0${size}\0${identity ?? ""}`;
const searched = fullySearched.get(key);
// The tasks can differ by the earlier scan.
if (searched) return { ...searched, backgroundTasks: facts.backgroundTasks };
if (searched) return searched;
let result = facts;
const handle = await fs.open(filePath, "r");
try {
@@ -220,15 +199,9 @@ export async function scanTranscriptFile(
...wider,
...(facts.model !== undefined && { model: facts.model }),
...(facts.tailCwd !== undefined && { tailCwd: facts.tailCwd }),
// The wider tail holds the narrow one, so a record it has is the
// latest; else the narrow scan's (inherited or from the head) stands.
...((wider.agentWorktree ?? facts.agentWorktree) !== undefined && {
agentWorktree: wider.agentWorktree ?? facts.agentWorktree,
}),
// A turn end the wider tail lacks may lie before it: the
// narrow scan inherited it from an earlier scan of this file.
lastTurnEndedAt: wider.lastTurnEndedAt ?? facts.lastTurnEndedAt,
backgroundTasks: facts.backgroundTasks,
hasMessages: facts.hasMessages,
lastPromptAt,
// Searched to the start of the file, or found.
@@ -248,22 +221,6 @@ export async function scanTranscriptFile(
return result;
}
/**
* The background tasks of the tail of a file of `size` bytes. `previous`:
* the tasks of an earlier scan, when the file only grew within the tail
* since. The set is complete when the tail is the whole file, or when it
* holds all records after a complete earlier set.
*/
function tailBackgroundTasks(
tail: string,
size: number,
previous: BackgroundTasks | undefined,
): BackgroundTasks {
if (size <= CHUNK_SIZE) return backgroundTasksOf(tail, { complete: true });
if (previous?.complete) return backgroundTasksOf(tail, { complete: true, after: previous });
return backgroundTasksOf(tail, { complete: false });
}
const RELOCATED_MARKER = '"relocated"';
/** The cwd of the last `relocated` record of the tail: where the session
@@ -281,43 +238,6 @@ export function relocatedCwd(tail: string): string | undefined {
return undefined;
}
const WORKTREE_STATE_MARKER = '"worktree-state"';
/**
* Whether the last `worktree-state` record of `text` says the session is in
* a worktree that Claude Code made for it, as AIR's native Claude provider
* reads it: a `worktreeSession` that is an object without
* `enteredExisting: true`. The last line that parses as a `worktree-state`
* record decides; any other `worktreeSession` (`null`, which the CLI writes
* when the session leaves the worktree, missing, or no object) means false.
* Only lines that are not JSON (such as the cut first line of a window) are
* skipped. Undefined when `text` holds no such record.
*
* The CLI writes the record when it enters or leaves a worktree (for
* `claude --worktree`, at the start of the session, so in the head), and
* writes it again with the other session metadata at the end of the
* transcript (on exit and as the transcript grows), so the tail usually
* holds the latest one. Only the head and the tail are read: a session that
* entered a worktree midway through a long transcript, with no metadata
* written after it yet, is not seen until the CLI writes the metadata again.
*/
export function agentWorktreeOf(text: string): boolean | undefined {
if (!text.includes(WORKTREE_STATE_MARKER)) return undefined;
const lines = text.split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i]!;
if (!line.includes(WORKTREE_STATE_MARKER)) continue;
const entry = parseLine(line);
// Only a line that is no JSON record is skipped: the latest record that
// parses decides, even when its `worktreeSession` is missing or no object.
if (entry?.type !== "worktree-state") continue;
const session = entry.worktreeSession;
if (!session || typeof session !== "object" || Array.isArray(session)) return false;
return (session as { enteredExisting?: unknown }).enteredExisting !== true;
}
return undefined;
}
/** The cwd a transcript belongs to, as the SDK reads it: the last
* `relocated` record of the tail, else the first cwd of the head. */
export function transcriptProjectCwd({ head, tail }: HeadTail): string | undefined {
@@ -424,10 +344,6 @@ export function scanTranscript(
if (headCwd !== undefined) facts.headCwd = decodeJsonString(headCwd);
const forkedFrom = FORKED_FROM_PATTERN.exec(head)?.[1];
if (forkedFrom !== undefined) facts.forkedFrom = forkedFrom;
// The tail only: scanTranscriptFile falls back to the head after it
// inherits what an earlier scan found before the tail.
const agentWorktree = agentWorktreeOf(tail);
if (agentWorktree !== undefined) facts.agentWorktree = agentWorktree;
const lines = tail.split("\n");
let turnEndFound = false;

View File

@@ -115,18 +115,6 @@ export async function worktreeCounterparts(cwd: string): Promise<string[]> {
return [...new Set(existing.filter((value): value is string => value !== undefined))];
}
/**
* Whether `cwd` lies in a linked worktree: its git dir is
* `<common-dir>/worktrees/<name>`. False in the main worktree, in a
* submodule and outside a repository.
*/
export async function isInLinkedWorktree(cwd: string): Promise<boolean> {
const gitDir = await findGitDir(cwd);
if (!gitDir) return false;
const commonDir = await commonDirOf(gitDir);
return path.resolve(path.dirname(gitDir)) === path.resolve(commonDir, "worktrees");
}
/**
* Every existing worktree of the repository that contains `cwd`: the main
* worktree and the linked ones. Empty outside a repository.

View File

@@ -5,7 +5,7 @@ import {
type SessionNotification,
type SetSessionModeRequest,
type SetSessionModeResponse,
} from "@agentclientprotocol/sdk/unstable";
} from "@agentclientprotocol/sdk";
import type {
ModelInfo,
PermissionMode,

View File

@@ -1,7 +1,6 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import type { ModelInfo, Query, Settings } from "@anthropic-ai/claude-agent-sdk";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, withAirMeta } from "./air-extension.js";
import { bedrockRegionPrefix } from "./gateway-models.js";
export const MODEL_CONFIG_ID = "model";
@@ -263,47 +262,31 @@ function resolveSettingsModel(
const MODEL_FAMILY_ALIASES = new Set(["opus", "sonnet", "haiku", "fable"]);
/** A model id without the spellings that name the same model: provider
* prefixes, dates, version suffixes, `:variant` suffixes, dotted versions,
* `-fast` and context hints. `deniedModels` and gateway listings both match on
* it. OpenRouter lists `anthropic/claude-opus-5.5:batch[1m]`, which normalizes
* to `claude-opus-5-5`; `anthropic/openai/gpt-5` keeps its `openai/` prefix and
* matches no Claude id. */
function normalizeModelId(value: string): string {
/** A model id without the spellings `deniedModels` ignores: provider
* prefixes, dates, version suffixes, `-fast` and context hints. */
function normalizeDeniedModelId(value: string): string {
let id = stripContextHints(value.trim().toLowerCase());
const providerPrefix = id.lastIndexOf("anthropic.");
if (providerPrefix !== -1) id = id.slice(providerPrefix + "anthropic.".length);
return (
id
// Only a leading `anthropic/`: OpenRouter's `anthropic/openai/gpt-5` must
// keep `openai/`, which `lastIndexOf` would strip along with it.
.replace(/^anthropic\//, "")
// `:batch`, `:free`. A letter must follow the colon, so Bedrock's `-v1:0`
// is left for the `-vN:M` step below.
.replace(/:[a-z][a-z0-9-]*$/, "")
// `claude-opus-5.5` -> `claude-opus-5-5`; only dots between two digits.
.replace(/(?<=\d)\.(?=\d)/g, "-")
.replace(/@.*$/, "")
.replace(/-v\d+(?::\d+)?$/, "")
.replace(/-fast$/, "")
.replace(/-\d{8}$/, "")
);
return id
.replace(/@.*$/, "")
.replace(/-v\d+(?::\d+)?$/, "")
.replace(/-fast$/, "")
.replace(/-\d{8}$/, "");
}
/**
* Whether managed `deniedModels` blocks a model id, following the CLI's rules
* closely enough for the picker: a family alias blocks the whole family, and a
* model id blocks that version in every spelling plus the later minor versions
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). Entries share
* `normalizeModelId`, so `claude-opus-4.5` or `anthropic/claude-opus-4.5` also
* blocks `claude-opus-4-5`, which the CLI may still allow. The CLI stays the
* enforcer; this only keeps the picker from offering a refused row.
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). The CLI stays
* the enforcer; this only keeps the picker from offering a refused row.
*/
export function isDeniedModel(modelId: string, deniedModels: readonly string[]): boolean {
const id = normalizeModelId(modelId);
const id = normalizeDeniedModelId(modelId);
if (!id) return false;
return deniedModels.some((entry) => {
const denied = normalizeModelId(entry);
const denied = normalizeDeniedModelId(entry);
if (!denied) return false;
if (MODEL_FAMILY_ALIASES.has(denied)) return id === denied || id.includes(`-${denied}`);
return id === denied || id.startsWith(`${denied}-`);
@@ -396,48 +379,6 @@ export function applyAvailableModelsAllowlist(
return result;
}
/**
* Keep the SDK models the `providers/set` gateway listed (`listGatewayModels`,
* `listBedrockModels`). A row matches when its `resolvedModel` (or
* its `value` when the SDK gives none) names a listed model in any spelling:
* `claude-sonnet-5[1m]` matches a listed `claude-sonnet-5-20260101`, and
* `eu.anthropic.claude-sonnet-5` matches a listed `eu.anthropic.claude-sonnet-5-v1:0`.
*
* With a Bedrock `regionPrefix`, a row resolved to another cross-region prefix
* (`us.anthropic.claude-sonnet-5` under `eu`) is dropped even when the same
* model is listed under `eu`: the CLI would call it in the other region.
*
* A row the SDK gives no `resolvedModel` names its model only by an alias, so
* it is checked as far as the alias allows: `default` is kept, and a family
* alias such as `opus` is kept when the gateway lists a model of that family.
*
* When no row matches by its model (a `default` row kept only for lacking a
* `resolvedModel` does not count), the gateway serves none of the SDK's
* models, and nothing is left: the session offers no model.
*/
export function narrowToGatewayModels(
models: ModelInfo[],
listedModelIds: readonly string[],
regionPrefix?: string,
): ModelInfo[] {
const listed = new Set(listedModelIds.map(normalizeModelId).filter(Boolean));
const narrowed = models.filter((model) => {
const modelId = model.resolvedModel ?? model.value;
const prefix = bedrockRegionPrefix(modelId);
if (regionPrefix !== undefined && prefix !== undefined && prefix !== regionPrefix) {
return false;
}
const id = normalizeModelId(modelId);
if (listed.has(id)) return true;
if (model.resolvedModel !== undefined) return false;
if (model.value === "default") return true;
return MODEL_FAMILY_ALIASES.has(id) && [...listed].some((entry) => entry.includes(`-${id}`));
});
return narrowed.some((model) => listed.has(normalizeModelId(model.resolvedModel ?? model.value)))
? narrowed
: [];
}
/** Whether a rejected `setModel` was vetoed by a user-configured
* PreModelSwitch hook. The CLI's rejection message is the stable,
* distinguishable marker ("Model switch blocked by a PreModelSwitch hook:
@@ -456,18 +397,7 @@ export async function getAvailableModels(
isResumedSession: boolean,
sessionId: string,
resumedModelHint?: string,
narrowedByGateway = false,
): Promise<SessionModelState> {
// A gateway that lists none of the SDK's models leaves none to offer
// (`narrowToGatewayModels`). The session stays on the model the SDK runs,
// and the gateway refuses its requests.
if (models.length === 0) {
return {
availableModels: [],
currentModelId: (isResumedSession && resumedModelHint) || sdkModels[0]?.value || "default",
};
}
const settings = settingsManager.getSettings();
let currentModel = models[0];
@@ -514,34 +444,9 @@ export async function getAvailableModels(
// be re-asserted to keep the reported model truthful.
// Anything else (fuzzy match, allowlist-synthesized value, alias) gets a
// setModel call so we don't drift from the user's intended pin.
// Case (a) does not hold when `narrowToGatewayModels` left the SDK on a
// model the gateway does not list: the dropped `default` row of a fresh or
// transcript-less session, or a resumed transcript model outside the list.
// Such a session switches to models[0].
const sdkSawSameValue = sdkModels.some((m) => m.value === currentModel.value);
const sdkDefault = sdkModels.find((m) => m.value === "default");
const sdkDefaultDropped = sdkDefault !== undefined && !models.some((m) => m.value === "default");
let switchToListed = false;
if (resolvedFromInput === undefined) {
if (!isResumedSession || !resumedModelHint) {
switchToListed = sdkDefaultDropped;
} else if (
narrowedByGateway &&
(!models.some((m) => m.value === currentModel.value) ||
// A loose match can map the transcript's model onto a listed row of
// another version, which the CLI does not resume on.
(currentModel.resolvedModel !== undefined &&
normalizeModelId(currentModel.resolvedModel) !== normalizeModelId(resumedModelHint)))
) {
currentModel = models[0];
switchToListed = true;
}
}
// When the switch fails, the SDK stays on the model it ran before it.
const modelBeforeSwitch = (): ModelInfo =>
switchToListed && sdkDefault !== undefined ? sdkDefault : models[0];
const skipSetModel =
(resolvedFromInput === undefined && !switchToListed) ||
resolvedFromInput === undefined ||
(!isResumedSession && currentModel.value === resolvedFromInput && sdkSawSameValue);
if (!skipSetModel) {
const setModelStartedAt = performance.now();
@@ -573,19 +478,15 @@ export async function getAvailableModels(
// live model back on this path: getContextUsage isn't serviced on a
// fresh session until the first prompt turn (issues #886/#880).
if (!isResumedSession) {
// A switch onto a listed model is advisory too: the session stays on
// the model the SDK runs and reports it.
if (!switchToListed && !isPreModelSwitchHookBlock(error)) throw error;
if (!isPreModelSwitchHookBlock(error)) throw error;
logger.error(
`Model switch to "${currentModel.value}" failed or was vetoed by a PreModelSwitch hook; staying on the default model:`,
`Model pin "${currentModel.value}" was vetoed by a PreModelSwitch hook; staying on the default model:`,
error,
);
currentModel = modelBeforeSwitch();
currentModel = models[0];
} else {
logger.error(`Failed to re-assert model "${currentModel.value}" on resume:`, error);
currentModel = resumedModelHint
? matchResumedModel(models, resumedModelHint)
: modelBeforeSwitch();
currentModel = resumedModelHint ? matchResumedModel(models, resumedModelHint) : models[0];
}
}
}
@@ -595,107 +496,14 @@ export async function getAvailableModels(
for (const name of displayNames) {
displayNameCounts.set(name, (displayNameCounts.get(name) ?? 0) + 1);
}
const names = models.map((model, index) =>
displayNameCounts.get(displayNames[index]) === 1 ? displayNames[index] : model.displayName,
);
const nameCounts = new Map<string, number>();
for (const name of names) {
nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
}
return {
availableModels: models.map((model, index) => ({
modelId: model.value,
// Allowlisted aliases can inherit the same SDK label. Keep every ID
// selectable, but distinguish the rows for clients that show only names.
name: nameCounts.get(names[index]) === 1 ? names[index] : `${names[index]} (${model.value})`,
name:
displayNameCounts.get(displayNames[index]) === 1 ? displayNames[index] : model.displayName,
description: model.description,
})),
currentModelId: currentModel.value,
};
}
/** The display name of the model id `model` in `modelInfos`, if it is there. */
export function modelDisplayName(modelInfos: ModelInfo[], model: string): string | undefined {
let byModel = displayNameCache.get(modelInfos);
if (!byModel) displayNameCache.set(modelInfos, (byModel = new Map()));
if (!byModel.has(model)) {
const info = modelInfos.find((info) => info.value === model || info.resolvedModel === model);
byModel.set(model, info?.displayName);
}
return byModel.get(model);
}
/** {@link modelDisplayName} per model list, which a session replaces and never edits. */
const displayNameCache = new WeakMap<ModelInfo[], Map<string, string | undefined>>();
/** The flat entries of a session's `model` select option. */
function modelOptionEntries(option: SessionConfigOption): Array<{ value: string; name: string }> {
if (option.type !== "select") return [];
return option.options.flatMap((entry) =>
"value" in entry ? [{ value: entry.value, name: entry.name }] : entry.options,
);
}
/**
* The entry of the `model` option among `configOptions` for `model`, a model
* id that a response reported, so that a client can compare it with the
* session's own model. Best first:
*
* 1. the current entry, when it runs that model up to a context hint: a
* session on `opus[1m]` or `default` that answers as `claude-opus-4-6`;
* 2. the entry whose value or resolved model is that id;
* 3. an entry that runs that model up to a context hint.
*
* Among several entries for one model, a concrete entry comes before
* `default`, as in {@link resolveModelPreference}. Undefined when no entry
* matches. A model's resolved id comes from `modelInfos`
* (`ModelInfo.resolvedModel`), the wire id that a response reports, as for
* {@link matchResumedModel}. It is compared as it is: a dated id such as
* `claude-haiku-4-5-20251001` matches only the same dated id.
*/
export function modelEntryFor(
configOptions: readonly SessionConfigOption[],
modelInfos: ModelInfo[],
model: string,
): { value: string; name: string } | undefined {
// Each response of a subagent reports its model, so the result is kept per
// list. A session replaces its lists on a change and never edits them.
let byInfos = modelEntryCache.get(configOptions);
if (!byInfos) modelEntryCache.set(configOptions, (byInfos = new WeakMap()));
let byModel = byInfos.get(modelInfos);
if (!byModel) byInfos.set(modelInfos, (byModel = new Map()));
if (byModel.has(model)) return byModel.get(model);
const entry = findModelEntry(configOptions, modelInfos, model);
byModel.set(model, entry);
return entry;
}
const modelEntryCache = new WeakMap<
readonly SessionConfigOption[],
WeakMap<ModelInfo[], Map<string, { value: string; name: string } | undefined>>
>();
function findModelEntry(
configOptions: readonly SessionConfigOption[],
modelInfos: ModelInfo[],
model: string,
): { value: string; name: string } | undefined {
const option = configOptions.find((candidate) => candidate.id === MODEL_CONFIG_ID);
if (option?.type !== "select") return undefined;
// Concrete entries first; `sort` keeps the list order otherwise.
const entries = modelOptionEntries(option).sort(
(left, right) => Number(left.value === "default") - Number(right.value === "default"),
);
const observed = canonicalizeModelId(model);
const resolved = (value: string) =>
canonicalizeModelId(modelInfos.find((info) => info.value === value)?.resolvedModel ?? value);
const sameModel = (value: string) =>
stripContextHints(resolved(value)) === stripContextHints(observed);
const current = entries.find((entry) => entry.value === option.currentValue);
if (current && sameModel(current.value)) return current;
return (
entries.find((entry) => entry.value === model || resolved(entry.value) === observed) ??
entries.find((entry) => sameModel(entry.value))
);
}

View File

@@ -1,4 +1,4 @@
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
/** Fire-and-forget advisory text for the user: live events rather than
* conversation history. See the ACP Session Notices RFD

View File

@@ -1,25 +1,20 @@
/**
* AI-generated session titles.
*
* Claude Code titles a new SDK session itself: when it reads the first prompt,
* it asks a small model for a title and stores it in the transcript as an
* `ai-title` entry, a few seconds in. It sends no message when it does. This
* module asks for the title with the `generate_session_title` control request,
* which returns the stored title or waits for the one being generated, and
* publishes it over ACP as a `session_info_update`. Where the CLI does not
* title the session itself (a resumed session, or one with
* `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or
* `CLAUDE_CODE_DISABLE_TERMINAL_TITLE` set), the same request generates it.
* Claude Code's own auto-title generation never runs under the Agent SDK — the
* latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
* degrades to the raw first prompt. This module asks the CLI for a real title
* instead, via the `generate_session_title` control request, and publishes it
* over ACP as a `session_info_update`.
*
* {@link SessionTitles} holds the per-session state and is owned by `Session`.
* `acp-agent.ts` drives it from five places: {@link SessionTitles.onPrompt} and
* `acp-agent.ts` drives it from four places: {@link SessionTitles.onPrompt} and
* {@link SessionTitles.onAssistantText} to collect text,
* {@link SessionTitles.onPromptQueued} once a prompt is sent to the CLI,
* {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
* {@link SessionTitles.reset} on `conversation_reset`.
*/
import type { ContentBlock, PromptRequest } from "@agentclientprotocol/sdk/unstable";
import type { ContentBlock, PromptRequest } from "@agentclientprotocol/sdk";
import { getSessionInfo, type Query, type SDKSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent, Session } from "./acp-agent.js";
import { ExplicitTitle } from "./session-index/explicit-title.js";
@@ -82,11 +77,6 @@ export function appendTitleContext(previous: string | undefined, text: string):
return next.length > MAX_TITLE_CONTEXT_LENGTH ? next.slice(-MAX_TITLE_CONTEXT_LENGTH) : next;
}
/** The text chunks of a prompt, one per line. */
function promptText(prompt: PromptRequest["prompt"]): string {
return prompt.flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : [])).join("\n");
}
/** Whether this SDK still offers on-demand title generation. */
function supportsTitleGeneration(query: Query): boolean {
return typeof (query as TitleCapableQuery).generateSessionTitle === "function";
@@ -110,11 +100,6 @@ export class SessionTitles {
* one. Released by {@link reset}, and when generation yields nothing. */
private settled = false;
/** Whether {@link onPromptQueued} asked for a title from the latest prompt.
* A turn end then does not ask again: like the CLI, a prompt that yields
* no title gets one try, and the next prompt tries again. */
private askedAtPrompt = false;
/** The title a client set, which no generated title replaces. */
private readonly explicit = new ExplicitTitle();
@@ -147,33 +132,11 @@ export class SessionTitles {
if (this.settled) {
return;
}
this.context = appendTitleContext(this.context, `\n${promptText(prompt)}\n`);
}
const promptText = prompt
.flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : []))
.join("\n");
/** Ask for the title as soon as a prompt is sent to the CLI, instead of
* waiting for `idle`: `idle` comes only after the whole turn and its
* background tasks, often minutes after the CLI stored its title. The CLI
* answers `generate_session_title` while the turn runs, and with the title
* it stored or is generating, so this makes no second model call. The
* prompt alone is the description, because the CLI titles from the prompt
* alone. When this yields no title, the stored summary is published and
* the next prompt tries again. */
onPromptQueued(session: Session, prompt: PromptRequest["prompt"]): void {
this.askedAtPrompt = false;
// `session.cancelled` is not checked: after a cancelled turn it stays set
// until the next turn starts, which is after this prompt is queued.
if (this.settled || this.explicit.active || session.queryClosed) {
return;
}
if (!supportsTitleGeneration(session.query)) {
return;
}
const description = promptText(prompt).trim();
if (description.length < MIN_TITLE_CONTEXT_LENGTH) {
return;
}
this.askedAtPrompt = true;
this.startGeneration(() => this.titleFromPrompt(session, description));
this.context = appendTitleContext(this.context, `\n${promptText}\n`);
}
/** Collect the assistant's own answer for the title. Chunks are appended
@@ -189,7 +152,6 @@ export class SessionTitles {
* `conversation_reset`, which mounts a fresh transcript. */
reset(): void {
this.settled = false;
this.askedAtPrompt = false;
this.explicit.reset();
this.context = undefined;
this.lastTitle = undefined;
@@ -203,12 +165,10 @@ export class SessionTitles {
* title — a user `/rename` or one generated earlier; the SDK folds both into
* that one field. Adopt it and latch, so neither is ever titled over.
*
* With no title yet, and none asked for at the latest prompt (see
* {@link onPromptQueued}), ask the SDK to generate one in the background
* from the prompts and replies so far: it is a ~2s small-model call and
* turn-end must not wait on it. That covers a prompt too short to title
* from, such as "hi". Only when that is not possible do we fall back to
* `summary`, which for an SDK-driven session is just the raw first prompt. */
* With no title yet, ask the SDK to generate one in the background: it is a
* ~2s small-model call and turn-end must not wait on it. Only when that is
* not possible do we fall back to `summary`, which for an SDK-driven session
* is just the raw first prompt. */
async onTurnEnd(session: Session): Promise<void> {
if (this.explicit.settledByClient) {
// No title is generated over the one the client set, but a custom title
@@ -240,9 +200,17 @@ export class SessionTitles {
? { title: info.summary, lastModified: info.lastModified }
: undefined;
if (!this.askedAtPrompt && this.canRequest(session)) {
const description = this.context?.trim() ?? "";
this.startGeneration(() => this.requestGenerateTitle(session, description, fallback));
if (this.canRequest(session)) {
this.settled = true;
this.explicit.track(
this.requestGenerateTitle(session, fallback).catch((error) => {
this.agent.logger.error(
`Session ${this.sessionId}: session title update failed: ${error}`,
);
}),
);
return;
}
// Only while this session has no title of its own: `summary` degrades to the
@@ -341,48 +309,17 @@ export class SessionTitles {
return (this.context?.trim().length ?? 0) >= MIN_TITLE_CONTEXT_LENGTH;
}
/** Latch the title and run `generation` in the background, so neither the
* prompt nor turn-end waits on a model call. A client title change waits
* for it to finish (see {@link ExplicitTitle.track}). */
private startGeneration(generation: () => Promise<void>): void {
this.settled = true;
this.explicit.track(
generation().catch((error) => {
this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
}),
);
}
/** The prompt-time title: the one the session file already holds (a resumed
* session's title, or a `/rename`), else one generated from `description`.
* The file is read first because the CLI checks only the titles it holds in
* memory before generating one. */
private async titleFromPrompt(session: Session, description: string): Promise<void> {
const info = await this.readSessionInfo(session);
if (this.agent.sessions[this.sessionId] !== session || this.explicit.active) {
return;
}
if (info?.customTitle) {
this.context = undefined;
await this.publish(info.customTitle, info.lastModified);
return;
}
const fallback = info?.summary
? { title: info.summary, lastModified: info.lastModified }
: undefined;
await this.requestGenerateTitle(session, description, fallback);
}
/** Generate a title from `description` and publish it. `persist` writes it
* to the session file, which is what carries it into `session/list` and
* `session/load`. A null or failed generation releases the latch so a later
* prompt or turn end retries, and publishes `fallback` (the stored summary)
* so a backend that can't generate titles is no worse off than before. */
/** Generate a title from the session's own text and publish it. `persist`
* writes it to the session file, which is what carries it into
* `session/list` and `session/load`. A null or failed generation releases the
* latch so a later turn retries, and publishes `fallback` (the stored
* summary) so a backend that can't generate titles is no worse off than
* before. */
private async requestGenerateTitle(
session: Session,
description: string,
fallback: TitleFallback | undefined,
): Promise<void> {
const description = this.context?.trim() ?? "";
let title: string | null = null;
try {

View File

@@ -1,136 +0,0 @@
import type {
SessionConfigOption,
SetSessionConfigOptionRequest,
} from "@agentclientprotocol/sdk/unstable";
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
import { ULTRACODE_CONFIG_ID } from "./session-config-ids.js";
export { ULTRACODE_CONFIG_ID } from "./session-config-ids.js";
/** Select-fallback values for clients without boolean config options. */
export const ULTRACODE_ON = "on";
export const ULTRACODE_OFF = "off";
const ULTRACODE_DESCRIPTION = "Dynamic workflows on every task, at any effort level";
/** Ultracode state from the CLI's `get_settings` control response
* (`applied.ultracodeAvailable` / `applied.ultracodeRequested`). Each field is
* undefined when the CLI does not report it. */
export type UltracodeSettingsReport = {
/** Workflows are enabled and the current model supports ultracode. */
available?: boolean;
/** Ultracode is turned on for the session, whether or not it can take
* effect on the current model. */
requested?: boolean;
};
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
// SDK's type declarations as of 0.3.310.
type GetSettingsQuery = Query & {
getSettings?: () => Promise<{
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };
}>;
};
/** Ask the CLI whether ultracode is available and requested for the session.
* Returns undefined when the SDK has no `getSettings` or the request fails;
* callers then fall back to {@link modelSupportsUltracode}. */
export async function readUltracodeSettings(
query: Query,
): Promise<UltracodeSettingsReport | undefined> {
const getSettings = (query as GetSettingsQuery).getSettings;
if (typeof getSettings !== "function") return undefined;
try {
const applied = (await getSettings.call(query))?.applied;
return {
available:
typeof applied?.ultracodeAvailable === "boolean" ? applied.ultracodeAvailable : undefined,
requested:
typeof applied?.ultracodeRequested === "boolean" ? applied.ultracodeRequested : undefined,
};
} catch {
return undefined;
}
}
/** The CLI supports ultracode on exactly the models that support `xhigh`
* effort (both use the same capability check), and `supportedEffortLevels`
* lists `xhigh` only for those models. E.g. Opus 4.7+, Sonnet 5+ and Haiku
* 5.5 qualify; Opus 4.6 and Haiku 4.5 do not. */
export function modelSupportsUltracode(modelInfo: ModelInfo | undefined): boolean {
return (
modelInfo?.supportsEffort === true &&
modelInfo.supportedEffortLevels?.includes("xhigh") === true
);
}
/** Whether dynamic workflows are enabled for the session, inferred from the
* CLI's `ultracodeAvailable` for the model the session started on. This is
* the part of availability that does not change with the model: the plan
* default (off on Pro), `enableWorkflows`/`disableWorkflows` and org policy.
* Undefined when the report can't tell, e.g. `ultracodeAvailable: false` on a
* model that lacks ultracode support anyway. */
export function ultracodeWorkflowsEnabled(
report: UltracodeSettingsReport | undefined,
modelInfo: ModelInfo | undefined,
): boolean | undefined {
if (report?.available === true) return true;
if (report?.available === false && modelSupportsUltracode(modelInfo)) return false;
return undefined;
}
/** Whether to offer the ultracode toggle for `modelInfo`. An unknown workflows
* state shows the toggle: the CLI keeps an unavailable request without
* error, so the toggle is merely inert there. */
export function ultracodeSupported(
workflowsEnabled: boolean | undefined,
modelInfo: ModelInfo | undefined,
): boolean {
return workflowsEnabled !== false && modelSupportsUltracode(modelInfo);
}
/** Ultracode state threaded into `buildConfigOptions`. The option is only
* surfaced while `supported`. */
export type UltracodeOptionState = {
supported: boolean;
enabled: boolean;
/** Whether the client opted into boolean config options. */
useBooleanOption: boolean;
};
/** Build the ultracode toggle: a native boolean for clients that opted into
* boolean config options, otherwise an "on"/"off" select. */
export function createUltracodeConfigOption(
enabled: boolean,
useBooleanOption: boolean,
): SessionConfigOption {
const base = {
id: ULTRACODE_CONFIG_ID,
name: "Ultracode",
description: ULTRACODE_DESCRIPTION,
category: "model_config",
} as const;
if (useBooleanOption) {
return { ...base, type: "boolean", currentValue: enabled };
}
return {
...base,
type: "select",
currentValue: enabled ? ULTRACODE_ON : ULTRACODE_OFF,
options: [
{ value: ULTRACODE_ON, name: "On" },
{ value: ULTRACODE_OFF, name: "Off" },
],
};
}
/** Read the requested ultracode value from a `session/set_config_option`
* request: a boolean, or the "on"/"off" select-fallback strings. */
export function resolveUltracodeEnabled(params: SetSessionConfigOptionRequest): boolean {
const value = params.value;
if (typeof value === "boolean") return value;
if (value === ULTRACODE_ON) return true;
if (value === ULTRACODE_OFF) return false;
throw new Error(`Invalid value for config option ${ULTRACODE_CONFIG_ID}: ${value}`);
}

View File

@@ -50,7 +50,6 @@ export class SettingsManager {
private cwd: string;
private effective: Settings = {};
private managedDeniedModels: string[] = [];
private managedApiKeyHelper?: string;
private watchers: fs.FSWatcher[] = [];
private onChange?: () => void;
private logger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
@@ -112,15 +111,10 @@ export class SettingsManager {
this.managedDeniedModels = resolved.sources
.filter((entry) => entry.source === "managed")
.flatMap((entry) => entry.settings.deniedModels ?? []);
this.managedApiKeyHelper =
resolved.provenance.apiKeyHelper?.source === "managed"
? resolved.effective.apiKeyHelper
: undefined;
} catch (error) {
this.logger.error("Failed to resolve settings:", error);
this.effective = {};
this.managedDeniedModels = [];
this.managedApiKeyHelper = undefined;
}
}
@@ -183,16 +177,6 @@ export class SettingsManager {
return this.effective;
}
/**
* The `apiKeyHelper` the CLI runs: managed settings outrank the `--settings`
* override, which outranks the user/project/local files.
*/
getApiKeyHelper(settingsOverride?: Settings): string | undefined {
return (
this.managedApiKeyHelper ?? settingsOverride?.apiKeyHelper ?? this.effective.apiKeyHelper
);
}
/**
* `deniedModels` entries from the managed tier, the only tier the CLI
* honors them from.

View File

@@ -1,35 +1,12 @@
import {
getSessionInfo,
getSubagentMessages,
type SessionMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { createReadStream } from "node:fs";
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import readline from "node:readline";
import { isNativeSubagentControlTool, type HistoricalSubagent } from "./native-subagents.js";
import { findTranscript } from "./resumed-session.js";
/** What the metadata file of a subagent says about it. */
export interface SubagentRecord {
/** The SDK agent id, the name of its transcript. */
agentId: string;
/** The `name` that the Agent call gave it. */
name?: string;
/** Its role, the `subagent_type` of the Agent call. */
agentType?: string;
/** The short `description` of the Agent call. */
description?: string;
/** Whether the Agent call ran it in the background. */
background: boolean;
/** Whether the user stopped it. */
stoppedByUser: boolean;
}
/** Where the subagent transcripts of a session are. */
export interface SubagentHistory {
/** The subagent of each Agent or Task tool use that launched one. */
agents: Map<string, SubagentRecord>;
/** The subagent id of each Agent or Task tool use that launched a subagent. */
ids: Map<string, string>;
/**
* The project directory of the session, as the SDK takes it in `dir`. With
* it, the SDK reads a subagent transcript without a search of every project.
@@ -46,282 +23,32 @@ export interface SubagentHistory {
* The ids are empty for a session without a local transcript.
*/
export async function subagentHistory(sessionId: string): Promise<SubagentHistory> {
const agents = new Map<string, SubagentRecord>();
const ids = new Map<string, string>();
const transcript = await findTranscript(sessionId);
if (!transcript) return { agents };
const directory = subagentsDirectory(transcript, sessionId);
if (!transcript) return { ids };
const directory = path.join(path.dirname(transcript), sessionId, "subagents");
let names: string[];
try {
names = await readdir(directory);
} catch {
return { agents };
return { ids };
}
await Promise.all(
names.map(async (name) => {
const match = /^agent-(.+)\.meta\.json$/.exec(name);
if (!match) return;
try {
const meta = JSON.parse(await readFile(path.join(directory, name), "utf8")) as Record<
string,
unknown
> | null;
if (typeof meta?.toolUseId !== "string") return;
const text = (value: unknown) => (typeof value === "string" ? value : undefined);
agents.set(meta.toolUseId, {
agentId: match[1]!,
name: text(meta.name),
agentType: text(meta.agentType),
description: text(meta.description),
background: meta.requestShape === "background",
stoppedByUser: meta.stoppedByUser === true,
});
const meta: unknown = JSON.parse(await readFile(path.join(directory, name), "utf8"));
const toolUseId = (meta as { toolUseId?: unknown } | null)?.toolUseId;
if (typeof toolUseId === "string") ids.set(toolUseId, match[1]!);
} catch {
// A damaged meta file leaves its subagent without history.
}
}),
);
if (agents.size === 0) return { agents };
if (ids.size === 0) return { ids };
// The folder name encodes the project directory, and the encoding loses
// characters. The session info reads it back from the transcript.
const dir = (await getSessionInfo(sessionId).catch(() => undefined))?.cwd;
return dir ? { agents, dir } : { agents };
}
/** The folder of the subagent transcripts, next to the session transcript `transcript`. */
function subagentsDirectory(transcript: string, sessionId: string): string {
return path.join(path.dirname(transcript), sessionId, "subagents");
}
/**
* The agent that the Agent or Task call `launchId` launched, from the subagent
* history of the session `sessionId`.
*/
export async function subagentLaunchedBy(
sessionId: string,
launchId: string,
): Promise<string | undefined> {
return (await subagentHistory(sessionId)).agents.get(launchId)?.agentId;
}
/**
* The agent `agentId` and its ancestors, root first, from the subagent history
* of the session `sessionId`. The runtime restores an agent from it when Claude
* Code resumes one that ran before the session was loaded or resumed.
*
* The metadata file of an agent names the call that launched it, but not the
* agent that made the call. So the parent of a launch is the session whose
* transcript has the call, as in the replay: the session's own transcript, or
* an agent's. The list is empty when the agent or one of its ancestors has no
* metadata, or no transcript has its launch.
*/
export async function subagentLineage(
sessionId: string,
agentId: string,
logger: { error(...args: unknown[]): void },
): Promise<HistoricalSubagent[]> {
const history = await subagentHistory(sessionId);
const launchOf = new Map<string, { launchId: string; record: SubagentRecord }>();
for (const [launchId, record] of history.agents) {
launchOf.set(record.agentId, { launchId, record });
}
if (!launchOf.has(agentId)) return [];
// The SDK's readers return only the active chain, which starts after the
// last compaction, so the launch calls come from the transcript files.
const transcript = await findTranscript(sessionId);
if (!transcript) return [];
const rootLaunches = await readLaunchIdsFromTranscript(transcript, null, logger);
const agentLaunches = new Map<string, Set<string>>();
/** The agent whose transcript has the launch `launchId`, `null` for the root's. */
const launcherOf = async (launchId: string): Promise<string | null | undefined> => {
if (rootLaunches.has(launchId)) return null;
for (const candidate of launchOf.keys()) {
let launches = agentLaunches.get(candidate);
if (!launches) {
launches = await readLaunchIdsFromTranscript(
path.join(subagentsDirectory(transcript, sessionId), `agent-${candidate}.jsonl`),
candidate,
logger,
);
agentLaunches.set(candidate, launches);
}
if (launches.has(launchId)) return candidate;
}
return undefined;
};
const lineage: HistoricalSubagent[] = [];
let current: string | null = agentId;
while (current !== null) {
const launch = launchOf.get(current);
// A cycle, or an ancestor without metadata.
if (!launch || lineage.some((entry) => entry.agentId === current)) return [];
const launcher = await launcherOf(launch.launchId);
if (launcher === undefined) return [];
lineage.unshift({
agentId: current,
launchId: launch.launchId,
...(launcher !== null ? { parentAgentId: launcher } : {}),
...(launch.record.name ? { name: launch.record.name } : {}),
...(launch.record.agentType ? { agentType: launch.record.agentType } : {}),
...(launch.record.description ? { description: launch.record.description } : {}),
});
current = launcher;
}
return lineage;
}
/** The ids of the Agent and Task calls that launched subagents in `content`. */
export function subagentLaunchIds(content: unknown): string[] {
if (!Array.isArray(content)) return [];
return content.flatMap((block) =>
typeof block === "object" &&
block !== null &&
block.type === "tool_use" &&
typeof block.id === "string" &&
isNativeSubagentControlTool(block.name)
? [block.id as string]
: [],
);
}
/**
* The ids of the launch calls in the transcript `file`, every record of it,
* including those before a compaction. `owner` is the agent the transcript
* belongs to, null for the session's. The session's transcript can hold
* sidechain records, which are an agent's, not the root's; an agent's
* transcript keeps only the records of that agent. A file that cannot be read
* has no launches, so the agent is not placed under the root.
*/
async function readLaunchIdsFromTranscript(
file: string,
owner: string | null,
logger: { error(...args: unknown[]): void },
): Promise<Set<string>> {
const launches = new Set<string>();
const stream = createReadStream(file, { encoding: "utf8" });
try {
for await (const line of readline.createInterface({ input: stream, crlfDelay: Infinity })) {
const forkDirective = owner !== null && line.includes(FORK_BOILERPLATE_OPEN);
if (!line.includes('"tool_use"') && !forkDirective) continue;
let record: {
type?: unknown;
isSidechain?: unknown;
agentId?: unknown;
message?: { content?: unknown };
} | null;
try {
record = JSON.parse(line);
} catch {
continue;
}
// Every record of a fork child's transcript before its directive message
// is inherited context: a copy of the launching message, whose launch
// calls are the parent's, not the fork's.
if (forkDirective && record !== null && isForkDirectiveMessage(record)) {
launches.clear();
continue;
}
if (record?.type !== "assistant") continue;
if (
owner === null
? record.isSidechain === true
: record.agentId !== undefined && record.agentId !== owner
) {
continue;
}
for (const id of subagentLaunchIds(record.message?.content)) launches.add(id);
}
} catch (error) {
logger.error(`Failed to read the transcript ${file}:`, error);
return new Set();
} finally {
stream.destroy();
}
return launches;
}
/**
* The transcript of the subagent `agentId` of a session, or an empty list
* when it cannot be read. With the project directory, the SDK skips its
* search of every project; a session opened from another directory falls
* back to that search.
*/
export async function readSubagentMessages(
sessionId: string,
agentId: string,
history: SubagentHistory,
logger: { error(...args: unknown[]): void },
): Promise<SessionMessage[]> {
try {
if (history.dir) {
const messages = await getSubagentMessages(sessionId, agentId, { dir: history.dir });
if (messages.length > 0) return stripForkPrefix(messages);
}
return stripForkPrefix(await getSubagentMessages(sessionId, agentId));
} catch (error) {
logger.error(`Failed to read the history of subagent ${agentId}:`, error);
return [];
}
}
/** How Claude Code opens the directive message of a `subagent_type: "fork"` child. */
const FORK_BOILERPLATE_OPEN = "<fork-boilerplate>";
/** What follows the boilerplate, right before the prompt of the Agent call. */
const FORK_DIRECTIVE_MARKER = "</fork-boilerplate>\nYour directive: ";
/** Whether `block` is the text block of a fork child's directive message. */
function isForkDirectiveText(block: unknown): block is { type: "text"; text: string } {
return (
typeof block === "object" &&
block !== null &&
(block as { type?: unknown }).type === "text" &&
typeof (block as { text?: unknown }).text === "string" &&
(block as { text: string }).text.startsWith(FORK_BOILERPLATE_OPEN)
);
}
/** Whether the transcript record `message` is the directive message of a fork child. */
function isForkDirectiveMessage(message: { type?: unknown; message?: unknown }): boolean {
if (message.type !== "user") return false;
const content = (message.message as { content?: unknown } | null | undefined)?.content;
return Array.isArray(content) && content.some(isForkDirectiveText);
}
/**
* The transcript of a `subagent_type: "fork"` child without the context it
* inherited. A fork transcript starts with a copy of the parent's launching
* assistant message (its text, thinking and every tool_use, including the
* Agent call itself), then a user message with a placeholder tool_result for
* each of those tool_uses and the text
* `<fork-boilerplate>…</fork-boilerplate>\nYour directive: <prompt>`. Since
* Claude Code 2.1.307 the fork's own Agent call gets "Fork started — that fork
* is you." and the other tool_uses get "This is the parent's call, not
* yours. …"; before that, every one got "Fork started — processing in
* background". Only the directive text is matched here. A fork
* launched from inside a subagent also gets a copy of that subagent's history
* before them. None of it is the child's own work: replayed as is, the
* parent's text and tool calls would show again in the child.
*
* This drops every record before the directive message, and reduces that
* message to the prompt. Other transcripts are returned unchanged.
*/
export function stripForkPrefix(messages: SessionMessage[]): SessionMessage[] {
const index = messages.findIndex(isForkDirectiveMessage);
if (index === -1) return messages;
const directive = messages[index];
const content = (directive.message as { content: unknown[] }).content;
const prompt = content.flatMap((block) => {
if (!isForkDirectiveText(block)) return [];
const start = block.text.indexOf(FORK_DIRECTIVE_MARKER);
return start === -1
? []
: [{ type: "text", text: block.text.slice(start + FORK_DIRECTIVE_MARKER.length) }];
});
const rest = messages.slice(index + 1);
if (prompt.length === 0) return rest;
return [
{ ...directive, message: { ...(directive.message as object), content: prompt } },
...rest,
];
return dir ? { ids, dir } : { ids };
}

View File

@@ -1,580 +0,0 @@
/**
* The replay of subagents to a client on the subagents RFD form.
*
* Claude Code keeps each subagent's history in its own transcript, and the
* subagent's metadata file names the Agent or Task call that launched it (see
* `subagentHistory`). The replay announces each child right after that call,
* under the session id it had live, the SDK agent id, and sends the child's
* history to the child session. Every child's end goes out before the replay
* ends. These states are history to the client, so each child's current
* state goes out again after the load response (`currentStates`).
*
* As live, the parent is not shown the Agent or Task call of a child that the
* replay announces: the launch's prompt is the parent's outgoing message, and
* a foreground child's answer is its reply to the parent. A child's
* successful SubagentHandback call is its message to the parent instead.
* Unlike live, the replay does not tell runs apart, so a child that handed
* back any report gets no reply from its launch's result.
*
* A SendMessage call whose message the replay delivered to a child before it
* reached the call is that message too: the sender's outgoing view, at the
* call, and the child's incoming one, in the child's history. A child's
* history follows its launch, so a call to a child from its parent is always
* delivered first. Any other SendMessage call stays a tool call.
*
* Right after its announcement, each child gets its `model` option: the model
* of its first response, in its transcript. A later response with another
* model sends it again. A child without responses gets none. After the load
* response, a model goes again only when the session's model list, which the
* replay can lack, maps it to another entry.
*
* A launch without metadata has no known id, so it is not announced: the RFD
* forbids inventing one. Its Agent call stays an ordinary tool call of the
* parent, and a notice says that subagent history is missing.
*/
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import type { AcpSessionNotification, SubagentState } from "./acp-subagents.js";
import {
childModelOption,
HANDBACK_TOOL,
handbackMessage,
isNativeSubagentControlTool,
launchReply,
linkedSessionMessages,
type ModelEntry,
nativeSubagentState,
responseModelEntry,
rfdEndState,
rfdLabels,
type SubagentModels,
type WorkState,
} from "./native-subagents.js";
import { noticeUpdate } from "./session-notices.js";
import type { SubagentHistory, SubagentRecord } from "./subagent-history.js";
type Publish = (notification: AcpSessionNotification) => Promise<void>;
/** A child that the replay shows: its launch has metadata. */
export type ReplayChild = {
/** The Agent or Task tool use that launched it. */
launchId: string;
/** Its session id: the SDK agent id, as live. */
sessionId: string;
record: SubagentRecord;
/** The session that made the launch. */
parentSessionId: string;
/** The input of the launch call. */
input: Record<string, unknown>;
/** How the launch's tool result ended a foreground child's run. */
launchEnd?: SubagentState;
/** The state of the child's last persisted task notification. */
notified?: SubagentState;
announced: boolean;
promptSent: boolean;
/** Whether it handed a report to its parent with a SubagentHandback call. */
handedBack?: boolean;
/** The `model` option entry that the replay sent. */
model?: ModelEntry;
/** The model id of the latest response in its transcript. */
responseModel?: string;
};
type SendMessageCall = {
toolUseId: string;
to: string;
message: string;
senderSessionId: string;
/** The child the replay delivered the message to. */
recipientSessionId?: string;
/** The replay reached the call before the delivery, so it stays a tool call. */
shownAsToolCall: boolean;
/** The sender's view of the message went out, at the call. */
outgoingSent: boolean;
};
export class RfdSubagentReplay {
/** The children by the id of the tool use that launched each. */
private readonly children = new Map<string, ReplayChild>();
private readonly byAgentId = new Map<string, ReplayChild>();
private readonly sendMessages: SendMessageCall[] = [];
/** Launches without metadata, which the replay leaves out. */
private readonly omitted = new Set<string>();
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
/** The SubagentHandback calls, with their report and whether their result failed. */
private readonly handbacks = new Map<
string,
{ message: string; failed: boolean; sent: boolean }
>();
constructor(
private readonly history: SubagentHistory,
private readonly publish: Publish,
private readonly models?: SubagentModels,
/**
* Whether the replay maps a model to the session's model list as it goes.
* A load lacks the list until the CLI started, so it maps only in
* {@link currentStates}, after the load response.
*/
private readonly mapsDuringReplay = true,
) {}
/**
* Registers the launches, the launch results, and the SendMessage calls in
* `messages`, the transcript of the session `ownerSessionId`.
*/
scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
for (const message of messages) {
for (const block of contentBlocks(message)) {
if (block.type === "tool_use" && typeof block.id === "string") {
if (isNativeSubagentControlTool(block.name)) {
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
} else if (block.name === HANDBACK_TOOL) {
const message = handbackMessage(block.input);
if (message) this.handbacks.set(block.id, { message, failed: false, sent: false });
} else if (block.name === "SendMessage") {
const input = inputOf(block);
if (typeof input.to === "string" && typeof input.message === "string") {
this.sendMessages.push({
toolUseId: block.id,
to: input.to,
message: input.message,
senderSessionId: ownerSessionId,
shownAsToolCall: false,
outgoingSent: false,
});
}
}
} else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
const handback = this.handbacks.get(block.tool_use_id);
if (handback && block.is_error === true) handback.failed = true;
const child = this.children.get(block.tool_use_id);
// A background launch answers at once; its result is not the end.
if (child && !child.record.background) {
child.launchEnd = replaySubagentTerminalState(block);
}
}
}
}
}
/** The child that the tool use `toolUseId` launched, when the replay shows it. */
childOfLaunch(toolUseId: string): ReplayChild | undefined {
return this.children.get(toolUseId);
}
/**
* Announces `child` on its parent, as live: running, with the parent's
* labels. Then it sends `firstModel`, the model of the child's first
* response, when the transcript has one.
*/
async announce(child: ReplayChild, firstModel?: string): Promise<void> {
if (child.announced) return;
const labels = rfdLabels(
child.record.name ?? child.input.name,
child.record.agentType ?? child.input.subagent_type,
child.record.description ?? child.input.description,
);
await this.publish({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
...(labels.title ? { title: labels.title } : {}),
...(labels.description ? { description: labels.description } : {}),
state: { state: "running" },
},
});
child.announced = true;
await this.responseModel(child, firstModel);
}
/**
* Takes the model of a replayed response of the child `owner`. A child
* whose model changes gets its `model` option again.
*/
async responseModel(owner: ReplayChild, model: string | undefined): Promise<void> {
const entry = responseModelEntry(model, this.mapsDuringReplay ? this.models : undefined);
if (!entry || !owner.announced) return;
owner.responseModel = model;
await this.sendModel(owner, entry);
}
private async sendModel(child: ReplayChild, model: ModelEntry): Promise<void> {
if (child.model?.value === model.value && child.model.name === model.name) return;
child.model = model;
await this.publish({
sessionId: child.sessionId,
update: {
sessionUpdate: "config_option_update",
configOptions: [childModelOption(model)],
},
});
}
/**
* The rest of a child's user message to replay, or `null` when nothing is
* left, after its text went out as the child's incoming `session_message`.
*
* A child session has no user, so the text of its user messages is never a
* user message. The first text is the prompt of the launch. A message that
* a SendMessage call delivered (`origin.kind: "coordinator"`) is another
* incoming message. Any other text is harness text, such as "[Request
* interrupted by user for tool use]", which the live child session does not
* show. Tool results stay.
*/
async childContent(
child: ReplayChild,
message: SessionMessage,
content: unknown,
): Promise<unknown> {
const text = textOf(content);
if (text.trim()) {
if (isDelivery(message)) await this.sendDelivery(child, message, text);
else if (!child.promptSent) await this.sendPrompt(child, text);
}
if (!Array.isArray(content)) return null;
const rest = content.filter(
(block) => !(typeof block === "object" && block !== null && block.type === "text"),
);
return rest.length > 0 ? rest : null;
}
/**
* Ends the replay of `child`'s history. `messages` is its transcript: when
* it is empty, the replay has only the child's association, and the prompt
* comes from the launch call.
*/
async childReplayed(child: ReplayChild, messages: readonly SessionMessage[]): Promise<void> {
if (messages.length === 0) this.withoutHistory++;
if (!child.promptSent && typeof child.input.prompt === "string") {
await this.sendPrompt(child, child.input.prompt);
}
}
/**
* Takes a replayed update of a tool call, from the transcript of `owner`
* when it is a child's. Returns whether the client is not shown it: an
* update of the launch of a child that the replay shows, of a SendMessage
* call whose message the replay already delivered, or of a successful
* SubagentHandback call of `owner`. A foreground launch's completed result
* goes out as the child's reply, a delivered SendMessage as the sender's
* outgoing message, and a hand-back as the child's message to its parent.
*/
async toolCallUpdate(
update: AcpSessionNotification["update"],
owner: ReplayChild | undefined,
): Promise<boolean> {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return false;
}
const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
if (sendMessage) {
if (sendMessage.recipientSessionId === undefined) {
sendMessage.shownAsToolCall = true;
return false;
}
if (!sendMessage.outgoingSent) {
sendMessage.outgoingSent = true;
const [outgoing] = linkedSessionMessages(
sendMessage.toolUseId,
sendMessage.senderSessionId,
sendMessage.recipientSessionId,
sendMessage.message,
);
if (outgoing) await this.publish(outgoing);
}
return true;
}
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
if (owner && handback && !handback.failed) {
if (!handback.sent) {
handback.sent = true;
owner.handedBack = true;
for (const view of linkedSessionMessages(
update.toolCallId,
owner.sessionId,
owner.parentSessionId,
handback.message,
)) {
await this.publish(view);
}
}
return true;
}
const child = this.children.get(update.toolCallId);
if (!child) return false;
if (
update.status === "completed" &&
child.announced &&
!child.record.background &&
!child.handedBack
) {
const reply = launchReply(child.launchId, child.sessionId, child.parentSessionId, update);
if (reply) await this.publish(reply);
}
return true;
}
/**
* Takes a persisted task notification. Returns whether it was a child's:
* its task id is the agent id.
*/
taskNotification(taskId: string, status: string): boolean {
const child = this.byAgentId.get(taskId);
if (!child) return false;
child.notified = nativeSubagentState(status) ?? child.notified;
return true;
}
/**
* Sends the end of every announced child (see `endState`), and a notice when
* subagent history is missing.
*/
async finish(supportsNotices: boolean, rootSessionId: string): Promise<void> {
for (const child of [...this.children.values()].reverse()) {
if (!child.announced) continue;
await this.publish({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
state: endState(child),
},
});
}
const missing = this.omitted.size + this.withoutHistory;
if (missing > 0 && supportsNotices) {
await this.publish({
sessionId: rootSessionId,
update: noticeUpdate({
severity: "info",
title: "Some subagent history is not available",
description:
missing === 1
? "The history of 1 subagent of this session could not be restored."
: `The history of ${missing} subagents of this session could not be restored.`,
}),
});
}
}
/**
* The current state of each announced child, to send after the load
* response. The RFD makes that response the freshness boundary: a client
* treats the replayed states before it as history, so they show no current
* activity and allow no cancel.
*
* `live` gives the work state of a child that still runs in this process, as
* when a live session is loaded again. Such a child reports that state and
* can be cancelled, as live. Any other child cannot be running, because the
* process that ran it is gone, so its replayed end is its current state.
*/
currentStates(
live: (sessionId: string) => WorkState["state"] | undefined,
liveConfiguration?: (sessionId: string) => SessionConfigOption[] | undefined,
): AcpSessionNotification[] {
return [...this.children.values()]
.filter((child) => child.announced)
.flatMap((child): AcpSessionNotification[] => {
const liveState = live(child.sessionId);
const state: WorkState =
liveState === undefined || liveState === "idle" ? endState(child) : { state: liveState };
// The session's model list may be known only now, after the load response.
const entry = responseModelEntry(child.responseModel, this.models);
const replayed = child.model ? JSON.stringify([childModelOption(child.model)]) : undefined;
const current =
liveConfiguration?.(child.sessionId) ?? (entry ? [childModelOption(entry)] : undefined);
// A configuration that the replay already sent does not go again.
const configOptions = current && JSON.stringify(current) !== replayed ? current : undefined;
return [
{
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
...(liveState !== undefined ? { capabilities: { cancel: {} } } : {}),
state,
},
},
...(configOptions
? [
{
sessionId: child.sessionId,
update: { sessionUpdate: "config_option_update" as const, configOptions },
},
]
: []),
];
});
}
private registerLaunch(
toolUseId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(toolUseId)) return;
const record = this.history.agents.get(toolUseId);
if (!record) {
this.omitted.add(toolUseId);
return;
}
const child: ReplayChild = {
launchId: toolUseId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
};
this.children.set(toolUseId, child);
this.byAgentId.set(record.agentId, child);
}
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
private async sendPrompt(child: ReplayChild, text: string): Promise<void> {
child.promptSent = true;
for (const view of linkedSessionMessages(
child.launchId,
child.parentSessionId,
child.sessionId,
text,
)) {
await this.publish(view);
}
}
/**
* A message that a SendMessage call delivered. Claude Code frames it for the
* child's model ("The coordinator sent a message while you were working:",
* then the message, then an instruction), so the call it came from is the
* first undelivered SendMessage to the child whose message the delivered
* text contains. With that call, the message has the call's id and text and
* its sender, as live; without it, its transcript id and the delivered text.
* Unless the replay already showed the call as a tool call, the message
* replaces it, so this view names the sender's view, which goes out at the
* call (see {@link toolCallUpdate}).
*/
private async sendDelivery(
child: ReplayChild,
message: SessionMessage,
text: string,
): Promise<void> {
const names = [child.sessionId, child.record.name, child.input.name];
const call = this.sendMessages.find(
(candidate) =>
candidate.recipientSessionId === undefined &&
names.includes(candidate.to) &&
text.includes(candidate.message.trim()),
);
if (call) {
call.recipientSessionId = child.sessionId;
await this.sendMessage(child, call.toolUseId, call.message, call.senderSessionId, {
linked: !call.shownAsToolCall,
});
} else {
await this.sendMessage(child, message.uuid, text, undefined);
}
}
private async sendMessage(
child: ReplayChild,
messageId: string,
text: string,
sender: string | undefined,
{ linked = false }: { linked?: boolean } = {},
): Promise<void> {
await this.publish({
sessionId: child.sessionId,
update: {
sessionUpdate: "session_message",
messageId,
...(sender ? { senderSessionId: sender } : {}),
recipientSessionId: child.sessionId,
...(linked ? { senderMessageId: messageId } : {}),
content: [{ type: "text", text }],
},
});
}
}
/** The model of the first response in `messages`, a subagent's transcript. */
export function firstResponseModel(messages: readonly SessionMessage[]): string | undefined {
for (const message of messages) {
if (message.type !== "assistant") continue;
const model = (message.message as { model?: unknown } | undefined)?.model;
if (typeof model === "string" && model && model !== "<synthetic>") return model;
}
return undefined;
}
/**
* The end of a foreground launch from its tool result: an error result that
* names a stop is a cancellation, any other error a failure.
*/
export function replaySubagentTerminalState(
block: Record<string, unknown>,
): "completed" | "failed" | "cancelled" {
if (block.is_error !== true) return "completed";
const text = replayContentText(block.content).toLowerCase();
return /\b(?:cancelled|canceled|interrupted|stopped|killed)\b/.test(text)
? "cancelled"
: "failed";
}
/**
* How `child`'s work ended: the state of its last task notification, else
* `cancelled` for a child the user stopped, else the result of its foreground
* launch, else `idle` with no stop reason.
*/
function endState(child: ReplayChild): WorkState {
const end = child.notified ?? (child.record.stoppedByUser ? "cancelled" : child.launchEnd);
return end ? rfdEndState(end, undefined) : { state: "idle" };
}
function replayContentText(value: unknown, seen = new Set<unknown>()): string {
if (typeof value === "string") return value;
if (typeof value !== "object" || value === null || seen.has(value)) return "";
seen.add(value);
if (Array.isArray(value)) return value.map((item) => replayContentText(item, seen)).join(" ");
const record = value as Record<string, unknown>;
return [record.text, record.content, record.message]
.map((item) => replayContentText(item, seen))
.filter(Boolean)
.join(" ");
}
function contentBlocks(message: SessionMessage): Record<string, unknown>[] {
const content = (message as { message?: { content?: unknown } }).message?.content;
return Array.isArray(content)
? content.filter(
(block): block is Record<string, unknown> => typeof block === "object" && block !== null,
)
: [];
}
function inputOf(block: Record<string, unknown>): Record<string, unknown> {
return typeof block.input === "object" && block.input !== null
? (block.input as Record<string, unknown>)
: {};
}
/** The text of user message content: a string, or its text blocks. */
function textOf(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.flatMap((block) =>
typeof block === "object" && block !== null && block.type === "text" ? [block.text] : [],
)
.filter((text): text is string => typeof text === "string")
.join("\n");
}
/** A message that a SendMessage call delivered to the child. */
function isDelivery(message: SessionMessage): boolean {
return (message as { origin?: { kind?: unknown } | null }).origin?.kind === "coordinator";
}

View File

@@ -1,75 +0,0 @@
/**
* The `<task-notification>` inputs that Claude Code writes to a transcript
* when a background task stops. The replay of a session (`acp-agent.ts`) and
* the session index (`session-index/background-tasks.ts`) read them here, so
* both read the same fields.
*/
export const TASK_NOTIFICATION_TAG = "task-notification";
/** The text between the first `<tag>` and the next `</tag>` after it. */
export function markerTagText(text: string, tag: string): string | undefined {
const open = `<${tag}>`;
const close = `</${tag}>`;
const start = text.indexOf(open);
if (start === -1) return undefined;
const end = text.indexOf(close, start + open.length);
if (end === -1) return undefined;
return text.slice(start + open.length, end);
}
/** The fields of a persisted `<task-notification>`, in the shape of the SDK
* `task_notification` frame that reported the same stop live. */
export type PersistedTaskNotification = {
task_id: string;
status: string;
tool_use_id?: string;
summary?: string;
output_file?: string;
};
/**
* Returns each `<task-notification>` in user-message content (a string or its
* text blocks). The `<result>` body is model context that the live frame does
* not carry, so the scan stops at it and a tag quoted in it cannot match.
*
* Only a notification with a `<task-id>` and a `<status>` is returned. A
* notification without a status comes while its task still runs: the
* check-in of a quiet shell ("is still running after …") or a `Monitor`
* event.
*/
export function taskNotificationsOf(content: unknown): PersistedTaskNotification[] {
const texts =
typeof content === "string"
? [content]
: Array.isArray(content)
? content.flatMap((block) =>
block?.type === "text" && typeof block.text === "string" ? [block.text as string] : [],
)
: [];
const open = `<${TASK_NOTIFICATION_TAG}>`;
const close = `</${TASK_NOTIFICATION_TAG}>`;
const notifications: PersistedTaskNotification[] = [];
for (const text of texts) {
let start = text.indexOf(open);
while (start !== -1) {
const end = text.indexOf(close, start + open.length);
if (end === -1) break;
const body = text.slice(start + open.length, end).split("<result>", 1)[0];
const field = (tag: string) => markerTagText(body, tag)?.trim() || undefined;
const taskId = field("task-id");
const status = field("status");
if (taskId && status) {
notifications.push({
task_id: taskId,
status,
tool_use_id: field("tool-use-id"),
summary: field("summary"),
output_file: field("output-file"),
});
}
start = text.indexOf(open, end + close.length);
}
}
return notifications;
}

View File

@@ -515,7 +515,7 @@ describe("ClaudeAcpAgent settings", () => {
_meta: { disableBuiltInTools: true },
});
await agent.setSessionMode({ sessionId, modeId: "default" });
const { RequestError } = await import("@agentclientprotocol/sdk/unstable");
const { RequestError } = await import("@agentclientprotocol/sdk");
const createSession = (agent as any).createSession.bind(agent);
const creations: object[] = [];
(agent as any).createSession = async (params: object, options: { resume?: string }) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"mcp__docs__search"},"is_mcp_tool_call":true,"jetbrains":{"air":{"mcp":{"server":"docs","tool":"search"},"version":1}}},"content":[],"kind":"other","name":"mcp__docs__search","sessionUpdate":"tool_call","status":"pending","title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"mcp__docs__search"},"is_mcp_tool_call":true},"content":[],"kind":"other","name":"mcp__docs__search","sessionUpdate":"tool_call","status":"pending","title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"rawInput":{"q":"acp"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_mcp"}}}
{"kind":"requestPermission","payload":{"_meta":{"jetbrains":{"air":{"permission":{"title":"mcp__docs__search","version":1},"version":1}}},"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","toolCall":{"_meta":{"claudeCode":{"mcpServer":{"name":"docs","source":"project"}}},"rawInput":{"q":"acp"},"title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}

View File

@@ -1,5 +1,4 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"SandboxNetworkAccess"}},"content":[],"kind":"other","name":"SandboxNetworkAccess","rawInput":{"host":"example.com"},"sessionUpdate":"tool_call","status":"pending","title":"SandboxNetworkAccess","toolCallId":"toolu_net"}}}
{"kind":"requestPermission","payload":{"_meta":{"jetbrains":{"air":{"permission":{"description":"Reason: Sandbox blocks network","title":"example.com","version":1},"version":1}}},"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","toolCall":{"rawInput":{"host":"example.com"},"title":"example.com","toolCallId":"toolu_net"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_net"}}}
{"kind":"promptResponse","payload":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}

View File

@@ -1,4 +1,4 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Check the file.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"I will edit it.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_message_chunk"}}}
@@ -8,10 +8,8 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"terminal_output_delta":{"data":"app.ts","terminal_id":"toolu_r_bash"}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_r_bash"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"terminal_exit":{"exit_code":0,"signal":null,"terminal_id":"toolu_r_bash"}},"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_bash"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"rawOutput":"The file <cwd>/src/app.ts has been updated successfully.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>:replay-subagent:toolu_r_task","update":{"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>:replay-subagent:toolu_r_task","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_r_task"}},"content":{"text":"Subagent text.","type":"text"},"messageId":"msg_r_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"<session>:replay-subagent:toolu_r_task"}}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>:replay-subagent:toolu_r_task","update":{"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,8 +1,8 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent","toolResponse":{"isAsync":true,"status":"async_launched"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Background review","prompt":"Review the diff","sessionUpdate":"subagent_spawned","subagentSessionId":"a2","task":"Review the diff"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"a2"}}}

View File

@@ -2,7 +2,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Check the logs","prompt":"Check logs","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_late","task":"Check logs"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late","toolName":"Read"}},"content":[],"kind":"read","locations":[],"name":"Read","sessionUpdate":"tool_call","status":"pending","title":"Read File","toolCallId":"toolu_late_read"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_late","update":{"locations":[{"line":1,"path":"/var/log/app.log"}],"rawInput":{"file_path":"/var/log/app.log"},"sessionUpdate":"tool_call_update","title":"Read /var/log/app.log","toolCallId":"toolu_late_read"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"agent_late"}}}

View File

@@ -2,7 +2,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Fix the build","prompt":"Make the build pass","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_n","task":"Make the build pass"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Grep"}},"content":[],"kind":"search","name":"Grep","sessionUpdate":"tool_call","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}

View File

@@ -2,9 +2,7 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Plan the work","prompt":"Plan it","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_outer","task":"Plan it"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_outer","update":{"capabilities":{},"name":"Read the spec","prompt":"Read spec.md","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_inner","task":"Read spec.md"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner","toolName":"Read"}},"content":[],"kind":"read","locations":[],"name":"Read","sessionUpdate":"tool_call","status":"pending","title":"Read File","toolCallId":"toolu_grandchild_read"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_inner","update":{"locations":[{"line":1,"path":"/spec.md"}],"rawInput":{"file_path":"/spec.md"},"sessionUpdate":"tool_call_update","title":"Read /spec.md","toolCallId":"toolu_grandchild_read"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_grandchild_read"}}}

View File

@@ -2,7 +2,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Explore code","prompt":"Find the parser","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_1","task":"Find the parser"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Looking for","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":" the parser.","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task","toolName":"Read"}},"content":[],"kind":"read","locations":[],"name":"Read","sessionUpdate":"tool_call","status":"pending","title":"Read File","toolCallId":"toolu_sub_read"}}}
@@ -19,9 +18,10 @@
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"content":[{"newText":"b","oldText":"a","path":"<cwd>/x.ts","type":"diff"}],"rawInput":{"file_path":"<cwd>/x.ts"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"requestPermission","payload":{"_meta":{"jetbrains":{"air":{"permission":{"title":"Edit x.ts","version":1},"version":1}}},"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"agent_1","toolCall":{"rawInput":{"file_path":"<cwd>/x.ts"},"title":"Edit x.ts","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"rawOutput":"The file has been updated.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Foun","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"d it.","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Task","toolResponse":{"status":"completed"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"agent_1"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"promptResponse","payload":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}

View File

@@ -2,7 +2,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Summarize","prompt":"Summarize the repo","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_t","task":"Summarize the repo"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"Look at t","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"he layout.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"The repo has ","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -11,7 +11,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"ls -la","description":"List files"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"terminalId":"toolu_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"ls -la","description":"List files"},"title":"ls -la","toolCallId":"toolu_bash"},"type":"tool_call"},"title":"ls -la"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"terminalId":"toolu_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"ls -la","description":"List files"},"status":"pending","title":"ls -la","toolCallId":"toolu_bash"},"type":"tool_call"},"title":"ls -la"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"output":{"data":"YS50cwpiLnRz"},"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{"exitCode":0},"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -11,7 +11,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"value = 2","old_string":"value = 1"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1 +1 @@\n-const value = 1;\n+const value = 2;\n"},"type":"diff"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1 +1 @@\n-const value = 1;\n+const value = 2;\n"},"type":"diff"}],"kind":"edit","locations":[{"path":"<cwd>/src/app.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"value = 2","old_string":"value = 1"},"title":"Edit src/app.ts","toolCallId":"toolu_edit"},"type":"tool_call"},"title":"Edit src/app.ts"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1 +1 @@\n-const value = 1;\n+const value = 2;\n"},"type":"diff"}],"kind":"edit","locations":[{"path":"<cwd>/src/app.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"value = 2","old_string":"value = 1"},"status":"pending","title":"Edit src/app.ts","toolCallId":"toolu_edit"},"type":"tool_call"},"title":"Edit src/app.ts"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\nThe user doesn't want to proceed with this tool use. The tool use was rejected.\n```","type":"text"},"type":"content"}],"rawOutput":"The user doesn't want to proceed with this tool use. The tool use was rejected.","sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -11,7 +11,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"const value = 2;","old_string":"const value = 1;"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1,3 +1,3 @@\n line 1\n-const value = 1;\n+const value = 2;\n line 3\n"},"type":"diff"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1,3 +1,3 @@\n line 1\n-const value = 1;\n+const value = 2;\n line 3\n"},"type":"diff"}],"kind":"edit","locations":[{"path":"<cwd>/src/app.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"const value = 2;","old_string":"const value = 1;"},"title":"Edit src/app.ts","toolCallId":"toolu_edit"},"type":"tool_call"},"title":"Edit src/app.ts"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1,3 +1,3 @@\n line 1\n-const value = 1;\n+const value = 2;\n line 3\n"},"type":"diff"}],"kind":"edit","locations":[{"path":"<cwd>/src/app.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"const value = 2;","old_string":"const value = 1;"},"status":"pending","title":"Edit src/app.ts","toolCallId":"toolu_edit"},"type":"tool_call"},"title":"Edit src/app.ts"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawOutput":"The file <cwd>/src/app.ts has been updated successfully.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"<cwd>/src/app.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -8,7 +8,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"rawInput":{"plan":"# Plan\n1. Do it"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes, manually approve edits","optionId":"exit-plan-default"},{"kind":"allow_always","name":"Yes, clear context (0% used) and use auto mode","optionId":"exit-plan-clear-auto"},{"kind":"allow_always","name":"Yes, and use auto mode","optionId":"exit-plan-auto"},{"kind":"allow_always","name":"Yes, and bypass permissions","optionId":"exit-plan-bypass"},{"kind":"reject_once","name":"No, keep planning","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{"plan":"# Plan\n1. Do it"},"title":"Approve Plan","toolCallId":"toolu_plan"},"type":"tool_call"},"title":"Ready to code?"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes, manually approve edits","optionId":"exit-plan-default"},{"kind":"allow_always","name":"Yes, clear context (0% used) and use auto mode","optionId":"exit-plan-clear-auto"},{"kind":"allow_always","name":"Yes, and use auto mode","optionId":"exit-plan-auto"},{"kind":"allow_always","name":"Yes, and bypass permissions","optionId":"exit-plan-bypass"},{"kind":"reject_once","name":"No, keep planning","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{"plan":"# Plan\n1. Do it"},"status":"pending","title":"Approve Plan","toolCallId":"toolu_plan"},"type":"tool_call"},"title":"Ready to code?"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawOutput":"User has approved your plan. You can now start coding.\n\n## Approved Plan:\n# Plan\n1. Do it","sessionUpdate":"tool_call_update","status":"completed","title":"Exited Plan Mode","toolCallId":"toolu_plan"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -8,7 +8,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"rawInput":{"plan":"# Plan\n1. Do it"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes, manually approve edits","optionId":"exit-plan-default"},{"kind":"allow_always","name":"Yes, clear context (0% used) and use auto mode","optionId":"exit-plan-clear-auto"},{"kind":"allow_always","name":"Yes, and use auto mode","optionId":"exit-plan-auto"},{"kind":"allow_always","name":"Yes, and bypass permissions","optionId":"exit-plan-bypass"},{"kind":"reject_once","name":"No, keep planning","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{"plan":"# Plan\n1. Do it"},"title":"Approve Plan","toolCallId":"toolu_plan"},"type":"tool_call"},"title":"Ready to code?"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes, manually approve edits","optionId":"exit-plan-default"},{"kind":"allow_always","name":"Yes, clear context (0% used) and use auto mode","optionId":"exit-plan-clear-auto"},{"kind":"allow_always","name":"Yes, and use auto mode","optionId":"exit-plan-auto"},{"kind":"allow_always","name":"Yes, and bypass permissions","optionId":"exit-plan-bypass"},{"kind":"reject_once","name":"No, keep planning","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{"plan":"# Plan\n1. Do it"},"status":"pending","title":"Approve Plan","toolCallId":"toolu_plan"},"type":"tool_call"},"title":"Ready to code?"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\n```\nThe user doesn't want to proceed with this tool use.\n```\n```","type":"text"},"type":"content"}],"rawOutput":"The user doesn't want to proceed with this tool use.","sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"/goal Ship the feature","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -8,7 +8,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"q":"acp"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_mcp"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"mcpServer":{"name":"docs","source":"project"}}},"content":[],"kind":"other","name":"mcp__docs__search","rawInput":{"q":"acp"},"title":"mcp__docs__search","toolCallId":"toolu_mcp"},"type":"tool_call"},"title":"mcp__docs__search"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"mcpServer":{"name":"docs","source":"project"}}},"content":[],"kind":"other","name":"mcp__docs__search","rawInput":{"q":"acp"},"status":"pending","title":"mcp__docs__search","toolCallId":"toolu_mcp"},"type":"tool_call"},"title":"mcp__docs__search"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Found 2 documents.","type":"text"},"type":"content"}],"rawOutput":[{"text":"Found 2 documents.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_mcp"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,11 +1,10 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"other","name":"SandboxNetworkAccess","rawInput":{"host":"example.com"},"sessionUpdate":"tool_call_update","status":"pending","title":"SandboxNetworkAccess","toolCallId":"toolu_net"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"description":"Reason: Sandbox blocks network","options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"```json\n{\n \"host\": \"example.com\"\n}\n```","type":"text"},"type":"content"}],"kind":"other","name":"SandboxNetworkAccess","rawInput":{"host":"example.com"},"title":"example.com","toolCallId":"toolu_net"},"type":"tool_call"},"title":"example.com"}}
{"kind":"request","method":"session/request_permission","payload":{"description":"Reason: Sandbox blocks network","options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"```json\n{\n \"host\": \"example.com\"\n}\n```","type":"text"},"type":"content"}],"kind":"other","name":"SandboxNetworkAccess","rawInput":{"host":"example.com"},"status":"pending","title":"example.com","toolCallId":"toolu_net"},"type":"tool_call"},"title":"example.com"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_net"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
@@ -11,7 +11,7 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"cell_id":"cell-1","edit_mode":"replace","new_source":"print('hi')","notebook_path":"<cwd>/n.ipynb"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_nb"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[],"kind":"other","name":"NotebookEdit","rawInput":{"cell_id":"cell-1","edit_mode":"replace","new_source":"print('hi')","notebook_path":"<cwd>/n.ipynb"},"title":"NotebookEdit","toolCallId":"toolu_nb"},"type":"tool_call"},"title":"NotebookEdit"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[],"kind":"other","name":"NotebookEdit","rawInput":{"cell_id":"cell-1","edit_mode":"replace","new_source":"print('hi')","notebook_path":"<cwd>/n.ipynb"},"status":"pending","title":"NotebookEdit","toolCallId":"toolu_nb"},"type":"tool_call"},"title":"NotebookEdit"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Updated cell cell-1 with print('hi')","type":"text"},"type":"content"}],"rawOutput":"Updated cell cell-1 with print('hi')","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_nb"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"msg_r1","sessionUpdate":"agent_thought"}}}
@@ -16,6 +16,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Report.","type":"text"},"type":"content"}],"rawOutput":"Report.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"msg_r2","sessionUpdate":"agent_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"description":"The history of 1 subagent of this session could not be restored.","sessionUpdate":"notice","severity":"info","title":"Some subagent history is not available"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"idle"}}}
{"kind":"response","method":"session/resume","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}]}}

View File

@@ -1,2 +1,2 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

Some files were not shown because too many files have changed in this diff Show More