mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-08 10:21:53 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
affc731e3a | ||
|
|
63b708176e | ||
|
|
dc640e2dcf |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.39.0"
|
||||
".": "0.37.0"
|
||||
}
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,25 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.39.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.38.0...v0.39.0) (2026-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** update Claude Agent SDK to 0.3.156 ([#722](https://github.com/agentclientprotocol/claude-agent-acp/issues/722)) ([3fb6db4](https://github.com/agentclientprotocol/claude-agent-acp/commit/3fb6db4e5ee5bd1f431cf5d2ce5331ce983f9597))
|
||||
|
||||
## [0.38.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.37.0...v0.38.0) (2026-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** bump to @anthropic-ai/claude-agent-sdk 0.3.154 (Opus 4.8) ([#716](https://github.com/agentclientprotocol/claude-agent-acp/issues/716)) ([a172885](https://github.com/agentclientprotocol/claude-agent-acp/commit/a172885c80fc32c108a8501268a543641088f543))
|
||||
* Support Opus 4.8 ([#718](https://github.com/agentclientprotocol/claude-agent-acp/issues/718)) ([98b54a0](https://github.com/agentclientprotocol/claude-agent-acp/commit/98b54a02c267cdef619becceccf6ef519fbf8f6f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove hide Claude auth flag handling ([#707](https://github.com/agentclientprotocol/claude-agent-acp/issues/707)) ([7ed1daf](https://github.com/agentclientprotocol/claude-agent-acp/commit/7ed1daf7b42088ad149e9bf742f764752b82d093))
|
||||
|
||||
## [0.37.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.36.1...v0.37.0) (2026-05-21)
|
||||
|
||||
|
||||
|
||||
280
package-lock.json
generated
280
package-lock.json
generated
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.39.0",
|
||||
"version": "0.37.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.39.0",
|
||||
"version": "0.37.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.22.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.146",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.100.0",
|
||||
"@anthropic-ai/sdk": "0.97.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.60.0",
|
||||
"@typescript-eslint/parser": "8.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.4",
|
||||
"@typescript-eslint/parser": "8.59.4",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.6.0",
|
||||
@@ -41,22 +41,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.156.tgz",
|
||||
"integrity": "sha512-6nM/Dj+VMds52UXJ2YaV4IKhYamlUqN0HtdDrFzYz5lvPMpDS935qD8YZDAUpy+ltdoD6PJMd1V/CKFY3/oWCQ==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.146.tgz",
|
||||
"integrity": "sha512-hK9/Ng+hOyexUemTxdIUsSWJ9o2LFi2YNWzHwz8/YMCohUYOnFMZkBiENvUAb0WIc5hieOyBZrOIlg5OewuJMg==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.156"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.146",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.146"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -65,9 +65,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.156.tgz",
|
||||
"integrity": "sha512-IkjcS9dqAUlD4Nb62L9AZtmAXCa+FV4ul8lIlyXXUprh3nlecbKsWOXVd/GORrzAhMmynJaX4+iV1JiutFKXUA==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.146.tgz",
|
||||
"integrity": "sha512-0IIvlEaenq2CRSVx5Bo5BaCtHQXS87GancM35WKEYveGVLn6DI+5G7ikYuTE4AKRPkMnogFtY4BJt6LulWGj+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -78,9 +78,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.156.tgz",
|
||||
"integrity": "sha512-6PKi5fPmGRuzXu+Em/iwLmPG3mqg0hl92wcTU8fmChqyNtxhxsjCw7LTbdFqp/05o5NeZVVV4k3p7YUv5IFD6g==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.146.tgz",
|
||||
"integrity": "sha512-Dk5xJ03Ff1JXbMRP1t2wc/TyfY6xF/2Ysp31wMhFPjoNiKSPHMWaIg242+T3CHdxLWmJ8plWHL1HL5cyZ/LCkw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -91,15 +91,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.156.tgz",
|
||||
"integrity": "sha512-H0Nfd41iw5isto9uQI1FlVSZ0eaDttr8rBpJMR25oK/mj3egMO5EmZ6aAxeeUYSLn2mSU50HA5VNxlGUE118TQ==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.146.tgz",
|
||||
"integrity": "sha512-mzBXDDWWBAC/vDtAYpO1G/dq5QvJtYSPXsqcb+sNdcDhiuf4IYnYp7ytRncYlsUNDkLmX6Gk2jkWAHUUA2Lozg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -107,15 +104,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.156.tgz",
|
||||
"integrity": "sha512-R7KEVjxkR4rYgIQoHGBzwPdUJYxRTO8I4vHjRbMLH1eW4FS7BJvVs7ogfKR/NnHFBvMVqtC+l6jHLQv8bobUiw==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.146.tgz",
|
||||
"integrity": "sha512-QlCid0ucdrmhUAOewfQjaofN2wlokWcfFTxSFePTSj1umk35JO7TDFP700F7jU49r1fPWIdvJpPwWGyB0DeFPA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -123,15 +117,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.156.tgz",
|
||||
"integrity": "sha512-ymhrdlbWoYvTACUdaGdhrEv+ZMfwXLsf0BRLkr/IvY5aqybP7URzWmmZGOtDQpqkT/8xu/UCGqUYH3woJwUxfg==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.146.tgz",
|
||||
"integrity": "sha512-B2baXU1tCBT5CVlD7jJMKjpC4xdO45NUIWpqImmwuOfKvlM/PITjyTXyTY662mGZf1dBmdqBBsqirwFH/jhi8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -139,15 +130,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.156.tgz",
|
||||
"integrity": "sha512-/Q6WUizI6a+hqZZ6ElwRU0PEuFhOoN4v6CuU35HHbiZ/7uaocGht4A8ZIgK1Fw6wOGtZzGLbc00CA1OU1Zg8EA==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.146.tgz",
|
||||
"integrity": "sha512-E3coK1ThQT08KIX80RLcsq7DWXFllCKOzoOe32it/bdtY56TBgPY9xemwXhIJ+cVBHTI9/MpBSIlKBcFCt+yQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -155,9 +143,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.156.tgz",
|
||||
"integrity": "sha512-5sAeNObQQrMy4NF9HwxewrMnU7mVxZDHh+/MfJVQSz0GSTvXQ6gOuRH8helMlfspoU6VOdekPxVLRooX/3foEw==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.146.tgz",
|
||||
"integrity": "sha512-CIwQxGX2r/yWpjCJ6ahB3smKXhghWgGTxL98+LGW52TUwqTiBnlNrH9DPqqgv1/+Hyquw6xfLrKU+StyfMgiLw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -168,9 +156,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.156",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.156.tgz",
|
||||
"integrity": "sha512-/PofeTWoiKgnWNSNk0wG4SsRn22GGLmnLhg2R94WcNhCRFOyOTmiZcYH2DBlWZBIRVTZDsSfa/Pl1DyPvYCGKw==",
|
||||
"version": "0.3.146",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.146.tgz",
|
||||
"integrity": "sha512-qmxrsyaqA8s4HShqJls7ZCRjdoqN66Jo/hbjQNB3uHepD8tEO1iD19aPV4+osdLT7feMkhDBfLT07Q30R2NB5w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -181,9 +169,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.100.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.100.0.tgz",
|
||||
"integrity": "sha512-cAm3aXm6qAiHIvHxyIIGd6tVmsD2gDqlc2h0R20ijNUzGgVnIN822bit4mKbF6CkuV7qIrLQIPoAepHEpanrQQ==",
|
||||
"version": "0.97.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.97.1.tgz",
|
||||
"integrity": "sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
@@ -202,9 +190,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -642,9 +630,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -662,9 +647,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -682,9 +664,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -702,9 +681,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -722,9 +698,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -742,9 +715,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -933,17 +903,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz",
|
||||
"integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz",
|
||||
"integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/type-utils": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"@typescript-eslint/scope-manager": "8.59.4",
|
||||
"@typescript-eslint/type-utils": "8.59.4",
|
||||
"@typescript-eslint/utils": "8.59.4",
|
||||
"@typescript-eslint/visitor-keys": "8.59.4",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -956,22 +926,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"@typescript-eslint/parser": "8.59.4",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz",
|
||||
"integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
|
||||
"integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"@typescript-eslint/scope-manager": "8.59.4",
|
||||
"@typescript-eslint/types": "8.59.4",
|
||||
"@typescript-eslint/typescript-estree": "8.59.4",
|
||||
"@typescript-eslint/visitor-keys": "8.59.4",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -987,14 +957,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz",
|
||||
"integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
|
||||
"integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.60.0",
|
||||
"@typescript-eslint/types": "^8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.4",
|
||||
"@typescript-eslint/types": "^8.59.4",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1009,14 +979,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz",
|
||||
"integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
|
||||
"integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0"
|
||||
"@typescript-eslint/types": "8.59.4",
|
||||
"@typescript-eslint/visitor-keys": "8.59.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1027,9 +997,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
|
||||
"integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1044,15 +1014,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
|
||||
"integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0",
|
||||
"@typescript-eslint/types": "8.59.4",
|
||||
"@typescript-eslint/typescript-estree": "8.59.4",
|
||||
"@typescript-eslint/utils": "8.59.4",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1069,9 +1039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz",
|
||||
"integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
|
||||
"integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1083,16 +1053,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz",
|
||||
"integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
|
||||
"integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"@typescript-eslint/project-service": "8.59.4",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.4",
|
||||
"@typescript-eslint/types": "8.59.4",
|
||||
"@typescript-eslint/visitor-keys": "8.59.4",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1111,16 +1081,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz",
|
||||
"integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz",
|
||||
"integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0"
|
||||
"@typescript-eslint/scope-manager": "8.59.4",
|
||||
"@typescript-eslint/types": "8.59.4",
|
||||
"@typescript-eslint/typescript-estree": "8.59.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1135,13 +1105,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz",
|
||||
"integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==",
|
||||
"version": "8.59.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
|
||||
"integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/types": "8.59.4",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1683,9 +1653,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -1977,9 +1947,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
|
||||
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2330,9 +2300,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -2343,9 +2313,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.23",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
|
||||
"integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
|
||||
"version": "4.12.21",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz",
|
||||
"integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2691,9 +2661,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2715,9 +2682,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2739,9 +2703,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2763,9 +2724,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3346,9 +3304,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -3568,9 +3526,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz",
|
||||
"integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
10
package.json
10
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.39.0",
|
||||
"version": "0.37.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -61,15 +61,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.22.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.156",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.146",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.100.0",
|
||||
"@anthropic-ai/sdk": "0.97.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.60.0",
|
||||
"@typescript-eslint/parser": "8.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.4",
|
||||
"@typescript-eslint/parser": "8.59.4",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.6.0",
|
||||
|
||||
182
src/acp-agent.ts
182
src/acp-agent.ts
@@ -165,6 +165,29 @@ type Session = {
|
||||
/** Accumulated task list for the session, keyed by task ID. Task IDs are
|
||||
* per-session, so this state must not be shared across sessions. */
|
||||
taskState: TaskState;
|
||||
/** Assistant message ids whose text/thinking content was already emitted as
|
||||
* `agent_message_chunk` / `agent_thought_chunk` via `stream_event` partial
|
||||
* messages. Used by the top-level `assistant` case to decide whether to
|
||||
* skip text/thinking blocks (the default, when streaming worked) or fall
|
||||
* back to emitting the assembled blocks (when the underlying SDK / model
|
||||
* proxy never produced `content_block_delta` for them). */
|
||||
textStreamedMessageIds: Set<string>;
|
||||
/** Keys of assembled text/thinking blocks the fallback path has already
|
||||
* forwarded this turn, used to dedup re-delivered blocks without dropping
|
||||
* genuinely new ones. A single assistant message id is frequently
|
||||
* delivered as several `assistant` messages, each carrying a *different*
|
||||
* subset of content blocks (e.g. an empty `thinking` block, then the real
|
||||
* `text` block); deduping at message-id granularity would emit the first
|
||||
* block, flag the id, then silently drop the later text. Keying on the
|
||||
* block content instead lets distinct blocks that share an id all through
|
||||
* while still collapsing identical re-deliveries (incremental + consolidated
|
||||
* messages, or the same message delivered twice). */
|
||||
fallbackEmittedBlockKeys: Set<string>;
|
||||
/** Id of the assistant message currently being streamed via `stream_event`
|
||||
* partial messages, captured at `message_start` so subsequent
|
||||
* `content_block_delta` events can record their parent into
|
||||
* `textStreamedMessageIds`. */
|
||||
currentStreamingMessageId?: string;
|
||||
};
|
||||
|
||||
/** Compute a stable fingerprint of the session-defining params so we can
|
||||
@@ -743,6 +766,14 @@ export class ClaudeAcpAgent implements Agent {
|
||||
cachedWriteTokens: 0,
|
||||
};
|
||||
|
||||
// Assistant message ids and fallback block keys are turn-scoped — clear
|
||||
// before a new turn so the sets don't grow unboundedly over a long-lived
|
||||
// session, and so that an id reused across turns (uncommon but possible)
|
||||
// is re-evaluated.
|
||||
session.textStreamedMessageIds.clear();
|
||||
session.fallbackEmittedBlockKeys.clear();
|
||||
session.currentStreamingMessageId = undefined;
|
||||
|
||||
let lastAssistantTotalUsage: number | null = null;
|
||||
let lastAssistantUsage: UsageSnapshot | null = null;
|
||||
let lastAssistantModel: string | null = null;
|
||||
@@ -753,13 +784,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
// forward it to clients as structured `data`, sparing them from
|
||||
// pattern-matching on the human-readable message text.
|
||||
let lastAssistantError: SDKAssistantMessageError | undefined;
|
||||
// Tracks whether we're inside a compaction. The SDK emits the terminal
|
||||
// `status` (compact_result success/failed) twice for a single failed
|
||||
// compaction, and the two messages are indistinguishable — so we report the
|
||||
// outcome only while a compaction is in progress, then clear this. A fresh
|
||||
// `compacting` status sets it again, so every distinct compaction (e.g.
|
||||
// repeated auto-compactions in a long turn) is still shown.
|
||||
let compactionInProgress = false;
|
||||
|
||||
const userMessage = promptToClaude(params);
|
||||
|
||||
@@ -819,7 +843,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
break;
|
||||
case "status": {
|
||||
if (message.status === "compacting") {
|
||||
compactionInProgress = true;
|
||||
await this.client.sessionUpdate({
|
||||
sessionId: message.session_id,
|
||||
update: {
|
||||
@@ -827,28 +850,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
content: { type: "text", text: "Compacting..." },
|
||||
},
|
||||
});
|
||||
} else if (message.compact_result === "success" && compactionInProgress) {
|
||||
// The SDK signals manual `/compact` completion with a status
|
||||
// message carrying `compact_result`, not the `compact_boundary`
|
||||
// message (which only fires when there's content to compact).
|
||||
compactionInProgress = false;
|
||||
await this.client.sessionUpdate({
|
||||
sessionId: message.session_id,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "\n\nCompacting completed." },
|
||||
},
|
||||
});
|
||||
} else if (message.compact_result === "failed" && compactionInProgress) {
|
||||
compactionInProgress = false;
|
||||
const reason = message.compact_error ? `: ${message.compact_error}` : ".";
|
||||
await this.client.sessionUpdate({
|
||||
sessionId: message.session_id,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: `\n\nCompacting failed${reason}` },
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -864,10 +865,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
// The alternative (no update) leaves the client showing e.g.
|
||||
// "944k/1m" right after the user sees "Compacting completed",
|
||||
// which is confusing and wrong.
|
||||
//
|
||||
// The "Compacting completed." text is emitted from the `status`
|
||||
// handler (keyed on `compact_result`), not here, so the failure
|
||||
// path gets a message too.
|
||||
lastAssistantTotalUsage = 0;
|
||||
lastAssistantUsage = null;
|
||||
await this.client.sessionUpdate({
|
||||
@@ -878,6 +875,13 @@ export class ClaudeAcpAgent implements Agent {
|
||||
size: session.contextWindowSize,
|
||||
},
|
||||
});
|
||||
await this.client.sessionUpdate({
|
||||
sessionId: message.session_id,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "\n\nCompacting completed." },
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "local_command_output": {
|
||||
@@ -953,7 +957,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
case "api_retry":
|
||||
case "mirror_error":
|
||||
case "permission_denied":
|
||||
case "thinking_tokens":
|
||||
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
||||
break;
|
||||
default:
|
||||
@@ -1084,6 +1087,31 @@ export class ClaudeAcpAgent implements Agent {
|
||||
break;
|
||||
}
|
||||
case "stream_event": {
|
||||
// Track which assistant message ids have produced streamed text /
|
||||
// thinking deltas so the `assistant` case below can fall back to
|
||||
// emitting the assembled content when streaming didn't (e.g. when
|
||||
// the SDK is talking to a model proxy that never produces
|
||||
// `content_block_delta` events for the second model call in a
|
||||
// tool-use loop).
|
||||
//
|
||||
// This runs for every stream_event (not gated on
|
||||
// `parent_tool_use_id === null` like the usage block below) because
|
||||
// subagent assistant messages flow through the same content filter.
|
||||
// `currentStreamingMessageId` is a single field, which is safe
|
||||
// because the SDK serializes the event stream and `message_start`
|
||||
// always precedes its own `content_block_delta`s, so the captured
|
||||
// id is always the parent of the deltas that follow.
|
||||
if (message.event.type === "message_start") {
|
||||
session.currentStreamingMessageId = message.event.message.id;
|
||||
} else if (
|
||||
message.event.type === "content_block_delta" &&
|
||||
(message.event.delta.type === "text_delta" ||
|
||||
message.event.delta.type === "thinking_delta") &&
|
||||
session.currentStreamingMessageId
|
||||
) {
|
||||
session.textStreamedMessageIds.add(session.currentStreamingMessageId);
|
||||
}
|
||||
|
||||
if (
|
||||
message.parent_tool_use_id === null &&
|
||||
(message.event.type === "message_start" || message.event.type === "message_delta")
|
||||
@@ -1199,7 +1227,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
// payloads (e.g. /compact's malformed output) strip to null and are
|
||||
// skipped. Mirrors the replay path at replaySessionHistory.
|
||||
if (
|
||||
message.message.role !== "system" &&
|
||||
typeof message.message.content === "string" &&
|
||||
message.message.content.includes("<local-command-stdout>")
|
||||
) {
|
||||
@@ -1244,9 +1271,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
) {
|
||||
break;
|
||||
}
|
||||
if (message.message.role === "system") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
message.type === "assistant" &&
|
||||
@@ -1259,14 +1283,77 @@ export class ClaudeAcpAgent implements Agent {
|
||||
throw RequestError.authRequired();
|
||||
}
|
||||
|
||||
// Text and thinking blocks are normally emitted from the
|
||||
// `stream_event` case above as `content_block_delta` arrives,
|
||||
// so the assembled message replays them here. We skip them in
|
||||
// that case to avoid duplication. However, if streaming never
|
||||
// produced any text/thinking delta for this assistant message
|
||||
// (e.g. because the underlying model proxy returned the response
|
||||
// as a single non-streamed block, which is common with OpenAI-
|
||||
// compatible gateways translating to the Anthropic protocol), the
|
||||
// delta path emitted nothing — filtering the assembled blocks out
|
||||
// would drop the assistant's final text on the floor while still
|
||||
// returning `stopReason: "end_turn"`. Detect that case via
|
||||
// `textStreamedMessageIds` and emit the assembled blocks as a
|
||||
// fallback.
|
||||
//
|
||||
// The fallback dedup must be per content block, not per message
|
||||
// id: such gateways frequently deliver one assistant response as
|
||||
// several `assistant` messages that *share a message id* but each
|
||||
// carry a *different* block (e.g. an empty `thinking` block, then
|
||||
// the real `text` block). Flagging the whole id after the first
|
||||
// block would emit that block, then silently drop the real text
|
||||
// arriving under the same id. So we key the dedup on the block
|
||||
// contents, which lets distinct blocks through while still
|
||||
// collapsing identical re-deliveries (an incremental block
|
||||
// followed by a consolidated message, or the same message
|
||||
// delivered twice).
|
||||
const assistantMessageId =
|
||||
message.type === "assistant" ? message.message.id : undefined;
|
||||
const liveStreamed =
|
||||
!!assistantMessageId && session.textStreamedMessageIds.has(assistantMessageId);
|
||||
|
||||
let emittedFallbackBlock = false;
|
||||
const content =
|
||||
message.type === "assistant"
|
||||
? // Handled by stream events above
|
||||
message.message.content.filter(
|
||||
(item) => !["text", "thinking"].includes(item.type),
|
||||
)
|
||||
? message.message.content.filter((item) => {
|
||||
if (item.type !== "text" && item.type !== "thinking") {
|
||||
return true;
|
||||
}
|
||||
// Already delivered live via streaming deltas — drop the
|
||||
// assembled copy to avoid duplication. Note this is
|
||||
// all-or-nothing per message id: a single delta of either
|
||||
// kind flags the id, so if a gateway streamed (say)
|
||||
// thinking deltas but delivered text only in the assembled
|
||||
// message under that same id, the assembled text is
|
||||
// dropped here. That mixed mode hasn't been observed —
|
||||
// gateways stream either everything or nothing — and it is
|
||||
// not a regression (the old code dropped it unconditionally).
|
||||
if (liveStreamed) {
|
||||
return false;
|
||||
}
|
||||
// Fallback path: emit each assembled block once, keyed on
|
||||
// its contents so re-delivered blocks dedup but distinct
|
||||
// blocks sharing a message id are all forwarded.
|
||||
const blockText = item.type === "text" ? item.text : item.thinking;
|
||||
const key = `${assistantMessageId ?? "<unknown>"}\u0000${item.type}\u0000${blockText}`;
|
||||
if (session.fallbackEmittedBlockKeys.has(key)) {
|
||||
return false;
|
||||
}
|
||||
session.fallbackEmittedBlockKeys.add(key);
|
||||
emittedFallbackBlock = true;
|
||||
return true;
|
||||
})
|
||||
: message.message.content;
|
||||
|
||||
if (emittedFallbackBlock) {
|
||||
this.logger.log(
|
||||
`[claude-agent-acp] No streamed text/thinking deltas seen for ` +
|
||||
`assistant message ${assistantMessageId ?? "<unknown>"}; ` +
|
||||
`emitting assembled blocks as fallback.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const notification of toAcpNotifications(
|
||||
content,
|
||||
message.message.role,
|
||||
@@ -2290,6 +2377,8 @@ export class ClaudeAcpAgent implements Agent {
|
||||
contextWindowSize:
|
||||
inferContextWindowFromModel(models.currentModelId) ?? DEFAULT_CONTEXT_WINDOW,
|
||||
taskState,
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -3172,7 +3261,6 @@ export function toAcpNotifications(
|
||||
case "compaction":
|
||||
case "compaction_delta":
|
||||
case "advisor_tool_result":
|
||||
case "mid_conv_system":
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -3240,11 +3328,7 @@ export function streamEventToAcpNotifications(
|
||||
taskState: options?.taskState,
|
||||
},
|
||||
);
|
||||
// No content. `ping` is a Messages-API keep-alive event that the SDK's
|
||||
// `BetaRawMessageStreamEvent` union doesn't include even though the
|
||||
// wire format emits it; the `as never` cast lets us no-op it here
|
||||
// instead of letting it fall through to `unreachable`.
|
||||
case "ping" as never:
|
||||
// No content
|
||||
case "message_start":
|
||||
case "message_delta":
|
||||
case "message_stop":
|
||||
|
||||
@@ -3,4 +3,4 @@ description: Say hello
|
||||
argument-hint: name
|
||||
---
|
||||
|
||||
Respond with "Hello $ARGUMENTS" and nothing else.
|
||||
Respond with "Hello $1" and nothing else.
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
ClaudeAcpAgent,
|
||||
claudeCliPath,
|
||||
describeAlwaysAllow,
|
||||
streamEventToAcpNotifications,
|
||||
type SDKMessageFilter,
|
||||
} from "../acp-agent.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
@@ -239,15 +238,13 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
|
||||
name: "compact",
|
||||
});
|
||||
|
||||
// Build up enough conversation that there's something to compact. The SDK
|
||||
// refuses to compact a conversation with too few message groups.
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await connection.prompt({
|
||||
prompt: [{ type: "text", text: `Reply with just the number ${i}.` }],
|
||||
sessionId: newSessionResponse.sessionId,
|
||||
});
|
||||
client.takeReceivedText();
|
||||
}
|
||||
// Send something
|
||||
await connection.prompt({
|
||||
prompt: [{ type: "text", text: "Hi" }],
|
||||
sessionId: newSessionResponse.sessionId,
|
||||
});
|
||||
// Clear response
|
||||
client.takeReceivedText();
|
||||
|
||||
await connection.prompt({
|
||||
prompt: [
|
||||
@@ -260,7 +257,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
|
||||
});
|
||||
|
||||
expect(client.takeReceivedText()).toContain("Compacting...\n\nCompacting completed.");
|
||||
}, 60000);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe("tool conversions", () => {
|
||||
@@ -665,7 +662,6 @@ describe("tool conversions", () => {
|
||||
server_tool_use: null,
|
||||
inference_geo: null,
|
||||
iterations: null,
|
||||
output_tokens_details: null,
|
||||
speed: null,
|
||||
},
|
||||
context_management: null,
|
||||
@@ -1548,6 +1544,8 @@ describe("stop reason propagation", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1692,6 +1690,8 @@ describe("stop reason propagation", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
|
||||
const response = await agent.prompt({
|
||||
@@ -1810,6 +1810,409 @@ describe("stop reason propagation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("assistant text fallback when streaming yields no text deltas", () => {
|
||||
function createCapturingAgent() {
|
||||
const notifications: SessionNotification[] = [];
|
||||
const mockClient = {
|
||||
sessionUpdate: async (n: SessionNotification) => {
|
||||
notifications.push(n);
|
||||
},
|
||||
extNotification: async () => {},
|
||||
} as unknown as AgentSideConnection;
|
||||
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
return { agent, notifications };
|
||||
}
|
||||
|
||||
function injectSession(agent: ClaudeAcpAgent, messages: any[]) {
|
||||
const input = new Pushable<any>();
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
const { value: userMessage, done } = await iter.next();
|
||||
if (!done && userMessage) {
|
||||
yield {
|
||||
type: "user",
|
||||
message: userMessage.message,
|
||||
parent_tool_use_id: null,
|
||||
uuid: userMessage.uuid,
|
||||
session_id: "test-session",
|
||||
isReplay: true,
|
||||
};
|
||||
}
|
||||
yield* messages;
|
||||
}
|
||||
agent.sessions["test-session"] = {
|
||||
query: messageGenerator() as any,
|
||||
input,
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: { currentModeId: "default", availableModes: [] },
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cachedReadTokens: 0,
|
||||
cachedWriteTokens: 0,
|
||||
},
|
||||
configOptions: [],
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
nextPendingOrder: 0,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
function makeResult() {
|
||||
return {
|
||||
type: "result" as const,
|
||||
subtype: "success" as const,
|
||||
stop_reason: null,
|
||||
is_error: false,
|
||||
result: "",
|
||||
errors: [],
|
||||
duration_ms: 0,
|
||||
duration_api_ms: 0,
|
||||
num_turns: 1,
|
||||
total_cost_usd: 0,
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
modelUsage: {},
|
||||
permission_denials: [],
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
};
|
||||
}
|
||||
|
||||
function makeAssistantMessage(messageId: string, text: string) {
|
||||
return makeAssistantMessageWithBlocks(messageId, [{ type: "text", text }]);
|
||||
}
|
||||
|
||||
// Same as makeAssistantMessage but with arbitrary content blocks, so tests
|
||||
// can reproduce gateways that split a single response (one message id) into
|
||||
// several `assistant` messages each carrying a different block.
|
||||
function makeAssistantMessageWithBlocks(messageId: string, content: any[]) {
|
||||
return {
|
||||
type: "assistant" as const,
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
message: {
|
||||
id: messageId,
|
||||
type: "message" as const,
|
||||
role: "assistant" as const,
|
||||
model: "claude-test",
|
||||
content,
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("emits the assembled text when no content_block_delta was streamed", async () => {
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "msg_no_stream";
|
||||
const finalText = "final answer never streamed";
|
||||
injectSession(agent, [
|
||||
makeAssistantMessage(messageId, finalText),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const response = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "go" }],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe("end_turn");
|
||||
const textChunks = notifications
|
||||
.map((n) => n.update)
|
||||
.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === finalText,
|
||||
);
|
||||
expect(textChunks.length).toBe(1);
|
||||
});
|
||||
|
||||
it("does not re-emit text already streamed via content_block_delta", async () => {
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "msg_streamed";
|
||||
const streamedText = "streamed answer";
|
||||
injectSession(agent, [
|
||||
{
|
||||
type: "stream_event",
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
event: {
|
||||
type: "message_start",
|
||||
message: {
|
||||
id: messageId,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: "claude-test",
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "stream_event",
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
event: {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "text_delta", text: streamedText },
|
||||
},
|
||||
},
|
||||
makeAssistantMessage(messageId, streamedText),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const response = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "go" }],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe("end_turn");
|
||||
const textChunks = notifications
|
||||
.map((n) => n.update)
|
||||
.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === streamedText,
|
||||
);
|
||||
// Only the streamed content_block_delta should produce a chunk; the
|
||||
// assembled message must not duplicate it.
|
||||
expect(textChunks.length).toBe(1);
|
||||
});
|
||||
|
||||
it("fallback emits exactly once when the same assistant message is delivered twice", async () => {
|
||||
// Regression test: the SDK occasionally delivers the same assembled
|
||||
// assistant message twice (observed in production logs). Without
|
||||
// recording the id into `textStreamedMessageIds` after the first
|
||||
// fallback emit, the second delivery would re-fire the fallback and
|
||||
// ACP clients would see duplicate text chunks. After the dedupe fix
|
||||
// the second delivery must be a no-op.
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "msg_delivered_twice";
|
||||
const finalText = "final answer delivered twice";
|
||||
injectSession(agent, [
|
||||
makeAssistantMessage(messageId, finalText),
|
||||
makeAssistantMessage(messageId, finalText),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const response = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "go" }],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe("end_turn");
|
||||
const textChunks = notifications
|
||||
.map((n) => n.update)
|
||||
.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === finalText,
|
||||
);
|
||||
expect(textChunks.length).toBe(1);
|
||||
});
|
||||
|
||||
it("textStreamedMessageIds is cleared at the start of each new turn", async () => {
|
||||
// Regression test: assistant message ids are turn-scoped. Without
|
||||
// per-turn clear, the set would accumulate over the session's lifetime
|
||||
// and a (rare) reuse of an id across turns would be misclassified as
|
||||
// "already streamed", silently dropping the fallback emit. The two
|
||||
// turns below use the same messageId; both should fallback-emit
|
||||
// exactly once because the set is reset on each `prompt()` entry.
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "msg_reused_across_turns";
|
||||
const finalText = "answer for both turns";
|
||||
injectSession(agent, [
|
||||
makeAssistantMessage(messageId, finalText),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
makeAssistantMessage(messageId, finalText),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const first = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "turn 1" }],
|
||||
});
|
||||
const second = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "turn 2" }],
|
||||
});
|
||||
|
||||
expect(first.stopReason).toBe("end_turn");
|
||||
expect(second.stopReason).toBe("end_turn");
|
||||
const textChunks = notifications
|
||||
.map((n) => n.update)
|
||||
.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === finalText,
|
||||
);
|
||||
// Two turns each fallback-emit once.
|
||||
expect(textChunks.length).toBe(2);
|
||||
});
|
||||
|
||||
it("emits the text block when it arrives in a later same-id message than the thinking block", async () => {
|
||||
// Production regression (OpenAI-compatible gpt-5 gateway): a single
|
||||
// assistant response (one message id) is delivered as *separate*
|
||||
// `assistant` messages — an empty `thinking` block first, then the real
|
||||
// `text` block — both sharing the same id and neither streamed via
|
||||
// content_block_delta. Deduping at message-id granularity flagged the id
|
||||
// on the thinking block and then dropped the text, so the client saw the
|
||||
// fallback fire but received an empty answer. The fallback must key on the
|
||||
// block contents so the later text block is still emitted.
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "resp_split_blocks";
|
||||
const finalText = "=== Ticket Review Summary === approved";
|
||||
injectSession(agent, [
|
||||
makeAssistantMessageWithBlocks(messageId, [
|
||||
{ type: "thinking", thinking: "", signature: "sig" },
|
||||
]),
|
||||
makeAssistantMessageWithBlocks(messageId, [{ type: "text", text: finalText }]),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const response = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "go" }],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe("end_turn");
|
||||
const textChunks = notifications
|
||||
.map((n) => n.update)
|
||||
.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === finalText,
|
||||
);
|
||||
expect(textChunks.length).toBe(1);
|
||||
});
|
||||
|
||||
it("emits distinct text blocks that share a message id", async () => {
|
||||
// A gateway may split a multi-block answer (thinking + text + a second
|
||||
// text) across same-id messages. Every distinct text block must reach the
|
||||
// client exactly once.
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "resp_multi_text";
|
||||
const firstText = "我先直接执行审批并返回工单结果。";
|
||||
const secondText = "Ticket approved.";
|
||||
injectSession(agent, [
|
||||
makeAssistantMessageWithBlocks(messageId, [
|
||||
{ type: "thinking", thinking: "considering options", signature: "sig" },
|
||||
]),
|
||||
makeAssistantMessageWithBlocks(messageId, [{ type: "text", text: firstText }]),
|
||||
makeAssistantMessageWithBlocks(messageId, [{ type: "text", text: secondText }]),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const response = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "go" }],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe("end_turn");
|
||||
const updates = notifications.map((n) => n.update);
|
||||
const firstChunks = updates.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === firstText,
|
||||
);
|
||||
const secondChunks = updates.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === secondText,
|
||||
);
|
||||
expect(firstChunks.length).toBe(1);
|
||||
expect(secondChunks.length).toBe(1);
|
||||
});
|
||||
|
||||
it("does not duplicate text when an incremental block is followed by a consolidated message", async () => {
|
||||
// Some gateways deliver an incremental per-block `assistant` message and
|
||||
// then a consolidated `assistant` message (same id) that repeats that
|
||||
// block alongside others. The repeated block must not be emitted twice.
|
||||
const { agent, notifications } = createCapturingAgent();
|
||||
const messageId = "resp_consolidated";
|
||||
const thinking = "step-by-step reasoning";
|
||||
const finalText = "the answer";
|
||||
injectSession(agent, [
|
||||
makeAssistantMessageWithBlocks(messageId, [{ type: "text", text: finalText }]),
|
||||
makeAssistantMessageWithBlocks(messageId, [
|
||||
{ type: "thinking", thinking, signature: "sig" },
|
||||
{ type: "text", text: finalText },
|
||||
]),
|
||||
makeResult(),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
const response = await agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "go" }],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe("end_turn");
|
||||
const updates = notifications.map((n) => n.update);
|
||||
const textChunks = updates.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_message_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === finalText,
|
||||
);
|
||||
const thoughtChunks = updates.filter(
|
||||
(u) =>
|
||||
u.sessionUpdate === "agent_thought_chunk" &&
|
||||
u.content.type === "text" &&
|
||||
u.content.text === thinking,
|
||||
);
|
||||
expect(textChunks.length).toBe(1);
|
||||
expect(thoughtChunks.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("session/close", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
@@ -1851,6 +2254,8 @@ describe("session/close", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
@@ -1935,6 +2340,8 @@ describe("session/delete", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
@@ -2036,6 +2443,8 @@ describe("getOrCreateSession param change detection", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
@@ -2271,6 +2680,8 @@ describe("usage_update computation", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3171,6 +3582,8 @@ describe("emitRawSDKMessages", () => {
|
||||
emitRawSDKMessages,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3399,6 +3812,8 @@ describe("result origin handling", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3574,6 +3989,8 @@ describe("memory_recall handling", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3804,6 +4221,8 @@ describe("post-error recovery", () => {
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
textStreamedMessageIds: new Set<string>(),
|
||||
fallbackEmittedBlockKeys: new Set<string>(),
|
||||
};
|
||||
return { interrupt };
|
||||
}
|
||||
@@ -3876,38 +4295,3 @@ describe("post-error recovery", () => {
|
||||
expect(session.pendingMessages.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamEventToAcpNotifications", () => {
|
||||
it("treats `ping` keep-alive events as no-ops without logging to stderr", () => {
|
||||
const errors: unknown[][] = [];
|
||||
const logger = {
|
||||
log: () => {},
|
||||
error: (...args: unknown[]) => {
|
||||
errors.push(args);
|
||||
},
|
||||
};
|
||||
const pingMessage = {
|
||||
type: "stream_event",
|
||||
parent_tool_use_id: null,
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
// The SDK's typed `BetaRawMessageStreamEvent` union doesn't include
|
||||
// `ping`, but the API emits it on the wire and the SDK passes it
|
||||
// through. Cast through `unknown` to feed the realistic runtime shape.
|
||||
event: { type: "ping" } as unknown,
|
||||
} as Parameters<typeof streamEventToAcpNotifications>[0];
|
||||
|
||||
const result = streamEventToAcpNotifications(
|
||||
pingMessage,
|
||||
"test-session",
|
||||
{},
|
||||
{ sessionUpdate: async () => {} } as unknown as Parameters<
|
||||
typeof streamEventToAcpNotifications
|
||||
>[3],
|
||||
logger,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,10 +15,7 @@ const mockQuery = vi.hoisted(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async () => ({
|
||||
...(await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
)),
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
||||
query: mockQuery,
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user