mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-09 19:02:05 +00:00
Compare commits
19 Commits
v0.100.0
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39e511c97 | ||
|
|
a58293f4c9 | ||
|
|
3b22586cf6 | ||
|
|
47b64c04de | ||
|
|
1ee2422934 | ||
|
|
7d1ef76c1d | ||
|
|
aa0c1234b5 | ||
|
|
54392e144d | ||
|
|
6597626d86 | ||
|
|
ba162346c9 | ||
|
|
85aec26ef9 | ||
|
|
7ea9eec839 | ||
|
|
a79d07324d | ||
|
|
1d4418f80d | ||
|
|
21d24f9e8b | ||
|
|
e3f0b6c89c | ||
|
|
8bbdea6cc6 | ||
|
|
1e986b526d | ||
|
|
53e3eec013 |
125
.github/workflows/binaries.yml
vendored
Normal file
125
.github/workflows/binaries.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
# Builds the standalone executables (scripts/build-binary.mjs) and smoke-tests
|
||||
# each one on a runner of its own platform. Each archive holds
|
||||
# `claude-agent-acp` and the Claude CLI (`claude`) it runs from its directory.
|
||||
# CI calls this to check that every target still builds and starts; the
|
||||
# release calls it with `upload: true` and attaches the archives to the GitHub
|
||||
# release.
|
||||
name: Binaries
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
upload:
|
||||
description: Upload each archive as a workflow artifact for the release job.
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Binary (${{ matrix.target }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: bun-linux-x64
|
||||
runner: ubuntu-latest
|
||||
archive: claude-agent-acp-linux-x64.tar.gz
|
||||
- target: bun-linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
archive: claude-agent-acp-linux-arm64.tar.gz
|
||||
# npm skips the musl package on a glibc host, so these are
|
||||
# cross-built and smoke-tested in an Alpine container.
|
||||
- target: bun-linux-x64-musl
|
||||
runner: ubuntu-latest
|
||||
archive: claude-agent-acp-linux-x64-musl.tar.gz
|
||||
musl: true
|
||||
- target: bun-linux-arm64-musl
|
||||
runner: ubuntu-24.04-arm
|
||||
archive: claude-agent-acp-linux-arm64-musl.tar.gz
|
||||
musl: true
|
||||
- target: bun-darwin-x64
|
||||
runner: macos-15-intel
|
||||
archive: claude-agent-acp-darwin-x64.zip
|
||||
- target: bun-darwin-arm64
|
||||
runner: macos-latest
|
||||
archive: claude-agent-acp-darwin-arm64.zip
|
||||
- target: bun-windows-x64
|
||||
runner: windows-latest
|
||||
archive: claude-agent-acp-windows-x64.zip
|
||||
- target: bun-windows-arm64
|
||||
runner: windows-11-arm
|
||||
archive: claude-agent-acp-windows-arm64.zip
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
check-latest: true
|
||||
cache: "npm"
|
||||
|
||||
# Unpinned: each build is smoke-tested on its platform before it uploads,
|
||||
# so a Bun release that breaks the executable fails here instead of
|
||||
# shipping. Pin a version if one ever regresses past the smoke test.
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Install the musl Claude CLI package
|
||||
if: ${{ matrix.musl }}
|
||||
run: |
|
||||
version="$(node -p "require('./node_modules/@anthropic-ai/claude-agent-sdk/package.json').version")"
|
||||
npm install --no-save --force "@anthropic-ai/claude-agent-sdk-linux-${RUNNER_ARCH,,}-musl@$version"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
echo "Built \`${{ matrix.target }}\` with Bun $(bun --version)" >> "$GITHUB_STEP_SUMMARY"
|
||||
node scripts/build-binary.mjs --target ${{ matrix.target }}
|
||||
|
||||
- name: Smoke test
|
||||
if: ${{ !matrix.musl }}
|
||||
run: node scripts/smoke-binary.mjs
|
||||
|
||||
# Claude Code's Alpine requirements: libgcc and libstdc++.
|
||||
- name: Smoke test (Alpine)
|
||||
if: ${{ matrix.musl }}
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/work" -w /work node:lts-alpine \
|
||||
sh -c 'apk add --no-cache libgcc libstdc++ >/dev/null && node scripts/smoke-binary.mjs'
|
||||
|
||||
- name: Archive
|
||||
if: ${{ inputs.upload }}
|
||||
env:
|
||||
ARCHIVE: ${{ matrix.archive }}
|
||||
run: |
|
||||
cd dist-bin
|
||||
if [[ "$ARCHIVE" == *.zip ]]; then
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
pwsh -Command "Compress-Archive -Path claude-agent-acp.exe,claude.exe -DestinationPath ../$ARCHIVE"
|
||||
else
|
||||
zip "../$ARCHIVE" claude-agent-acp claude
|
||||
fi
|
||||
else
|
||||
tar czf "../$ARCHIVE" claude-agent-acp claude
|
||||
fi
|
||||
|
||||
- name: Upload
|
||||
if: ${{ inputs.upload }}
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ matrix.archive }}
|
||||
path: ${{ matrix.archive }}
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -44,3 +44,7 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:run
|
||||
|
||||
binaries:
|
||||
name: Binaries
|
||||
uses: ./.github/workflows/binaries.yml
|
||||
|
||||
38
.github/workflows/publish.yml
vendored
38
.github/workflows/publish.yml
vendored
@@ -88,6 +88,38 @@ jobs:
|
||||
npm publish
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Standalone executables for the GitHub release, stable releases only. They
|
||||
# don't gate the npm publish: a failed build leaves the release without
|
||||
# binaries, and re-running the failed jobs attaches them.
|
||||
build-binaries:
|
||||
name: Build binaries
|
||||
needs: [release-please]
|
||||
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
||||
# The called workflow only reads the repository; upload-binaries does the write.
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/binaries.yml
|
||||
with:
|
||||
upload: true
|
||||
|
||||
upload-binaries:
|
||||
name: Attach binaries to the release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-please, build-binaries]
|
||||
permissions:
|
||||
contents: write
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Upload to the release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.release-please.outputs.tag_name }}
|
||||
run: gh release upload "$TAG" artifacts/* --clobber --repo "$GITHUB_REPOSITORY"
|
||||
|
||||
# Same shape as publish-npm. Tagging is a separate job, so nothing here needs
|
||||
# write access to the repository.
|
||||
publish-npm-preview:
|
||||
@@ -199,7 +231,11 @@ jobs:
|
||||
echo "Tagged \`v$VERSION\` at \`$SHA\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
trigger-registry-update:
|
||||
needs: [publish-npm, publish-npm-preview]
|
||||
# upload-binaries too, so a stable release's archives are attached before the
|
||||
# registry reads the release: it takes each archive's sha256 from the release
|
||||
# assets when it bumps the version. It only waits; a failed or skipped upload
|
||||
# (every preview) doesn't stop the dispatch.
|
||||
needs: [publish-npm, publish-npm-preview, upload-binaries]
|
||||
# Both channels dispatch this: the registry has its own handling for preview
|
||||
# versions. The two never fire together — a release merge publishes stable and
|
||||
# skips the preview, and every other push does the reverse — so the registry sees
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ dist
|
||||
.DS_Store
|
||||
.claude/*.local.*
|
||||
CLAUDE.local.md
|
||||
.idea/
|
||||
.idea/
|
||||
dist-bin
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.100.0"
|
||||
".": "0.101.0"
|
||||
}
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [0.101.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.100.0...v0.101.0) (2026-11-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* compact idle sessions before the prompt cache expires ([#1522](https://github.com/agentclientprotocol/claude-agent-acp/issues/1522)) ([a79d073](https://github.com/agentclientprotocol/claude-agent-acp/commit/a79d07324dc15808c94a889a3b60615cca5a9fbd)), closes [#1454](https://github.com/agentclientprotocol/claude-agent-acp/issues/1454) [#1518](https://github.com/agentclientprotocol/claude-agent-acp/issues/1518)
|
||||
* ship single-file executables built with bun ([#1524](https://github.com/agentclientprotocol/claude-agent-acp/issues/1524)) ([e3f0b6c](https://github.com/agentclientprotocol/claude-agent-acp/commit/e3f0b6c89c783939e6ca8c19c220c21b58263f46))
|
||||
* start sessions in Auto mode by default ([#1533](https://github.com/agentclientprotocol/claude-agent-acp/issues/1533)) ([7ea9eec](https://github.com/agentclientprotocol/claude-agent-acp/commit/7ea9eec8392a43191b25a5c3636c181cc7eb42f3))
|
||||
* stream the output of v2 background commands ([#1512](https://github.com/agentclientprotocol/claude-agent-acp/issues/1512)) ([53e3eec](https://github.com/agentclientprotocol/claude-agent-acp/commit/53e3eec0134209654f53c46f319aaac851b8fff0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* honor configured fallback for unavailable restored modes ([#1520](https://github.com/agentclientprotocol/claude-agent-acp/issues/1520)) ([8bbdea6](https://github.com/agentclientprotocol/claude-agent-acp/commit/8bbdea6cc62a7ae080cb73ff476ed28ad7574018))
|
||||
* update to @anthropic-ai/claude-agent-sdk 0.3.318 ([#1525](https://github.com/agentclientprotocol/claude-agent-acp/issues/1525)) ([1d4418f](https://github.com/agentclientprotocol/claude-agent-acp/commit/1d4418f80daf14c29cecc5b23286f52c7f0402ec))
|
||||
|
||||
## [0.100.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.1...v0.100.0) (2026-11-02)
|
||||
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -36,6 +36,23 @@ To try changes that have landed on `main` but are not released yet, install from
|
||||
npm install @agentclientprotocol/claude-agent-acp@preview
|
||||
```
|
||||
|
||||
### Standalone executables
|
||||
|
||||
Each release attaches archives for Linux (glibc and musl), macOS and Windows, on
|
||||
x64 and arm64, to its [GitHub release](https://github.com/agentclientprotocol/claude-agent-acp/releases).
|
||||
They need no Node.js install. Each holds two files: `claude-agent-acp`, and the
|
||||
Claude CLI (`claude`) it runs. Keep them in the same directory; a symlink to
|
||||
`claude-agent-acp` elsewhere works. To use a Claude CLI you already have
|
||||
instead, set `CLAUDE_CODE_EXECUTABLE`. On Alpine, install `libgcc` and
|
||||
`libstdc++` first.
|
||||
|
||||
To build one for your machine (needs [Bun](https://bun.sh)) and check it:
|
||||
|
||||
```sh
|
||||
npm run build:binary
|
||||
npm run smoke:binary # add `-- --full` to drive a real session; needs a Claude login
|
||||
```
|
||||
|
||||
### Subagent sessions
|
||||
|
||||
A client that declares `clientCapabilities.subagents`, other than JetBrains AIR, gets each Agent or
|
||||
|
||||
@@ -7,7 +7,9 @@ keeps a single release PR open, titled `chore(main): release X.Y.Z` and labelled
|
||||
`autorelease: pending`.
|
||||
|
||||
Merging that PR is what releases. It tags `vX.Y.Z`, creates the GitHub release,
|
||||
publishes to npm, and dispatches a version update to the agent registry.
|
||||
publishes to npm, dispatches a version update to the agent registry, and
|
||||
attaches the [standalone executables](../README.md#standalone-executables) to
|
||||
the GitHub release. Previews get no executables.
|
||||
|
||||
There is no manual release button, and versions are never typed in by hand: the
|
||||
version is an output of the commit history, not an input.
|
||||
@@ -45,6 +47,9 @@ gh release view "v<version>"
|
||||
npm view "@agentclientprotocol/claude-agent-acp@<version>"
|
||||
```
|
||||
|
||||
The release should list 8 `claude-agent-acp-*` archives. They are attached a
|
||||
few minutes after the release is created, once every platform has built.
|
||||
|
||||
## Preview releases
|
||||
|
||||
Every push to `main` that is not a release merge publishes a preview from the
|
||||
@@ -203,6 +208,13 @@ npm versions are immutable. If the package already published and only the
|
||||
registry update failed, pass `-f publish_npm=false` so the run skips publishing
|
||||
and only re-dispatches the registry update.
|
||||
|
||||
### The release has no executables
|
||||
|
||||
`build-binaries` and `upload-binaries` don't gate npm, so a failed build only
|
||||
leaves the release without archives. Re-run the failed jobs of that publish run
|
||||
(`gh run rerun <run-id> --failed`); the upload uses `--clobber`, so archives
|
||||
already attached are replaced rather than duplicated.
|
||||
|
||||
### A preview published but the commit was not tagged
|
||||
|
||||
Only the publish is irreversible, so re-run just the tag job:
|
||||
|
||||
@@ -1679,14 +1679,14 @@ For AIR, both forms carry `_meta.jetbrains.air.contextCompaction`:
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
| ------------ | --------------------------------------------------------------------- |
|
||||
| `version` | Must equal `1`. |
|
||||
| `trigger` | `manual` or `automatic`. |
|
||||
| `preTokens` | The context size before the compaction. |
|
||||
| `postTokens` | The context size after the compaction. |
|
||||
| `durationMs` | The duration of the compaction. |
|
||||
| `error` | The error of a failed compaction. Only the tool call form carries it. |
|
||||
| Field | Meaning |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------- |
|
||||
| `version` | Must equal `1`. |
|
||||
| `trigger` | `manual`, `automatic`, or `idle`: the adapter compacted an idle session before its prompt cache expired. |
|
||||
| `preTokens` | The context size before the compaction. |
|
||||
| `postTokens` | The context size after the compaction. |
|
||||
| `durationMs` | The duration of the compaction. |
|
||||
| `error` | The error of a failed compaction. Only the tool call form carries it. |
|
||||
|
||||
The standard `toolCallId` or `compactionId` and the `status` own the identity and the phase.
|
||||
The `compaction_update` form puts the error in the standard `error` field.
|
||||
|
||||
176
package-lock.json
generated
176
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.100.0",
|
||||
"version": "0.101.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.100.0",
|
||||
"version": "0.101.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.4.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
||||
"diff": "9.0.0",
|
||||
"zod": "4.6.7"
|
||||
},
|
||||
@@ -18,7 +18,7 @@
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "1.3.0",
|
||||
"@anthropic-ai/sdk": "1.6.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.6",
|
||||
"@types/node": "26.6.6",
|
||||
@@ -31,7 +31,7 @@
|
||||
"globals": "17.14.0",
|
||||
"prettier": "3.9.10",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
"vitest": "5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
@@ -53,22 +53,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.316.tgz",
|
||||
"integrity": "sha512-mCMr0que5kIy80JslJub6WFDB2+GYRGWNeVgSZaBpkBwXrr4wnRMnnGHitkA2BEJ5QP5xWyc6YH/6do/edxCWg==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.319.tgz",
|
||||
"integrity": "sha512-7X+sh4uZv/u4OL4I4cQvMr9N3NM6FDiuyk829hFylgbrnNwVyiGElWEDWGuO26zwMIxmgpKdPoDILoFIHh1uwA==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.316"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.319",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.319"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -77,9 +77,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.316.tgz",
|
||||
"integrity": "sha512-1I1oJ/YGqq4pFQvJqycD2e8pbsUQLLR+DwEzG9jQAGCoSKkxP1TBfHpjSik/0byCixyzpYObrYXPC0PKBIR91g==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.319.tgz",
|
||||
"integrity": "sha512-rNiAbMbRhgOM2CELsDTIq7q++sFVIZukTkgeswcncGbcX8STIgtf0XaZzKdRXtKK+v9ez+mQn8P4pb2Ka17wGA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -90,9 +90,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.316.tgz",
|
||||
"integrity": "sha512-gzNncKrID/6yJIKCAuF/pY1kfygQZ4dU0J+MNFl0bU+iQW65ERoIyoghYVfrAQ/gH2fuvnQtWxa8LQNScHgNMA==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.319.tgz",
|
||||
"integrity": "sha512-jM730Fl+cFotDnjF9cStIf+Xe57YdfDB3TXaSVVv8uVAXW4xoWFwIj2eDvmsciEEe5UMkKzjVqXu9kR21OrXng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -103,9 +103,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.316.tgz",
|
||||
"integrity": "sha512-+lm9XSw/em14yMZ41fbo80kfblbylfBQc9nYS6GgLoZVvgT3wSeEbv88v+mEK/jFw9g3VCH4ZI01sjNM34o7uw==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.319.tgz",
|
||||
"integrity": "sha512-g2tpMsHLMQahI4bk5CIcMHAB/RwzxoPGmL5Q5G5MDir5WTrl71vulWbtLQISUPm/yFPRG97tdzZnCa5Uq1v/dw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -119,9 +119,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.316.tgz",
|
||||
"integrity": "sha512-1qqaEdsAs6eipEQQ4bMAWiwDjIouyA/YN5KQWS0/+kuPb+GKS8rUVW2dty2elV703hsMovb4IhMP4cv5K7R9YA==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.319.tgz",
|
||||
"integrity": "sha512-8KDZQJAS2CEb5aGAvuXkpyHMc9TOM/zYTw2iKmbbVi4im0atw93EtJPapYyp0g/lQI/Xk8jMYdWU6Zlv9EWjLw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -135,9 +135,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.316.tgz",
|
||||
"integrity": "sha512-+csYyGkHziNp4It+ZykqTmXRZBuNrieTiBnji+zwAxNmLLCWz8xEWjNmwhaLPIMpUiaYREkDhZXt/AS6xXta5w==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.319.tgz",
|
||||
"integrity": "sha512-uBAZA8x0hd5gVifyL5Lo73dVmi9xZeJ6jro2NO0al6yyWaw/6UL9xbsVh9lOhLFOmYIdSbQ2j4zAfnvG/t5F3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -151,9 +151,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.316.tgz",
|
||||
"integrity": "sha512-pMG9MRkjxMMbIxpwg/cKz6JorNjZtJOA9l/fifRQ/A01zGTKrRIaSCWoGg+Asmc64QBOa3aA4vzE/od9B1R0Tw==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.319.tgz",
|
||||
"integrity": "sha512-XkKYj3c1jVFjLATrZr4YTsmS5AIb5s2iphF9X670EcVQqDCCGIKKTVNLLg9mg1e7D+2xX8FU0XNlOJatWfpBeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -167,9 +167,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.316.tgz",
|
||||
"integrity": "sha512-Xj4iTS8NsB6isGhnZEnaIXIpUcSspv/lyIVnfPZjVGwuttaVHjq4IxfDopsPaVsPuTKd+L7DLAaBram1k85U2g==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.319.tgz",
|
||||
"integrity": "sha512-YoP0lYHJCziENFCotHQq9qd/gaJrwUNkAcnZyVjXxg1OPmtKuyw31aG60s4cWXRDXXGm/m1siNSQaGGfAYBvrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -180,9 +180,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.316",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.316.tgz",
|
||||
"integrity": "sha512-FzQA1zHbYJTtLR9nn1McwkGYeiEmroTmYjIg7xZqo5eweJ8U/DESZKR/QdfF04MXe2DF23oLL8WHjEa+WxbGQw==",
|
||||
"version": "0.3.319",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.319.tgz",
|
||||
"integrity": "sha512-UC27x+8gfCS2OjNGJWrueP0gLOOKsLEsJ/kg0p6aa8GToZcJYjJ8KeAapHHRYMUAIB6Sarqe49Itm5P1wswCcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -193,9 +193,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.3.0.tgz",
|
||||
"integrity": "sha512-k7z9BlLLPxfZH+ov7dS6EnaN/wiZeV8YNT1LZfrpAHIy7oY7pHr4UzirF9f2OlHtFEnRBOX+AZQ91Y5fK+TPTw==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.6.0.tgz",
|
||||
"integrity": "sha512-XqiO/46n8S6So8jQ/6/uNCkHntyW6QHHs1gZ+hlwTx1fWDr929g4VmwIXfdMVk7SuBWNdQvAlwyi1G3t6SEIuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
@@ -215,9 +215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
|
||||
"integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
|
||||
"version": "7.29.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz",
|
||||
"integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -267,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz",
|
||||
"integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -843,9 +843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz",
|
||||
"integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
@@ -1146,14 +1146,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz",
|
||||
"integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz",
|
||||
"integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@vitest/spy": "5.0.5",
|
||||
"@vitest/spy": "5.1.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^1.4.3"
|
||||
},
|
||||
@@ -1174,9 +1174,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz",
|
||||
"integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz",
|
||||
"integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1945,9 +1945,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.7.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.2.tgz",
|
||||
"integrity": "sha512-U1ScocxDD9WI09LaBVypvNkOtNR5yWAmZLkuKJdAzIchA4YKFYF6GT5IKER7epG58oXtjvGlV0/6ii0F83EyZg==",
|
||||
"version": "8.7.3",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.3.tgz",
|
||||
"integrity": "sha512-zPLqDI2f0gV2WmGnacLf9XSSqn4VldRyP5LCzOwx0BKH40mO6+2PCFkoSsRO2jePx87/Kf6NUF6P2QOTKXHbQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -2270,9 +2270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.16",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.16.tgz",
|
||||
"integrity": "sha512-AOLOEhvrV+iH92xXCnFr/5+3KjPKcI3xP63XJtX5spAnkznPdHijlIsUTpi7/9EhWqrTCekw5c5zh1K+YsVw7A==",
|
||||
"version": "4.13.17",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.17.tgz",
|
||||
"integrity": "sha512-vZIcbkvjOR5hHRWERbGkr8fGfF0GGM1ooYkuH448iO3b2AdVEr3AXZSw4Z3Kn+1L5z0CVoVeyV29VGYmFh05kQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2352,9 +2352,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.7.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz",
|
||||
"integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==",
|
||||
"version": "10.7.6",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.6.tgz",
|
||||
"integrity": "sha512-00LYkM6byILa9eQIx6qoaXCM40xTLd56AZ4vyaPW3YTyb49VZYPo5xsiAq4tuQ54GH4R7gVdYScJ1B7B77PuTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2408,9 +2408,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.15",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.15.tgz",
|
||||
"integrity": "sha512-qdF6ewcZARtef63fJ9AKKZPgxw+t7USH6HLXYcD4CrdmSvDp79rpJSPFEKc90QBeGS63u1KQtN2MZQHC0yDIzA==",
|
||||
"version": "6.2.16",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.16.tgz",
|
||||
"integrity": "sha512-4LSJpbuBuPdGrONCdZUy0xnxtLf8eEz+EY0SMXdHQEBU7bgUHDVblU/JAjryv3wID9/LK7/YuwFwq+AQNfMscQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
@@ -3334,9 +3334,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"version": "7.8.6",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
|
||||
"integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -3699,17 +3699,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
|
||||
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz",
|
||||
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.30",
|
||||
"rolldown": "~1.2.14",
|
||||
"postcss": "^8.5.32",
|
||||
"rolldown": "~1.2.16",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -3778,14 +3778,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz",
|
||||
"integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz",
|
||||
"integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/mocker": "5.0.5",
|
||||
"@vitest/mocker": "5.1.0",
|
||||
"chai": "^6.3.0",
|
||||
"es-module-lexer": "^2.3.2",
|
||||
"expect-type": "^1.4.0",
|
||||
@@ -3811,12 +3811,12 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "5.0.5",
|
||||
"@vitest/browser-preview": "5.0.5",
|
||||
"@vitest/browser-playwright": "5.1.0",
|
||||
"@vitest/browser-preview": "5.1.0",
|
||||
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.5",
|
||||
"@vitest/coverage-v8": "5.0.5",
|
||||
"@vitest/ui": "5.0.5",
|
||||
"@vitest/coverage-istanbul": "5.1.0",
|
||||
"@vitest/coverage-v8": "5.1.0",
|
||||
"@vitest/ui": "5.1.0",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||
|
||||
11
package.json
11
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.100.0",
|
||||
"version": "0.101.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -21,12 +21,15 @@
|
||||
"files": [
|
||||
"dist/",
|
||||
"!dist/tests/",
|
||||
"!dist/binary-entry.*",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:binary": "node scripts/build-binary.mjs",
|
||||
"smoke:binary": "node scripts/smoke-binary.mjs",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "npm run build && npm run start",
|
||||
"example:simple-client": "node --experimental-strip-types --disable-warning=ExperimentalWarning examples/simple-client.ts",
|
||||
@@ -66,12 +69,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.4.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.316",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
||||
"diff": "9.0.0",
|
||||
"zod": "4.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "1.3.0",
|
||||
"@anthropic-ai/sdk": "1.6.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.6",
|
||||
"@types/node": "26.6.6",
|
||||
@@ -84,6 +87,6 @@
|
||||
"globals": "17.14.0",
|
||||
"prettier": "3.9.10",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
"vitest": "5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
92
scripts/build-binary.mjs
Normal file
92
scripts/build-binary.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds an executable of the agent with `bun build --compile` and copies the
|
||||
// target platform's Claude CLI next to it. Both files go in the release
|
||||
// archive: the executable runs the CLI from its own directory.
|
||||
//
|
||||
// node scripts/build-binary.mjs [--target bun-linux-x64-musl] [--outfile path]
|
||||
//
|
||||
// --target defaults to the host. Cross-compiling needs the target's SDK
|
||||
// platform package installed; the script prints the install command when it
|
||||
// is missing.
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, copyFileSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
function hostTarget() {
|
||||
const os = process.platform === "win32" ? "windows" : process.platform;
|
||||
// Node linked against glibc reports its version; musl builds don't.
|
||||
const musl =
|
||||
process.platform === "linux" && !process.report.getReport().header.glibcVersionRuntime;
|
||||
return `bun-${os}-${process.arch}${musl ? "-musl" : ""}`;
|
||||
}
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
target: { type: "string", default: hostTarget() },
|
||||
outfile: { type: "string" },
|
||||
},
|
||||
});
|
||||
const target = values.target;
|
||||
|
||||
// Bun targets look like `bun-<os>-<arch>` with optional `-musl`, `-baseline` or
|
||||
// `-modern` suffixes; the SDK names its packages `<platform>-<arch>[-musl]`.
|
||||
const match = /^bun-(linux|darwin|windows)-(x64|arm64|aarch64)(?:-.+)?$/.exec(target);
|
||||
if (!match) {
|
||||
console.error(
|
||||
`Unsupported target "${target}". Expected e.g. bun-linux-x64, bun-darwin-arm64, bun-windows-x64.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const [, os, rawArch] = match;
|
||||
const platform = os === "windows" ? "win32" : os;
|
||||
const arch = rawArch === "aarch64" ? "arm64" : rawArch;
|
||||
const musl = target.split("-").includes("musl");
|
||||
const exe = os === "windows" ? ".exe" : "";
|
||||
const cliPackage = `@anthropic-ai/claude-agent-sdk-${platform}-${arch}${musl ? "-musl" : ""}`;
|
||||
const cliSpecifier = `${cliPackage}/claude${exe}`;
|
||||
const outfile = resolve(values.outfile ?? join(repoRoot, "dist-bin", `claude-agent-acp${exe}`));
|
||||
|
||||
// Resolve from the SDK, the way claudeCliPath() does at runtime, so a nested
|
||||
// install is checked rather than only a hoisted one.
|
||||
const sdkRequire = createRequire(
|
||||
createRequire(join(repoRoot, "package.json")).resolve("@anthropic-ai/claude-agent-sdk"),
|
||||
);
|
||||
let cliPath;
|
||||
try {
|
||||
cliPath = sdkRequire.resolve(cliSpecifier);
|
||||
} catch {
|
||||
const sdkVersion = JSON.parse(
|
||||
readFileSync(
|
||||
join(dirname(sdkRequire.resolve("@anthropic-ai/claude-agent-sdk")), "package.json"),
|
||||
"utf8",
|
||||
),
|
||||
).version;
|
||||
console.error(
|
||||
`${cliSpecifier} is not installed. npm only installs the host's platform package; for ${target} run:\n\n` +
|
||||
` npm install --no-save --force ${cliPackage}@${sdkVersion}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bun = spawnSync(
|
||||
"bun",
|
||||
["build", "src/binary-entry.ts", "--compile", "--target", target, "--outfile", outfile],
|
||||
{ cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" },
|
||||
);
|
||||
if (bun.error) {
|
||||
console.error(`Could not run bun: ${bun.error.message}. Install it from https://bun.sh.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (bun.status !== 0) process.exit(bun.status ?? 1);
|
||||
|
||||
// The executable runs the CLI from its own directory (src/binary-entry.ts).
|
||||
const cliOutfile = join(dirname(outfile), `claude${exe}`);
|
||||
copyFileSync(cliPath, cliOutfile);
|
||||
chmodSync(cliOutfile, 0o755);
|
||||
console.log(`Copied ${cliSpecifier} to ${cliOutfile}`);
|
||||
470
scripts/smoke-binary.mjs
Normal file
470
scripts/smoke-binary.mjs
Normal file
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env node
|
||||
// Smoke-tests a built agent over ACP. Defaults to the executable from
|
||||
// scripts/build-binary.mjs, with `claude` next to it. Pass another command
|
||||
// after `--` to compare, e.g.
|
||||
// `node scripts/smoke-binary.mjs --full -- node "$PWD/dist/index.js"` (the full
|
||||
// run starts the agent in a scratch directory, so paths must be absolute).
|
||||
//
|
||||
// node scripts/smoke-binary.mjs # no login needed
|
||||
// node scripts/smoke-binary.mjs --full # talks to the model; needs a logged-in Claude
|
||||
//
|
||||
// SMOKE_DEBUG=1 prints every tool call the full run saw.
|
||||
//
|
||||
// The basic run checks `--version`, `--cli --version`, `initialize` (v1 and
|
||||
// v2) and the `terminal-auth` login command. The
|
||||
// full run also has Claude use Bash, Read (text and image), Edit and a
|
||||
// subagent in a scratch directory, then cancels a turn, lists the session,
|
||||
// loads it in a fresh agent process, and forks and deletes it.
|
||||
//
|
||||
// Claude Code's native builds have no Grep or Glob tool: its Bash shell wraps
|
||||
// `grep` and `find` in functions that run the ugrep and bfs built into the
|
||||
// Claude binary at `$CLAUDE_CODE_EXECPATH`. The full run checks that those
|
||||
// run, since they are the helpers a packaged build could fail to reach.
|
||||
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import {
|
||||
copyFileSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { deflateSync } from "node:zlib";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const { values, positionals } = parseArgs({
|
||||
options: { full: { type: "boolean", default: false } },
|
||||
allowPositionals: true,
|
||||
});
|
||||
const exe = process.platform === "win32" ? ".exe" : "";
|
||||
const [command, ...commandArgs] =
|
||||
positionals.length > 0 ? positionals : [join(repoRoot, "dist-bin", `claude-agent-acp${exe}`)];
|
||||
|
||||
let failures = 0;
|
||||
function check(ok, label, detail) {
|
||||
console.log(
|
||||
`${ok ? "ok " : "FAIL"} ${label}${!ok && detail !== undefined ? `: ${detail}` : ""}`,
|
||||
);
|
||||
if (!ok) failures++;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- basic
|
||||
|
||||
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs]) {
|
||||
const result = spawnSync(cmd, [...cmdArgs, ...args], {
|
||||
encoding: "utf8",
|
||||
timeout: 60_000,
|
||||
});
|
||||
return { stdout: (result.stdout ?? "").trim(), error: result.error ?? result.stderr };
|
||||
}
|
||||
|
||||
const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
|
||||
const own = runSync(["--version"]);
|
||||
check(own.stdout === version, `--version prints ${version}`, own.stdout || own.error);
|
||||
const cli = runSync(["--cli", "--version"]);
|
||||
check(
|
||||
/\(Claude Code\)$/.test(cli.stdout),
|
||||
"--cli --version runs the bundled Claude CLI",
|
||||
cli.stdout || cli.error,
|
||||
);
|
||||
|
||||
// The release archive's layout: the executable finds `claude` in the directory
|
||||
// of its real path. Only for the default executable; a `--` command has no
|
||||
// such layout.
|
||||
if (positionals.length === 0) {
|
||||
const scratch = mkdtempSync(join(tmpdir(), "acp-layout-"));
|
||||
try {
|
||||
if (process.platform !== "win32") {
|
||||
const link = join(scratch, "claude-agent-acp");
|
||||
symlinkSync(command, link);
|
||||
const linked = runSync(["--cli", "--version"], [link]);
|
||||
check(
|
||||
/\(Claude Code\)$/.test(linked.stdout),
|
||||
"a symlink to the executable finds the bundled CLI",
|
||||
linked.stdout || linked.error,
|
||||
);
|
||||
rmSync(link);
|
||||
}
|
||||
const alone = join(scratch, `claude-agent-acp${exe}`);
|
||||
copyFileSync(command, alone);
|
||||
const missing = runSync(["--cli", "--version"], [alone]);
|
||||
check(
|
||||
String(missing.error).includes("The Claude CLI is missing"),
|
||||
"without claude next to it, says the CLI is missing",
|
||||
missing.stdout || missing.error,
|
||||
);
|
||||
} finally {
|
||||
rmSync(scratch, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the agent and speaks newline-delimited JSON-RPC to it. */
|
||||
function startAgent(cwd, onNotification) {
|
||||
const child = spawn(command, commandArgs, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
||||
const stderr = [];
|
||||
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
||||
let nextId = 0;
|
||||
const pending = new Map();
|
||||
const write = (message) =>
|
||||
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n");
|
||||
readline.createInterface({ input: child.stdout }).on("line", (line) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
check(false, "stdout carries only JSON-RPC", line.slice(0, 200));
|
||||
return;
|
||||
}
|
||||
if (message.method === undefined && pending.has(message.id)) {
|
||||
pending.get(message.id)(message);
|
||||
pending.delete(message.id);
|
||||
} else if (message.method === "session/request_permission") {
|
||||
const allow =
|
||||
message.params.options.find((o) => o.kind === "allow_once") ?? message.params.options[0];
|
||||
write({
|
||||
id: message.id,
|
||||
result: { outcome: { outcome: "selected", optionId: allow.optionId } },
|
||||
});
|
||||
} else if (message.id !== undefined && message.method !== undefined) {
|
||||
write({
|
||||
id: message.id,
|
||||
error: { code: -32601, message: `smoke client does not handle ${message.method}` },
|
||||
});
|
||||
} else {
|
||||
onNotification?.(message);
|
||||
}
|
||||
});
|
||||
const exited = new Promise((resolve) => child.on("exit", resolve));
|
||||
// Answer anything still pending if the agent dies, instead of hanging.
|
||||
let exitError;
|
||||
exited.then((code) => {
|
||||
exitError = { message: `agent exited with ${code}: ${Buffer.concat(stderr)}` };
|
||||
for (const resolve of pending.values()) resolve({ error: exitError });
|
||||
pending.clear();
|
||||
});
|
||||
return {
|
||||
request(method, params) {
|
||||
const id = ++nextId;
|
||||
return new Promise((resolve) => {
|
||||
if (exitError) return resolve({ error: exitError });
|
||||
pending.set(id, resolve);
|
||||
write({ id, method, params });
|
||||
});
|
||||
},
|
||||
notify: (method, params) => write({ method, params }),
|
||||
async stop() {
|
||||
child.stdin.end();
|
||||
const code = await Promise.race([
|
||||
exited,
|
||||
new Promise((r) => setTimeout(() => r("timeout"), 10_000)),
|
||||
]);
|
||||
if (code === "timeout") child.kill();
|
||||
return { code, stderr: Buffer.concat(stderr).toString() };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const initializeParams = { protocolVersion: 1, clientCapabilities: {} };
|
||||
{
|
||||
const agent = startAgent(process.cwd());
|
||||
const init = await agent.request("initialize", initializeParams);
|
||||
check(
|
||||
init.result?.agentInfo?.version === version,
|
||||
"initialize answers",
|
||||
JSON.stringify(init.error),
|
||||
);
|
||||
const { code } = await agent.stop();
|
||||
check(code === 0, "exits 0 when stdin closes", code);
|
||||
}
|
||||
{
|
||||
// The `terminal-auth` login command reruns the agent; run it with
|
||||
// `--version` in place of `auth login --console` instead of logging in.
|
||||
// `--version` wins over a stray argument, so also check that no path inside
|
||||
// a compiled executable leaked into the arguments.
|
||||
const agent = startAgent(process.cwd());
|
||||
const init = await agent.request("initialize", {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: { _meta: { "terminal-auth": true } },
|
||||
});
|
||||
await agent.stop();
|
||||
const login = init.result?.authMethods?.find((m) => m.id === "console-login")?._meta?.[
|
||||
"terminal-auth"
|
||||
];
|
||||
const cliArgs = login?.args?.slice(0, login.args.indexOf("--cli") + 1);
|
||||
const rerun = login
|
||||
? runSync([...cliArgs, "--version"], [login.command])
|
||||
: { stdout: "", error: "no terminal-auth console-login method" };
|
||||
check(
|
||||
/\(Claude Code\)$/.test(rerun.stdout) &&
|
||||
!login.args.some((arg) => arg.includes("$bunfs") || arg.includes("~BUN")),
|
||||
"the terminal-auth login command reruns the agent",
|
||||
`${JSON.stringify(login)} -> ${rerun.stdout || rerun.error}`,
|
||||
);
|
||||
}
|
||||
{
|
||||
const agent = startAgent(process.cwd());
|
||||
const init = await agent.request("initialize", {
|
||||
protocolVersion: 2,
|
||||
info: { name: "smoke-binary", version },
|
||||
capabilities: {},
|
||||
});
|
||||
check(
|
||||
init.result?.protocolVersion === 2,
|
||||
"initialize answers ACP v2",
|
||||
JSON.stringify(init.error),
|
||||
);
|
||||
await agent.stop();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- full
|
||||
|
||||
if (values.full) {
|
||||
const work = mkdtempSync(join(tmpdir(), "acp-smoke-"));
|
||||
try {
|
||||
await fullSmoke(work);
|
||||
} finally {
|
||||
rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** A solid red 16x16 PNG, so the model has something to name. */
|
||||
function redPng() {
|
||||
const crcTable = Array.from({ length: 256 }, (_, n) => {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
return c >>> 0;
|
||||
});
|
||||
const crc = (buf) => {
|
||||
let c = 0xffffffff;
|
||||
for (const byte of buf) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
};
|
||||
const chunk = (type, data) => {
|
||||
const body = Buffer.concat([Buffer.from(type), data]);
|
||||
const out = Buffer.alloc(body.length + 8);
|
||||
out.writeUInt32BE(data.length, 0);
|
||||
body.copy(out, 4);
|
||||
out.writeUInt32BE(crc(body), body.length + 4);
|
||||
return out;
|
||||
};
|
||||
const size = 16;
|
||||
const header = Buffer.alloc(13);
|
||||
header.writeUInt32BE(size, 0);
|
||||
header.writeUInt32BE(size, 4);
|
||||
header[8] = 8; // bit depth
|
||||
header[9] = 2; // RGB
|
||||
const row = Buffer.concat([Buffer.from([0]), Buffer.alloc(size * 3, Buffer.from([255, 0, 0]))]);
|
||||
return Buffer.concat([
|
||||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
||||
chunk("IHDR", header),
|
||||
chunk("IDAT", deflateSync(Buffer.concat(Array(size).fill(row)))),
|
||||
chunk("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Writes its results to files so the checks don't depend on the model's reply. */
|
||||
function searchCommand(token) {
|
||||
return [
|
||||
`grep -rl "${token}" . > grep.out`,
|
||||
'find . -name "*.md" > find.out',
|
||||
'{ echo "$CLAUDE_CODE_EXECPATH"; grep --version | head -1; find --version | head -1; } > tools.out',
|
||||
].join("; ");
|
||||
}
|
||||
|
||||
function searchResults(work) {
|
||||
const read = (name) => {
|
||||
try {
|
||||
return readFileSync(join(work, name), "utf8");
|
||||
} catch (error) {
|
||||
return String(error);
|
||||
}
|
||||
};
|
||||
const [execPath = "", grepVersion = "", findVersion = ""] = read("tools.out").split("\n");
|
||||
return { grep: read("grep.out"), find: read("find.out"), execPath, grepVersion, findVersion };
|
||||
}
|
||||
|
||||
async function fullSmoke(work) {
|
||||
const token = `SMOKE_TOKEN_${process.pid}`;
|
||||
mkdirSync(join(work, "notes"));
|
||||
mkdirSync(join(work, "src"));
|
||||
writeFileSync(join(work, "notes", "alpha.md"), `# Alpha\n\nThe token is ${token}.\n`);
|
||||
writeFileSync(join(work, "notes", "beta.md"), "# Beta\n\nNothing here.\n");
|
||||
writeFileSync(join(work, "src", "gamma.ts"), `export const value = "${token}";\n`);
|
||||
writeFileSync(join(work, "pixel.png"), redPng());
|
||||
|
||||
const toolCalls = new Map();
|
||||
let text = "";
|
||||
const onNotification = (message) => {
|
||||
if (message.method !== "session/update") return;
|
||||
const update = message.params.update;
|
||||
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
|
||||
text += update.content.text;
|
||||
} else if (
|
||||
update.sessionUpdate === "tool_call" ||
|
||||
update.sessionUpdate === "tool_call_update"
|
||||
) {
|
||||
const call = toolCalls.get(update.toolCallId) ?? {};
|
||||
toolCalls.set(update.toolCallId, {
|
||||
...call,
|
||||
name: update._meta?.claudeCode?.toolName ?? call.name,
|
||||
status: update.status ?? call.status,
|
||||
title: update.title ?? call.title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const agent = startAgent(work, onNotification);
|
||||
await agent.request("initialize", initializeParams);
|
||||
const created = await agent.request("session/new", { cwd: work, mcpServers: [] });
|
||||
if (!check(created.result?.sessionId, "session/new", JSON.stringify(created.error))) {
|
||||
console.error((await agent.stop()).stderr);
|
||||
return;
|
||||
}
|
||||
const sessionId = created.result.sessionId;
|
||||
|
||||
const started = Date.now();
|
||||
const prompt = await agent.request("session/prompt", {
|
||||
sessionId,
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"This is an automated smoke test of your tools. Do each step with the named tool, in order, one tool call per step:",
|
||||
`1. Bash: run exactly \`${searchCommand(token)}\``,
|
||||
"2. Read: read notes/alpha.md.",
|
||||
"3. Read: read pixel.png and note its color.",
|
||||
`4. Edit: in notes/alpha.md replace "${token}" with "EDITED".`,
|
||||
"5. Bash: run `cat notes/alpha.md`.",
|
||||
'6. Agent: launch a subagent (subagent_type "Explore") that counts the .ts files under src/ and reports the number.',
|
||||
"Then reply with one line: SMOKE-DONE color=<the color of pixel.png> tsfiles=<the subagent's count>.",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
});
|
||||
check(
|
||||
prompt.result?.stopReason === "end_turn",
|
||||
"prompt ends with end_turn",
|
||||
JSON.stringify(prompt.result ?? prompt.error),
|
||||
);
|
||||
console.log(` (turn took ${Math.round((Date.now() - started) / 1000)}s)`);
|
||||
|
||||
const calls = [...toolCalls.values()];
|
||||
if (process.env.SMOKE_DEBUG) console.log(JSON.stringify(calls, null, 1));
|
||||
const searched = searchResults(work);
|
||||
check(
|
||||
searched.grep.includes("alpha.md") && searched.grep.includes("gamma.ts"),
|
||||
"grep found the token",
|
||||
searched.grep,
|
||||
);
|
||||
check(
|
||||
searched.find.includes("alpha.md") && searched.find.includes("beta.md"),
|
||||
"find listed the .md files",
|
||||
searched.find,
|
||||
);
|
||||
check(
|
||||
/^ugrep /.test(searched.grepVersion),
|
||||
"grep ran Claude's embedded ugrep",
|
||||
searched.grepVersion,
|
||||
);
|
||||
check(/^bfs /.test(searched.findVersion), "find ran Claude's embedded bfs", searched.findVersion);
|
||||
console.log(` (CLAUDE_CODE_EXECPATH=${searched.execPath})`);
|
||||
for (const name of ["Read", "Edit", "Bash"]) {
|
||||
const used = calls.filter((c) => c.name === name);
|
||||
check(
|
||||
used.length > 0 && used.every((c) => c.status === "completed"),
|
||||
`${name} ran and completed`,
|
||||
JSON.stringify(used),
|
||||
);
|
||||
}
|
||||
const subagent = calls.filter((c) => c.name === "Agent" || c.name === "Task");
|
||||
check(
|
||||
subagent.some((c) => c.status === "completed"),
|
||||
"subagent ran and completed",
|
||||
JSON.stringify(subagent),
|
||||
);
|
||||
const failed = calls.filter((c) => c.status === "failed");
|
||||
check(failed.length === 0, "no tool call failed", JSON.stringify(failed));
|
||||
check(
|
||||
readFileSync(join(work, "notes", "alpha.md"), "utf8").includes("EDITED"),
|
||||
"Edit changed the file on disk",
|
||||
);
|
||||
check(
|
||||
/SMOKE-DONE/.test(text) && /color=\W*red/i.test(text),
|
||||
"image read as red",
|
||||
text.slice(-300),
|
||||
);
|
||||
check(/tsfiles=\W*1\b/.test(text), "subagent counted 1 .ts file", text.slice(-300));
|
||||
|
||||
// A cancel interrupts a running Bash command and ends the turn as cancelled.
|
||||
const cancelled = agent.request("session/prompt", {
|
||||
sessionId,
|
||||
prompt: [
|
||||
{ type: "text", text: "Use the Bash tool to run `sleep 60`. Do not run anything else." },
|
||||
],
|
||||
});
|
||||
const deadline = Date.now() + 60_000;
|
||||
const bashRunning = () =>
|
||||
[...toolCalls.values()].some((c) => c.name === "Bash" && c.status !== "completed");
|
||||
while (!bashRunning() && Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
agent.notify("session/cancel", { sessionId });
|
||||
const cancelResult = await cancelled;
|
||||
check(
|
||||
cancelResult.result?.stopReason === "cancelled",
|
||||
"session/cancel ends the turn as cancelled",
|
||||
JSON.stringify(cancelResult.result ?? cancelResult.error),
|
||||
);
|
||||
|
||||
const listed = await agent.request("session/list", { cwd: work });
|
||||
check(
|
||||
listed.result?.sessions?.some((s) => s.sessionId === sessionId),
|
||||
"session/list finds the session",
|
||||
JSON.stringify(listed.result ?? listed.error),
|
||||
);
|
||||
const { code, stderr } = await agent.stop();
|
||||
check(code === 0, "exits 0 when stdin closes", code);
|
||||
if (failures > 0) console.error(stderr);
|
||||
|
||||
// A fresh process, so the replay comes from the transcript on disk.
|
||||
let replayed = "";
|
||||
const reloaded = startAgent(work, (message) => {
|
||||
const update = message.params?.update;
|
||||
if (update?.sessionUpdate === "user_message_chunk" && update.content.type === "text")
|
||||
replayed += update.content.text;
|
||||
});
|
||||
await reloaded.request("initialize", initializeParams);
|
||||
const loaded = await reloaded.request("session/load", { sessionId, cwd: work, mcpServers: [] });
|
||||
check(!loaded.error, "session/load in a new process", JSON.stringify(loaded.error));
|
||||
check(replayed.includes(token), "session/load replays the first prompt", replayed.slice(0, 200));
|
||||
|
||||
// Fork and delete go through the SDK's transcript helpers, not the CLI.
|
||||
const forked = await reloaded.request("session/fork", { sessionId, cwd: work, mcpServers: [] });
|
||||
const forkId = forked.result?.sessionId;
|
||||
check(
|
||||
forkId && forkId !== sessionId,
|
||||
"session/fork",
|
||||
JSON.stringify(forked.result ?? forked.error),
|
||||
);
|
||||
if (forkId) {
|
||||
const deleted = await reloaded.request("session/delete", { sessionId: forkId });
|
||||
const after = await reloaded.request("session/list", { cwd: work });
|
||||
check(
|
||||
!deleted.error && !after.result?.sessions?.some((s) => s.sessionId === forkId),
|
||||
"session/delete removes the fork",
|
||||
JSON.stringify(deleted.error ?? after.result),
|
||||
);
|
||||
}
|
||||
await reloaded.stop();
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? "\nsmoke test passed" : `\n${failures} check(s) failed`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
530
src/acp-agent.ts
530
src/acp-agent.ts
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
agent as acpAgent,
|
||||
AgentApp,
|
||||
AgentContext,
|
||||
AuthenticateRequest,
|
||||
AuthMethod,
|
||||
AvailableCommand,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
CompleteElicitationNotification,
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
DisableProviderRequest,
|
||||
@@ -37,7 +35,6 @@ import {
|
||||
RequestError,
|
||||
SetProviderRequest,
|
||||
SetProviderResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
ResumeSessionRequest,
|
||||
ResumeSessionResponse,
|
||||
@@ -106,6 +103,10 @@ import {
|
||||
toGoalSnapshot,
|
||||
} from "./goal-extension.js";
|
||||
import { blobResourceToClaude } from "./blob-resources.js";
|
||||
import { ChangedMetaClient, ClientConnection } from "./acp-client.js";
|
||||
import type { AcpClient, Logger } from "./acp-client.js";
|
||||
|
||||
export type { AcpClient, Logger } from "./acp-client.js";
|
||||
import { promptSuggestionsMeta, promptSuggestionText } from "./prompt-suggestions.js";
|
||||
import { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
||||
import {
|
||||
@@ -165,20 +166,11 @@ import {
|
||||
clientSupportsAsyncTasks,
|
||||
} from "./async-tasks.js";
|
||||
import {
|
||||
backgroundLevel,
|
||||
type BackgroundLevel,
|
||||
type BackgroundSink,
|
||||
backgroundToolCallPause,
|
||||
backgroundToolCallStates,
|
||||
type BackgroundTaskEnd,
|
||||
type BackgroundToolCall,
|
||||
BackgroundWorkReporter,
|
||||
cancelBackgroundWork,
|
||||
droppedTasks,
|
||||
endBackgroundWork,
|
||||
registerBackgroundToolCalls,
|
||||
takeBackgroundTerminalExit,
|
||||
takeBackgroundToolCallEnd,
|
||||
SessionBackgroundWork,
|
||||
} from "./background-work.js";
|
||||
import {
|
||||
AUTH_STATUS_PROBE_TIMEOUT_MS,
|
||||
@@ -193,6 +185,8 @@ import {
|
||||
notLoggedInAuthStatus,
|
||||
sameAuthStatus,
|
||||
} from "./auth-status.js";
|
||||
import { agentCommand } from "./agent-command.js";
|
||||
import { claudeCliPath } from "./claude-cli.js";
|
||||
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import { execFile } from "node:child_process";
|
||||
@@ -239,7 +233,7 @@ import {
|
||||
type ResumedSessionSnapshot,
|
||||
} from "./resumed-session.js";
|
||||
import { SessionTiming } from "./session-timing.js";
|
||||
import { ALLOW_BYPASS } from "./permissions/modes.js";
|
||||
import { ALLOW_BYPASS, autoModeDisabled } from "./permissions/modes.js";
|
||||
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
||||
import { buildClaudePermissionOptions } from "./permissions/options.js";
|
||||
import {
|
||||
@@ -288,8 +282,10 @@ import {
|
||||
contextCompactionMetadataFromBoundary,
|
||||
isCompactSummaryMessage,
|
||||
} from "./context-compaction.js";
|
||||
import { IdleCompaction } from "./idle-compaction.js";
|
||||
import {
|
||||
clientSupportsNotices,
|
||||
collapseHookSpeaker,
|
||||
MAX_NOTICE_TITLE_LENGTH,
|
||||
normalizeNoticeText,
|
||||
noticeOrTranscriptUpdate,
|
||||
@@ -323,7 +319,6 @@ import {
|
||||
withRunningTerminal,
|
||||
withRunningToolCall,
|
||||
} from "./tool-calls/background.js";
|
||||
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
||||
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
|
||||
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
|
||||
import {
|
||||
@@ -432,16 +427,6 @@ function isEmptyUserInterruptionDiagnostic(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger interface for customizing logging output
|
||||
*/
|
||||
export interface Logger {
|
||||
log: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
|
||||
warn?: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
type AccumulatedUsage = {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -804,6 +789,11 @@ function parseSteerRequest(params: unknown): SteerRequest {
|
||||
};
|
||||
}
|
||||
|
||||
/** Who started a turn. `idleCompaction` is the adapter's own `/compact` (see
|
||||
* `IdleCompaction`): it is not a user prompt, so it neither disarms idle
|
||||
* compaction, asks for a title from its prompt, nor clears the published prompt suggestion. */
|
||||
type TurnOrigin = "client" | "idleCompaction";
|
||||
|
||||
/** One in-flight turn, started by `startTurn()`. A persistent per-session
|
||||
* consumer (see `runConsumer`) drains the SDK query stream for the whole
|
||||
* session and settles each Turn when that turn's outcome is known, so
|
||||
@@ -1476,30 +1466,17 @@ export type Session = {
|
||||
* to stop each one itself. A client that can cancel one subagent gets the
|
||||
* declaration, but it has no control for a workflow unless it is AIR with
|
||||
* `asyncTasks` (`_session/async_task/stop`), or gets the session's
|
||||
* background work (`reportsBackground`, with `tool_call/cancel`), which
|
||||
* background work (`background.reported`, with `tool_call/cancel`), which
|
||||
* `session/cancel` must leave running. So for any other such client the
|
||||
* adapter stops the workflows after the interrupt, as the interrupt did
|
||||
* before. */
|
||||
stopsWorkflowsOnCancel?: boolean;
|
||||
/** The display terminals of the commands that went to the background and
|
||||
* still run, by the id of the command's task: the Bash tool call whose
|
||||
* terminal it is, and the subagent tool call that ran it, if any. The
|
||||
* terminal exits when the task ends, not at the tool result (see
|
||||
* `withRunningTerminal`). Only for a client that renders terminals and is
|
||||
* not AIR, which shows the command through its async tasks. */
|
||||
backgroundTerminals?: Map<string, { toolCallId: string; parentToolUseId?: string }>;
|
||||
/** The tool calls whose tasks went on in the background and still run, by
|
||||
* task id: a Monitor, a Workflow, or a backgrounded MCP call. The tool call
|
||||
* stays `in_progress` (or `paused`) after its result, until its task ends
|
||||
* (see `withRunningToolCall`). Only while `reportsBackground`. */
|
||||
backgroundToolCalls?: Map<string, BackgroundToolCall>;
|
||||
/** Whether the client gets the session's background work (see
|
||||
* `background-work.ts`). Only when Claude Code's interrupt spares
|
||||
* background tasks, as `perTaskStopAffordance` makes it. */
|
||||
reportsBackground?: boolean;
|
||||
/** Claude Code's live background task set, from its last
|
||||
* `background_tasks_changed`. Per CLI process, as is a session. */
|
||||
backgroundLevel?: BackgroundLevel;
|
||||
/** The tasks that Claude Code runs in the background for the session: the
|
||||
* terminals of commands, kept for a client that renders terminals and is
|
||||
* not AIR (AIR shows a command through its async tasks), and, when the
|
||||
* client gets the session's background work, the tool calls of tasks and
|
||||
* the `background` lists. Per CLI process, as is a session. */
|
||||
background: SessionBackgroundWork;
|
||||
/** The parent tool call of each subagent that a settle bookend removed from
|
||||
* `liveBackgroundTasks`. The SDK can resume such a subagent under the same
|
||||
* agent id without a new `task_started` (a running `task_updated` patch or
|
||||
@@ -1540,6 +1517,8 @@ export type Session = {
|
||||
/** The consumer's compaction lifecycle, exposed so the PostCompact hook can
|
||||
* hand it the retained summary. */
|
||||
contextCompaction?: ContextCompactionLifecycle;
|
||||
/** Compacts the session shortly before its one-hour prompt cache expires. */
|
||||
idleCompaction?: IdleCompaction;
|
||||
/** Whether any top-level assistant text reached the client since the last
|
||||
* stretch boundary. Set as a side effect of sending in the consumer's
|
||||
* `sendUpdate`, never at an emission site; read at the terminal `result`
|
||||
@@ -2184,54 +2163,6 @@ function recoveredToolInput(prefix: string): Record<string, unknown> | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function claudeCliPath(): Promise<string> {
|
||||
if (process.env.CLAUDE_CODE_EXECUTABLE) {
|
||||
return process.env.CLAUDE_CODE_EXECUTABLE;
|
||||
}
|
||||
// The SDK's CLI is a native binary shipped as a platform-specific optional
|
||||
// dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
|
||||
// to the SDK so nested installs are found even when npm doesn't hoist.
|
||||
const { createRequire } = await import("node:module");
|
||||
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
// On linux, both glibc and musl variants may be installed side-by-side
|
||||
// (e.g. bunx hydrates every optional dep), so picking one by trial is
|
||||
// unreliable: the wrong binary segfaults at runtime instead of failing to
|
||||
// spawn. Detect the runtime libc and prefer the matching variant, falling
|
||||
// back to the other only if the preferred one isn't installed.
|
||||
const candidates =
|
||||
process.platform === "linux"
|
||||
? isMuslLibc()
|
||||
? [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
]
|
||||
: [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
]
|
||||
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return req.resolve(candidate);
|
||||
} catch {
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Claude native binary not found for ${process.platform}-${process.arch}. ` +
|
||||
`Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`,
|
||||
);
|
||||
}
|
||||
|
||||
function isMuslLibc(): boolean {
|
||||
// process.report.getReport().header.glibcVersionRuntime is populated when
|
||||
// Node is dynamically linked against glibc, and absent on musl.
|
||||
const report = process.report?.getReport() as
|
||||
{ header?: { glibcVersionRuntime?: string } } | undefined;
|
||||
return !report?.header?.glibcVersionRuntime;
|
||||
}
|
||||
|
||||
/** Returned to clients when a prompt or cancel targets a session whose SDK
|
||||
* query stream has already ended (ran to `done` or died). The stream is not
|
||||
* revivable, so the only recovery is a fresh session. */
|
||||
@@ -2468,137 +2399,6 @@ export function isReplayHiddenMetaMessage(message: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-facing surface the agent calls back into. This is the subset of ACP
|
||||
* client methods the agent actually uses, expressed as a narrow interface so
|
||||
* tests can supply lightweight mocks. In production it is backed by
|
||||
* {@link ClientConnection} over the SDK's typed `AgentContext`.
|
||||
*/
|
||||
export interface AcpClient {
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* permission request so the client can dismiss its prompt, and rejects the
|
||||
* call at once: a client that ignores the cancel never answers. */
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse>;
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* elicitation so the client can dismiss its prompt, and rejects the call
|
||||
* at once, as {@link requestPermission} does. */
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse>;
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
|
||||
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
|
||||
* exposed by `AgentApp.connect(...)` as `connection.client`. The peer handle is
|
||||
* valid for the entire connection lifetime, so it is captured once at
|
||||
* construction.
|
||||
*/
|
||||
class ClientConnection implements AcpClient {
|
||||
constructor(private readonly ctx: AgentContext) {}
|
||||
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.session.update, asSdkSessionNotification(params));
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: RequestPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.ctx.request(methods.client.session.requestPermission, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.readTextFile, params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.writeTextFile, params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.ctx.request(methods.client.elicitation.create, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.elicitation.complete, params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.ctx.notify(method, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client that the agent talks to. For an AIR client, every session update
|
||||
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
|
||||
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
|
||||
* merges only the top-level tool call fields, not the `_meta` keys. So another
|
||||
* client gets the full `_meta` on each update.
|
||||
*/
|
||||
class ChangedMetaClient implements AcpClient {
|
||||
private readonly filter = new ChangedMetaFilter();
|
||||
|
||||
constructor(
|
||||
private readonly inner: AcpClient,
|
||||
private readonly airClient: () => boolean,
|
||||
) {}
|
||||
|
||||
async sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
if (!this.airClient()) return this.inner.sessionUpdate(params);
|
||||
const update = this.filter.apply(params.update as SessionNotification["update"]);
|
||||
if (update) await this.inner.sessionUpdate({ ...params, update } as AcpSessionNotification);
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.inner.requestPermission(params, signal);
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.inner.readTextFile(params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.inner.writeTextFile(params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.inner.createElicitation(params, signal);
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.inner.completeElicitation(params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.inner.extNotification(method, params);
|
||||
}
|
||||
}
|
||||
|
||||
function raceWithAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
||||
@@ -3050,10 +2850,11 @@ export class ClaudeAcpAgent {
|
||||
};
|
||||
|
||||
if (supportsMetaTerminalAuth) {
|
||||
const agent = agentCommand();
|
||||
remoteLoginMethod._meta = {
|
||||
"terminal-auth": {
|
||||
command: process.execPath,
|
||||
args: [...process.argv.slice(1), "--cli"],
|
||||
command: agent.command,
|
||||
args: [...agent.args, "--cli"],
|
||||
label: "Claude Login",
|
||||
},
|
||||
};
|
||||
@@ -3080,18 +2881,18 @@ export class ClaudeAcpAgent {
|
||||
};
|
||||
|
||||
if (supportsMetaTerminalAuth) {
|
||||
const baseArgs = process.argv.slice(1);
|
||||
const agent = agentCommand();
|
||||
claudeLoginMethod._meta = {
|
||||
"terminal-auth": {
|
||||
command: process.execPath,
|
||||
args: [...baseArgs, "--cli", "auth", "login", "--claudeai"],
|
||||
command: agent.command,
|
||||
args: [...agent.args, "--cli", "auth", "login", "--claudeai"],
|
||||
label: "Claude Login",
|
||||
},
|
||||
};
|
||||
consoleLoginMethod._meta = {
|
||||
"terminal-auth": {
|
||||
command: process.execPath,
|
||||
args: [...baseArgs, "--cli", "auth", "login", "--console"],
|
||||
command: agent.command,
|
||||
args: [...agent.args, "--cli", "auth", "login", "--console"],
|
||||
label: "Anthropic Console Login",
|
||||
},
|
||||
};
|
||||
@@ -3949,7 +3750,7 @@ export class ClaudeAcpAgent {
|
||||
* `promptToClaude` would drop it. v2 prompts reach `startTurn` through
|
||||
* `v1PromptRequest`, which rejects these blocks itself.
|
||||
*/
|
||||
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
||||
async prompt(params: PromptRequest, origin: TurnOrigin = "client"): Promise<PromptResponse> {
|
||||
for (const block of params.prompt) {
|
||||
if (!SUPPORTED_PROMPT_CONTENT_TYPES.includes(block.type)) {
|
||||
throw RequestError.invalidParams(
|
||||
@@ -3962,7 +3763,7 @@ export class ClaudeAcpAgent {
|
||||
const outcome = new Promise<TurnOutcome>((resolve, reject) => {
|
||||
events = { inserted() {}, ended: resolve, failed: reject };
|
||||
});
|
||||
await this.startTurn(params, events);
|
||||
await this.startTurn(params, events, origin);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -3972,7 +3773,15 @@ export class ClaudeAcpAgent {
|
||||
* on, `events` reports how the turn goes (see {@link TurnEvents}), possibly
|
||||
* before this resolves.
|
||||
*/
|
||||
async startTurn(params: PromptRequest, events: TurnEvents): Promise<void> {
|
||||
async startTurn(
|
||||
params: PromptRequest,
|
||||
events: TurnEvents,
|
||||
origin: TurnOrigin = "client",
|
||||
): Promise<void> {
|
||||
// Before the first await below: a timer that fires during those awaits
|
||||
// would queue `/compact` right behind this prompt. The idle compaction's
|
||||
// own turn comes from that timer, which is already cleared.
|
||||
if (origin === "client") this.sessions[params.sessionId]?.idleCompaction?.disarm("prompt");
|
||||
const received = ++this.promptsReceived;
|
||||
while (this.providerUpdate) await this.providerUpdate;
|
||||
let session = this.sessions[params.sessionId];
|
||||
@@ -4024,10 +3833,7 @@ export class ClaudeAcpAgent {
|
||||
// around it. Refuse instead of interleaving. `foregroundWork` also covers
|
||||
// a cycle that Claude Code started on its own, such as its follow-up
|
||||
// after a background subagent finished, which is not in `turnQueue`.
|
||||
if (
|
||||
this.foregroundWork.has(params.sessionId) ||
|
||||
(session.turnQueue ?? []).some((turn) => !turn.settled)
|
||||
) {
|
||||
if (this.claudeIsWorking(params.sessionId, session)) {
|
||||
throw RequestError.invalidRequest(
|
||||
undefined,
|
||||
"Remote Control can't be changed while Claude is working. Try again when the turn ends.",
|
||||
@@ -4091,18 +3897,22 @@ export class ClaudeAcpAgent {
|
||||
const localCommand = this.localCommandMarkdown(params);
|
||||
const localCommandAbort = localCommand ? new AbortController() : undefined;
|
||||
|
||||
session.titles.onPrompt(params.prompt);
|
||||
// An idle compaction is not something the user sent: it must not title the
|
||||
// session `/compact` or spend the prediction the user is still looking at.
|
||||
if (origin === "client") {
|
||||
session.titles.onPrompt(params.prompt);
|
||||
|
||||
// The prompt the previous turn predicted is spent: the user either sent it
|
||||
// or sent something else. Clearing it here keeps a turn that predicts
|
||||
// nothing (the CLI predicts none in plan mode, after an API error, or at
|
||||
// the usage limit) from leaving the stale prediction in the composer. A
|
||||
// session that holds no prediction needs no clear, and the prompt path
|
||||
// then keeps the ordering it has without this feature.
|
||||
if (session.lastPublishedPromptSuggestion != null) {
|
||||
await this.publishPromptSuggestion(params.sessionId, null);
|
||||
} else {
|
||||
session.promptSuggestionExpected = false;
|
||||
// The prompt the previous turn predicted is spent: the user either sent it
|
||||
// or sent something else. Clearing it here keeps a turn that predicts
|
||||
// nothing (the CLI predicts none in plan mode, after an API error, or at
|
||||
// the usage limit) from leaving the stale prediction in the composer. A
|
||||
// session that holds no prediction needs no clear, and the prompt path
|
||||
// then keeps the ordering it has without this feature.
|
||||
if (session.lastPublishedPromptSuggestion != null) {
|
||||
await this.publishPromptSuggestion(params.sessionId, null);
|
||||
} else {
|
||||
session.promptSuggestionExpected = false;
|
||||
}
|
||||
}
|
||||
|
||||
// A `/remote-control` command still in flight answers into the session's
|
||||
@@ -4160,7 +3970,7 @@ export class ClaudeAcpAgent {
|
||||
(session.sentUserUuids ??= new Set()).add(promptUuid);
|
||||
session.input.push(userMessage);
|
||||
this.ensureConsumer(session, params.sessionId);
|
||||
session.titles.onPromptQueued(session, params.prompt);
|
||||
if (origin === "client") session.titles.onPromptQueued(session, params.prompt);
|
||||
// The prompt is queued, so its turn goes ahead even if the client misses
|
||||
// the goal it sets; the turn's events report the outcome.
|
||||
await this.publishGoalFromPrompt(params.sessionId, firstText, promptUuid).catch((error) =>
|
||||
@@ -4636,6 +4446,7 @@ export class ClaudeAcpAgent {
|
||||
// check: the interrupt can have the CLI finalizing the aborted cycle by the
|
||||
// time the consumer next runs, and an unmarked result would settle the turn
|
||||
// (see Turn.steeredEchoes).
|
||||
session.idleCompaction?.disarm("steer");
|
||||
(turnInFlight.steeredEchoes ??= new Set()).add(steeredUuid);
|
||||
(turnInFlight.steeredUuids ??= new Set()).add(steeredUuid);
|
||||
// A turn already held for background subagents has a recorded outcome the
|
||||
@@ -4709,6 +4520,8 @@ export class ClaudeAcpAgent {
|
||||
let lastAssistantTotalUsage: number | null = null;
|
||||
let lastAssistantUsage: UsageSnapshot | null = null;
|
||||
let lastAssistantModel: string | null = null;
|
||||
// A rate_limit_event that came before the turn had any usage to report.
|
||||
let pendingRateLimit: unknown = null;
|
||||
// When the Claude SDK classifies a turn as failed (e.g. rate limit, auth
|
||||
// problem, billing), it sets a categorical `error` field on the
|
||||
// `SDKAssistantMessage` that precedes the final `result` message. We capture
|
||||
@@ -5016,7 +4829,8 @@ export class ClaudeAcpAgent {
|
||||
};
|
||||
|
||||
// ACP's usage_update has no model field, so the model the usage belongs
|
||||
// to rides in `_meta` alongside the other `_claude/*` keys.
|
||||
// to rides in `_meta` alongside the other `_claude/*` keys, as does a
|
||||
// pending rate limit.
|
||||
const attachUsageModel = <
|
||||
T extends {
|
||||
sessionUpdate: "usage_update";
|
||||
@@ -5027,6 +4841,13 @@ export class ClaudeAcpAgent {
|
||||
>(
|
||||
update: T,
|
||||
): T => {
|
||||
if (pendingRateLimit !== null) {
|
||||
update = {
|
||||
...update,
|
||||
_meta: { "_claude/rateLimit": pendingRateLimit, ...(update._meta ?? {}) },
|
||||
};
|
||||
pendingRateLimit = null;
|
||||
}
|
||||
if (!lastAssistantModel) return update;
|
||||
return {
|
||||
...update,
|
||||
@@ -5417,16 +5238,10 @@ export class ClaudeAcpAgent {
|
||||
this.syncAwaitingUser(params.sessionId, session);
|
||||
};
|
||||
|
||||
/** Ends the display terminal of a command that went to the background, and
|
||||
* gives a tool call whose task went on in the background its final
|
||||
* status, once the task ends (see `Session.backgroundTerminals` and
|
||||
* `Session.backgroundToolCalls`). */
|
||||
const endBackgroundTask = async (taskId: string, end: BackgroundTaskEnd) => {
|
||||
const exit = takeBackgroundTerminalExit(session, params.sessionId, taskId);
|
||||
if (exit) await sendUpdate(exit);
|
||||
const toolCallEnd = takeBackgroundToolCallEnd(session, params.sessionId, taskId, end);
|
||||
if (toolCallEnd) await sendUpdate(toolCallEnd);
|
||||
};
|
||||
/** Ends the terminal and the tool call of a task that ran in the
|
||||
* background, once the task ends (see `Session.background`). */
|
||||
const endBackgroundTask = (taskId: string, end: BackgroundTaskEnd) =>
|
||||
session.background.taskEnded(taskId, end, sendUpdate);
|
||||
|
||||
/** Settles a task. Its request may still be open, and an agent without a
|
||||
* record no longer blocks the foreground work, so that is checked again. */
|
||||
@@ -6376,6 +6191,7 @@ export class ClaudeAcpAgent {
|
||||
} else if (message.compact_result === "success") {
|
||||
await compaction.finish(message.uuid, "completed");
|
||||
} else if (message.compact_result === "failed") {
|
||||
session.idleCompaction?.noteCompactionFailed(message.compact_error);
|
||||
await compaction.finish(message.uuid, "failed", {
|
||||
...(message.compact_error ? { error: message.compact_error } : {}),
|
||||
});
|
||||
@@ -6402,7 +6218,11 @@ export class ClaudeAcpAgent {
|
||||
await compaction.finish(
|
||||
message.uuid,
|
||||
"completed",
|
||||
compactMetadata ? contextCompactionMetadataFromBoundary(compactMetadata) : {},
|
||||
compactMetadata
|
||||
? contextCompactionMetadataFromBoundary(compactMetadata, {
|
||||
idleCompaction: session.idleCompaction?.isCompacting,
|
||||
})
|
||||
: {},
|
||||
true,
|
||||
);
|
||||
const usedTokens = compactMetadata?.post_tokens ?? 0;
|
||||
@@ -6454,8 +6274,6 @@ export class ClaudeAcpAgent {
|
||||
!firstUnsettledQueuedTurn()
|
||||
) {
|
||||
await asyncTasks.autonomousTurnStarted();
|
||||
} else if (message.state === "idle" && asyncTasks.autonomousTurnOpen) {
|
||||
await asyncTasks.autonomousTurnEnded("completed");
|
||||
}
|
||||
if (
|
||||
message.state === "running" &&
|
||||
@@ -6487,6 +6305,9 @@ export class ClaudeAcpAgent {
|
||||
// here. An owed idle belongs to an earlier result: it can lag
|
||||
// past the cycle's first output.
|
||||
if (session.owedTrailingIdles === 0) {
|
||||
if (asyncTasks.autonomousTurnOpen) {
|
||||
await asyncTasks.autonomousTurnEnded("completed");
|
||||
}
|
||||
await closeCycleToolCalls();
|
||||
this.endAutonomousCycle(params.sessionId, {
|
||||
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
|
||||
@@ -6620,6 +6441,10 @@ export class ClaudeAcpAgent {
|
||||
// generatable; see SessionTitles.onTurnEnd.
|
||||
await session.titles.onTurnEnd(session);
|
||||
}
|
||||
// After the settle block above: steered turns, held turns and
|
||||
// autonomous cycles count as work until it settles them, and no
|
||||
// further idle arrives to arm the timer.
|
||||
if (message.state === "idle") session.idleCompaction?.onIdle();
|
||||
break;
|
||||
}
|
||||
case "memory_recall": {
|
||||
@@ -6664,6 +6489,16 @@ export class ClaudeAcpAgent {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "session_writes_lost": {
|
||||
// A full disk (ENOSPC) or used-up quota (EDQUOT) kept the CLI
|
||||
// from saving transcript lines or ~/.claude.json. The CLI sends
|
||||
// it as it exits, after the last result, so no turn is open to
|
||||
// carry a session/update: log it like `mirror_error`.
|
||||
this.logger.error(
|
||||
`Session ${message.session_id}: Claude Code could not save ${message.lost.join(" and ")} (${message.error_code}); resuming may miss part of the conversation`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "permission_denied": {
|
||||
unregisterHookCallback(message.tool_use_id);
|
||||
// A tool call was auto-denied (by a rule, the classifier,
|
||||
@@ -6745,10 +6580,11 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
}
|
||||
const severity = message.level === "warning" ? "warning" : "info";
|
||||
const transcriptContent = collapseHookSpeaker(message.content);
|
||||
const transcriptText =
|
||||
message.level === "info"
|
||||
? message.content
|
||||
: `**${sentenceCase(message.level)}:** ${message.content}`;
|
||||
? transcriptContent
|
||||
: `**${sentenceCase(message.level)}:** ${transcriptContent}`;
|
||||
await sendUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: noticeOrTranscriptUpdate(
|
||||
@@ -6931,23 +6767,13 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
settleLiveTask(message.task_id);
|
||||
} else if (message.patch.status === "paused") {
|
||||
const pause = backgroundToolCallPause(
|
||||
session,
|
||||
params.sessionId,
|
||||
message.task_id,
|
||||
true,
|
||||
);
|
||||
const pause = session.background.taskPaused(message.task_id, true);
|
||||
if (pause) await sendUpdate(pause);
|
||||
} else if (
|
||||
message.patch.status === "running" ||
|
||||
message.patch.status === "pending"
|
||||
) {
|
||||
const resume = backgroundToolCallPause(
|
||||
session,
|
||||
params.sessionId,
|
||||
message.task_id,
|
||||
false,
|
||||
);
|
||||
const resume = session.background.taskPaused(message.task_id, false);
|
||||
if (resume) await sendUpdate(resume);
|
||||
// The SDK can resume a finished subagent under the same
|
||||
// agent id without a new task_started.
|
||||
@@ -7118,13 +6944,7 @@ export class ClaudeAcpAgent {
|
||||
break;
|
||||
case "background_tasks_changed": {
|
||||
await asyncTasks.backgroundTasksChanged(message.tasks);
|
||||
if (session.reportsBackground) {
|
||||
const level = backgroundLevel(message.tasks);
|
||||
for (const taskId of droppedTasks(session, level)) {
|
||||
await endBackgroundTask(taskId, undefined);
|
||||
}
|
||||
session.backgroundLevel = level;
|
||||
}
|
||||
await session.background.levelChanged(message.tasks, sendUpdate);
|
||||
// A level signal: the full live background-task set on every
|
||||
// membership change, with REPLACE semantics. Used only to
|
||||
// reconcile `liveBackgroundTasks` — dropping (or, for
|
||||
@@ -7984,6 +7804,15 @@ export class ClaudeAcpAgent {
|
||||
const model = message.event.message.model;
|
||||
if (model && model !== "<synthetic>") {
|
||||
lastAssistantModel = model;
|
||||
// `message_start` arrives with the first streamed token, close to
|
||||
// when the provider wrote the cache. The `assistant` message
|
||||
// below covers a response that was not streamed (the CLI's
|
||||
// non-streaming fallback); its shared id keeps the per-block
|
||||
// messages from moving the time.
|
||||
session.idleCompaction?.noteRequest(
|
||||
message.event.message.usage,
|
||||
message.event.message.id || undefined,
|
||||
);
|
||||
// Only upgrade from the heuristic default — once we have an
|
||||
// authoritative window (cache-seeded at session creation or
|
||||
// on a model switch, read from the resumed session on
|
||||
@@ -8355,6 +8184,10 @@ export class ClaudeAcpAgent {
|
||||
lastAssistantUsage = snapshotFromUsage(message.message.usage);
|
||||
lastAssistantTotalUsage = totalTokens(lastAssistantUsage);
|
||||
session.contextUsedTokens = lastAssistantTotalUsage;
|
||||
session.idleCompaction?.noteRequest(
|
||||
message.message.usage,
|
||||
message.message.id || undefined,
|
||||
);
|
||||
}
|
||||
lastAssistantWasUsageLimit = isSyntheticUsageLimitMessage(message.message);
|
||||
if (message.error || lastAssistantWasUsageLimit) {
|
||||
@@ -8527,6 +8360,7 @@ export class ClaudeAcpAgent {
|
||||
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
||||
let backgroundedToolCalls: ReadonlySet<string> = new Set();
|
||||
let runningTerminal: string | undefined;
|
||||
let runningTerminalTask: string | undefined;
|
||||
let runningToolCalls: ReadonlySet<string> = new Set();
|
||||
if (message.type === "user") {
|
||||
rememberResolvedToolNames(session, content);
|
||||
@@ -8540,6 +8374,18 @@ export class ClaudeAcpAgent {
|
||||
content,
|
||||
toolUseResult,
|
||||
session.toolUseCache,
|
||||
(toolUseId) => {
|
||||
for (const [taskId, task] of session.liveBackgroundTasks) {
|
||||
if (
|
||||
task.parentToolUseId === toolUseId &&
|
||||
task.taskType === "local_bash" &&
|
||||
task.backgrounded
|
||||
) {
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
if (backgroundBashTask) await asyncTasks.taskBackgrounded(backgroundBashTask);
|
||||
// The command's terminal runs until its task ends, unless the
|
||||
@@ -8553,14 +8399,18 @@ export class ClaudeAcpAgent {
|
||||
session.liveBackgroundTasks.has(backgroundBashTask.taskId)
|
||||
) {
|
||||
runningTerminal = terminalId;
|
||||
(session.backgroundTerminals ??= new Map()).set(backgroundBashTask.taskId, {
|
||||
session.background.terminalStarted(backgroundBashTask.taskId, {
|
||||
toolCallId: terminalId,
|
||||
...(message.parent_tool_use_id
|
||||
? { parentToolUseId: message.parent_tool_use_id }
|
||||
: {}),
|
||||
...(typeof backgroundBashTask.outputFilePath === "string"
|
||||
? { outputFilePath: backgroundBashTask.outputFilePath }
|
||||
: {}),
|
||||
});
|
||||
runningTerminalTask = backgroundBashTask.taskId;
|
||||
}
|
||||
runningToolCalls = registerBackgroundToolCalls(
|
||||
runningToolCalls = session.background.toolResults(
|
||||
session,
|
||||
content,
|
||||
toolUseResult,
|
||||
@@ -8642,6 +8492,18 @@ export class ClaudeAcpAgent {
|
||||
),
|
||||
);
|
||||
}
|
||||
// After the result's own updates: the command's output replaces
|
||||
// the placeholder that they gave its terminal.
|
||||
if (runningTerminalTask) {
|
||||
session.background.followOutput(runningTerminalTask, (notification) =>
|
||||
sendUpdate(notification).catch((error) => {
|
||||
this.logger.error(
|
||||
`Session ${params.sessionId}: failed to send background command output:`,
|
||||
error,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tool_progress": {
|
||||
@@ -8697,7 +8559,12 @@ export class ClaudeAcpAgent {
|
||||
break;
|
||||
}
|
||||
case "rate_limit_event": {
|
||||
if (lastAssistantTotalUsage !== null) {
|
||||
session.idleCompaction?.noteRateLimit(message.rate_limit_info);
|
||||
// With partial messages on, the SDK sends this before the turn's
|
||||
// first assistant frame, while there is no usage yet to send it with.
|
||||
if (lastAssistantTotalUsage === null) {
|
||||
pendingRateLimit = message.rate_limit_info;
|
||||
} else {
|
||||
await sendUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: attachUsageModel({
|
||||
@@ -8731,6 +8598,8 @@ export class ClaudeAcpAgent {
|
||||
// The follow-up prompt was predicted from the transcript the reset
|
||||
// dropped, so it no longer follows from anything.
|
||||
await this.publishPromptSuggestion(params.sessionId, null);
|
||||
// The recorded request and TTL describe the dropped conversation.
|
||||
session.idleCompaction?.reset();
|
||||
break;
|
||||
}
|
||||
case "tool_use_summary":
|
||||
@@ -8967,6 +8836,7 @@ export class ClaudeAcpAgent {
|
||||
return;
|
||||
}
|
||||
session.cancelled = true;
|
||||
session.idleCompaction?.disarm("cancel");
|
||||
// Only prompts received before this cancel are cancelled. A prompt that
|
||||
// arrives while the awaits below run is queued and runs after the cancel
|
||||
// (ACP v2: a `session/prompt` received after `session/cancel` is not
|
||||
@@ -9358,7 +9228,7 @@ export class ClaudeAcpAgent {
|
||||
// Its list row now shows what the registry and the transcript tell.
|
||||
this.sessionIndex.onOwnSessionChanged(sessionId);
|
||||
// Its background tasks end with the query's process.
|
||||
for (const exit of endBackgroundWork(session, sessionId)) {
|
||||
for (const exit of session.background.queryClosed()) {
|
||||
this.client.sessionUpdate(exit).catch((error) => {
|
||||
this.logger.error(`Session ${sessionId}: failed to end a background terminal:`, error);
|
||||
});
|
||||
@@ -9367,6 +9237,7 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
session.consumer = undefined;
|
||||
session.contextCompaction = undefined;
|
||||
session.idleCompaction?.dispose();
|
||||
session.settingsManager.dispose();
|
||||
session.input.end();
|
||||
session.query.close();
|
||||
@@ -9469,7 +9340,7 @@ export class ClaudeAcpAgent {
|
||||
/** Stops a command running in the background (v2 `terminal/cancel`, see
|
||||
* {@link cancelBackgroundWork}). */
|
||||
cancelBackgroundTerminal(params: { sessionId: string; terminalId: string }): Promise<void> {
|
||||
return cancelBackgroundWork(this.sessions, params.sessionId, {
|
||||
return cancelBackgroundWork(Object.values(this.sessions), params.sessionId, {
|
||||
terminalId: params.terminalId,
|
||||
});
|
||||
}
|
||||
@@ -9477,7 +9348,7 @@ export class ClaudeAcpAgent {
|
||||
/** Stops the task of a tool call running in the background (v2
|
||||
* `tool_call/cancel`, see {@link cancelBackgroundWork}). */
|
||||
cancelBackgroundToolCall(params: { sessionId: string; toolCallId: string }): Promise<void> {
|
||||
return cancelBackgroundWork(this.sessions, params.sessionId, {
|
||||
return cancelBackgroundWork(Object.values(this.sessions), params.sessionId, {
|
||||
toolCallId: params.toolCallId,
|
||||
});
|
||||
}
|
||||
@@ -9492,7 +9363,7 @@ export class ClaudeAcpAgent {
|
||||
if (!session) return;
|
||||
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
||||
try {
|
||||
for (const update of backgroundToolCallStates(session, sessionId, toolCallIds, renderer)) {
|
||||
for (const update of session.background.toolCallStates(toolCallIds, renderer, session.cwd)) {
|
||||
await this.client.sessionUpdate(update);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -10042,9 +9913,10 @@ export class ClaudeAcpAgent {
|
||||
// The entity is keyed by the summary message's uuid, which differs from
|
||||
// the live id (the `compacting` status uuid) — replay is terminal-first
|
||||
// by the RFD, and no client merges a replay into a live entity store.
|
||||
// A subagent's own compaction summary is not the root session's, and a
|
||||
// summary whose framing is unrecognized stays visible as transcript
|
||||
// text rather than vanishing behind a summary-less entity.
|
||||
// A subagent's own compaction summary is not the root session's. A summary
|
||||
// whose framing is unrecognized is still this compaction, without summary
|
||||
// text: isCompactSummary says what it is, and the framing is Claude Code's
|
||||
// to change.
|
||||
if (
|
||||
replayCompactionUpdates &&
|
||||
isCompactSummaryMessage(message) &&
|
||||
@@ -10060,10 +9932,9 @@ export class ClaudeAcpAgent {
|
||||
airClient: this.toolCallCapabilities.air.client,
|
||||
},
|
||||
);
|
||||
if (replayCompaction.recordSummary(assistantMessageText(message.message))) {
|
||||
await replayCompaction.finish(message.uuid, "completed");
|
||||
return;
|
||||
}
|
||||
replayCompaction.recordSummary(assistantMessageText(message.message));
|
||||
await replayCompaction.finish(message.uuid, "completed");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const notification of toAcpNotifications(
|
||||
@@ -10283,6 +10154,15 @@ export class ClaudeAcpAgent {
|
||||
else if (!withdrawnByCancel) work.events.running();
|
||||
}
|
||||
|
||||
/** Whether Claude is working in the session `sessionId`: an unsettled turn in
|
||||
* its queue, or foreground work that no turn owns, such as a cycle that
|
||||
* Claude Code started on its own. */
|
||||
private claudeIsWorking(sessionId: string, session: Session): boolean {
|
||||
return (
|
||||
this.foregroundWork.has(sessionId) || (session.turnQueue ?? []).some((turn) => !turn.settled)
|
||||
);
|
||||
}
|
||||
|
||||
/** Sends the current title and `updatedAt` of the session `sessionId` as a
|
||||
* `session_info_update` (see {@link SessionTitles.publishCurrent}). A
|
||||
* failure is logged: the info is optional, so it does not fail a resume. */
|
||||
@@ -11473,6 +11353,7 @@ export class ClaudeAcpAgent {
|
||||
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
||||
const modelChanged = session.models.currentModelId !== value;
|
||||
if (modelChanged) {
|
||||
session.idleCompaction?.disarm("model change");
|
||||
// Seed the new model's context window WITHOUT awaited IPC on the
|
||||
// switch path: cached authoritative value if we've already learned it
|
||||
// (from a prior turn's `result.modelUsage`), else the text heuristic,
|
||||
@@ -12079,15 +11960,18 @@ export class ClaudeAcpAgent {
|
||||
settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
|
||||
sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
|
||||
|
||||
const initialPermissionMode = resolveInitialPermissionMode(
|
||||
{
|
||||
explicit: creationOpts.permissionMode,
|
||||
resumed: resumedPermissionMode,
|
||||
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
|
||||
},
|
||||
this.logger,
|
||||
allowBypass,
|
||||
);
|
||||
const allowAuto = !autoModeDisabled(settingsManager.getSettings());
|
||||
|
||||
const { mode: initialPermissionMode, builtInDefault: builtInPermissionMode } =
|
||||
resolveInitialPermissionMode(
|
||||
{
|
||||
explicit: creationOpts.permissionMode,
|
||||
resumed: resumedPermissionMode,
|
||||
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
|
||||
},
|
||||
this.logger,
|
||||
{ allowBypass, allowAuto },
|
||||
);
|
||||
|
||||
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
||||
const forwardSubagentText = this.forwardsSubagentText(params._meta);
|
||||
@@ -12268,7 +12152,7 @@ export class ClaudeAcpAgent {
|
||||
supportedDialogKinds: [REFUSAL_FALLBACK_DIALOG_KIND],
|
||||
}
|
||||
: {}),
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),
|
||||
pathToClaudeCodeExecutable: await claudeCliPath(),
|
||||
extraArgs: {
|
||||
...userProvidedOptions?.extraArgs,
|
||||
"replay-user-messages": "",
|
||||
@@ -12563,9 +12447,11 @@ export class ClaudeAcpAgent {
|
||||
const { modes, autoModeFallbackWarningPending } = await this.sessionModes.initialize({
|
||||
query: q,
|
||||
requestedMode: initialPermissionMode,
|
||||
builtInDefault: builtInPermissionMode,
|
||||
currentModelInfo,
|
||||
currentModelId: models.currentModelId,
|
||||
allowBypass,
|
||||
allowAuto,
|
||||
});
|
||||
timing.phase("modes");
|
||||
|
||||
@@ -12726,7 +12612,10 @@ export class ClaudeAcpAgent {
|
||||
emittedToolCalls: new Set(),
|
||||
eagerToolCallSessions: new Map(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
reportsBackground: this.backgroundWork.enabled && options.perTaskStopAffordance === true,
|
||||
background: new SessionBackgroundWork(
|
||||
sessionId,
|
||||
this.backgroundWork.enabled && options.perTaskStopAffordance === true,
|
||||
),
|
||||
stopsWorkflowsOnCancel:
|
||||
options.perTaskStopAffordance === true &&
|
||||
!this.backgroundWork.enabled &&
|
||||
@@ -12746,6 +12635,27 @@ export class ClaudeAcpAgent {
|
||||
)?.kind,
|
||||
fileChangeReporter,
|
||||
};
|
||||
const registered = this.sessions[sessionId];
|
||||
registered.idleCompaction = new IdleCompaction(
|
||||
{
|
||||
// `openUserInputs` also covers permission requests: `requestPermission`
|
||||
// is wrapped in `withPendingUserInput`.
|
||||
isBusy: () =>
|
||||
this.claudeIsWorking(sessionId, registered) ||
|
||||
(registered.openUserInputs?.size ?? 0) > 0,
|
||||
contextTokens: () => registered.contextUsedTokens,
|
||||
settings: () => registered.settingsManager.getSettings(),
|
||||
compact: () =>
|
||||
this.prompt(
|
||||
{ sessionId, prompt: [{ type: "text", text: "/compact" }] },
|
||||
"idleCompaction",
|
||||
),
|
||||
// The logger has no debug level.
|
||||
log: (message) => this.logger.log(message),
|
||||
},
|
||||
// The CLI's own environment: process env, the client's and the provider's.
|
||||
{ env },
|
||||
);
|
||||
timing.phase("register");
|
||||
this.backgroundWork.sync(
|
||||
sessionId,
|
||||
|
||||
158
src/acp-client.ts
Normal file
158
src/acp-client.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
AgentContext,
|
||||
CompleteElicitationNotification,
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
methods,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionNotification,
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { AcpSessionNotification, asSdkSessionNotification } from "./acp-subagents.js";
|
||||
import { type AcpPermissionRequest } from "./permissions/presentation.js";
|
||||
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
||||
|
||||
/**
|
||||
* Logger interface for customizing logging output
|
||||
*/
|
||||
export interface Logger {
|
||||
log: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
|
||||
warn?: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-facing surface the agent calls back into. This is the subset of ACP
|
||||
* client methods the agent actually uses, expressed as a narrow interface so
|
||||
* tests can supply lightweight mocks. In production it is backed by
|
||||
* {@link ClientConnection} over the SDK's typed `AgentContext`.
|
||||
*/
|
||||
export interface AcpClient {
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* permission request so the client can dismiss its prompt, and rejects the
|
||||
* call at once: a client that ignores the cancel never answers. */
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse>;
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
|
||||
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
|
||||
* elicitation so the client can dismiss its prompt, and rejects the call
|
||||
* at once, as {@link requestPermission} does. */
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse>;
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
|
||||
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
|
||||
* exposed by `AgentApp.connect(...)` as `connection.client`. The peer handle is
|
||||
* valid for the entire connection lifetime, so it is captured once at
|
||||
* construction.
|
||||
*/
|
||||
export class ClientConnection implements AcpClient {
|
||||
constructor(private readonly ctx: AgentContext) {}
|
||||
|
||||
sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.session.update, asSdkSessionNotification(params));
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: RequestPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.ctx.request(methods.client.session.requestPermission, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.readTextFile, params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.ctx.request(methods.client.fs.writeTextFile, params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.ctx.request(methods.client.elicitation.create, params, {
|
||||
cancellationSignal: signal,
|
||||
stopWaitingOnCancel: true,
|
||||
});
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.ctx.notify(methods.client.elicitation.complete, params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.ctx.notify(method, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client that the agent talks to. For an AIR client, every session update
|
||||
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
|
||||
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
|
||||
* merges only the top-level tool call fields, not the `_meta` keys. So another
|
||||
* client gets the full `_meta` on each update.
|
||||
*/
|
||||
export class ChangedMetaClient implements AcpClient {
|
||||
private readonly filter = new ChangedMetaFilter();
|
||||
|
||||
constructor(
|
||||
private readonly inner: AcpClient,
|
||||
private readonly airClient: () => boolean,
|
||||
) {}
|
||||
|
||||
async sessionUpdate(params: AcpSessionNotification): Promise<void> {
|
||||
if (!this.airClient()) return this.inner.sessionUpdate(params);
|
||||
const update = this.filter.apply(params.update as SessionNotification["update"]);
|
||||
if (update) await this.inner.sessionUpdate({ ...params, update } as AcpSessionNotification);
|
||||
}
|
||||
|
||||
requestPermission(
|
||||
params: AcpPermissionRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
return this.inner.requestPermission(params, signal);
|
||||
}
|
||||
|
||||
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
return this.inner.readTextFile(params);
|
||||
}
|
||||
|
||||
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
return this.inner.writeTextFile(params);
|
||||
}
|
||||
|
||||
createElicitation(
|
||||
params: CreateElicitationRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CreateElicitationResponse> {
|
||||
return this.inner.createElicitation(params, signal);
|
||||
}
|
||||
|
||||
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
|
||||
return this.inner.completeElicitation(params);
|
||||
}
|
||||
|
||||
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
|
||||
return this.inner.extNotification(method, params);
|
||||
}
|
||||
}
|
||||
19
src/agent-command.ts
Normal file
19
src/agent-command.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* The command that starts this agent again with the flags it was started with,
|
||||
* for a client to run in a terminal (e.g. `terminal-auth` login).
|
||||
*
|
||||
* Under Node, `process.argv` is `[node, script, ...flags]`, so the command is
|
||||
* `node script ...flags`. In a `bun build --compile` executable it is
|
||||
* `["bun", "/$bunfs/root/<name>", ...flags]` (`B:\~BUN\root\<name>` on
|
||||
* Windows): `process.execPath` is the executable itself and `argv[1]` names a
|
||||
* file inside it, which must not be passed back as an argument.
|
||||
*/
|
||||
export function agentCommand(argv: readonly string[] = process.argv): {
|
||||
command: string;
|
||||
args: string[];
|
||||
} {
|
||||
const [, script, ...flags] = argv;
|
||||
if (script === undefined) return { command: process.execPath, args: [] };
|
||||
const compiled = script.includes("$bunfs") || script.includes("~BUN");
|
||||
return { command: process.execPath, args: compiled ? flags : [script, ...flags] };
|
||||
}
|
||||
@@ -863,18 +863,29 @@ function recordPublished(task: AsyncTask, fields: Record<string, unknown>): void
|
||||
* Recovers a background Bash task from the structured `backgroundTaskId` of
|
||||
* its tool result. The text of a tool result never gives a task id: a
|
||||
* foreground command can print any text.
|
||||
*
|
||||
* Claude Code sends no `tool_use_result` with a subagent's tool results. For
|
||||
* those, `startedTaskOf` gives the background `local_bash` task whose
|
||||
* `task_started` named the Bash call, which comes before the result.
|
||||
*/
|
||||
export function backgroundBashTaskFromToolResult(
|
||||
content: unknown,
|
||||
toolUseResult: unknown,
|
||||
toolUses: Record<string, { name: string; input: unknown }>,
|
||||
startedTaskOf?: (toolUseId: string) => string | undefined,
|
||||
): (AsyncTaskStarted & { taskId: string; toolCallId: string }) | undefined {
|
||||
if (!Array.isArray(content)) return undefined;
|
||||
const toolResults = content.flatMap(toolResultBlock);
|
||||
const bashResults = toolResults.filter((result) => toolUses[result.toolUseId]?.name === "Bash");
|
||||
if (bashResults.length === 0) return undefined;
|
||||
|
||||
const backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
|
||||
let backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
|
||||
if (backgroundResults.length === 0 && startedTaskOf) {
|
||||
backgroundResults = bashResults.flatMap((result) => {
|
||||
const taskId = result.isError ? undefined : startedTaskOf(result.toolUseId);
|
||||
return taskId ? [{ taskId, toolUseId: result.toolUseId }] : [];
|
||||
});
|
||||
}
|
||||
const distinctTaskIds = new Set(backgroundResults.map((result) => result.taskId));
|
||||
if (distinctTaskIds.size !== 1) return undefined;
|
||||
const taskId = [...distinctTaskIds][0];
|
||||
|
||||
@@ -210,6 +210,15 @@ export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus
|
||||
if (!externalBackend && !status.loggedIn && !status.apiKeySource) {
|
||||
return notLoggedInAuthStatus();
|
||||
}
|
||||
if (
|
||||
status.loggedIn &&
|
||||
status.authMethod === "oauth_token" &&
|
||||
status.apiProvider === "firstParty" &&
|
||||
!status.apiKeySource &&
|
||||
!status.subscriptionType
|
||||
) {
|
||||
return { kind: "account", label: "Claude subscription", detail: "OAuth token" };
|
||||
}
|
||||
return mapAuthFields({
|
||||
apiProvider: status.apiProvider,
|
||||
subscriptionType: status.subscriptionType,
|
||||
|
||||
131
src/background-output.ts
Normal file
131
src/background-output.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* The output of a command that went to the background, read from the file
|
||||
* that Claude Code writes it to. Claude Code streams no output of such a
|
||||
* command, so its display terminal would show only the placeholder of the
|
||||
* Bash result ("Command running in background with ID …") until the command
|
||||
* exits. ACP v2 asks an Agent to keep a listed terminal's output current
|
||||
* whenever it can read it (the v2 prompt lifecycle, "Background work").
|
||||
*
|
||||
* The file is polled: watching a file is not reliable on every platform, and
|
||||
* a missing file only means that nothing was written yet.
|
||||
*/
|
||||
import { open } from "node:fs/promises";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
|
||||
/** How often a running command's output file is read. */
|
||||
export const BACKGROUND_OUTPUT_POLL_MS = 500;
|
||||
|
||||
/** The most bytes one read takes, so a burst of output does not block the loop. */
|
||||
export const BACKGROUND_OUTPUT_READ_BYTES = 256 * 1024;
|
||||
|
||||
/**
|
||||
* The most unread bytes that are still sent. A command that writes faster
|
||||
* than the polls read falls behind. Past this, the reads skip to the file's
|
||||
* last bytes, as a snapshot: a terminal shows the latest output, and a
|
||||
* client keeps a bounded amount of it anyway.
|
||||
*/
|
||||
export const BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES = 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Text that the file adds. A snapshot replaces the terminal's output so far:
|
||||
* the first one replaces the placeholder of the Bash result, and a later one
|
||||
* follows a skip or a file that shrank. Other text appends to it.
|
||||
*/
|
||||
export type BackgroundOutput = { text: string; replaces: boolean };
|
||||
|
||||
/**
|
||||
* Follows the output file `path` of one background command and hands each
|
||||
* piece of new text to `publish`, in order. Reads never overlap. A read that
|
||||
* fails, for example because the file does not exist yet, is tried again at
|
||||
* the next poll. `publish` must not reject: the next read waits for it.
|
||||
*/
|
||||
export class BackgroundOutputTail {
|
||||
private offset = 0;
|
||||
private decoder = new StringDecoder("utf8");
|
||||
/** Whether the next text replaces the terminal's output. */
|
||||
private replaces = true;
|
||||
private stopped = false;
|
||||
private reading: Promise<void> = Promise.resolve();
|
||||
private readonly timer: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor(
|
||||
private readonly path: string,
|
||||
private readonly publish: (output: BackgroundOutput) => Promise<void>,
|
||||
) {
|
||||
this.timer = setInterval(() => void this.read(false), BACKGROUND_OUTPUT_POLL_MS);
|
||||
// A command that runs on must not keep the agent process alive.
|
||||
this.timer.unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the rest of the file and stops. The command ended, so what the file
|
||||
* holds is its whole output, and it goes out before the terminal's exit.
|
||||
*/
|
||||
async finish(): Promise<void> {
|
||||
clearInterval(this.timer);
|
||||
await this.read(true);
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/** Stops without reading more: the session ended. */
|
||||
stop(): void {
|
||||
clearInterval(this.timer);
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads what the file adds, one read of at most
|
||||
* {@link BACKGROUND_OUTPUT_READ_BYTES} at a time, and publishes each read's
|
||||
* text before the next. `toEnd` reads until the end of the file, and ends a
|
||||
* character that the command cut off with a replacement character.
|
||||
*/
|
||||
private read(toEnd: boolean): Promise<void> {
|
||||
this.reading = this.reading.then(async () => {
|
||||
if (this.stopped) return;
|
||||
let file;
|
||||
try {
|
||||
file = await open(this.path, "r");
|
||||
} catch {
|
||||
// Nothing written yet, or the file is gone: try again at the next
|
||||
// poll. At the end, the terminal keeps the output it has.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { size } = await file.stat();
|
||||
// A file that shrank is a new one: its output starts over. Past the
|
||||
// backlog, the reads skip to the file's last bytes.
|
||||
const start = size < this.offset ? 0 : this.offset;
|
||||
const skipTo = Math.max(start, size - BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES);
|
||||
if (skipTo !== this.offset) {
|
||||
this.offset = skipTo;
|
||||
this.decoder = new StringDecoder("utf8");
|
||||
this.replaces = true;
|
||||
}
|
||||
const buffer = Buffer.alloc(BACKGROUND_OUTPUT_READ_BYTES);
|
||||
let bytesRead: number;
|
||||
do {
|
||||
({ bytesRead } = await file.read(buffer, 0, buffer.length, this.offset));
|
||||
this.offset += bytesRead;
|
||||
const text = this.decoder.write(buffer.subarray(0, bytesRead));
|
||||
if (text !== "") await this.send(text);
|
||||
} while (toEnd && bytesRead > 0);
|
||||
if (toEnd) {
|
||||
const rest = this.decoder.end();
|
||||
// A command that wrote nothing still replaces the placeholder.
|
||||
if (rest !== "" || this.replaces) await this.send(rest);
|
||||
}
|
||||
} catch {
|
||||
// The read failed: try again at the next poll.
|
||||
} finally {
|
||||
await file.close();
|
||||
}
|
||||
});
|
||||
return this.reading;
|
||||
}
|
||||
|
||||
private async send(text: string): Promise<void> {
|
||||
const replaces = this.replaces;
|
||||
this.replaces = false;
|
||||
await this.publish({ text, replaces });
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
* "Background work").
|
||||
*
|
||||
* The lists are built from Claude Code's live background task set
|
||||
* (`background_tasks_changed`), without the tasks it marks `ambient`, which
|
||||
* are housekeeping that it started on its own. The root session and each
|
||||
* (`background_tasks_changed`). Claude Code marks `ambient` its own
|
||||
* housekeeping and every live-update watcher; those are left out, except a
|
||||
* task that a tool call started, such as a `Monitor` call's. The root session and each
|
||||
* announced subagent session (the subagents RFD form) has its own list. Each
|
||||
* task is listed once, in the list of the session that runs it:
|
||||
*
|
||||
@@ -30,6 +31,7 @@ import { RequestError } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { AcpSessionNotification } from "./acp-subagents.js";
|
||||
import type { NativeSubagent } from "./native-subagents.js";
|
||||
import { type AcpToolCallRenderer, V2_CANCELLED, V2_PAUSED } from "./tool-calls/renderer.js";
|
||||
import { type BackgroundOutput, BackgroundOutputTail } from "./background-output.js";
|
||||
|
||||
/** An item of a `background` list. */
|
||||
export type BackgroundItem =
|
||||
@@ -38,6 +40,18 @@ export type BackgroundItem =
|
||||
| { type: "subagent"; sessionId: string }
|
||||
| { type: "_claude/task"; taskId: string; description: string };
|
||||
|
||||
/** The display terminal of a command that went to the background (see `Session.backgroundTerminals`). */
|
||||
export interface BackgroundTerminal {
|
||||
/** The Bash tool call, whose id names the terminal. */
|
||||
toolCallId: string;
|
||||
/** The subagent tool call that ran the command, if any. */
|
||||
parentToolUseId?: string;
|
||||
/** The file that Claude Code writes the command's output to, from the Bash result. */
|
||||
outputFilePath?: string;
|
||||
/** Reads that file into the terminal while the command runs. */
|
||||
output?: BackgroundOutputTail;
|
||||
}
|
||||
|
||||
/** A tool call whose task runs on in the background (see `Session.backgroundToolCalls`). */
|
||||
export interface BackgroundToolCall {
|
||||
toolCallId: string;
|
||||
@@ -78,223 +92,452 @@ export interface BackgroundSink {
|
||||
forget(sessionId: string): void;
|
||||
}
|
||||
|
||||
/** The state of a session that its lists are built from. */
|
||||
export interface BackgroundWorkSession {
|
||||
reportsBackground?: boolean;
|
||||
backgroundLevel?: BackgroundLevel;
|
||||
backgroundTerminals?: Map<string, { toolCallId: string; parentToolUseId?: string }>;
|
||||
backgroundToolCalls?: Map<string, BackgroundToolCall>;
|
||||
/**
|
||||
* The state of a session that other modules own, and that the background
|
||||
* work reads: Claude Code's tasks as the consumer tracks them, the subagent
|
||||
* sessions, and the query that stops a task.
|
||||
*/
|
||||
export interface BackgroundOwner {
|
||||
liveBackgroundTasks: Map<string, { parentToolUseId?: string; isSubagent: boolean }>;
|
||||
nativeSubagentsByTaskId?: Map<string, NativeSubagent>;
|
||||
liveBackgroundTasks?: Map<string, { parentToolUseId?: string; isSubagent: boolean }>;
|
||||
/** The tool uses whose result has not come yet, by id. */
|
||||
toolUseCache?: Record<string, unknown>;
|
||||
query: { stopTask(taskId: string): Promise<void> };
|
||||
}
|
||||
|
||||
/** A task of a `background_tasks_changed` payload. */
|
||||
export type LevelTask = {
|
||||
task_id: string;
|
||||
task_type: string;
|
||||
description: string;
|
||||
parent_task_id?: string;
|
||||
ambient?: boolean;
|
||||
};
|
||||
|
||||
/** The most returned tool calls that {@link SessionBackgroundWork} remembers. */
|
||||
const MAX_RETURNED_CALLS = 1024;
|
||||
|
||||
/** The tools whose structured result names the task that the call started. */
|
||||
const TASK_TOOLS = new Set(["Monitor", "Workflow"]);
|
||||
|
||||
/**
|
||||
* Registers each tool call of a tool result message whose task goes on in
|
||||
* the background: a `Monitor` or `Workflow` call, whose structured result
|
||||
* names its task, or a call that a live task names as its tool use, such as
|
||||
* a backgrounded MCP call. The task must be in the level: one that ended
|
||||
* already, or that Claude Code marks `ambient`, leaves its call as it is.
|
||||
* Returns the ids of the calls, whose result then reports `in_progress` (see
|
||||
* `withRunningToolCall`).
|
||||
* The background work of one root session, and of its subagent sessions: the
|
||||
* terminals and tool calls of the tasks that run in the background, and what
|
||||
* the client is told of them.
|
||||
*
|
||||
* Terminals are kept for every client that renders them, as before the
|
||||
* background lists. The rest is kept only when `reported`.
|
||||
*/
|
||||
export function registerBackgroundToolCalls(
|
||||
session: BackgroundWorkSession,
|
||||
content: unknown,
|
||||
toolUseResult: unknown,
|
||||
toolUses: Record<string, { name: string; input: unknown }>,
|
||||
parentToolUseId: string | null,
|
||||
): Set<string> {
|
||||
const registered = new Set<string>();
|
||||
if (!session.reportsBackground || !Array.isArray(content)) return registered;
|
||||
const results = content.filter(
|
||||
(block): block is { type: "tool_result"; tool_use_id: string; is_error?: boolean } =>
|
||||
typeof block === "object" &&
|
||||
block !== null &&
|
||||
block.type === "tool_result" &&
|
||||
typeof block.tool_use_id === "string" &&
|
||||
block.is_error !== true,
|
||||
);
|
||||
const structuredTaskId =
|
||||
results.length === 1 &&
|
||||
typeof toolUseResult === "object" &&
|
||||
toolUseResult !== null &&
|
||||
typeof (toolUseResult as { taskId?: unknown }).taskId === "string"
|
||||
? (toolUseResult as { taskId: string }).taskId
|
||||
: undefined;
|
||||
for (const { tool_use_id: toolCallId } of results) {
|
||||
const toolUse = toolUses[toolCallId];
|
||||
if (!toolUse) continue;
|
||||
const taskId =
|
||||
(TASK_TOOLS.has(toolUse.name) ? structuredTaskId : undefined) ??
|
||||
[...(session.liveBackgroundTasks ?? [])].find(
|
||||
([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent,
|
||||
)?.[0];
|
||||
const task = taskId === undefined ? undefined : session.backgroundLevel?.get(taskId);
|
||||
if (
|
||||
!task ||
|
||||
task.ambient ||
|
||||
task.taskType === "local_bash" ||
|
||||
task.taskType === "local_agent"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
(session.backgroundToolCalls ??= new Map()).set(taskId!, {
|
||||
toolCallId,
|
||||
toolUse,
|
||||
...(parentToolUseId ? { parentToolUseId } : {}),
|
||||
});
|
||||
registered.add(toolCallId);
|
||||
export class SessionBackgroundWork {
|
||||
/** Claude Code's live background task set, from its last `background_tasks_changed`. */
|
||||
private level: BackgroundLevel = new Map();
|
||||
/** The terminals of the commands that went to the background, by task id. */
|
||||
private readonly terminals = new Map<string, BackgroundTerminal>();
|
||||
/** The tool calls whose tasks went on in the background, by task id. */
|
||||
private readonly toolCalls = new Map<string, BackgroundToolCall>();
|
||||
/**
|
||||
* The tool calls that returned, oldest first. A task that a call started
|
||||
* is part of that call until it returns, and the foreground work waits on
|
||||
* it. The task's `task_started` can come after the call returned. Bounded
|
||||
* by {@link MAX_RETURNED_CALLS}: a task joins the level around the time its
|
||||
* call returns.
|
||||
*/
|
||||
private readonly returned = new Set<string>();
|
||||
|
||||
/**
|
||||
* `reported`: whether the client gets the lists. Only when Claude Code's
|
||||
* interrupt spares background tasks, as `perTaskStopAffordance` makes it.
|
||||
*/
|
||||
constructor(
|
||||
private readonly sessionId: string,
|
||||
readonly reported: boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Keeps the display terminal of the command `taskId` running after its Bash
|
||||
* result, until its task ends (see `withRunningTerminal`).
|
||||
*/
|
||||
terminalStarted(taskId: string, terminal: BackgroundTerminal): void {
|
||||
this.terminals.set(taskId, terminal);
|
||||
}
|
||||
return registered;
|
||||
}
|
||||
|
||||
/** The level of a `background_tasks_changed` payload. */
|
||||
export function backgroundLevel(
|
||||
tasks: {
|
||||
task_id: string;
|
||||
task_type: string;
|
||||
description: string;
|
||||
parent_task_id?: string;
|
||||
ambient?: boolean;
|
||||
}[],
|
||||
): BackgroundLevel {
|
||||
return new Map(
|
||||
tasks.map((task) => [
|
||||
task.task_id,
|
||||
{
|
||||
ambient: task.ambient === true,
|
||||
taskType: task.task_type,
|
||||
description: task.description,
|
||||
...(task.parent_task_id !== undefined ? { parentTaskId: task.parent_task_id } : {}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Reads what a tool result message tells of the background tasks:
|
||||
*
|
||||
* - The returned calls no longer hold the tasks that they started.
|
||||
* - Each call whose task goes on in the background is registered: a
|
||||
* `Monitor` or `Workflow` call, whose structured result names its task,
|
||||
* or a call that a live task names as its tool use, such as a
|
||||
* backgrounded MCP call. The task must be in the level: one that ended
|
||||
* already leaves its call as it is. Claude Code marks every live-update
|
||||
* watcher `ambient`, including the task of a `Monitor` call; a call
|
||||
* started it, so it is work the client can see and stop, and is
|
||||
* registered all the same. Ambient tasks without a call of their own,
|
||||
* such as watchers Claude Code starts by itself, are never registered.
|
||||
*
|
||||
* Returns the ids of the registered calls, whose result then reports
|
||||
* `in_progress` (see `withRunningToolCall`).
|
||||
*/
|
||||
toolResults(
|
||||
owner: BackgroundOwner,
|
||||
content: unknown,
|
||||
toolUseResult: unknown,
|
||||
toolUses: Record<string, { name: string; input: unknown }>,
|
||||
parentToolUseId: string | null,
|
||||
): Set<string> {
|
||||
const registered = new Set<string>();
|
||||
if (!this.reported || !Array.isArray(content)) return registered;
|
||||
const results = content.filter(
|
||||
(block): block is { type: "tool_result"; tool_use_id: string; is_error?: boolean } =>
|
||||
typeof block === "object" &&
|
||||
block !== null &&
|
||||
block.type === "tool_result" &&
|
||||
typeof block.tool_use_id === "string",
|
||||
);
|
||||
for (const { tool_use_id: toolCallId } of results) {
|
||||
this.returned.add(toolCallId);
|
||||
if (this.returned.size > MAX_RETURNED_CALLS) {
|
||||
this.returned.delete(this.returned.values().next().value!);
|
||||
}
|
||||
}
|
||||
const succeeded = results.filter((block) => block.is_error !== true);
|
||||
const structuredTaskId =
|
||||
succeeded.length === 1 &&
|
||||
typeof toolUseResult === "object" &&
|
||||
toolUseResult !== null &&
|
||||
typeof (toolUseResult as { taskId?: unknown }).taskId === "string"
|
||||
? (toolUseResult as { taskId: string }).taskId
|
||||
: undefined;
|
||||
for (const { tool_use_id: toolCallId } of succeeded) {
|
||||
const toolUse = toolUses[toolCallId];
|
||||
if (!toolUse) continue;
|
||||
const taskId =
|
||||
(TASK_TOOLS.has(toolUse.name) ? structuredTaskId : undefined) ??
|
||||
[...owner.liveBackgroundTasks].find(
|
||||
([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent,
|
||||
)?.[0];
|
||||
const task = taskId === undefined ? undefined : this.level.get(taskId);
|
||||
if (!task || task.taskType === "local_bash" || task.taskType === "local_agent") {
|
||||
continue;
|
||||
}
|
||||
this.toolCalls.set(taskId!, {
|
||||
toolCallId,
|
||||
toolUse,
|
||||
...(parentToolUseId ? { parentToolUseId } : {}),
|
||||
});
|
||||
registered.add(toolCallId);
|
||||
}
|
||||
return registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `background` list of the root session `rootSessionId` and of each of
|
||||
* its announced subagent sessions, by session id, deeper sessions first and
|
||||
* the root last. Each list is empty while
|
||||
* the level has not been reported: Claude Code sends none before the first
|
||||
* background task starts.
|
||||
*/
|
||||
export function backgroundLists(
|
||||
session: BackgroundWorkSession,
|
||||
rootSessionId: string,
|
||||
): Map<string, BackgroundItem[]> {
|
||||
const children = [...(session.nativeSubagentsByTaskId ?? [])].filter(
|
||||
([, child]) => child.announced,
|
||||
);
|
||||
const childByTask = new Map(children);
|
||||
const childByToolUse = new Map(
|
||||
children.flatMap(([, child]) =>
|
||||
child.parentToolUseId ? [[child.parentToolUseId, child] as const] : [],
|
||||
),
|
||||
);
|
||||
const lists = new Map<string, BackgroundItem[]>([
|
||||
[rootSessionId, []],
|
||||
...children.map(([, child]): [string, BackgroundItem[]] => [child.sessionId, []]),
|
||||
]);
|
||||
const level: BackgroundLevel = session.backgroundLevel ?? new Map();
|
||||
for (const [taskId, task] of level) {
|
||||
if (task.ambient) continue;
|
||||
/**
|
||||
* Starts reading the output of the background command `taskId` into its
|
||||
* terminal, when the client gets the lists and the Bash result named the
|
||||
* command's output file. `send` sends an update of the terminal's tool
|
||||
* call, routed as the call's other updates. Called once the Bash result's
|
||||
* own updates are sent: the first output replaces the placeholder that they
|
||||
* gave the terminal.
|
||||
*/
|
||||
followOutput(taskId: string, send: Send): void {
|
||||
const terminal = this.terminals.get(taskId);
|
||||
if (!this.reported || !terminal?.outputFilePath || terminal.output) return;
|
||||
terminal.output = new BackgroundOutputTail(terminal.outputFilePath, (output) =>
|
||||
send(terminalOutputUpdate(this.sessionId, terminal, output)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The task `taskId` ended. Reads the rest of its command's output, then
|
||||
* exits its terminal, and gives a tool call whose task it was its final
|
||||
* status: `cancelled` for a task that Claude Code stopped, and `completed`
|
||||
* for one that ended without saying how, such as one that the level
|
||||
* dropped: it no longer runs, and reported no failure. Claude Code reports
|
||||
* no exit code for a command, and `completed` does not mean 0: Claude Code
|
||||
* accepts some non-zero codes as a success, such as grep's 1.
|
||||
*/
|
||||
async taskEnded(taskId: string, end: BackgroundTaskEnd, send: Send): Promise<void> {
|
||||
const terminal = this.terminals.get(taskId);
|
||||
if (terminal) {
|
||||
await terminal.output?.finish();
|
||||
this.terminals.delete(taskId);
|
||||
await send(terminalExit(this.sessionId, terminal));
|
||||
}
|
||||
const toolCall = this.toolCalls.get(taskId);
|
||||
if (toolCall) {
|
||||
this.toolCalls.delete(taskId);
|
||||
await send(
|
||||
toolCallStatusUpdate(
|
||||
this.sessionId,
|
||||
toolCall,
|
||||
end === "stopped" ? V2_CANCELLED : end === "failed" ? "failed" : "completed",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The update that reports the background tool call of the task `taskId` as
|
||||
* `paused`, or as `in_progress` again, when `paused` changes it. Claude Code
|
||||
* pauses a workflow this way.
|
||||
*/
|
||||
taskPaused(taskId: string, paused: boolean): AcpSessionNotification | undefined {
|
||||
const toolCall = this.toolCalls.get(taskId);
|
||||
if (!toolCall || (toolCall.paused ?? false) === paused) return undefined;
|
||||
toolCall.paused = paused;
|
||||
return toolCallStatusUpdate(this.sessionId, toolCall, paused ? V2_PAUSED : "in_progress");
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes Claude Code's new live background task set. The terminals and tool
|
||||
* calls whose tasks it no longer holds end first, before a list drops them,
|
||||
* as v2 requires. A terminal or tool call is registered at its tool result,
|
||||
* after its task joined the level, so a later level without the task means
|
||||
* that it ended. Claude Code usually sends a task's end before that level,
|
||||
* which ends them first, but the SDK does not promise that order.
|
||||
*/
|
||||
async levelChanged(tasks: LevelTask[], send: Send): Promise<void> {
|
||||
if (!this.reported) return;
|
||||
const level: BackgroundLevel = new Map(
|
||||
tasks.map((task) => [
|
||||
task.task_id,
|
||||
{
|
||||
ambient: task.ambient === true,
|
||||
taskType: task.task_type,
|
||||
description: task.description,
|
||||
...(task.parent_task_id !== undefined ? { parentTaskId: task.parent_task_id } : {}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
for (const taskId of [...this.terminals.keys(), ...this.toolCalls.keys()]) {
|
||||
if (!level.has(taskId)) await this.taskEnded(taskId, undefined, send);
|
||||
}
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* The query closed: Claude Code's background tasks end with its process.
|
||||
* Stops reading output, and returns the updates that exit the root
|
||||
* session's terminals and cancel its tool calls. The level empties, so the
|
||||
* next lists are empty. A subagent's command or tool call is left as it
|
||||
* was: it is on the child's stream, which the closed query's routing owned.
|
||||
*/
|
||||
queryClosed(): AcpSessionNotification[] {
|
||||
for (const terminal of this.terminals.values()) terminal.output?.stop();
|
||||
if (!this.reported) return [];
|
||||
const updates: AcpSessionNotification[] = [];
|
||||
for (const [taskId, terminal] of this.terminals) {
|
||||
if (terminal.parentToolUseId) continue;
|
||||
this.terminals.delete(taskId);
|
||||
updates.push(terminalExit(this.sessionId, terminal));
|
||||
}
|
||||
for (const [taskId, toolCall] of this.toolCalls) {
|
||||
if (toolCall.parentToolUseId) continue;
|
||||
this.toolCalls.delete(taskId);
|
||||
updates.push(toolCallStatusUpdate(this.sessionId, toolCall, V2_CANCELLED));
|
||||
}
|
||||
this.level = new Map();
|
||||
this.returned.clear();
|
||||
return updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `background` list of the root session and of each of its announced
|
||||
* subagent sessions, with what their repeated states need, deeper sessions
|
||||
* first and the root last: a child's own list goes out before its parent's
|
||||
* list drops it. Each list is empty while the level has not been reported:
|
||||
* Claude Code sends none before the first background task starts.
|
||||
*/
|
||||
reports(owner: BackgroundOwner, rootState: WorkStateName): BackgroundReport[] {
|
||||
const children = [...(owner.nativeSubagentsByTaskId ?? [])].filter(
|
||||
([, child]) => child.announced,
|
||||
);
|
||||
const childByTask = new Map(children);
|
||||
const childByToolUse = new Map(
|
||||
children.flatMap(([, child]) =>
|
||||
child.parentToolUseId ? [[child.parentToolUseId, child] as const] : [],
|
||||
),
|
||||
);
|
||||
const lists = new Map<string, BackgroundItem[]>([
|
||||
[this.sessionId, []],
|
||||
...children.map(([, child]): [string, BackgroundItem[]] => [child.sessionId, []]),
|
||||
]);
|
||||
// A subagent's command or tool call runs on the child's stream.
|
||||
const ownerOf = (parentToolUseId: string | undefined) =>
|
||||
parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : rootSessionId;
|
||||
const terminal = session.backgroundTerminals?.get(taskId);
|
||||
if (terminal) {
|
||||
const owner = ownerOf(terminal.parentToolUseId);
|
||||
if (owner) lists.get(owner)!.push({ type: "terminal", terminalId: terminal.toolCallId });
|
||||
continue;
|
||||
parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : this.sessionId;
|
||||
for (const [taskId, task] of this.level) {
|
||||
// A registered call's task is listed even when ambient, as a `Monitor`
|
||||
// call's is (see `toolResults`).
|
||||
const toolCall = this.toolCalls.get(taskId);
|
||||
if (toolCall) {
|
||||
const session = ownerOf(toolCall.parentToolUseId);
|
||||
if (session)
|
||||
lists.get(session)!.push({ type: "tool_call", toolCallId: toolCall.toolCallId });
|
||||
continue;
|
||||
}
|
||||
if (task.ambient) continue;
|
||||
const terminal = this.terminals.get(taskId);
|
||||
if (terminal) {
|
||||
const session = ownerOf(terminal.parentToolUseId);
|
||||
if (session)
|
||||
lists.get(session)!.push({ type: "terminal", terminalId: terminal.toolCallId });
|
||||
continue;
|
||||
}
|
||||
// A command without a terminal yet is one that foreground work waits on.
|
||||
// A subagent is listed below, as its session.
|
||||
if (task.taskType === "local_bash" || task.taskType === "local_agent") continue;
|
||||
// A task joins the level before its `task_started`, and before the tool
|
||||
// call that starts it returns. Until both, the task is unknown or part
|
||||
// of that tool call.
|
||||
const live = owner.liveBackgroundTasks.get(taskId);
|
||||
if (!live || (live.parentToolUseId && !this.returned.has(live.parentToolUseId))) continue;
|
||||
const parent = task.parentTaskId ? childByTask.get(task.parentTaskId) : undefined;
|
||||
lists
|
||||
.get(parent?.sessionId ?? this.sessionId)!
|
||||
.push({ type: "_claude/task", taskId, description: task.description });
|
||||
}
|
||||
const toolCall = session.backgroundToolCalls?.get(taskId);
|
||||
if (toolCall) {
|
||||
const owner = ownerOf(toolCall.parentToolUseId);
|
||||
if (owner) lists.get(owner)!.push({ type: "tool_call", toolCallId: toolCall.toolCallId });
|
||||
continue;
|
||||
// A child is listed in its parent's list while it works in the background,
|
||||
// and while it is idle with background work of its own. Deeper children
|
||||
// first, so a child's list is whole before its parent's reads it.
|
||||
const sessions = new Map(children.map(([, child]) => [child.sessionId, child]));
|
||||
const depth = (child: NativeSubagent): number => {
|
||||
const parent = sessions.get(child.parentSessionId);
|
||||
return parent ? depth(parent) + 1 : 0;
|
||||
};
|
||||
const ordered = children.sort(([, a], [, b]) => depth(b) - depth(a));
|
||||
for (const [taskId, child] of ordered) {
|
||||
const task = this.level.get(taskId);
|
||||
const working =
|
||||
child.workState !== undefined &&
|
||||
child.workState !== "idle" &&
|
||||
task !== undefined &&
|
||||
!task.ambient;
|
||||
if (working || lists.get(child.sessionId)!.length > 0) {
|
||||
lists.get(child.parentSessionId)?.push({ type: "subagent", sessionId: child.sessionId });
|
||||
}
|
||||
}
|
||||
// A command without a terminal yet is one that foreground work waits on.
|
||||
// A subagent is listed below, as its session.
|
||||
if (task.taskType === "local_bash" || task.taskType === "local_agent") continue;
|
||||
// A task joins the level before its `task_started`, and before the tool
|
||||
// call that starts it returns. Until both, the task is unknown or part of
|
||||
// that tool call, which the foreground work waits on. Claude Code drops a
|
||||
// tool use from its cache at the tool's result.
|
||||
const live = session.liveBackgroundTasks?.get(taskId);
|
||||
if (!live || (live.parentToolUseId && session.toolUseCache?.[live.parentToolUseId])) {
|
||||
continue;
|
||||
}
|
||||
const parent = task.parentTaskId ? childByTask.get(task.parentTaskId) : undefined;
|
||||
lists
|
||||
.get(parent?.sessionId ?? rootSessionId)!
|
||||
.push({ type: "_claude/task", taskId, description: task.description });
|
||||
return [
|
||||
...ordered.map(([, child]) => ({
|
||||
sessionId: child.sessionId,
|
||||
items: lists.get(child.sessionId)!,
|
||||
state: workStateName(child.workState),
|
||||
parentSessionId: child.parentSessionId,
|
||||
})),
|
||||
{ sessionId: this.sessionId, items: lists.get(this.sessionId)!, state: rootState },
|
||||
];
|
||||
}
|
||||
// A child is listed in its parent's list while it works in the background,
|
||||
// and while it is idle with background work of its own. Deeper children
|
||||
// first, so a child's list is whole before its parent's reads it.
|
||||
const sessions = new Map(children.map(([, child]) => [child.sessionId, child]));
|
||||
const depth = (child: NativeSubagent): number => {
|
||||
const parent = sessions.get(child.parentSessionId);
|
||||
return parent ? depth(parent) + 1 : 0;
|
||||
};
|
||||
for (const [taskId, child] of children.sort(([, a], [, b]) => depth(b) - depth(a))) {
|
||||
const task = level.get(taskId);
|
||||
const working =
|
||||
child.workState !== undefined &&
|
||||
child.workState !== "idle" &&
|
||||
task !== undefined &&
|
||||
!task.ambient;
|
||||
if (working || lists.get(child.sessionId)!.length > 0) {
|
||||
lists.get(child.parentSessionId)?.push({ type: "subagent", sessionId: child.sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* The current state of each background tool call in `toolCallIds`: the
|
||||
* call as `renderer` shows its tool use, with its status now (see
|
||||
* `ClaudeAcpAgent.publishBackgroundToolCalls`).
|
||||
*/
|
||||
toolCallStates(
|
||||
toolCallIds: string[],
|
||||
renderer: AcpToolCallRenderer,
|
||||
cwd: string,
|
||||
): AcpSessionNotification[] {
|
||||
return [...this.toolCalls.values()]
|
||||
.filter((toolCall) => toolCallIds.includes(toolCall.toolCallId))
|
||||
.map((toolCall) => {
|
||||
const update = renderer.toolCall({ id: toolCall.toolCallId, ...toolCall.toolUse }, { cwd });
|
||||
const status = toolCall.paused ? V2_PAUSED : "in_progress";
|
||||
return {
|
||||
sessionId: this.sessionId,
|
||||
update: { ...update, status } as AcpSessionNotification["update"],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The task of the terminal or tool call `target` that the session
|
||||
* `sessionId` lists, or undefined when it lists no such item.
|
||||
*/
|
||||
listedTask(
|
||||
owner: BackgroundOwner,
|
||||
sessionId: string,
|
||||
target: BackgroundTarget,
|
||||
): string | undefined {
|
||||
if (!this.reported) return undefined;
|
||||
const listed = this.reports(owner, "idle")
|
||||
.find((report) => report.sessionId === sessionId)
|
||||
?.items.some((item) =>
|
||||
"terminalId" in target
|
||||
? item.type === "terminal" && item.terminalId === target.terminalId
|
||||
: item.type === "tool_call" && item.toolCallId === target.toolCallId,
|
||||
);
|
||||
if (!listed) return undefined;
|
||||
const id = "terminalId" in target ? target.terminalId : target.toolCallId;
|
||||
const entries = "terminalId" in target ? this.terminals : this.toolCalls;
|
||||
return [...entries].find(([, entry]) => entry.toolCallId === id)?.[0];
|
||||
}
|
||||
// In the same order, the root last: a child's own list goes out before its
|
||||
// parent's list drops it.
|
||||
return new Map([
|
||||
...children.map(([, child]): [string, BackgroundItem[]] => [
|
||||
child.sessionId,
|
||||
lists.get(child.sessionId)!,
|
||||
]),
|
||||
[rootSessionId, lists.get(rootSessionId)!],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The tasks of the terminals and tool calls that `level` does not hold:
|
||||
* Claude Code no longer runs them, so they end before a list drops them, as
|
||||
* v2 requires. A terminal or tool call is registered at its tool result,
|
||||
* after its task joined the level, so a later level without the task means
|
||||
* that it ended. Claude Code usually sends a task's end before that level,
|
||||
* which ends them first, but the SDK does not promise that order.
|
||||
*/
|
||||
export function droppedTasks(session: BackgroundWorkSession, level: BackgroundLevel): string[] {
|
||||
return [
|
||||
...(session.backgroundTerminals?.keys() ?? []),
|
||||
...(session.backgroundToolCalls?.keys() ?? []),
|
||||
].filter((taskId) => !level.has(taskId));
|
||||
}
|
||||
type Send = (notification: AcpSessionNotification) => Promise<void>;
|
||||
|
||||
/** A terminal or tool call that `terminal/cancel` or `tool_call/cancel` names. */
|
||||
export type BackgroundTarget = { terminalId: string } | { toolCallId: string };
|
||||
|
||||
/**
|
||||
* Removes the terminal of the background command `taskId` of the session
|
||||
* `sessionId`, and returns the update that exits it, or undefined when the
|
||||
* command has no terminal. Claude Code reports no exit code for it, and
|
||||
* `completed` does not mean 0: Claude Code accepts some non-zero codes as a
|
||||
* success, such as grep's 1.
|
||||
* Stops the task of a terminal or tool call that the session `sessionId`
|
||||
* lists (v2 `terminal/cancel` and `tool_call/cancel`). `sessions` are the
|
||||
* root sessions: a subagent session's list is built with its root's. The
|
||||
* terminal exits, or the tool call gets its final status, when Claude Code
|
||||
* reports the task stopped.
|
||||
*/
|
||||
export function takeBackgroundTerminalExit(
|
||||
session: BackgroundWorkSession,
|
||||
export async function cancelBackgroundWork(
|
||||
sessions: Array<BackgroundOwner & { background?: SessionBackgroundWork }>,
|
||||
sessionId: string,
|
||||
taskId: string,
|
||||
): AcpSessionNotification | undefined {
|
||||
const terminal = session.backgroundTerminals?.get(taskId);
|
||||
if (!terminal) return undefined;
|
||||
session.backgroundTerminals!.delete(taskId);
|
||||
target: BackgroundTarget,
|
||||
): Promise<void> {
|
||||
const notCancellable = () =>
|
||||
"terminalId" in target
|
||||
? RequestError.invalidParams(
|
||||
{ sessionId, terminalId: target.terminalId },
|
||||
`Terminal ${target.terminalId} is not a command running in the background, so it cannot be cancelled.`,
|
||||
)
|
||||
: RequestError.invalidParams(
|
||||
{ sessionId, toolCallId: target.toolCallId },
|
||||
`Tool call ${target.toolCallId} is not running in the background, so it cannot be cancelled.`,
|
||||
);
|
||||
for (const session of sessions) {
|
||||
const taskId = session.background?.listedTask(session, sessionId, target);
|
||||
if (taskId === undefined) continue;
|
||||
try {
|
||||
await session.query.stopTask(taskId);
|
||||
} catch (error) {
|
||||
// The task ended meanwhile: it has ended on the client too, or is about to.
|
||||
if (session.background?.listedTask(session, sessionId, target) === undefined) {
|
||||
throw notCancellable();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw notCancellable();
|
||||
}
|
||||
|
||||
/** The update that gives the terminal of `terminal` the text `output`. */
|
||||
function terminalOutputUpdate(
|
||||
sessionId: string,
|
||||
terminal: BackgroundTerminal,
|
||||
output: BackgroundOutput,
|
||||
): AcpSessionNotification {
|
||||
const data = { terminal_id: terminal.toolCallId, data: output.text };
|
||||
return {
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: terminal.toolCallId,
|
||||
_meta: {
|
||||
...(terminal.parentToolUseId
|
||||
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
|
||||
: {}),
|
||||
...(output.replaces ? { terminal_output: data } : { terminal_output_delta: data }),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** The update that exits the terminal of `terminal`, with no exit code. */
|
||||
function terminalExit(sessionId: string, terminal: BackgroundTerminal): AcpSessionNotification {
|
||||
return {
|
||||
sessionId,
|
||||
update: {
|
||||
@@ -310,69 +553,6 @@ export function takeBackgroundTerminalExit(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the background tool call of the task `taskId` of the session
|
||||
* `sessionId`, and returns the update that gives it its final status, or
|
||||
* undefined when the task has no such call. A task that Claude Code stopped
|
||||
* is `cancelled`. One that ended without saying how, such as one that the
|
||||
* level dropped, is `completed`: it no longer runs, and reported no failure.
|
||||
*/
|
||||
export function takeBackgroundToolCallEnd(
|
||||
session: BackgroundWorkSession,
|
||||
sessionId: string,
|
||||
taskId: string,
|
||||
end: BackgroundTaskEnd,
|
||||
): AcpSessionNotification | undefined {
|
||||
const toolCall = session.backgroundToolCalls?.get(taskId);
|
||||
if (!toolCall) return undefined;
|
||||
session.backgroundToolCalls!.delete(taskId);
|
||||
return toolCallStatusUpdate(
|
||||
sessionId,
|
||||
toolCall,
|
||||
end === "stopped" ? V2_CANCELLED : end === "failed" ? "failed" : "completed",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The update that reports the background tool call of the task `taskId` as
|
||||
* `paused`, or as `in_progress` again, when `paused` changes it. Claude Code
|
||||
* pauses a workflow this way.
|
||||
*/
|
||||
export function backgroundToolCallPause(
|
||||
session: BackgroundWorkSession,
|
||||
sessionId: string,
|
||||
taskId: string,
|
||||
paused: boolean,
|
||||
): AcpSessionNotification | undefined {
|
||||
const toolCall = session.backgroundToolCalls?.get(taskId);
|
||||
if (!toolCall || (toolCall.paused ?? false) === paused) return undefined;
|
||||
toolCall.paused = paused;
|
||||
return toolCallStatusUpdate(sessionId, toolCall, paused ? V2_PAUSED : "in_progress");
|
||||
}
|
||||
|
||||
/**
|
||||
* The current state of each background tool call in `toolCallIds` of the
|
||||
* root session `sessionId`: the call as `renderer` shows its tool use, with
|
||||
* its status now (see `ClaudeAcpAgent.publishBackgroundToolCalls`).
|
||||
*/
|
||||
export function backgroundToolCallStates(
|
||||
session: BackgroundWorkSession & { cwd: string },
|
||||
sessionId: string,
|
||||
toolCallIds: string[],
|
||||
renderer: AcpToolCallRenderer,
|
||||
): AcpSessionNotification[] {
|
||||
return [...(session.backgroundToolCalls?.values() ?? [])]
|
||||
.filter((toolCall) => toolCallIds.includes(toolCall.toolCallId))
|
||||
.map((toolCall) => {
|
||||
const update = renderer.toolCall(
|
||||
{ id: toolCall.toolCallId, ...toolCall.toolUse },
|
||||
{ cwd: session.cwd },
|
||||
);
|
||||
const status = toolCall.paused ? V2_PAUSED : "in_progress";
|
||||
return { sessionId, update: { ...update, status } as AcpSessionNotification["update"] };
|
||||
});
|
||||
}
|
||||
|
||||
function toolCallStatusUpdate(
|
||||
sessionId: string,
|
||||
toolCall: BackgroundToolCall,
|
||||
@@ -393,80 +573,6 @@ function toolCallStatusUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the background work of a session whose query closed: Claude Code's
|
||||
* background tasks end with its process. Returns the updates that exit the
|
||||
* root session's terminals and cancel its tool calls, and empties the level,
|
||||
* so the next lists are empty. A subagent's command or tool call is left as
|
||||
* it was: it is on the child's stream, which the closed query's routing
|
||||
* owned.
|
||||
*/
|
||||
export function endBackgroundWork(
|
||||
session: BackgroundWorkSession,
|
||||
sessionId: string,
|
||||
): AcpSessionNotification[] {
|
||||
if (!session.reportsBackground) return [];
|
||||
const exits = [...(session.backgroundTerminals ?? [])].flatMap(([taskId, terminal]) => {
|
||||
const exit = terminal.parentToolUseId
|
||||
? undefined
|
||||
: takeBackgroundTerminalExit(session, sessionId, taskId);
|
||||
return exit ? [exit] : [];
|
||||
});
|
||||
const ends = [...(session.backgroundToolCalls ?? [])].flatMap(([taskId, toolCall]) => {
|
||||
const end = toolCall.parentToolUseId
|
||||
? undefined
|
||||
: takeBackgroundToolCallEnd(session, sessionId, taskId, "stopped");
|
||||
return end ? [end] : [];
|
||||
});
|
||||
session.backgroundLevel = undefined;
|
||||
return [...exits, ...ends];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the task of a terminal or tool call that the session `sessionId`
|
||||
* lists (v2 `terminal/cancel` and `tool_call/cancel`). `sessions` are the
|
||||
* root sessions, by id: a subagent session's list is built with its root's.
|
||||
* The terminal exits, or the tool call gets its final status, when Claude
|
||||
* Code reports the task stopped.
|
||||
*/
|
||||
export async function cancelBackgroundWork(
|
||||
sessions: Record<string, BackgroundWorkSession>,
|
||||
sessionId: string,
|
||||
target: { terminalId: string } | { toolCallId: string },
|
||||
): Promise<void> {
|
||||
const notCancellable = () =>
|
||||
"terminalId" in target
|
||||
? RequestError.invalidParams(
|
||||
{ sessionId, terminalId: target.terminalId },
|
||||
`Terminal ${target.terminalId} is not a command running in the background, so it cannot be cancelled.`,
|
||||
)
|
||||
: RequestError.invalidParams(
|
||||
{ sessionId, toolCallId: target.toolCallId },
|
||||
`Tool call ${target.toolCallId} is not running in the background, so it cannot be cancelled.`,
|
||||
);
|
||||
const isTarget = (item: BackgroundItem) =>
|
||||
"terminalId" in target
|
||||
? item.type === "terminal" && item.terminalId === target.terminalId
|
||||
: item.type === "tool_call" && item.toolCallId === target.toolCallId;
|
||||
const listed = (root: string, session: BackgroundWorkSession) =>
|
||||
session.reportsBackground === true &&
|
||||
(backgroundLists(session, root).get(sessionId)?.some(isTarget) ?? false);
|
||||
const owner = Object.entries(sessions).find(([root, session]) => listed(root, session));
|
||||
if (!owner) throw notCancellable();
|
||||
const [root, session] = owner;
|
||||
const id = "terminalId" in target ? target.terminalId : target.toolCallId;
|
||||
const taskId = [
|
||||
...(("terminalId" in target ? session.backgroundTerminals : session.backgroundToolCalls) ?? []),
|
||||
].find(([, entry]) => entry.toolCallId === id)![0];
|
||||
try {
|
||||
await session.query.stopTask(taskId);
|
||||
} catch (error) {
|
||||
// The task ended meanwhile: it has ended on the client too, or is about to.
|
||||
if (!listed(root, session)) throw notCancellable();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the lists of each session that reports them, each time one
|
||||
* changes. The sink sends them to the client synchronously, so a list follows
|
||||
@@ -488,25 +594,17 @@ export class BackgroundWorkReporter {
|
||||
* first call for a session publishes its list, also when it is empty.
|
||||
* `rootState` is the root session's state now.
|
||||
*/
|
||||
sync(rootSessionId: string, session: BackgroundWorkSession, rootState: WorkStateName): void {
|
||||
if (!this.sink || !session.reportsBackground) return;
|
||||
const children = new Map(
|
||||
[...(session.nativeSubagentsByTaskId?.values() ?? [])].map((child) => [
|
||||
child.sessionId,
|
||||
child,
|
||||
]),
|
||||
);
|
||||
for (const [sessionId, items] of backgroundLists(session, rootSessionId)) {
|
||||
const json = JSON.stringify(items);
|
||||
if (this.published.get(sessionId)?.json === json) continue;
|
||||
this.published.set(sessionId, { json, root: rootSessionId });
|
||||
const child = children.get(sessionId);
|
||||
this.sink.changed({
|
||||
sessionId,
|
||||
items,
|
||||
state: child ? workStateName(child.workState) : rootState,
|
||||
...(child ? { parentSessionId: child.parentSessionId } : {}),
|
||||
});
|
||||
sync(
|
||||
rootSessionId: string,
|
||||
session: BackgroundOwner & { background?: SessionBackgroundWork },
|
||||
rootState: WorkStateName,
|
||||
): void {
|
||||
if (!this.sink || !session.background?.reported) return;
|
||||
for (const report of session.background.reports(session, rootState)) {
|
||||
const json = JSON.stringify(report.items);
|
||||
if (this.published.get(report.sessionId)?.json === json) continue;
|
||||
this.published.set(report.sessionId, { json, root: rootSessionId });
|
||||
this.sink.changed(report);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
src/binary-entry.ts
Normal file
12
src/binary-entry.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Entry of the release executable (`scripts/build-binary.mjs`). It registers the
|
||||
// Claude CLI shipped next to the executable before importing the agent:
|
||||
// index.ts starts serving (or, with --cli, spawns the CLI) while it evaluates,
|
||||
// and a static import would evaluate it before this module's body runs.
|
||||
import { realpathSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { useBundledClaudeCli } from "./claude-cli.js";
|
||||
|
||||
// realpath, so a symlink to the executable (e.g. in /usr/local/bin) still finds
|
||||
// the CLI next to the real file.
|
||||
useBundledClaudeCli(dirname(realpathSync(process.execPath)));
|
||||
await import("./index.js");
|
||||
88
src/claude-cli.ts
Normal file
88
src/claude-cli.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const CLI_FILE_NAME = process.platform === "win32" ? "claude.exe" : "claude";
|
||||
|
||||
let bundledCliDirectory: string | undefined;
|
||||
|
||||
/**
|
||||
* Called by the executable's entry (`src/binary-entry.ts`) before the agent
|
||||
* starts. A `bun build --compile` executable has no `node_modules` to resolve
|
||||
* the SDK's platform package from, so the release archive ships the Claude CLI
|
||||
* next to it. The CLI isn't embedded: a child process can't run a file inside
|
||||
* Bun's `$bunfs`, so an embedded copy would have to be extracted to disk first.
|
||||
*/
|
||||
export function useBundledClaudeCli(directory: string): void {
|
||||
bundledCliDirectory = directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Claude CLI binary to spawn: `CLAUDE_CODE_EXECUTABLE` if set, then the CLI
|
||||
* shipped next to the executable, then the SDK's platform-specific package.
|
||||
*/
|
||||
export async function claudeCliPath(): Promise<string> {
|
||||
if (process.env.CLAUDE_CODE_EXECUTABLE) {
|
||||
return process.env.CLAUDE_CODE_EXECUTABLE;
|
||||
}
|
||||
if (bundledCliDirectory !== undefined) {
|
||||
return bundledClaudeCliPath(bundledCliDirectory);
|
||||
}
|
||||
return installedClaudeCliPath();
|
||||
}
|
||||
|
||||
function bundledClaudeCliPath(directory: string): string {
|
||||
const path = join(directory, CLI_FILE_NAME);
|
||||
// Without this check, spawning a missing CLI fails with a bare ENOENT.
|
||||
if (!existsSync(path)) {
|
||||
throw new Error(
|
||||
`The Claude CLI is missing: expected ${path}. Keep ${CLI_FILE_NAME} from the release archive ` +
|
||||
"in the same directory as claude-agent-acp, or set CLAUDE_CODE_EXECUTABLE to a Claude CLI binary.",
|
||||
);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
async function installedClaudeCliPath(): Promise<string> {
|
||||
// The SDK's CLI is a native binary shipped as a platform-specific optional
|
||||
// dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
|
||||
// to the SDK so nested installs are found even when npm doesn't hoist.
|
||||
const { createRequire } = await import("node:module");
|
||||
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
// On linux, both glibc and musl variants may be installed side-by-side
|
||||
// (e.g. bunx hydrates every optional dep), so picking one by trial is
|
||||
// unreliable: the wrong binary segfaults at runtime instead of failing to
|
||||
// spawn. Detect the runtime libc and prefer the matching variant, falling
|
||||
// back to the other only if the preferred one isn't installed.
|
||||
const candidates =
|
||||
process.platform === "linux"
|
||||
? isMuslLibc()
|
||||
? [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
]
|
||||
: [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
]
|
||||
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return req.resolve(candidate);
|
||||
} catch {
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Claude native binary not found for ${process.platform}-${process.arch}. ` +
|
||||
`Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`,
|
||||
);
|
||||
}
|
||||
|
||||
function isMuslLibc(): boolean {
|
||||
// process.report.getReport().header.glibcVersionRuntime is populated when
|
||||
// Node is dynamically linked against glibc, and absent on musl.
|
||||
const report = process.report?.getReport() as
|
||||
{ header?: { glibcVersionRuntime?: string } } | undefined;
|
||||
return !report?.header?.glibcVersionRuntime;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { AIR_CONTEXT_COMPACTION_KEY, withAirMeta } from "./air-extension.js";
|
||||
|
||||
export const CONTEXT_COMPACTION_META_VERSION = 1;
|
||||
|
||||
export type ContextCompactionTrigger = "manual" | "automatic";
|
||||
export type ContextCompactionTrigger = "manual" | "automatic" | "idle";
|
||||
|
||||
export interface ContextCompactionMetadata {
|
||||
version: typeof CONTEXT_COMPACTION_META_VERSION;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import {
|
||||
ContextCompactionMetadata,
|
||||
ContextCompactionTrigger,
|
||||
createContextCompactionMeta,
|
||||
} from "./context-compaction-meta.js";
|
||||
import { compactionToolCall } from "./tool-calls/reporters/compaction.js";
|
||||
@@ -66,7 +67,7 @@ const ANALYSIS_BLOCKS = [
|
||||
* it as the post-compaction user message. */
|
||||
const CONTINUATION_PREAMBLE = /^\s*This session is being continued from a previous conversation/i;
|
||||
const PERSISTED_PREAMBLE =
|
||||
/^\s*This session is being continued from a previous conversation[^\n]*\n+Summary:[ \t]*\r?\n/;
|
||||
/^\s*This session is being continued from a previous conversation[^\n]*\n(?:[\s\S]*?\n)?Summary:[ \t]*\r?\n/;
|
||||
const PERSISTED_TRAILERS = [
|
||||
/\n+If you need specific details from before compaction[\s\S]*$/,
|
||||
/\n+(?:Please )?[Cc]ontinue the conversation from where it left (?:it )?off[\s\S]*$/,
|
||||
@@ -408,14 +409,29 @@ function withoutError({
|
||||
return facts;
|
||||
}
|
||||
|
||||
export function contextCompactionMetadataFromBoundary(compactMetadata: {
|
||||
trigger: "manual" | "auto";
|
||||
pre_tokens: number;
|
||||
post_tokens?: number;
|
||||
duration_ms?: number;
|
||||
}): Omit<ContextCompactionMetadata, "version"> {
|
||||
function compactionTrigger(
|
||||
cliTrigger: "manual" | "auto",
|
||||
sentByAdapter: boolean,
|
||||
): ContextCompactionTrigger {
|
||||
// The CLI's own automatic compaction stays automatic, even inside the adapter's /compact turn.
|
||||
if (cliTrigger === "auto") return "automatic";
|
||||
// The CLI reports every /compact as manual; only the adapter knows it sent this one itself.
|
||||
if (sentByAdapter) return "idle";
|
||||
return "manual";
|
||||
}
|
||||
|
||||
/** `idleCompaction` marks a `/compact` that the adapter sent itself (see `IdleCompaction`). */
|
||||
export function contextCompactionMetadataFromBoundary(
|
||||
compactMetadata: {
|
||||
trigger: "manual" | "auto";
|
||||
pre_tokens: number;
|
||||
post_tokens?: number;
|
||||
duration_ms?: number;
|
||||
},
|
||||
{ idleCompaction = false }: { idleCompaction?: boolean } = {},
|
||||
): Omit<ContextCompactionMetadata, "version"> {
|
||||
return {
|
||||
trigger: compactMetadata.trigger === "auto" ? "automatic" : "manual",
|
||||
trigger: compactionTrigger(compactMetadata.trigger, idleCompaction),
|
||||
preTokens: compactMetadata.pre_tokens,
|
||||
...(compactMetadata.post_tokens !== undefined
|
||||
? { postTokens: compactMetadata.post_tokens }
|
||||
|
||||
@@ -303,7 +303,7 @@ export function applyAskElicitationResponse(
|
||||
questions: AskUserQuestion[],
|
||||
options: { reportUnanswered?: boolean } = {},
|
||||
): AskUserQuestionOutcome {
|
||||
// Typed against the output schema: as of SDK 0.3.316 only it lists
|
||||
// Typed against the output schema: as of SDK 0.3.319 only it lists
|
||||
// `unanswered`, though the tool reads the same annotations from its input.
|
||||
const annotations: NonNullable<AskUserQuestionOutput["annotations"]> = {};
|
||||
const withAnnotations = (answers: AskUserQuestionOutput["answers"]) => ({
|
||||
|
||||
240
src/idle-compaction.ts
Normal file
240
src/idle-compaction.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
export type PromptCacheTtl = "5m" | "1h";
|
||||
|
||||
export type IdleCompactionHost = {
|
||||
/** Claude is working (`startTurn`'s check) or a permission request / user input is pending. */
|
||||
isBusy(): boolean;
|
||||
/** Context size after the last main-thread request, or undefined if unknown. */
|
||||
contextTokens(): number | undefined;
|
||||
/** Current merged Claude Code settings. */
|
||||
settings(): {
|
||||
idleCompaction?: boolean;
|
||||
autoCompactEnabled?: boolean;
|
||||
env?: Record<string, string | undefined>;
|
||||
};
|
||||
/** Runs `/compact` as an adapter-originated turn; settles with that turn. */
|
||||
compact(): Promise<unknown>;
|
||||
/** Debug-level log. */
|
||||
log(message: string): void;
|
||||
};
|
||||
|
||||
export type IdleCompactionOptions = {
|
||||
env?: Record<string, string | undefined>;
|
||||
setTimeout?: (callback: () => void, ms: number) => unknown;
|
||||
};
|
||||
|
||||
/** The longest delay `setTimeout` takes; a longer one fires at once. */
|
||||
const MAX_TIMER_MS = 2_147_483_647;
|
||||
const HOUR_MS = 3_600_000;
|
||||
const DEFAULT_MIN_TOKENS = 200_000;
|
||||
const MIN_TOKENS_FLOOR = 100_000;
|
||||
// 54 min for a 1h cache: leaves a margin for the gap between the provider
|
||||
// writing the cache and the adapter receiving the response.
|
||||
const DEFAULT_DELAY_MS = 0.9 * HOUR_MS;
|
||||
// Node timers do not run while the machine sleeps, so one can fire hours after
|
||||
// its deadline, on a cache that expired meanwhile. Same threshold as Claude Code.
|
||||
const LAPSED_AFTER_MS = 60_000;
|
||||
// The values Claude Code reads as "on" for env flags such as DISABLE_COMPACT
|
||||
// (case-insensitive, trimmed).
|
||||
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
|
||||
|
||||
function isTruthyEnv(value: string | undefined): boolean {
|
||||
return value !== undefined && TRUTHY_ENV.has(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function parseCacheTtl(usage: unknown): PromptCacheTtl | undefined {
|
||||
const creation = (usage as { cache_creation?: unknown } | null | undefined)?.cache_creation;
|
||||
if (typeof creation !== "object" || creation === null) return undefined;
|
||||
const { ephemeral_1h_input_tokens: hour, ephemeral_5m_input_tokens: fiveMin } = creation as {
|
||||
ephemeral_1h_input_tokens?: unknown;
|
||||
ephemeral_5m_input_tokens?: unknown;
|
||||
};
|
||||
if (typeof hour === "number" && hour > 0) return "1h";
|
||||
if (typeof fiveMin === "number" && fiveMin > 0) return "5m";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compacts a session shortly before its one-hour prompt cache expires, so the
|
||||
* next prompt writes a short summary instead of the whole context. Claude Code
|
||||
* has this feature but arms it only for "attended" sessions, which SDK sessions
|
||||
* are not. One instance per session.
|
||||
*/
|
||||
export class IdleCompaction {
|
||||
private readonly env: Record<string, string | undefined>;
|
||||
private readonly setTimer: (callback: () => void, ms: number) => unknown;
|
||||
private ttl: PromptCacheTtl | undefined;
|
||||
private lastRequestAt: number | undefined;
|
||||
private lastMessageId: string | undefined;
|
||||
/** Set when `/compact` was sent, cleared by the next request: a compaction that failed,
|
||||
* or left the context large, would otherwise re-arm at once and loop until the user prompts. */
|
||||
private compactedSinceLastRequest = false;
|
||||
private timer: unknown;
|
||||
private compacting = false;
|
||||
private disposed = false;
|
||||
private warnedInvalidDelay = false;
|
||||
/** `status` of the last `rate_limit_event`; unknown until one arrives. */
|
||||
private rateLimitStatus: string | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly host: IdleCompactionHost,
|
||||
options: IdleCompactionOptions = {},
|
||||
) {
|
||||
this.env = options.env ?? process.env;
|
||||
this.setTimer = options.setTimeout ?? ((cb, ms) => setTimeout(cb, ms));
|
||||
}
|
||||
|
||||
/** Whether an idle compaction it started is still running. `acp-agent.ts` reports
|
||||
* `trigger: "idle"` while it is true. A client's `/compact` queued behind is read
|
||||
* only after the adapter's turn's `result`, which clears `compacting`; this relies
|
||||
* on the CLI emitting that compaction's `compact_boundary` after the previous
|
||||
* result, which nothing here enforces. */
|
||||
get isCompacting(): boolean {
|
||||
return this.compacting;
|
||||
}
|
||||
|
||||
/** A main-thread response arrived. Records its time and cache TTL, and drops any armed timer.
|
||||
* The provider writes the cache when the request starts, so the deadline is measured from the
|
||||
* response's first sighting: the `message_start` stream event, or its first `assistant` message
|
||||
* when it was not streamed. The CLI sends one `assistant` message per content block, and a
|
||||
* 7-minute response would otherwise put the deadline 7 minutes after the cache was written.
|
||||
* A later call with the same `messageId` only updates the TTL. */
|
||||
noteRequest(usage: unknown, messageId?: string): void {
|
||||
if (this.disposed) return;
|
||||
this.ttl = parseCacheTtl(usage) ?? this.ttl;
|
||||
if (messageId !== undefined && messageId === this.lastMessageId) return;
|
||||
this.lastMessageId = messageId;
|
||||
this.lastRequestAt = Date.now();
|
||||
this.compactedSinceLastRequest = false;
|
||||
this.disarm("new request");
|
||||
}
|
||||
|
||||
/** The session became idle. Arms the timer if every condition holds, else logs why not. */
|
||||
onIdle(): void {
|
||||
if (this.disposed) return;
|
||||
const skip = this.skipReason();
|
||||
if (skip) {
|
||||
this.host.log(`[idle-compaction] skip: ${skip}`);
|
||||
return;
|
||||
}
|
||||
this.disarm("rearm");
|
||||
const deadline = this.lastRequestAt! + this.delayMs();
|
||||
this.timer = this.setTimer(() => void this.fire(), Math.max(0, deadline - Date.now()));
|
||||
(this.timer as { unref?: () => void } | undefined)?.unref?.();
|
||||
this.host.log(`[idle-compaction] armed deadline=${new Date(deadline).toISOString()}`);
|
||||
}
|
||||
|
||||
/** A `rate_limit_event` arrived (`SDKRateLimitInfo`). Anything but `allowed` skips compaction,
|
||||
* as in Claude Code. This is account state, so `reset()` keeps it. */
|
||||
noteRateLimit(info: unknown): void {
|
||||
const status = (info as { status?: unknown } | null | undefined)?.status;
|
||||
if (typeof status === "string") this.rateLimitStatus = status;
|
||||
}
|
||||
|
||||
/** Claude Code reported its compaction as failed. Its turn still ends normally, so `compact()` does not reject. */
|
||||
noteCompactionFailed(error: string | undefined): void {
|
||||
if (!this.compacting) return;
|
||||
this.host.log(`[idle-compaction] compaction failed: ${error ?? "no reason given"}`);
|
||||
}
|
||||
|
||||
/** The conversation was replaced (`/clear`): forgets the recorded request and TTL, and drops any armed timer. */
|
||||
reset(): void {
|
||||
this.ttl = undefined;
|
||||
this.lastRequestAt = undefined;
|
||||
this.lastMessageId = undefined;
|
||||
this.compactedSinceLastRequest = false;
|
||||
this.disarm("conversation reset");
|
||||
}
|
||||
|
||||
/** Drops the armed timer, if any. `reason` is only logged. */
|
||||
disarm(reason: string): void {
|
||||
if (this.timer === undefined) return;
|
||||
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
||||
this.timer = undefined;
|
||||
this.host.log(`[idle-compaction] disarmed: ${reason}`);
|
||||
}
|
||||
|
||||
/** Disarms and ignores every later call. */
|
||||
dispose(): void {
|
||||
this.disarm("disposed");
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
private async fire(): Promise<void> {
|
||||
this.timer = undefined;
|
||||
if (this.disposed) return;
|
||||
const skip = this.skipReason();
|
||||
if (skip) {
|
||||
this.host.log(`[idle-compaction] skip at fire: ${skip}`);
|
||||
return;
|
||||
}
|
||||
this.host.log("[idle-compaction] firing /compact");
|
||||
this.compacting = true;
|
||||
this.compactedSinceLastRequest = true;
|
||||
try {
|
||||
await this.host.compact();
|
||||
} catch (error) {
|
||||
this.host.log(`[idle-compaction] compaction failed: ${error}`);
|
||||
} finally {
|
||||
this.compacting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Why no compaction should be scheduled right now, or undefined if all conditions hold. */
|
||||
private skipReason(): string | undefined {
|
||||
if (this.compacting) return "compaction already running";
|
||||
const disabled = this.disabledBy();
|
||||
if (disabled) return `disabled by ${disabled}`;
|
||||
if (this.lastRequestAt === undefined) return "no request recorded in this process";
|
||||
if (Date.now() > this.lastRequestAt + this.delayMs() + LAPSED_AFTER_MS) {
|
||||
return "cache lapsed: more than 60 s past the deadline";
|
||||
}
|
||||
if (this.rateLimitStatus !== undefined && this.rateLimitStatus !== "allowed") {
|
||||
return `rate limit status is ${this.rateLimitStatus}`;
|
||||
}
|
||||
if (this.compactedSinceLastRequest) return "already compacted for this request";
|
||||
if (this.ttl !== "1h") return `cache TTL is ${this.ttl ?? "unknown"}, not 1h`;
|
||||
const tokens = this.host.contextTokens();
|
||||
const min = this.minTokens();
|
||||
if (tokens === undefined || tokens < min) {
|
||||
return `context ${tokens ?? "unknown"} tokens is below the ${min} minimum`;
|
||||
}
|
||||
if (this.host.isBusy()) return "session is busy";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private disabledBy(): string | undefined {
|
||||
const settings = this.host.settings();
|
||||
if (settings.idleCompaction === false) return "idleCompaction setting";
|
||||
if (settings.autoCompactEnabled === false) return "autoCompactEnabled setting";
|
||||
for (const name of ["DISABLE_COMPACT", "DISABLE_AUTO_COMPACT"]) {
|
||||
if (isTruthyEnv(this.claudeCodeEnv(name))) return name;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** The CLI applies the settings `env` block after the launch env, so it wins. */
|
||||
private claudeCodeEnv(name: string): string | undefined {
|
||||
return this.host.settings().env?.[name] ?? this.env[name];
|
||||
}
|
||||
|
||||
/** `0` and invalid values fall back to 200,000, as in the CLI, which declares the variable
|
||||
* with `min: 1` and computes `Math.max(100000, env ?? 200000)`. */
|
||||
private minTokens(): number {
|
||||
const raw = this.claudeCodeEnv("CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS");
|
||||
const parsed = raw !== undefined && /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : 0;
|
||||
return Math.max(MIN_TOKENS_FLOOR, parsed > 0 ? parsed : DEFAULT_MIN_TOKENS);
|
||||
}
|
||||
|
||||
private delayMs(): number {
|
||||
const raw = this.env.CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS;
|
||||
if (raw === undefined) return DEFAULT_DELAY_MS;
|
||||
if (/^\d+$/.test(raw.trim()) && Number(raw.trim()) <= MAX_TIMER_MS) return Number(raw.trim());
|
||||
if (!this.warnedInvalidDelay) {
|
||||
this.warnedInvalidDelay = true;
|
||||
this.host.log(
|
||||
`[idle-compaction] ignoring invalid CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS=${raw}`,
|
||||
);
|
||||
}
|
||||
return DEFAULT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { claudeCliPath } from "./acp-agent.js";
|
||||
import { claudeCliPath } from "./claude-cli.js";
|
||||
import { applyManagedPolicyEnv } from "./managed-policy.js";
|
||||
import { acpProtocolsFromEnv, serveAcp } from "./serve.js";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
|
||||
@@ -111,8 +111,11 @@ function needsReconnect(status: McpServerStatus): boolean {
|
||||
function codeSpan(text: string): string {
|
||||
const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
||||
const fence = "`".repeat(longestRun + 1);
|
||||
// CommonMark strips one space from each end of a span, so a pad keeps the text unchanged.
|
||||
const pad = text.startsWith("`") || text.endsWith("`") || /^ .*[^ ].* $/.test(text) ? " " : "";
|
||||
// CommonMark strips one space from each end of a span that is not all spaces
|
||||
// (line endings count as spaces), so a pad keeps the text unchanged. Plain
|
||||
// string checks, not one regex: `^ .*[^ ].* $` backtracks quadratically.
|
||||
const strippedByCommonMark = text.startsWith(" ") && text.endsWith(" ") && /[^ \r\n]/.test(text);
|
||||
const pad = text.startsWith("`") || text.endsWith("`") || strippedByCommonMark ? " " : "";
|
||||
return `${fence}${pad}${text}${pad}${fence}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { PermissionMode, Settings } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
interface PermissionModeLogger {
|
||||
error: (...args: unknown[]) => void;
|
||||
@@ -58,3 +58,10 @@ export function resolvePermissionMode(
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/** Claude Code reads `disableAutoMode` both at the top level and under `permissions`. */
|
||||
export function autoModeDisabled(settings: Settings): boolean {
|
||||
return (
|
||||
settings.disableAutoMode === "disable" || settings.permissions?.disableAutoMode === "disable"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,29 +66,50 @@ type ModeConfigSession = SessionMode & {
|
||||
configOptions: SessionConfigOption[];
|
||||
};
|
||||
|
||||
type InitializeSessionModeParams = {
|
||||
query: Pick<Query, "setPermissionMode">;
|
||||
requestedMode: PermissionMode;
|
||||
currentModelInfo?: ModelInfo;
|
||||
currentModelId: string;
|
||||
/** Which optional modes the session's mode catalog offers. */
|
||||
export type ModeAvailability = {
|
||||
/** When false, bypassPermissions is omitted from the mode catalog. */
|
||||
allowBypass: boolean;
|
||||
/** When false (`disableAutoMode: "disable"` in settings), auto is omitted. */
|
||||
allowAuto: boolean;
|
||||
};
|
||||
|
||||
type InitializeSessionModeParams = ModeAvailability & {
|
||||
query: Pick<Query, "setPermissionMode">;
|
||||
requestedMode: PermissionMode;
|
||||
/** The mode is the built-in default: no request, transcript or setting chose it. */
|
||||
builtInDefault?: boolean;
|
||||
currentModelInfo?: ModelInfo;
|
||||
currentModelId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The permission mode a new Claude query starts in.
|
||||
*
|
||||
* An explicit mode wins. A resume or a load continues in the mode of the last
|
||||
* user record of the transcript. If that mode is unknown or not available now,
|
||||
* for example bypass, the session starts in `defaultMode` of the settings.
|
||||
* user record of the transcript. If that mode is unknown or absent from the
|
||||
* session's mode catalog, use an available `defaultMode` from settings, or
|
||||
* `default` when that setting is unavailable too.
|
||||
*
|
||||
* With no `defaultMode` set, the session starts in `auto` when the catalog
|
||||
* offers it and in `default` otherwise (`builtInDefault: true`), like an
|
||||
* interactive Claude Code session. Claude Code also starts organizations with
|
||||
* the HIPAA configuration in `default`; the SDK does not report that
|
||||
* configuration, so those sessions start in `auto` here.
|
||||
*/
|
||||
export function resolveInitialPermissionMode(
|
||||
sources: { explicit?: PermissionMode; resumed?: string; defaultMode?: unknown },
|
||||
logger: { error: (...args: unknown[]) => void },
|
||||
allowBypass: boolean,
|
||||
): PermissionMode {
|
||||
availability: ModeAvailability,
|
||||
): { mode: PermissionMode; builtInDefault: boolean } {
|
||||
const { allowBypass } = availability;
|
||||
const available = (mode: PermissionMode) =>
|
||||
buildAvailableModes(availability).some((entry) => entry.id === mode);
|
||||
if (sources.explicit !== undefined) {
|
||||
return resolvePermissionMode(sources.explicit, logger, allowBypass);
|
||||
return {
|
||||
mode: resolvePermissionMode(sources.explicit, logger, allowBypass),
|
||||
builtInDefault: false,
|
||||
};
|
||||
}
|
||||
if (sources.resumed !== undefined) {
|
||||
let rejected = false;
|
||||
@@ -97,13 +118,23 @@ export function resolveInitialPermissionMode(
|
||||
{ error: () => (rejected = true) },
|
||||
allowBypass,
|
||||
);
|
||||
if (!rejected) return mode;
|
||||
if (!rejected && available(mode)) {
|
||||
return { mode, builtInDefault: false };
|
||||
}
|
||||
logger.error(
|
||||
`Ignoring permission mode "${sources.resumed}" of the resumed transcript: ` +
|
||||
`not available in this session.`,
|
||||
);
|
||||
}
|
||||
return resolvePermissionMode(sources.defaultMode, logger, allowBypass);
|
||||
if (sources.defaultMode === undefined) {
|
||||
return { mode: available("auto") ? "auto" : "default", builtInDefault: true };
|
||||
}
|
||||
const configuredMode = resolvePermissionMode(sources.defaultMode, logger, allowBypass);
|
||||
// A rejected transcript mode falls back only to a mode this session can offer.
|
||||
if (sources.resumed !== undefined && !available(configuredMode)) {
|
||||
return { mode: "default", builtInDefault: false };
|
||||
}
|
||||
return { mode: configuredMode, builtInDefault: false };
|
||||
}
|
||||
|
||||
/** Owns session-mode policy and the ACP/SDK synchronization it requires. */
|
||||
@@ -113,22 +144,40 @@ export class SessionModeManager<S extends SessionMode> {
|
||||
async initialize({
|
||||
query,
|
||||
requestedMode,
|
||||
builtInDefault = false,
|
||||
currentModelInfo,
|
||||
currentModelId,
|
||||
allowBypass,
|
||||
allowAuto,
|
||||
}: InitializeSessionModeParams): Promise<{
|
||||
modes: SessionModeState;
|
||||
autoModeFallbackWarningPending: boolean;
|
||||
}> {
|
||||
const availableModes = this.buildAvailableModes(allowBypass);
|
||||
const availableModes = buildAvailableModes(
|
||||
{ allowBypass, allowAuto },
|
||||
this.options.airClient?.() ?? false,
|
||||
);
|
||||
let effectiveMode = requestedMode;
|
||||
let autoModeFallbackWarningPending = false;
|
||||
|
||||
if (
|
||||
const autoUnsupported =
|
||||
effectiveMode === "auto" &&
|
||||
currentModelInfo !== undefined &&
|
||||
currentModelInfo.supportsAutoMode !== true
|
||||
) {
|
||||
currentModelInfo.supportsAutoMode !== true;
|
||||
if (!this.isAvailable(availableModes, effectiveMode)) {
|
||||
this.options.logError(
|
||||
`permissions.defaultMode "${effectiveMode}" is not available in ` +
|
||||
`this session; falling back to "default".`,
|
||||
);
|
||||
effectiveMode = "default";
|
||||
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
|
||||
} else if (autoUnsupported && builtInDefault) {
|
||||
// Like Claude Code: a built-in Auto default the model can't serve starts
|
||||
// in Manual, without a warning. Nobody chose Auto, so don't escalate to
|
||||
// acceptEdits.
|
||||
effectiveMode = "default";
|
||||
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
|
||||
} else if (autoUnsupported) {
|
||||
this.options.logError(
|
||||
`permissions.defaultMode "auto" is not available for model ` +
|
||||
`"${currentModelId}"; falling back to "${AUTO_MODE_FALLBACK}".`,
|
||||
@@ -136,13 +185,6 @@ export class SessionModeManager<S extends SessionMode> {
|
||||
effectiveMode = AUTO_MODE_FALLBACK;
|
||||
autoModeFallbackWarningPending = true;
|
||||
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
|
||||
} else if (!this.isAvailable(availableModes, effectiveMode)) {
|
||||
this.options.logError(
|
||||
`permissions.defaultMode "${effectiveMode}" is not available in ` +
|
||||
`this session; falling back to "default".`,
|
||||
);
|
||||
effectiveMode = "default";
|
||||
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -350,50 +392,6 @@ export class SessionModeManager<S extends SessionMode> {
|
||||
}
|
||||
}
|
||||
|
||||
private buildAvailableModes(allowBypass: boolean): SessionModeState["availableModes"] {
|
||||
// Only AIR gets the mode kind.
|
||||
const air = this.options.airClient?.() ?? false;
|
||||
const kind = (value: string) => {
|
||||
const meta = airOnlyMeta(air, AIR_KIND_KEY, value);
|
||||
return meta ? { _meta: meta } : {};
|
||||
};
|
||||
const modes: SessionModeState["availableModes"] = [
|
||||
{
|
||||
id: "default",
|
||||
name: "Manual",
|
||||
description: "Always ask before making changes",
|
||||
...kind("standard"),
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept edits",
|
||||
description: "Automatically accept all file edits",
|
||||
...kind("standard"),
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan",
|
||||
description: "Create a plan before making changes",
|
||||
...kind("plan"),
|
||||
},
|
||||
{
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Claude handles permission decisions",
|
||||
...kind("auto_review"),
|
||||
},
|
||||
];
|
||||
if (allowBypass) {
|
||||
modes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass permissions",
|
||||
description: "Accepts all permissions",
|
||||
...kind("full_access"),
|
||||
});
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private async trySyncMode(
|
||||
query: Pick<Query, "setPermissionMode">,
|
||||
mode: PermissionMode,
|
||||
@@ -406,3 +404,51 @@ export class SessionModeManager<S extends SessionMode> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildAvailableModes(
|
||||
{ allowBypass, allowAuto }: ModeAvailability,
|
||||
air = false,
|
||||
): SessionModeState["availableModes"] {
|
||||
// Only AIR gets the mode kind.
|
||||
const kind = (value: string) => {
|
||||
const meta = airOnlyMeta(air, AIR_KIND_KEY, value);
|
||||
return meta ? { _meta: meta } : {};
|
||||
};
|
||||
const modes: SessionModeState["availableModes"] = [
|
||||
{
|
||||
id: "default",
|
||||
name: "Manual",
|
||||
description: "Always ask before making changes",
|
||||
...kind("standard"),
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept edits",
|
||||
description: "Automatically accept all file edits",
|
||||
...kind("standard"),
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan",
|
||||
description: "Create a plan before making changes",
|
||||
...kind("plan"),
|
||||
},
|
||||
];
|
||||
if (allowAuto) {
|
||||
modes.push({
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Claude handles permission decisions",
|
||||
...kind("auto_review"),
|
||||
});
|
||||
}
|
||||
if (allowBypass) {
|
||||
modes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass permissions",
|
||||
description: "Accepts all permissions",
|
||||
...kind("full_access"),
|
||||
});
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,35 @@ export function noticeOrTranscriptUpdate(
|
||||
if (supportsNotices) return noticeUpdate(notice);
|
||||
return {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: transcriptText },
|
||||
content: { type: "text", text: transcriptBlock(transcriptText) },
|
||||
...(transcriptMeta ? { _meta: transcriptMeta } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Clients render consecutive agent_message_chunks as one markdown stream, so
|
||||
* a bare `**Notice:** …` chunk lands mid-sentence: "…all done.**Notice:** Stop
|
||||
* says: …". Put it in its own blockquote paragraph, blank lines on both sides,
|
||||
* so it starts on a new line and Claude's next chunk does not continue the
|
||||
* quote. */
|
||||
export function transcriptBlock(text: string): string {
|
||||
const lines = text.trim().split(/\r?\n/);
|
||||
return `\n\n${lines.map((line) => (line ? `> ${line}` : ">")).join("\n")}\n\n`;
|
||||
}
|
||||
|
||||
const HOOK_SPEAKER = /^[^\n:]{1,80} says: /;
|
||||
|
||||
/** Claude Code prefixes every line of a hook's `systemMessage` with
|
||||
* "<HookEvent> says: ", so a two-line message reads "Stop says: a\nStop says:
|
||||
* b". Keep the prefix on the first line only when every line repeats it. */
|
||||
export function collapseHookSpeaker(text: string): string {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const prefix = lines[0].match(HOOK_SPEAKER)?.[0];
|
||||
if (!prefix || lines.length < 2 || !lines.every((line) => line.startsWith(prefix))) {
|
||||
return text;
|
||||
}
|
||||
return [lines[0], ...lines.slice(1).map((line) => line.slice(prefix.length))].join("\n");
|
||||
}
|
||||
|
||||
/** Fragments that read on after a bold label stand alone as a notice
|
||||
* description, so start them with a capital. */
|
||||
export function sentenceCase(text: string): string {
|
||||
|
||||
@@ -25,7 +25,7 @@ export type UltracodeSettingsReport = {
|
||||
};
|
||||
|
||||
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
|
||||
// SDK's type declarations as of 0.3.316.
|
||||
// SDK's type declarations as of 0.3.319.
|
||||
type GetSettingsQuery = Query & {
|
||||
getSettings?: () => Promise<{
|
||||
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet 4.5",
|
||||
description: "Default",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -284,7 +285,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
it("drops escalating defaultMode when it comes from project-tier settings", async () => {
|
||||
// bypassPermissions in .claude/settings.json (a repo-committed tier) is
|
||||
// filtered by the SDK's trust policy and clamps to 'default'.
|
||||
// filtered by the SDK's trust policy, so the built-in default applies.
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(path.join(projectDir, ".claude"), { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
@@ -303,8 +304,8 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
expect(getCapturedOptions().permissionMode).toBe("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(response.modes.currentModeId).toBe("auto");
|
||||
});
|
||||
|
||||
it("honors the host opt-out from bypassPermissions", async () => {
|
||||
@@ -380,7 +381,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("defaults to 'default' when no permissions.defaultMode is set", async () => {
|
||||
it("defaults to 'auto' when no permissions.defaultMode is set", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
@@ -395,8 +396,37 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
expect(getCapturedOptions().permissionMode).toBe("default");
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(response.modes.currentModeId).toBe("auto");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "top-level disableAutoMode", settings: { disableAutoMode: "disable" } },
|
||||
{
|
||||
name: "permissions.disableAutoMode with defaultMode 'auto'",
|
||||
settings: { permissions: { disableAutoMode: "disable", defaultMode: "auto" } },
|
||||
},
|
||||
])("starts in 'default' without Auto in the catalog for $name", async ({ settings }) => {
|
||||
await fs.promises.writeFile(path.join(tempDir, "settings.json"), JSON.stringify(settings));
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).logger = { log: () => {}, error: () => {} };
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
expect(response.modes.availableModes.map((mode: { id: string }) => mode.id)).not.toContain(
|
||||
"auto",
|
||||
);
|
||||
});
|
||||
|
||||
describe("on resume", () => {
|
||||
@@ -464,6 +494,16 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(response.modes.currentModeId).toBe("plan");
|
||||
});
|
||||
|
||||
it("uses the available configured mode when the restored mode is not in the catalog", async () => {
|
||||
const { permissionMode, response } = await resume([user("dontAsk")]);
|
||||
|
||||
expect(permissionMode).toBe("plan");
|
||||
expect(response.modes.currentModeId).toBe("plan");
|
||||
expect(response.configOptions.find((option: any) => option.id === "mode").currentValue).toBe(
|
||||
"plan",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses permissions.defaultMode when bypass is not available now", async () => {
|
||||
const { permissionMode, response } = await resume([user("bypassPermissions")], {
|
||||
claudeCode: { options: { allowDangerouslySkipPermissions: false } },
|
||||
@@ -536,7 +576,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to 'default' when permissions.defaultMode is invalid", async () => {
|
||||
it("falls back to the built-in default when permissions.defaultMode is invalid", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
@@ -560,9 +600,9 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Bad mode is ignored at the usage site; session creation must not throw.
|
||||
expect(getCapturedOptions().permissionMode).toBe("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
// Settings resolution drops the bad mode; session creation must not throw.
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(response.modes.currentModeId).toBe("auto");
|
||||
});
|
||||
|
||||
it("ignores model from settings when it is not a string", async () => {
|
||||
@@ -704,6 +744,30 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(modeIds).not.toContain("dontAsk");
|
||||
});
|
||||
|
||||
it("starts the built-in Auto default in Manual on an unsupported model", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { setPermissionModeSpy } = mockQueryWithModels([
|
||||
{ value: "claude-haiku-4-5", displayName: "Claude Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
expect((agent as any).sessions[response.sessionId].autoModeFallbackWarningPending).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back permissions.defaultMode='auto' to Accept edits on an unsupported model", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
isSyntheticLoginMessage,
|
||||
stripLocalCommandMetadata,
|
||||
ClaudeAcpAgent,
|
||||
claudeCliPath,
|
||||
computeSessionFingerprint,
|
||||
streamEventToAcpNotifications,
|
||||
messageIdForGrouping,
|
||||
@@ -56,6 +55,7 @@ import {
|
||||
type StreamedToolInputCache,
|
||||
} from "../acp-agent.js";
|
||||
import { SessionTitles } from "../session-titles.js";
|
||||
import { SessionBackgroundWork } from "../background-work.js";
|
||||
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "../usage-markdown.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { heldRequest, initializeClient, makeMockQuery } from "./helpers.js";
|
||||
@@ -4935,11 +4935,6 @@ describe("prompt conversion", () => {
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("SDK behavior", () => {
|
||||
it("finds vendored cli path", async () => {
|
||||
const path = await claudeCliPath();
|
||||
expect(path).toMatch(/@anthropic-ai\/claude-agent-sdk-[^/]+\/claude(\.exe)?$/);
|
||||
});
|
||||
|
||||
it("query has a 'default' model", async () => {
|
||||
const q = query({ prompt: "hi" });
|
||||
const models = await q.supportedModels();
|
||||
@@ -5201,6 +5196,7 @@ describe("permission request cancellation", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork(sessionId, false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -13862,7 +13858,7 @@ describe("MCP server startup failures", () => {
|
||||
);
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0].content.text).toBe(
|
||||
'**MCP server "client-fs" failed to start:** Its tools are unavailable in this session. Run /mcp for the error.',
|
||||
'\n\n> **MCP server "client-fs" failed to start:** Its tools are unavailable in this session. Run /mcp for the error.\n\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14998,6 +14994,7 @@ describe("session/close", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork(sessionId, false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -15120,6 +15117,7 @@ describe("session/delete", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork(sessionId, false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -15283,6 +15281,7 @@ describe("getOrCreateSession param change detection", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork(sessionId, false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -18611,8 +18610,8 @@ describe("assembled assistant text fallback", () => {
|
||||
]);
|
||||
expect(child.some((update) => update.sessionUpdate === "compaction_update")).toBe(false);
|
||||
|
||||
// Unrecognized framing: the text stays visible rather than vanishing
|
||||
// behind a summary-less entity.
|
||||
// Unrecognized framing: still the compaction isCompactSummary says it is,
|
||||
// without summary text, and never a message from the user.
|
||||
const unknownFraming = await replay(true, [
|
||||
{
|
||||
...summaryMessage("odd-summary"),
|
||||
@@ -18623,12 +18622,14 @@ describe("assembled assistant text fallback", () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(unknownFraming.some((update) => update.sessionUpdate === "compaction_update")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(unknownFraming.some((update) => update.sessionUpdate === "user_message_chunk")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(unknownFraming).toEqual([
|
||||
{
|
||||
sessionUpdate: "compaction_update",
|
||||
compactionId: "odd-summary",
|
||||
status: "completed",
|
||||
_meta: { jetbrains: { air: { version: 1, contextCompaction: { version: 1 } } } },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves the model response after multiple compactions in one turn", async () => {
|
||||
@@ -18849,12 +18850,44 @@ describe("assembled assistant text fallback", () => {
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
|
||||
const chunks = updates.filter(({ update }) => update.sessionUpdate === "agent_message_chunk");
|
||||
expect(messageChunkTexts(updates)).toEqual(["**Warning:** hook says no"]);
|
||||
expect(messageChunkTexts(updates)).toEqual(["\n\n> **Warning:** hook says no\n\n"]);
|
||||
expect(chunks[0]?.update).toMatchObject({
|
||||
_meta: { claudeCode: { kind: "informational", level: "warning" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("logs session_writes_lost after the last result without sending an update", async () => {
|
||||
const updates: any[] = [];
|
||||
const error = vi.fn();
|
||||
const agent = new ClaudeAcpAgent(
|
||||
{ sessionUpdate: async (n: any) => updates.push(n) } as unknown as AcpClient,
|
||||
{ log: () => {}, error },
|
||||
);
|
||||
injectSession(agent, [
|
||||
replayedResult("done"),
|
||||
idle,
|
||||
{
|
||||
type: "system",
|
||||
subtype: "session_writes_lost",
|
||||
error_code: "ENOSPC",
|
||||
lost: ["transcript", "global_config"],
|
||||
transcript_session_ids: ["test-session"],
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
},
|
||||
]);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
const sentAtTurnEnd = updates.length;
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
"Session test-session: Claude Code could not save transcript and global_config (ENOSPC); resuming may miss part of the conversation",
|
||||
),
|
||||
);
|
||||
expect(updates).toHaveLength(sentAtTurnEnd);
|
||||
});
|
||||
|
||||
it("marks info-level notices even when their text has no severity prefix", async () => {
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
@@ -18872,12 +18905,33 @@ describe("assembled assistant text fallback", () => {
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
|
||||
const chunks = updates.filter(({ update }) => update.sessionUpdate === "agent_message_chunk");
|
||||
expect(messageChunkTexts(updates)).toEqual(["plain hook detail"]);
|
||||
expect(messageChunkTexts(updates)).toEqual(["\n\n> plain hook detail\n\n"]);
|
||||
expect(chunks[0]?.update).toMatchObject({
|
||||
_meta: { claudeCode: { kind: "informational", level: "info" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a multi-line hook notice as one quote with a single speaker prefix", async () => {
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
{
|
||||
type: "system",
|
||||
subtype: "informational",
|
||||
content: "Stop says: token: 3 calls\nStop says: hand off now",
|
||||
level: "notice",
|
||||
session_id: "test-session",
|
||||
},
|
||||
result(),
|
||||
idle,
|
||||
]);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
|
||||
|
||||
expect(messageChunkTexts(updates)).toEqual([
|
||||
"\n\n> **Notice:** Stop says: token: 3 calls\n> hand off now\n\n",
|
||||
]);
|
||||
});
|
||||
|
||||
it("delivers an informational frame as a notice and still owns the result text", async () => {
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
await initializeClient(agent, { session: { notices: {} } });
|
||||
@@ -19764,6 +19818,7 @@ describe("post-error recovery", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork("test-session", false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -25919,6 +25974,82 @@ describe("deferred settlement for live background subagents (issues #864/#866)",
|
||||
]);
|
||||
});
|
||||
|
||||
// Issue #1453: a previous cycle's trailing idle completed the next turn task.
|
||||
it.each(["result", "idle"] as const)(
|
||||
"keeps the next turn task open across an owed idle until its own %s",
|
||||
async (end) => {
|
||||
const { agent, asyncTaskUpdates } = await airAgent();
|
||||
let wake = () => {};
|
||||
const woken = new Promise<void>((resolve) => (wake = resolve));
|
||||
let finish = () => {};
|
||||
const finished = new Promise<void>((resolve) => (finish = resolve));
|
||||
injectGeneratorSession(agent, (input) => {
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
yield userEcho((await iter.next()).value);
|
||||
yield running();
|
||||
yield resultMessage();
|
||||
yield idle();
|
||||
await woken;
|
||||
yield running();
|
||||
yield assistantText("first autonomous cycle");
|
||||
yield resultMessage({ origin: { kind: "task-notification" } });
|
||||
// The CLI stays running: the next output opens a new turn task.
|
||||
yield assistantText("next autonomous cycle");
|
||||
yield idle(); // The previous autonomous result's late trailer.
|
||||
await finished;
|
||||
yield assistantText("still working in the next cycle");
|
||||
if (end === "result") {
|
||||
yield resultMessage({ origin: { kind: "task-notification" } });
|
||||
}
|
||||
yield idle();
|
||||
}
|
||||
return messageGenerator();
|
||||
});
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
wake();
|
||||
try {
|
||||
await waitFor(
|
||||
() =>
|
||||
asyncTaskUpdates().filter((u) => u.sessionUpdate === "async_task_spawned").length ===
|
||||
2 && agent.sessions["test-session"].owedTrailingIdles === 0,
|
||||
);
|
||||
const [first, firstEnd, next] = asyncTaskUpdates();
|
||||
expect(asyncTaskUpdates()).toEqual([
|
||||
first,
|
||||
{
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: first.asyncTaskId,
|
||||
state: "completed",
|
||||
},
|
||||
next,
|
||||
]);
|
||||
expect(firstEnd.state).toBe("completed");
|
||||
expect(next.sessionUpdate).toBe("async_task_spawned");
|
||||
expect(next.asyncTaskId).not.toBe(first.asyncTaskId);
|
||||
} finally {
|
||||
finish();
|
||||
await agent.sessions["test-session"]?.consumer;
|
||||
}
|
||||
const [first, , next] = asyncTaskUpdates();
|
||||
expect(asyncTaskUpdates()).toEqual([
|
||||
first,
|
||||
{
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: first.asyncTaskId,
|
||||
state: "completed",
|
||||
},
|
||||
next,
|
||||
{
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: next.asyncTaskId,
|
||||
state: "completed",
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
describe("started by a scheduler fire", () => {
|
||||
const lifecycleStarted = (fire: { wakeup_source?: string; scheduled_for?: number }) =>
|
||||
({
|
||||
@@ -28199,6 +28330,7 @@ describe("session/cancel wedge recovery (issue #680)", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork("test-session", false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -30107,6 +30239,7 @@ describe("agent selection config option", () => {
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork(sessionId, false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
@@ -30172,6 +30305,61 @@ describe("agent selection config option", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rate_limit_event", () => {
|
||||
it("rides on the next usage_update when it comes before the turn's first assistant frame", async () => {
|
||||
const updates: SessionNotification[] = [];
|
||||
const agent = new ClaudeAcpAgent(
|
||||
{
|
||||
sessionUpdate: async (notification: SessionNotification) => {
|
||||
updates.push(notification);
|
||||
},
|
||||
} as unknown as AcpClient,
|
||||
{ log: () => {}, error: () => {} },
|
||||
);
|
||||
const rateLimit = { status: "allowed", resetsAt: 1700000000, rateLimitType: "seven_day" };
|
||||
const usage = {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
};
|
||||
injectGeneratorSession(agent, async function* (input) {
|
||||
const { value } = await input[Symbol.asyncIterator]().next();
|
||||
yield userEcho(value);
|
||||
yield {
|
||||
type: "rate_limit_event",
|
||||
rate_limit_info: rateLimit,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
};
|
||||
yield {
|
||||
type: "assistant",
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
message: {
|
||||
id: "msg_rate",
|
||||
role: "assistant",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
usage,
|
||||
},
|
||||
};
|
||||
yield successfulResultMessage({ usage });
|
||||
yield { type: "system", subtype: "session_state_changed", state: "idle" };
|
||||
});
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
const usageMetas = updates
|
||||
.map((notification) => notification.update)
|
||||
.filter((update) => update.sessionUpdate === "usage_update")
|
||||
.map((update) => update._meta?.["_claude/rateLimit"]);
|
||||
expect(usageMetas[0]).toEqual(rateLimit);
|
||||
expect(usageMetas.slice(1)).toEqual(usageMetas.slice(1).map(() => undefined));
|
||||
});
|
||||
});
|
||||
|
||||
describe("the SDK session id of a fresh Claude context", () => {
|
||||
// After a clear-context restart the SDK runs under a new session id, and
|
||||
// every SDK message carries it. The client knows only the ACP session id.
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
{"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":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","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":"auto"},"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"}}}
|
||||
|
||||
@@ -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":"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":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","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":"auto"},"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"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
{"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":"session_info_update","title":"Fix the app","updatedAt":"2026-01-01T00:00:00.000Z"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"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","type":"command"}],"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"}]}}
|
||||
{"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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"}]}}
|
||||
|
||||
@@ -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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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>"}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{"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":"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","type":"command"}],"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":"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","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","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":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Edit"}},"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>","update":{"_meta":{"claudeCode":{"toolName":"Task"}},"content":[{"content":{"text":"Report.","type":"text"},"type":"content"}],"rawOutput":"Report.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_task"}}}
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
|
||||
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{"kind":"initialize","payload":{"_meta":{"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","recommendedValue"],"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":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
|
||||
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Edit"}},"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>","update":{"_meta":{"claudeCode":{"toolName":"Task"}},"content":[{"content":{"text":"Report.","type":"text"},"type":"content"}],"rawOutput":"Report.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_task"}}}
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
|
||||
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{"kind":"initialize","payload":{"_meta":{"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","recommendedValue"],"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":[{"_meta":{"terminal-auth":{"args":["<argv>","--cli","auth","login","--claudeai"],"command":"<executable>","label":"Claude Login"}},"args":["--cli","auth","login","--claudeai"],"description":"Use Claude subscription ","id":"claude-ai-login","name":"Claude Subscription","type":"terminal"},{"_meta":{"terminal-auth":{"args":["<argv>","--cli","auth","login","--console"],"command":"<executable>","label":"Anthropic Console Login"}},"args":["--cli","auth","login","--console"],"description":"Use Anthropic Console (API usage billing)","id":"console-login","name":"Anthropic Console","type":"terminal"}],"protocolVersion":1}}
|
||||
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
|
||||
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
|
||||
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* schema, so a malformed v2 message fails the test.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -615,6 +615,40 @@ describe("ACP v2 sessions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resumes an unavailable mode using the configured mode in the v2 response", async () => {
|
||||
const project = path.join(tempDir, "claude", "projects", "-project");
|
||||
await mkdir(project, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(tempDir, "claude", "settings.json"),
|
||||
JSON.stringify({ permissions: { defaultMode: "plan" } }),
|
||||
);
|
||||
const sessionId = randomUUID();
|
||||
await writeFile(
|
||||
path.join(project, `${sessionId}.jsonl`),
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
uuid: randomUUID(),
|
||||
parentUuid: null,
|
||||
sessionId,
|
||||
cwd,
|
||||
isSidechain: false,
|
||||
origin: { kind: "human" },
|
||||
message: { role: "user", content: "hello" },
|
||||
permissionMode: "dontAsk",
|
||||
}) + "\n",
|
||||
);
|
||||
const { app, authUpdate } = v2Client();
|
||||
await app.connectWith(connectRouter(), async (agent) => {
|
||||
await initializeV2(agent);
|
||||
const response = await agent.request(v2.methods.agent.session.resume, { sessionId, cwd });
|
||||
expect(sdk.queryOptions.at(-1)?.permissionMode).toBe("plan");
|
||||
expect(
|
||||
response.configOptions?.find((option) => option.configId === "mode")?.currentValue,
|
||||
).toBe("plan");
|
||||
await authUpdate(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("sets a config option with a v2 value and reports modes only as config options", async () => {
|
||||
const client = v2Client();
|
||||
const { response, updatesBeforeResponse } = await client.app.connectWith(
|
||||
@@ -2695,6 +2729,78 @@ describe("ACP v2 prompts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ACP v2 idle compaction", () => {
|
||||
/** A main-thread request that wrote 250k tokens to the one-hour cache. */
|
||||
const largeOneHourRequest = (options: Options) => ({
|
||||
...assistantText(options, "Done."),
|
||||
message: {
|
||||
...assistantText(options, "Done.").message,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_creation_input_tokens: 250_000,
|
||||
cache_creation: { ephemeral_1h_input_tokens: 250_000, ephemeral_5m_input_tokens: 0 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** The session is idle for 54 minutes after a large one-hour request, and the adapter's `/compact` runs. */
|
||||
it("reports the compaction that the adapter starts as running, then idle, with no user_message", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
|
||||
try {
|
||||
const received: string[] = [];
|
||||
sdk.run = async function* (prompts, options) {
|
||||
const input = prompts[Symbol.asyncIterator]();
|
||||
for (;;) {
|
||||
const { value, done } = await input.next();
|
||||
if (done) return;
|
||||
received.push((value.message.content as Array<{ text: string }>)[0].text);
|
||||
if (received.length === 1) {
|
||||
yield {
|
||||
type: "user",
|
||||
message: value.message,
|
||||
parent_tool_use_id: null,
|
||||
uuid: value.uuid,
|
||||
session_id: options.sessionId,
|
||||
isReplay: true,
|
||||
};
|
||||
yield largeOneHourRequest(options);
|
||||
} else {
|
||||
yield { type: "system", subtype: "status", status: "compacting", uuid: randomUUID() };
|
||||
}
|
||||
yield result(options);
|
||||
yield idleState(options);
|
||||
}
|
||||
};
|
||||
const client = v2Client();
|
||||
await client.app.connectWith(connectRouter(), async (agent) => {
|
||||
await initializeV2(agent);
|
||||
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
|
||||
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(54 * 60_000);
|
||||
for (let i = 0; i < 20 && received.length < 2; i++) await vi.advanceTimersByTimeAsync(0);
|
||||
for (let i = 0; i < 20 && turnTrace(client.sessionUpdates).length < 6; i++) {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
}
|
||||
await client.authUpdate(1);
|
||||
});
|
||||
|
||||
expect(received).toEqual(["hi", "/compact"]);
|
||||
expect(turnTrace(client.sessionUpdates)).toEqual([
|
||||
"user_message",
|
||||
"running",
|
||||
"agent_message_chunk",
|
||||
"idle end_turn",
|
||||
"running",
|
||||
"idle end_turn",
|
||||
]);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Claude Code runs model cycles that no prompt started, for example its
|
||||
// follow-up after a background command finished. v2 reports them as
|
||||
// foreground work.
|
||||
@@ -4404,10 +4510,13 @@ describe("ACP v2 requests that subagents make", () => {
|
||||
description: "make test",
|
||||
tool_use_id: "toolu_bg",
|
||||
parent_task_id: "agent-1",
|
||||
owned_by_subagent: true,
|
||||
is_backgrounded: true,
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
// As seen live (CLI 2.1.318): a subagent's tool result has no
|
||||
// `tool_use_result`, so only the `task_started` above names the task.
|
||||
yield childMessage(options, {
|
||||
type: "user",
|
||||
message: {
|
||||
@@ -4420,13 +4529,6 @@ describe("ACP v2 requests that subagents make", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
tool_use_result: {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
interrupted: false,
|
||||
isImage: false,
|
||||
backgroundTaskId: "bg1",
|
||||
},
|
||||
});
|
||||
yield taskEnd(options, "agent-1", "toolu_agent-1");
|
||||
yield level(options, [bash]);
|
||||
@@ -5598,6 +5700,78 @@ describe("ACP v2 terminal of a command in the background", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads the command's output file into its terminal, and the rest before its exit", async () => {
|
||||
const dir = await mkdtemp(path.join(os.tmpdir(), "background-output-"));
|
||||
const outputFile = path.join(dir, "tasks", "bg1.output");
|
||||
await mkdir(path.dirname(outputFile));
|
||||
await writeFile(outputFile, "compiling\n");
|
||||
const ended = Promise.withResolvers<void>();
|
||||
scriptTurns(async function* (options) {
|
||||
yield level(options, [{}]);
|
||||
yield taskStarted(options);
|
||||
yield bashUse(options);
|
||||
// The Bash result names the file, as Claude Code writes it.
|
||||
const bashResult = backgrounded(options);
|
||||
yield {
|
||||
...bashResult,
|
||||
message: {
|
||||
...bashResult.message,
|
||||
content: [
|
||||
{
|
||||
...bashResult.message.content[0],
|
||||
content: `Command running in background with ID: bg1. Output is being written to: ${outputFile}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
yield result(options);
|
||||
yield idleState(options);
|
||||
await ended.promise;
|
||||
yield* taskEnded(options);
|
||||
yield level(options, []);
|
||||
});
|
||||
const output = (client: ReturnType<typeof v2Client>) =>
|
||||
client.sessionUpdates.flatMap(({ update }) => {
|
||||
if (update.sessionUpdate === "terminal_output_chunk") {
|
||||
return [`chunk ${Buffer.from(String(update.data), "base64").toString()}`];
|
||||
}
|
||||
if (update.sessionUpdate !== "terminal_update") return [];
|
||||
return [
|
||||
...(update.output
|
||||
? [
|
||||
`output ${Buffer.from((update.output as { data: string }).data, "base64").toString()}`,
|
||||
]
|
||||
: []),
|
||||
...(update.exitStatus ? ["exit"] : []),
|
||||
];
|
||||
});
|
||||
try {
|
||||
const client = await promptUntil("idle end_turn [toolu_bg]", async (_, __, client) => {
|
||||
await vi.waitFor(() => expect(output(client)).toContain("output compiling\n"), {
|
||||
timeout: 2000,
|
||||
});
|
||||
await appendFile(outputFile, "tests passed\n");
|
||||
await vi.waitFor(() => expect(output(client)).toContain("chunk tests passed\n"), {
|
||||
timeout: 2000,
|
||||
});
|
||||
// The command writes its last line as it ends.
|
||||
await appendFile(outputFile, "done\n");
|
||||
ended.resolve();
|
||||
await vi.waitFor(() => expect(backgroundTrace(client)).toContain("idle []"));
|
||||
});
|
||||
|
||||
// The file's first text replaces the Bash result's placeholder.
|
||||
expect(output(client).slice(-4)).toEqual([
|
||||
"output compiling\n",
|
||||
"chunk tests passed\n",
|
||||
"chunk done\n",
|
||||
"exit",
|
||||
]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("ends the listed terminals when the query ends, as its background tasks end with it", async () => {
|
||||
scriptTurns(async function* (options) {
|
||||
yield level(options, [{}]);
|
||||
@@ -5701,10 +5875,13 @@ describe("ACP v2 terminal of a command in the background", () => {
|
||||
});
|
||||
|
||||
describe("a task that a tool call started", () => {
|
||||
// Claude Code marks every live-update watcher ambient, a requested
|
||||
// Monitor's task included.
|
||||
const monitor = {
|
||||
task_id: "mon1",
|
||||
task_type: "local_monitor",
|
||||
description: "Watch the logs",
|
||||
ambient: true,
|
||||
};
|
||||
const workflow = { task_id: "wf1", task_type: "local_workflow", description: "Review" };
|
||||
/** A Monitor call whose task `mon1` runs on after the call returns. The
|
||||
@@ -5910,25 +6087,53 @@ describe("ACP v2 terminal of a command in the background", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists a task without such a call as a _claude/task item", async () => {
|
||||
it("lists a task whose call returned before the task started as a _claude/task item", async () => {
|
||||
scriptTurns(async function* (options) {
|
||||
yield level(options, [monitor]);
|
||||
// No tool call started it, such as a task that a hook started.
|
||||
yield taskEvent(options, "task_started", monitor);
|
||||
yield toolUseMessage(options, "toolu_mcp", "mcp__ci__watch", { job: "build" });
|
||||
yield toolResultMessage(options, "toolu_mcp", { content: "Watching the build." });
|
||||
// The call's task joins the level and starts after the call returned,
|
||||
// so the call ended and cannot be listed.
|
||||
const task = { task_id: "mcp1", task_type: "mcp_task", description: "Watch the build" };
|
||||
yield level(options, [task]);
|
||||
yield taskEvent(options, "task_started", { ...task, tool_use_id: "toolu_mcp" });
|
||||
yield result(options);
|
||||
yield idleState(options);
|
||||
});
|
||||
const client = await promptUntil("idle end_turn [_claude/task]");
|
||||
|
||||
expect(backgroundTrace(client)).toEqual([
|
||||
"running []",
|
||||
"running [_claude/task]",
|
||||
"idle end_turn [_claude/task]",
|
||||
]);
|
||||
expect(client.updates("state_update").at(-1)).toMatchObject({
|
||||
background: [{ type: "_claude/task", taskId: "mon1", description: "Watch the logs" }],
|
||||
background: [{ type: "_claude/task", taskId: "mcp1", description: "Watch the build" }],
|
||||
});
|
||||
});
|
||||
|
||||
// No tool call started these, such as tasks that a hook started.
|
||||
it.each([
|
||||
{
|
||||
task: workflow,
|
||||
trace: ["running []", "running [_claude/task]", "idle end_turn [_claude/task]"],
|
||||
background: [{ type: "_claude/task", taskId: "wf1", description: "Review" }],
|
||||
},
|
||||
// A watcher Claude Code started by itself is housekeeping.
|
||||
{
|
||||
task: monitor,
|
||||
trace: ["running []", "idle end_turn []"],
|
||||
background: [],
|
||||
},
|
||||
])(
|
||||
"lists a $task.task_type task without such a call unless it is ambient",
|
||||
async ({ task, trace, background }) => {
|
||||
scriptTurns(async function* (options) {
|
||||
yield level(options, [task]);
|
||||
yield taskEvent(options, "task_started", task);
|
||||
yield result(options);
|
||||
yield idleState(options);
|
||||
});
|
||||
const client = await promptUntil(trace.at(-1)!);
|
||||
|
||||
expect(backgroundTrace(client)).toEqual(trace);
|
||||
expect(client.updates("state_update").at(-1)).toMatchObject({ background });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects terminal/cancel with -32602 when the command ends before Claude Code stops it", async () => {
|
||||
|
||||
24
src/tests/agent-command.test.ts
Normal file
24
src/tests/agent-command.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { agentCommand } from "../agent-command.js";
|
||||
|
||||
describe("agentCommand", () => {
|
||||
it.each([
|
||||
{
|
||||
runtime: "Node",
|
||||
argv: ["/usr/bin/node", "/opt/acp/dist/index.js", "--hide-claude-auth"],
|
||||
args: ["/opt/acp/dist/index.js", "--hide-claude-auth"],
|
||||
},
|
||||
{
|
||||
runtime: "a compiled executable",
|
||||
argv: ["bun", "/$bunfs/root/claude-agent-acp", "--hide-claude-auth"],
|
||||
args: ["--hide-claude-auth"],
|
||||
},
|
||||
{
|
||||
runtime: "a compiled Windows executable",
|
||||
argv: ["bun", "B:\\~BUN\\root\\claude-agent-acp.exe", "--hide-claude-auth"],
|
||||
args: ["--hide-claude-auth"],
|
||||
},
|
||||
])("reruns $runtime with its own flags", ({ argv, args }) => {
|
||||
expect(agentCommand(argv)).toEqual({ command: process.execPath, args });
|
||||
});
|
||||
});
|
||||
@@ -168,7 +168,10 @@ describe("AsyncTaskRuntime", () => {
|
||||
expect(published[1]?.update).not.toHaveProperty("summary");
|
||||
expect(published.at(-1)?.update).toMatchObject({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Task stopped by user:** Build generated assets." },
|
||||
content: {
|
||||
type: "text",
|
||||
text: "\n\n> **Task stopped by user:** Build generated assets.\n\n",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,7 +237,7 @@ describe("AsyncTaskRuntime", () => {
|
||||
]);
|
||||
expect(published.at(-1)?.update).toMatchObject({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Task stopped by user:** npm run build." },
|
||||
content: { type: "text", text: "\n\n> **Task stopped by user:** npm run build.\n\n" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +267,7 @@ describe("AsyncTaskRuntime", () => {
|
||||
});
|
||||
expect(published.at(-1)?.update).toMatchObject({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Task stopped by user:** npm run build." },
|
||||
content: { type: "text", text: "\n\n> **Task stopped by user:** npm run build.\n\n" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -306,6 +306,14 @@ describe("auth status mappers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps a setup-token CLI login without profile fields to an account", () => {
|
||||
expect(
|
||||
fromCliStatus(
|
||||
JSON.stringify({ loggedIn: true, authMethod: "oauth_token", apiProvider: "firstParty" }),
|
||||
),
|
||||
).toEqual({ kind: "account", label: "Claude subscription", detail: "OAuth token" });
|
||||
});
|
||||
|
||||
it("lets a third-party backend outrank the logged-out flag", () => {
|
||||
// `loggedIn` tracks the claude.ai login only; on Bedrock the credentials
|
||||
// are external and the agent is authenticated all the same.
|
||||
|
||||
129
src/tests/background-output.test.ts
Normal file
129
src/tests/background-output.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { appendFile, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES,
|
||||
BACKGROUND_OUTPUT_READ_BYTES,
|
||||
type BackgroundOutput,
|
||||
BackgroundOutputTail,
|
||||
} from "../background-output.js";
|
||||
|
||||
describe("BackgroundOutputTail", () => {
|
||||
let dir: string;
|
||||
let file: string;
|
||||
let published: BackgroundOutput[];
|
||||
let tail: BackgroundOutputTail | undefined;
|
||||
const follow = () =>
|
||||
(tail = new BackgroundOutputTail(file, async (output) => void published.push(output)));
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), "background-output-"));
|
||||
file = path.join(dir, "bg1.output");
|
||||
published = [];
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
tail?.stop();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("replaces the terminal's output with the file's first text, then appends what the file adds", async () => {
|
||||
await writeFile(file, "building\n");
|
||||
follow();
|
||||
await vi.waitFor(() => expect(published).toHaveLength(1), { timeout: 2000 });
|
||||
await appendFile(file, "done\n");
|
||||
await vi.waitFor(() => expect(published).toHaveLength(2), { timeout: 2000 });
|
||||
|
||||
expect(published).toEqual([
|
||||
{ text: "building\n", replaces: true },
|
||||
{ text: "done\n", replaces: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("waits for a file that does not exist yet", async () => {
|
||||
follow();
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
expect(published).toEqual([]);
|
||||
|
||||
await writeFile(file, "started\n");
|
||||
await vi.waitFor(() => expect(published).toHaveLength(1), { timeout: 2000 });
|
||||
expect(published[0]).toEqual({ text: "started\n", replaces: true });
|
||||
});
|
||||
|
||||
it("holds a character that a read splits until its last byte is written", async () => {
|
||||
const check = Buffer.from("✓", "utf8");
|
||||
await writeFile(file, check.subarray(0, 1));
|
||||
follow();
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
expect(published).toEqual([]);
|
||||
|
||||
await appendFile(file, check.subarray(1));
|
||||
await tail!.finish();
|
||||
expect(published).toEqual([{ text: "✓", replaces: true }]);
|
||||
});
|
||||
|
||||
it("reads the rest of the file on finish, and nothing after", async () => {
|
||||
await writeFile(file, "a");
|
||||
follow();
|
||||
await appendFile(file, "b");
|
||||
await tail!.finish();
|
||||
await appendFile(file, "c");
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
|
||||
expect(published.map((output) => output.text).join("")).toBe("ab");
|
||||
});
|
||||
|
||||
it("sends at most the backlog, read by read, from the file's last bytes as a snapshot", async () => {
|
||||
// More than the backlog, written faster than the polls read it.
|
||||
const skipped = "x".repeat(BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES);
|
||||
const kept = "y".repeat(BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES - 4) + "end\n";
|
||||
await writeFile(file, skipped + kept);
|
||||
follow();
|
||||
await tail!.finish();
|
||||
|
||||
expect(published.map((output) => output.text).join("")).toBe(kept);
|
||||
expect(published[0].replaces).toBe(true);
|
||||
expect(published.slice(1).every((output) => !output.replaces)).toBe(true);
|
||||
expect(
|
||||
published.every((output) => Buffer.byteLength(output.text) <= BACKGROUND_OUTPUT_READ_BYTES),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("starts over with a snapshot when the file shrinks", async () => {
|
||||
await writeFile(file, "old output\n");
|
||||
follow();
|
||||
await vi.waitFor(() => expect(published).toHaveLength(1), { timeout: 2000 });
|
||||
await writeFile(file, "new\n");
|
||||
await tail!.finish();
|
||||
|
||||
expect(published).toEqual([
|
||||
{ text: "old output\n", replaces: true },
|
||||
{ text: "new\n", replaces: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces the placeholder with an empty output when the command wrote nothing", async () => {
|
||||
await writeFile(file, "");
|
||||
follow();
|
||||
await tail!.finish();
|
||||
|
||||
expect(published).toEqual([{ text: "", replaces: true }]);
|
||||
});
|
||||
|
||||
it("publishes nothing on finish when the file never appeared", async () => {
|
||||
follow();
|
||||
await tail!.finish();
|
||||
|
||||
expect(published).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads nothing after stop", async () => {
|
||||
follow();
|
||||
tail!.stop();
|
||||
await writeFile(file, "late\n");
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
|
||||
expect(published).toEqual([]);
|
||||
});
|
||||
});
|
||||
57
src/tests/claude-cli.test.ts
Normal file
57
src/tests/claude-cli.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Fresh module state per test: the bundled CLI directory is process-wide.
|
||||
async function load() {
|
||||
vi.resetModules();
|
||||
return import("../claude-cli.js");
|
||||
}
|
||||
|
||||
describe("claudeCliPath", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), "claude-cli-"));
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const cliFileName = process.platform === "win32" ? "claude.exe" : "claude";
|
||||
|
||||
it("prefers CLAUDE_CODE_EXECUTABLE over the bundled CLI", async () => {
|
||||
const { claudeCliPath, useBundledClaudeCli } = await load();
|
||||
writeFileSync(join(dir, cliFileName), "");
|
||||
useBundledClaudeCli(dir);
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/opt/claude");
|
||||
|
||||
expect(await claudeCliPath()).toBe("/opt/claude");
|
||||
});
|
||||
|
||||
it("returns the CLI next to the executable", async () => {
|
||||
const { claudeCliPath, useBundledClaudeCli } = await load();
|
||||
writeFileSync(join(dir, cliFileName), "");
|
||||
useBundledClaudeCli(dir);
|
||||
|
||||
expect(await claudeCliPath()).toBe(join(dir, cliFileName));
|
||||
});
|
||||
|
||||
it("throws when the CLI is missing next to the executable", async () => {
|
||||
const { claudeCliPath, useBundledClaudeCli } = await load();
|
||||
useBundledClaudeCli(dir);
|
||||
|
||||
await expect(claudeCliPath()).rejects.toThrow(
|
||||
`The Claude CLI is missing: expected ${join(dir, cliFileName)}. Keep ${cliFileName} from the release archive`,
|
||||
);
|
||||
});
|
||||
|
||||
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)("finds the installed CLI", async () => {
|
||||
const { claudeCliPath } = await load();
|
||||
expect(await claudeCliPath()).toMatch(/@anthropic-ai\/claude-agent-sdk-[^/]+\/claude(\.exe)?$/);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ContextCompactionLifecycle,
|
||||
clientSupportsCompactionUpdates,
|
||||
compactionSummaryText,
|
||||
contextCompactionMetadataFromBoundary,
|
||||
isCompactSummaryMessage,
|
||||
} from "../context-compaction.js";
|
||||
|
||||
@@ -129,6 +130,17 @@ describe("compactionSummaryText", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("strips the framing when other content sits before the Summary line", () => {
|
||||
expect(
|
||||
compactionSummaryText(
|
||||
"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n" +
|
||||
"<system-reminder>\n<tools-used-before-summary>Read, Bash</tools-used-before-summary>\n</system-reminder>\n" +
|
||||
"Summary:\n1. Primary Request and Intent:\n Count upward.\n\n" +
|
||||
"Continue the conversation from where it left off without asking the user any further questions.",
|
||||
),
|
||||
).toBe("1. Primary Request and Intent:\n Count upward.");
|
||||
});
|
||||
|
||||
it("strips the older tagged transcript framing", () => {
|
||||
expect(
|
||||
compactionSummaryText(
|
||||
@@ -635,3 +647,20 @@ describe("ContextCompactionLifecycle (tool_call)", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("contextCompactionMetadataFromBoundary", () => {
|
||||
/** An adapter-sent compaction is `idle` only when the CLI calls it manual; the CLI's own auto trigger stays `automatic`. */
|
||||
it.each([
|
||||
{ cli: "manual", idleCompaction: false, expected: "manual" },
|
||||
{ cli: "manual", idleCompaction: true, expected: "idle" },
|
||||
{ cli: "auto", idleCompaction: true, expected: "automatic" },
|
||||
] as const)(
|
||||
"reports $expected for a $cli boundary (idleCompaction: $idleCompaction)",
|
||||
({ cli, idleCompaction, expected }) => {
|
||||
expect(
|
||||
contextCompactionMetadataFromBoundary({ trigger: cli, pre_tokens: 1 }, { idleCompaction })
|
||||
.trigger,
|
||||
).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,7 +413,7 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "**Fast mode turned off:** requires extra usage to be enabled for this account.",
|
||||
text: "\n\n> **Fast mode turned off:** requires extra usage to be enabled for this account.\n\n",
|
||||
},
|
||||
});
|
||||
expect(sessionUpdates[1].update).toMatchObject({ sessionUpdate: "config_option_update" });
|
||||
|
||||
555
src/tests/idle-compaction-agent.test.ts
Normal file
555
src/tests/idle-compaction-agent.test.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* Idle compaction wired into the agent (see `idle-compaction.ts` for the
|
||||
* timer's own rules): a scripted Claude Code answers each prompt it receives,
|
||||
* and fake timers move the clock.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
|
||||
import { initializeClient } from "./helpers.js";
|
||||
import { successfulResultMessage } from "./session-doubles.js";
|
||||
|
||||
const execFileSpy = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
||||
return { ...actual, execFile: execFileSpy };
|
||||
});
|
||||
|
||||
/** What the mocked Claude Agent SDK answers. Reset before each test. */
|
||||
const sdk = vi.hoisted(() => ({
|
||||
run: async function* (
|
||||
_prompts: AsyncIterable<SDKUserMessage>,
|
||||
_options: Options,
|
||||
): AsyncGenerator<Record<string, unknown>> {},
|
||||
models: [
|
||||
{ value: "claude-sonnet-4-6", displayName: "Claude Sonnet", description: "Fast" },
|
||||
{ value: "claude-opus-4-6", displayName: "Claude Opus", description: "Smart" },
|
||||
] as Array<Record<string, unknown>>,
|
||||
}));
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
);
|
||||
const { makeMockQuery } = await import("./helpers.js");
|
||||
return {
|
||||
...actual,
|
||||
query: ({ prompt, options }: { prompt: AsyncIterable<SDKUserMessage>; options: Options }) => {
|
||||
const messages = sdk.run(prompt, options);
|
||||
const controls = makeMockQuery({
|
||||
[Symbol.asyncIterator]: () => messages,
|
||||
initializationResult: async () => ({ models: sdk.models }),
|
||||
});
|
||||
return Object.assign(messages, controls);
|
||||
},
|
||||
getSessionMessages: vi.fn(async () => []),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
const MINUTE_MS = 60_000;
|
||||
const AIR_CLIENT_CAPABILITIES = {
|
||||
_meta: { jetbrains: { air: { version: 1, capabilities: [] as string[] } } },
|
||||
};
|
||||
|
||||
/** As the CLI reports a request whose prompt cache lives one hour: 250k tokens of context. */
|
||||
const LARGE_ONE_HOUR_USAGE = {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 250_000,
|
||||
cache_creation: { ephemeral_1h_input_tokens: 250_000, ephemeral_5m_input_tokens: 0 },
|
||||
};
|
||||
|
||||
let tempDir: string;
|
||||
let cwd: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
execFileSpy.mockImplementation((...invocation: unknown[]) => {
|
||||
const callback = invocation[invocation.length - 1] as (...a: unknown[]) => void;
|
||||
callback(null, { stdout: "", stderr: "" });
|
||||
});
|
||||
tempDir = await mkdtemp(path.join(os.tmpdir(), "idle-compaction-"));
|
||||
cwd = await mkdtemp(path.join(tempDir, "project-"));
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", path.join(tempDir, "claude"));
|
||||
vi.stubEnv("CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS", "");
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
execFileSpy.mockReset();
|
||||
vi.unstubAllEnvs();
|
||||
await rm(tempDir, { recursive: true, force: true, maxRetries: 10 });
|
||||
});
|
||||
|
||||
function assistant(
|
||||
options: Options,
|
||||
usage: Record<string, unknown>,
|
||||
parent: string | null = null,
|
||||
id = `msg_${randomUUID()}`,
|
||||
) {
|
||||
return {
|
||||
type: "assistant",
|
||||
message: {
|
||||
id,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: "claude-sonnet-4-6",
|
||||
content: [{ type: "text", text: "Done." }],
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage,
|
||||
},
|
||||
parent_tool_use_id: parent,
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
function system(options: Options, subtype: string, fields: Record<string, unknown>) {
|
||||
return { type: "system", subtype, ...fields, uuid: randomUUID(), session_id: options.sessionId };
|
||||
}
|
||||
|
||||
/** The `user` message Claude Code replays for a prompt it took in. */
|
||||
function echo(value: SDKUserMessage, options: Options) {
|
||||
return {
|
||||
type: "user",
|
||||
message: value.message,
|
||||
parent_tool_use_id: null,
|
||||
uuid: value.uuid,
|
||||
session_id: options.sessionId,
|
||||
isReplay: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Code takes in each prompt in order. A prompt answers with one
|
||||
* main-thread request (with `usage`, after `during`) and goes idle; `/compact`
|
||||
* answers with a compaction, as the CLI reports a manual one, and goes idle.
|
||||
* `received` lists the texts that Claude Code took in.
|
||||
*/
|
||||
function scriptClaudeCode({
|
||||
usage = LARGE_ONE_HOUR_USAGE,
|
||||
during,
|
||||
compactResult = "success",
|
||||
suggestion,
|
||||
responseTakesMs,
|
||||
}: {
|
||||
usage?: Record<string, unknown>;
|
||||
during?: (options: Options) => AsyncGenerator<any>;
|
||||
compactResult?: "success" | "failed";
|
||||
/** The prompt the CLI predicts after each answered prompt, as it does after a result. */
|
||||
suggestion?: string;
|
||||
/** Streams the answer: `message_start` first, then the `assistant` message (same id) this long after. */
|
||||
responseTakesMs?: number;
|
||||
} = {}) {
|
||||
const received: string[] = [];
|
||||
/** Resolves each time the consumer has handled an `idle` message. */
|
||||
let idleHandled = Promise.withResolvers<void>();
|
||||
sdk.run = async function* (prompts, options) {
|
||||
const input = prompts[Symbol.asyncIterator]();
|
||||
for (;;) {
|
||||
const { value, done } = await input.next();
|
||||
if (done) return;
|
||||
const text = (value.message.content as Array<{ text: string }>)[0].text;
|
||||
received.push(text);
|
||||
if (text === "/compact") {
|
||||
yield system(options, "status", { status: "compacting" });
|
||||
if (compactResult === "success") {
|
||||
yield system(options, "compact_boundary", {
|
||||
compact_metadata: { trigger: "manual", pre_tokens: 250_000, post_tokens: 12_000 },
|
||||
});
|
||||
}
|
||||
yield system(options, "status", {
|
||||
status: null,
|
||||
compact_result: compactResult,
|
||||
...(compactResult === "failed" ? { compact_error: "too short" } : {}),
|
||||
});
|
||||
} else if (text === "/clear") {
|
||||
yield echo(value, options);
|
||||
yield {
|
||||
type: "conversation_reset",
|
||||
new_conversation_id: randomUUID(),
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
} else {
|
||||
yield echo(value, options);
|
||||
if (during) yield* during(options);
|
||||
const id = `msg_${randomUUID()}`;
|
||||
if (responseTakesMs !== undefined) {
|
||||
yield {
|
||||
type: "stream_event",
|
||||
event: {
|
||||
type: "message_start",
|
||||
message: { id, role: "assistant", model: "claude-sonnet-4-6", content: [], usage },
|
||||
},
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
await vi.advanceTimersByTimeAsync(responseTakesMs);
|
||||
}
|
||||
yield assistant(options, usage, null, id);
|
||||
}
|
||||
yield successfulResultMessage({ session_id: options.sessionId });
|
||||
if (suggestion && text !== "/compact") {
|
||||
yield {
|
||||
type: "prompt_suggestion",
|
||||
suggestion,
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
}
|
||||
yield system(options, "session_state_changed", { state: "idle" });
|
||||
idleHandled.resolve();
|
||||
}
|
||||
};
|
||||
return {
|
||||
received,
|
||||
/** Waits until the consumer has handled an `idle`, then re-arms for the next. */
|
||||
async untilIdle() {
|
||||
await idleHandled.promise;
|
||||
idleHandled = Promise.withResolvers();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function connect(clientCapabilities: Record<string, unknown> = {}) {
|
||||
vi.resetModules();
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const updates: Array<{ update: Record<string, any> }> = [];
|
||||
const client = {
|
||||
sessionUpdate: async (notification: { update: Record<string, any> }) => {
|
||||
updates.push(notification);
|
||||
},
|
||||
} as unknown as AcpClient;
|
||||
const logs: string[] = [];
|
||||
const agent: ClaudeAcpAgent = new ClaudeAcpAgent(client, {
|
||||
log: (message: string) => logs.push(message),
|
||||
error: () => {},
|
||||
});
|
||||
await initializeClient(agent, clientCapabilities);
|
||||
const session = await agent.newSession({ cwd, mcpServers: [] });
|
||||
return { agent, sessionId: session.sessionId, updates, logs };
|
||||
}
|
||||
|
||||
async function promptHi(agent: ClaudeAcpAgent, id: string) {
|
||||
await agent.prompt({ sessionId: id, prompt: [{ type: "text", text: "hi" }] });
|
||||
}
|
||||
|
||||
describe("idle compaction in the agent", () => {
|
||||
/** Idle after a large 1h request: `/compact` goes into the input at 54 minutes, not before. */
|
||||
it("compacts an idle session with a large context shortly before its one-hour cache expires, with no client prompt", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS - 1);
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(claude.received).toEqual(["hi", "/compact"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** A client prompt 30 minutes in moves the deadline, so 54 minutes after the first request passes quietly. */
|
||||
it("does not compact when a client prompt comes during the wait", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(30 * MINUTE_MS);
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
// The second request moved the deadline: 54 minutes after the first has passed.
|
||||
await vi.advanceTimersByTimeAsync(30 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi", "hi"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** Only a subagent's request reports 1h, so the main thread has no TTL and nothing arms. */
|
||||
it("does not take a subagent's request for the session's cache TTL", async () => {
|
||||
const claude = scriptClaudeCode({
|
||||
// The main thread's large request wrote no cache; only the subagent's request reports 1h.
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 250_000,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 0 },
|
||||
},
|
||||
during: async function* (options) {
|
||||
yield assistant(options, LARGE_ONE_HOUR_USAGE, "toolu_agent");
|
||||
},
|
||||
});
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** Switching the model during the wait drops the timer. */
|
||||
it("does not compact when the model changes during the wait", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(10 * MINUTE_MS);
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "claude-opus-4-6" });
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** Closing the session during the wait drops the timer. */
|
||||
it("does not compact once the session is closed", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await agent.closeSession({ sessionId });
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
});
|
||||
|
||||
/** A failed `/compact` is not repeated while no new request arrives. */
|
||||
it("does not compact again for the same request after a compaction failed", async () => {
|
||||
const claude = scriptClaudeCode({ compactResult: "failed" });
|
||||
const { agent, sessionId, logs } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(3 * 60 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi", "/compact"]);
|
||||
expect(logs).toContain("[idle-compaction] compaction failed: too short");
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** A `/clear` during the wait forgets the request, so the next idle arms nothing. */
|
||||
it("does not compact for a request that a /clear dropped", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(10 * MINUTE_MS);
|
||||
await agent.prompt({ sessionId, prompt: [{ type: "text", text: "/clear" }] });
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(3 * 60 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi", "/clear"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** The steered turn settles at the idle, so only an `onIdle` after that settle can arm. */
|
||||
it("arms after the idle that settles a steered turn", async () => {
|
||||
const received: string[] = [];
|
||||
const release = Promise.withResolvers<void>();
|
||||
const idleHandled = Promise.withResolvers<void>();
|
||||
sdk.run = async function* (prompts, options) {
|
||||
const input = prompts[Symbol.asyncIterator]();
|
||||
const { value: first } = await input.next();
|
||||
received.push((first.message.content as Array<{ text: string }>)[0].text);
|
||||
yield echo(first, options);
|
||||
await release.promise;
|
||||
yield assistant(options, LARGE_ONE_HOUR_USAGE);
|
||||
yield successfulResultMessage({ session_id: options.sessionId });
|
||||
const { value: steer } = await input.next();
|
||||
received.push((steer.message.content as Array<{ text: string }>)[0].text);
|
||||
yield echo(steer, options);
|
||||
yield assistant(options, LARGE_ONE_HOUR_USAGE);
|
||||
yield successfulResultMessage({ session_id: options.sessionId });
|
||||
yield system(options, "session_state_changed", { state: "idle" });
|
||||
idleHandled.resolve();
|
||||
for (;;) {
|
||||
const { value, done } = await input.next();
|
||||
if (done) return;
|
||||
received.push((value.message.content as Array<{ text: string }>)[0].text);
|
||||
}
|
||||
};
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
const prompted = promptHi(agent, sessionId);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await agent.steer({ sessionId, prompt: [{ type: "text", text: "also X" }] });
|
||||
release.resolve();
|
||||
await prompted;
|
||||
await idleHandled.promise;
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
|
||||
expect(received).toEqual(["hi", "also X", "/compact"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** The idle `/compact` is not the prompt the user is looking at a prediction for, so the prediction stays. */
|
||||
it("keeps the published prompt suggestion through the idle compaction", async () => {
|
||||
const claude = scriptClaudeCode({ suggestion: "Run the tests" });
|
||||
const { agent, sessionId, updates } = await connect(AIR_CLIENT_CAPABILITIES);
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
await claude.untilIdle();
|
||||
|
||||
expect(claude.received).toEqual(["hi", "/compact"]);
|
||||
expect(promptSuggestions(updates)).toEqual([{ suggestions: [{ text: "Run the tests" }] }]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** A prompt still waiting on a provider update when the timer fires disarmed it first, so no `/compact` lands before the prompt. */
|
||||
it("does not compact behind a prompt that is waiting on a provider update", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId, logs } = await connect();
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
|
||||
const update = Promise.withResolvers<void>();
|
||||
(agent as any).providerUpdate = update.promise;
|
||||
const waiting = promptHi(agent, sessionId);
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
expect(logs).not.toContain("[idle-compaction] firing /compact");
|
||||
expect(logs).toContain("[idle-compaction] disarmed: prompt");
|
||||
(agent as any).providerUpdate = null;
|
||||
update.resolve();
|
||||
await waiting;
|
||||
|
||||
expect(claude.received).toEqual(["hi", "hi"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** The idle `/compact` is not a user prompt, so it asks for no title; a client prompt in the same session does. */
|
||||
it("does not feed the idle compaction's prompt to the title machinery", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
const titles = agent.sessions[sessionId].titles;
|
||||
const onPrompt = vi.spyOn(titles, "onPrompt");
|
||||
const onPromptQueued = vi.spyOn(titles, "onPromptQueued");
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
expect(onPrompt).toHaveBeenCalledTimes(1);
|
||||
expect(onPromptQueued).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
await claude.untilIdle();
|
||||
|
||||
expect(claude.received).toEqual(["hi", "/compact"]);
|
||||
expect(onPrompt).toHaveBeenCalledTimes(1);
|
||||
expect(onPromptQueued).toHaveBeenCalledTimes(1);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** A rate limit warning that arrives mid-turn, before the turn's first request, still skips the compaction at the deadline. */
|
||||
it("does not compact after a rate limit warning", async () => {
|
||||
const claude = scriptClaudeCode({
|
||||
during: async function* (options) {
|
||||
yield {
|
||||
type: "rate_limit_event",
|
||||
rate_limit_info: { status: "allowed_warning", resetsAt: 1700000000, utilization: 0.9 },
|
||||
uuid: randomUUID(),
|
||||
session_id: options.sessionId,
|
||||
};
|
||||
},
|
||||
});
|
||||
const { agent, sessionId, logs } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
expect(logs).toContain("[idle-compaction] skip: rate limit status is allowed_warning");
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** The cache is written when the request starts, so a 7-minute answer still compacts 54 minutes after `message_start`, not after its last block. */
|
||||
it("measures the deadline from the start of a long streamed response", async () => {
|
||||
const claude = scriptClaudeCode({ responseTakesMs: 7 * MINUTE_MS });
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(47 * MINUTE_MS - 1);
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(claude.received).toEqual(["hi", "/compact"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** A timer that fires hours late (the machine slept) finds a cold cache: compacting would write the whole context. */
|
||||
it("does not compact when the clock jumped past the deadline before the timer fired", async () => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId } = await connect();
|
||||
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
vi.setSystemTime(Date.now() + 8 * 60 * MINUTE_MS);
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
|
||||
expect(claude.received).toEqual(["hi"]);
|
||||
await agent.dispose();
|
||||
});
|
||||
|
||||
/** AIR gets `trigger: "idle"` for the adapter's compaction and `"manual"` for a client's `/compact`. */
|
||||
it.each([
|
||||
{ name: "an idle compaction", prompt: "hi", expected: "idle" },
|
||||
{ name: "a manual /compact", prompt: "/compact", expected: "manual" },
|
||||
])("tells AIR that $name has trigger $expected", async ({ prompt, expected }) => {
|
||||
const claude = scriptClaudeCode();
|
||||
const { agent, sessionId, updates } = await connect(AIR_CLIENT_CAPABILITIES);
|
||||
|
||||
if (expected === "idle") {
|
||||
await promptHi(agent, sessionId);
|
||||
await claude.untilIdle();
|
||||
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
|
||||
await vi.waitFor(() => expect(compactionTriggers(updates)).toHaveLength(1));
|
||||
} else {
|
||||
await agent.prompt({ sessionId, prompt: [{ type: "text", text: prompt }] });
|
||||
}
|
||||
|
||||
expect(compactionTriggers(updates)).toEqual([expected]);
|
||||
await agent.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
/** Each `promptSuggestions` value that the client received. */
|
||||
function promptSuggestions(updates: Array<{ update: Record<string, any> }>) {
|
||||
return updates.flatMap(({ update }) => {
|
||||
const suggestions = update._meta?.promptSuggestions;
|
||||
return suggestions ? [suggestions] : [];
|
||||
});
|
||||
}
|
||||
|
||||
/** The `trigger` of each compaction report that the client received. */
|
||||
function compactionTriggers(updates: Array<{ update: Record<string, any> }>) {
|
||||
return updates.flatMap(({ update }) => {
|
||||
const trigger = update._meta?.jetbrains?.air?.contextCompaction?.trigger;
|
||||
return trigger ? [trigger] : [];
|
||||
});
|
||||
}
|
||||
416
src/tests/idle-compaction.test.ts
Normal file
416
src/tests/idle-compaction.test.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { IdleCompaction, type IdleCompactionHost } from "../idle-compaction.js";
|
||||
|
||||
const MIN = 60_000;
|
||||
const ONE_HOUR = {
|
||||
cache_creation: { ephemeral_1h_input_tokens: 1000, ephemeral_5m_input_tokens: 0 },
|
||||
};
|
||||
const FIVE_MIN = {
|
||||
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 1000 },
|
||||
};
|
||||
const NOTHING_WRITTEN = {
|
||||
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 0 },
|
||||
};
|
||||
|
||||
function setup(
|
||||
options: {
|
||||
env?: Record<string, string | undefined>;
|
||||
tokens?: number;
|
||||
settings?: ReturnType<IdleCompactionHost["settings"]>;
|
||||
setTimeout?: (callback: () => void, ms: number) => unknown;
|
||||
} = {},
|
||||
) {
|
||||
const state = {
|
||||
busy: false,
|
||||
tokens: options.tokens ?? 200_000,
|
||||
settings: options.settings ?? {},
|
||||
};
|
||||
const host = {
|
||||
isBusy: vi.fn(() => state.busy),
|
||||
contextTokens: vi.fn(() => state.tokens),
|
||||
settings: vi.fn(() => state.settings),
|
||||
compact: vi.fn(async (): Promise<unknown> => undefined),
|
||||
log: vi.fn(),
|
||||
};
|
||||
const idle = new IdleCompaction(host, { env: options.env ?? {}, setTimeout: options.setTimeout });
|
||||
return { idle, host, state };
|
||||
}
|
||||
|
||||
/** Records a 1h-cache request and goes idle. */
|
||||
function requestAndIdle(idle: IdleCompaction, usage: unknown = ONE_HOUR) {
|
||||
idle.noteRequest(usage);
|
||||
idle.onIdle();
|
||||
}
|
||||
|
||||
describe("IdleCompaction", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
/** A failed compaction is logged and must not re-arm from the same request, or it loops until the user prompts. */
|
||||
it("does not arm again for a request it already compacted for", async () => {
|
||||
const { idle, host } = setup();
|
||||
host.compact.mockRejectedValue(new Error("boom"));
|
||||
requestAndIdle(idle);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("boom"));
|
||||
|
||||
idle.onIdle();
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
|
||||
requestAndIdle(idle);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
/** `/clear` replaces the conversation: what was recorded no longer describes it. */
|
||||
it("does not arm after reset until a new request", async () => {
|
||||
const { idle, host } = setup();
|
||||
requestAndIdle(idle);
|
||||
idle.reset();
|
||||
idle.onIdle();
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MIN);
|
||||
expect(host.compact).not.toHaveBeenCalled();
|
||||
|
||||
requestAndIdle(idle);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** A reported failure is logged only while the adapter's own compaction runs. */
|
||||
it("logs a reported compaction failure only during its own compaction", async () => {
|
||||
const { idle, host } = setup();
|
||||
const running = Promise.withResolvers<void>();
|
||||
host.compact.mockReturnValue(running.promise);
|
||||
requestAndIdle(idle);
|
||||
|
||||
idle.noteCompactionFailed("too short");
|
||||
expect(host.log).not.toHaveBeenCalledWith(expect.stringContaining("compaction failed"));
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
idle.noteCompactionFailed("too short");
|
||||
expect(host.log).toHaveBeenCalledWith("[idle-compaction] compaction failed: too short");
|
||||
});
|
||||
|
||||
/** `isCompacting` is true exactly while the compaction it started runs. */
|
||||
it("reports isCompacting while its compaction runs", async () => {
|
||||
const { idle, host } = setup();
|
||||
const running = Promise.withResolvers<void>();
|
||||
host.compact.mockReturnValue(running.promise);
|
||||
requestAndIdle(idle);
|
||||
|
||||
expect(idle.isCompacting).toBe(false);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(idle.isCompacting).toBe(true);
|
||||
running.resolve();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(idle.isCompacting).toBe(false);
|
||||
});
|
||||
|
||||
/** A 1h-cache request with 200k context compacts at 54 minutes, not before. */
|
||||
it("arms and compacts at 54 minutes", async () => {
|
||||
const { idle, host } = setup();
|
||||
requestAndIdle(idle);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN - 1);
|
||||
expect(host.compact).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** The deadline counts from the request, not from when the session went idle. */
|
||||
it("measures the deadline from the request time", async () => {
|
||||
const { idle, host } = setup();
|
||||
idle.noteRequest(ONE_HOUR);
|
||||
await vi.advanceTimersByTimeAsync(10 * MIN);
|
||||
idle.onIdle();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(44 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** A cache whose deadline passed more than 60 s ago is cold: a timer that fires that late (the machine slept) compacts nothing. */
|
||||
it.each([
|
||||
["the timer fires 61 s late", 61_000, false],
|
||||
["the timer fires 60 s late", 60_000, true],
|
||||
])("when %s, compacts: %s", async (_name, lateMs, compacts) => {
|
||||
const { idle, host } = setup();
|
||||
requestAndIdle(idle);
|
||||
vi.setSystemTime(Date.now() + lateMs);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
|
||||
expect(host.compact).toHaveBeenCalledTimes(compacts ? 1 : 0);
|
||||
});
|
||||
|
||||
/** Going idle after the deadline is more than 60 s past arms nothing. */
|
||||
it("does not arm when the deadline passed more than 60 s ago", async () => {
|
||||
const { idle, host } = setup();
|
||||
idle.noteRequest(ONE_HOUR);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN + 61_000);
|
||||
idle.onIdle();
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(host.compact).not.toHaveBeenCalled();
|
||||
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("lapsed"));
|
||||
});
|
||||
|
||||
/** The rate limit status is the last one reported: a reset or a malformed event keeps `rejected`, a later `allowed` clears it. */
|
||||
it("keeps the rate limit status across a reset and a malformed event until an allowed one", async () => {
|
||||
const { idle, host } = setup();
|
||||
idle.noteRateLimit({ status: "rejected" });
|
||||
idle.noteRateLimit(null);
|
||||
idle.reset();
|
||||
requestAndIdle(idle);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
|
||||
idle.noteRateLimit({ status: "allowed" });
|
||||
idle.onIdle();
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** Later blocks of one response (same message id) keep the deadline from its first sighting; a new id moves it. */
|
||||
it.each([
|
||||
["the same message id", "msg_1", 54 * MIN],
|
||||
["a new message id", "msg_2", 54 * MIN + 7 * MIN],
|
||||
])("a request with %s: the deadline stays or moves", async (_name, id, firesAt) => {
|
||||
const { idle, host } = setup();
|
||||
idle.noteRequest(ONE_HOUR, "msg_1");
|
||||
await vi.advanceTimersByTimeAsync(7 * MIN);
|
||||
idle.noteRequest(ONE_HOUR, id);
|
||||
idle.onIdle();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(firesAt - 7 * MIN - 1);
|
||||
expect(host.compact).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** A deadline already in the past fires immediately. */
|
||||
it("fires right away when the deadline has passed", async () => {
|
||||
const { idle, host } = setup();
|
||||
idle.noteRequest(ONE_HOUR);
|
||||
await vi.advanceTimersByTimeAsync(55 * MIN);
|
||||
idle.onIdle();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** Each condition that fails R2 leaves no timer armed. */
|
||||
it.each([
|
||||
["5m TTL", { usage: FIVE_MIN }],
|
||||
["unknown TTL", { usage: {} }],
|
||||
["malformed usage", { usage: null }],
|
||||
["context below the minimum", { tokens: 199_999 }],
|
||||
["idleCompaction: false", { settings: { idleCompaction: false } }],
|
||||
["autoCompactEnabled: false", { settings: { autoCompactEnabled: false } }],
|
||||
["DISABLE_COMPACT", { env: { DISABLE_COMPACT: "1" } }],
|
||||
["DISABLE_AUTO_COMPACT", { env: { DISABLE_AUTO_COMPACT: "true" } }],
|
||||
// Claude Code also reads these from the `env` block of settings.json.
|
||||
[
|
||||
"DISABLE_AUTO_COMPACT in the settings env",
|
||||
{ settings: { env: { DISABLE_AUTO_COMPACT: "1" } } },
|
||||
],
|
||||
["rate limit status allowed_warning", { rateLimit: "allowed_warning" }],
|
||||
["rate limit status rejected", { rateLimit: "rejected" }],
|
||||
[
|
||||
"CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS in the settings env",
|
||||
{ tokens: 250_000, settings: { env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "500000" } } },
|
||||
],
|
||||
[
|
||||
"CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS in the settings env over the launch env",
|
||||
{
|
||||
tokens: 250_000,
|
||||
env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "100000" },
|
||||
settings: { env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "500000" } },
|
||||
},
|
||||
],
|
||||
])("does not arm: %s", async (_name, options0: Record<string, any>) => {
|
||||
const { usage, rateLimit, ...options } = options0;
|
||||
const { idle, host } = setup(options);
|
||||
if (rateLimit) idle.noteRateLimit({ status: rateLimit });
|
||||
requestAndIdle(idle, "usage" in options0 ? usage : ONE_HOUR);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2 * 60 * MIN);
|
||||
expect(host.compact).not.toHaveBeenCalled();
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("skip"));
|
||||
});
|
||||
|
||||
/** A falsy value is not an opt-out, and the settings env is applied after the launch env, so its value wins, not an OR of the two. */
|
||||
it.each([
|
||||
["DISABLE_COMPACT=0 in the launch env", { env: { DISABLE_COMPACT: "0" } }],
|
||||
[
|
||||
"DISABLE_AUTO_COMPACT=0 in the settings env over =1 in the launch env",
|
||||
{
|
||||
env: { DISABLE_AUTO_COMPACT: "1" },
|
||||
settings: { env: { DISABLE_AUTO_COMPACT: "0" } },
|
||||
},
|
||||
],
|
||||
])("arms: %s", async (_name, options) => {
|
||||
const { idle, host } = setup(options);
|
||||
requestAndIdle(idle);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** Idle without any request in this instance (resumed session) arms nothing. */
|
||||
it("does not arm before the first request", () => {
|
||||
const { idle, host } = setup();
|
||||
idle.onIdle();
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("no request recorded"));
|
||||
});
|
||||
|
||||
/** A request that writes no cache keeps the previous request's TTL. */
|
||||
it("keeps the previous TTL when the request writes nothing", async () => {
|
||||
const { idle, host } = setup();
|
||||
idle.noteRequest(ONE_HOUR);
|
||||
requestAndIdle(idle, NOTHING_WRITTEN);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** A 5m request after a 1h one switches the TTL, so nothing arms. */
|
||||
it("switches to 5m when a later request writes a 5m cache", () => {
|
||||
const { idle } = setup();
|
||||
idle.noteRequest(ONE_HOUR);
|
||||
requestAndIdle(idle, FIVE_MIN);
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
/** The min-tokens env raises the threshold and is clamped to 100k from below. */
|
||||
it.each([
|
||||
["300000", 299_999, false],
|
||||
["300000", 300_000, true],
|
||||
["50000", 99_999, false],
|
||||
["50000", 100_000, true],
|
||||
["abc", 200_000, true],
|
||||
["abc", 199_999, false],
|
||||
])("CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS=%s with %i tokens arms: %s", (min, tokens, arms) => {
|
||||
const { idle } = setup({ env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: min }, tokens });
|
||||
requestAndIdle(idle);
|
||||
|
||||
expect(vi.getTimerCount()).toBe(arms ? 1 : 0);
|
||||
});
|
||||
|
||||
/** The delay env overrides the 54 minute default; invalid values fall back and are logged once. */
|
||||
it.each([
|
||||
["1000", 1000, false],
|
||||
["0", 0, false],
|
||||
["-5", 54 * MIN, true],
|
||||
["soon", 54 * MIN, true],
|
||||
["2147483648", 54 * MIN, true],
|
||||
])("CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS=%s fires after %ims", async (value, ms, invalid) => {
|
||||
const { idle, host } = setup({ env: { CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS: value } });
|
||||
requestAndIdle(idle);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(ms - 1 < 0 ? 0 : ms - 1);
|
||||
if (ms > 0) expect(host.compact).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
const warnings = host.log.mock.calls.filter(([m]) => String(m).includes("invalid"));
|
||||
expect(warnings).toHaveLength(invalid ? 1 : 0);
|
||||
});
|
||||
|
||||
/** A new request drops the armed timer, so the deadline cannot fire for the old one; disarm and dispose drop it too. */
|
||||
it.each([
|
||||
["noteRequest", (idle: IdleCompaction) => idle.noteRequest(ONE_HOUR)],
|
||||
["disarm", (idle: IdleCompaction) => idle.disarm("prompt")],
|
||||
["dispose", (idle: IdleCompaction) => idle.dispose()],
|
||||
])("%s drops the timer", async (_name, drop) => {
|
||||
const { idle, host } = setup();
|
||||
requestAndIdle(idle);
|
||||
drop(idle);
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
await vi.advanceTimersByTimeAsync(60 * MIN);
|
||||
expect(host.compact).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/** After dispose, requests and idle notifications do nothing. */
|
||||
it("ignores calls after dispose", () => {
|
||||
const { idle } = setup();
|
||||
idle.dispose();
|
||||
requestAndIdle(idle);
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
/** A change after arming (busy, context shrunk, rate limit reached) is re-checked at the deadline: nothing is compacted and nothing re-arms. */
|
||||
it.each([
|
||||
["the session became busy", "busy", (ctx: ReturnType<typeof setup>) => (ctx.state.busy = true)],
|
||||
[
|
||||
"the context shrank below the minimum",
|
||||
"below the",
|
||||
(ctx: ReturnType<typeof setup>) => (ctx.state.tokens = 50_000),
|
||||
],
|
||||
[
|
||||
"the rate limit status became rejected",
|
||||
"rate limit",
|
||||
(ctx: ReturnType<typeof setup>) => ctx.idle.noteRateLimit({ status: "rejected" }),
|
||||
],
|
||||
])("does not compact at the deadline when %s", async (_name, logged, change) => {
|
||||
const ctx = setup();
|
||||
requestAndIdle(ctx.idle);
|
||||
change(ctx);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(ctx.host.compact).not.toHaveBeenCalled();
|
||||
expect(ctx.host.log).toHaveBeenCalledWith(expect.stringContaining(logged));
|
||||
});
|
||||
|
||||
/** Going idle while busy arms nothing. */
|
||||
it("does not arm while busy", () => {
|
||||
const { idle, state } = setup();
|
||||
state.busy = true;
|
||||
requestAndIdle(idle);
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
/** Arming twice leaves a single timer, so only one compaction runs. */
|
||||
it("rearming replaces the timer", async () => {
|
||||
const { idle, host } = setup();
|
||||
requestAndIdle(idle);
|
||||
idle.onIdle();
|
||||
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
expect(host.compact).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
/** While a compaction runs, an idle notification does not arm another. */
|
||||
it("does not arm while a compaction is in flight", async () => {
|
||||
const { idle, host } = setup();
|
||||
let finish!: () => void;
|
||||
host.compact.mockReturnValue(new Promise<void>((resolve) => (finish = resolve)));
|
||||
requestAndIdle(idle);
|
||||
await vi.advanceTimersByTimeAsync(54 * MIN);
|
||||
|
||||
idle.onIdle();
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
finish();
|
||||
});
|
||||
|
||||
/** The timer handle is unref'd so it never keeps the process alive. */
|
||||
it("unrefs the timer", () => {
|
||||
const unref = vi.fn();
|
||||
const setTimeoutSpy = vi.fn(() => ({ unref }));
|
||||
const { idle } = setup({ setTimeout: setTimeoutSpy });
|
||||
requestAndIdle(idle);
|
||||
|
||||
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
|
||||
expect(unref).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -381,6 +381,15 @@ describe("formatMcpStatus", () => {
|
||||
expect(markdown).toContain("- ``a`b``\n");
|
||||
expect(markdown).toContain("**Disabled:** `` `edge ``");
|
||||
});
|
||||
|
||||
/** A name with a space at each end is padded so CommonMark keeps both spaces;
|
||||
* an all-space name is not, since CommonMark does not strip it. */
|
||||
it.each([
|
||||
[" db ", "- ` db `"],
|
||||
[" ", "- ` `"],
|
||||
])("pads the server name %j only when CommonMark would strip it", (name, line) => {
|
||||
expect(formatMcpStatus([{ name, status: "connected" }])).toContain(line);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanMcpError", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { vi } from "vitest";
|
||||
import { randomUUID } from "crypto";
|
||||
import { SessionBackgroundWork } from "../background-work.js";
|
||||
import { SessionTitles } from "../session-titles.js";
|
||||
|
||||
/** Stand-in agent for a `SessionTitles` whose test doesn't care about titles:
|
||||
@@ -91,6 +92,7 @@ export function mockSessionState(
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
liveBackgroundTasks: new Map(),
|
||||
background: new SessionBackgroundWork(sessionId, false),
|
||||
emittedAssistantText: false,
|
||||
owedTrailingIdles: 0,
|
||||
messageIdToUuid: new Map(),
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("session mode", () => {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "**Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.",
|
||||
text: "\n\n> **Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.\n\n",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -174,6 +174,7 @@ describe("session mode", () => {
|
||||
requestedMode: "default",
|
||||
currentModelId: "opus",
|
||||
allowBypass: false,
|
||||
allowAuto: true,
|
||||
});
|
||||
|
||||
expect(result.modes.availableModes.map((mode) => mode.id)).not.toContain("bypassPermissions");
|
||||
@@ -188,6 +189,7 @@ describe("session mode", () => {
|
||||
currentModelInfo: { value: "haiku", displayName: "Haiku", description: "" },
|
||||
currentModelId: "haiku",
|
||||
allowBypass: true,
|
||||
allowAuto: true,
|
||||
});
|
||||
|
||||
expect(result.modes.currentModeId).toBe("acceptEdits");
|
||||
@@ -199,6 +201,24 @@ describe("session mode", () => {
|
||||
expect(notifications.logError).toHaveBeenCalledWith(expect.stringContaining("falling back"));
|
||||
});
|
||||
|
||||
it("starts a built-in Auto default in Manual on a model without Auto", async () => {
|
||||
const setPermissionMode = vi.fn();
|
||||
const { manager } = createManager(undefined);
|
||||
const result = await manager.initialize({
|
||||
query: { setPermissionMode },
|
||||
requestedMode: "auto",
|
||||
builtInDefault: true,
|
||||
currentModelInfo: { value: "haiku", displayName: "Haiku", description: "" },
|
||||
currentModelId: "haiku",
|
||||
allowBypass: true,
|
||||
allowAuto: true,
|
||||
});
|
||||
|
||||
expect(result.modes.currentModeId).toBe("default");
|
||||
expect(result.autoModeFallbackWarningPending).toBe(false);
|
||||
expect(setPermissionMode).toHaveBeenCalledWith("default");
|
||||
});
|
||||
|
||||
it("keeps mode state and its config option synchronized", () => {
|
||||
const session = {
|
||||
...createSession(),
|
||||
@@ -248,24 +268,46 @@ describe("session mode", () => {
|
||||
|
||||
describe("resolveInitialPermissionMode", () => {
|
||||
const logger = () => ({ error: vi.fn() });
|
||||
const initialMode = (
|
||||
sources: Parameters<typeof resolveInitialPermissionMode>[0],
|
||||
log: { error: (...args: unknown[]) => void },
|
||||
allowBypass: boolean,
|
||||
allowAuto = true,
|
||||
) => resolveInitialPermissionMode(sources, log, { allowBypass, allowAuto }).mode;
|
||||
|
||||
it.each([
|
||||
{ allowAuto: true, expected: "auto" },
|
||||
{ allowAuto: false, expected: "default" },
|
||||
])(
|
||||
"starts in $expected without defaultMode when allowAuto is $allowAuto",
|
||||
({ allowAuto, expected }) => {
|
||||
expect(resolveInitialPermissionMode({}, logger(), { allowBypass: true, allowAuto })).toEqual({
|
||||
mode: expected,
|
||||
builtInDefault: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("falls back to defaultMode for a resumed auto when Auto is disabled", () => {
|
||||
expect(initialMode({ resumed: "auto", defaultMode: "plan" }, logger(), true, false)).toBe(
|
||||
"plan",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses defaultMode when nothing else is given", () => {
|
||||
expect(resolveInitialPermissionMode({ defaultMode: "plan" }, logger(), true)).toBe("plan");
|
||||
expect(initialMode({ defaultMode: "plan" }, logger(), true)).toBe("plan");
|
||||
});
|
||||
|
||||
it("continues in the mode of the resumed transcript instead of defaultMode", () => {
|
||||
expect(
|
||||
resolveInitialPermissionMode(
|
||||
{ resumed: "bypassPermissions", defaultMode: "plan" },
|
||||
logger(),
|
||||
true,
|
||||
),
|
||||
).toBe("bypassPermissions");
|
||||
});
|
||||
it.each(["default", "acceptEdits", "plan", "auto", "bypassPermissions"])(
|
||||
"continues in the supported restored mode %s instead of defaultMode",
|
||||
(resumed) => {
|
||||
expect(initialMode({ resumed, defaultMode: "dontAsk" }, logger(), true)).toBe(resumed);
|
||||
},
|
||||
);
|
||||
|
||||
it("lets an explicit mode win over the resumed transcript", () => {
|
||||
expect(
|
||||
resolveInitialPermissionMode(
|
||||
initialMode(
|
||||
{ explicit: "acceptEdits", resumed: "bypassPermissions", defaultMode: "plan" },
|
||||
logger(),
|
||||
true,
|
||||
@@ -275,19 +317,31 @@ describe("resolveInitialPermissionMode", () => {
|
||||
|
||||
it("falls back to defaultMode when bypass is not available now", () => {
|
||||
const log = logger();
|
||||
expect(
|
||||
resolveInitialPermissionMode(
|
||||
{ resumed: "bypassPermissions", defaultMode: "plan" },
|
||||
log,
|
||||
false,
|
||||
),
|
||||
).toBe("plan");
|
||||
expect(initialMode({ resumed: "bypassPermissions", defaultMode: "plan" }, log, false)).toBe(
|
||||
"plan",
|
||||
);
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('"bypassPermissions"'));
|
||||
});
|
||||
|
||||
it("falls back to defaultMode for an unknown resumed mode", () => {
|
||||
expect(
|
||||
resolveInitialPermissionMode({ resumed: "warp", defaultMode: "acceptEdits" }, logger(), true),
|
||||
).toBe("acceptEdits");
|
||||
it.each(["warp", "dontAsk"])("falls back to defaultMode for resumed mode %s", (resumed) => {
|
||||
expect(initialMode({ resumed, defaultMode: "acceptEdits" }, logger(), true)).toBe(
|
||||
"acceptEdits",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ defaultMode: "plan", allowBypass: true, expected: "plan" },
|
||||
{ defaultMode: "bypassPermissions", allowBypass: true, expected: "bypassPermissions" },
|
||||
{ defaultMode: "bypassPermissions", allowBypass: false, expected: "default" },
|
||||
{ defaultMode: "dontAsk", allowBypass: true, expected: "default" },
|
||||
{ defaultMode: "warp", allowBypass: true, expected: "default" },
|
||||
{ defaultMode: undefined, allowBypass: true, expected: "auto" },
|
||||
])(
|
||||
"uses available settings fallback $defaultMode for an unavailable restored mode",
|
||||
({ defaultMode, allowBypass, expected }) => {
|
||||
expect(initialMode({ resumed: "dontAsk", defaultMode }, logger(), allowBypass)).toBe(
|
||||
expected,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
clientSupportsNotices,
|
||||
collapseHookSpeaker,
|
||||
MAX_NOTICE_TITLE_LENGTH,
|
||||
normalizeNoticeText,
|
||||
noticeOrTranscriptUpdate,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
noticeTranscriptText,
|
||||
sentenceCase,
|
||||
splitNoticeText,
|
||||
transcriptBlock,
|
||||
} from "../session-notices.js";
|
||||
|
||||
describe("session notices", () => {
|
||||
@@ -30,21 +32,42 @@ describe("session notices", () => {
|
||||
expect(noticeTranscriptText({ severity: "info", title: "Done" })).toBe("**Done**");
|
||||
expect(noticeOrTranscriptUpdate(notice, false)).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "**Fast mode turned off:** Why." },
|
||||
content: { type: "text", text: "\n\n> **Fast mode turned off:** Why.\n\n" },
|
||||
});
|
||||
expect(noticeOrTranscriptUpdate(notice, false, "custom line")).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "custom line" },
|
||||
content: { type: "text", text: "\n\n> custom line\n\n" },
|
||||
});
|
||||
const meta = { claudeCode: { kind: "informational", level: "warning" } };
|
||||
expect(noticeOrTranscriptUpdate(notice, false, "custom line", meta)).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "custom line" },
|
||||
content: { type: "text", text: "\n\n> custom line\n\n" },
|
||||
_meta: meta,
|
||||
});
|
||||
expect(noticeOrTranscriptUpdate(notice, true, "custom line", meta)).not.toHaveProperty("_meta");
|
||||
});
|
||||
|
||||
it("puts a transcript notice in its own blockquote paragraph", () => {
|
||||
// Without blank lines the chunk renders glued to Claude's last sentence:
|
||||
// "…all done.**Notice:** Stop says: …".
|
||||
expect(transcriptBlock("**Notice:** one line")).toBe("\n\n> **Notice:** one line\n\n");
|
||||
expect(transcriptBlock("**Notice:** first\nsecond\n\nfourth\n")).toBe(
|
||||
"\n\n> **Notice:** first\n> second\n>\n> fourth\n\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a hook's repeated speaker prefix on the first line only", () => {
|
||||
expect(collapseHookSpeaker("Stop says: token: 3 calls\nStop says: hand off now")).toBe(
|
||||
"Stop says: token: 3 calls\nhand off now",
|
||||
);
|
||||
expect(collapseHookSpeaker("Stop says: one line")).toBe("Stop says: one line");
|
||||
// Left alone unless every line repeats the same prefix.
|
||||
expect(collapseHookSpeaker("Stop says: a\nsomething else")).toBe(
|
||||
"Stop says: a\nsomething else",
|
||||
);
|
||||
expect(collapseHookSpeaker("no prefix\nStop says: b")).toBe("no prefix\nStop says: b");
|
||||
});
|
||||
|
||||
it("sends a notice update when the client can present one", () => {
|
||||
expect(
|
||||
noticeOrTranscriptUpdate({ severity: "info", title: "Task stopped by user" }, true),
|
||||
|
||||
@@ -2739,6 +2739,19 @@ describe("applyTaskCreate / applyTaskUpdate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("takes the subject and description from the output, which holds the task as saved", () => {
|
||||
const state: TaskState = new Map();
|
||||
applyTaskCreate(
|
||||
state,
|
||||
{ subject: "Write tsets", description: "Cover Task*", activeForm: "Writing tests" },
|
||||
{ task: { id: "1", subject: "Write tests", description: "Cover Task* flow" } },
|
||||
);
|
||||
expect(state.get("1")).toMatchObject({
|
||||
subject: "Write tests",
|
||||
description: "Cover Task* flow",
|
||||
});
|
||||
});
|
||||
|
||||
it("is a no-op when the output has no task ID", () => {
|
||||
const state: TaskState = new Map();
|
||||
applyTaskCreate(state, { subject: "X", description: "Y" }, undefined);
|
||||
|
||||
@@ -278,11 +278,14 @@ export function applyTaskCreate(
|
||||
): void {
|
||||
const taskId = output?.task?.id;
|
||||
if (!taskId || !input) return;
|
||||
// The output holds the task as saved, which differs from the input when
|
||||
// Claude Code corrects a malformed call or a PreToolUse hook changes it.
|
||||
// `description` is in outputs since CLI 2.1.317.
|
||||
state.set(taskId, {
|
||||
subject: input.subject,
|
||||
subject: output.task.subject ?? input.subject,
|
||||
status: "pending",
|
||||
activeForm: input.activeForm,
|
||||
description: input.description,
|
||||
description: output.task.description ?? input.description,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user