mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-09 10:51:47 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a88de2a83 | ||
|
|
ff9b96d462 | ||
|
|
1887ada215 | ||
|
|
06c3d7bdbd | ||
|
|
92548f0435 | ||
|
|
cc5d5e8b80 | ||
|
|
82cd692e50 |
25
.github/workflows/publish.yml
vendored
25
.github/workflows/publish.yml
vendored
@@ -53,3 +53,28 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm publish
|
||||
|
||||
trigger-registry-update:
|
||||
needs: publish-npm
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Generate token scoped to the registry repo
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
id: registry-token
|
||||
with:
|
||||
app-id: ${{ secrets.REGISTRY_UPDATER_APP_ID }}
|
||||
private-key: ${{ secrets.REGISTRY_UPDATER_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: registry
|
||||
|
||||
- name: Dispatch registry version update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.registry-token.outputs.token }}
|
||||
run: |
|
||||
gh workflow run update-versions.yml \
|
||||
--repo ${{ github.repository_owner }}/registry \
|
||||
--ref main \
|
||||
-f apply=true \
|
||||
-f agents=claude-acp
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.59.0"
|
||||
".": "0.60.0"
|
||||
}
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [0.60.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.59.0...v0.60.0) (2026-07-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** Update to claude-agent-sdk 0.3.215 ([#890](https://github.com/agentclientprotocol/claude-agent-acp/issues/890)) ([92548f0](https://github.com/agentclientprotocol/claude-agent-acp/commit/92548f043547b0ddac95921ece020e69f7c12c5f))
|
||||
* implement configurable LLM providers ([#853](https://github.com/agentclientprotocol/claude-agent-acp/issues/853)) ([82cd692](https://github.com/agentclientprotocol/claude-agent-acp/commit/82cd692e500eedec182f817b18cebb005c4b98ce))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* parse Agent/Task trailers without regex ([#879](https://github.com/agentclientprotocol/claude-agent-acp/issues/879)) ([06c3d7b](https://github.com/agentclientprotocol/claude-agent-acp/commit/06c3d7bdbd8cc9415c8cabac060a50e0951c758b))
|
||||
* remove ~15s stall on session/new and model switch by seeding the context window synchronously ([#894](https://github.com/agentclientprotocol/claude-agent-acp/issues/894)) ([ff9b96d](https://github.com/agentclientprotocol/claude-agent-acp/commit/ff9b96d462831b1c3b96722ea20215ff6e529cb1))
|
||||
* Silence missing PostToolUse callbacks ([#895](https://github.com/agentclientprotocol/claude-agent-acp/issues/895)) ([1887ada](https://github.com/agentclientprotocol/claude-agent-acp/commit/1887ada215b27bb1025d9b7696a46ae7a4ac0f7a)), closes [#889](https://github.com/agentclientprotocol/claude-agent-acp/issues/889)
|
||||
|
||||
## [0.59.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.58.1...v0.59.0) (2026-07-13)
|
||||
|
||||
|
||||
|
||||
339
package-lock.json
generated
339
package-lock.json
generated
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.59.0",
|
||||
"version": "0.60.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.59.0",
|
||||
"version": "0.60.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.215",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.111.0",
|
||||
"@anthropic-ai/sdk": "0.112.3",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"eslint": "10.7.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.7.0",
|
||||
@@ -45,22 +45,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.207.tgz",
|
||||
"integrity": "sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.215.tgz",
|
||||
"integrity": "sha512-fBktJCwfu8ZeOSnnSLcWVkIHyp/pjouJsGVGtRnQ0HkcRuOReIbRcB/6n2O5dYV331Ok+MfdGHiPaTORj7pCtQ==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.207"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.215"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -69,9 +69,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.207.tgz",
|
||||
"integrity": "sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.215.tgz",
|
||||
"integrity": "sha512-KIOe3N/ypVIdsI7fnJUHT0Djei1RH01TbxK6LI2mgoHKZ6NVDt/Q9kQ1+On3b/l899RhE6C1SMAiQ0iB4sbkjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -82,9 +82,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.207.tgz",
|
||||
"integrity": "sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.215.tgz",
|
||||
"integrity": "sha512-cyjfQgkgF/zZbSJP6uJpPXoIJE/gYFOgz+u/SjklvakcO56BpEnsbdl/oHVu5G8GCw69V11hKFZ8T8YsVOiv2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -95,9 +95,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.207.tgz",
|
||||
"integrity": "sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.215.tgz",
|
||||
"integrity": "sha512-lb7ayxZeWLiEhlOjzF8oX+DdJHrVgR1hvwkwRdYo+LgTT3hSxv6h43sheS3hZ1I9LYvOTKWa2E0O1EnffOzyCg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -111,9 +111,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.207.tgz",
|
||||
"integrity": "sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.215.tgz",
|
||||
"integrity": "sha512-dXMR8afbZCFWUKQGyvn9swDYYvMtZWGqqbZ994ahS7HvGN4AgF63uvojHVErW1Xxn2601Bm+eCYQt+BSLk6iZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -127,9 +127,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.207.tgz",
|
||||
"integrity": "sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.215.tgz",
|
||||
"integrity": "sha512-Iivu3oq7hIEc81dpTu1W0GJ/kCE8TRtj9tb+wdZCp1grsuGEJiS3Bh1tLxCrhLOTDxPggK6xZGVN/RPJh84ywA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -143,9 +143,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.207.tgz",
|
||||
"integrity": "sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.215.tgz",
|
||||
"integrity": "sha512-jSmrjSupnWtw8/I1Wmr32J6lB02gFXytmNbse4kcLEgH6UGtdZckKnDVnh/ejHma4QE9WGSn54xLsSFnjHVrRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -159,9 +159,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.207.tgz",
|
||||
"integrity": "sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.215.tgz",
|
||||
"integrity": "sha512-9Xtpwd4DzfObOycDkfYfQ7clhWoFu2fmRqmMZWyQRyk2mEHWR3gLBwUgpfjYb6m4Fke9eTmIx2lXDG2i1OaBzA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -172,9 +172,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.207.tgz",
|
||||
"integrity": "sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg==",
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.215.tgz",
|
||||
"integrity": "sha512-DkPwdc1zS6KIf6YNKXhlqAw95wnjG8Uh5bSXRrFA+MxsaWedlVFeja6zFM5uZ84ouGrcHEeU3PGUcGaKR1U4cQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.111.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.111.0.tgz",
|
||||
"integrity": "sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q==",
|
||||
"version": "0.112.3",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.112.3.tgz",
|
||||
"integrity": "sha512-wjcozJlitVIuBEw9cj/xBuRznwkhcLmXmNzlFoeHbh4AvrDG3HGZrdvEOTTmobcbhjGkfOpKbmDTCQ4s9LQvCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
@@ -944,17 +944,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
|
||||
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/type-utils": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -967,22 +967,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
|
||||
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -998,14 +998,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.63.0",
|
||||
"@typescript-eslint/types": "^8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1020,14 +1020,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0"
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1038,9 +1038,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1055,15 +1055,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1080,9 +1080,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1094,16 +1094,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1122,16 +1122,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
|
||||
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0"
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1146,13 +1146,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2069,12 +2069,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
|
||||
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2114,9 +2115,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
||||
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2371,9 +2372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.30",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz",
|
||||
"integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==",
|
||||
"version": "4.12.31",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
|
||||
"integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2419,9 +2420,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2577,9 +2578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
@@ -2593,23 +2594,23 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
"lightningcss-android-arm64": "1.33.0",
|
||||
"lightningcss-darwin-arm64": "1.33.0",
|
||||
"lightningcss-darwin-x64": "1.33.0",
|
||||
"lightningcss-freebsd-x64": "1.33.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.33.0",
|
||||
"lightningcss-linux-arm64-musl": "1.33.0",
|
||||
"lightningcss-linux-x64-gnu": "1.33.0",
|
||||
"lightningcss-linux-x64-musl": "1.33.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.33.0",
|
||||
"lightningcss-win32-x64-msvc": "1.33.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2628,9 +2629,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2649,9 +2650,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2670,9 +2671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2691,9 +2692,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
||||
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2712,9 +2713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2736,9 +2737,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2760,9 +2761,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2784,9 +2785,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2808,9 +2809,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2829,9 +2830,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3024,9 +3025,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||||
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
@@ -3188,9 +3189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.19",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
||||
"version": "8.5.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
|
||||
"integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3208,7 +3209,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -3826,16 +3827,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.4",
|
||||
"postcss": "^8.5.17",
|
||||
"rolldown": "~1.1.5",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
10
package.json
10
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.59.0",
|
||||
"version": "0.60.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -64,16 +64,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.215",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.111.0",
|
||||
"@anthropic-ai/sdk": "0.112.3",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"eslint": "10.7.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.7.0",
|
||||
|
||||
686
src/acp-agent.ts
686
src/acp-agent.ts
@@ -9,10 +9,15 @@ import {
|
||||
CompleteElicitationNotification,
|
||||
CreateElicitationRequest,
|
||||
CreateElicitationResponse,
|
||||
DisableProviderRequest,
|
||||
DisableProviderResponse,
|
||||
ForkSessionRequest,
|
||||
ForkSessionResponse,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
ListProvidersRequest,
|
||||
ListProvidersResponse,
|
||||
LlmProtocol,
|
||||
ListSessionsRequest,
|
||||
ListSessionsResponse,
|
||||
LoadSessionRequest,
|
||||
@@ -25,9 +30,12 @@ import {
|
||||
PermissionOption,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
ProviderInfo,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestError,
|
||||
SetProviderRequest,
|
||||
SetProviderResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
ResumeSessionRequest,
|
||||
@@ -51,6 +59,7 @@ import {
|
||||
AgentInfo,
|
||||
CanUseTool,
|
||||
deleteSession,
|
||||
EffortLevel,
|
||||
FastModeState,
|
||||
getSessionInfo,
|
||||
getSessionMessages,
|
||||
@@ -66,7 +75,6 @@ import {
|
||||
PermissionUpdate,
|
||||
Query,
|
||||
query,
|
||||
Settings,
|
||||
SDKAssistantMessageError,
|
||||
SDKMessage,
|
||||
SDKMessageOrigin,
|
||||
@@ -385,11 +393,32 @@ type Session = {
|
||||
emitRawSDKMessages: boolean | SDKMessageFilter[];
|
||||
/** Context window size of the session's current model, carried across
|
||||
* prompts so mid-stream usage_update notifications report a correct `size`
|
||||
* before the turn's first result message arrives. Seeded from the SDK's
|
||||
* getContextUsage report at session creation (DEFAULT_CONTEXT_WINDOW when
|
||||
* that and the text heuristic both fail), refreshed the same way on model
|
||||
* switches, and confirmed by each result's modelUsage. */
|
||||
* before the turn's first result message arrives. Seeded synchronously at
|
||||
* session creation and on model switches from the per-model cache or the
|
||||
* text heuristic (DEFAULT_CONTEXT_WINDOW when both miss; on session/load the
|
||||
* resumed session's own `getContextUsage` report wins, see
|
||||
* `readResumedLiveModel`), then confirmed — and the cache populated — by each
|
||||
* result's modelUsage. No extra `getContextUsage` IPC is on these paths: on a
|
||||
* fresh session it stalls until the first turn runs (see the seeding call
|
||||
* sites and `contextWindowCache`). */
|
||||
contextWindowSize: number;
|
||||
/** Whether `contextWindowSize` came from an authoritative source (the
|
||||
* cross-session cache, a resumed session's `getContextUsage` report, or a
|
||||
* `result.modelUsage`) rather than the text heuristic / default. Guards the
|
||||
* mid-stream `message_start` heuristic upgrade: an authoritative window that
|
||||
* happens to equal DEFAULT_CONTEXT_WINDOW must not be mistaken for "unseeded"
|
||||
* and clobbered by a "1m" text match. */
|
||||
contextWindowAuthoritative: boolean;
|
||||
/** Stable identifier of the LLM backend this session's query was created
|
||||
* against, derived from the routing-relevant vars of the exact `env` handed
|
||||
* to the SDK at query creation (see {@link providerCacheKeyFor}). The context
|
||||
* window is a property of (model id, backend) — the same resolved model id
|
||||
* can name different windows behind different base URLs, routing headers, or
|
||||
* credentials — so this scopes the module-global `contextWindowCache` per
|
||||
* backend. Captured from the query's own env (not re-resolved later) because
|
||||
* the process-wide provider config can change while a session is being
|
||||
* created, while the query stays baked to the env it was created with. */
|
||||
providerCacheKey: string;
|
||||
/** Accumulated task list for the session, keyed by task ID. Task IDs are
|
||||
* per-session, so this state must not be shared across sessions. */
|
||||
taskState: TaskState;
|
||||
@@ -658,6 +687,50 @@ type GatewayAuthMeta = {
|
||||
|
||||
type GatewayAuthRequest = AuthenticateRequest & { _meta?: GatewayAuthMeta };
|
||||
|
||||
/**
|
||||
* The single provider ID this agent exposes via `providers/*`. Claude Code has
|
||||
* one LLM backend selected by protocol (anthropic / bedrock / vertex), so there
|
||||
* is exactly one configurable provider.
|
||||
*/
|
||||
const PROVIDER_ID = "main";
|
||||
|
||||
/**
|
||||
* Protocols the `main` provider can be configured with. These mirror the
|
||||
* env-var mappings understood by {@link createEnvForProvider}.
|
||||
*/
|
||||
const SUPPORTED_PROTOCOLS: LlmProtocol[] = ["anthropic", "bedrock", "vertex"];
|
||||
|
||||
/**
|
||||
* Vertex needs project + region that the standard `providers/set` payload
|
||||
* (`apiType`/`baseUrl`/`headers`) does not model, so clients pass them through
|
||||
* `_meta.claudeCode.vertex`. Required only when `apiType === "vertex"`.
|
||||
*/
|
||||
type SetProviderMeta = {
|
||||
claudeCode?: {
|
||||
vertex?: {
|
||||
projectId: string;
|
||||
region: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolved, non-secret + secret routing config for the `main` provider. This is
|
||||
* the shared shape produced by both `providers/set` and the legacy gateway auth
|
||||
* path, and consumed by {@link createEnvForProvider}. `null` means the provider
|
||||
* is unconfigured (no client-managed routing in effect).
|
||||
*/
|
||||
type ProviderConfig = {
|
||||
apiType: LlmProtocol;
|
||||
baseUrl: string;
|
||||
headers: Record<string, string>;
|
||||
/** Present only for `apiType === "vertex"`. */
|
||||
vertex?: {
|
||||
projectId: string;
|
||||
region: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extra metadata that the agent provides for each tool_call / tool_update update.
|
||||
*/
|
||||
@@ -1128,6 +1201,10 @@ export class ClaudeAcpAgent {
|
||||
clientCapabilities?: ClientCapabilities;
|
||||
logger: Logger;
|
||||
gatewayAuthRequest?: GatewayAuthRequest;
|
||||
/** Client-managed LLM routing set via `providers/set`. Process-scoped and
|
||||
* never persisted to disk (see the Configurable LLM Providers RFD). When
|
||||
* set, it takes precedence over {@link gatewayAuthRequest}. */
|
||||
providerConfig?: ProviderConfig;
|
||||
/** Grace period before a `session/cancel` forces a wedged prompt loop to
|
||||
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
||||
* tests can shrink it. */
|
||||
@@ -1268,6 +1345,10 @@ export class ClaudeAcpAgent {
|
||||
auth: {
|
||||
logout: {},
|
||||
},
|
||||
// Client-managed LLM routing via `providers/list`, `providers/set`, and
|
||||
// `providers/disable`. Advertised unconditionally; there is no client
|
||||
// capability prerequisite for the provider methods.
|
||||
providers: {},
|
||||
loadSession: true,
|
||||
sessionCapabilities: {
|
||||
additionalDirectories: {},
|
||||
@@ -1405,11 +1486,121 @@ export class ClaudeAcpAgent {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* `providers/list` — returns the single client-configurable custom gateway
|
||||
* provider (`main`). `current` carries only non-secret routing (never headers,
|
||||
* which may hold secrets); only `apiType`/`baseUrl` are surfaced for UI
|
||||
* display, and is `null` when the provider is not configured/disabled. The
|
||||
* provider is optional (`required: false`): while disabled/unconfigured the
|
||||
* agent falls back to its own default routing (normal Claude login).
|
||||
*/
|
||||
async unstable_listProviders(_params: ListProvidersRequest): Promise<ListProvidersResponse> {
|
||||
const config = this.resolveProviderConfig();
|
||||
const provider: ProviderInfo = {
|
||||
providerId: PROVIDER_ID,
|
||||
supported: SUPPORTED_PROTOCOLS,
|
||||
required: false,
|
||||
current: config ? { apiType: config.apiType, baseUrl: config.baseUrl } : null,
|
||||
};
|
||||
return { providers: [provider] };
|
||||
}
|
||||
|
||||
/**
|
||||
* `providers/set` — replace the full configuration for the `main` provider.
|
||||
* Rejects unknown IDs, unsupported protocols, and empty/invalid base URLs with
|
||||
* `invalid_params`. Config is process-scoped and applies to sessions created or
|
||||
* loaded after this call.
|
||||
*/
|
||||
async unstable_setProvider(params: SetProviderRequest): Promise<SetProviderResponse> {
|
||||
if (params.providerId !== PROVIDER_ID) {
|
||||
throw RequestError.invalidParams(
|
||||
{ providerId: params.providerId },
|
||||
`Unknown provider ID "${params.providerId}"; expected "${PROVIDER_ID}".`,
|
||||
);
|
||||
}
|
||||
if (!SUPPORTED_PROTOCOLS.includes(params.apiType)) {
|
||||
throw RequestError.invalidParams(
|
||||
{ apiType: params.apiType, supported: SUPPORTED_PROTOCOLS },
|
||||
`Unsupported apiType "${params.apiType}" for provider "${PROVIDER_ID}".`,
|
||||
);
|
||||
}
|
||||
if (!isValidBaseUrl(params.baseUrl)) {
|
||||
throw RequestError.invalidParams(
|
||||
{ baseUrl: params.baseUrl },
|
||||
"baseUrl must be a non-empty absolute http(s) URL.",
|
||||
);
|
||||
}
|
||||
|
||||
const config: ProviderConfig = {
|
||||
apiType: params.apiType,
|
||||
baseUrl: params.baseUrl,
|
||||
headers: params.headers ?? {},
|
||||
};
|
||||
|
||||
// Vertex requires project + region, which the standard payload cannot
|
||||
// carry, so they arrive via `_meta.claudeCode.vertex`.
|
||||
if (params.apiType === "vertex") {
|
||||
const vertex = (params._meta as SetProviderMeta | undefined)?.claudeCode?.vertex;
|
||||
if (
|
||||
!vertex ||
|
||||
typeof vertex.projectId !== "string" ||
|
||||
vertex.projectId.trim() === "" ||
|
||||
typeof vertex.region !== "string" ||
|
||||
vertex.region.trim() === ""
|
||||
) {
|
||||
throw RequestError.invalidParams(
|
||||
undefined,
|
||||
"vertex apiType requires non-empty `_meta.claudeCode.vertex.projectId` and `_meta.claudeCode.vertex.region`.",
|
||||
);
|
||||
}
|
||||
config.vertex = { projectId: vertex.projectId, region: vertex.region };
|
||||
}
|
||||
|
||||
this.providerConfig = config;
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* `providers/disable` — disabling the `main` provider clears any client-managed
|
||||
* routing (both a `providers/set` config and the legacy gateway auth request),
|
||||
* so the agent reverts to its own default routing and `providers/list` reports
|
||||
* `current: null`. Disabling any other (unknown) ID is treated as a successful
|
||||
* no-op per the RFD's idempotency rule.
|
||||
*/
|
||||
async unstable_disableProvider(params: DisableProviderRequest): Promise<DisableProviderResponse> {
|
||||
if (params.providerId === PROVIDER_ID) {
|
||||
this.providerConfig = undefined;
|
||||
this.gatewayAuthRequest = undefined;
|
||||
}
|
||||
// Unknown provider: idempotent success.
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective client-managed routing config. `providers/set` takes
|
||||
* precedence; otherwise fall back to the legacy gateway auth request. Returns
|
||||
* `null` when neither is configured.
|
||||
*/
|
||||
resolveProviderConfig(): ProviderConfig | null {
|
||||
if (this.providerConfig) {
|
||||
return this.providerConfig;
|
||||
}
|
||||
return gatewayRequestToProviderConfig(this.gatewayAuthRequest);
|
||||
}
|
||||
|
||||
async logout(_params: LogoutRequest): Promise<void> {
|
||||
// Clear in-memory gateway credentials supplied via `authenticate`. The
|
||||
// gateway method never touches the on-disk credential store, so dropping
|
||||
// this reference is the whole logout for that path.
|
||||
// Clear in-memory gateway credentials supplied via `authenticate` and any
|
||||
// provider routing set via `providers/set`. Neither touches the on-disk
|
||||
// credential store, so dropping these references is the whole logout for
|
||||
// those paths.
|
||||
this.gatewayAuthRequest = undefined;
|
||||
this.providerConfig = undefined;
|
||||
// Learned context windows are per-account state too: 1M-context
|
||||
// entitlement is gated per org/tier, and an OAuth re-login is invisible to
|
||||
// the env-derived provider cache key, so windows learned under the old
|
||||
// login must not seed sessions under the next. Worst case of clearing is
|
||||
// re-learning on each model's next turn.
|
||||
contextWindowCache.clear();
|
||||
|
||||
// For the Claude/Console login methods the credentials live in the native
|
||||
// CLI's store (keychain or config dir), which only the binary can clear.
|
||||
@@ -2726,12 +2917,43 @@ export class ClaudeAcpAgent {
|
||||
const matchingModelUsage = lastAssistantModel
|
||||
? getMatchingModelUsage(message.modelUsage, lastAssistantModel)
|
||||
: null;
|
||||
// Only overwrite when we have an authoritative value — a miss
|
||||
// (e.g. a turn with no top-level assistant message) would
|
||||
// otherwise discard the window learned on a prior turn and
|
||||
// leave the next prompt's mid-stream updates reporting 200k.
|
||||
if (matchingModelUsage) {
|
||||
session.contextWindowSize = matchingModelUsage.contextWindow;
|
||||
// Only overwrite when we have an authoritative, sane value. A miss
|
||||
// (e.g. a turn with no top-level assistant message), or a
|
||||
// nonsensical non-positive/NaN window (observed from third-party
|
||||
// backends), would otherwise discard the window learned on a prior
|
||||
// turn and leave the next prompt's mid-stream updates reporting a
|
||||
// wrong size. `cacheContextWindow` applies the same `> 0` guard, so
|
||||
// a bad value never reaches the cross-session cache either.
|
||||
if (
|
||||
matchingModelUsage &&
|
||||
typeof matchingModelUsage.usage.contextWindow === "number" &&
|
||||
matchingModelUsage.usage.contextWindow > 0
|
||||
) {
|
||||
session.contextWindowSize = matchingModelUsage.usage.contextWindow;
|
||||
session.contextWindowAuthoritative = true;
|
||||
// Authoritative: fold it into the cross-session cache keyed on
|
||||
// (this session's provider, the resolved model id —
|
||||
// matchingModelUsage.key, e.g. "claude-sonnet-5[1m]") so a later
|
||||
// session/new or switch on the same provider that resolves to
|
||||
// this model seeds the correct window synchronously, with no
|
||||
// getContextUsage IPC.
|
||||
cacheContextWindow(
|
||||
contextWindowCacheKey(session.providerCacheKey, matchingModelUsage.key),
|
||||
matchingModelUsage.usage.contextWindow,
|
||||
);
|
||||
// Also cache under the assistant message's own (bare) spelling.
|
||||
// Seed-time reads fall back to a picker value / verbatim live id
|
||||
// when a row carries no resolvedModel (the synthesized
|
||||
// out-of-allowlist resume row sets it undefined on purpose), and
|
||||
// those spellings match `.model` from the assistant message, not
|
||||
// the decorated modelUsage key — without this entry such rows
|
||||
// could never hit the cache.
|
||||
if (lastAssistantModel && lastAssistantModel !== matchingModelUsage.key) {
|
||||
cacheContextWindow(
|
||||
contextWindowCacheKey(session.providerCacheKey, lastAssistantModel),
|
||||
matchingModelUsage.usage.contextWindow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Send usage_update notification
|
||||
@@ -3003,11 +3225,18 @@ export class ClaudeAcpAgent {
|
||||
const model = message.event.message.model;
|
||||
if (model && model !== "<synthetic>") {
|
||||
lastAssistantModel = model;
|
||||
// Only upgrade from the default — once the SDK has given us
|
||||
// an authoritative window (seeded at session creation,
|
||||
// refreshed on model switches in `applyConfigOptionValue`,
|
||||
// confirmed by each `result`), trust it over the heuristic.
|
||||
if (session.contextWindowSize === DEFAULT_CONTEXT_WINDOW) {
|
||||
// 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
|
||||
// session/load, confirmed by each `result`), trust it over
|
||||
// the heuristic. The flag, not the value, is the sentinel: an
|
||||
// authoritative window can legitimately equal
|
||||
// DEFAULT_CONTEXT_WINDOW (e.g. a backend serving a 200k lane
|
||||
// under a "[1m]"-spelled id) and must not be clobbered.
|
||||
if (
|
||||
!session.contextWindowAuthoritative &&
|
||||
session.contextWindowSize === DEFAULT_CONTEXT_WINDOW
|
||||
) {
|
||||
const inferred = inferContextWindowFromModel(model);
|
||||
if (inferred !== null) {
|
||||
session.contextWindowSize = inferred;
|
||||
@@ -3339,7 +3568,20 @@ export class ClaudeAcpAgent {
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
toolName: message.tool_name,
|
||||
toolResponse: { elapsedTimeSeconds: message.elapsed_time_seconds },
|
||||
toolResponse: {
|
||||
elapsedTimeSeconds: message.elapsed_time_seconds,
|
||||
// For Agent/Task calls: the subagent's type, and — when
|
||||
// the subagent is waiting out an API rate-limit retry —
|
||||
// the SDK's retry counters (attempt, max_retries,
|
||||
// retry_delay_ms, …), forwarded verbatim so clients can
|
||||
// show why a spawn looks stalled.
|
||||
...(message.subagent_type !== undefined && {
|
||||
subagentType: message.subagent_type,
|
||||
}),
|
||||
...(message.subagent_retry !== undefined && {
|
||||
subagentRetry: message.subagent_retry,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies ToolUpdateMeta,
|
||||
},
|
||||
@@ -4021,7 +4263,11 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
|
||||
canUseTool(sessionId: string): CanUseTool {
|
||||
return async (toolName, toolInput, { signal, suggestions, toolUseID, agentID }) => {
|
||||
return async (
|
||||
toolName,
|
||||
toolInput,
|
||||
{ signal, suggestions, toolUseID, agentID, matchedAskRule },
|
||||
) => {
|
||||
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
|
||||
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
|
||||
const session = this.sessions[sessionId];
|
||||
@@ -4157,7 +4403,14 @@ export class ClaudeAcpAgent {
|
||||
}
|
||||
}
|
||||
|
||||
if (session.modes.currentModeId === "bypassPermissions") {
|
||||
// In bypass mode the CLI skips permission checks itself; the asks that
|
||||
// still reach canUseTool are the ones it insists on prompting for even
|
||||
// under --dangerously-skip-permissions. Keep auto-allowing those —
|
||||
// bypass means bypass — EXCEPT rule-forced asks (`matchedAskRule`): the
|
||||
// user explicitly configured a permissions.ask rule for this tool, and
|
||||
// the SDK's guidance is that hosts running auto-approval must treat such
|
||||
// asks as a human prompt. Fall through to the normal request below.
|
||||
if (session.modes.currentModeId === "bypassPermissions" && !matchedAskRule) {
|
||||
return {
|
||||
behavior: "allow",
|
||||
updatedInput: toolInput,
|
||||
@@ -4401,22 +4654,21 @@ export class ClaudeAcpAgent {
|
||||
// carries no "1m" token.
|
||||
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
||||
if (session.models.currentModelId !== value) {
|
||||
// The cached context window was learned for the previous model. The
|
||||
// SDK is already running the new model here (user-driven switches call
|
||||
// `query.setModel` before this, and the refusal-fallback sync only
|
||||
// reconciles a switch the SDK already made), so ask it for the new
|
||||
// window; fall back to the text heuristic so mid-stream updates
|
||||
// between now and the next `result` reflect the user's selection
|
||||
// instead of the old model's window.
|
||||
session.contextWindowSize =
|
||||
(await fetchContextWindowSize(session.query, this.logger)) ??
|
||||
inferContextWindowFromModel(
|
||||
value,
|
||||
newModelInfo?.resolvedModel,
|
||||
newModelInfo?.displayName,
|
||||
newModelInfo?.description,
|
||||
) ??
|
||||
DEFAULT_CONTEXT_WINDOW;
|
||||
// Seed the new model's context window WITHOUT any 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, else the
|
||||
// default. We deliberately do NOT call `getContextUsage` here — before
|
||||
// a fresh session's first prompt turn that control request is not
|
||||
// serviced (~15s stall, issues #886/#880), and (because SDK control
|
||||
// requests are serialized over one channel) it would drag the awaited
|
||||
// `setModel` down with it. The authoritative window arrives on the
|
||||
// first `result.modelUsage` for the model and is cached from there;
|
||||
// until then a switched-to alias that has never run a turn shows the
|
||||
// heuristic/default window, which self-corrects on its first response
|
||||
// (matches pre-0.59.0 behavior).
|
||||
const seeded = immediateContextWindow(session.providerCacheKey, value, newModelInfo);
|
||||
session.contextWindowSize = seeded.size;
|
||||
session.contextWindowAuthoritative = seeded.authoritative;
|
||||
}
|
||||
session.models = { ...session.models, currentModelId: value };
|
||||
|
||||
@@ -4826,6 +5078,29 @@ export class ClaudeAcpAgent {
|
||||
// the same Map that the streaming message handler will read from.
|
||||
const taskState: TaskState = new Map();
|
||||
|
||||
// The exact env the query will be created with. Built (and the provider
|
||||
// cache key derived from it, below) in one place so the key always
|
||||
// describes the backend this query actually talks to: `providers/set`,
|
||||
// `providers/disable`, and `logout` mutate the process-wide provider
|
||||
// config concurrently, so re-resolving it after any of the awaits between
|
||||
// here and the session registration could disagree with the env baked
|
||||
// into the query.
|
||||
const env = {
|
||||
...process.env,
|
||||
...userProvidedOptions?.env,
|
||||
// Client-managed LLM routing: `providers/set` config wins, else the
|
||||
// legacy gateway auth request. Baked into the query at creation, so it
|
||||
// only affects sessions started after the change (matching the RFD).
|
||||
...createEnvForProvider(this.resolveProviderConfig()),
|
||||
// Opt-in to session state events like when the agent is idle
|
||||
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
||||
};
|
||||
// Scopes the context-window cache to this query's backend (see
|
||||
// `contextWindowCache`). Derived from the same `env` object handed to the
|
||||
// SDK, so per-session `_meta` env routing and ambient process-env routing
|
||||
// are distinguished exactly as the CLI will see them.
|
||||
const providerCacheKey = providerCacheKeyFor(env);
|
||||
|
||||
const options: Options = {
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
@@ -4842,13 +5117,7 @@ export class ClaudeAcpAgent {
|
||||
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
...process.env,
|
||||
...userProvidedOptions?.env,
|
||||
...createEnvForGateway(this.gatewayAuthRequest),
|
||||
// Opt-in to session state events like when the agent is idle
|
||||
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
||||
},
|
||||
env,
|
||||
// Override certain fields that must be controlled by ACP
|
||||
cwd: params.cwd,
|
||||
includePartialMessages: true,
|
||||
@@ -4890,7 +5159,7 @@ export class ClaudeAcpAgent {
|
||||
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
||||
{
|
||||
hooks: [
|
||||
createPostToolUseHook(this.logger, {
|
||||
createPostToolUseHook({
|
||||
onEnterPlanMode: async () => {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
@@ -5015,7 +5284,7 @@ export class ClaudeAcpAgent {
|
||||
)
|
||||
: initializationResult.models;
|
||||
|
||||
const models = await getAvailableModels(
|
||||
const { modelState: models, resumedContextWindow } = await getAvailableModels(
|
||||
q,
|
||||
allowedModels,
|
||||
initializationResult.models,
|
||||
@@ -5139,31 +5408,40 @@ export class ClaudeAcpAgent {
|
||||
initialEffort.currentValue !== "default"
|
||||
) {
|
||||
await q.applyFlagSettings({
|
||||
effortLevel: initialEffort.currentValue as Settings["effortLevel"],
|
||||
effortLevel: toSdkEffortLevel(initialEffort.currentValue),
|
||||
});
|
||||
}
|
||||
// Seed the context window from the SDK's authoritative report. Text
|
||||
// inference alone misses aliases that resolve to extended-context models
|
||||
// with no "1m" token anywhere in their id or description (e.g. `sonnet` →
|
||||
// claude-sonnet-5, natively ~1M): those streamed `usage_update.size:
|
||||
// 200000` until the first result's modelUsage corrected it — again on
|
||||
// every process restart or session re-creation, since the learned window
|
||||
// lives only on the Session (issue #596).
|
||||
// Seed the context window WITHOUT any extra IPC on the session/new path.
|
||||
// On session/load, the resumed session's own `getContextUsage` report — a
|
||||
// response `getAvailableModels` already awaited to learn the live model
|
||||
// (resumed sessions ARE serviced pre-turn, unlike fresh ones) — is
|
||||
// authoritative and wins. Otherwise: the cached authoritative window if a
|
||||
// prior turn has learned it for this model (`result.modelUsage`,
|
||||
// cross-session), else the text heuristic, else the default. We
|
||||
// deliberately do NOT issue a getContextUsage call here: on a fresh
|
||||
// session that control request is not serviced until the first prompt
|
||||
// turn runs, so awaiting it — as 0.59.0 did — made session/new take ~15s
|
||||
// (issues #886/#880). The authoritative window arrives on the first
|
||||
// `result.modelUsage` and is cached from there.
|
||||
//
|
||||
// Text inference alone misses aliases that resolve to extended-context
|
||||
// models with no "1m" token anywhere in their id or description (e.g.
|
||||
// `sonnet` → claude-sonnet-5, natively ~1M): those stream
|
||||
// `usage_update.size: 200000` until the first result's modelUsage corrects
|
||||
// it — but the cache means only the FIRST session to ever run a turn on such
|
||||
// a model eats that window, not every fresh session after a process
|
||||
// restart (issue #596; a post-restart session/load is covered by the
|
||||
// resumed report above).
|
||||
//
|
||||
// The inference fallback is deliberately keyed to the allowlisted entry: a
|
||||
// fallback-resolved sibling's resolvedModel/displayName/description can
|
||||
// describe a different context lane than the verbatim live id (e.g. an
|
||||
// "opus[1m]" row matched for a bare 200k id), so on the fallback path only
|
||||
// the id itself is a trustworthy window signal.
|
||||
const contextWindowSize =
|
||||
(await fetchContextWindowSize(q, this.logger)) ??
|
||||
inferContextWindowFromModel(
|
||||
models.currentModelId,
|
||||
allowlistedModelInfo?.resolvedModel,
|
||||
allowlistedModelInfo?.displayName,
|
||||
allowlistedModelInfo?.description,
|
||||
) ??
|
||||
DEFAULT_CONTEXT_WINDOW;
|
||||
const seededWindow =
|
||||
resumedContextWindow !== null
|
||||
? { size: resumedContextWindow, authoritative: true }
|
||||
: immediateContextWindow(providerCacheKey, models.currentModelId, allowlistedModelInfo);
|
||||
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
@@ -5187,7 +5465,9 @@ export class ClaudeAcpAgent {
|
||||
fastModeEnabled,
|
||||
abortController,
|
||||
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
||||
contextWindowSize,
|
||||
contextWindowSize: seededWindow.size,
|
||||
contextWindowAuthoritative: seededWindow.authoritative,
|
||||
providerCacheKey,
|
||||
taskState,
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -5289,29 +5569,81 @@ function snapshotFromUsage(usage: {
|
||||
};
|
||||
}
|
||||
|
||||
function createEnvForGateway(request?: GatewayAuthRequest) {
|
||||
/**
|
||||
* Adapt a legacy gateway `authenticate` request into the shared
|
||||
* {@link ProviderConfig} shape. Returns `null` when no gateway request is
|
||||
* present. `methodId` selects the protocol: `gateway-bedrock` → bedrock,
|
||||
* otherwise anthropic.
|
||||
*/
|
||||
function gatewayRequestToProviderConfig(request?: GatewayAuthRequest): ProviderConfig | null {
|
||||
if (!request?._meta) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
apiType: request.methodId === "gateway-bedrock" ? "bedrock" : "anthropic",
|
||||
baseUrl: request._meta.gateway.baseUrl,
|
||||
headers: request._meta.gateway.headers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a resolved provider config into the Claude Code env vars that redirect API
|
||||
* traffic and inject headers. Returns an empty object when routing is
|
||||
* unconfigured. The token/bypass placeholders (`" "`) are required so the CLI
|
||||
* skips its normal login/credential checks when a gateway is in use.
|
||||
*/
|
||||
function createEnvForProvider(config: ProviderConfig | null): Record<string, string> {
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
const customHeaders = Object.entries(request._meta.gateway.headers)
|
||||
const customHeaders = Object.entries(config.headers)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n");
|
||||
|
||||
if (request.methodId === "gateway-bedrock") {
|
||||
if (config.apiType === "bedrock") {
|
||||
return {
|
||||
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||
AWS_BEARER_TOKEN_BEDROCK: " ", // Must be non-empty to bypass pass configuration check
|
||||
ANTHROPIC_BEDROCK_BASE_URL: request._meta.gateway.baseUrl,
|
||||
ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
|
||||
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.apiType === "vertex") {
|
||||
// `config.vertex` is guaranteed present for vertex by `unstable_setProvider`
|
||||
// validation; fall back to empty strings defensively.
|
||||
return {
|
||||
CLAUDE_CODE_USE_VERTEX: "1",
|
||||
ANTHROPIC_VERTEX_BASE_URL: config.baseUrl,
|
||||
ANTHROPIC_VERTEX_PROJECT_ID: config.vertex?.projectId ?? "",
|
||||
CLOUD_ML_REGION: config.vertex?.region ?? "",
|
||||
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ANTHROPIC_BASE_URL: request._meta.gateway.baseUrl,
|
||||
ANTHROPIC_BASE_URL: config.baseUrl,
|
||||
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
||||
ANTHROPIC_AUTH_TOKEN: " ", // Must be specified to bypass claude login requirement
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a provider base URL: must be a non-empty absolute http(s) URL.
|
||||
*/
|
||||
function isValidBaseUrl(baseUrl: string): boolean {
|
||||
if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(baseUrl);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the list of permission modes the agent will advertise for the given
|
||||
* model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
|
||||
@@ -5371,9 +5703,12 @@ function buildAvailableModes(modelInfo: ModelInfo | undefined): SessionModeState
|
||||
// and only clears a key when an explicit `null` is sent — see
|
||||
// `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
|
||||
// `"default"` sentinel and `undefined` (effort option absent for the model) to
|
||||
// `null` ensures any previously-applied flag is actually cleared.
|
||||
function toSdkEffortLevel(value: string | undefined): Settings["effortLevel"] | null {
|
||||
return value === undefined || value === "default" ? null : (value as Settings["effortLevel"]);
|
||||
// `null` ensures any previously-applied flag is actually cleared. Typed as
|
||||
// `EffortLevel` (not `Settings["effortLevel"]`): the picker offers whatever
|
||||
// `supportedEffortLevels` reports, which includes the session-scoped `"max"`
|
||||
// that the persisted Settings shape deliberately excludes.
|
||||
function toSdkEffortLevel(value: string | undefined): EffortLevel | null {
|
||||
return value === undefined || value === "default" ? null : (value as EffortLevel);
|
||||
}
|
||||
|
||||
// `supportedAgents()` always returns Claude Code's built-in subagents — the
|
||||
@@ -5899,20 +6234,29 @@ export function applyAvailableModelsAllowlist(
|
||||
|
||||
/** Read the model a resumed session is actually running (via the
|
||||
* `getContextUsage` control request — the same source `/context` prints) and
|
||||
* map it onto the picker. Best-effort: a control-request failure is logged
|
||||
* and returns null so callers keep their current choice; failing the whole
|
||||
* session/load over an unreadable report would be worse. */
|
||||
* map it onto the picker, along with the report's authoritative context
|
||||
* window (`rawMaxTokens`). Resumed sessions get this request serviced before
|
||||
* any turn runs in the new process — unlike fresh sessions, where it stalls
|
||||
* until the first prompt turn (issues #886/#880) — so the same response that
|
||||
* restores the live model (issue #845) also seeds the window for free,
|
||||
* covering post-restart reloads of models the text heuristic misses (issue
|
||||
* #596). Best-effort: a control-request failure is logged and returns nulls
|
||||
* so callers keep their current choice; failing the whole session/load over
|
||||
* an unreadable report would be worse. */
|
||||
async function readResumedLiveModel(
|
||||
query: Query,
|
||||
models: ModelInfo[],
|
||||
logger: Logger,
|
||||
): Promise<ModelInfo | null> {
|
||||
): Promise<{ model: ModelInfo | null; contextWindow: number | null }> {
|
||||
try {
|
||||
const liveModel = (await query.getContextUsage()).model;
|
||||
return liveModel ? matchResumedModel(models, liveModel) : null;
|
||||
const usage = await query.getContextUsage();
|
||||
return {
|
||||
model: usage.model ? matchResumedModel(models, usage.model) : null,
|
||||
contextWindow: usage.rawMaxTokens > 0 ? usage.rawMaxTokens : null,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Failed to read the resumed session's live model:", error);
|
||||
return null;
|
||||
return { model: null, contextWindow: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5923,11 +6267,16 @@ async function getAvailableModels(
|
||||
settingsManager: SettingsManager,
|
||||
logger: Logger,
|
||||
isResumedSession: boolean,
|
||||
): Promise<SessionModelState> {
|
||||
): Promise<{ modelState: SessionModelState; resumedContextWindow: number | null }> {
|
||||
const settings = settingsManager.getSettings();
|
||||
|
||||
let currentModel = models[0];
|
||||
let resolvedFromInput: string | undefined;
|
||||
// The context window reported alongside a resumed session's live model.
|
||||
// Only ever non-null on the paths where `currentModel` IS the live model
|
||||
// (no override, or a failed override re-assert), so the window always
|
||||
// describes the model the session actually runs.
|
||||
let resumedContextWindow: number | null = null;
|
||||
|
||||
// Model priority (highest to lowest):
|
||||
// 1. ANTHROPIC_MODEL environment variable
|
||||
@@ -5956,7 +6305,9 @@ async function getAvailableModels(
|
||||
// the SDK is already running this model, and pushing a picker alias back
|
||||
// (e.g. "opus[1m]") could change the live model rather than describe it.
|
||||
if (resolvedFromInput === undefined && isResumedSession) {
|
||||
currentModel = (await readResumedLiveModel(query, models, logger)) ?? currentModel;
|
||||
const live = await readResumedLiveModel(query, models, logger);
|
||||
currentModel = live.model ?? currentModel;
|
||||
resumedContextWindow = live.contextWindow;
|
||||
}
|
||||
|
||||
// Skip the setModel round-trip when we can prove the SDK has already landed
|
||||
@@ -5989,17 +6340,22 @@ async function getAvailableModels(
|
||||
// pin the session isn't running.
|
||||
if (!isResumedSession) throw error;
|
||||
logger.error(`Failed to re-assert model "${currentModel.value}" on resume:`, error);
|
||||
currentModel = (await readResumedLiveModel(query, models, logger)) ?? currentModel;
|
||||
const live = await readResumedLiveModel(query, models, logger);
|
||||
currentModel = live.model ?? currentModel;
|
||||
resumedContextWindow = live.contextWindow;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
availableModels: models.map((model) => ({
|
||||
modelId: model.value,
|
||||
name: model.displayName,
|
||||
description: model.description,
|
||||
})),
|
||||
currentModelId: currentModel.value,
|
||||
modelState: {
|
||||
availableModels: models.map((model) => ({
|
||||
modelId: model.value,
|
||||
name: model.displayName,
|
||||
description: model.description,
|
||||
})),
|
||||
currentModelId: currentModel.value,
|
||||
},
|
||||
resumedContextWindow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6130,6 +6486,12 @@ export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
|
||||
},
|
||||
session_id: prompt.sessionId,
|
||||
parent_tool_use_id: null,
|
||||
// ACP prompts are the user's own input relayed by the client. Stamp the
|
||||
// provenance explicitly: per the SDK, a host wrapping keyboard input must
|
||||
// send `{kind: "human"}` — an absent `origin` is treated as unattributed
|
||||
// and fails closed at the CLI's strict isHuman() trust gates (e.g. the
|
||||
// ultracode keyword opt-in honors only human-originated turns).
|
||||
origin: { kind: "human" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6872,6 +7234,9 @@ export function runAcp() {
|
||||
agent.setSessionConfigOption(ctx.params),
|
||||
)
|
||||
.onRequest(methods.agent.authenticate, (ctx) => agent.authenticate(ctx.params))
|
||||
.onRequest(methods.agent.providers.list, (ctx) => agent.unstable_listProviders(ctx.params))
|
||||
.onRequest(methods.agent.providers.set, (ctx) => agent.unstable_setProvider(ctx.params))
|
||||
.onRequest(methods.agent.providers.disable, (ctx) => agent.unstable_disableProvider(ctx.params))
|
||||
.onRequest(methods.agent.logout, (ctx) => agent.logout(ctx.params))
|
||||
.onRequest(methods.agent.session.prompt, (ctx) =>
|
||||
runPromptWithCancellation(agent, ctx.params, ctx.signal),
|
||||
@@ -6891,9 +7256,9 @@ function commonPrefixLength(a: string, b: string) {
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Best-effort first guess of a model's context window, used only as a
|
||||
* fallback when the SDK's authoritative `getContextUsage` is unavailable (and
|
||||
* until a `result` message arrives with the `modelUsage` value).
|
||||
/** Best-effort first guess of a model's context window, used to seed the
|
||||
* window synchronously (via `immediateContextWindow`) until a `result` message
|
||||
* arrives with the authoritative `modelUsage` value.
|
||||
*
|
||||
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
|
||||
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
|
||||
@@ -6903,9 +7268,12 @@ function commonPrefixLength(a: string, b: string) {
|
||||
* "claude-opus-4-8[1m]", "Opus 4.7 (1M context)"), so callers pass those too.
|
||||
* This text scan can't catch every model — some resolve to extended-context
|
||||
* models with no "1m" anywhere (e.g. `sonnet` → claude-sonnet-5, natively
|
||||
* ~1M) — which is why `fetchContextWindowSize` is preferred wherever a live
|
||||
* query is available. A miss falls back to the default window and is
|
||||
* corrected by `result.modelUsage` within one turn. */
|
||||
* ~1M). Such a miss falls back to the default window and is corrected by
|
||||
* `result.modelUsage` (and cached) within one turn. We do NOT consult the
|
||||
* SDK's `getContextUsage` to close that gap: on a fresh session it is not
|
||||
* serviced before the first prompt turn (issues #886/#880, see
|
||||
* `contextWindowCache`; resumed sessions do get it, via
|
||||
* `readResumedLiveModel`). */
|
||||
function inferContextWindowFromModel(...texts: Array<string | undefined>): number | null {
|
||||
if (texts.some((text) => text != null && /\b1m\b/i.test(text))) return 1_000_000;
|
||||
return null;
|
||||
@@ -6926,28 +7294,112 @@ async function fetchContextUsedTokens(query: Query, logger: Logger): Promise<num
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch the current model's full context window (`rawMaxTokens`) via the
|
||||
* `getContextUsage` control request — the same source `/context` prints.
|
||||
* This is the only pre-`result` signal that covers semantic aliases whose
|
||||
* text carries no "1m" token (e.g. `sonnet` → claude-sonnet-5, natively
|
||||
* ~1M), so it's the primary window source at session creation and on model
|
||||
* switches; `inferContextWindowFromModel` remains the fallback. A returned
|
||||
* window is still superseded by each `result.modelUsage.contextWindow`.
|
||||
/** Cross-session cache of authoritative context windows, keyed by
|
||||
* `${providerCacheKey}\0${modelId}` (see {@link contextWindowCacheKey}).
|
||||
* The window is a property of (model id, backend): the same resolved model id
|
||||
* (e.g. "claude-sonnet-5[1m]", the spelling of the `result.modelUsage` keys)
|
||||
* can name different context lanes behind different base URLs, routing
|
||||
* headers, or credentials, so the key carries both. Caching it module-level
|
||||
* lets a later session/new or switch that resolves to the same (backend,
|
||||
* model) — in this session or any other, within the adapter's lifetime — seed
|
||||
* the correct window synchronously with no IPC. Keying on the resolved id
|
||||
* (rather than the picker value) means aliases that resolve to the same
|
||||
* concrete model share one entry; the result handler additionally writes the
|
||||
* bare assistant-message spelling so seed-time reads that fall back to a
|
||||
* verbatim live id (rows without `resolvedModel`) can hit too.
|
||||
*
|
||||
* (Older CLIs under-reported extended 1M windows here — commit 20ef663
|
||||
* dropped the field for that reason — but the CLI vendored by the pinned
|
||||
* SDK reports them correctly again.) Returns `null` on any control-request
|
||||
* failure or a nonsensical (non-positive) window. */
|
||||
async function fetchContextWindowSize(query: Query, logger: Logger): Promise<number | null> {
|
||||
try {
|
||||
const usage = await query.getContextUsage();
|
||||
return usage.rawMaxTokens > 0 ? usage.rawMaxTokens : null;
|
||||
} catch (error) {
|
||||
logger.error("Failed to fetch context window size from SDK:", error);
|
||||
return null;
|
||||
* Populated authoritatively by each `result.modelUsage` a turn confirms (see
|
||||
* the consumer's result handler). We deliberately never populate it from a
|
||||
* fresh session's `getContextUsage`: before that session's first prompt turn
|
||||
* has run the control request is not serviced (it stalls ~15s, and serializes
|
||||
* ahead of an awaited `setModel` — issues #886/#880, regressed in 0.59.0), so
|
||||
* it can neither beat the first `result` nor be issued cheaply before one.
|
||||
* Resumed sessions are the exception — their report IS serviced pre-turn, and
|
||||
* the session/load path seeds (but does not cache) the window from the same
|
||||
* response that restores the live model, see `readResumedLiveModel`.
|
||||
* Cleared on `logout`: 1M-context entitlement can differ per account/tier, so
|
||||
* windows learned under one login must not seed sessions under the next. */
|
||||
const contextWindowCache = new Map<string, number>();
|
||||
|
||||
/** The env vars that determine which LLM backend — and which context lane on
|
||||
* it — a query's API traffic reaches: endpoint selection (base URLs and the
|
||||
* Bedrock/Vertex switches with their project/region), routing/beta headers
|
||||
* (an `anthropic-beta: context-1m-…` header flips the same model id at the
|
||||
* same endpoint between context lanes), and credential identity (extended
|
||||
* context is entitlement-gated per account). Used to derive the
|
||||
* provider-cache key from the exact env a query is created with, so
|
||||
* `providers/set` config, per-session `_meta` env overrides, and ambient
|
||||
* process env are all distinguished exactly as the CLI will see them. */
|
||||
const PROVIDER_ROUTING_ENV_VARS = [
|
||||
"ANTHROPIC_BASE_URL",
|
||||
"ANTHROPIC_BEDROCK_BASE_URL",
|
||||
"ANTHROPIC_VERTEX_BASE_URL",
|
||||
"CLAUDE_CODE_USE_BEDROCK",
|
||||
"CLAUDE_CODE_USE_VERTEX",
|
||||
"ANTHROPIC_VERTEX_PROJECT_ID",
|
||||
"CLOUD_ML_REGION",
|
||||
"AWS_REGION",
|
||||
"ANTHROPIC_CUSTOM_HEADERS",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
] as const;
|
||||
|
||||
/** Stable identifier for the LLM backend a session's query is created against,
|
||||
* used to scope {@link contextWindowCache} per backend. Positional `\0`-join
|
||||
* of {@link PROVIDER_ROUTING_ENV_VARS} values, so no segment can masquerade
|
||||
* as another and unset vars everywhere yield one stable "default" bucket.
|
||||
* Header/credential values can be secrets; the key only ever lives as an
|
||||
* in-memory Map key and is never logged or surfaced. Over-keying is the safe
|
||||
* side: a var change that didn't really change the backend costs one cache
|
||||
* miss (heuristic seed until the next result), while under-keying would serve
|
||||
* one backend's window for another's. */
|
||||
function providerCacheKeyFor(env: Record<string, string | undefined>): string {
|
||||
return PROVIDER_ROUTING_ENV_VARS.map((name) => env[name] ?? "").join("\0");
|
||||
}
|
||||
|
||||
/** Compose the `contextWindowCache` key from a session's provider key and a
|
||||
* model id. `\0`-joined so the model segment can't collide with a provider
|
||||
* segment. */
|
||||
function contextWindowCacheKey(providerCacheKey: string, modelId: string): string {
|
||||
return `${providerCacheKey}\0${modelId}`;
|
||||
}
|
||||
|
||||
function cacheContextWindow(modelKey: string, window: number): void {
|
||||
if (window > 0) {
|
||||
contextWindowCache.set(modelKey, window);
|
||||
}
|
||||
}
|
||||
|
||||
/** The context window to report *right now* for a model, with NO IPC on the
|
||||
* critical path: the cached authoritative value if we've learned it (from a
|
||||
* prior turn's `result.modelUsage`, this or any session on the same backend),
|
||||
* else the text heuristic over the model row's identity strings, else the
|
||||
* default. Derives the cache key itself — `modelInfo?.resolvedModel ?? modelId`,
|
||||
* the same rule at every seed site — so read keys can't drift from the write
|
||||
* site's spelling. `authoritative` reports whether the value came from the
|
||||
* cache: an authoritative window can legitimately equal
|
||||
* DEFAULT_CONTEXT_WINDOW, so the value alone can't tell the caller. */
|
||||
function immediateContextWindow(
|
||||
providerCacheKey: string,
|
||||
modelId: string,
|
||||
modelInfo?: Pick<ModelInfo, "resolvedModel" | "displayName" | "description">,
|
||||
): { size: number; authoritative: boolean } {
|
||||
const cached = contextWindowCache.get(
|
||||
contextWindowCacheKey(providerCacheKey, modelInfo?.resolvedModel ?? modelId),
|
||||
);
|
||||
if (cached !== undefined) return { size: cached, authoritative: true };
|
||||
return {
|
||||
size:
|
||||
inferContextWindowFromModel(
|
||||
modelId,
|
||||
modelInfo?.resolvedModel,
|
||||
modelInfo?.displayName,
|
||||
modelInfo?.description,
|
||||
) ?? DEFAULT_CONTEXT_WINDOW,
|
||||
authoritative: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Translate the legacy `MAX_THINKING_TOKENS` env var into the SDK's `thinking`
|
||||
* option. The `maxThinkingTokens` option it used to feed is deprecated and
|
||||
* reduced to on/off on current models, so map the value to explicit thinking
|
||||
@@ -6994,6 +7446,12 @@ function getMatchingModelUsage(modelUsage: Record<string, ModelUsage>, currentMo
|
||||
}
|
||||
|
||||
if (bestKey) {
|
||||
return modelUsage[bestKey];
|
||||
// `bestKey` is the SDK's resolved model id (e.g. "claude-sonnet-5[1m]"),
|
||||
// the same spelling as ModelInfo.resolvedModel — the primary key the
|
||||
// window is cached under. `currentModel` (the assistant message's
|
||||
// `.model`) can be the bare form (e.g. "claude-sonnet-5"); the result
|
||||
// handler caches under that spelling too, for seed-time reads that fall
|
||||
// back to a bare id (rows without `resolvedModel`).
|
||||
return { key: bestKey, usage: modelUsage[bestKey] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ function mockSessionState(overrides: Record<string, any> = {}) {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -1947,6 +1949,8 @@ describe("permission request cancellation", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -2300,6 +2304,63 @@ describe("tool_call emitted before permission request", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canUseTool in bypassPermissions mode", () => {
|
||||
function setup() {
|
||||
const events: string[] = [];
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
requestPermission: async () => {
|
||||
events.push("permission");
|
||||
return { outcome: { outcome: "selected", optionId: "allow" } };
|
||||
},
|
||||
} as unknown as AcpClient;
|
||||
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
agent.sessions["session-1"] = mockSessionState({
|
||||
modes: { currentModeId: "bypassPermissions", availableModes: [] },
|
||||
});
|
||||
// The tool_call was already surfaced by the streamed tool_use chunk, so
|
||||
// the permission flow (when taken) goes straight to requestPermission.
|
||||
agent.sessions["session-1"]!.emittedToolCalls.add("tool-1");
|
||||
return { agent, events };
|
||||
}
|
||||
|
||||
it("auto-allows asks that carry no matchedAskRule", async () => {
|
||||
const { agent, events } = setup();
|
||||
|
||||
const result = await agent.canUseTool("session-1")("Bash", { command: "ls" }, {
|
||||
signal: new AbortController().signal,
|
||||
suggestions: [],
|
||||
toolUseID: "tool-1",
|
||||
} as any);
|
||||
|
||||
expect(events).toEqual([]);
|
||||
expect(result).toMatchObject({ behavior: "allow" });
|
||||
});
|
||||
|
||||
// The asks that still reach canUseTool in bypass mode are the ones the CLI
|
||||
// insists on prompting for even under --dangerously-skip-permissions. When
|
||||
// one was forced by the user's own permissions.ask rule (matchedAskRule),
|
||||
// honoring that rule beats bypass: it must go to the client instead of
|
||||
// being silently auto-allowed.
|
||||
it("prompts the client when the ask was forced by a permissions.ask rule", async () => {
|
||||
const { agent, events } = setup();
|
||||
|
||||
const result = await agent.canUseTool("session-1")("Bash", { command: "terraform destroy" }, {
|
||||
signal: new AbortController().signal,
|
||||
suggestions: [],
|
||||
toolUseID: "tool-1",
|
||||
matchedAskRule: {
|
||||
source: "projectSettings",
|
||||
toolName: "Bash",
|
||||
ruleContent: "Bash(terraform:*)",
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(events).toEqual(["permission"]);
|
||||
expect(result).toMatchObject({ behavior: "allow" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("subagent permission attribution (issue #851)", () => {
|
||||
// A background subagent's permission requests reach canUseTool with only an
|
||||
// `agentID`; the streamed subagent messages carry `parent_tool_use_id`
|
||||
@@ -3730,6 +3791,8 @@ describe("session/close", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -3820,6 +3883,8 @@ describe("session/delete", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -3927,6 +3992,8 @@ describe("getOrCreateSession param change detection", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -5045,6 +5112,359 @@ describe("usage_update computation", () => {
|
||||
expect(usageUpdate.update.size).toBe(200000);
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
});
|
||||
|
||||
it("caches the turn's authoritative window under the resolved id and serves it on a later switch, with no getContextUsage IPC", async () => {
|
||||
// End-to-end for the cross-session context-window cache:
|
||||
// - WRITE: a turn's result.modelUsage is the only authoritative window. The
|
||||
// assistant message reports the BARE model id ("…-9") while modelUsage is
|
||||
// keyed by the RESOLVED id ("…-9[1m]"); the cache must be written under the
|
||||
// resolved key (matched by getMatchingModelUsage), the same spelling as
|
||||
// ModelInfo.resolvedModel — otherwise a later read never hits.
|
||||
// - READ: switching to a picker value whose resolvedModel is that key seeds
|
||||
// the window synchronously from the cache, with NO getContextUsage.
|
||||
// 777_000 is chosen so it can only come from the cache: text inference on the
|
||||
// resolved id "…-9[1m]" would yield 1_000_000 (the "1m" token), the default
|
||||
// is 200_000, and the pre-switch sentinel is 123_456.
|
||||
//
|
||||
// The `contextWindowCache` is module-global and this file does not
|
||||
// vi.resetModules() per test, so it persists across tests. This test stays
|
||||
// isolated by using a unique resolved id ("claude-cachehit-probe-9[1m]") that
|
||||
// no other test writes or reads — the convention here, since a statically
|
||||
// imported module's cache can't be cleared from a test.
|
||||
const RESOLVED_ID = "claude-cachehit-probe-9[1m]";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: "claude-cachehit-probe-9" }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[RESOLVED_ID]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 777_000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
const session = agent.sessions["test-session"]!;
|
||||
// The turn learned the window from modelUsage even though the assistant
|
||||
// message carried the bare id — confirms the write matched on the resolved key.
|
||||
expect(session.contextWindowSize).toBe(777_000);
|
||||
|
||||
// Now switch to a picker value that resolves to the cached id. Seed a
|
||||
// sentinel and a getContextUsage spy first: a cache miss would surface as
|
||||
// 1_000_000 (inference on the "1m" id), and any IPC as a spy call.
|
||||
const getContextUsage = vi.fn(async () => ({ rawMaxTokens: 200000 }));
|
||||
(session.query as any).getContextUsage = getContextUsage;
|
||||
session.contextWindowSize = 123_456;
|
||||
session.models = { currentModelId: "default", availableModels: [] };
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "probe-alias",
|
||||
displayName: "Probe",
|
||||
description: "probe model",
|
||||
resolvedModel: RESOLVED_ID,
|
||||
},
|
||||
] as any;
|
||||
session.configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: "probe-alias", name: "Probe" }],
|
||||
},
|
||||
] as any;
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: "probe-alias",
|
||||
});
|
||||
|
||||
expect(getContextUsage).not.toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(777_000);
|
||||
});
|
||||
|
||||
it("scopes the window cache per provider: a switch on a different provider does not read another provider's window", async () => {
|
||||
// The window is a property of (model id, provider). A turn on provider-A
|
||||
// learns 500_000 for RESOLVED_ID; a switch to the SAME resolved id on
|
||||
// provider-B must NOT read it (falls to inference → 1_000_000 for the "1m"
|
||||
// id), while a switch on provider-A DOES read it (500_000). All three
|
||||
// outcomes are distinct: cached 500_000 vs inference 1_000_000 vs default.
|
||||
// Uses a unique resolved id ("claude-provkey-probe[1m]") so the module-global
|
||||
// cache (not reset per test in this file) can't cross this test with others.
|
||||
const RESOLVED_ID = "claude-provkey-probe[1m]";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
|
||||
// Provider-A session learns the authoritative window on a turn.
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: "claude-provkey-probe" }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[RESOLVED_ID]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 500_000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const sessionA = agent.sessions["test-session"]!;
|
||||
sessionA.providerCacheKey = "apiType-A https://a.example";
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
expect(sessionA.contextWindowSize).toBe(500_000);
|
||||
|
||||
const modelInfos = [
|
||||
{
|
||||
value: "alias",
|
||||
displayName: "Alias",
|
||||
description: "probe model",
|
||||
resolvedModel: RESOLVED_ID,
|
||||
},
|
||||
];
|
||||
const configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: "alias", name: "Alias" }],
|
||||
},
|
||||
];
|
||||
|
||||
// A DIFFERENT provider seeing the same resolved id must not inherit A's
|
||||
// window — it falls to inference (1_000_000), not the cached 500_000.
|
||||
agent.sessions["session-B"] = mockSessionState({
|
||||
providerCacheKey: "apiType-B https://b.example",
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos,
|
||||
configOptions,
|
||||
query: {
|
||||
setModel: vi.fn(async () => {}),
|
||||
setPermissionMode: vi.fn(async () => {}),
|
||||
applyFlagSettings: vi.fn(async () => {}),
|
||||
getContextUsage: vi.fn(async () => ({ rawMaxTokens: 200000 })),
|
||||
supportedCommands: vi.fn(async () => []),
|
||||
},
|
||||
});
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "session-B",
|
||||
configId: "model",
|
||||
value: "alias",
|
||||
});
|
||||
expect(agent.sessions["session-B"]!.contextWindowSize).toBe(1_000_000);
|
||||
|
||||
// The SAME provider (A) switching to that id DOES read the cached window.
|
||||
sessionA.contextWindowSize = 123_456; // sentinel
|
||||
sessionA.models = { currentModelId: "default", availableModels: [] };
|
||||
sessionA.modelInfos = modelInfos as any;
|
||||
sessionA.configOptions = configOptions as any;
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: "alias",
|
||||
});
|
||||
expect(sessionA.contextWindowSize).toBe(500_000);
|
||||
});
|
||||
|
||||
it("ignores a nonsensical (non-positive) reported window: keeps the prior window and doesn't poison the cache", async () => {
|
||||
// A result.modelUsage that reports a non-positive contextWindow (observed
|
||||
// from third-party backends) must not overwrite the window learned earlier,
|
||||
// nor be written to the cross-session cache. Unique id keeps this isolated
|
||||
// from other tests sharing the module-global cache (no resetModules here).
|
||||
const RESOLVED_ID = "claude-nonpositive-probe[1m]";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: "claude-nonpositive-probe" }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[RESOLVED_ID]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 0, // nonsensical
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
session.contextWindowSize = 900_000; // a window learned on a prior turn
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
// The bad window was ignored — the prior window is preserved.
|
||||
expect(session.contextWindowSize).toBe(900_000);
|
||||
|
||||
// And it never reached the cache: a switch to that id falls to inference
|
||||
// (1_000_000 for the "1m" id), not the bad 0.
|
||||
session.contextWindowSize = 123_456; // sentinel
|
||||
session.models = { currentModelId: "default", availableModels: [] };
|
||||
session.modelInfos = [
|
||||
{ value: "alias", displayName: "Alias", description: "probe", resolvedModel: RESOLVED_ID },
|
||||
] as any;
|
||||
session.configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: "alias", name: "Alias" }],
|
||||
},
|
||||
] as any;
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: "alias",
|
||||
});
|
||||
expect(session.contextWindowSize).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("does not let the message_start heuristic clobber an authoritative 200k window", async () => {
|
||||
// An authoritative window can legitimately equal DEFAULT_CONTEXT_WINDOW
|
||||
// (e.g. a third-party backend serving a 200k lane under a "[1m]"-spelled
|
||||
// id). The message_start upgrade must key off the authoritative flag, not
|
||||
// the value — otherwise the "1m" text match overwrites the cache-seeded
|
||||
// 200k with 1M mid-stream on every turn.
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createStreamEvent("message_start", {
|
||||
model: "claude-authprobe-1[1m]",
|
||||
usage: {
|
||||
input_tokens: 100,
|
||||
output_tokens: 10,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
}),
|
||||
// Empty modelUsage: the result settles the turn without supplying a
|
||||
// window of its own, so the assertion isolates the message_start path.
|
||||
createResultMessageWithModel({ modelUsage: {} }),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
// Simulate a cache-seeded authoritative window that equals the default.
|
||||
session.contextWindowSize = 200000;
|
||||
session.contextWindowAuthoritative = true;
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
const usageUpdates = updates.filter((u: any) => u.update?.sessionUpdate === "usage_update");
|
||||
for (const u of usageUpdates) {
|
||||
expect(u.update.size).toBe(200000);
|
||||
}
|
||||
});
|
||||
|
||||
it("still upgrades a heuristic default window from the message_start model id", async () => {
|
||||
// Companion to the authoritative-200k test: with no authoritative seed the
|
||||
// old behavior stands — a "1m"-carrying live model id upgrades the default
|
||||
// mid-stream so usage_update reports the right size before the result.
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createStreamEvent("message_start", {
|
||||
model: "claude-authprobe-2[1m]",
|
||||
usage: {
|
||||
input_tokens: 100,
|
||||
output_tokens: 10,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
}),
|
||||
// Empty modelUsage: settles the turn without a window of its own.
|
||||
createResultMessageWithModel({ modelUsage: {} }),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
expect(session.contextWindowAuthoritative).toBe(false);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
expect(session.contextWindowSize).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("caches the turn's window under the bare assistant-message id too, so resolvedModel-less rows can hit", async () => {
|
||||
// Seed-time reads fall back to the picker value / verbatim live id when a
|
||||
// model row carries no resolvedModel (the synthesized out-of-allowlist
|
||||
// resume row sets it undefined on purpose). Those spellings match the
|
||||
// assistant message's bare `.model`, not the "[1m]"-decorated modelUsage
|
||||
// key, so the result handler must write both spellings — otherwise such
|
||||
// rows silently never hit the cache. 555_000 can only come from the cache:
|
||||
// inference on the bare id (no "1m" token) yields the 200_000 default, and
|
||||
// the pre-switch sentinel is 123_456.
|
||||
const BARE_ID = "claude-bareprobe-7";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: BARE_ID }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[`${BARE_ID}[1m]`]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 555_000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
expect(session.contextWindowSize).toBe(555_000);
|
||||
|
||||
// Switch to a row registered under the bare id with NO resolvedModel —
|
||||
// the shape of the out-of-allowlist resume row.
|
||||
session.contextWindowSize = 123_456; // sentinel
|
||||
session.contextWindowAuthoritative = false;
|
||||
session.models = { currentModelId: "default", availableModels: [] };
|
||||
session.modelInfos = [{ value: BARE_ID, displayName: "Bare", description: "probe" }] as any;
|
||||
session.configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: BARE_ID, name: "Bare" }],
|
||||
},
|
||||
] as any;
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: BARE_ID,
|
||||
});
|
||||
|
||||
expect(session.contextWindowSize).toBe(555_000);
|
||||
expect(session.contextWindowAuthoritative).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assembled assistant text fallback", () => {
|
||||
@@ -6396,6 +6816,8 @@ describe("post-error recovery", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -9107,6 +9529,8 @@ describe("session/cancel wedge recovery (issue #680)", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -10376,6 +10800,8 @@ describe("agent selection config option", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
let capturedOptions: Options | undefined;
|
||||
let contextUsageResult: (() => Promise<{ rawMaxTokens: number }>) | undefined;
|
||||
let contextUsageResult: (() => Promise<{ rawMaxTokens: number; model?: string }>) | undefined;
|
||||
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",
|
||||
@@ -625,38 +625,101 @@ describe("createSession options merging", () => {
|
||||
return (agent as unknown as { sessions: Record<string, any> }).sessions[sessionId];
|
||||
}
|
||||
|
||||
it("seeds contextWindowSize from the SDK's getContextUsage report", async () => {
|
||||
// Aliases like `sonnet` can resolve to an extended-context model with no
|
||||
// "1m" token anywhere in id/displayName/description, so the authoritative
|
||||
// report must win over text inference (issue #596).
|
||||
it("does not call getContextUsage during session creation", async () => {
|
||||
// getContextUsage stalls until the session's first prompt turn has run
|
||||
// (it is not serviced pre-turn), so session/new must never call it —
|
||||
// awaiting it inline is what regressed session/new latency in 0.59.0.
|
||||
const ctxSpy = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
contextUsageResult = ctxSpy;
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(ctxSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("seeds contextWindowSize from text inference, falling back to the default when it misses", async () => {
|
||||
// The mock model ("claude-sonnet-4-6" / "Claude Sonnet" / "Fast") carries
|
||||
// no "1m" token anywhere, so inference misses and the window falls back to
|
||||
// the default; the authoritative value arrives later via result.modelUsage.
|
||||
contextUsageResult = async () => ({ rawMaxTokens: 967000 });
|
||||
|
||||
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(967000);
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
|
||||
expect(sessionFor(response.sessionId).contextWindowAuthoritative).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the default window when getContextUsage fails and inference misses", async () => {
|
||||
// getContextUsage rejects, and the mock model ("claude-sonnet-4-6" /
|
||||
// "Claude Sonnet" / "Fast") has no "1m" hint.
|
||||
contextUsageResult = () => Promise.reject(new Error("no context usage mocked"));
|
||||
// The rejection is deliberate — capture the agent's warning instead of
|
||||
// letting it hit the console.
|
||||
const errorSpy = vi.fn();
|
||||
(agent as any).logger = { log: () => {}, error: errorSpy };
|
||||
it("session/load seeds the window from the resumed session's getContextUsage report", async () => {
|
||||
// Resumed sessions get getContextUsage serviced pre-turn (issue #845 uses
|
||||
// it to restore the live model), and the same response carries the
|
||||
// authoritative window (`rawMaxTokens`). After a process restart the
|
||||
// module cache is empty and text inference misses natively-1M aliases, so
|
||||
// discarding this in-hand value would replay the issue-#596 flicker on
|
||||
// every reload — the flagship scenario. 888_000 can only come from the
|
||||
// report: inference on the mock model yields null → 200_000 default.
|
||||
contextUsageResult = async () => ({ rawMaxTokens: 888_000, model: "claude-sonnet-4-6" });
|
||||
|
||||
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
await (
|
||||
agent as unknown as {
|
||||
createSession: (params: object, opts: { resume?: string }) => Promise<unknown>;
|
||||
}
|
||||
).createSession({ cwd: process.cwd(), mcpServers: [] }, { resume: "resumed-window-probe" });
|
||||
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
const session = sessionFor("resumed-window-probe");
|
||||
expect(session.contextWindowSize).toBe(888_000);
|
||||
expect(session.contextWindowAuthoritative).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a nonsensical (non-positive) reported window", async () => {
|
||||
contextUsageResult = async () => ({ rawMaxTokens: 0 });
|
||||
it("scopes providerCacheKey by per-session env routing", async () => {
|
||||
// The context-window cache key must distinguish backends exactly as the
|
||||
// CLI will see them: a session routed to a proxy via _meta env shares a
|
||||
// model id spelling with default-routed sessions but not a context lane,
|
||||
// so it must land in its own cache bucket (and two default-routed
|
||||
// sessions must share one).
|
||||
const r1 = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
const r2 = await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
options: { env: { ANTHROPIC_BASE_URL: "https://window-probe-proxy.example" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const r3 = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
expect(sessionFor(r2.sessionId).providerCacheKey).not.toBe(
|
||||
sessionFor(r1.sessionId).providerCacheKey,
|
||||
);
|
||||
expect(sessionFor(r3.sessionId).providerCacheKey).toBe(
|
||||
sessionFor(r1.sessionId).providerCacheKey,
|
||||
);
|
||||
});
|
||||
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
|
||||
it("scopes providerCacheKey by provider headers: same endpoint, different headers → different buckets", async () => {
|
||||
// Two providers/set configs sharing apiType+baseUrl but differing in
|
||||
// headers (e.g. an `anthropic-beta: context-1m-…` routing header) can
|
||||
// serve different context lanes for the same model id, so they must not
|
||||
// share a window-cache bucket.
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gw.example",
|
||||
headers: {},
|
||||
});
|
||||
const plain = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gw.example",
|
||||
headers: { "anthropic-beta": "context-1m-2025-08-07" },
|
||||
});
|
||||
const beta = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(sessionFor(beta.sessionId).providerCacheKey).not.toBe(
|
||||
sessionFor(plain.sessionId).providerCacheKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
303
src/tests/providers.test.ts
Normal file
303
src/tests/providers.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
|
||||
import { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
|
||||
|
||||
const mockQuery = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
initializationResult: vi.fn().mockResolvedValue({
|
||||
models: [
|
||||
{ value: "id", displayName: "name", description: "description", supportsAutoMode: true },
|
||||
],
|
||||
}),
|
||||
setModel: vi.fn(),
|
||||
setPermissionMode: vi.fn(),
|
||||
supportedCommands: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async () => ({
|
||||
...(await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
)),
|
||||
query: mockQuery,
|
||||
}));
|
||||
|
||||
// `logout` shells out to `claude auth logout`; make execFile succeed so the
|
||||
// real logout body runs (and clears provider config) without a live CLI.
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
||||
return {
|
||||
...actual,
|
||||
execFile: vi.fn((_file: string, _args: string[], cb: (...a: unknown[]) => void) =>
|
||||
cb(null, { stdout: "", stderr: "" }),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe("providers", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.runAllTimers();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
async function createAgentMock(): Promise<[ClaudeAcpAgent, Mock]> {
|
||||
const connectionMock = {
|
||||
sessionUpdate: async (_: any) => {},
|
||||
} as AcpClient;
|
||||
const agent = new ClaudeAcpAgent(connectionMock);
|
||||
return [agent, mockQuery];
|
||||
}
|
||||
|
||||
it("advertises the providers capability in initialize", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
const response = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
expect(response.agentCapabilities?.providers).toEqual({});
|
||||
});
|
||||
|
||||
it("lists a single optional 'main' provider, unconfigured by default", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
const response = await agent.unstable_listProviders({});
|
||||
expect(response.providers).toEqual([
|
||||
{
|
||||
providerId: "main",
|
||||
supported: ["anthropic", "bedrock", "vertex"],
|
||||
required: false,
|
||||
current: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reflects apiType/baseUrl after set, and never echoes headers", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example/v1",
|
||||
headers: { authorization: "Bearer secret" },
|
||||
});
|
||||
|
||||
const response = await agent.unstable_listProviders({});
|
||||
expect(response.providers[0].current).toEqual({
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example/v1",
|
||||
});
|
||||
// headers must not leak through list
|
||||
expect(JSON.stringify(response.providers)).not.toContain("secret");
|
||||
});
|
||||
|
||||
it("rejects set for an unknown providerId", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "openai",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("rejects set for an unsupported apiType", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "openai",
|
||||
baseUrl: "https://gateway.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("rejects set for an empty or non-http baseUrl", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({ providerId: "main", apiType: "anthropic", baseUrl: "" }),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "ftp://nope.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("disables the 'main' provider by clearing config and reporting current: null", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example/v1",
|
||||
});
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).not.toBeNull();
|
||||
|
||||
await expect(agent.unstable_disableProvider({ providerId: "main" })).resolves.toEqual({});
|
||||
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
|
||||
});
|
||||
|
||||
it("treats disabling an unknown provider as an idempotent no-op", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(agent.unstable_disableProvider({ providerId: "openai" })).resolves.toEqual({});
|
||||
});
|
||||
|
||||
it("routes anthropic provider config into session env", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example",
|
||||
headers: { "x-api-key": "test" },
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
ANTHROPIC_AUTH_TOKEN: " ",
|
||||
ANTHROPIC_BASE_URL: "https://gateway.example",
|
||||
ANTHROPIC_CUSTOM_HEADERS: "x-api-key: test",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes bedrock provider config into session env", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "bedrock",
|
||||
baseUrl: "https://gateway.example",
|
||||
headers: { "custom-header": "test" },
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||
AWS_BEARER_TOKEN_BEDROCK: " ",
|
||||
ANTHROPIC_BEDROCK_BASE_URL: "https://gateway.example",
|
||||
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts vertex config via _meta and routes it into session env", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
headers: { "custom-header": "test" },
|
||||
_meta: { claudeCode: { vertex: { projectId: "my-project", region: "us-east5" } } },
|
||||
});
|
||||
|
||||
// list surfaces apiType/baseUrl but not the _meta extras
|
||||
const listed = await agent.unstable_listProviders({});
|
||||
expect(listed.providers[0].current).toEqual({
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
CLAUDE_CODE_USE_VERTEX: "1",
|
||||
ANTHROPIC_VERTEX_BASE_URL: "https://vertex.example",
|
||||
ANTHROPIC_VERTEX_PROJECT_ID: "my-project",
|
||||
CLOUD_ML_REGION: "us-east5",
|
||||
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects vertex set without _meta project/region", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
|
||||
// partial _meta (missing region) is also rejected
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
_meta: { claudeCode: { vertex: { projectId: "my-project" } } } as any,
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("providers/set takes precedence over gateway auth", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: { auth: { terminal: true, _meta: { gateway: true } } } as any,
|
||||
});
|
||||
|
||||
// Gateway auth first...
|
||||
await agent.authenticate({
|
||||
methodId: "gateway",
|
||||
_meta: { gateway: { baseUrl: "https://gateway.example", headers: {} } },
|
||||
});
|
||||
// ...then providers/set wins.
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://provider.example",
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
ANTHROPIC_BASE_URL: "https://provider.example",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears provider config on logout", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
// Avoids resolving the native CLI binary in claudeCliPath().
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/bin/true");
|
||||
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://provider.example",
|
||||
});
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).not.toBeNull();
|
||||
|
||||
await agent.logout({});
|
||||
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -936,30 +936,16 @@ describe("session config options", () => {
|
||||
return (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
}
|
||||
|
||||
it("refreshes contextWindowSize from getContextUsage when the model changes", async () => {
|
||||
it("sets the window from text inference on model switch, without any getContextUsage IPC", async () => {
|
||||
// getContextUsage stalls until the session's first prompt turn, so the
|
||||
// switch path must never call it; the window is seeded from the text
|
||||
// heuristic (here via the new model's resolvedModel) and later confirmed
|
||||
// by result.modelUsage.
|
||||
const session = getSession();
|
||||
session.query.getContextUsage = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(session.query.getContextUsage).toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(967000);
|
||||
});
|
||||
|
||||
it("falls back to resolvedModel text inference when getContextUsage fails", async () => {
|
||||
const session = getSession();
|
||||
session.query.getContextUsage = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
session.modelInfos = session.modelInfos.map((m: ModelInfo) =>
|
||||
m.value === "claude-sonnet-4-6" ? { ...m, resolvedModel: "claude-sonnet-5[1m]" } : m,
|
||||
);
|
||||
// The rejection is deliberate — capture the agent's warning instead of
|
||||
// letting it hit the console.
|
||||
const errorSpy = vi.fn();
|
||||
(agent as any).logger = { log: () => {}, error: errorSpy };
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
@@ -967,18 +953,17 @@ describe("session config options", () => {
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(session.query.getContextUsage).not.toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(1_000_000);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the default window when the SDK and inference both miss", async () => {
|
||||
it("falls back to the default window when inference misses, without any getContextUsage IPC", async () => {
|
||||
const session = getSession();
|
||||
session.contextWindowSize = 1_000_000;
|
||||
session.query.getContextUsage = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
// The rejection is deliberate — capture the agent's warning instead of
|
||||
// letting it hit the console.
|
||||
const errorSpy = vi.fn();
|
||||
(agent as any).logger = { log: () => {}, error: errorSpy };
|
||||
// Present but must NOT be called; the switch never consults it.
|
||||
session.query.getContextUsage = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
// claude-sonnet-4-6 carries no "1m" token in its id, resolvedModel,
|
||||
// displayName, or description, so inference misses → default window.
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
@@ -986,8 +971,22 @@ describe("session config options", () => {
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(session.query.getContextUsage).not.toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call getContextUsage even when switching to a fresh model", async () => {
|
||||
const session = getSession();
|
||||
const spy = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
session.query.getContextUsage = spy;
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the learned window when re-asserting the current model", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { ClientCapabilities } from "@agentclientprotocol/sdk";
|
||||
import { ImageBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
import {
|
||||
@@ -1024,7 +1024,7 @@ describe("Bash terminal output", () => {
|
||||
);
|
||||
|
||||
// Fire PostToolUse hook with a structuredPatch in tool_response
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1102,7 +1102,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1182,7 +1182,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1203,6 +1203,36 @@ describe("Bash terminal output", () => {
|
||||
expect(hookUpdate.content).toBeUndefined();
|
||||
expect(hookUpdate.locations).toBeUndefined();
|
||||
});
|
||||
|
||||
// Regression for issue #889: tool uses that never register a callback
|
||||
// (TodoWrite/Task* are rendered as plan updates, not tool_calls) fire the
|
||||
// PostToolUse hook too. That's expected — the hook must stay silent
|
||||
// instead of spamming "No onPostToolUseHook found" to stderr.
|
||||
it("should not log an error when no callback is registered for the tool use", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
try {
|
||||
const hook = createPostToolUseHook();
|
||||
const result = await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
tool_name: "TodoWrite",
|
||||
tool_input: { todos: [] },
|
||||
tool_response: { success: true },
|
||||
tool_use_id: "toolu_todo_no_callback",
|
||||
session_id: "test-session",
|
||||
transcript_path: "/tmp/test",
|
||||
cwd: "/tmp",
|
||||
},
|
||||
"toolu_todo_no_callback",
|
||||
{ signal: AbortSignal.abort() },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ continue: true });
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("post-tool-use hook sends diff content for Write tool", () => {
|
||||
@@ -1239,7 +1269,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1315,7 +1345,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1438,7 +1468,7 @@ describe("Bash terminal output", () => {
|
||||
expect((resultNotifications[1].update as any).status).toBe("completed");
|
||||
|
||||
// Step 3: Fire the PostToolUse hook (simulates what Claude Code SDK does)
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1519,7 +1549,7 @@ describe("Bash terminal output", () => {
|
||||
);
|
||||
|
||||
// Fire hook
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -2275,6 +2305,62 @@ describe("Agent/Task tool_result rendering from tool_use_result", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves malformed trailers alone", () => {
|
||||
// Incomplete trailers are ordinary report text, not metadata to strip.
|
||||
const malformedResult: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_agent",
|
||||
content: [
|
||||
{ type: "text", text: "Report.\n<usage>missing closing tag" },
|
||||
{ type: "text", text: "Report.\nagentId: abc-123 (missing closing paren" },
|
||||
],
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(malformedResult, agentToolUse, false);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "Report.\n<usage>missing closing tag" } },
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "Report.\nagentId: abc-123 (missing closing paren" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips only the trailer when the report itself mentions <usage>", () => {
|
||||
const result: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_agent",
|
||||
content: "Grep for <usage> found 3 hits.\n<usage>subagent_tokens: 5</usage>",
|
||||
};
|
||||
const update = toolUpdateFromToolResult(result, agentToolUse, false);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "Grep for <usage> found 3 hits." } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles adversarial trailer-shaped input in linear time", () => {
|
||||
// Regression: the old regex-based strip backtracked quadratically on
|
||||
// these shapes (CodeQL js/polynomial-redos) — at this size it would
|
||||
// blow the test timeout rather than merely run slow.
|
||||
const result: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_agent",
|
||||
content: [
|
||||
{ type: "text", text: "agentId: - (".repeat(20000) },
|
||||
{ type: "text", text: "<usage>".repeat(30000) },
|
||||
],
|
||||
};
|
||||
const update = toolUpdateFromToolResult(result, agentToolUse, false);
|
||||
|
||||
// Neither is a real trailer, so both come through unchanged.
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "agentId: - (".repeat(20000) } },
|
||||
{ type: "content", content: { type: "text", text: "<usage>".repeat(30000) } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back (trailer-stripped) when tool_use_result is the async_launched variant", () => {
|
||||
const update = toolUpdateFromToolResult(rawResult, agentToolUse, false, {
|
||||
status: "async_launched",
|
||||
|
||||
52
src/tools.ts
52
src/tools.ts
@@ -60,7 +60,6 @@ import {
|
||||
BetaWebSearchToolResultBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import path from "node:path";
|
||||
import { Logger } from "./acp-agent.js";
|
||||
|
||||
/**
|
||||
* Union of all possible content types that can appear in tool results from the Anthropic SDK.
|
||||
@@ -515,9 +514,42 @@ function structuredResult<T extends object>(toolUseResult: unknown): T | undefin
|
||||
* matching rather than mangle the report.
|
||||
*/
|
||||
function stripAgentTrailer(text: string): string {
|
||||
return text
|
||||
.replace(/\n?<usage>[\s\S]*?<\/usage>\s*$/, "")
|
||||
.replace(/\n?agentId: [\w-]+ \([^)]*\)\s*$/, "");
|
||||
return stripAgentIdLine(stripUsageBlock(text));
|
||||
}
|
||||
|
||||
const USAGE_OPEN = "<usage>";
|
||||
const USAGE_CLOSE = "</usage>";
|
||||
|
||||
/** Remove a trailing `<usage>…</usage>` block, plus trailing whitespace and
|
||||
* one preceding newline. Matches from the *last* `<usage>` so a report that
|
||||
* merely mentions the marker earlier isn't truncated at the mention. */
|
||||
function stripUsageBlock(text: string): string {
|
||||
const body = text.trimEnd();
|
||||
if (!body.endsWith(USAGE_CLOSE)) {
|
||||
return text;
|
||||
}
|
||||
const open = body.lastIndexOf(USAGE_OPEN, body.length - USAGE_CLOSE.length - USAGE_OPEN.length);
|
||||
if (open === -1) {
|
||||
return text;
|
||||
}
|
||||
return body.slice(0, open > 0 && body[open - 1] === "\n" ? open - 1 : open);
|
||||
}
|
||||
|
||||
/** The continuation line, anchored to a whole line so the regex has a single
|
||||
* start position and no ambiguous repetition (`[\w-]+` can't consume the
|
||||
* following space, `[^)]*` can't consume the closing paren) — it runs in
|
||||
* linear time on any input. */
|
||||
const AGENT_ID_LINE = /^agentId: [\w-]+ \([^)]*\)$/;
|
||||
|
||||
/** Remove a final `agentId: <id> (…)` line, plus trailing whitespace and the
|
||||
* newline that preceded the line. */
|
||||
function stripAgentIdLine(text: string): string {
|
||||
const body = text.trimEnd();
|
||||
const lineStart = body.lastIndexOf("\n") + 1;
|
||||
if (!AGENT_ID_LINE.test(body.slice(lineStart))) {
|
||||
return text;
|
||||
}
|
||||
return body.slice(0, Math.max(lineStart - 1, 0));
|
||||
}
|
||||
|
||||
/** Apply {@link stripAgentTrailer} across a raw tool_result `content` (plain
|
||||
@@ -1184,12 +1216,7 @@ export const registerHookCallback = (
|
||||
|
||||
/* A callback for Claude Code that is called when receiving a PostToolUse hook */
|
||||
export const createPostToolUseHook =
|
||||
(
|
||||
logger: Logger = console,
|
||||
options?: {
|
||||
onEnterPlanMode?: () => Promise<void>;
|
||||
},
|
||||
): HookCallback =>
|
||||
(options?: { onEnterPlanMode?: () => Promise<void> }): HookCallback =>
|
||||
async (input: any, toolUseID: string | undefined): Promise<{ continue: boolean }> => {
|
||||
if (input.hook_event_name === "PostToolUse") {
|
||||
// Handle EnterPlanMode tool - notify client of mode change after successful execution
|
||||
@@ -1201,11 +1228,8 @@ export const createPostToolUseHook =
|
||||
const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
|
||||
if (onPostToolUseHook) {
|
||||
await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
|
||||
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
|
||||
} else {
|
||||
logger.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`);
|
||||
delete toolUseCallbacks[toolUseID];
|
||||
}
|
||||
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
|
||||
}
|
||||
}
|
||||
return { continue: true };
|
||||
|
||||
Reference in New Issue
Block a user