mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-09 10:51:47 +00:00
Compare commits
1 Commits
v0.95.0
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98e7fe3231 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.95.0"
|
||||
".": "0.94.0"
|
||||
}
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,27 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.95.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.94.0...v0.95.0) (2026-10-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* end a turn at its result for AIR clients with backgroundSubagents ([#1262](https://github.com/agentclientprotocol/claude-agent-acp/issues/1262)) ([9b3a5c8](https://github.com/agentclientprotocol/claude-agent-acp/commit/9b3a5c891c75e78b985adb31f4e4b59a16ad2ebc))
|
||||
* report read-only subagent configuration ([#1417](https://github.com/agentclientprotocol/claude-agent-acp/issues/1417)) ([6ab670b](https://github.com/agentclientprotocol/claude-agent-acp/commit/6ab670baaefb30aaac2606d8ee6524e08243e5b5))
|
||||
* report RFD delegations as linked session messages ([#1405](https://github.com/agentclientprotocol/claude-agent-acp/issues/1405)) ([59e292e](https://github.com/agentclientprotocol/claude-agent-acp/commit/59e292e29f773cc6f03c0999993cbe8a2d97a7e3))
|
||||
* serve experimental ACP v2 ([#1400](https://github.com/agentclientprotocol/claude-agent-acp/issues/1400)) ([4d378f9](https://github.com/agentclientprotocol/claude-agent-acp/commit/4d378f93f36d37ba5ef64af87b617706dd9c6e82))
|
||||
* show messages typed in other Remote Control clients ([#1371](https://github.com/agentclientprotocol/claude-agent-acp/issues/1371)) ([5b11f47](https://github.com/agentclientprotocol/claude-agent-acp/commit/5b11f470b1c05b114c876b3b6e5b4df9441b1087))
|
||||
* update to Claude Code 2.1.309 ([#1413](https://github.com/agentclientprotocol/claude-agent-acp/issues/1413)) ([a1521b6](https://github.com/agentclientprotocol/claude-agent-acp/commit/a1521b6196c58dbc5065f9c19fc9694273d3d435))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump fast-uri from 3.1.8 to 3.1.9 ([#1410](https://github.com/agentclientprotocol/claude-agent-acp/issues/1410)) ([0fe22ae](https://github.com/agentclientprotocol/claude-agent-acp/commit/0fe22ae5a172b228a574665bb7136b900746041d))
|
||||
* bump postcss from 8.5.30 to 8.5.31 in the minor group ([#1403](https://github.com/agentclientprotocol/claude-agent-acp/issues/1403)) ([ecaa2c9](https://github.com/agentclientprotocol/claude-agent-acp/commit/ecaa2c935b9f2b63c1846ca17c6c3398390fb0ef))
|
||||
* bump the minor group across 1 directory with 2 updates ([#1416](https://github.com/agentclientprotocol/claude-agent-acp/issues/1416)) ([f7d6030](https://github.com/agentclientprotocol/claude-agent-acp/commit/f7d6030c04af23171058772480d55f6d2aee5298))
|
||||
* IJAI-3744 keep a session running while its background tasks are open ([#1399](https://github.com/agentclientprotocol/claude-agent-acp/issues/1399)) ([e9f986b](https://github.com/agentclientprotocol/claude-agent-acp/commit/e9f986b8a941f5c1789078a39e474f247bd0db60))
|
||||
* publish the session title as soon as the CLI has it ([#1406](https://github.com/agentclientprotocol/claude-agent-acp/issues/1406)) ([50cd76e](https://github.com/agentclientprotocol/claude-agent-acp/commit/50cd76e78dd9bb1ab0e4850a8d95431175ab5947))
|
||||
* publish turns that Claude Code starts after end_turn as an async task in v1 ([#1390](https://github.com/agentclientprotocol/claude-agent-acp/issues/1390)) ([a1d0eee](https://github.com/agentclientprotocol/claude-agent-acp/commit/a1d0eeeaad3d67a0765f4b89e3d81c1af6ca0dbb))
|
||||
|
||||
## [0.94.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.93.0...v0.94.0) (2026-10-19)
|
||||
|
||||
|
||||
|
||||
47
docs/background-work-experiment.md
Normal file
47
docs/background-work-experiment.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Background work RFD experiment
|
||||
|
||||
This branch prototypes [RFD 2452](https://github.com/agentclientprotocol/agent-client-protocol/pull/2452)
|
||||
at revision `33c473ba0723a0f6ffcfcbb54fd780c05df7b208`. It is an opt-in local
|
||||
experiment, not a released protocol implementation.
|
||||
|
||||
Build with `npm ci --ignore-scripts` and `npm run build`. Launch with
|
||||
`ACP_BACKGROUND_WORK_EXPERIMENT=1 node dist/index.js`. Omit the environment
|
||||
variable to restore normal behavior. No package publication is needed.
|
||||
|
||||
The message-stream bridge works on ACP v1 and v2. It reuses the installed
|
||||
native asynchronous-task adapter and its private negotiation internally. Known
|
||||
background shells keep the existing tool call `in_progress`; state snapshots
|
||||
reference that tool. `tool_call/cancel` is translated to the existing native
|
||||
task stop request. A successful response is acceptance only. A native terminal
|
||||
event finalizes the tool. ACP v1 represents cancellation as `failed` because its
|
||||
tool status enum has no `cancelled`; ACP v2 uses `cancelled`.
|
||||
|
||||
Native replace-semantics task levels establish whether the list is complete.
|
||||
Before a native level arrives, after a malformed level, or while any native task
|
||||
cannot be mapped to an announced tool, `background` is `null`. This includes
|
||||
unmapped monitors, scheduled tasks and subagents. The prototype does not claim
|
||||
their full RFD support. Forks start with `background: []`. Reconnects remain
|
||||
unreported until native current state and referenced entities are available.
|
||||
|
||||
This bridge does not upgrade the SDK schema. A client must accept the draft
|
||||
fields and raw `tool_call/cancel`, and must treat repeated `idle` snapshots
|
||||
without a stop reason as list changes rather than new completions. The private
|
||||
negotiation can also select the adapter's existing client-specific tool rendering;
|
||||
this is a bounded experiment, not general client capability negotiation.
|
||||
|
||||
Suggested manual evaluation: start a bounded background shell, finish the
|
||||
foreground turn, cancel one shell, and separately allow a shell to finish while
|
||||
idle. Verify that the tool remains in progress and listed until native evidence
|
||||
ends it. Send foreground `session/cancel` while background work is listed, then
|
||||
close the session. Reconnect and fork must not invent live tools from history.
|
||||
Exercise pending permissions separately; deterministic protocol tests are not
|
||||
evidence of live native permission or session-close behavior.
|
||||
|
||||
A tool already reported final before native background discovery is never revived.
|
||||
That task remains unreported by this bridge. Native subagents also remain unreported
|
||||
while associated, including an idle child whose own background state is unknown.
|
||||
|
||||
Codex list disappearance proves the process ended but supplies no exit outcome.
|
||||
The existing native adapter reports this as stopped, represented here as cancelled
|
||||
(v2) or failed (v1); only an explicit native command completion supplies a reliable
|
||||
completed/failed outcome. This is a known limit of the experiment.
|
||||
394
package-lock.json
generated
394
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.95.0",
|
||||
"version": "0.94.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.95.0",
|
||||
"version": "0.94.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.309",
|
||||
"@agentclientprotocol/sdk": "2.0.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.303",
|
||||
"diff": "9.0.0",
|
||||
"zod": "4.6.7"
|
||||
},
|
||||
@@ -18,18 +18,19 @@
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "1.1.0",
|
||||
"@anthropic-ai/sdk": "0.135.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.6",
|
||||
"@types/node": "26.6.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.72.0",
|
||||
"@typescript-eslint/parser": "8.72.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.71.2",
|
||||
"@typescript-eslint/parser": "8.71.2",
|
||||
"ajv": "8.20.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"eslint": "10.13.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.13.0",
|
||||
"prettier": "3.9.10",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
},
|
||||
@@ -38,31 +39,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.1.tgz",
|
||||
"integrity": "sha512-v4E/NcBTGFEKmorGso8ysAjJTkNh5UMJPP4aDznyMNmnP9t10g/aCCo/Djy7RYaYZy5ZROFK3G5cEj9tDgGOTQ==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.0.tgz",
|
||||
"integrity": "sha512-Za5gWOPQ44aBOdegDJZ+OjCAPn7POc0cw8BlFJQ1mmUY1OoUWxRi3+QZT4gdZnyEWgKXyfeGRb0Oo1ag9YkQ1w==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.309.tgz",
|
||||
"integrity": "sha512-Tou0fXBkxKJDcgT09v3Iv/Jd7bNgapgfAiHxGPctAADPFQhidfhZbcv7at1cVL0Lon5sbs+OzP/pEOigw2TI0A==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.303.tgz",
|
||||
"integrity": "sha512-XBql04V40xr+fmGJh8AmIpqRZKFNrs99BzXzzSVlx+uE5NjFCzKmkNKTdMD0DQtDvFJlijOLoh1siaH+au9Psw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.309",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.309"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.303",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.303"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -71,9 +72,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.309.tgz",
|
||||
"integrity": "sha512-qC93iDGEioaXqWsYazcZzNv3hd8d4VHkCFgb+AzvbkCWOzZHnRu1EqF4Iz5kCTLDs4VdMvCa1NkzlUwtTedBVw==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.303.tgz",
|
||||
"integrity": "sha512-QAWV9Zd7i7FQDaUSwZNVXJirqCS9CSF4aPwbkGwLvLH83qcfFOmnyxWUg1H7dU6tJFLOCyDLQzc3xzUA4dakoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -84,9 +85,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.309.tgz",
|
||||
"integrity": "sha512-RvP0UOcYxn42c1lgByoMufHaMOH86U2ThGGFupOxBKATY4Q6WJd6M1CukQ/qygfgJ8lcxvK8GOOd3SIny4kQ8Q==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.303.tgz",
|
||||
"integrity": "sha512-o931musaryuq04fMaEjceCEmgsBrLh39WvwsQWmv070h9jXS8EmWRFpdSILv1xrUFXiukRSq8yUDRVzg+FvTug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -97,15 +98,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.309.tgz",
|
||||
"integrity": "sha512-+8rQwzycuXTW2e/C+yotvP/SDiEDHxItdPvIriGAf2ixh7nPRjVuzHOj1tamgVO0J8nmpPvIAO4edzJ7VZCcAg==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.303.tgz",
|
||||
"integrity": "sha512-c04i9i1BU3j9G3D7XdVzXTdM3/sDpQH+1o8jNcMPnztOTYbNFmH/cacgeX9x3N112r+5VhCEHXGILoe5GxVxeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -113,15 +111,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.309.tgz",
|
||||
"integrity": "sha512-14ZD9hdUeobw+jr166YvCZyZwSYG3hDWi5S4yQxpSCrRuY/1z4Kl1AfymUlugiN3UT4Q1rif32K6GnjRJBvQdA==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.303.tgz",
|
||||
"integrity": "sha512-a/OsLTEx9ly9hC26cXidg9mtTMA00SQnEiTN9P4IPNj64uSp/7uBnlWl0VeUoVFbgKYYq6XrGRcD6F1ilQsYxg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -129,15 +124,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.309.tgz",
|
||||
"integrity": "sha512-96G8/uTArlvTTg0qGtiP36W76Ug/YT0gMBh8KX7J2RigdOTrk6QkHtUeCzrw6yff5hEIzecN7EWNw6uh9USo4A==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.303.tgz",
|
||||
"integrity": "sha512-lsm1sX2+9Wos8glBbn86M8leFuK60LKB1cJdS0b2HiTSwiDCqpUKYRGJNUlW1JdUTwJB5nDv6S8/FOJExm1/tw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -145,15 +137,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.309.tgz",
|
||||
"integrity": "sha512-9ViOt4MGMiE2h8V+JUPR5l9F0ZTFuhKC3jawIL/MrMfYY1m2Gt3gcMuP7h7YK914a4tmpXrmITWrpTOu1WOZQQ==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.303.tgz",
|
||||
"integrity": "sha512-yixAa3ZOZd1hiI4mpoukzT9TnuDCzSEHyh7pHcbYTRmdRE2y8bAOmI0eiMFJPivnG4iyGhS2C4XNAe6hAjl30w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -161,9 +150,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.309.tgz",
|
||||
"integrity": "sha512-lDsmw5hguDF7KNMG2OvvKlG0Zgs9bKlrX9Gs2Rv/NjLVl0PIy1TUsh7byOsUR4pXwoMU1toyU1CKqBc/Au8hHw==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.303.tgz",
|
||||
"integrity": "sha512-AnEseiw5E1JF3XRaGIZN880V/i2OxCaTOQJF9F1+oELleSdvLnCUxCE6/baGvaZjrgvffO9YbYChBpgUKj9j7A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -174,9 +163,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.309",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.309.tgz",
|
||||
"integrity": "sha512-otGHQimz6F++6SajGqSAKBclRPGTn60cx+8u3dnhMa5yovKZ5SgChFgstFsiDIWr/ZBqpJysvFemSgHX61mlmQ==",
|
||||
"version": "0.3.303",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.303.tgz",
|
||||
"integrity": "sha512-j5kABFEQ6x6T5AB/glmn0r9VwZRbzcH+nSEOWglB69ODNfkZA2btjcz9r794+2BLqJ9QAyswiYWcOX4sDz/RmQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -187,10 +176,10 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.1.0.tgz",
|
||||
"integrity": "sha512-Op/fKMsfVwFCfObc87leLOFlORE0RjOvLzRzPCWWLI7Ww6bobpKR6YmYC8ZYgulRh+m2a/3/61VUXoWHFtCXIw==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "0.135.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.135.0.tgz",
|
||||
"integrity": "sha512-WWrdL57JhU5KoynpTjt9GXK6k7ud9nPw2uw/Trn+oBXh6s0KpjhAccWZm2Qxw08ARYG1UDQj5iC+JO42j1rt/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
@@ -241,6 +230,19 @@
|
||||
"keyv": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
|
||||
@@ -452,6 +454,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/bigmap": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
|
||||
@@ -518,9 +531,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",
|
||||
"integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
|
||||
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -821,6 +834,34 @@
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz",
|
||||
"integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node22": {
|
||||
"version": "22.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.6.tgz",
|
||||
@@ -878,17 +919,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz",
|
||||
"integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz",
|
||||
"integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.72.0",
|
||||
"@typescript-eslint/type-utils": "8.72.0",
|
||||
"@typescript-eslint/utils": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0",
|
||||
"@typescript-eslint/scope-manager": "8.71.2",
|
||||
"@typescript-eslint/type-utils": "8.71.2",
|
||||
"@typescript-eslint/utils": "8.71.2",
|
||||
"@typescript-eslint/visitor-keys": "8.71.2",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -901,22 +942,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.72.0",
|
||||
"@typescript-eslint/parser": "^8.71.2",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz",
|
||||
"integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz",
|
||||
"integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0",
|
||||
"@typescript-eslint/scope-manager": "8.71.2",
|
||||
"@typescript-eslint/types": "8.71.2",
|
||||
"@typescript-eslint/typescript-estree": "8.71.2",
|
||||
"@typescript-eslint/visitor-keys": "8.71.2",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -932,14 +973,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz",
|
||||
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz",
|
||||
"integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.72.0",
|
||||
"@typescript-eslint/types": "^8.72.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.71.2",
|
||||
"@typescript-eslint/types": "^8.71.2",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -954,14 +995,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz",
|
||||
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz",
|
||||
"integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0"
|
||||
"@typescript-eslint/types": "8.71.2",
|
||||
"@typescript-eslint/visitor-keys": "8.71.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -972,9 +1013,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz",
|
||||
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz",
|
||||
"integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -989,15 +1030,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz",
|
||||
"integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz",
|
||||
"integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0",
|
||||
"@typescript-eslint/utils": "8.72.0",
|
||||
"@typescript-eslint/types": "8.71.2",
|
||||
"@typescript-eslint/typescript-estree": "8.71.2",
|
||||
"@typescript-eslint/utils": "8.71.2",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1014,9 +1055,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz",
|
||||
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz",
|
||||
"integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1028,16 +1069,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz",
|
||||
"integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz",
|
||||
"integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.72.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0",
|
||||
"@typescript-eslint/project-service": "8.71.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.71.2",
|
||||
"@typescript-eslint/types": "8.71.2",
|
||||
"@typescript-eslint/visitor-keys": "8.71.2",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1056,16 +1097,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz",
|
||||
"integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz",
|
||||
"integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0"
|
||||
"@typescript-eslint/scope-manager": "8.71.2",
|
||||
"@typescript-eslint/types": "8.71.2",
|
||||
"@typescript-eslint/typescript-estree": "8.71.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1080,13 +1121,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz",
|
||||
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
|
||||
"version": "8.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz",
|
||||
"integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/types": "8.71.2",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1196,6 +1237,19 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
@@ -1229,6 +1283,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -1428,6 +1489,13 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -1977,9 +2045,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
|
||||
"integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2380,9 +2448,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.13",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
|
||||
"integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
|
||||
"version": "6.2.12",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
|
||||
"integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
@@ -2745,6 +2813,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -3557,6 +3632,60 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node/node_modules/diff": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -3644,6 +3773,13 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -3861,6 +3997,16 @@
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
13
package.json
13
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.95.0",
|
||||
"version": "0.94.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -65,24 +65,25 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.309",
|
||||
"@agentclientprotocol/sdk": "2.0.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.303",
|
||||
"diff": "9.0.0",
|
||||
"zod": "4.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "1.1.0",
|
||||
"@anthropic-ai/sdk": "0.135.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.6",
|
||||
"@types/node": "26.6.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.72.0",
|
||||
"@typescript-eslint/parser": "8.72.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.71.2",
|
||||
"@typescript-eslint/parser": "8.71.2",
|
||||
"ajv": "8.20.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"eslint": "10.13.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.13.0",
|
||||
"prettier": "3.9.10",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
}
|
||||
|
||||
185
src/acp-agent.ts
185
src/acp-agent.ts
@@ -353,7 +353,6 @@ import {
|
||||
buildEffortConfigOption,
|
||||
EFFORT_CONFIG_ID,
|
||||
effortFlagSettings,
|
||||
effortLevelName,
|
||||
mergeEffortSettings,
|
||||
settingsEffortForModel,
|
||||
} from "./session-effort.js";
|
||||
@@ -4411,7 +4410,10 @@ export class ClaudeAcpAgent {
|
||||
} else {
|
||||
await session.query.stopTask(params.asyncTaskId);
|
||||
}
|
||||
await asyncTasks.taskStopped(params.asyncTaskId);
|
||||
// Acceptance is separate from the native terminal event in the RFD experiment.
|
||||
if (process.env.ACP_BACKGROUND_WORK_EXPERIMENT !== "1") {
|
||||
await asyncTasks.taskStopped(params.asyncTaskId);
|
||||
}
|
||||
return { stopped: true };
|
||||
} catch (error) {
|
||||
asyncTasks.releaseStop(params.asyncTaskId);
|
||||
@@ -5877,28 +5879,9 @@ export class ClaudeAcpAgent {
|
||||
// turn activation and settlement stay driven by echoes/results/idle.
|
||||
// (Raw-mode emission above still forwards these frames.)
|
||||
if ((message as { type: string }).type === "command_lifecycle") {
|
||||
const frame = message as unknown as {
|
||||
command_uuid: string;
|
||||
state: string;
|
||||
wakeup_source?: string;
|
||||
scheduled_for?: number;
|
||||
};
|
||||
const frame = message as unknown as { command_uuid: string; state: string };
|
||||
switch (frame.state) {
|
||||
case "started": {
|
||||
// CLIs 2.1.307+ (capability lifecycle_wakeup_source_v1) mark a
|
||||
// command that a scheduler fire started. With no prompt of ours
|
||||
// open, it starts an autonomous turn: name the fire as its cause,
|
||||
// whether `session_state_changed` already opened the task or not.
|
||||
const cause = scheduledFireCause(frame);
|
||||
if (
|
||||
cause &&
|
||||
asyncTasks.enabled &&
|
||||
!session.cancelled &&
|
||||
!session.activeTurn &&
|
||||
!firstUnsettledQueuedTurn()
|
||||
) {
|
||||
await asyncTasks.autonomousTurnStarted(cause);
|
||||
}
|
||||
// Remember dispatch on the live turn so a cancel() that orphans
|
||||
// it seeds the right state (see Turn.commandStarted)...
|
||||
const queued = findUnsettledTurn(frame.command_uuid);
|
||||
@@ -6819,6 +6802,19 @@ export class ClaudeAcpAgent {
|
||||
case "control_request_progress":
|
||||
break;
|
||||
case "background_tasks_changed":
|
||||
if (process.env.ACP_BACKGROUND_WORK_EXPERIMENT === "1") {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: {
|
||||
sessionUpdate: "session_info_update",
|
||||
_meta: {
|
||||
backgroundWorkExperiment: {
|
||||
taskIds: message.tasks.map((task) => task.task_id),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
await asyncTasks.backgroundTasksChanged(message.tasks);
|
||||
// A level signal: the full live background-task set on every
|
||||
// membership change, with REPLACE semantics. Used only to
|
||||
@@ -7649,11 +7645,6 @@ export class ClaudeAcpAgent {
|
||||
// it) can all be tagged with the same, replay-stable id.
|
||||
if (message.event.type === "message_start") {
|
||||
currentStreamMessageId = message.event.message.id || undefined;
|
||||
await subagents.reportModel(
|
||||
message.parent_tool_use_id,
|
||||
message.event.message.model,
|
||||
sendUpdate,
|
||||
);
|
||||
// A new top-level message starts: clear any streamed-content
|
||||
// residue from a prior message that never reached its
|
||||
// consolidated reset — a cancelled turn breaks out before the
|
||||
@@ -8020,14 +8011,6 @@ export class ClaudeAcpAgent {
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.type === "assistant") {
|
||||
await subagents.reportModel(
|
||||
message.parent_tool_use_id,
|
||||
message.message.model,
|
||||
sendUpdate,
|
||||
);
|
||||
}
|
||||
|
||||
// Snapshot the latest top-level assistant usage and model so the
|
||||
// next `result` can emit a usage_update tied to the right context
|
||||
// window. Subagent messages are excluded to keep the snapshot
|
||||
@@ -8629,7 +8612,13 @@ export class ClaudeAcpAgent {
|
||||
session.cancelled = true;
|
||||
// The interrupt below makes Claude Code withdraw the requests that are open
|
||||
// now (see `withPendingUserInput`).
|
||||
for (const input of session.openUserInputs ?? []) input.cancelled = true;
|
||||
for (const input of session.openUserInputs ?? []) {
|
||||
if (
|
||||
process.env.ACP_BACKGROUND_WORK_EXPERIMENT !== "1" ||
|
||||
blocksForeground(session, input.agentID)
|
||||
)
|
||||
input.cancelled = true;
|
||||
}
|
||||
// Before anything awaits, so a model request that the consumer reads in
|
||||
// the meantime is already judged against these prompts.
|
||||
session.cancelledPrompts ??= new Set();
|
||||
@@ -10153,69 +10142,6 @@ export class ClaudeAcpAgent {
|
||||
};
|
||||
}
|
||||
|
||||
private subagentConfigurationHook(sessionId: string): HookCallback {
|
||||
return async (input) => {
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session || !input.agent_id) return { continue: true };
|
||||
const options: SessionConfigOption[] = [];
|
||||
if (input.permission_mode) {
|
||||
const mode = session.modes.availableModes.find((mode) => mode.id === input.permission_mode);
|
||||
options.push(
|
||||
SessionModeManager.configOption({
|
||||
currentModeId: input.permission_mode,
|
||||
availableModes: [mode ?? { id: input.permission_mode, name: input.permission_mode }],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (input.hook_event_name === "PostModelSwitch" && input.to_model) {
|
||||
options.push({
|
||||
id: MODEL_CONFIG_ID,
|
||||
name: "Model",
|
||||
category: "model",
|
||||
type: "select",
|
||||
currentValue: input.to_model,
|
||||
options: [{ value: input.to_model, name: input.to_model }],
|
||||
});
|
||||
}
|
||||
if (input.effort?.level) {
|
||||
options.push({
|
||||
id: EFFORT_CONFIG_ID,
|
||||
name: "Effort",
|
||||
description: "Current reasoning effort",
|
||||
category: "thought_level",
|
||||
type: "select",
|
||||
currentValue: input.effort.level,
|
||||
options: [{ value: input.effort.level, name: effortLevelName(input.effort.level) }],
|
||||
});
|
||||
}
|
||||
if (input.agent_type) {
|
||||
const agent = session.agents.find((agent) => agent.name === input.agent_type);
|
||||
options.push({
|
||||
id: AGENT_CONFIG_ID,
|
||||
name: "Agent",
|
||||
description: "Subagent type",
|
||||
type: "select",
|
||||
currentValue: input.agent_type,
|
||||
options: [
|
||||
{
|
||||
value: input.agent_type,
|
||||
name: input.agent_type,
|
||||
...(agent?.description ? { description: agent.description } : {}),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
await session.nativeSubagentRuntime
|
||||
?.reportConfiguration(input.agent_id, options)
|
||||
.catch((error) =>
|
||||
this.logger.error(
|
||||
`Session ${sessionId}: failed to report subagent configuration: ${error}`,
|
||||
),
|
||||
);
|
||||
return { continue: true };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The PreToolUse hook of a v2 connection, for Edit and Write: the tool call
|
||||
* shows the exact patch of the change before the tool runs.
|
||||
@@ -11478,10 +11404,6 @@ export class ClaudeAcpAgent {
|
||||
`[session/query] sessionId=${sessionId} resume=${creationOpts?.resume ?? "none"} apiType=${resolvedProvider?.apiType ?? "native"} baseUrl=${resolvedProvider?.baseUrl ?? "native"}`,
|
||||
);
|
||||
|
||||
const subagentConfigHook =
|
||||
subagentForm(this.clientCapabilities) === "rfd"
|
||||
? this.subagentConfigurationHook(sessionId)
|
||||
: undefined;
|
||||
const options: Options = {
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
@@ -11496,7 +11418,10 @@ export class ClaudeAcpAgent {
|
||||
// `stopBackgroundWorkflows` stops instead. Background commands survive an
|
||||
// interrupt either way. Before `userProvidedOptions`, so a client's own
|
||||
// `perTaskStopAffordance` wins.
|
||||
...(subagentForm(this.clientCapabilities) === "rfd" ? { perTaskStopAffordance: true } : {}),
|
||||
...(subagentForm(this.clientCapabilities) === "rfd" ||
|
||||
process.env.ACP_BACKGROUND_WORK_EXPERIMENT === "1"
|
||||
? { perTaskStopAffordance: true }
|
||||
: {}),
|
||||
// One predicted follow-up prompt per turn, published as
|
||||
// `session_info_update._meta.promptSuggestions`. It rides the turn's own
|
||||
// prompt cache, and the user can turn it off in settings.json
|
||||
@@ -11550,33 +11475,14 @@ export class ClaudeAcpAgent {
|
||||
tools,
|
||||
hooks: {
|
||||
...userProvidedOptions?.hooks,
|
||||
...(subagentConfigHook
|
||||
? {
|
||||
SubagentStart: [
|
||||
...(userProvidedOptions?.hooks?.SubagentStart ?? []),
|
||||
{ hooks: [subagentConfigHook] },
|
||||
],
|
||||
SubagentStop: [
|
||||
...(userProvidedOptions?.hooks?.SubagentStop ?? []),
|
||||
{ hooks: [subagentConfigHook] },
|
||||
],
|
||||
PostToolUseFailure: [
|
||||
...(userProvidedOptions?.hooks?.PostToolUseFailure ?? []),
|
||||
{ hooks: [subagentConfigHook] },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
// On v2, an Edit or Write shows its exact patch before it runs, also
|
||||
// when no permission request shows it (`fileChangePreviewHook`): a v2
|
||||
// diff cannot hold the input's snippet.
|
||||
...(this.v2 || subagentConfigHook
|
||||
...(this.v2
|
||||
? {
|
||||
PreToolUse: [
|
||||
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
||||
...(subagentConfigHook ? [{ hooks: [subagentConfigHook] }] : []),
|
||||
...(this.v2
|
||||
? [{ matcher: "Edit|Write", hooks: [this.fileChangePreviewHook(sessionId)] }]
|
||||
: []),
|
||||
{ matcher: "Edit|Write", hooks: [this.fileChangePreviewHook(sessionId)] },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
@@ -11584,7 +11490,6 @@ export class ClaudeAcpAgent {
|
||||
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
||||
{
|
||||
hooks: [
|
||||
...(subagentConfigHook ? [subagentConfigHook] : []),
|
||||
createPostToolUseHook({
|
||||
onEnterPlanMode: async () => {
|
||||
await this.sessionModes.publishCurrent(sessionId, "plan");
|
||||
@@ -11608,11 +11513,9 @@ export class ClaudeAcpAgent {
|
||||
...(userProvidedOptions?.hooks?.PostModelSwitch || []),
|
||||
{
|
||||
hooks: [
|
||||
...(subagentConfigHook ? [subagentConfigHook] : []),
|
||||
async (input) => {
|
||||
if (
|
||||
input.hook_event_name === "PostModelSwitch" &&
|
||||
!input.agent_id &&
|
||||
input.source !== "sdk" &&
|
||||
input.source !== "resume"
|
||||
) {
|
||||
@@ -14095,29 +13998,3 @@ function getMatchingModelUsage(modelUsage: Record<string, ModelUsage>, currentMo
|
||||
return { key: bestKey, usage: modelUsage[bestKey] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What a `command_lifecycle` "started" frame says started the command: a
|
||||
* ScheduleWakeup (dynamic `/loop`) wakeup, with the time it was set for in
|
||||
* epoch milliseconds, or another scheduled task such as a cron job. Undefined
|
||||
* for any other command, and for a value this adapter does not know. The time
|
||||
* is given in UTC because the agent does not know the client's time zone.
|
||||
*/
|
||||
function scheduledFireCause(frame: {
|
||||
wakeup_source?: string;
|
||||
scheduled_for?: number;
|
||||
}): string | undefined {
|
||||
switch (frame.wakeup_source) {
|
||||
case "loop_wakeup": {
|
||||
const scheduledFor =
|
||||
typeof frame.scheduled_for === "number" ? new Date(frame.scheduled_for) : undefined;
|
||||
return scheduledFor && !Number.isNaN(scheduledFor.getTime())
|
||||
? `A scheduled wakeup fired (set for ${scheduledFor.toISOString()}).`
|
||||
: "A scheduled wakeup fired.";
|
||||
}
|
||||
case "schedule_wakeup":
|
||||
return "A scheduled task fired.";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,25 +458,18 @@ export class AsyncTaskRuntime {
|
||||
* example to answer a task notification. ACP v1 has no session state, and
|
||||
* the client already got the stop reason of its last prompt, so this task is
|
||||
* the only sign that the agent works. The tool calls of the turn are in the
|
||||
* transcript already, so the task is not shown there.
|
||||
*
|
||||
* [cause] names what started the turn when Claude Code says so, for example
|
||||
* "A scheduled wakeup fired." It replaces the description of a task that is
|
||||
* already open; without it, an open task is left as it is.
|
||||
* transcript already, so the task is not shown there. Does nothing while
|
||||
* such a task is open.
|
||||
*/
|
||||
async autonomousTurnStarted(cause?: string): Promise<void> {
|
||||
if (!this.enabled) return;
|
||||
if (this.autonomousTurn) {
|
||||
if (cause) await this.publishProgress(this.autonomousTurn, { description: cause });
|
||||
return;
|
||||
}
|
||||
async autonomousTurnStarted(): Promise<void> {
|
||||
if (!this.enabled || this.autonomousTurn) return;
|
||||
const task = this.task(`autonomous-turn-${randomUUID()}`);
|
||||
task.autonomousTurn = true;
|
||||
task.startedObserved = true;
|
||||
task.name = "Claude is working";
|
||||
task.taskType = "turn";
|
||||
task.description =
|
||||
cause ?? this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
|
||||
this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
|
||||
task.showInTranscript = false;
|
||||
this.lastNotificationSummary = undefined;
|
||||
this.autonomousTurn = task;
|
||||
|
||||
362
src/background-work-experiment.ts
Normal file
362
src/background-work-experiment.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
/** Opt-in wire prototype for RFD 2452 over the installed native task adapters. */
|
||||
type Json = Record<string, unknown>;
|
||||
type Task = {
|
||||
id: string;
|
||||
toolCallId?: string;
|
||||
canStop: boolean;
|
||||
final: boolean;
|
||||
supported: boolean;
|
||||
};
|
||||
type Session = {
|
||||
state: string;
|
||||
known: boolean;
|
||||
level?: Set<string>;
|
||||
tasks: Map<string, Task>;
|
||||
tools: Set<string>;
|
||||
finalTools: Set<string>;
|
||||
children: Set<string>;
|
||||
};
|
||||
function object(value: unknown): Json {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Json)
|
||||
: {};
|
||||
}
|
||||
|
||||
export class BackgroundWorkExperiment {
|
||||
private sessions = new Map<string, Session>();
|
||||
private requests = new Map<unknown, { method: string; sessionId?: string }>();
|
||||
private cancellations = new Set<unknown>();
|
||||
private protocol = 1;
|
||||
private session(id: string): Session {
|
||||
let session = this.sessions.get(id);
|
||||
if (!session) {
|
||||
session = {
|
||||
state: "idle",
|
||||
known: false,
|
||||
tasks: new Map(),
|
||||
tools: new Set(),
|
||||
finalTools: new Set(),
|
||||
children: new Set(),
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
private notification(sessionId: string, update: Json): Json {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
method: "session/update",
|
||||
params: { sessionId, update },
|
||||
};
|
||||
}
|
||||
snapshot(sessionId: string): Json {
|
||||
const session = this.session(sessionId);
|
||||
const live = [...session.tasks.values()].filter(
|
||||
(task) => !task.final && session.level?.has(task.id),
|
||||
);
|
||||
const complete =
|
||||
session.known &&
|
||||
session.children.size === 0 &&
|
||||
[...(session.level ?? [])].every((id) => {
|
||||
const task = session.tasks.get(id);
|
||||
return (
|
||||
task &&
|
||||
task.supported &&
|
||||
!task.final &&
|
||||
task.toolCallId !== undefined &&
|
||||
session.tools.has(task.toolCallId) &&
|
||||
!session.finalTools.has(task.toolCallId)
|
||||
);
|
||||
});
|
||||
return this.notification(sessionId, {
|
||||
sessionUpdate: "state_update",
|
||||
state: session.state,
|
||||
background: complete
|
||||
? live.map((task) => ({
|
||||
type: "tool_call",
|
||||
toolCallId: task.toolCallId,
|
||||
}))
|
||||
: null,
|
||||
});
|
||||
}
|
||||
inbound(message: Json): { forward?: Json; send: Json[] } {
|
||||
const params = object(message["params"]);
|
||||
const sessionId = typeof params["sessionId"] === "string" ? params["sessionId"] : undefined;
|
||||
const send: Json[] = [];
|
||||
if (message["method"] === "initialize") {
|
||||
this.protocol = typeof params["protocolVersion"] === "number" ? params["protocolVersion"] : 1;
|
||||
const capabilities = object(params["clientCapabilities"]),
|
||||
meta = object(capabilities["_meta"]);
|
||||
const jetbrains = object(meta["jetbrains"]),
|
||||
air = object(jetbrains["air"]);
|
||||
const old = Array.isArray(air["capabilities"]) ? air["capabilities"] : [];
|
||||
// The native task adapters already implement these private capabilities.
|
||||
message = {
|
||||
...message,
|
||||
params: {
|
||||
...params,
|
||||
clientCapabilities: {
|
||||
...capabilities,
|
||||
_meta: {
|
||||
...meta,
|
||||
jetbrains: {
|
||||
...jetbrains,
|
||||
air: {
|
||||
...air,
|
||||
version: 1,
|
||||
capabilities: [...new Set([...old, "asyncTasks", "backgroundSubagents"])],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (["session/resume", "session/load"].includes(String(message["method"])) && sessionId) {
|
||||
const session = this.session(sessionId);
|
||||
session.known = false;
|
||||
delete session.level;
|
||||
session.tools.clear();
|
||||
}
|
||||
if (message["method"] === "tool_call/cancel") {
|
||||
const session = sessionId ? this.session(sessionId) : undefined;
|
||||
const task =
|
||||
session &&
|
||||
[...session.tasks.values()].find(
|
||||
(task) =>
|
||||
!task.final &&
|
||||
task.supported &&
|
||||
task.canStop &&
|
||||
task.toolCallId === params["toolCallId"] &&
|
||||
task.toolCallId !== undefined &&
|
||||
session.tools.has(task.toolCallId) &&
|
||||
!session.finalTools.has(task.toolCallId),
|
||||
);
|
||||
if (!task)
|
||||
return {
|
||||
send: [
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: message["id"],
|
||||
error: {
|
||||
code: -32602,
|
||||
message: "Tool call is not cancellable background work",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
this.cancellations.add(message["id"]);
|
||||
return {
|
||||
forward: {
|
||||
...message,
|
||||
method: "_session/async_task/stop",
|
||||
params: { sessionId, asyncTaskId: task.id },
|
||||
},
|
||||
send,
|
||||
};
|
||||
}
|
||||
if (message["id"] !== undefined && typeof message["method"] === "string")
|
||||
this.requests.set(message["id"], {
|
||||
method: message["method"],
|
||||
sessionId,
|
||||
});
|
||||
if (message["method"] === "session/prompt" && sessionId && this.protocol < 2) {
|
||||
this.session(sessionId).state = "running";
|
||||
send.push(this.snapshot(sessionId));
|
||||
}
|
||||
return { forward: message, send };
|
||||
}
|
||||
outbound(message: Json): Json[] {
|
||||
if (!message["method"] && this.cancellations.delete(message["id"])) {
|
||||
if (message["error"]) return [message];
|
||||
return object(message["result"])["stopped"] === true
|
||||
? [{ ...message, result: {} }]
|
||||
: [
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: message["id"],
|
||||
error: {
|
||||
code: -32602,
|
||||
message: "Native cancellation was not accepted",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
const request = !message["method"] ? this.requests.get(message["id"]) : undefined;
|
||||
if (request) {
|
||||
this.requests.delete(message["id"]);
|
||||
const result = object(message["result"]);
|
||||
const id = typeof result["sessionId"] === "string" ? result["sessionId"] : request.sessionId;
|
||||
if (id && !message["error"]) {
|
||||
if (request.method === "session/new" || request.method === "session/fork") {
|
||||
if (request.method === "session/fork") {
|
||||
this.session(id).known = true;
|
||||
this.session(id).level = new Set();
|
||||
}
|
||||
return [message, this.snapshot(id)];
|
||||
}
|
||||
if (request.method === "session/resume" || request.method === "session/load")
|
||||
return [this.snapshot(id), message];
|
||||
if (request.method === "session/close") this.sessions.delete(id);
|
||||
}
|
||||
if (id && request.method === "session/prompt" && this.protocol < 2) {
|
||||
this.session(id).state = "idle";
|
||||
return [this.snapshot(id), message];
|
||||
}
|
||||
}
|
||||
const params = object(message["params"]),
|
||||
update = object(params["update"]);
|
||||
const sessionId = typeof params["sessionId"] === "string" ? params["sessionId"] : undefined;
|
||||
if (message["method"] !== "session/update" || !sessionId) return [message];
|
||||
const session = this.session(sessionId);
|
||||
const marker = object(object(update["_meta"])["backgroundWorkExperiment"]);
|
||||
if (update["sessionUpdate"] === "session_info_update" && "taskIds" in marker) {
|
||||
if (
|
||||
!Array.isArray(marker["taskIds"]) ||
|
||||
!marker["taskIds"].every((id) => typeof id === "string" && id.length > 0)
|
||||
) {
|
||||
session.known = false;
|
||||
} else {
|
||||
session.level = new Set(marker["taskIds"] as string[]);
|
||||
session.known = true;
|
||||
}
|
||||
return [this.snapshot(sessionId)];
|
||||
}
|
||||
if (update["sessionUpdate"] === "state_update") {
|
||||
if (typeof update["state"] === "string") session.state = update["state"];
|
||||
return [
|
||||
{
|
||||
...message,
|
||||
params: {
|
||||
...params,
|
||||
update: {
|
||||
...update,
|
||||
background: object(object(this.snapshot(sessionId)["params"])["update"])[
|
||||
"background"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
if (
|
||||
["subagent_update", "_subagent_update", "subagent_spawned", "subagent_state_update"].includes(
|
||||
String(update["sessionUpdate"]),
|
||||
)
|
||||
) {
|
||||
const id = String(update["sessionId"] ?? update["subagentSessionId"] ?? "unknown");
|
||||
const state =
|
||||
typeof update["state"] === "string" ? update["state"] : object(update["state"])["state"];
|
||||
if (["completed", "failed", "cancelled"].includes(String(state))) session.children.delete(id);
|
||||
else session.children.add(id);
|
||||
return [message, this.snapshot(sessionId)];
|
||||
}
|
||||
if (update["sessionUpdate"] === "tool_call" || update["sessionUpdate"] === "tool_call_update") {
|
||||
const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : "";
|
||||
if (id && (this.protocol >= 2 || update["sessionUpdate"] === "tool_call"))
|
||||
session.tools.add(id);
|
||||
const task = [...session.tasks.values()].find(
|
||||
(task) =>
|
||||
!task.final && task.supported && task.toolCallId === id && !session.finalTools.has(id),
|
||||
);
|
||||
if (!task && ["completed", "failed", "cancelled"].includes(String(update["status"])))
|
||||
session.finalTools.add(id);
|
||||
return task
|
||||
? [
|
||||
{
|
||||
...message,
|
||||
params: {
|
||||
...params,
|
||||
update: {
|
||||
...update,
|
||||
status: "in_progress",
|
||||
capabilities: task.canStop ? { cancel: {} } : {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [message];
|
||||
}
|
||||
if (
|
||||
typeof update["sessionUpdate"] !== "string" ||
|
||||
!update["sessionUpdate"].replace(/^_/, "").startsWith("async_task_")
|
||||
)
|
||||
return [message];
|
||||
const id = typeof update["asyncTaskId"] === "string" ? update["asyncTaskId"] : undefined;
|
||||
if (!id) {
|
||||
session.known = false;
|
||||
return [message, this.snapshot(sessionId)];
|
||||
}
|
||||
let task = session.tasks.get(id);
|
||||
if (!task) {
|
||||
task = { id, canStop: false, final: false, supported: false };
|
||||
session.tasks.set(id, task);
|
||||
}
|
||||
if (String(update["sessionUpdate"]).replace(/^_/, "") === "async_task_spawned")
|
||||
task.supported = update["taskType"] === "shell";
|
||||
if (typeof update["toolCallId"] === "string") task.toolCallId = update["toolCallId"];
|
||||
if (update["canStop"] === true) task.canStop = true;
|
||||
const terminal = ["completed", "failed", "stopped"].includes(String(update["state"]));
|
||||
if (!terminal && session.level && !session.level.has(id)) session.known = false;
|
||||
const wasFinal = task.final;
|
||||
if (terminal) {
|
||||
task.final = true;
|
||||
session.level?.delete(id);
|
||||
}
|
||||
if (wasFinal) return [this.snapshot(sessionId)];
|
||||
if (
|
||||
!task.supported ||
|
||||
!task.toolCallId ||
|
||||
!session.tools.has(task.toolCallId) ||
|
||||
session.finalTools.has(task.toolCallId)
|
||||
)
|
||||
return [message, this.snapshot(sessionId)];
|
||||
const updates: Json[] = [];
|
||||
if (!wasFinal)
|
||||
updates.push(
|
||||
this.notification(sessionId, {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: task.toolCallId,
|
||||
status: terminal
|
||||
? update["state"] === "stopped"
|
||||
? this.protocol < 2
|
||||
? "failed"
|
||||
: "cancelled"
|
||||
: update["state"]
|
||||
: "in_progress",
|
||||
capabilities: terminal ? null : task.canStop ? { cancel: {} } : {},
|
||||
}),
|
||||
);
|
||||
if (terminal) session.finalTools.add(task.toolCallId);
|
||||
return [...updates, this.snapshot(sessionId)];
|
||||
}
|
||||
}
|
||||
|
||||
/** Draft messages are added at the stream boundary, outside the installed SDK schema. */
|
||||
export function backgroundWorkStream<T>(stream: {
|
||||
readable: ReadableStream<T>;
|
||||
writable: WritableStream<T>;
|
||||
}): typeof stream {
|
||||
if (process.env["ACP_BACKGROUND_WORK_EXPERIMENT"] !== "1") return stream;
|
||||
const experiment = new BackgroundWorkExperiment(),
|
||||
writer = stream.writable.getWriter();
|
||||
const send = async (messages: Json[]) => {
|
||||
for (const message of messages) await writer.write(message as T);
|
||||
};
|
||||
return {
|
||||
readable: stream.readable.pipeThrough(
|
||||
new TransformStream<T, T>({
|
||||
async transform(message, controller) {
|
||||
const result = experiment.inbound(object(message));
|
||||
await send(result["send"]);
|
||||
if (result["forward"]) controller.enqueue(result["forward"] as T);
|
||||
},
|
||||
}),
|
||||
),
|
||||
writable: new WritableStream<T>({
|
||||
write: (message) => send(experiment.outbound(object(message))),
|
||||
close: () => writer.close(),
|
||||
abort: (reason) => writer.abort(reason),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
RequestError,
|
||||
type SessionConfigOption,
|
||||
type SessionNotification,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { RequestError, type SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { AcpSessionNotification, SubagentForm, SubagentState } from "./acp-subagents.js";
|
||||
import { AIR_SUBAGENT_KEY, airExtensionMeta } from "./air-extension.js";
|
||||
import { isFinalToolCallStatus } from "./tool-calls/renderer.js";
|
||||
@@ -68,12 +64,6 @@ export type NativeSubagentSession = {
|
||||
type Publish = (notification: AcpSessionNotification) => Promise<void>;
|
||||
type Logger = { log(message: string): void };
|
||||
|
||||
type ChildConfiguration = {
|
||||
options: Map<string, SessionConfigOption>;
|
||||
published?: string;
|
||||
publishing: Promise<void>;
|
||||
};
|
||||
|
||||
/** What the SDK says about the end of a task, beyond its status. */
|
||||
export type TaskEnd = {
|
||||
/** The SDK's text for a failure. */
|
||||
@@ -147,7 +137,6 @@ export class NativeSubagentRuntime {
|
||||
private readonly parentByToolUse: Map<string, string>;
|
||||
private readonly identityByToolUse = new Map<string, SubagentIdentity>();
|
||||
private readonly controlByToolUse = new Map<string, AcpSessionNotification>();
|
||||
private readonly configBySession = new Map<string, ChildConfiguration>();
|
||||
private readonly childByParentToolUse = new Map<string, NativeSubagent>();
|
||||
/**
|
||||
* The child session of each tool call that went to a child session. A later
|
||||
@@ -227,95 +216,6 @@ export class NativeSubagentRuntime {
|
||||
return routed;
|
||||
}
|
||||
|
||||
/** Report the model of a child response, not the parent's model or a launch-time alias. */
|
||||
async reportModel(
|
||||
parentToolUseId: string | null,
|
||||
model: string | undefined,
|
||||
deliver: Publish,
|
||||
): Promise<void> {
|
||||
if (this.form !== "rfd" || !parentToolUseId || !model || model === "<synthetic>") {
|
||||
return;
|
||||
}
|
||||
await deliver({
|
||||
sessionId: this.rootSessionId,
|
||||
update: {
|
||||
sessionUpdate: "config_option_update",
|
||||
configOptions: [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
category: "model",
|
||||
type: "select",
|
||||
currentValue: model,
|
||||
options: [{ value: model, name: model }],
|
||||
},
|
||||
],
|
||||
_meta: { claudeCode: { parentToolUseId } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Hook observations can precede the task event that establishes a child's parent. */
|
||||
async reportConfiguration(
|
||||
agentId: string,
|
||||
options: SessionConfigOption[],
|
||||
meta?: SessionNotification["update"]["_meta"],
|
||||
): Promise<void> {
|
||||
if (this.form !== "rfd" || agentId === this.rootSessionId || options.length === 0) return;
|
||||
let config = this.configBySession.get(agentId);
|
||||
if (!config) {
|
||||
const pending = [...this.configBySession.keys()].filter(
|
||||
(id) => !this.children.has(id),
|
||||
).length;
|
||||
if (!this.children.has(agentId) && pending >= MAX_PENDING_PARENTS) {
|
||||
this.logger.log(
|
||||
`Session ${this.rootSessionId}: dropping configuration for unannounced subagent ${agentId}; pending buffer limit reached`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
config = { options: new Map(), publishing: Promise.resolve() };
|
||||
this.configBySession.set(agentId, config);
|
||||
}
|
||||
for (const option of options) config.options.set(option.id, option);
|
||||
const child = this.children.get(agentId);
|
||||
if (child?.announced) await this.publishConfiguration(child, meta);
|
||||
}
|
||||
|
||||
private async publishConfiguration(
|
||||
child: NativeSubagent,
|
||||
meta?: SessionNotification["update"]["_meta"],
|
||||
): Promise<void> {
|
||||
const config = this.configBySession.get(child.sessionId);
|
||||
if (!config) return;
|
||||
// SDK hooks can run concurrently with streamed responses. Serialize snapshots
|
||||
// so a delayed publication cannot replace newer settings with older ones.
|
||||
const publishing = config.publishing.then(async () => {
|
||||
if (this.configBySession.get(child.sessionId) !== config) return;
|
||||
const priority = ["mode", "model", "thought_level", "model_config"];
|
||||
const rank = (option: SessionConfigOption) => {
|
||||
const index = priority.indexOf(option.category ?? "");
|
||||
return index < 0 ? priority.length : index;
|
||||
};
|
||||
const configOptions = [...config.options.values()].sort(
|
||||
(left, right) => rank(left) - rank(right),
|
||||
);
|
||||
const snapshot = JSON.stringify(configOptions);
|
||||
if (config.published === snapshot) return;
|
||||
await this.publish({
|
||||
sessionId: child.sessionId,
|
||||
update: {
|
||||
sessionUpdate: "config_option_update",
|
||||
configOptions,
|
||||
...(meta ? { _meta: meta } : {}),
|
||||
},
|
||||
});
|
||||
config.published = snapshot;
|
||||
});
|
||||
// The caller receives the error; later observations must still be able to retry.
|
||||
config.publishing = publishing.catch(() => {});
|
||||
await publishing;
|
||||
}
|
||||
|
||||
private async routeUpdate(
|
||||
notification: AcpSessionNotification,
|
||||
deliver: Publish,
|
||||
@@ -416,14 +316,6 @@ export class NativeSubagentRuntime {
|
||||
? this.childByParentToolUse.get(claudeMeta.parentToolUseId)
|
||||
: undefined);
|
||||
if (this.form === "rfd" && announcedChild?.announced) {
|
||||
if (update.sessionUpdate === "config_option_update") {
|
||||
await this.reportConfiguration(
|
||||
announcedChild.sessionId,
|
||||
update.configOptions,
|
||||
update._meta,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const handback = await this.routeRfdHandback(notification, announcedChild);
|
||||
if (handback !== undefined) return handback;
|
||||
}
|
||||
@@ -703,7 +595,6 @@ export class NativeSubagentRuntime {
|
||||
this.pendingCount = 0;
|
||||
this.identityByToolUse.clear();
|
||||
this.controlByToolUse.clear();
|
||||
this.configBySession.clear();
|
||||
this.parentByToolUse.clear();
|
||||
this.forgetRestores();
|
||||
}
|
||||
@@ -1017,7 +908,6 @@ export class NativeSubagentRuntime {
|
||||
*/
|
||||
private withdraw(taskId: string, child: NativeSubagent): void {
|
||||
this.children.delete(taskId);
|
||||
this.configBySession.delete(child.sessionId);
|
||||
if (child.parentToolUseId) {
|
||||
this.takePending(child.parentToolUseId);
|
||||
this.cleanupControl(child.parentToolUseId);
|
||||
@@ -1042,7 +932,6 @@ export class NativeSubagentRuntime {
|
||||
/** Announces `child` in the client's form. A runtime without one never does. */
|
||||
private async announce(child: NativeSubagent): Promise<void> {
|
||||
if (this.form) await announceNativeSubagent(child, this.publish, this.form);
|
||||
await this.publishConfiguration(child);
|
||||
}
|
||||
|
||||
/** The RFD form: publishes the work state of `child` on its parent. */
|
||||
@@ -1268,7 +1157,6 @@ export class NativeSubagentRuntime {
|
||||
this.parentByToolUse.clear();
|
||||
this.identityByToolUse.clear();
|
||||
this.controlByToolUse.clear();
|
||||
this.configBySession.clear();
|
||||
this.childByParentToolUse.clear();
|
||||
this.taskFinishPromises.clear();
|
||||
this.senderByToolUse.clear();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { backgroundWorkStream } from "./background-work-experiment.js";
|
||||
import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
|
||||
import { v1AgentApp, type ClaudeAcpAgent, type Logger } from "./acp-agent.js";
|
||||
import { nodeToWebReadable, nodeToWebWritable } from "./utils.js";
|
||||
@@ -37,7 +38,7 @@ export function serveAcp(
|
||||
agent = created;
|
||||
};
|
||||
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols).connect(
|
||||
v2.ndJsonStream(output, input),
|
||||
backgroundWorkStream(v2.ndJsonStream(output, input)),
|
||||
);
|
||||
return {
|
||||
closed,
|
||||
|
||||
@@ -79,13 +79,6 @@ export function mergeEffortSettings(base: Settings, override: Settings | undefin
|
||||
};
|
||||
}
|
||||
|
||||
export function effortLevelName(level: string): string {
|
||||
return level
|
||||
.split(/[_-]/)
|
||||
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function buildEffortConfigOption(
|
||||
modelInfos: ModelInfo[],
|
||||
currentModelId: string,
|
||||
@@ -105,7 +98,10 @@ export function buildEffortConfigOption(
|
||||
...(useRecommendedValue ? [] : [{ value: "default", name: "Default" }]),
|
||||
...supportedLevels.map((level) => ({
|
||||
value: level,
|
||||
name: effortLevelName(level),
|
||||
name: level
|
||||
.split(/[_-]/)
|
||||
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
||||
.join(" "),
|
||||
})),
|
||||
];
|
||||
const includes = (level: string) =>
|
||||
|
||||
@@ -292,13 +292,9 @@ function isForkDirectiveMessage(message: { type?: unknown; message?: unknown }):
|
||||
* The transcript of a `subagent_type: "fork"` child without the context it
|
||||
* inherited. A fork transcript starts with a copy of the parent's launching
|
||||
* assistant message (its text, thinking and every tool_use, including the
|
||||
* Agent call itself), then a user message with a placeholder tool_result for
|
||||
* each of those tool_uses and the text
|
||||
* `<fork-boilerplate>…</fork-boilerplate>\nYour directive: <prompt>`. Since
|
||||
* Claude Code 2.1.307 the fork's own Agent call gets "Fork started — that fork
|
||||
* is you." and the other tool_uses get "This is the parent's call, not
|
||||
* yours. …"; before that, every one got "Fork started — processing in
|
||||
* background". Only the directive text is matched here. A fork
|
||||
* Agent call itself), then a user message with a "Fork started — processing
|
||||
* in background" tool_result for each of those tool_uses and the text
|
||||
* `<fork-boilerplate>…</fork-boilerplate>\nYour directive: <prompt>`. A fork
|
||||
* launched from inside a subagent also gets a copy of that subagent's history
|
||||
* before them. None of it is the child's own work: replayed as is, the
|
||||
* parent's text and tool calls would show again in the child.
|
||||
|
||||
@@ -2269,57 +2269,6 @@ describe("usage Markdown", () => {
|
||||
expect(formatted).not.toContain("lowest");
|
||||
});
|
||||
|
||||
it("renders each window's recommendations in order, escaped, before its MCP table", () => {
|
||||
const formatted = formatUsageResponse({
|
||||
...usageResponse,
|
||||
behaviors: {
|
||||
...usageResponse.behaviors,
|
||||
day: {
|
||||
...usageResponse.behaviors.day,
|
||||
recommendations: [
|
||||
{ key: "mcp_server", pct: 41, title: "Disable my_server", detail: "/mcp" },
|
||||
{ key: "effort", pct: 12, title: "Lower effort | max" },
|
||||
],
|
||||
fix: { keys: ["mcp_server"], messages: ["/mcp disable my_server"] },
|
||||
},
|
||||
week: {
|
||||
...usageResponse.behaviors.week,
|
||||
mcp_servers: [],
|
||||
recommendations: [{ key: "agent", pct: 5, title: "Trim the reviewer agent" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const day = formatted.slice(
|
||||
formatted.indexOf("**Last 24h**"),
|
||||
formatted.indexOf("**Last 7d**"),
|
||||
);
|
||||
expect(day).toContain(
|
||||
"- **41%** Disable my\\_server · /mcp\n- **12%** Lower effort \\| max\n\n| MCP server |",
|
||||
);
|
||||
expect(formatted).not.toContain("undefined");
|
||||
expect(formatted).not.toContain("/mcp disable");
|
||||
expect(
|
||||
formatted.endsWith(
|
||||
"**Last 7d** · 43 requests · 5 sessions\n\n- **5%** Trim the reviewer agent",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to the CLI text for a malformed recommendation", () => {
|
||||
expect(
|
||||
parseUsageResponse({
|
||||
...usageResponse,
|
||||
behaviors: {
|
||||
...usageResponse.behaviors,
|
||||
day: {
|
||||
...usageResponse.behaviors.day,
|
||||
recommendations: [{ key: "effort", pct: "high", title: "Lower effort" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("recognizes only exact local usage commands", () => {
|
||||
expect(isUsageCommandText(" /usage ")).toBe(true);
|
||||
expect(isUsageCommandText("/cost")).toBe(false);
|
||||
@@ -3888,8 +3837,7 @@ describe("subagent transcript replay", () => {
|
||||
it("replays a fork child without the launching message and boilerplate it inherited", async () => {
|
||||
// A `subagent_type: "fork"` transcript starts with a copy of the
|
||||
// launching message (same API message id) and a directive message with
|
||||
// a placeholder result for each of its tool_uses, worded as Claude Code
|
||||
// 2.1.307+ writes them.
|
||||
// a "Fork started" result for each of its tool_uses.
|
||||
const launching = [
|
||||
{ type: "text", text: "I'll fork a worker." },
|
||||
toolUse("toolu_fork", "Agent", {
|
||||
@@ -3902,15 +3850,7 @@ describe("subagent transcript replay", () => {
|
||||
const forkStarted = (id: string) => ({
|
||||
type: "tool_result",
|
||||
tool_use_id: id,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
id === "toolu_fork"
|
||||
? "Fork started — that fork is you."
|
||||
: "This is the parent's call, not yours. It may not have run yet, and its result goes to the parent.",
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text: "Fork started — processing in background" }],
|
||||
});
|
||||
const trace = await replayRfd(
|
||||
[
|
||||
@@ -7237,64 +7177,6 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
session_id: "test-session",
|
||||
});
|
||||
|
||||
it("reports the child's observed model as a singleton config without enabling changes", async () => {
|
||||
const response = (model: string) => ({
|
||||
type: "assistant",
|
||||
parent_tool_use_id: "toolu_parent",
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
message: {
|
||||
id: randomUUID(),
|
||||
role: "assistant",
|
||||
model,
|
||||
content: [{ type: "text", text: "Working" }],
|
||||
usage: { input_tokens: 1, output_tokens: 1 },
|
||||
},
|
||||
});
|
||||
const first = response("child-model");
|
||||
const { agent, updates } = await runAgent([
|
||||
{ ...taskStarted("agent-42", "toolu_parent"), subagent_type: "Explore" },
|
||||
{
|
||||
type: "stream_event",
|
||||
parent_tool_use_id: "toolu_parent",
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
event: { type: "message_start", message: first.message },
|
||||
},
|
||||
first,
|
||||
response("fallback-model"),
|
||||
taskUpdated({ status: "completed" }),
|
||||
]);
|
||||
const configs = updates.filter(
|
||||
({ update }) => update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configs).toHaveLength(2);
|
||||
for (const [index, model] of ["child-model", "fallback-model"].entries()) {
|
||||
expect(configs[index]).toMatchObject({
|
||||
sessionId: "agent-42",
|
||||
update: {
|
||||
configOptions: [
|
||||
{
|
||||
id: "model",
|
||||
currentValue: model,
|
||||
options: [{ value: model, name: model }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(
|
||||
updates.findIndex(({ update }) => update.sessionUpdate === "subagent_update"),
|
||||
).toBeLessThan(updates.indexOf(configs[0]!));
|
||||
await expect(
|
||||
agent.setSessionConfigOption({
|
||||
sessionId: "agent-42",
|
||||
configId: "model",
|
||||
value: "fallback-model",
|
||||
}),
|
||||
).rejects.toThrow("Session not found");
|
||||
});
|
||||
|
||||
it("announces the child as running, then sends it the prompt as a message", async () => {
|
||||
const updates = await run([
|
||||
{ ...taskStarted("agent-42", "toolu_parent"), subagent_type: "Explore", prompt: "Go" },
|
||||
@@ -24281,93 +24163,6 @@ describe("deferred settlement for live background subagents (issues #864/#866)",
|
||||
]);
|
||||
});
|
||||
|
||||
describe("started by a scheduler fire", () => {
|
||||
const lifecycleStarted = (fire: { wakeup_source?: string; scheduled_for?: number }) =>
|
||||
({
|
||||
type: "command_lifecycle",
|
||||
command_uuid: "scheduler-fire",
|
||||
state: "started",
|
||||
...fire,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
}) as any;
|
||||
|
||||
const autonomousTurn = async (fireFrames: (afterRunning: boolean) => any[]) => {
|
||||
const { agent, asyncTaskUpdates } = await airAgent();
|
||||
let wake = () => {};
|
||||
const woken = new Promise<void>((resolve) => (wake = resolve));
|
||||
injectGeneratorSession(agent, (input) => {
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
yield userEcho((await iter.next()).value);
|
||||
yield running();
|
||||
yield resultMessage();
|
||||
yield idle();
|
||||
await woken;
|
||||
yield* fireFrames(false);
|
||||
yield running();
|
||||
yield* fireFrames(true);
|
||||
yield assistantText("woke up");
|
||||
yield resultMessage();
|
||||
yield idle();
|
||||
}
|
||||
return messageGenerator();
|
||||
});
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
wake();
|
||||
await agent.sessions["test-session"]?.consumer;
|
||||
return asyncTaskUpdates()
|
||||
.filter((update) => update.sessionUpdate !== "async_task_state_update")
|
||||
.map(({ sessionUpdate, description }) => ({ sessionUpdate, description }));
|
||||
};
|
||||
|
||||
it("names a ScheduleWakeup fire that arrives before the running state", async () => {
|
||||
const updates = await autonomousTurn((afterRunning) =>
|
||||
afterRunning
|
||||
? []
|
||||
: [
|
||||
lifecycleStarted({
|
||||
wakeup_source: "loop_wakeup",
|
||||
scheduled_for: 1_790_000_000_000,
|
||||
}),
|
||||
],
|
||||
);
|
||||
expect(updates).toEqual([
|
||||
{
|
||||
sessionUpdate: "async_task_spawned",
|
||||
description: "A scheduled wakeup fired (set for 2026-09-21T14:13:20.000Z).",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("renames the open task for a scheduled task fire after the running state", async () => {
|
||||
const updates = await autonomousTurn((afterRunning) =>
|
||||
afterRunning ? [lifecycleStarted({ wakeup_source: "schedule_wakeup" })] : [],
|
||||
);
|
||||
expect(updates).toEqual([
|
||||
{
|
||||
sessionUpdate: "async_task_spawned",
|
||||
description: "Claude Code started a turn without a prompt.",
|
||||
},
|
||||
{ sessionUpdate: "async_task_progress", description: "A scheduled task fired." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves the task as before for a started frame without a known wakeup source", async () => {
|
||||
const updates = await autonomousTurn((afterRunning) =>
|
||||
afterRunning
|
||||
? [lifecycleStarted({}), lifecycleStarted({ wakeup_source: "future_source" })]
|
||||
: [],
|
||||
);
|
||||
expect(updates).toEqual([
|
||||
{
|
||||
sessionUpdate: "async_task_spawned",
|
||||
description: "Claude Code started a turn without a prompt.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("ends the turn task when a prompt is folded into the turn", async () => {
|
||||
const { agent, updates, asyncTaskUpdates } = await airAgent();
|
||||
let wake = () => {};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientMessageId":"toolu_agent","recipientSessionId":"a2","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientSessionId":"a2","senderMessageId":"toolu_agent","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientMessageId":"toolu_late","recipientSessionId":"agent_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientSessionId":"agent_late","senderMessageId":"toolu_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_late_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"locations":[{"line":1,"path":"/var/log/app.log"}],"rawInput":{"file_path":"/var/log/app.log"},"sessionUpdate":"tool_call_update","title":"Read /var/log/app.log","toolCallId":"toolu_late_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"The logs are clean.","type":"text"}],"messageId":"toolu_late:reply","recipientSessionId":"<session>","senderSessionId":"agent_late","sessionUpdate":"session_message"}}}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientMessageId":"toolu_native","recipientSessionId":"agent_n","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientSessionId":"agent_n","senderMessageId":"toolu_native","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientMessageId":"toolu_outer","recipientSessionId":"agent_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientSessionId":"agent_outer","senderMessageId":"toolu_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"capabilities":{"cancel":{}},"description":"Read the spec","sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientMessageId":"toolu_inner","recipientSessionId":"agent_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientSessionId":"agent_inner","senderMessageId":"toolu_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_grandchild_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"locations":[{"line":1,"path":"/spec.md"}],"rawInput":{"file_path":"/spec.md"},"sessionUpdate":"tool_call_update","title":"Read /spec.md","toolCallId":"toolu_grandchild_read"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"content":{"text":"```\n1\tspec\n```","type":"text"},"type":"content"}],"rawOutput":"1\tspec","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_grandchild_read"}}}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientMessageId":"toolu_task","recipientSessionId":"agent_1","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientSessionId":"agent_1","senderMessageId":"toolu_task","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Looking for","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":" the parser.","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_sub_read"}}}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientMessageId":"toolu_transcript","recipientSessionId":"agent_t","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientSessionId":"agent_t","senderMessageId":"toolu_transcript","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"Look at t","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"he layout.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"The repo has ","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}
|
||||
|
||||
244
src/tests/background-work-experiment.test.ts
Normal file
244
src/tests/background-work-experiment.test.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BackgroundWorkExperiment, backgroundWorkStream } from "../background-work-experiment.js";
|
||||
|
||||
const update = (value: Record<string, unknown>, sessionId = "s") => ({
|
||||
method: "session/update",
|
||||
params: { sessionId, update: value },
|
||||
});
|
||||
const level = (taskIds: unknown) =>
|
||||
update({
|
||||
sessionUpdate: "session_info_update",
|
||||
_meta: { backgroundWorkExperiment: { taskIds } },
|
||||
});
|
||||
const expected = (background: unknown, state = "idle") => ({
|
||||
params: {
|
||||
sessionId: "s",
|
||||
update: { sessionUpdate: "state_update", state, background },
|
||||
},
|
||||
});
|
||||
|
||||
describe.each([1, 2])("background work experiment v%s", (version) => {
|
||||
function fixture() {
|
||||
const bridge = new BackgroundWorkExperiment();
|
||||
bridge.inbound({
|
||||
id: 1,
|
||||
method: "initialize",
|
||||
params: { protocolVersion: version },
|
||||
});
|
||||
bridge.inbound({ id: 2, method: "session/new", params: {} });
|
||||
bridge.outbound({ id: 2, result: { sessionId: "s" } });
|
||||
return bridge;
|
||||
}
|
||||
function spawn(bridge: BackgroundWorkExperiment) {
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: version === 1 ? "tool_call" : "tool_call_update",
|
||||
toolCallId: "tool",
|
||||
status: "in_progress",
|
||||
}),
|
||||
);
|
||||
bridge.outbound(level(["native"]));
|
||||
return bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: version === 1 ? "async_task_spawned" : "_async_task_spawned",
|
||||
asyncTaskId: "native",
|
||||
taskType: "shell",
|
||||
toolCallId: "tool",
|
||||
canStop: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
it("does not infer empty from a silent backend or unmapped monitor", () => {
|
||||
const bridge = fixture();
|
||||
expect(bridge.snapshot("s")).toMatchObject(expected(null));
|
||||
expect(bridge.outbound(level(["monitor"]))).toMatchObject([expected(null)]);
|
||||
expect(bridge.outbound(level([]))).toMatchObject([expected([])]);
|
||||
expect(bridge.outbound(level([null]))).toMatchObject([expected(null)]);
|
||||
expect(bridge.outbound(level("damaged"))).toMatchObject([expected(null)]);
|
||||
});
|
||||
it("references announced tools and keeps them running after foreground completion", () => {
|
||||
const bridge = fixture();
|
||||
expect(spawn(bridge)).toMatchObject([
|
||||
{
|
||||
params: {
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool",
|
||||
status: "in_progress",
|
||||
capabilities: { cancel: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
expected([{ type: "tool_call", toolCallId: "tool" }]),
|
||||
]);
|
||||
expect(
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool",
|
||||
status: "completed",
|
||||
}),
|
||||
),
|
||||
).toMatchObject([{ params: { update: { status: "in_progress" } } }]);
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: "state_update",
|
||||
state: "idle",
|
||||
stopReason: "end_turn",
|
||||
}),
|
||||
);
|
||||
expect(bridge.outbound(level(["native"]))).toMatchObject([
|
||||
expected([{ type: "tool_call", toolCallId: "tool" }]),
|
||||
]);
|
||||
expect(bridge.outbound(level(["native"]))[0]).not.toHaveProperty("params.update.stopReason");
|
||||
bridge.inbound({
|
||||
method: "session/cancel",
|
||||
params: { sessionId: "s" },
|
||||
});
|
||||
expect(bridge.snapshot("s")).toMatchObject(
|
||||
expected([{ type: "tool_call", toolCallId: "tool" }]),
|
||||
);
|
||||
});
|
||||
it("acceptance does not finalize, and completion wins a cancellation race", () => {
|
||||
const bridge = fixture();
|
||||
spawn(bridge);
|
||||
expect(
|
||||
bridge.inbound({
|
||||
id: 3,
|
||||
method: "tool_call/cancel",
|
||||
params: { sessionId: "s", toolCallId: "tool" },
|
||||
}),
|
||||
).toMatchObject({
|
||||
forward: {
|
||||
method: "_session/async_task/stop",
|
||||
params: { asyncTaskId: "native" },
|
||||
},
|
||||
});
|
||||
expect(bridge.outbound({ id: 3, result: { stopped: true } })).toMatchObject([{ result: {} }]);
|
||||
expect(bridge.snapshot("s")).toMatchObject(
|
||||
expected([{ type: "tool_call", toolCallId: "tool" }]),
|
||||
);
|
||||
expect(
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: "native",
|
||||
state: "completed",
|
||||
}),
|
||||
),
|
||||
).toMatchObject([
|
||||
{ params: { update: { status: "completed", capabilities: null } } },
|
||||
expected([]),
|
||||
]);
|
||||
expect(
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: "async_task_state_update",
|
||||
asyncTaskId: "native",
|
||||
state: "stopped",
|
||||
}),
|
||||
),
|
||||
).toMatchObject([expected([])]);
|
||||
expect(
|
||||
bridge.inbound({
|
||||
id: 4,
|
||||
method: "tool_call/cancel",
|
||||
params: { sessionId: "s", toolCallId: "tool" },
|
||||
}),
|
||||
).toMatchObject({ send: [{ error: { code: -32602 } }] });
|
||||
});
|
||||
it("does not revive a tool finalized before late native discovery", () => {
|
||||
const bridge = fixture();
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: version === 1 ? "tool_call" : "tool_call_update",
|
||||
toolCallId: "late",
|
||||
status: "completed",
|
||||
}),
|
||||
);
|
||||
bridge.outbound(level(["native"]));
|
||||
const messages = bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: "async_task_spawned",
|
||||
asyncTaskId: "native",
|
||||
taskType: "shell",
|
||||
toolCallId: "late",
|
||||
canStop: true,
|
||||
}),
|
||||
);
|
||||
expect(messages).toContainEqual(expect.objectContaining(expected(null)));
|
||||
expect(messages).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
update: expect.objectContaining({ status: "in_progress" }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
bridge.inbound({
|
||||
id: 30,
|
||||
method: "tool_call/cancel",
|
||||
params: { sessionId: "s", toolCallId: "late" },
|
||||
}),
|
||||
).toMatchObject({ send: [{ error: { code: -32602 } }] });
|
||||
});
|
||||
it("does not claim scheduled workflows even when they name an announced tool", () => {
|
||||
const bridge = fixture();
|
||||
bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: version === 1 ? "tool_call" : "tool_call_update",
|
||||
toolCallId: "scheduled",
|
||||
status: "pending",
|
||||
}),
|
||||
);
|
||||
bridge.outbound(level(["workflow"]));
|
||||
const messages = bridge.outbound(
|
||||
update({
|
||||
sessionUpdate: "async_task_spawned",
|
||||
asyncTaskId: "workflow",
|
||||
taskType: "workflow",
|
||||
toolCallId: "scheduled",
|
||||
canStop: true,
|
||||
}),
|
||||
);
|
||||
expect(messages.at(-1)).toMatchObject(expected(null));
|
||||
expect(
|
||||
bridge.inbound({
|
||||
id: 31,
|
||||
method: "tool_call/cancel",
|
||||
params: { sessionId: "s", toolCallId: "scheduled" },
|
||||
}),
|
||||
).toMatchObject({ send: [{ error: { code: -32602 } }] });
|
||||
expect(
|
||||
bridge.outbound(
|
||||
update({ sessionUpdate: "tool_call_update", toolCallId: "scheduled", status: "pending" }),
|
||||
)[0],
|
||||
).toMatchObject({ params: { update: { status: "pending" } } });
|
||||
});
|
||||
it("does not fabricate referenced tools during resume and starts forks empty", () => {
|
||||
const bridge = fixture();
|
||||
bridge.inbound({
|
||||
id: 8,
|
||||
method: "session/resume",
|
||||
params: { sessionId: "restored" },
|
||||
});
|
||||
expect(bridge.outbound({ id: 8, result: {} })[0]).toMatchObject({
|
||||
params: { sessionId: "restored", update: { background: null } },
|
||||
});
|
||||
bridge.inbound({
|
||||
id: 9,
|
||||
method: "session/fork",
|
||||
params: { sessionId: "s" },
|
||||
});
|
||||
expect(bridge.outbound({ id: 9, result: { sessionId: "fork" } })[1]).toMatchObject({
|
||||
params: { sessionId: "fork", update: { background: [] } },
|
||||
});
|
||||
});
|
||||
it("leaves the stream untouched when disabled", () => {
|
||||
const stream = {
|
||||
readable: new ReadableStream(),
|
||||
writable: new WritableStream(),
|
||||
};
|
||||
expect(backgroundWorkStream(stream)).toBe(stream);
|
||||
});
|
||||
});
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import { getSessionMessages, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import type { AcpSessionNotification } from "../acp-subagents.js";
|
||||
import { ALLOW_BYPASS } from "../permissions/modes.js";
|
||||
import { NativeSubagentRuntime } from "../native-subagents.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -187,122 +185,6 @@ describe("createSession options merging", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([false, true])("reports child settings through registered hooks (v2: %s)", async (v2) => {
|
||||
const updates: AcpSessionNotification[] = [];
|
||||
const client = createMockClient();
|
||||
client.sessionUpdate = async (notification) => {
|
||||
updates.push(notification);
|
||||
};
|
||||
const observedAgent = new ClaudeAcpAgent(client, undefined, { v2 });
|
||||
await observedAgent.initialize({ protocolVersion: 1, clientCapabilities: { subagents: {} } });
|
||||
const { sessionId } = await observedAgent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
const session = observedAgent.sessions[sessionId]!;
|
||||
const parentOptions = structuredClone(session.configOptions);
|
||||
const runtime = (session.nativeSubagentRuntime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
sessionId,
|
||||
session,
|
||||
(notification) => client.sessionUpdate(notification),
|
||||
{ log: () => {} },
|
||||
));
|
||||
const invoke = async (
|
||||
event: "PreToolUse" | "PostToolUse" | "PostModelSwitch" | "SubagentStart" | "SubagentStop",
|
||||
fields: Record<string, unknown>,
|
||||
) => {
|
||||
const matcher = capturedOptions!.hooks![event]!.find((matcher) => !matcher.matcher);
|
||||
expect(matcher).toBeDefined();
|
||||
for (const hook of matcher!.hooks) {
|
||||
await expect(
|
||||
hook(
|
||||
{
|
||||
hook_event_name: event,
|
||||
session_id: sessionId,
|
||||
cwd: process.cwd(),
|
||||
transcript_path: "/tmp/transcript",
|
||||
...fields,
|
||||
} as any,
|
||||
undefined,
|
||||
{ signal: new AbortController().signal },
|
||||
),
|
||||
).resolves.toMatchObject({ continue: true });
|
||||
}
|
||||
};
|
||||
// A main-thread --agent persona has agent_type too, but no agent_id.
|
||||
await invoke("PreToolUse", {
|
||||
agent_type: "reviewer",
|
||||
permission_mode: "plan",
|
||||
effort: { level: "max" },
|
||||
});
|
||||
expect(updates).toEqual([]);
|
||||
await invoke("SubagentStart", {
|
||||
agent_id: "child",
|
||||
agent_type: "Explore",
|
||||
permission_mode: "acceptEdits",
|
||||
});
|
||||
expect(updates).toEqual([]);
|
||||
await runtime.taskStarted({ taskId: "child", subagentType: "Explore" }, async () => {});
|
||||
const configs = () =>
|
||||
updates.flatMap(({ update }) =>
|
||||
update.sessionUpdate === "config_option_update" ? [update.configOptions] : [],
|
||||
);
|
||||
expect(
|
||||
configs()
|
||||
.at(-1)
|
||||
?.map((option) => [option.id, option.currentValue]),
|
||||
).toEqual([
|
||||
["mode", "acceptEdits"],
|
||||
["agent", "Explore"],
|
||||
]);
|
||||
await invoke("PreToolUse", {
|
||||
agent_id: "child",
|
||||
agent_type: "Explore",
|
||||
effort: { level: "high" },
|
||||
});
|
||||
expect(
|
||||
configs()
|
||||
.at(-1)
|
||||
?.find((option) => option.id === "effort"),
|
||||
).toMatchObject({
|
||||
category: "thought_level",
|
||||
currentValue: "high",
|
||||
options: [{ value: "high", name: "High" }],
|
||||
});
|
||||
await invoke("PostToolUse", {
|
||||
agent_id: "child",
|
||||
tool_name: "EnterPlanMode",
|
||||
permission_mode: "plan",
|
||||
});
|
||||
await invoke("PostModelSwitch", {
|
||||
agent_id: "child",
|
||||
to_model: "child-model",
|
||||
source: "auto",
|
||||
effort: { level: "low" },
|
||||
});
|
||||
await invoke("SubagentStop", { agent_id: "child", agent_type: "Explore" });
|
||||
expect(
|
||||
configs()
|
||||
.at(-1)
|
||||
?.map((option) => [option.id, option.currentValue]),
|
||||
).toEqual([
|
||||
["mode", "plan"],
|
||||
["model", "child-model"],
|
||||
["effort", "low"],
|
||||
["agent", "Explore"],
|
||||
]);
|
||||
expect(
|
||||
configs()
|
||||
.at(-1)
|
||||
?.every((option) => option.type === "select" && option.options.length === 1),
|
||||
).toBe(true);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(session.configOptions).toEqual(parentOptions);
|
||||
expect(
|
||||
updates
|
||||
.filter(({ update }) => update.sessionUpdate === "config_option_update")
|
||||
.every(({ sessionId: target }) => target === "child"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("merges user-provided disallowedTools with ACP internal list", async () => {
|
||||
await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
|
||||
@@ -842,221 +842,6 @@ describe("NativeSubagentRuntime in the RFD form", () => {
|
||||
update.sessionUpdate === "subagent_update" && update.state ? [update.state] : [],
|
||||
);
|
||||
|
||||
it("reports only the observed child model and refreshes it on a model change", async () => {
|
||||
const { runtime, published } = await runningWorker({ model: "inherit" });
|
||||
const deliver = async (notification: AcpSessionNotification) => {
|
||||
const routed = await runtime.route(notification, deliver);
|
||||
if (routed) published.push(routed);
|
||||
};
|
||||
await runtime.reportModel(null, "parent-model", deliver);
|
||||
await runtime.reportModel("launch-1", "<synthetic>", deliver);
|
||||
expect(published.some(({ update }) => update.sessionUpdate === "config_option_update")).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
await runtime.reportModel("launch-1", "child-model", deliver);
|
||||
await runtime.reportModel("launch-1", "child-model", deliver);
|
||||
await runtime.finishTask("worker-1", "completed", deliver, "launch-1");
|
||||
await runtime.taskStarted(
|
||||
{ taskId: "worker-1", toolUseId: "resume-1", prompt: "Continue" },
|
||||
deliver,
|
||||
);
|
||||
await runtime.reportModel("resume-1", "fallback-model", deliver);
|
||||
|
||||
const configs = published.filter(
|
||||
({ update }) => update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configs).toHaveLength(2);
|
||||
for (const [index, model] of ["child-model", "fallback-model"].entries()) {
|
||||
expect(configs[index]).toMatchObject({
|
||||
sessionId: "worker-1",
|
||||
update: {
|
||||
sessionUpdate: "config_option_update",
|
||||
configOptions: [
|
||||
{
|
||||
id: "model",
|
||||
category: "model",
|
||||
type: "select",
|
||||
currentValue: model,
|
||||
options: [{ value: model, name: model }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("buffers model observations until the child is announced", async () => {
|
||||
const published: AcpSessionNotification[] = [];
|
||||
const runtime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
"root",
|
||||
{},
|
||||
async (notification) => {
|
||||
published.push(notification);
|
||||
},
|
||||
{ log: () => {} },
|
||||
);
|
||||
const deliver = async (notification: AcpSessionNotification) => {
|
||||
const routed = await runtime.route(notification, deliver);
|
||||
if (routed) published.push(routed);
|
||||
};
|
||||
await runtime.reportModel("launch-1", "child-model", deliver);
|
||||
expect(published).toEqual([]);
|
||||
await runtime.taskStarted(
|
||||
{ taskId: "worker-1", toolUseId: "launch-1", subagentType: "Explore" },
|
||||
deliver,
|
||||
);
|
||||
expect(published).toEqual([]);
|
||||
await deliver(launch("launch-1", {}));
|
||||
expect(published.map(({ update }) => update.sessionUpdate)).toEqual([
|
||||
"subagent_update",
|
||||
"config_option_update",
|
||||
]);
|
||||
expect(published[1]?.sessionId).toBe("worker-1");
|
||||
});
|
||||
|
||||
const effortOption = (level: string) => ({
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
category: "thought_level",
|
||||
type: "select" as const,
|
||||
currentValue: level,
|
||||
options: [{ value: level, name: level }],
|
||||
});
|
||||
|
||||
it("merges hook observations with model snapshots without inheriting other sessions", async () => {
|
||||
const { runtime, published } = await runningWorker();
|
||||
const deliver = async (notification: AcpSessionNotification) => {
|
||||
const routed = await runtime.route(notification, deliver);
|
||||
if (routed) published.push(routed);
|
||||
};
|
||||
await runtime.reportConfiguration("worker-1", [
|
||||
effortOption("high"),
|
||||
{
|
||||
id: "mode",
|
||||
name: "Mode",
|
||||
category: "mode",
|
||||
type: "select",
|
||||
currentValue: "plan",
|
||||
options: [{ value: "plan", name: "Plan" }],
|
||||
},
|
||||
]);
|
||||
await runtime.reportModel("launch-1", "child-model", deliver);
|
||||
await runtime.reportConfiguration("worker-1", [effortOption("low")]);
|
||||
await runtime.reportModel("launch-1", "fallback-model", deliver);
|
||||
const configs = published.flatMap(({ update }) =>
|
||||
update.sessionUpdate === "config_option_update" ? [update.configOptions] : [],
|
||||
);
|
||||
expect(configs).toHaveLength(4);
|
||||
expect(configs.at(-1)?.map((option) => [option.id, option.currentValue])).toEqual([
|
||||
["mode", "plan"],
|
||||
["model", "fallback-model"],
|
||||
["effort", "low"],
|
||||
]);
|
||||
expect(
|
||||
configs.at(-1)?.every((option) => option.type === "select" && option.options.length === 1),
|
||||
).toBe(true);
|
||||
await runtime.reportConfiguration("root", [effortOption("max")]);
|
||||
expect(
|
||||
published.filter(({ update }) => update.sessionUpdate === "config_option_update"),
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("keeps hook observations pending until truthful parentage is announced", async () => {
|
||||
const published: AcpSessionNotification[] = [];
|
||||
const runtime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
"root",
|
||||
{},
|
||||
async (notification) => {
|
||||
published.push(notification);
|
||||
},
|
||||
{ log: () => {} },
|
||||
);
|
||||
await runtime.reportConfiguration("worker-1", [effortOption("high")]);
|
||||
expect(published).toEqual([]);
|
||||
await runtime.taskStarted(
|
||||
{ taskId: "worker-1", toolUseId: "launch-1", subagentType: "Explore" },
|
||||
async () => {},
|
||||
);
|
||||
expect(published).toEqual([]);
|
||||
await runtime.route(launch("launch-1", {}), async () => {});
|
||||
expect(published.map(({ sessionId, update }) => [sessionId, update.sessionUpdate])).toEqual([
|
||||
["root", "subagent_update"],
|
||||
["worker-1", "config_option_update"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("serializes concurrent config publications and retries a failed snapshot", async () => {
|
||||
const started = deferred();
|
||||
const release = deferred();
|
||||
const published: AcpSessionNotification[] = [];
|
||||
const value = child({ announced: true });
|
||||
let fail = false;
|
||||
const runtime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
"root",
|
||||
sessionWith(value),
|
||||
async (notification) => {
|
||||
if (fail) throw new Error("transport failed");
|
||||
published.push(notification);
|
||||
if (published.length === 1) {
|
||||
started.resolve();
|
||||
await release.promise;
|
||||
}
|
||||
},
|
||||
{ log: () => {} },
|
||||
);
|
||||
const first = runtime.reportConfiguration(value.sessionId, [effortOption("high")]);
|
||||
await started.promise;
|
||||
const second = runtime.reportConfiguration(value.sessionId, [effortOption("low")]);
|
||||
release.resolve();
|
||||
await Promise.all([first, second]);
|
||||
expect(
|
||||
published.flatMap(({ update }) =>
|
||||
update.sessionUpdate === "config_option_update"
|
||||
? update.configOptions.map((option) => option.currentValue)
|
||||
: [],
|
||||
),
|
||||
).toEqual(["high", "low"]);
|
||||
fail = true;
|
||||
await expect(
|
||||
runtime.reportConfiguration(value.sessionId, [effortOption("max")]),
|
||||
).rejects.toThrow("transport failed");
|
||||
fail = false;
|
||||
await runtime.reportConfiguration(value.sessionId, [effortOption("max")]);
|
||||
expect(published).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("forgets unannounced configuration when the runtime is cleared", async () => {
|
||||
const published: AcpSessionNotification[] = [];
|
||||
const runtime = new NativeSubagentRuntime(
|
||||
"rfd",
|
||||
"root",
|
||||
{},
|
||||
async (notification) => {
|
||||
published.push(notification);
|
||||
},
|
||||
{ log: () => {} },
|
||||
);
|
||||
await runtime.reportConfiguration("worker-1", [effortOption("high")]);
|
||||
runtime.clear();
|
||||
await runtime.taskStarted({ taskId: "worker-1", subagentType: "Explore" }, async () => {});
|
||||
expect(published.map(({ update }) => update.sessionUpdate)).toEqual(["subagent_update"]);
|
||||
});
|
||||
|
||||
it.each(["air", undefined] as const)(
|
||||
"does not report child config in the %s form",
|
||||
async (form) => {
|
||||
const deliver = vi.fn(async () => {});
|
||||
const runtime = new NativeSubagentRuntime(form, "root", {}, deliver, { log: () => {} });
|
||||
await runtime.reportModel("launch-1", "child-model", deliver);
|
||||
await runtime.reportConfiguration("worker-1", [effortOption("high")]);
|
||||
expect(deliver).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("hides the Agent tool call, which the parent's session messages replace", async () => {
|
||||
const { runtime, control } = await runningWorker();
|
||||
|
||||
|
||||
@@ -41,8 +41,7 @@ const launch = (uuid: string, launchId: string) => ({
|
||||
});
|
||||
/**
|
||||
* The directive message that Claude Code writes after the copy of the launching
|
||||
* message in a `subagent_type: "fork"` child's transcript, with the tool_result
|
||||
* text of Claude Code before 2.1.307.
|
||||
* message in a `subagent_type: "fork"` child's transcript.
|
||||
*/
|
||||
const forkDirective = (uuid: string, launchIds: string[], prompt: string) => ({
|
||||
...base,
|
||||
|
||||
@@ -431,7 +431,7 @@ export const createPostToolUseHook =
|
||||
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
|
||||
if (input.tool_name === "EnterPlanMode" && !input.agent_id && options?.onEnterPlanMode) {
|
||||
if (input.tool_name === "EnterPlanMode" && options?.onEnterPlanMode) {
|
||||
await options.onEnterPlanMode();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,10 @@ const usageWindowSchema = z
|
||||
.nullable()
|
||||
.optional();
|
||||
const contributionSchema = z.object({ name: z.string(), pct: percentSchema });
|
||||
// Claude Code 2.1.305+ suggests one change per notable source, largest share first.
|
||||
const recommendationSchema = z.object({
|
||||
key: z.string(),
|
||||
pct: percentSchema,
|
||||
title: z.string(),
|
||||
detail: z.string().optional(),
|
||||
});
|
||||
const behaviorPeriodSchema = z.object({
|
||||
request_count: countSchema,
|
||||
session_count: countSchema,
|
||||
mcp_servers: z.array(contributionSchema),
|
||||
recommendations: z.array(recommendationSchema).optional(),
|
||||
});
|
||||
const modelUsageSchema = z.object({
|
||||
inputTokens: countSchema,
|
||||
@@ -141,23 +133,12 @@ function appendContributions(
|
||||
request_count: number;
|
||||
session_count: number;
|
||||
mcp_servers: { name: string; pct: number }[];
|
||||
recommendations?: { pct: number; title: string; detail?: string }[];
|
||||
},
|
||||
): void {
|
||||
lines.push(
|
||||
"",
|
||||
`**${label}** · ${period.request_count} requests · ${period.session_count} sessions`,
|
||||
);
|
||||
// Rendered in the order Claude Code sends them, as its own `/usage` does. The
|
||||
// window's `fix` is left out: its messages must be queued at priority "later"
|
||||
// with `client_composed` set, which an ACP prompt cannot express.
|
||||
if (period.recommendations?.length) {
|
||||
lines.push("");
|
||||
for (const recommendation of period.recommendations) {
|
||||
const detail = recommendation.detail ? ` · ${escapeMarkdown(recommendation.detail)}` : "";
|
||||
lines.push(`- **${recommendation.pct}%** ${escapeMarkdown(recommendation.title)}${detail}`);
|
||||
}
|
||||
}
|
||||
if (period.mcp_servers.length === 0) return;
|
||||
lines.push("", "| MCP server | Usage |", "|:--|--:|");
|
||||
for (const server of [...period.mcp_servers].sort((a, b) => b.pct - a.pct).slice(0, 3)) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
RequestPermissionResponse,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
|
||||
import { z } from "zod";
|
||||
import { ClaudeAcpAgent, type AcpClient, type Logger } from "../acp-agent.js";
|
||||
import type { AcpSessionNotification } from "../acp-subagents.js";
|
||||
import type { AcpPermissionRequest } from "../permissions/presentation.js";
|
||||
@@ -129,7 +130,16 @@ export function v2AgentApp(
|
||||
void client.send({ sessionId: params.sessionId, update });
|
||||
}),
|
||||
)
|
||||
.onNotification(v2.methods.agent.session.cancel, ({ params }) => agent.cancel(params));
|
||||
.onNotification(v2.methods.agent.session.cancel, ({ params }) => agent.cancel(params))
|
||||
.onRequest(
|
||||
"_session/async_task/stop",
|
||||
z.object({ sessionId: z.string(), asyncTaskId: z.string() }),
|
||||
({ params }) => {
|
||||
if (process.env.ACP_BACKGROUND_WORK_EXPERIMENT !== "1")
|
||||
throw new Error("Background work experiment is disabled");
|
||||
return agent.stopAsyncTask(params);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** The `state_update` that reports `state` as the current foreground state. */
|
||||
|
||||
@@ -76,6 +76,15 @@ export function v2SessionUpdate(
|
||||
// `configOptions` of a `session/set_config_option` response.
|
||||
return undefined;
|
||||
default:
|
||||
if (
|
||||
process.env.ACP_BACKGROUND_WORK_EXPERIMENT === "1" &&
|
||||
update.sessionUpdate.startsWith("async_task_")
|
||||
) {
|
||||
return {
|
||||
...update,
|
||||
sessionUpdate: `_${update.sessionUpdate}`,
|
||||
} as unknown as v2.SessionUpdate;
|
||||
}
|
||||
throw new Error(
|
||||
`The ACP v2 surface does not translate ${update.sessionUpdate} session updates yet`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user