mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
Simplify compaction: remove proactive abort, use Agent.continue() for retry
- Add agentLoopContinue() to pi-ai for resuming from existing context - Add Agent.continue() method and transport.continue() interface - Simplify AgentSession compaction to two cases: overflow (auto-retry) and threshold (no retry) - Remove proactive mid-turn compaction abort - Merge turn prefix summary into main summary - Add isCompacting property to AgentSession and RPC state - Block input during compaction in interactive mode - Show compaction count on session resume - Rename RPC.md to rpc.md for consistency Related to #128
This commit is contained in:
parent
d67c69c6e9
commit
5a9d844f9a
27 changed files with 1261 additions and 1011 deletions
725
package-lock.json
generated
725
package-lock.json
generated
|
|
@ -11,6 +11,9 @@
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/web-ui/example"
|
"packages/web-ui/example"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.4.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.3.5",
|
"@biomejs/biome": "2.3.5",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
|
|
@ -758,6 +761,29 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eastasianwidth": "^0.2.0",
|
||||||
|
"emoji-regex": "^9.2.2",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|
@ -1451,6 +1477,19 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@parcel/watcher/node_modules/detect-libc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"detect-libc": "bin/detect-libc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
|
|
@ -2593,21 +2632,6 @@
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bl/node_modules/readable-stream": {
|
|
||||||
"version": "3.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
|
||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"string_decoder": "^1.1.1",
|
|
||||||
"util-deprecate": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
|
@ -2798,13 +2822,6 @@
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cliui/node_modules/emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cliui/node_modules/string-width": {
|
"node_modules/cliui/node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
|
@ -3062,16 +3079,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
|
||||||
"detect-libc": "bin/detect-libc.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
|
|
@ -3122,9 +3135,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
|
|
@ -3947,6 +3960,36 @@
|
||||||
"setimmediate": "^1.0.5"
|
"setimmediate": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jszip/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jszip/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jwa": {
|
"node_modules/jwa": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
|
@ -4251,15 +4294,6 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss/node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lit": {
|
"node_modules/lit": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz",
|
||||||
|
|
@ -4795,16 +4829,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
|
@ -4845,25 +4869,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "2.3.8",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"inherits": "^2.0.3",
|
||||||
"inherits": "~2.0.3",
|
"string_decoder": "^1.1.1",
|
||||||
"isarray": "~1.0.0",
|
"util-deprecate": "^1.0.1"
|
||||||
"process-nextick-args": "~2.0.0",
|
|
||||||
"safe-buffer": "~5.1.1",
|
|
||||||
"string_decoder": "~1.1.1",
|
|
||||||
"util-deprecate": "~1.0.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
"engines": {
|
||||||
"version": "5.1.2",
|
"node": ">= 6"
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
}
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
|
|
@ -5203,32 +5221,26 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder/node_modules/safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eastasianwidth": "^0.2.0",
|
"get-east-asian-width": "^1.3.0",
|
||||||
"emoji-regex": "^9.2.2",
|
"strip-ansi": "^7.1.0"
|
||||||
"strip-ansi": "^7.0.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
|
@ -5258,12 +5270,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
|
@ -5430,21 +5436,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-stream/node_modules/readable-stream": {
|
|
||||||
"version": "3.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
|
||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"string_decoder": "^1.1.1",
|
|
||||||
"util-deprecate": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|
@ -6342,12 +6333,6 @@
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
|
@ -6374,6 +6359,29 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/string-width": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eastasianwidth": "^0.2.0",
|
||||||
|
"emoji-regex": "^9.2.2",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|
@ -6463,13 +6471,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yargs/node_modules/emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/yargs/node_modules/string-width": {
|
"node_modules/yargs/node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
|
@ -6522,8 +6523,8 @@
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
"@mariozechner/pi-ai": "^0.16.0",
|
||||||
"@mariozechner/pi-tui": "^0.15.0"
|
"@mariozechner/pi-tui": "^0.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
|
|
@ -6534,93 +6535,20 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/agent/node_modules/@mariozechner/pi-ai": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-vCwDfL4DtIZ73+gnWngFDpi6e7yTzHnX21sWTkpWHOT86BRLVu9gWhUo9lEvQbwA8R15qO5rjRR5J6MRu34Tjw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
|
||||||
"@google/genai": "1.31.0",
|
|
||||||
"@sinclair/typebox": "^0.34.41",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"chalk": "^5.6.2",
|
|
||||||
"openai": "6.10.0",
|
|
||||||
"partial-json": "^0.1.7",
|
|
||||||
"zod-to-json-schema": "^3.24.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/agent/node_modules/@mariozechner/pi-tui": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-4kSCO1fbMmpsyro4F+v4hqkkkPBlM1t5LF8UE51AgcA9i/OwVX3Nq5tPgMJ5vvPgexhd8N9hgzFwlQfTzYOetg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mime-types": "^2.1.4",
|
|
||||||
"chalk": "^5.5.0",
|
|
||||||
"marked": "^15.0.12",
|
|
||||||
"mime-types": "^3.0.1",
|
|
||||||
"string-width": "^8.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/agent/node_modules/@types/node": {
|
"packages/agent/node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
|
||||||
|
"integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/agent/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/agent/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/agent/node_modules/string-width": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/agent/node_modules/undici-types": {
|
"packages/agent/node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -6649,7 +6577,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ai/node_modules/@types/node": {
|
"packages/ai/node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
|
||||||
|
"integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -6658,6 +6588,8 @@
|
||||||
},
|
},
|
||||||
"packages/ai/node_modules/undici-types": {
|
"packages/ai/node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -6666,9 +6598,9 @@
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent-core": "^0.15.0",
|
"@mariozechner/pi-agent-core": "^0.16.0",
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
"@mariozechner/pi-ai": "^0.16.0",
|
||||||
"@mariozechner/pi-tui": "^0.15.0",
|
"@mariozechner/pi-tui": "^0.16.0",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^11.0.3"
|
"glob": "^11.0.3"
|
||||||
|
|
@ -6686,55 +6618,6 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/coding-agent/node_modules/@mariozechner/pi-agent-core": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-p1SosZkdYR2fGG/2/2RzUyl4kVkrjrJQ2VGgc0lQklBf/zlKBtkiMDKHGdEVDq3y3J8JnDUleqTZZEAlzMcdXQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
|
||||||
"@mariozechner/pi-tui": "^0.15.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/coding-agent/node_modules/@mariozechner/pi-ai": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-vCwDfL4DtIZ73+gnWngFDpi6e7yTzHnX21sWTkpWHOT86BRLVu9gWhUo9lEvQbwA8R15qO5rjRR5J6MRu34Tjw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
|
||||||
"@google/genai": "1.31.0",
|
|
||||||
"@sinclair/typebox": "^0.34.41",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"chalk": "^5.6.2",
|
|
||||||
"openai": "6.10.0",
|
|
||||||
"partial-json": "^0.1.7",
|
|
||||||
"zod-to-json-schema": "^3.24.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/coding-agent/node_modules/@mariozechner/pi-tui": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-4kSCO1fbMmpsyro4F+v4hqkkkPBlM1t5LF8UE51AgcA9i/OwVX3Nq5tPgMJ5vvPgexhd8N9hgzFwlQfTzYOetg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mime-types": "^2.1.4",
|
|
||||||
"chalk": "^5.5.0",
|
|
||||||
"marked": "^15.0.12",
|
|
||||||
"mime-types": "^3.0.1",
|
|
||||||
"string-width": "^8.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/coding-agent/node_modules/@types/node": {
|
"packages/coding-agent/node_modules/@types/node": {
|
||||||
"version": "24.10.2",
|
"version": "24.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
|
||||||
|
|
@ -6745,47 +6628,6 @@
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/coding-agent/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/coding-agent/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/coding-agent/node_modules/string-width": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/coding-agent/node_modules/undici-types": {
|
"packages/coding-agent/node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
|
@ -6799,8 +6641,8 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sandbox-runtime": "^0.0.16",
|
"@anthropic-ai/sandbox-runtime": "^0.0.16",
|
||||||
"@mariozechner/pi-agent-core": "^0.15.0",
|
"@mariozechner/pi-agent-core": "^0.16.0",
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
"@mariozechner/pi-ai": "^0.16.0",
|
||||||
"@sinclair/typebox": "^0.34.0",
|
"@sinclair/typebox": "^0.34.0",
|
||||||
"@slack/socket-mode": "^2.0.0",
|
"@slack/socket-mode": "^2.0.0",
|
||||||
"@slack/web-api": "^7.0.0",
|
"@slack/web-api": "^7.0.0",
|
||||||
|
|
@ -6819,106 +6661,20 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/mom/node_modules/@mariozechner/pi-agent-core": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-p1SosZkdYR2fGG/2/2RzUyl4kVkrjrJQ2VGgc0lQklBf/zlKBtkiMDKHGdEVDq3y3J8JnDUleqTZZEAlzMcdXQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
|
||||||
"@mariozechner/pi-tui": "^0.15.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/mom/node_modules/@mariozechner/pi-ai": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-vCwDfL4DtIZ73+gnWngFDpi6e7yTzHnX21sWTkpWHOT86BRLVu9gWhUo9lEvQbwA8R15qO5rjRR5J6MRu34Tjw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
|
||||||
"@google/genai": "1.31.0",
|
|
||||||
"@sinclair/typebox": "^0.34.41",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"chalk": "^5.6.2",
|
|
||||||
"openai": "6.10.0",
|
|
||||||
"partial-json": "^0.1.7",
|
|
||||||
"zod-to-json-schema": "^3.24.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/mom/node_modules/@mariozechner/pi-tui": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-4kSCO1fbMmpsyro4F+v4hqkkkPBlM1t5LF8UE51AgcA9i/OwVX3Nq5tPgMJ5vvPgexhd8N9hgzFwlQfTzYOetg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mime-types": "^2.1.4",
|
|
||||||
"chalk": "^5.5.0",
|
|
||||||
"marked": "^15.0.12",
|
|
||||||
"mime-types": "^3.0.1",
|
|
||||||
"string-width": "^8.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/mom/node_modules/@types/node": {
|
"packages/mom/node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
|
||||||
|
"integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/mom/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/mom/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/mom/node_modules/string-width": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/mom/node_modules/undici-types": {
|
"packages/mom/node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -6927,7 +6683,7 @@
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent-core": "^0.15.0",
|
"@mariozechner/pi-agent-core": "^0.16.0",
|
||||||
"chalk": "^5.5.0"
|
"chalk": "^5.5.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -6938,96 +6694,6 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/pods/node_modules/@mariozechner/pi-agent-core": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-p1SosZkdYR2fGG/2/2RzUyl4kVkrjrJQ2VGgc0lQklBf/zlKBtkiMDKHGdEVDq3y3J8JnDUleqTZZEAlzMcdXQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
|
||||||
"@mariozechner/pi-tui": "^0.15.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/pods/node_modules/@mariozechner/pi-ai": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-vCwDfL4DtIZ73+gnWngFDpi6e7yTzHnX21sWTkpWHOT86BRLVu9gWhUo9lEvQbwA8R15qO5rjRR5J6MRu34Tjw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
|
||||||
"@google/genai": "1.31.0",
|
|
||||||
"@sinclair/typebox": "^0.34.41",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"chalk": "^5.6.2",
|
|
||||||
"openai": "6.10.0",
|
|
||||||
"partial-json": "^0.1.7",
|
|
||||||
"zod-to-json-schema": "^3.24.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/pods/node_modules/@mariozechner/pi-tui": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-4kSCO1fbMmpsyro4F+v4hqkkkPBlM1t5LF8UE51AgcA9i/OwVX3Nq5tPgMJ5vvPgexhd8N9hgzFwlQfTzYOetg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mime-types": "^2.1.4",
|
|
||||||
"chalk": "^5.5.0",
|
|
||||||
"marked": "^15.0.12",
|
|
||||||
"mime-types": "^3.0.1",
|
|
||||||
"string-width": "^8.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/pods/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/pods/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/pods/node_modules/string-width": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/proxy": {
|
"packages/proxy": {
|
||||||
"name": "@mariozechner/pi-proxy",
|
"name": "@mariozechner/pi-proxy",
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
|
|
@ -7065,6 +6731,8 @@
|
||||||
},
|
},
|
||||||
"packages/tui/node_modules/mime-db": {
|
"packages/tui/node_modules/mime-db": {
|
||||||
"version": "1.54.0",
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
|
|
@ -7072,6 +6740,8 @@
|
||||||
},
|
},
|
||||||
"packages/tui/node_modules/mime-types": {
|
"packages/tui/node_modules/mime-types": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "^1.54.0"
|
"mime-db": "^1.54.0"
|
||||||
|
|
@ -7084,30 +6754,14 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/tui/node_modules/string-width": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/web-ui": {
|
"packages/web-ui": {
|
||||||
"name": "@mariozechner/pi-web-ui",
|
"name": "@mariozechner/pi-web-ui",
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
"@mariozechner/pi-ai": "^0.15.0",
|
"@mariozechner/pi-ai": "^0.16.0",
|
||||||
"@mariozechner/pi-tui": "^0.15.0",
|
"@mariozechner/pi-tui": "^0.16.0",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.544.0",
|
"lucide": "^0.544.0",
|
||||||
|
|
@ -7141,87 +6795,6 @@
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^7.1.6"
|
"vite": "^7.1.6"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"packages/web-ui/example/node_modules/@mariozechner/pi-ai": {
|
|
||||||
"resolved": "packages/ai",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"packages/web-ui/node_modules/@mariozechner/pi-ai": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-vCwDfL4DtIZ73+gnWngFDpi6e7yTzHnX21sWTkpWHOT86BRLVu9gWhUo9lEvQbwA8R15qO5rjRR5J6MRu34Tjw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@anthropic-ai/sdk": "0.71.2",
|
|
||||||
"@google/genai": "1.31.0",
|
|
||||||
"@sinclair/typebox": "^0.34.41",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"chalk": "^5.6.2",
|
|
||||||
"openai": "6.10.0",
|
|
||||||
"partial-json": "^0.1.7",
|
|
||||||
"zod-to-json-schema": "^3.24.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/web-ui/node_modules/@mariozechner/pi-tui": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-4kSCO1fbMmpsyro4F+v4hqkkkPBlM1t5LF8UE51AgcA9i/OwVX3Nq5tPgMJ5vvPgexhd8N9hgzFwlQfTzYOetg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mime-types": "^2.1.4",
|
|
||||||
"chalk": "^5.5.0",
|
|
||||||
"marked": "^15.0.12",
|
|
||||||
"mime-types": "^3.0.1",
|
|
||||||
"string-width": "^8.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/web-ui/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/web-ui/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/web-ui/node_modules/string-width": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,8 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"version": "0.0.3"
|
"version": "0.0.3",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.4.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,11 +176,6 @@ export class Agent {
|
||||||
throw new Error("No model configured");
|
throw new Error("No model configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up running prompt tracking
|
|
||||||
this.runningPrompt = new Promise<void>((resolve) => {
|
|
||||||
this.resolveRunningPrompt = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build user message with attachments
|
// Build user message with attachments
|
||||||
const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
|
const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
|
||||||
if (attachments?.length) {
|
if (attachments?.length) {
|
||||||
|
|
@ -204,6 +199,62 @@ export class Agent {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await this._runAgentLoop(userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue from the current context without adding a new user message.
|
||||||
|
* Used for retry after overflow recovery when context already has user message or tool results.
|
||||||
|
*/
|
||||||
|
async continue() {
|
||||||
|
const messages = this._state.messages;
|
||||||
|
if (messages.length === 0) {
|
||||||
|
throw new Error("No messages to continue from");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
if (lastMessage.role !== "user" && lastMessage.role !== "toolResult") {
|
||||||
|
throw new Error(`Cannot continue from message role: ${lastMessage.role}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._runAgentLoopContinue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal: Run the agent loop with a new user message.
|
||||||
|
*/
|
||||||
|
private async _runAgentLoop(userMessage: AppMessage) {
|
||||||
|
const { llmMessages, cfg } = await this._prepareRun();
|
||||||
|
|
||||||
|
const events = this.transport.run(llmMessages, userMessage as Message, cfg, this.abortController!.signal);
|
||||||
|
|
||||||
|
await this._processEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal: Continue the agent loop from current context.
|
||||||
|
*/
|
||||||
|
private async _runAgentLoopContinue() {
|
||||||
|
const { llmMessages, cfg } = await this._prepareRun();
|
||||||
|
|
||||||
|
const events = this.transport.continue(llmMessages, cfg, this.abortController!.signal);
|
||||||
|
|
||||||
|
await this._processEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare for running the agent loop.
|
||||||
|
*/
|
||||||
|
private async _prepareRun() {
|
||||||
|
const model = this._state.model;
|
||||||
|
if (!model) {
|
||||||
|
throw new Error("No model configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.runningPrompt = new Promise<void>((resolve) => {
|
||||||
|
this.resolveRunningPrompt = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
this._state.isStreaming = true;
|
this._state.isStreaming = true;
|
||||||
this._state.streamMessage = null;
|
this._state.streamMessage = null;
|
||||||
|
|
@ -222,9 +273,7 @@ export class Agent {
|
||||||
model,
|
model,
|
||||||
reasoning,
|
reasoning,
|
||||||
getQueuedMessages: async <T>() => {
|
getQueuedMessages: async <T>() => {
|
||||||
// Return queued messages based on queue mode
|
|
||||||
if (this.queueMode === "one-at-a-time") {
|
if (this.queueMode === "one-at-a-time") {
|
||||||
// Return only first message
|
|
||||||
if (this.messageQueue.length > 0) {
|
if (this.messageQueue.length > 0) {
|
||||||
const first = this.messageQueue[0];
|
const first = this.messageQueue[0];
|
||||||
this.messageQueue = this.messageQueue.slice(1);
|
this.messageQueue = this.messageQueue.slice(1);
|
||||||
|
|
@ -232,7 +281,6 @@ export class Agent {
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
// Return all queued messages at once
|
|
||||||
const queued = this.messageQueue.slice();
|
const queued = this.messageQueue.slice();
|
||||||
this.messageQueue = [];
|
this.messageQueue = [];
|
||||||
return queued as QueuedMessage<T>[];
|
return queued as QueuedMessage<T>[];
|
||||||
|
|
@ -240,32 +288,30 @@ export class Agent {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track all messages generated in this prompt
|
|
||||||
const generatedMessages: AppMessage[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
let partial: Message | null = null;
|
|
||||||
|
|
||||||
// Transform app messages to LLM-compatible messages (initial set)
|
|
||||||
const llmMessages = await this.messageTransformer(this._state.messages);
|
const llmMessages = await this.messageTransformer(this._state.messages);
|
||||||
|
|
||||||
for await (const ev of this.transport.run(
|
return { llmMessages, cfg, model };
|
||||||
llmMessages,
|
}
|
||||||
userMessage as Message,
|
|
||||||
cfg,
|
/**
|
||||||
this.abortController.signal,
|
* Process events from the transport.
|
||||||
)) {
|
*/
|
||||||
// Update internal state BEFORE emitting events
|
private async _processEvents(events: AsyncIterable<AgentEvent>) {
|
||||||
// so handlers see consistent state
|
const model = this._state.model!;
|
||||||
|
const generatedMessages: AppMessage[] = [];
|
||||||
|
let partial: AppMessage | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const ev of events) {
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case "message_start": {
|
case "message_start": {
|
||||||
partial = ev.message;
|
partial = ev.message as AppMessage;
|
||||||
this._state.streamMessage = ev.message;
|
this._state.streamMessage = ev.message as Message;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "message_update": {
|
case "message_update": {
|
||||||
partial = ev.message;
|
partial = ev.message as AppMessage;
|
||||||
this._state.streamMessage = ev.message;
|
this._state.streamMessage = ev.message as Message;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "message_end": {
|
case "message_end": {
|
||||||
|
|
@ -299,7 +345,6 @@ export class Agent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit after state is updated
|
|
||||||
this.emit(ev as AgentEvent);
|
this.emit(ev as AgentEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import type {
|
||||||
ToolCall,
|
ToolCall,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
import { agentLoop } from "@mariozechner/pi-ai";
|
import { agentLoop, agentLoopContinue } from "@mariozechner/pi-ai";
|
||||||
import { AssistantMessageEventStream } from "@mariozechner/pi-ai/dist/utils/event-stream.js";
|
import { AssistantMessageEventStream } from "@mariozechner/pi-ai/dist/utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "@mariozechner/pi-ai/dist/utils/json-parse.js";
|
import { parseStreamingJson } from "@mariozechner/pi-ai/dist/utils/json-parse.js";
|
||||||
import type { ProxyAssistantMessageEvent } from "./proxy-types.js";
|
import type { ProxyAssistantMessageEvent } from "./proxy-types.js";
|
||||||
|
|
@ -335,14 +335,8 @@ export class AppTransport implements AgentTransport {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
private async getStreamFn(authToken: string) {
|
||||||
const authToken = await this.options.getAuthToken();
|
return <TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions) => {
|
||||||
if (!authToken) {
|
|
||||||
throw new Error("Auth token is required for AppTransport");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use proxy - no local API key needed
|
|
||||||
const streamFn = <TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions) => {
|
|
||||||
return streamSimpleProxy(
|
return streamSimpleProxy(
|
||||||
model,
|
model,
|
||||||
context,
|
context,
|
||||||
|
|
@ -353,24 +347,51 @@ export class AppTransport implements AgentTransport {
|
||||||
this.options.proxyUrl,
|
this.options.proxyUrl,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Messages are already LLM-compatible (filtered by Agent)
|
private buildContext(messages: Message[], cfg: AgentRunConfig): AgentContext {
|
||||||
const context: AgentContext = {
|
return {
|
||||||
systemPrompt: cfg.systemPrompt,
|
systemPrompt: cfg.systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
tools: cfg.tools,
|
tools: cfg.tools,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const pc: AgentLoopConfig = {
|
private buildLoopConfig(cfg: AgentRunConfig): AgentLoopConfig {
|
||||||
|
return {
|
||||||
model: cfg.model,
|
model: cfg.model,
|
||||||
reasoning: cfg.reasoning,
|
reasoning: cfg.reasoning,
|
||||||
getQueuedMessages: cfg.getQueuedMessages,
|
getQueuedMessages: cfg.getQueuedMessages,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const authToken = await this.options.getAuthToken();
|
||||||
|
if (!authToken) {
|
||||||
|
throw new Error("Auth token is required for AppTransport");
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamFn = await this.getStreamFn(authToken);
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(cfg);
|
||||||
|
|
||||||
// Yield events from the upstream agentLoop iterator
|
|
||||||
// Pass streamFn as the 5th parameter to use proxy
|
|
||||||
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal, streamFn as any)) {
|
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal, streamFn as any)) {
|
||||||
yield ev;
|
yield ev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *continue(messages: Message[], cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const authToken = await this.options.getAuthToken();
|
||||||
|
if (!authToken) {
|
||||||
|
throw new Error("Auth token is required for AppTransport");
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamFn = await this.getStreamFn(authToken);
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(cfg);
|
||||||
|
|
||||||
|
for await (const ev of agentLoopContinue(context, pc, signal, streamFn as any)) {
|
||||||
|
yield ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
type AgentContext,
|
type AgentContext,
|
||||||
type AgentLoopConfig,
|
type AgentLoopConfig,
|
||||||
agentLoop,
|
agentLoop,
|
||||||
|
agentLoopContinue,
|
||||||
type Message,
|
type Message,
|
||||||
type UserMessage,
|
type UserMessage,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
|
|
@ -33,18 +34,15 @@ export class ProviderTransport implements AgentTransport {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
private async getModelAndKey(cfg: AgentRunConfig) {
|
||||||
// Get API key
|
|
||||||
let apiKey: string | undefined;
|
let apiKey: string | undefined;
|
||||||
if (this.options.getApiKey) {
|
if (this.options.getApiKey) {
|
||||||
apiKey = await this.options.getApiKey(cfg.model.provider);
|
apiKey = await this.options.getApiKey(cfg.model.provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(`No API key found for provider: ${cfg.model.provider}`);
|
throw new Error(`No API key found for provider: ${cfg.model.provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone model and modify baseUrl if CORS proxy is enabled
|
|
||||||
let model = cfg.model;
|
let model = cfg.model;
|
||||||
if (this.options.corsProxyUrl && cfg.model.baseUrl) {
|
if (this.options.corsProxyUrl && cfg.model.baseUrl) {
|
||||||
model = {
|
model = {
|
||||||
|
|
@ -53,23 +51,43 @@ export class ProviderTransport implements AgentTransport {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages are already LLM-compatible (filtered by Agent)
|
return { model, apiKey };
|
||||||
const context: AgentContext = {
|
}
|
||||||
|
|
||||||
|
private buildContext(messages: Message[], cfg: AgentRunConfig): AgentContext {
|
||||||
|
return {
|
||||||
systemPrompt: cfg.systemPrompt,
|
systemPrompt: cfg.systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
tools: cfg.tools,
|
tools: cfg.tools,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const pc: AgentLoopConfig = {
|
private buildLoopConfig(model: typeof cfg.model, apiKey: string, cfg: AgentRunConfig): AgentLoopConfig {
|
||||||
|
return {
|
||||||
model,
|
model,
|
||||||
reasoning: cfg.reasoning,
|
reasoning: cfg.reasoning,
|
||||||
apiKey,
|
apiKey,
|
||||||
getQueuedMessages: cfg.getQueuedMessages,
|
getQueuedMessages: cfg.getQueuedMessages,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const { model, apiKey } = await this.getModelAndKey(cfg);
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(model, apiKey, cfg);
|
||||||
|
|
||||||
// Yield events from agentLoop
|
|
||||||
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal)) {
|
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal)) {
|
||||||
yield ev;
|
yield ev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *continue(messages: Message[], cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const { model, apiKey } = await this.getModelAndKey(cfg);
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(model, apiKey, cfg);
|
||||||
|
|
||||||
|
for await (const ev of agentLoopContinue(context, pc, signal)) {
|
||||||
|
yield ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,14 @@ export interface AgentRunConfig {
|
||||||
* Events yielded must match the @mariozechner/pi-ai AgentEvent types.
|
* Events yielded must match the @mariozechner/pi-ai AgentEvent types.
|
||||||
*/
|
*/
|
||||||
export interface AgentTransport {
|
export interface AgentTransport {
|
||||||
|
/** Run with a new user message */
|
||||||
run(
|
run(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
userMessage: Message,
|
userMessage: Message,
|
||||||
config: AgentRunConfig,
|
config: AgentRunConfig,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): AsyncIterable<AgentEvent>;
|
): AsyncIterable<AgentEvent>;
|
||||||
|
|
||||||
|
/** Continue from current context (no new user message) */
|
||||||
|
continue(messages: Message[], config: AgentRunConfig, signal?: AbortSignal): AsyncIterable<AgentEvent>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
import type { Model } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, Model, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
|
||||||
import { calculateTool, getModel } from "@mariozechner/pi-ai";
|
import { calculateTool, getModel } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { Agent, ProviderTransport } from "../src/index.js";
|
import { Agent, ProviderTransport } from "../src/index.js";
|
||||||
|
|
||||||
async function basicPrompt(model: Model<any>) {
|
function createTransport() {
|
||||||
const agent = new Agent({
|
return new ProviderTransport({
|
||||||
initialState: {
|
|
||||||
systemPrompt: "You are a helpful assistant. Keep your responses concise.",
|
|
||||||
model,
|
|
||||||
thinkingLevel: "off",
|
|
||||||
tools: [],
|
|
||||||
},
|
|
||||||
transport: new ProviderTransport({
|
|
||||||
getApiKey: async (provider) => {
|
getApiKey: async (provider) => {
|
||||||
// Map provider names to env var names
|
|
||||||
const envVarMap: Record<string, string> = {
|
const envVarMap: Record<string, string> = {
|
||||||
google: "GEMINI_API_KEY",
|
google: "GEMINI_API_KEY",
|
||||||
openai: "OPENAI_API_KEY",
|
openai: "OPENAI_API_KEY",
|
||||||
|
|
@ -26,7 +18,18 @@ async function basicPrompt(model: Model<any>) {
|
||||||
const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
|
const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
|
||||||
return process.env[envVar];
|
return process.env[envVar];
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function basicPrompt(model: Model<any>) {
|
||||||
|
const agent = new Agent({
|
||||||
|
initialState: {
|
||||||
|
systemPrompt: "You are a helpful assistant. Keep your responses concise.",
|
||||||
|
model,
|
||||||
|
thinkingLevel: "off",
|
||||||
|
tools: [],
|
||||||
|
},
|
||||||
|
transport: createTransport(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await agent.prompt("What is 2+2? Answer with just the number.");
|
await agent.prompt("What is 2+2? Answer with just the number.");
|
||||||
|
|
@ -54,22 +57,7 @@ async function toolExecution(model: Model<any>) {
|
||||||
thinkingLevel: "off",
|
thinkingLevel: "off",
|
||||||
tools: [calculateTool],
|
tools: [calculateTool],
|
||||||
},
|
},
|
||||||
transport: new ProviderTransport({
|
transport: createTransport(),
|
||||||
getApiKey: async (provider) => {
|
|
||||||
// Map provider names to env var names
|
|
||||||
const envVarMap: Record<string, string> = {
|
|
||||||
google: "GEMINI_API_KEY",
|
|
||||||
openai: "OPENAI_API_KEY",
|
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
|
||||||
xai: "XAI_API_KEY",
|
|
||||||
groq: "GROQ_API_KEY",
|
|
||||||
cerebras: "CEREBRAS_API_KEY",
|
|
||||||
zai: "ZAI_API_KEY",
|
|
||||||
};
|
|
||||||
const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
|
|
||||||
return process.env[envVar];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await agent.prompt("Calculate 123 * 456 using the calculator tool.");
|
await agent.prompt("Calculate 123 * 456 using the calculator tool.");
|
||||||
|
|
@ -111,22 +99,7 @@ async function abortExecution(model: Model<any>) {
|
||||||
thinkingLevel: "off",
|
thinkingLevel: "off",
|
||||||
tools: [calculateTool],
|
tools: [calculateTool],
|
||||||
},
|
},
|
||||||
transport: new ProviderTransport({
|
transport: createTransport(),
|
||||||
getApiKey: async (provider) => {
|
|
||||||
// Map provider names to env var names
|
|
||||||
const envVarMap: Record<string, string> = {
|
|
||||||
google: "GEMINI_API_KEY",
|
|
||||||
openai: "OPENAI_API_KEY",
|
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
|
||||||
xai: "XAI_API_KEY",
|
|
||||||
groq: "GROQ_API_KEY",
|
|
||||||
cerebras: "CEREBRAS_API_KEY",
|
|
||||||
zai: "ZAI_API_KEY",
|
|
||||||
};
|
|
||||||
const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
|
|
||||||
return process.env[envVar];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const promptPromise = agent.prompt("Calculate 100 * 200, then 300 * 400, then sum the results.");
|
const promptPromise = agent.prompt("Calculate 100 * 200, then 300 * 400, then sum the results.");
|
||||||
|
|
@ -156,22 +129,7 @@ async function stateUpdates(model: Model<any>) {
|
||||||
thinkingLevel: "off",
|
thinkingLevel: "off",
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
transport: new ProviderTransport({
|
transport: createTransport(),
|
||||||
getApiKey: async (provider) => {
|
|
||||||
// Map provider names to env var names
|
|
||||||
const envVarMap: Record<string, string> = {
|
|
||||||
google: "GEMINI_API_KEY",
|
|
||||||
openai: "OPENAI_API_KEY",
|
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
|
||||||
xai: "XAI_API_KEY",
|
|
||||||
groq: "GROQ_API_KEY",
|
|
||||||
cerebras: "CEREBRAS_API_KEY",
|
|
||||||
zai: "ZAI_API_KEY",
|
|
||||||
};
|
|
||||||
const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
|
|
||||||
return process.env[envVar];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const events: Array<string> = [];
|
const events: Array<string> = [];
|
||||||
|
|
@ -204,22 +162,7 @@ async function multiTurnConversation(model: Model<any>) {
|
||||||
thinkingLevel: "off",
|
thinkingLevel: "off",
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
transport: new ProviderTransport({
|
transport: createTransport(),
|
||||||
getApiKey: async (provider) => {
|
|
||||||
// Map provider names to env var names
|
|
||||||
const envVarMap: Record<string, string> = {
|
|
||||||
google: "GEMINI_API_KEY",
|
|
||||||
openai: "OPENAI_API_KEY",
|
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
|
||||||
xai: "XAI_API_KEY",
|
|
||||||
groq: "GROQ_API_KEY",
|
|
||||||
cerebras: "CEREBRAS_API_KEY",
|
|
||||||
zai: "ZAI_API_KEY",
|
|
||||||
};
|
|
||||||
const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
|
|
||||||
return process.env[envVar];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await agent.prompt("My name is Alice.");
|
await agent.prompt("My name is Alice.");
|
||||||
|
|
@ -284,8 +227,8 @@ describe("Agent E2E Tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic Provider (claude-3-5-haiku-20241022)", () => {
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic Provider (claude-haiku-4-5)", () => {
|
||||||
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
|
||||||
it("should handle basic text prompt", async () => {
|
it("should handle basic text prompt", async () => {
|
||||||
await basicPrompt(model);
|
await basicPrompt(model);
|
||||||
|
|
@ -404,3 +347,164 @@ describe("Agent E2E Tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Agent.continue()", () => {
|
||||||
|
describe("validation", () => {
|
||||||
|
it("should throw when no messages in context", async () => {
|
||||||
|
const agent = new Agent({
|
||||||
|
initialState: {
|
||||||
|
systemPrompt: "Test",
|
||||||
|
model: getModel("anthropic", "claude-haiku-4-5"),
|
||||||
|
},
|
||||||
|
transport: createTransport(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(agent.continue()).rejects.toThrow("No messages to continue from");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw when last message is assistant", async () => {
|
||||||
|
const agent = new Agent({
|
||||||
|
initialState: {
|
||||||
|
systemPrompt: "Test",
|
||||||
|
model: getModel("anthropic", "claude-haiku-4-5"),
|
||||||
|
},
|
||||||
|
transport: createTransport(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistantMessage: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-haiku-4-5",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
agent.replaceMessages([assistantMessage]);
|
||||||
|
|
||||||
|
await expect(agent.continue()).rejects.toThrow("Cannot continue from message role: assistant");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("continue from user message", () => {
|
||||||
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
|
||||||
|
it("should continue and get response when last message is user", async () => {
|
||||||
|
const agent = new Agent({
|
||||||
|
initialState: {
|
||||||
|
systemPrompt: "You are a helpful assistant. Follow instructions exactly.",
|
||||||
|
model,
|
||||||
|
thinkingLevel: "off",
|
||||||
|
tools: [],
|
||||||
|
},
|
||||||
|
transport: createTransport(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually add a user message without calling prompt()
|
||||||
|
const userMessage: UserMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Say exactly: HELLO WORLD" }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
agent.replaceMessages([userMessage]);
|
||||||
|
|
||||||
|
// Continue from the user message
|
||||||
|
await agent.continue();
|
||||||
|
|
||||||
|
expect(agent.state.isStreaming).toBe(false);
|
||||||
|
expect(agent.state.messages.length).toBe(2);
|
||||||
|
expect(agent.state.messages[0].role).toBe("user");
|
||||||
|
expect(agent.state.messages[1].role).toBe("assistant");
|
||||||
|
|
||||||
|
const assistantMsg = agent.state.messages[1] as AssistantMessage;
|
||||||
|
const textContent = assistantMsg.content.find((c) => c.type === "text");
|
||||||
|
expect(textContent).toBeDefined();
|
||||||
|
if (textContent?.type === "text") {
|
||||||
|
expect(textContent.text.toUpperCase()).toContain("HELLO WORLD");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("continue from tool result", () => {
|
||||||
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
|
||||||
|
it("should continue and process tool results", async () => {
|
||||||
|
const agent = new Agent({
|
||||||
|
initialState: {
|
||||||
|
systemPrompt:
|
||||||
|
"You are a helpful assistant. After getting a calculation result, state the answer clearly.",
|
||||||
|
model,
|
||||||
|
thinkingLevel: "off",
|
||||||
|
tools: [calculateTool],
|
||||||
|
},
|
||||||
|
transport: createTransport(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up a conversation state as if tool was just executed
|
||||||
|
const userMessage: UserMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "What is 5 + 3?" }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const assistantMessage: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Let me calculate that." },
|
||||||
|
{ type: "toolCall", id: "calc-1", name: "calculate", arguments: { expression: "5 + 3" } },
|
||||||
|
],
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-haiku-4-5",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
stopReason: "toolUse",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolResult: ToolResultMessage = {
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: "calc-1",
|
||||||
|
toolName: "calculate",
|
||||||
|
content: [{ type: "text", text: "5 + 3 = 8" }],
|
||||||
|
isError: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
agent.replaceMessages([userMessage, assistantMessage, toolResult]);
|
||||||
|
|
||||||
|
// Continue from the tool result
|
||||||
|
await agent.continue();
|
||||||
|
|
||||||
|
expect(agent.state.isStreaming).toBe(false);
|
||||||
|
// Should have added an assistant response
|
||||||
|
expect(agent.state.messages.length).toBeGreaterThanOrEqual(4);
|
||||||
|
|
||||||
|
const lastMessage = agent.state.messages[agent.state.messages.length - 1];
|
||||||
|
expect(lastMessage.role).toBe("assistant");
|
||||||
|
|
||||||
|
if (lastMessage.role === "assistant") {
|
||||||
|
const textContent = lastMessage.content
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => (c as { type: "text"; text: string }).text)
|
||||||
|
.join(" ");
|
||||||
|
// Should mention 8 in the response
|
||||||
|
expect(textContent).toMatch(/8/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "node",
|
environment: "node",
|
||||||
testTimeout: 10000, // 10 seconds
|
testTimeout: 30000, // 30 seconds for API calls
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`agentLoopContinue` function**: Continue an agent loop from existing context without adding a new user message. Validates that the last message is `user` or `toolResult`. Useful for retry after context overflow or resuming from manually-added tool results.
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
- Removed provider-level tool argument validation. Validation now happens in `agentLoop` via `executeToolCalls`, allowing models to retry on validation errors. For manual tool execution, use `validateToolCall(tools, toolCall)` or `validateToolArguments(tool, toolCall)`.
|
- Removed provider-level tool argument validation. Validation now happens in `agentLoop` via `executeToolCalls`, allowing models to retry on validation errors. For manual tool execution, use `validateToolCall(tools, toolCall)` or `validateToolArguments(tool, toolCall)`.
|
||||||
|
|
|
||||||
|
|
@ -898,6 +898,34 @@ const messages = await stream.result();
|
||||||
context.messages.push(...messages);
|
context.messages.push(...messages);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Continuing from Existing Context
|
||||||
|
|
||||||
|
Use `agentLoopContinue` to resume an agent loop without adding a new user message. This is useful for:
|
||||||
|
- Retrying after context overflow (after compaction reduces context size)
|
||||||
|
- Resuming from tool results that were added manually to the context
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { agentLoopContinue, AgentContext } from '@mariozechner/pi-ai';
|
||||||
|
|
||||||
|
// Context already has messages - last must be 'user' or 'toolResult'
|
||||||
|
const context: AgentContext = {
|
||||||
|
systemPrompt: 'You are helpful.',
|
||||||
|
messages: [userMessage, assistantMessage, toolResult],
|
||||||
|
tools: [myTool]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continue processing from the tool result
|
||||||
|
const stream = agentLoopContinue(context, { model });
|
||||||
|
|
||||||
|
for await (const event of stream) {
|
||||||
|
// Same events as agentLoop, but no user message events emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMessages = await stream.result();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation**: Throws if context has no messages or if the last message is an assistant message.
|
||||||
|
|
||||||
### Defining Tools with TypeBox
|
### Defining Tools with TypeBox
|
||||||
|
|
||||||
Tools use TypeBox schemas for runtime validation and type inference:
|
Tools use TypeBox schemas for runtime validation and type inference:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import { EventStream } from "../utils/event-stream.js";
|
||||||
import { validateToolArguments } from "../utils/validation.js";
|
import { validateToolArguments } from "../utils/validation.js";
|
||||||
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, AgentToolResult, QueuedMessage } from "./types.js";
|
import type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, AgentToolResult, QueuedMessage } from "./types.js";
|
||||||
|
|
||||||
// Main prompt function - returns a stream of events
|
/**
|
||||||
|
* Start an agent loop with a new user message.
|
||||||
|
* The prompt is added to the context and events are emitted for it.
|
||||||
|
*/
|
||||||
export function agentLoop(
|
export function agentLoop(
|
||||||
prompt: UserMessage,
|
prompt: UserMessage,
|
||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
|
|
@ -12,31 +15,80 @@ export function agentLoop(
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
streamFn?: typeof streamSimple,
|
streamFn?: typeof streamSimple,
|
||||||
): EventStream<AgentEvent, AgentContext["messages"]> {
|
): EventStream<AgentEvent, AgentContext["messages"]> {
|
||||||
const stream = new EventStream<AgentEvent, AgentContext["messages"]>(
|
const stream = createAgentStream();
|
||||||
(event: AgentEvent) => event.type === "agent_end",
|
|
||||||
(event: AgentEvent) => (event.type === "agent_end" ? event.messages : []),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run the prompt async
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// Track new messages generated during this prompt
|
const newMessages: AgentContext["messages"] = [prompt];
|
||||||
const newMessages: AgentContext["messages"] = [];
|
const currentContext: AgentContext = {
|
||||||
// Create user message for the prompt
|
...context,
|
||||||
const messages = [...context.messages, prompt];
|
messages: [...context.messages, prompt],
|
||||||
newMessages.push(prompt);
|
};
|
||||||
|
|
||||||
stream.push({ type: "agent_start" });
|
stream.push({ type: "agent_start" });
|
||||||
stream.push({ type: "turn_start" });
|
stream.push({ type: "turn_start" });
|
||||||
stream.push({ type: "message_start", message: prompt });
|
stream.push({ type: "message_start", message: prompt });
|
||||||
stream.push({ type: "message_end", message: prompt });
|
stream.push({ type: "message_end", message: prompt });
|
||||||
|
|
||||||
// Update context with new messages
|
await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
|
||||||
const currentContext: AgentContext = {
|
})();
|
||||||
...context,
|
|
||||||
messages,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keep looping while we have tool calls or queued messages
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue an agent loop from the current context without adding a new message.
|
||||||
|
* Used for retry after overflow - context already has user message or tool results.
|
||||||
|
* Throws if the last message is not a user message or tool result.
|
||||||
|
*/
|
||||||
|
export function agentLoopContinue(
|
||||||
|
context: AgentContext,
|
||||||
|
config: AgentLoopConfig,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
streamFn?: typeof streamSimple,
|
||||||
|
): EventStream<AgentEvent, AgentContext["messages"]> {
|
||||||
|
// Validate that we can continue from this context
|
||||||
|
const lastMessage = context.messages[context.messages.length - 1];
|
||||||
|
if (!lastMessage) {
|
||||||
|
throw new Error("Cannot continue: no messages in context");
|
||||||
|
}
|
||||||
|
if (lastMessage.role !== "user" && lastMessage.role !== "toolResult") {
|
||||||
|
throw new Error(`Cannot continue from message role: ${lastMessage.role}. Expected 'user' or 'toolResult'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = createAgentStream();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const newMessages: AgentContext["messages"] = [];
|
||||||
|
const currentContext: AgentContext = { ...context };
|
||||||
|
|
||||||
|
stream.push({ type: "agent_start" });
|
||||||
|
stream.push({ type: "turn_start" });
|
||||||
|
// No user message events - we're continuing from existing context
|
||||||
|
|
||||||
|
await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAgentStream(): EventStream<AgentEvent, AgentContext["messages"]> {
|
||||||
|
return new EventStream<AgentEvent, AgentContext["messages"]>(
|
||||||
|
(event: AgentEvent) => event.type === "agent_end",
|
||||||
|
(event: AgentEvent) => (event.type === "agent_end" ? event.messages : []),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared loop logic for both agentLoop and agentLoopContinue.
|
||||||
|
*/
|
||||||
|
async function runLoop(
|
||||||
|
currentContext: AgentContext,
|
||||||
|
newMessages: AgentContext["messages"],
|
||||||
|
config: AgentLoopConfig,
|
||||||
|
signal: AbortSignal | undefined,
|
||||||
|
stream: EventStream<AgentEvent, AgentContext["messages"]>,
|
||||||
|
streamFn?: typeof streamSimple,
|
||||||
|
): Promise<void> {
|
||||||
let hasMoreToolCalls = true;
|
let hasMoreToolCalls = true;
|
||||||
let firstTurn = true;
|
let firstTurn = true;
|
||||||
let queuedMessages: QueuedMessage<any>[] = (await config.getQueuedMessages?.()) || [];
|
let queuedMessages: QueuedMessage<any>[] = (await config.getQueuedMessages?.()) || [];
|
||||||
|
|
@ -61,8 +113,6 @@ export function agentLoop(
|
||||||
queuedMessages = [];
|
queuedMessages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("agent-loop: ", [...currentContext.messages]);
|
|
||||||
|
|
||||||
// Stream assistant response
|
// Stream assistant response
|
||||||
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
||||||
newMessages.push(message);
|
newMessages.push(message);
|
||||||
|
|
@ -91,11 +141,9 @@ export function agentLoop(
|
||||||
// Get queued messages after turn completes
|
// Get queued messages after turn completes
|
||||||
queuedMessages = (await config.getQueuedMessages?.()) || [];
|
queuedMessages = (await config.getQueuedMessages?.()) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.push({ type: "agent_end", messages: newMessages });
|
stream.push({ type: "agent_end", messages: newMessages });
|
||||||
stream.end(newMessages);
|
stream.end(newMessages);
|
||||||
})();
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export { agentLoop } from "./agent-loop.js";
|
export { agentLoop, agentLoopContinue } from "./agent-loop.js";
|
||||||
export * from "./tools/index.js";
|
export * from "./tools/index.js";
|
||||||
export type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, QueuedMessage } from "./types.js";
|
export type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, QueuedMessage } from "./types.js";
|
||||||
|
|
|
||||||
|
|
@ -3275,13 +3275,13 @@ export const MODELS = {
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.19999999999999998,
|
input: 0.15,
|
||||||
output: 0.7999999999999999,
|
output: 0.75,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 163840,
|
contextWindow: 8192,
|
||||||
maxTokens: 163840,
|
maxTokens: 7168,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-4o-audio-preview": {
|
"openai/gpt-4o-audio-preview": {
|
||||||
id: "openai/gpt-4o-audio-preview",
|
id: "openai/gpt-4o-audio-preview",
|
||||||
|
|
@ -4516,8 +4516,8 @@ export const MODELS = {
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.049999999999999996,
|
input: 0.04,
|
||||||
output: 0.22,
|
output: 0.15,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { agentLoop } from "../src/agent/agent-loop.js";
|
import { agentLoop, agentLoopContinue } from "../src/agent/agent-loop.js";
|
||||||
import { calculateTool } from "../src/agent/tools/calculate.js";
|
import { calculateTool } from "../src/agent/tools/calculate.js";
|
||||||
import type { AgentContext, AgentEvent, AgentLoopConfig } from "../src/agent/types.js";
|
import type { AgentContext, AgentEvent, AgentLoopConfig } from "../src/agent/types.js";
|
||||||
import { getModel } from "../src/models.js";
|
import { getModel } from "../src/models.js";
|
||||||
import type { Api, Message, Model, OptionsForApi, UserMessage } from "../src/types.js";
|
import type {
|
||||||
|
Api,
|
||||||
|
AssistantMessage,
|
||||||
|
Message,
|
||||||
|
Model,
|
||||||
|
OptionsForApi,
|
||||||
|
ToolResultMessage,
|
||||||
|
UserMessage,
|
||||||
|
} from "../src/types.js";
|
||||||
|
|
||||||
async function calculateTest<TApi extends Api>(model: Model<TApi>, options: OptionsForApi<TApi> = {}) {
|
async function calculateTest<TApi extends Api>(model: Model<TApi>, options: OptionsForApi<TApi> = {}) {
|
||||||
// Create the agent context with the calculator tool
|
// Create the agent context with the calculator tool
|
||||||
|
|
@ -282,7 +290,7 @@ describe("Agent Calculator Tests", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic Provider Agent", () => {
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic Provider Agent", () => {
|
||||||
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
|
||||||
it("should calculate multiple expressions and sum the results", async () => {
|
it("should calculate multiple expressions and sum the results", async () => {
|
||||||
const result = await calculateTest(model);
|
const result = await calculateTest(model);
|
||||||
|
|
@ -351,3 +359,175 @@ describe("Agent Calculator Tests", () => {
|
||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("agentLoopContinue", () => {
|
||||||
|
describe("validation", () => {
|
||||||
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
const baseContext: AgentContext = {
|
||||||
|
systemPrompt: "You are a helpful assistant.",
|
||||||
|
messages: [],
|
||||||
|
tools: [],
|
||||||
|
};
|
||||||
|
const config: AgentLoopConfig = { model };
|
||||||
|
|
||||||
|
it("should throw when context has no messages", () => {
|
||||||
|
expect(() => agentLoopContinue(baseContext, config)).toThrow("Cannot continue: no messages in context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw when last message is an assistant message", () => {
|
||||||
|
const assistantMessage: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-haiku-4-5",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
const context: AgentContext = {
|
||||||
|
...baseContext,
|
||||||
|
messages: [assistantMessage],
|
||||||
|
};
|
||||||
|
expect(() => agentLoopContinue(context, config)).toThrow(
|
||||||
|
"Cannot continue from message role: assistant. Expected 'user' or 'toolResult'.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: "should not throw" tests for valid inputs are covered by the E2E tests below
|
||||||
|
// which actually consume the stream and verify the output
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("continue from user message", () => {
|
||||||
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
|
||||||
|
it("should continue and get assistant response when last message is user", async () => {
|
||||||
|
const userMessage: UserMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Say exactly: HELLO WORLD" }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const context: AgentContext = {
|
||||||
|
systemPrompt: "You are a helpful assistant. Follow instructions exactly.",
|
||||||
|
messages: [userMessage],
|
||||||
|
tools: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: AgentLoopConfig = { model };
|
||||||
|
|
||||||
|
const events: AgentEvent[] = [];
|
||||||
|
const stream = agentLoopContinue(context, config);
|
||||||
|
|
||||||
|
for await (const event of stream) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await stream.result();
|
||||||
|
|
||||||
|
// Should have gotten an assistant response
|
||||||
|
expect(messages.length).toBe(1);
|
||||||
|
expect(messages[0].role).toBe("assistant");
|
||||||
|
|
||||||
|
// Verify event sequence - no user message events since we're continuing
|
||||||
|
const eventTypes = events.map((e) => e.type);
|
||||||
|
expect(eventTypes).toContain("agent_start");
|
||||||
|
expect(eventTypes).toContain("turn_start");
|
||||||
|
expect(eventTypes).toContain("message_start");
|
||||||
|
expect(eventTypes).toContain("message_end");
|
||||||
|
expect(eventTypes).toContain("turn_end");
|
||||||
|
expect(eventTypes).toContain("agent_end");
|
||||||
|
|
||||||
|
// Should NOT have user message events (that's the difference from agentLoop)
|
||||||
|
const messageEndEvents = events.filter((e) => e.type === "message_end");
|
||||||
|
expect(messageEndEvents.length).toBe(1); // Only assistant message
|
||||||
|
expect((messageEndEvents[0] as any).message.role).toBe("assistant");
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("continue from tool result", () => {
|
||||||
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||||
|
|
||||||
|
it("should continue processing after tool results", async () => {
|
||||||
|
// Simulate a conversation where:
|
||||||
|
// 1. User asked to calculate something
|
||||||
|
// 2. Assistant made a tool call
|
||||||
|
// 3. Tool result is ready
|
||||||
|
// 4. We continue from here
|
||||||
|
|
||||||
|
const userMessage: UserMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "What is 5 + 3? Use the calculator." }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const assistantMessage: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Let me calculate that for you." },
|
||||||
|
{ type: "toolCall", id: "calc-1", name: "calculate", arguments: { expression: "5 + 3" } },
|
||||||
|
],
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-haiku-4-5",
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
stopReason: "toolUse",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolResult: ToolResultMessage = {
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: "calc-1",
|
||||||
|
toolName: "calculate",
|
||||||
|
content: [{ type: "text", text: "5 + 3 = 8" }],
|
||||||
|
isError: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const context: AgentContext = {
|
||||||
|
systemPrompt: "You are a helpful assistant. After getting a calculation result, state the answer clearly.",
|
||||||
|
messages: [userMessage, assistantMessage, toolResult],
|
||||||
|
tools: [calculateTool],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: AgentLoopConfig = { model };
|
||||||
|
|
||||||
|
const events: AgentEvent[] = [];
|
||||||
|
const stream = agentLoopContinue(context, config);
|
||||||
|
|
||||||
|
for await (const event of stream) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await stream.result();
|
||||||
|
|
||||||
|
// Should have gotten an assistant response
|
||||||
|
expect(messages.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
expect(lastMessage.role).toBe("assistant");
|
||||||
|
|
||||||
|
// The assistant should mention the result (8)
|
||||||
|
if (lastMessage.role === "assistant") {
|
||||||
|
const textContent = lastMessage.content
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => (c as any).text)
|
||||||
|
.join(" ");
|
||||||
|
expect(textContent).toMatch(/8/);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Simplified compaction flow**: Removed proactive compaction (aborting mid-turn when threshold approached). Compaction now triggers in two cases only: (1) overflow error from LLM, which compacts and auto-retries, or (2) threshold crossed after a successful turn, which compacts without retry.
|
||||||
|
|
||||||
|
- **Compaction retry uses `Agent.continue()`**: Auto-retry after overflow now uses the new `continue()` API instead of re-sending the user message, preserving exact context state.
|
||||||
|
|
||||||
|
- **Merged turn prefix summary**: When a turn is split during compaction, the turn prefix summary is now merged into the main history summary instead of being stored separately.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`isCompacting` property on AgentSession**: Check if auto-compaction is currently running.
|
||||||
|
|
||||||
|
- **Session compaction indicator**: When resuming a compacted session, displays "Session compacted N times" status message.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Block input during compaction**: User input is now blocked while auto-compaction is running to prevent race conditions.
|
||||||
|
|
||||||
|
- **Skip error messages in usage calculation**: Context size estimation now skips both aborted and error messages, as neither have valid usage data.
|
||||||
|
|
||||||
## [0.16.0] - 2025-12-09
|
## [0.16.0] - 2025-12-09
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,11 @@ Long sessions can exhaust context windows. Compaction summarizes older messages
|
||||||
|
|
||||||
**Manual:** `/compact` or `/compact Focus on the API changes`
|
**Manual:** `/compact` or `/compact Focus on the API changes`
|
||||||
|
|
||||||
**Automatic:** Enable with `/autocompact`. Triggers when context exceeds threshold.
|
**Automatic:** Enable with `/autocompact`. When enabled, triggers in two cases:
|
||||||
|
- **Overflow recovery**: LLM returns context overflow error. Compacts and auto-retries.
|
||||||
|
- **Threshold maintenance**: Context exceeds `contextWindow - reserveTokens` after a successful turn. Compacts without retry.
|
||||||
|
|
||||||
|
When disabled, neither case triggers automatic compaction (use `/compact` manually if needed).
|
||||||
|
|
||||||
**How it works:**
|
**How it works:**
|
||||||
1. Cut point calculated to keep ~20k tokens of recent messages
|
1. Cut point calculated to keep ~20k tokens of recent messages
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ Response:
|
||||||
"model": {...},
|
"model": {...},
|
||||||
"thinkingLevel": "medium",
|
"thinkingLevel": "medium",
|
||||||
"isStreaming": false,
|
"isStreaming": false,
|
||||||
|
"isCompacting": false,
|
||||||
"queueMode": "all",
|
"queueMode": "all",
|
||||||
"sessionFile": "/path/to/session.jsonl",
|
"sessionFile": "/path/to/session.jsonl",
|
||||||
"sessionId": "abc123",
|
"sessionId": "abc123",
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
import type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage, Model, ToolResultMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, Model } from "@mariozechner/pi-ai";
|
||||||
import { isContextOverflow } from "@mariozechner/pi-ai";
|
import { isContextOverflow } from "@mariozechner/pi-ai";
|
||||||
import { getModelsPath } from "../config.js";
|
import { getModelsPath } from "../config.js";
|
||||||
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
|
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
|
||||||
import { calculateContextTokens, compact, estimateTokens, shouldCompact } from "./compaction.js";
|
import { calculateContextTokens, compact, shouldCompact } from "./compaction.js";
|
||||||
import { exportSessionToHtml } from "./export-html.js";
|
import { exportSessionToHtml } from "./export-html.js";
|
||||||
import type { BashExecutionMessage } from "./messages.js";
|
import type { BashExecutionMessage } from "./messages.js";
|
||||||
import { getApiKeyForModel, getAvailableModels } from "./model-config.js";
|
import { getApiKeyForModel, getAvailableModels } from "./model-config.js";
|
||||||
|
|
@ -112,8 +112,6 @@ export class AgentSession {
|
||||||
// Compaction state
|
// Compaction state
|
||||||
private _compactionAbortController: AbortController | null = null;
|
private _compactionAbortController: AbortController | null = null;
|
||||||
private _autoCompactionAbortController: AbortController | null = null;
|
private _autoCompactionAbortController: AbortController | null = null;
|
||||||
private _abortingForCompaction = false;
|
|
||||||
private _lastUserMessageText: string | null = null;
|
|
||||||
|
|
||||||
// Bash execution state
|
// Bash execution state
|
||||||
private _bashAbortController: AbortController | null = null;
|
private _bashAbortController: AbortController | null = null;
|
||||||
|
|
@ -148,48 +146,19 @@ export class AgentSession {
|
||||||
|
|
||||||
// Handle session persistence
|
// Handle session persistence
|
||||||
if (event.type === "message_end") {
|
if (event.type === "message_end") {
|
||||||
// Skip saving aborted message if we're aborting for compaction
|
|
||||||
const isAbortedForCompaction =
|
|
||||||
this._abortingForCompaction &&
|
|
||||||
event.message.role === "assistant" &&
|
|
||||||
(event.message as AssistantMessage).stopReason === "aborted";
|
|
||||||
|
|
||||||
if (!isAbortedForCompaction) {
|
|
||||||
this.sessionManager.saveMessage(event.message);
|
this.sessionManager.saveMessage(event.message);
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize session after first user+assistant exchange
|
// Initialize session after first user+assistant exchange
|
||||||
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
||||||
this.sessionManager.startSession(this.agent.state);
|
this.sessionManager.startSession(this.agent.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track user message text for potential retry after overflow
|
|
||||||
if (event.message.role === "user") {
|
|
||||||
const content = (event.message as { content: unknown }).content;
|
|
||||||
if (typeof content === "string") {
|
|
||||||
this._lastUserMessageText = content;
|
|
||||||
} else if (Array.isArray(content)) {
|
|
||||||
this._lastUserMessageText = content
|
|
||||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
||||||
.map((c) => c.text)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track assistant message for auto-compaction (checked on agent_end)
|
// Track assistant message for auto-compaction (checked on agent_end)
|
||||||
if (event.message.role === "assistant") {
|
if (event.message.role === "assistant") {
|
||||||
this._lastAssistantMessage = event.message as AssistantMessage;
|
this._lastAssistantMessage = event.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle turn_end for proactive compaction check
|
|
||||||
if (event.type === "turn_end") {
|
|
||||||
await this._checkProactiveCompaction(
|
|
||||||
event.message as AssistantMessage,
|
|
||||||
event.toolResults as ToolResultMessage[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check auto-compaction after agent completes
|
// Check auto-compaction after agent completes
|
||||||
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
||||||
const msg = this._lastAssistantMessage;
|
const msg = this._lastAssistantMessage;
|
||||||
|
|
@ -274,6 +243,11 @@ export class AgentSession {
|
||||||
return this.agent.state.isStreaming;
|
return this.agent.state.isStreaming;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether auto-compaction is currently running */
|
||||||
|
get isCompacting(): boolean {
|
||||||
|
return this._autoCompactionAbortController !== null || this._compactionAbortController !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/** All messages including custom types like BashExecutionMessage */
|
/** All messages including custom types like BashExecutionMessage */
|
||||||
get messages(): AppMessage[] {
|
get messages(): AppMessage[] {
|
||||||
return this.agent.state.messages;
|
return this.agent.state.messages;
|
||||||
|
|
@ -622,91 +596,41 @@ export class AgentSession {
|
||||||
this._autoCompactionAbortController?.abort();
|
this._autoCompactionAbortController?.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for proactive compaction after turn_end (before next LLM call).
|
|
||||||
* Estimates context size and aborts if threshold would be crossed.
|
|
||||||
*/
|
|
||||||
private async _checkProactiveCompaction(
|
|
||||||
assistantMessage: AssistantMessage,
|
|
||||||
toolResults: ToolResultMessage[],
|
|
||||||
): Promise<void> {
|
|
||||||
const settings = this.settingsManager.getCompactionSettings();
|
|
||||||
if (!settings.enabled) return;
|
|
||||||
|
|
||||||
// Skip if message was aborted or errored
|
|
||||||
if (assistantMessage.stopReason === "aborted" || assistantMessage.stopReason === "error") return;
|
|
||||||
|
|
||||||
// Only check if there are tool calls (meaning another turn will happen)
|
|
||||||
const hasToolCalls = assistantMessage.content.some((c) => c.type === "toolCall");
|
|
||||||
if (!hasToolCalls) return;
|
|
||||||
|
|
||||||
// Estimate context size: last usage + tool results
|
|
||||||
const contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
||||||
const toolResultTokens = toolResults.reduce((sum, msg) => sum + estimateTokens(msg), 0);
|
|
||||||
const estimatedTotal = contextTokens + toolResultTokens;
|
|
||||||
|
|
||||||
const contextWindow = this.model?.contextWindow ?? 0;
|
|
||||||
|
|
||||||
if (!shouldCompact(estimatedTotal, contextWindow, settings)) return;
|
|
||||||
|
|
||||||
// Threshold crossed - abort for compaction
|
|
||||||
this._abortingForCompaction = true;
|
|
||||||
this.agent.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle compaction after agent_end.
|
* Handle compaction after agent_end.
|
||||||
* Checks for overflow (reactive) or threshold (proactive after abort).
|
* Two cases:
|
||||||
|
* 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry
|
||||||
|
* 2. Threshold: Turn succeeded but context over threshold, compact, NO auto-retry (user continues manually)
|
||||||
*/
|
*/
|
||||||
private async _handleAgentEndCompaction(assistantMessage: AssistantMessage): Promise<void> {
|
private async _handleAgentEndCompaction(assistantMessage: AssistantMessage): Promise<void> {
|
||||||
const settings = this.settingsManager.getCompactionSettings();
|
const settings = this.settingsManager.getCompactionSettings();
|
||||||
|
if (!settings.enabled) return;
|
||||||
|
|
||||||
|
// Skip if message was aborted (user cancelled)
|
||||||
|
if (assistantMessage.stopReason === "aborted") return;
|
||||||
|
|
||||||
const contextWindow = this.model?.contextWindow ?? 0;
|
const contextWindow = this.model?.contextWindow ?? 0;
|
||||||
|
|
||||||
// Check 1: Overflow detection (reactive recovery)
|
// Case 1: Overflow - LLM returned context overflow error
|
||||||
const isOverflow = isContextOverflow(assistantMessage, contextWindow);
|
if (isContextOverflow(assistantMessage, contextWindow)) {
|
||||||
|
// Remove the error message from agent state (it IS saved to session for history,
|
||||||
// Check 2: Aborted for compaction (proactive)
|
// but we don't want it in context for the retry)
|
||||||
const wasAbortedForCompaction = this._abortingForCompaction;
|
|
||||||
this._abortingForCompaction = false;
|
|
||||||
|
|
||||||
// Check 3: Threshold crossed but turn succeeded (maintenance compaction)
|
|
||||||
const contextTokens =
|
|
||||||
assistantMessage.stopReason === "error" ? 0 : calculateContextTokens(assistantMessage.usage);
|
|
||||||
const thresholdCrossed = settings.enabled && shouldCompact(contextTokens, contextWindow, settings);
|
|
||||||
|
|
||||||
// Determine which action to take
|
|
||||||
let reason: "overflow" | "threshold" | null = null;
|
|
||||||
let willRetry = false;
|
|
||||||
|
|
||||||
if (isOverflow) {
|
|
||||||
reason = "overflow";
|
|
||||||
willRetry = true;
|
|
||||||
// Remove the overflow error message from agent state
|
|
||||||
const messages = this.agent.state.messages;
|
const messages = this.agent.state.messages;
|
||||||
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
||||||
this.agent.replaceMessages(messages.slice(0, -1));
|
this.agent.replaceMessages(messages.slice(0, -1));
|
||||||
}
|
}
|
||||||
} else if (wasAbortedForCompaction) {
|
await this._runAutoCompaction("overflow", true);
|
||||||
reason = "threshold";
|
return;
|
||||||
willRetry = true;
|
|
||||||
// Remove the aborted message from agent state
|
|
||||||
const messages = this.agent.state.messages;
|
|
||||||
if (
|
|
||||||
messages.length > 0 &&
|
|
||||||
messages[messages.length - 1].role === "assistant" &&
|
|
||||||
(messages[messages.length - 1] as AssistantMessage).stopReason === "aborted"
|
|
||||||
) {
|
|
||||||
this.agent.replaceMessages(messages.slice(0, -1));
|
|
||||||
}
|
|
||||||
} else if (thresholdCrossed) {
|
|
||||||
reason = "threshold";
|
|
||||||
willRetry = false; // Turn succeeded, no retry needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reason) return;
|
// Case 2: Threshold - turn succeeded but context is getting large
|
||||||
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
||||||
|
if (assistantMessage.stopReason === "error") return;
|
||||||
|
|
||||||
// Run compaction
|
const contextTokens = calculateContextTokens(assistantMessage.usage);
|
||||||
await this._runAutoCompaction(reason, willRetry);
|
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
||||||
|
await this._runAutoCompaction("threshold", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -754,11 +678,22 @@ export class AgentSession {
|
||||||
};
|
};
|
||||||
this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
||||||
|
|
||||||
// Auto-retry if needed
|
// Auto-retry if needed - use continue() since user message is already in context
|
||||||
if (willRetry && this._lastUserMessageText) {
|
if (willRetry) {
|
||||||
// Small delay to let UI update
|
// Remove trailing error message from agent state (it's kept in session file for history)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
// This is needed because continue() requires last message to be user or toolResult
|
||||||
await this.prompt(this._lastUserMessageText);
|
const messages = this.agent.state.messages;
|
||||||
|
const lastMsg = messages[messages.length - 1];
|
||||||
|
if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
|
||||||
|
this.agent.replaceMessages(messages.slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use setTimeout to break out of the event handler chain
|
||||||
|
setTimeout(() => {
|
||||||
|
this.agent.continue().catch(() => {
|
||||||
|
// Retry failed - silently ignore, user can manually retry
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Compaction failed - emit end event without retry
|
// Compaction failed - emit end event without retry
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,12 @@ export function calculateContextTokens(usage: Usage): number {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get usage from an assistant message if available.
|
* Get usage from an assistant message if available.
|
||||||
|
* Skips aborted and error messages as they don't have valid usage data.
|
||||||
*/
|
*/
|
||||||
function getAssistantUsage(msg: AppMessage): Usage | null {
|
function getAssistantUsage(msg: AppMessage): Usage | null {
|
||||||
if (msg.role === "assistant" && "usage" in msg) {
|
if (msg.role === "assistant" && "usage" in msg) {
|
||||||
const assistantMsg = msg as AssistantMessage;
|
const assistantMsg = msg as AssistantMessage;
|
||||||
if (assistantMsg.stopReason !== "aborted" && assistantMsg.usage) {
|
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
||||||
return assistantMsg.usage;
|
return assistantMsg.usage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,38 +82,61 @@ export function shouldCompact(contextTokens: number, contextWindow: number, sett
|
||||||
/**
|
/**
|
||||||
* Estimate token count for a message using chars/4 heuristic.
|
* Estimate token count for a message using chars/4 heuristic.
|
||||||
* This is conservative (overestimates tokens).
|
* This is conservative (overestimates tokens).
|
||||||
* Accepts any message type (AppMessage, ToolResultMessage, etc.)
|
|
||||||
*/
|
*/
|
||||||
export function estimateTokens(message: {
|
export function estimateTokens(message: AppMessage): number {
|
||||||
role: string;
|
|
||||||
content?: unknown;
|
|
||||||
command?: string;
|
|
||||||
output?: string;
|
|
||||||
}): number {
|
|
||||||
let chars = 0;
|
let chars = 0;
|
||||||
|
|
||||||
// Handle custom message types that don't have standard content
|
// Handle bashExecution messages
|
||||||
if (message.role === "bashExecution") {
|
if (message.role === "bashExecution") {
|
||||||
chars = (message.command?.length || 0) + (message.output?.length || 0);
|
const bash = message as unknown as { command: string; output: string };
|
||||||
|
chars = bash.command.length + bash.output.length;
|
||||||
return Math.ceil(chars / 4);
|
return Math.ceil(chars / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard messages with content
|
// Handle user messages
|
||||||
const content = message.content;
|
if (message.role === "user") {
|
||||||
|
const content = (message as { content: string | Array<{ type: string; text?: string }> }).content;
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
chars = content.length;
|
chars = content.length;
|
||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
for (const block of content) {
|
for (const block of content) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text" && block.text) {
|
||||||
chars += block.text.length;
|
chars += block.text.length;
|
||||||
} else if (block.type === "thinking") {
|
|
||||||
chars += block.thinking.length;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Math.ceil(chars / 4);
|
return Math.ceil(chars / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle assistant messages
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
const assistant = message as AssistantMessage;
|
||||||
|
for (const block of assistant.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
chars += block.text.length;
|
||||||
|
} else if (block.type === "thinking") {
|
||||||
|
chars += block.thinking.length;
|
||||||
|
} else if (block.type === "toolCall") {
|
||||||
|
chars += block.name.length + JSON.stringify(block.arguments).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tool results
|
||||||
|
if (message.role === "toolResult") {
|
||||||
|
const toolResult = message as { content: Array<{ type: string; text?: string }> };
|
||||||
|
for (const block of toolResult.content) {
|
||||||
|
if (block.type === "text" && block.text) {
|
||||||
|
chars += block.text.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.ceil(chars / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find valid cut points: indices of user, assistant, or bashExecution messages.
|
* Find valid cut points: indices of user, assistant, or bashExecution messages.
|
||||||
* Never cut at tool results (they must follow their tool call).
|
* Never cut at tool results (they must follow their tool call).
|
||||||
|
|
@ -166,6 +190,9 @@ export interface CutPointResult {
|
||||||
/**
|
/**
|
||||||
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
||||||
*
|
*
|
||||||
|
* Algorithm: Walk backwards from newest, accumulating estimated message sizes.
|
||||||
|
* Stop when we've accumulated >= keepRecentTokens. Cut at that point.
|
||||||
|
*
|
||||||
* Can cut at user OR assistant messages (never tool results). When cutting at an
|
* Can cut at user OR assistant messages (never tool results). When cutting at an
|
||||||
* assistant message with tool calls, its tool results come after and will be kept.
|
* assistant message with tool calls, its tool results come after and will be kept.
|
||||||
*
|
*
|
||||||
|
|
@ -188,46 +215,23 @@ export function findCutPoint(
|
||||||
return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
|
return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect assistant usages walking backwards from endIndex
|
// Walk backwards from newest, accumulating estimated message sizes
|
||||||
const assistantUsages: Array<{ index: number; tokens: number }> = [];
|
let accumulatedTokens = 0;
|
||||||
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
||||||
const entry = entries[i];
|
|
||||||
if (entry.type === "message") {
|
|
||||||
const usage = getAssistantUsage(entry.message);
|
|
||||||
if (usage) {
|
|
||||||
assistantUsages.push({
|
|
||||||
index: i,
|
|
||||||
tokens: calculateContextTokens(usage),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assistantUsages.length === 0) {
|
|
||||||
// No usage info, keep from last cut point
|
|
||||||
const lastCutPoint = cutPoints[cutPoints.length - 1];
|
|
||||||
const entry = entries[lastCutPoint];
|
|
||||||
const isUser = entry.type === "message" && entry.message.role === "user";
|
|
||||||
return {
|
|
||||||
firstKeptEntryIndex: lastCutPoint,
|
|
||||||
turnStartIndex: isUser ? -1 : findTurnStartIndex(entries, lastCutPoint, startIndex),
|
|
||||||
isSplitTurn: !isUser,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk through and find where cumulative token difference exceeds keepRecentTokens
|
|
||||||
const newestTokens = assistantUsages[0].tokens;
|
|
||||||
let cutIndex = startIndex; // Default: keep everything in range
|
let cutIndex = startIndex; // Default: keep everything in range
|
||||||
|
|
||||||
for (let i = 1; i < assistantUsages.length; i++) {
|
for (let i = endIndex - 1; i >= startIndex; i--) {
|
||||||
const tokenDiff = newestTokens - assistantUsages[i].tokens;
|
const entry = entries[i];
|
||||||
if (tokenDiff >= keepRecentTokens) {
|
if (entry.type !== "message") continue;
|
||||||
// Find the valid cut point at or after the assistant we want to keep
|
|
||||||
const lastKeptAssistantIndex = assistantUsages[i - 1].index;
|
|
||||||
|
|
||||||
// Find closest valid cut point at or before lastKeptAssistantIndex
|
// Estimate this message's size
|
||||||
for (let c = cutPoints.length - 1; c >= 0; c--) {
|
const messageTokens = estimateTokens(entry.message);
|
||||||
if (cutPoints[c] <= lastKeptAssistantIndex) {
|
accumulatedTokens += messageTokens;
|
||||||
|
|
||||||
|
// Check if we've exceeded the budget
|
||||||
|
if (accumulatedTokens >= keepRecentTokens) {
|
||||||
|
// Find the closest valid cut point at or after this entry
|
||||||
|
for (let c = 0; c < cutPoints.length; c++) {
|
||||||
|
if (cutPoints[c] >= i) {
|
||||||
cutIndex = cutPoints[c];
|
cutIndex = cutPoints[c];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -404,9 +408,8 @@ export async function compact(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate summaries (can be parallel if both needed)
|
// Generate summaries (can be parallel if both needed) and merge into one
|
||||||
let summary: string;
|
let summary: string;
|
||||||
let turnPrefixSummary: string | undefined;
|
|
||||||
|
|
||||||
if (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {
|
if (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {
|
||||||
// Generate both summaries in parallel
|
// Generate both summaries in parallel
|
||||||
|
|
@ -416,8 +419,8 @@ export async function compact(
|
||||||
: Promise.resolve("No prior history."),
|
: Promise.resolve("No prior history."),
|
||||||
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),
|
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),
|
||||||
]);
|
]);
|
||||||
summary = historyResult;
|
// Merge into single summary
|
||||||
turnPrefixSummary = turnPrefixResult;
|
summary = historyResult + "\n\n---\n\n**Turn Context (split turn):**\n\n" + turnPrefixResult;
|
||||||
} else {
|
} else {
|
||||||
// Just generate history summary
|
// Just generate history summary
|
||||||
summary = await generateSummary(
|
summary = await generateSummary(
|
||||||
|
|
@ -434,7 +437,6 @@ export async function compact(
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
summary,
|
summary,
|
||||||
turnPrefixSummary,
|
|
||||||
firstKeptEntryIndex: cutResult.firstKeptEntryIndex,
|
firstKeptEntryIndex: cutResult.firstKeptEntryIndex,
|
||||||
tokensBefore,
|
tokensBefore,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@ export interface CompactionEntry {
|
||||||
type: "compaction";
|
type: "compaction";
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
/** Summary of turn prefix when a turn was split (user message to first kept message) */
|
|
||||||
turnPrefixSummary?: string;
|
|
||||||
firstKeptEntryIndex: number; // Index into session entries where we start keeping
|
firstKeptEntryIndex: number; // Index into session entries where we start keeping
|
||||||
tokensBefore: number;
|
tokensBefore: number;
|
||||||
}
|
}
|
||||||
|
|
@ -180,18 +178,9 @@ export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build final messages: summaries + kept messages
|
// Build final messages: summary + kept messages
|
||||||
const messages: AppMessage[] = [];
|
const messages: AppMessage[] = [];
|
||||||
|
|
||||||
// Add history summary
|
|
||||||
messages.push(createSummaryMessage(compactionEvent.summary));
|
messages.push(createSummaryMessage(compactionEvent.summary));
|
||||||
|
|
||||||
// Add turn prefix summary if present (when a turn was split)
|
|
||||||
if (compactionEvent.turnPrefixSummary) {
|
|
||||||
messages.push(createSummaryMessage(compactionEvent.turnPrefixSummary));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add kept messages
|
|
||||||
messages.push(...keptMessages);
|
messages.push(...keptMessages);
|
||||||
|
|
||||||
return { messages, thinkingLevel, model };
|
return { messages, thinkingLevel, model };
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,11 @@ export class InteractiveMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block input during compaction (will retry automatically)
|
||||||
|
if (this.session.isCompacting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Queue message if agent is streaming
|
// Queue message if agent is streaming
|
||||||
if (this.session.isStreaming) {
|
if (this.session.isStreaming) {
|
||||||
await this.session.queueMessage(text);
|
await this.session.queueMessage(text);
|
||||||
|
|
@ -604,10 +609,6 @@ export class InteractiveMode {
|
||||||
compactionComponent.setExpanded(this.toolOutputExpanded);
|
compactionComponent.setExpanded(this.toolOutputExpanded);
|
||||||
this.chatContainer.addChild(compactionComponent);
|
this.chatContainer.addChild(compactionComponent);
|
||||||
this.footer.updateState(this.session.state);
|
this.footer.updateState(this.session.state);
|
||||||
|
|
||||||
if (event.willRetry) {
|
|
||||||
this.showStatus("Compacted context, retrying...");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
break;
|
break;
|
||||||
|
|
@ -743,6 +744,14 @@ export class InteractiveMode {
|
||||||
|
|
||||||
renderInitialMessages(state: AgentState): void {
|
renderInitialMessages(state: AgentState): void {
|
||||||
this.renderMessages(state.messages, { updateFooter: true, populateHistory: true });
|
this.renderMessages(state.messages, { updateFooter: true, populateHistory: true });
|
||||||
|
|
||||||
|
// Show compaction info if session was compacted
|
||||||
|
const entries = this.sessionManager.loadEntries();
|
||||||
|
const compactionCount = entries.filter((e) => e.type === "compaction").length;
|
||||||
|
if (compactionCount > 0) {
|
||||||
|
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
||||||
|
this.showStatus(`Session compacted ${times}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserInput(): Promise<string> {
|
async getUserInput(): Promise<string> {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
model: session.model,
|
model: session.model,
|
||||||
thinkingLevel: session.thinkingLevel,
|
thinkingLevel: session.thinkingLevel,
|
||||||
isStreaming: session.isStreaming,
|
isStreaming: session.isStreaming,
|
||||||
|
isCompacting: session.isCompacting,
|
||||||
queueMode: session.queueMode,
|
queueMode: session.queueMode,
|
||||||
sessionFile: session.sessionFile,
|
sessionFile: session.sessionFile,
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ export interface RpcSessionState {
|
||||||
model: Model<any> | null;
|
model: Model<any> | null;
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
|
isCompacting: boolean;
|
||||||
queueMode: "all" | "one-at-a-time";
|
queueMode: "all" | "one-at-a-time";
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
|
||||||
233
packages/coding-agent/test/agent-session-compaction.test.ts
Normal file
233
packages/coding-agent/test/agent-session-compaction.test.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
/**
|
||||||
|
* E2E tests for AgentSession compaction behavior.
|
||||||
|
*
|
||||||
|
* These tests use real LLM calls (no mocking) to verify:
|
||||||
|
* - Manual compaction works correctly
|
||||||
|
* - Session persistence during compaction
|
||||||
|
* - Compaction entry is saved to session file
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { Agent, ProviderTransport } from "@mariozechner/pi-agent-core";
|
||||||
|
import { getModel } from "@mariozechner/pi-ai";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.js";
|
||||||
|
import { SessionManager } from "../src/core/session-manager.js";
|
||||||
|
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||||
|
import { codingTools } from "../src/core/tools/index.js";
|
||||||
|
|
||||||
|
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
|
||||||
|
|
||||||
|
describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||||
|
let session: AgentSession;
|
||||||
|
let tempDir: string;
|
||||||
|
let sessionManager: SessionManager;
|
||||||
|
let events: AgentSessionEvent[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temp directory for session files
|
||||||
|
tempDir = join(tmpdir(), `pi-compaction-test-${Date.now()}`);
|
||||||
|
mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
// Track events
|
||||||
|
events = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (session) {
|
||||||
|
session.dispose();
|
||||||
|
}
|
||||||
|
if (tempDir && existsSync(tempDir)) {
|
||||||
|
rmSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function createSession() {
|
||||||
|
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
|
|
||||||
|
const transport = new ProviderTransport({
|
||||||
|
getApiKey: () => API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent = new Agent({
|
||||||
|
transport,
|
||||||
|
initialState: {
|
||||||
|
model,
|
||||||
|
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||||
|
tools: codingTools,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sessionManager = new SessionManager(false);
|
||||||
|
const settingsManager = new SettingsManager(tempDir);
|
||||||
|
|
||||||
|
session = new AgentSession({
|
||||||
|
agent,
|
||||||
|
sessionManager,
|
||||||
|
settingsManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to track events
|
||||||
|
session.subscribe((event) => {
|
||||||
|
events.push(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should trigger manual compaction via compact()", async () => {
|
||||||
|
createSession();
|
||||||
|
|
||||||
|
// Send a few prompts to build up history
|
||||||
|
await session.prompt("What is 2+2? Reply with just the number.");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
await session.prompt("What is 3+3? Reply with just the number.");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
// Manually compact
|
||||||
|
const result = await session.compact();
|
||||||
|
|
||||||
|
expect(result.summary).toBeDefined();
|
||||||
|
expect(result.summary.length).toBeGreaterThan(0);
|
||||||
|
expect(result.tokensBefore).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify messages were compacted (should have summary + recent)
|
||||||
|
const messages = session.messages;
|
||||||
|
expect(messages.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// First message should be the summary (a user message with summary content)
|
||||||
|
const firstMsg = messages[0];
|
||||||
|
expect(firstMsg.role).toBe("user");
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
it("should maintain valid session state after compaction", async () => {
|
||||||
|
createSession();
|
||||||
|
|
||||||
|
// Build up history
|
||||||
|
await session.prompt("What is the capital of France? One word answer.");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
await session.prompt("What is the capital of Germany? One word answer.");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
// Compact
|
||||||
|
await session.compact();
|
||||||
|
|
||||||
|
// Session should still be usable
|
||||||
|
await session.prompt("What is the capital of Italy? One word answer.");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
// Should have messages after compaction
|
||||||
|
expect(session.messages.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// The agent should have responded
|
||||||
|
const assistantMessages = session.messages.filter((m) => m.role === "assistant");
|
||||||
|
expect(assistantMessages.length).toBeGreaterThan(0);
|
||||||
|
}, 180000);
|
||||||
|
|
||||||
|
it("should persist compaction to session file", async () => {
|
||||||
|
createSession();
|
||||||
|
|
||||||
|
await session.prompt("Say hello");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
await session.prompt("Say goodbye");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
// Compact
|
||||||
|
await session.compact();
|
||||||
|
|
||||||
|
// Load entries from session manager
|
||||||
|
const entries = sessionManager.loadEntries();
|
||||||
|
|
||||||
|
// Should have a compaction entry
|
||||||
|
const compactionEntries = entries.filter((e) => e.type === "compaction");
|
||||||
|
expect(compactionEntries.length).toBe(1);
|
||||||
|
|
||||||
|
const compaction = compactionEntries[0];
|
||||||
|
expect(compaction.type).toBe("compaction");
|
||||||
|
if (compaction.type === "compaction") {
|
||||||
|
expect(compaction.summary.length).toBeGreaterThan(0);
|
||||||
|
// firstKeptEntryIndex can be 0 if all messages fit within keepRecentTokens
|
||||||
|
// (which is the case for small conversations)
|
||||||
|
expect(compaction.firstKeptEntryIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(compaction.tokensBefore).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
it("should work with --no-session mode (in-memory only)", async () => {
|
||||||
|
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
|
|
||||||
|
const transport = new ProviderTransport({
|
||||||
|
getApiKey: () => API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent = new Agent({
|
||||||
|
transport,
|
||||||
|
initialState: {
|
||||||
|
model,
|
||||||
|
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||||
|
tools: codingTools,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create session manager and disable file persistence
|
||||||
|
const noSessionManager = new SessionManager(false);
|
||||||
|
noSessionManager.disable();
|
||||||
|
|
||||||
|
const settingsManager = new SettingsManager(tempDir);
|
||||||
|
|
||||||
|
const noSessionSession = new AgentSession({
|
||||||
|
agent,
|
||||||
|
sessionManager: noSessionManager,
|
||||||
|
settingsManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send prompts
|
||||||
|
await noSessionSession.prompt("What is 2+2? Reply with just the number.");
|
||||||
|
await noSessionSession.agent.waitForIdle();
|
||||||
|
|
||||||
|
await noSessionSession.prompt("What is 3+3? Reply with just the number.");
|
||||||
|
await noSessionSession.agent.waitForIdle();
|
||||||
|
|
||||||
|
// Compact should work even without file persistence
|
||||||
|
const result = await noSessionSession.compact();
|
||||||
|
|
||||||
|
expect(result.summary).toBeDefined();
|
||||||
|
expect(result.summary.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// In-memory entries should have the compaction
|
||||||
|
const entries = noSessionManager.loadEntries();
|
||||||
|
const compactionEntries = entries.filter((e) => e.type === "compaction");
|
||||||
|
expect(compactionEntries.length).toBe(1);
|
||||||
|
} finally {
|
||||||
|
noSessionSession.dispose();
|
||||||
|
}
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
it("should emit correct events during auto-compaction", async () => {
|
||||||
|
createSession();
|
||||||
|
|
||||||
|
// Build some history
|
||||||
|
await session.prompt("Say hello");
|
||||||
|
await session.agent.waitForIdle();
|
||||||
|
|
||||||
|
// Manually trigger compaction and check events
|
||||||
|
await session.compact();
|
||||||
|
|
||||||
|
// Check that no auto_compaction events were emitted for manual compaction
|
||||||
|
const autoCompactionEvents = events.filter(
|
||||||
|
(e) => e.type === "auto_compaction_start" || e.type === "auto_compaction_end",
|
||||||
|
);
|
||||||
|
// Manual compaction doesn't emit auto_compaction events
|
||||||
|
expect(autoCompactionEvents.length).toBe(0);
|
||||||
|
|
||||||
|
// Regular events should have been emitted
|
||||||
|
const messageEndEvents = events.filter((e) => e.type === "message_end");
|
||||||
|
expect(messageEndEvents.length).toBeGreaterThan(0);
|
||||||
|
}, 120000);
|
||||||
|
});
|
||||||
|
|
@ -11,7 +11,7 @@ import type {
|
||||||
ToolCall,
|
ToolCall,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
import { agentLoop } from "@mariozechner/pi-ai";
|
import { agentLoop, agentLoopContinue } from "@mariozechner/pi-ai";
|
||||||
import { AssistantMessageEventStream } from "@mariozechner/pi-ai/dist/utils/event-stream.js";
|
import { AssistantMessageEventStream } from "@mariozechner/pi-ai/dist/utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "@mariozechner/pi-ai/dist/utils/json-parse.js";
|
import { parseStreamingJson } from "@mariozechner/pi-ai/dist/utils/json-parse.js";
|
||||||
import { clearAuthToken, getAuthToken } from "../../utils/auth-token.js";
|
import { clearAuthToken, getAuthToken } from "../../utils/auth-token.js";
|
||||||
|
|
@ -315,51 +315,57 @@ function streamSimpleProxy(
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy transport executes the turn using a remote proxy server
|
|
||||||
/**
|
/**
|
||||||
* Transport that uses an app server with user authentication tokens.
|
* Transport that uses an app server with user authentication tokens.
|
||||||
* The server manages user accounts and proxies requests to LLM providers.
|
* The server manages user accounts and proxies requests to LLM providers.
|
||||||
*/
|
*/
|
||||||
export class AppTransport implements AgentTransport {
|
export class AppTransport implements AgentTransport {
|
||||||
// Hardcoded proxy URL for now - will be made configurable later
|
|
||||||
private readonly proxyUrl = "https://genai.mariozechner.at";
|
private readonly proxyUrl = "https://genai.mariozechner.at";
|
||||||
|
|
||||||
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
private async getStreamFn() {
|
||||||
const authToken = await getAuthToken();
|
const authToken = await getAuthToken();
|
||||||
if (!authToken) {
|
if (!authToken) {
|
||||||
throw new Error(i18n("Auth token is required for proxy transport"));
|
throw new Error(i18n("Auth token is required for proxy transport"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use proxy - no local API key needed
|
return <TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions) => {
|
||||||
const streamFn = <TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions) => {
|
return streamSimpleProxy(model, context, { ...options, authToken }, this.proxyUrl);
|
||||||
return streamSimpleProxy(
|
|
||||||
model,
|
|
||||||
context,
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
authToken,
|
|
||||||
},
|
|
||||||
this.proxyUrl,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Messages are already LLM-compatible (filtered by Agent)
|
private buildContext(messages: Message[], cfg: AgentRunConfig): AgentContext {
|
||||||
const context: AgentContext = {
|
return {
|
||||||
systemPrompt: cfg.systemPrompt,
|
systemPrompt: cfg.systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
tools: cfg.tools,
|
tools: cfg.tools,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const pc: AgentLoopConfig = {
|
private buildLoopConfig(cfg: AgentRunConfig): AgentLoopConfig {
|
||||||
|
return {
|
||||||
model: cfg.model,
|
model: cfg.model,
|
||||||
reasoning: cfg.reasoning,
|
reasoning: cfg.reasoning,
|
||||||
getQueuedMessages: cfg.getQueuedMessages,
|
getQueuedMessages: cfg.getQueuedMessages,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const streamFn = await this.getStreamFn();
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(cfg);
|
||||||
|
|
||||||
// Yield events from the upstream agentLoop iterator
|
|
||||||
// Pass streamFn as the 5th parameter to use proxy
|
|
||||||
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal, streamFn as any)) {
|
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal, streamFn as any)) {
|
||||||
yield ev;
|
yield ev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *continue(messages: Message[], cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const streamFn = await this.getStreamFn();
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(cfg);
|
||||||
|
|
||||||
|
for await (const ev of agentLoopContinue(context, pc, signal, streamFn as any)) {
|
||||||
|
yield ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
type AgentContext,
|
type AgentContext,
|
||||||
type AgentLoopConfig,
|
type AgentLoopConfig,
|
||||||
agentLoop,
|
agentLoop,
|
||||||
|
agentLoopContinue,
|
||||||
type Message,
|
type Message,
|
||||||
type UserMessage,
|
type UserMessage,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
|
|
@ -14,37 +15,53 @@ import type { AgentRunConfig, AgentTransport } from "./types.js";
|
||||||
* Uses CORS proxy only for providers that require it (Anthropic OAuth, Z-AI).
|
* Uses CORS proxy only for providers that require it (Anthropic OAuth, Z-AI).
|
||||||
*/
|
*/
|
||||||
export class ProviderTransport implements AgentTransport {
|
export class ProviderTransport implements AgentTransport {
|
||||||
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
private async getModelAndKey(cfg: AgentRunConfig) {
|
||||||
// Get API key from storage
|
|
||||||
const apiKey = await getAppStorage().providerKeys.get(cfg.model.provider);
|
const apiKey = await getAppStorage().providerKeys.get(cfg.model.provider);
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error("no-api-key");
|
throw new Error("no-api-key");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get proxy URL from settings (if available)
|
|
||||||
const proxyEnabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
|
const proxyEnabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
|
||||||
const proxyUrl = await getAppStorage().settings.get<string>("proxy.url");
|
const proxyUrl = await getAppStorage().settings.get<string>("proxy.url");
|
||||||
|
|
||||||
// Apply proxy only if this provider/key combination requires it
|
|
||||||
const model = applyProxyIfNeeded(cfg.model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined);
|
const model = applyProxyIfNeeded(cfg.model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined);
|
||||||
|
|
||||||
// Messages are already LLM-compatible (filtered by Agent)
|
return { model, apiKey };
|
||||||
const context: AgentContext = {
|
}
|
||||||
|
|
||||||
|
private buildContext(messages: Message[], cfg: AgentRunConfig): AgentContext {
|
||||||
|
return {
|
||||||
systemPrompt: cfg.systemPrompt,
|
systemPrompt: cfg.systemPrompt,
|
||||||
messages,
|
messages,
|
||||||
tools: cfg.tools,
|
tools: cfg.tools,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const pc: AgentLoopConfig = {
|
private buildLoopConfig(model: typeof cfg.model, apiKey: string, cfg: AgentRunConfig): AgentLoopConfig {
|
||||||
|
return {
|
||||||
model,
|
model,
|
||||||
reasoning: cfg.reasoning,
|
reasoning: cfg.reasoning,
|
||||||
apiKey,
|
apiKey,
|
||||||
getQueuedMessages: cfg.getQueuedMessages,
|
getQueuedMessages: cfg.getQueuedMessages,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const { model, apiKey } = await this.getModelAndKey(cfg);
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(model, apiKey, cfg);
|
||||||
|
|
||||||
// Yield events from agentLoop
|
|
||||||
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal)) {
|
for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal)) {
|
||||||
yield ev;
|
yield ev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *continue(messages: Message[], cfg: AgentRunConfig, signal?: AbortSignal) {
|
||||||
|
const { model, apiKey } = await this.getModelAndKey(cfg);
|
||||||
|
const context = this.buildContext(messages, cfg);
|
||||||
|
const pc = this.buildLoopConfig(model, apiKey, cfg);
|
||||||
|
|
||||||
|
for await (const ev of agentLoopContinue(context, pc, signal)) {
|
||||||
|
yield ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,14 @@ export interface AgentRunConfig {
|
||||||
// We re-export the Message type above; consumers should use the upstream AgentEvent type.
|
// We re-export the Message type above; consumers should use the upstream AgentEvent type.
|
||||||
|
|
||||||
export interface AgentTransport {
|
export interface AgentTransport {
|
||||||
|
/** Run with a new user message */
|
||||||
run(
|
run(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
userMessage: Message,
|
userMessage: Message,
|
||||||
config: AgentRunConfig,
|
config: AgentRunConfig,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): AsyncIterable<AgentEvent>; // passthrough of AgentEvent from upstream
|
): AsyncIterable<AgentEvent>;
|
||||||
|
|
||||||
|
/** Continue from current context (no new user message) */
|
||||||
|
continue(messages: Message[], config: AgentRunConfig, signal?: AbortSignal): AsyncIterable<AgentEvent>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue