onbuzz 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +267 -0
- package/README.md +425 -0
- package/bin/cli.js +556 -0
- package/bin/loxia-terminal-v2.js +162 -0
- package/bin/loxia-terminal.js +90 -0
- package/bin/start-with-terminal.js +200 -0
- package/node_modules/@isaacs/balanced-match/LICENSE.md +23 -0
- package/node_modules/@isaacs/balanced-match/README.md +60 -0
- package/node_modules/@isaacs/balanced-match/dist/commonjs/index.d.ts +9 -0
- package/node_modules/@isaacs/balanced-match/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/@isaacs/balanced-match/dist/commonjs/index.js +59 -0
- package/node_modules/@isaacs/balanced-match/dist/commonjs/index.js.map +1 -0
- package/node_modules/@isaacs/balanced-match/dist/commonjs/package.json +3 -0
- package/node_modules/@isaacs/balanced-match/dist/esm/index.d.ts +9 -0
- package/node_modules/@isaacs/balanced-match/dist/esm/index.d.ts.map +1 -0
- package/node_modules/@isaacs/balanced-match/dist/esm/index.js +54 -0
- package/node_modules/@isaacs/balanced-match/dist/esm/index.js.map +1 -0
- package/node_modules/@isaacs/balanced-match/dist/esm/package.json +3 -0
- package/node_modules/@isaacs/balanced-match/package.json +79 -0
- package/node_modules/@isaacs/brace-expansion/LICENSE +23 -0
- package/node_modules/@isaacs/brace-expansion/README.md +97 -0
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.d.ts +6 -0
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js +199 -0
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +1 -0
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/package.json +3 -0
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.d.ts +6 -0
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.d.ts.map +1 -0
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.js +195 -0
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +1 -0
- package/node_modules/@isaacs/brace-expansion/dist/esm/package.json +3 -0
- package/node_modules/@isaacs/brace-expansion/package.json +60 -0
- package/node_modules/glob/LICENSE.md +63 -0
- package/node_modules/glob/README.md +1177 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts +388 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/glob.js +247 -0
- package/node_modules/glob/dist/commonjs/glob.js.map +1 -0
- package/node_modules/glob/dist/commonjs/has-magic.d.ts +14 -0
- package/node_modules/glob/dist/commonjs/has-magic.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/has-magic.js +27 -0
- package/node_modules/glob/dist/commonjs/has-magic.js.map +1 -0
- package/node_modules/glob/dist/commonjs/ignore.d.ts +24 -0
- package/node_modules/glob/dist/commonjs/ignore.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/ignore.js +119 -0
- package/node_modules/glob/dist/commonjs/ignore.js.map +1 -0
- package/node_modules/glob/dist/commonjs/index.d.ts +97 -0
- package/node_modules/glob/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/index.js +68 -0
- package/node_modules/glob/dist/commonjs/index.js.map +1 -0
- package/node_modules/glob/dist/commonjs/index.min.js +4 -0
- package/node_modules/glob/dist/commonjs/index.min.js.map +7 -0
- package/node_modules/glob/dist/commonjs/package.json +3 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts +76 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/pattern.js +219 -0
- package/node_modules/glob/dist/commonjs/pattern.js.map +1 -0
- package/node_modules/glob/dist/commonjs/processor.d.ts +59 -0
- package/node_modules/glob/dist/commonjs/processor.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/processor.js +301 -0
- package/node_modules/glob/dist/commonjs/processor.js.map +1 -0
- package/node_modules/glob/dist/commonjs/walker.d.ts +97 -0
- package/node_modules/glob/dist/commonjs/walker.d.ts.map +1 -0
- package/node_modules/glob/dist/commonjs/walker.js +387 -0
- package/node_modules/glob/dist/commonjs/walker.js.map +1 -0
- package/node_modules/glob/dist/esm/glob.d.ts +388 -0
- package/node_modules/glob/dist/esm/glob.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/glob.js +243 -0
- package/node_modules/glob/dist/esm/glob.js.map +1 -0
- package/node_modules/glob/dist/esm/has-magic.d.ts +14 -0
- package/node_modules/glob/dist/esm/has-magic.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/has-magic.js +23 -0
- package/node_modules/glob/dist/esm/has-magic.js.map +1 -0
- package/node_modules/glob/dist/esm/ignore.d.ts +24 -0
- package/node_modules/glob/dist/esm/ignore.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/ignore.js +115 -0
- package/node_modules/glob/dist/esm/ignore.js.map +1 -0
- package/node_modules/glob/dist/esm/index.d.ts +97 -0
- package/node_modules/glob/dist/esm/index.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/index.js +55 -0
- package/node_modules/glob/dist/esm/index.js.map +1 -0
- package/node_modules/glob/dist/esm/index.min.js +4 -0
- package/node_modules/glob/dist/esm/index.min.js.map +7 -0
- package/node_modules/glob/dist/esm/package.json +3 -0
- package/node_modules/glob/dist/esm/pattern.d.ts +76 -0
- package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/pattern.js +215 -0
- package/node_modules/glob/dist/esm/pattern.js.map +1 -0
- package/node_modules/glob/dist/esm/processor.d.ts +59 -0
- package/node_modules/glob/dist/esm/processor.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/processor.js +294 -0
- package/node_modules/glob/dist/esm/processor.js.map +1 -0
- package/node_modules/glob/dist/esm/walker.d.ts +97 -0
- package/node_modules/glob/dist/esm/walker.d.ts.map +1 -0
- package/node_modules/glob/dist/esm/walker.js +381 -0
- package/node_modules/glob/dist/esm/walker.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/LICENSE.md +55 -0
- package/node_modules/glob/node_modules/minimatch/README.md +453 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +2 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js +14 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +20 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +591 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts +8 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +152 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +15 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +30 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +94 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +1029 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/package.json +3 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +22 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +38 -0
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +2 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js +10 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +20 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +587 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts +8 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +148 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +15 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +26 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +94 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +1016 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/package.json +3 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +22 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +34 -0
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -0
- package/node_modules/glob/node_modules/minimatch/package.json +67 -0
- package/node_modules/glob/package.json +101 -0
- package/node_modules/minipass/LICENSE +15 -0
- package/node_modules/minipass/README.md +825 -0
- package/node_modules/minipass/dist/commonjs/index.d.ts +549 -0
- package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/minipass/dist/commonjs/index.js +1028 -0
- package/node_modules/minipass/dist/commonjs/index.js.map +1 -0
- package/node_modules/minipass/dist/commonjs/package.json +3 -0
- package/node_modules/minipass/dist/esm/index.d.ts +549 -0
- package/node_modules/minipass/dist/esm/index.d.ts.map +1 -0
- package/node_modules/minipass/dist/esm/index.js +1018 -0
- package/node_modules/minipass/dist/esm/index.js.map +1 -0
- package/node_modules/minipass/dist/esm/package.json +3 -0
- package/node_modules/minipass/package.json +82 -0
- package/node_modules/package-json-from-dist/LICENSE.md +63 -0
- package/node_modules/package-json-from-dist/README.md +110 -0
- package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts +89 -0
- package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/package-json-from-dist/dist/commonjs/index.js +134 -0
- package/node_modules/package-json-from-dist/dist/commonjs/index.js.map +1 -0
- package/node_modules/package-json-from-dist/dist/commonjs/package.json +3 -0
- package/node_modules/package-json-from-dist/dist/esm/index.d.ts +89 -0
- package/node_modules/package-json-from-dist/dist/esm/index.d.ts.map +1 -0
- package/node_modules/package-json-from-dist/dist/esm/index.js +129 -0
- package/node_modules/package-json-from-dist/dist/esm/index.js.map +1 -0
- package/node_modules/package-json-from-dist/dist/esm/package.json +3 -0
- package/node_modules/package-json-from-dist/package.json +68 -0
- package/node_modules/path-scurry/LICENSE.md +55 -0
- package/node_modules/path-scurry/README.md +636 -0
- package/node_modules/path-scurry/dist/commonjs/index.d.ts +1115 -0
- package/node_modules/path-scurry/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/path-scurry/dist/commonjs/index.js +2018 -0
- package/node_modules/path-scurry/dist/commonjs/index.js.map +1 -0
- package/node_modules/path-scurry/dist/commonjs/package.json +3 -0
- package/node_modules/path-scurry/dist/esm/index.d.ts +1115 -0
- package/node_modules/path-scurry/dist/esm/index.d.ts.map +1 -0
- package/node_modules/path-scurry/dist/esm/index.js +1983 -0
- package/node_modules/path-scurry/dist/esm/index.js.map +1 -0
- package/node_modules/path-scurry/dist/esm/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/LICENSE.md +55 -0
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +383 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +1323 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +1589 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +1323 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +1585 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +101 -0
- package/node_modules/path-scurry/package.json +88 -0
- package/node_modules/rimraf/LICENSE.md +55 -0
- package/node_modules/rimraf/README.md +226 -0
- package/node_modules/rimraf/dist/commonjs/default-tmp.d.ts +3 -0
- package/node_modules/rimraf/dist/commonjs/default-tmp.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/default-tmp.js +58 -0
- package/node_modules/rimraf/dist/commonjs/default-tmp.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/error.d.ts +6 -0
- package/node_modules/rimraf/dist/commonjs/error.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/error.js +10 -0
- package/node_modules/rimraf/dist/commonjs/error.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/fix-eperm.d.ts +3 -0
- package/node_modules/rimraf/dist/commonjs/fix-eperm.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/fix-eperm.js +38 -0
- package/node_modules/rimraf/dist/commonjs/fix-eperm.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/fs.d.ts +15 -0
- package/node_modules/rimraf/dist/commonjs/fs.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/fs.js +33 -0
- package/node_modules/rimraf/dist/commonjs/fs.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/ignore-enoent.d.ts +3 -0
- package/node_modules/rimraf/dist/commonjs/ignore-enoent.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/ignore-enoent.js +24 -0
- package/node_modules/rimraf/dist/commonjs/ignore-enoent.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/index.d.ts +50 -0
- package/node_modules/rimraf/dist/commonjs/index.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/index.js +78 -0
- package/node_modules/rimraf/dist/commonjs/index.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/opt-arg.d.ts +34 -0
- package/node_modules/rimraf/dist/commonjs/opt-arg.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/opt-arg.js +53 -0
- package/node_modules/rimraf/dist/commonjs/opt-arg.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/package.json +3 -0
- package/node_modules/rimraf/dist/commonjs/path-arg.d.ts +4 -0
- package/node_modules/rimraf/dist/commonjs/path-arg.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/path-arg.js +48 -0
- package/node_modules/rimraf/dist/commonjs/path-arg.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/readdir-or-error.d.ts +3 -0
- package/node_modules/rimraf/dist/commonjs/readdir-or-error.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/readdir-or-error.js +19 -0
- package/node_modules/rimraf/dist/commonjs/readdir-or-error.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/retry-busy.d.ts +8 -0
- package/node_modules/rimraf/dist/commonjs/retry-busy.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/retry-busy.js +65 -0
- package/node_modules/rimraf/dist/commonjs/retry-busy.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-manual.d.ts +3 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-manual.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-manual.js +8 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-manual.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.d.ts +4 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.js +138 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-move-remove.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-native.d.ts +4 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-native.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-native.js +24 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-native.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-posix.d.ts +4 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-posix.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-posix.js +103 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-posix.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-windows.d.ts +4 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-windows.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-windows.js +159 -0
- package/node_modules/rimraf/dist/commonjs/rimraf-windows.js.map +1 -0
- package/node_modules/rimraf/dist/commonjs/use-native.d.ts +4 -0
- package/node_modules/rimraf/dist/commonjs/use-native.d.ts.map +1 -0
- package/node_modules/rimraf/dist/commonjs/use-native.js +18 -0
- package/node_modules/rimraf/dist/commonjs/use-native.js.map +1 -0
- package/node_modules/rimraf/dist/esm/bin.d.mts +3 -0
- package/node_modules/rimraf/dist/esm/bin.d.mts.map +1 -0
- package/node_modules/rimraf/dist/esm/bin.mjs +250 -0
- package/node_modules/rimraf/dist/esm/bin.mjs.map +1 -0
- package/node_modules/rimraf/dist/esm/default-tmp.d.ts +3 -0
- package/node_modules/rimraf/dist/esm/default-tmp.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/default-tmp.js +55 -0
- package/node_modules/rimraf/dist/esm/default-tmp.js.map +1 -0
- package/node_modules/rimraf/dist/esm/error.d.ts +6 -0
- package/node_modules/rimraf/dist/esm/error.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/error.js +5 -0
- package/node_modules/rimraf/dist/esm/error.js.map +1 -0
- package/node_modules/rimraf/dist/esm/fix-eperm.d.ts +3 -0
- package/node_modules/rimraf/dist/esm/fix-eperm.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/fix-eperm.js +33 -0
- package/node_modules/rimraf/dist/esm/fix-eperm.js.map +1 -0
- package/node_modules/rimraf/dist/esm/fs.d.ts +15 -0
- package/node_modules/rimraf/dist/esm/fs.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/fs.js +18 -0
- package/node_modules/rimraf/dist/esm/fs.js.map +1 -0
- package/node_modules/rimraf/dist/esm/ignore-enoent.d.ts +3 -0
- package/node_modules/rimraf/dist/esm/ignore-enoent.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/ignore-enoent.js +19 -0
- package/node_modules/rimraf/dist/esm/ignore-enoent.js.map +1 -0
- package/node_modules/rimraf/dist/esm/index.d.ts +50 -0
- package/node_modules/rimraf/dist/esm/index.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/index.js +70 -0
- package/node_modules/rimraf/dist/esm/index.js.map +1 -0
- package/node_modules/rimraf/dist/esm/opt-arg.d.ts +34 -0
- package/node_modules/rimraf/dist/esm/opt-arg.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/opt-arg.js +46 -0
- package/node_modules/rimraf/dist/esm/opt-arg.js.map +1 -0
- package/node_modules/rimraf/dist/esm/package.json +3 -0
- package/node_modules/rimraf/dist/esm/path-arg.d.ts +4 -0
- package/node_modules/rimraf/dist/esm/path-arg.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/path-arg.js +46 -0
- package/node_modules/rimraf/dist/esm/path-arg.js.map +1 -0
- package/node_modules/rimraf/dist/esm/readdir-or-error.d.ts +3 -0
- package/node_modules/rimraf/dist/esm/readdir-or-error.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/readdir-or-error.js +14 -0
- package/node_modules/rimraf/dist/esm/readdir-or-error.js.map +1 -0
- package/node_modules/rimraf/dist/esm/retry-busy.d.ts +8 -0
- package/node_modules/rimraf/dist/esm/retry-busy.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/retry-busy.js +60 -0
- package/node_modules/rimraf/dist/esm/retry-busy.js.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-manual.d.ts +3 -0
- package/node_modules/rimraf/dist/esm/rimraf-manual.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-manual.js +5 -0
- package/node_modules/rimraf/dist/esm/rimraf-manual.js.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-move-remove.d.ts +4 -0
- package/node_modules/rimraf/dist/esm/rimraf-move-remove.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-move-remove.js +133 -0
- package/node_modules/rimraf/dist/esm/rimraf-move-remove.js.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-native.d.ts +4 -0
- package/node_modules/rimraf/dist/esm/rimraf-native.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-native.js +19 -0
- package/node_modules/rimraf/dist/esm/rimraf-native.js.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-posix.d.ts +4 -0
- package/node_modules/rimraf/dist/esm/rimraf-posix.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-posix.js +98 -0
- package/node_modules/rimraf/dist/esm/rimraf-posix.js.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-windows.d.ts +4 -0
- package/node_modules/rimraf/dist/esm/rimraf-windows.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/rimraf-windows.js +154 -0
- package/node_modules/rimraf/dist/esm/rimraf-windows.js.map +1 -0
- package/node_modules/rimraf/dist/esm/use-native.d.ts +4 -0
- package/node_modules/rimraf/dist/esm/use-native.d.ts.map +1 -0
- package/node_modules/rimraf/dist/esm/use-native.js +15 -0
- package/node_modules/rimraf/dist/esm/use-native.js.map +1 -0
- package/node_modules/rimraf/package.json +92 -0
- package/package.json +152 -0
- package/scripts/install-scanners.js +258 -0
- package/scripts/watchdog.js +147 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +283 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/SparrowAnalyzer.js +341 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +250 -0
- package/src/analyzers/codeCloneDetector/index.js +192 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +88 -0
- package/src/core/agentPool.js +1957 -0
- package/src/core/agentScheduler.js +3212 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/flowExecutor.js +928 -0
- package/src/core/messageProcessor.js +808 -0
- package/src/core/orchestrator.js +584 -0
- package/src/core/stateManager.js +1500 -0
- package/src/index.js +972 -0
- package/src/interfaces/cli.js +553 -0
- package/src/interfaces/terminal/__tests__/smoke/advancedFeatures.test.js +208 -0
- package/src/interfaces/terminal/__tests__/smoke/agentControl.test.js +236 -0
- package/src/interfaces/terminal/__tests__/smoke/agents.test.js +138 -0
- package/src/interfaces/terminal/__tests__/smoke/components.test.js +137 -0
- package/src/interfaces/terminal/__tests__/smoke/connection.test.js +350 -0
- package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -0
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +332 -0
- package/src/interfaces/terminal/__tests__/smoke/messages.test.js +256 -0
- package/src/interfaces/terminal/__tests__/smoke/tools.test.js +388 -0
- package/src/interfaces/terminal/api/apiClient.js +299 -0
- package/src/interfaces/terminal/api/messageRouter.js +262 -0
- package/src/interfaces/terminal/api/session.js +266 -0
- package/src/interfaces/terminal/api/websocket.js +497 -0
- package/src/interfaces/terminal/components/AgentCreator.js +705 -0
- package/src/interfaces/terminal/components/AgentEditor.js +678 -0
- package/src/interfaces/terminal/components/AgentSwitcher.js +330 -0
- package/src/interfaces/terminal/components/ErrorBoundary.js +92 -0
- package/src/interfaces/terminal/components/ErrorPanel.js +264 -0
- package/src/interfaces/terminal/components/Header.js +28 -0
- package/src/interfaces/terminal/components/HelpPanel.js +231 -0
- package/src/interfaces/terminal/components/InputBox.js +118 -0
- package/src/interfaces/terminal/components/Layout.js +603 -0
- package/src/interfaces/terminal/components/LoadingSpinner.js +71 -0
- package/src/interfaces/terminal/components/MessageList.js +281 -0
- package/src/interfaces/terminal/components/MultilineTextInput.js +251 -0
- package/src/interfaces/terminal/components/SearchPanel.js +265 -0
- package/src/interfaces/terminal/components/SettingsPanel.js +415 -0
- package/src/interfaces/terminal/components/StatusBar.js +65 -0
- package/src/interfaces/terminal/components/TextInput.js +127 -0
- package/src/interfaces/terminal/config/agentEditorConstants.js +227 -0
- package/src/interfaces/terminal/config/constants.js +393 -0
- package/src/interfaces/terminal/index.js +168 -0
- package/src/interfaces/terminal/state/useAgentControl.js +496 -0
- package/src/interfaces/terminal/state/useAgents.js +537 -0
- package/src/interfaces/terminal/state/useConnection.js +444 -0
- package/src/interfaces/terminal/state/useMessages.js +630 -0
- package/src/interfaces/terminal/state/useTools.js +554 -0
- package/src/interfaces/terminal/utils/debugLogger.js +44 -0
- package/src/interfaces/terminal/utils/settingsStorage.js +232 -0
- package/src/interfaces/terminal/utils/theme.js +85 -0
- package/src/interfaces/webServer.js +5457 -0
- package/src/modules/fileExplorer/controller.js +413 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +158 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/agentActivityService.js +399 -0
- package/src/services/aiService.js +2618 -0
- package/src/services/apiKeyManager.js +334 -0
- package/src/services/benchmarkService.js +196 -0
- package/src/services/budgetService.js +565 -0
- package/src/services/contextInjectionService.js +268 -0
- package/src/services/conversationCompactionService.js +1103 -0
- package/src/services/credentialVault.js +685 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +547 -0
- package/src/services/flowContextService.js +189 -0
- package/src/services/memoryService.js +521 -0
- package/src/services/modelRouterService.js +365 -0
- package/src/services/modelsService.js +323 -0
- package/src/services/ollamaService.js +452 -0
- package/src/services/portRegistry.js +336 -0
- package/src/services/portTracker.js +223 -0
- package/src/services/projectDetector.js +404 -0
- package/src/services/promptService.js +372 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/scheduleService.js +725 -0
- package/src/services/serviceRegistry.js +386 -0
- package/src/services/skillsService.js +486 -0
- package/src/services/telegramService.js +920 -0
- package/src/services/tokenCountingService.js +316 -0
- package/src/services/visualEditorBridge.js +1033 -0
- package/src/services/visualEditorServer.js +1727 -0
- package/src/services/whatsappService.js +663 -0
- package/src/tools/__tests__/webTool.e2e.test.js +569 -0
- package/src/tools/__tests__/webTool.unit.test.js +195 -0
- package/src/tools/agentCommunicationTool.js +1343 -0
- package/src/tools/agentDelayTool.js +498 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +887 -0
- package/src/tools/browserTool.js +897 -0
- package/src/tools/cloneDetectionTool.js +581 -0
- package/src/tools/codeMapTool.js +857 -0
- package/src/tools/dependencyResolverTool.js +1212 -0
- package/src/tools/docxTool.js +623 -0
- package/src/tools/excelTool.js +636 -0
- package/src/tools/fileContentReplaceTool.js +840 -0
- package/src/tools/fileTreeTool.js +833 -0
- package/src/tools/filesystemTool.js +1217 -0
- package/src/tools/helpTool.js +198 -0
- package/src/tools/imageTool.js +1034 -0
- package/src/tools/importAnalyzerTool.js +1056 -0
- package/src/tools/jobDoneTool.js +388 -0
- package/src/tools/memoryTool.js +554 -0
- package/src/tools/pdfTool.js +627 -0
- package/src/tools/seekTool.js +883 -0
- package/src/tools/skillsTool.js +276 -0
- package/src/tools/staticAnalysisTool.js +2146 -0
- package/src/tools/taskManagerTool.js +2836 -0
- package/src/tools/terminalTool.js +2486 -0
- package/src/tools/userPromptTool.js +474 -0
- package/src/tools/videoTool.js +1139 -0
- package/src/tools/visionTool.js +507 -0
- package/src/tools/visualEditorTool.js +1175 -0
- package/src/tools/webTool.js +3114 -0
- package/src/tools/whatsappTool.js +457 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +288 -0
- package/src/utilities/browserStealth.js +630 -0
- package/src/utilities/configManager.js +618 -0
- package/src/utilities/constants.js +870 -0
- package/src/utilities/directoryAccessManager.js +566 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/humanBehavior.js +453 -0
- package/src/utilities/jsonRepair.js +242 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/platformUtils.js +255 -0
- package/src/utilities/platformUtils.test.js +98 -0
- package/src/utilities/stealthConstants.js +377 -0
- package/src/utilities/structuredFileValidator.js +699 -0
- package/src/utilities/tagParser.js +878 -0
- package/src/utilities/toolConstants.js +415 -0
- package/src/utilities/userDataDir.js +300 -0
- package/web-ui/build/brands/autopilot/favicon.svg +1 -0
- package/web-ui/build/brands/autopilot/logo.webp +0 -0
- package/web-ui/build/brands/onbuzz/favicon.svg +1 -0
- package/web-ui/build/brands/onbuzz/logo-text.webp +0 -0
- package/web-ui/build/brands/onbuzz/logo.webp +0 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-SmQFfvBs.js +746 -0
- package/web-ui/build/static/index-V2ySwjHp.css +1 -0
|
@@ -0,0 +1,3212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentScheduler - Manages cooperative execution of multiple agents
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - Uses centralized AgentActivityService to determine which agents should be active
|
|
6
|
+
* - Iterates over active agents in round-robin fashion
|
|
7
|
+
* - For each agent, processes queued messages (toolResults, interAgentMessages, userMessages) in arrival order
|
|
8
|
+
* - Sends conversation history to AI service for completion
|
|
9
|
+
* - Handles agent mode differences (CHAT vs AGENT)
|
|
10
|
+
* - Respects agent delays set by agentDelay tool
|
|
11
|
+
*
|
|
12
|
+
* Key Change: Instead of managing add/remove events scattered across codebase,
|
|
13
|
+
* the scheduler now queries AgentActivityService.getActiveAgents() each cycle
|
|
14
|
+
* to determine which agents should be processed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AGENT_MODES,
|
|
19
|
+
MESSAGE_ROLES,
|
|
20
|
+
COMPACTION_CONFIG,
|
|
21
|
+
COMPACTION_STATUS,
|
|
22
|
+
COMPACTION_STRATEGIES,
|
|
23
|
+
SCHEDULER_CONFIG,
|
|
24
|
+
AGENT_ACTIVITY_STATUS,
|
|
25
|
+
TASK_STATUS,
|
|
26
|
+
TASK_PRIORITY_ORDER
|
|
27
|
+
} from '../utilities/constants.js';
|
|
28
|
+
import ContextInjectionService from '../services/contextInjectionService.js';
|
|
29
|
+
import FlowContextService from '../services/flowContextService.js';
|
|
30
|
+
import TokenCountingService from '../services/tokenCountingService.js';
|
|
31
|
+
import ConversationCompactionService from '../services/conversationCompactionService.js';
|
|
32
|
+
import {
|
|
33
|
+
shouldAgentBeActive,
|
|
34
|
+
getActiveAgents,
|
|
35
|
+
shouldSkipIteration
|
|
36
|
+
} from '../services/agentActivityService.js';
|
|
37
|
+
|
|
38
|
+
class AgentScheduler {
|
|
39
|
+
constructor(agentPool, messageProcessor, aiService, logger, webSocketManager = null, modelRouterService = null, modelsService = null) {
|
|
40
|
+
this.agentPool = agentPool;
|
|
41
|
+
this.messageProcessor = messageProcessor;
|
|
42
|
+
this.aiService = aiService;
|
|
43
|
+
this.logger = logger;
|
|
44
|
+
this.webSocketManager = webSocketManager;
|
|
45
|
+
this.modelRouterService = modelRouterService;
|
|
46
|
+
this.modelsService = modelsService;
|
|
47
|
+
|
|
48
|
+
// Initialize ContextInjectionService for file attachments
|
|
49
|
+
this.contextInjectionService = new ContextInjectionService({}, logger);
|
|
50
|
+
|
|
51
|
+
// Initialize FlowContextService for flow execution context
|
|
52
|
+
this.flowContextService = new FlowContextService({}, logger);
|
|
53
|
+
|
|
54
|
+
// Initialize compactization services
|
|
55
|
+
this.tokenCountingService = new TokenCountingService(logger, modelsService);
|
|
56
|
+
this.compactionService = new ConversationCompactionService(
|
|
57
|
+
this.tokenCountingService,
|
|
58
|
+
aiService,
|
|
59
|
+
logger
|
|
60
|
+
);
|
|
61
|
+
// Inject modelsService for runtime compaction model validation
|
|
62
|
+
if (modelsService) {
|
|
63
|
+
this.compactionService.setModelsService(modelsService);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Compactization state tracking
|
|
67
|
+
this.compactionInProgress = new Map(); // Map of agentId to compaction status
|
|
68
|
+
|
|
69
|
+
// Scheduler state
|
|
70
|
+
this.isRunning = false;
|
|
71
|
+
this.agentSessionMap = new Map(); // Map of agentId to sessionId (for API key resolution)
|
|
72
|
+
this.scheduleInterval = null;
|
|
73
|
+
|
|
74
|
+
// Repetition detection - sliding window of recent state hashes per agent
|
|
75
|
+
// Structure: Map<agentId, Array<{hash, timestamp}>>
|
|
76
|
+
this.stateHashHistory = new Map();
|
|
77
|
+
|
|
78
|
+
// Per-agent processing locks — prevent the same agent from being processed
|
|
79
|
+
// concurrently across overlapping cycles. No global lock needed; each cycle
|
|
80
|
+
// skips agents that are already in-flight from a previous cycle.
|
|
81
|
+
this.agentProcessingLocks = new Map(); // Map of agentId to processing state
|
|
82
|
+
|
|
83
|
+
// Round-robin fairness: tracks last cycle's launch order so agents that
|
|
84
|
+
// were skipped due to concurrency cap get priority in the next cycle.
|
|
85
|
+
this._lastLaunchedAgentIds = new Set();
|
|
86
|
+
|
|
87
|
+
// Token limit error retry tracking - tracks failed attempts per agent
|
|
88
|
+
// Structure: Map<agentId, { attempts: number, lastError: string, timestamp: Date }>
|
|
89
|
+
this.tokenLimitRetryTracker = new Map();
|
|
90
|
+
this.MAX_TOKEN_LIMIT_RETRIES = 2;
|
|
91
|
+
|
|
92
|
+
// Consecutive messages without tool usage tracking (AGENT mode only)
|
|
93
|
+
// Structure: Map<agentId, number> - count of consecutive messages without tools
|
|
94
|
+
this.consecutiveNoToolMessages = new Map();
|
|
95
|
+
|
|
96
|
+
// Configuration from constants (no magic numbers)
|
|
97
|
+
this.iterationDelayMs = SCHEDULER_CONFIG.ITERATION_DELAY_MS;
|
|
98
|
+
this.maxIterationsPerCycle = SCHEDULER_CONFIG.MAX_ITERATIONS_PER_CYCLE;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Start the agent scheduler
|
|
103
|
+
*/
|
|
104
|
+
start() {
|
|
105
|
+
if (this.isRunning) {
|
|
106
|
+
this.logger.info('Agent scheduler is already running');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.isRunning = true;
|
|
111
|
+
this.logger.info('Starting Agent Scheduler with centralized activity service');
|
|
112
|
+
|
|
113
|
+
// Start the main scheduler loop
|
|
114
|
+
// No need to initialize agents - the processing cycle queries active agents each iteration
|
|
115
|
+
this.scheduleInterval = setInterval(() => {
|
|
116
|
+
this.processingCycle().catch(error => {
|
|
117
|
+
this.logger.error('Scheduler processing cycle failed:', error);
|
|
118
|
+
});
|
|
119
|
+
}, this.iterationDelayMs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register session ID for an agent (used for API key resolution)
|
|
124
|
+
* This is called when a message is sent to an agent to associate the session
|
|
125
|
+
* @param {string} agentId - Agent ID
|
|
126
|
+
* @param {string} sessionId - Session ID for API key resolution
|
|
127
|
+
*/
|
|
128
|
+
registerAgentSession(agentId, sessionId) {
|
|
129
|
+
if (agentId && sessionId) {
|
|
130
|
+
this.agentSessionMap.set(agentId, sessionId);
|
|
131
|
+
this.logger.debug(`Registered session for agent: ${agentId}`, { sessionId });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get session ID for an agent
|
|
137
|
+
* @param {string} agentId - Agent ID
|
|
138
|
+
* @returns {string|undefined} Session ID or undefined
|
|
139
|
+
*/
|
|
140
|
+
getAgentSession(agentId) {
|
|
141
|
+
return this.agentSessionMap.get(agentId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Stop the agent scheduler
|
|
146
|
+
*/
|
|
147
|
+
stop() {
|
|
148
|
+
if (!this.isRunning) return;
|
|
149
|
+
|
|
150
|
+
this.isRunning = false;
|
|
151
|
+
if (this.scheduleInterval) {
|
|
152
|
+
clearInterval(this.scheduleInterval);
|
|
153
|
+
this.scheduleInterval = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.agentSessionMap.clear();
|
|
157
|
+
this.compactionInProgress.clear();
|
|
158
|
+
this.stateHashHistory.clear();
|
|
159
|
+
this.agentProcessingLocks.clear();
|
|
160
|
+
this.consecutiveNoToolMessages.clear();
|
|
161
|
+
|
|
162
|
+
// Cleanup services
|
|
163
|
+
if (this.tokenCountingService && this.tokenCountingService.cleanup) {
|
|
164
|
+
this.tokenCountingService.cleanup();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.logger.info('Agent Scheduler stopped');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Register an agent's session and ensure scheduler is running
|
|
172
|
+
*
|
|
173
|
+
* NOTE: This method is kept for backward compatibility. The actual decision
|
|
174
|
+
* of whether an agent should be processed is now made by AgentActivityService.
|
|
175
|
+
* This method now only:
|
|
176
|
+
* 1. Registers the session ID for API key resolution
|
|
177
|
+
* 2. Ensures the scheduler is running
|
|
178
|
+
*
|
|
179
|
+
* @param {string} agentId - Agent ID
|
|
180
|
+
* @param {Object} context - Context containing sessionId
|
|
181
|
+
*/
|
|
182
|
+
async addAgent(agentId, context = {}) {
|
|
183
|
+
// Register session ID for API key resolution
|
|
184
|
+
if (context.sessionId) {
|
|
185
|
+
this.registerAgentSession(agentId, context.sessionId);
|
|
186
|
+
} else {
|
|
187
|
+
this.logger.warn(`Agent ${agentId} registered without sessionId - API key resolution may fail`, {
|
|
188
|
+
triggeredBy: context.triggeredBy
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Clear hash history when user sends a new message
|
|
193
|
+
// This provides a "fresh start" after user intervention, preventing false loop detection
|
|
194
|
+
if (context.triggeredBy === 'user-message') {
|
|
195
|
+
this.clearHashHistory(agentId);
|
|
196
|
+
this.logger.debug(`Hash history cleared for agent ${agentId} due to user message`);
|
|
197
|
+
|
|
198
|
+
// Also reset consecutive no-tool counter on user message (fresh interaction)
|
|
199
|
+
if (this.consecutiveNoToolMessages.has(agentId)) {
|
|
200
|
+
this.consecutiveNoToolMessages.set(agentId, 0);
|
|
201
|
+
this.logger.debug(`Consecutive no-tool counter reset for agent ${agentId} due to user message`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Initialize hash history for this agent if not exists
|
|
206
|
+
if (!this.stateHashHistory.has(agentId)) {
|
|
207
|
+
this.stateHashHistory.set(agentId, []);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.logger.debug(`Agent session registered: ${agentId}`, {
|
|
211
|
+
sessionId: context.sessionId || 'NO_SESSION_ID',
|
|
212
|
+
triggeredBy: context.triggeredBy || 'unknown'
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Start scheduler if not running
|
|
216
|
+
if (!this.isRunning) {
|
|
217
|
+
this.start();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Clean up session tracking for an agent
|
|
223
|
+
*
|
|
224
|
+
* NOTE: This method is kept for backward compatibility and cleanup.
|
|
225
|
+
* The actual decision of whether to stop processing an agent is now
|
|
226
|
+
* made by AgentActivityService - agents are not "removed" from scheduler,
|
|
227
|
+
* they simply become inactive based on their state.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} agentId - Agent ID
|
|
230
|
+
* @param {string} reason - Reason for cleanup (for logging)
|
|
231
|
+
*/
|
|
232
|
+
removeAgent(agentId, reason = 'completed') {
|
|
233
|
+
// Clean up session mapping
|
|
234
|
+
if (this.agentSessionMap.has(agentId)) {
|
|
235
|
+
this.agentSessionMap.delete(agentId);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Clean up state hash history for this agent
|
|
239
|
+
if (this.stateHashHistory.has(agentId)) {
|
|
240
|
+
this.stateHashHistory.delete(agentId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Clean up processing lock
|
|
244
|
+
if (this.agentProcessingLocks.has(agentId)) {
|
|
245
|
+
this.agentProcessingLocks.delete(agentId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Clean up consecutive no-tool counter
|
|
249
|
+
if (this.consecutiveNoToolMessages.has(agentId)) {
|
|
250
|
+
this.consecutiveNoToolMessages.delete(agentId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.logger.debug(`Agent session cleaned up: ${agentId}`, { reason });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if agent should currently be active in scheduler
|
|
258
|
+
* Uses the centralized AgentActivityService for the decision
|
|
259
|
+
* @param {string} agentId - Agent ID to check
|
|
260
|
+
* @returns {Promise<boolean>} True if agent should be active
|
|
261
|
+
*/
|
|
262
|
+
async isAgentInScheduler(agentId) {
|
|
263
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
264
|
+
if (!agent) return false;
|
|
265
|
+
|
|
266
|
+
const result = shouldAgentBeActive(agent);
|
|
267
|
+
return result.active;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Stop autonomous execution for a specific agent
|
|
272
|
+
* This sets the agent state so that shouldAgentBeActive() returns false
|
|
273
|
+
* @param {string} agentId - Agent ID to stop
|
|
274
|
+
* @returns {Promise<Object>} Result with agent state
|
|
275
|
+
*/
|
|
276
|
+
async stopAgentExecution(agentId) {
|
|
277
|
+
try {
|
|
278
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
279
|
+
if (!agent) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: 'Agent not found'
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Set stopRequested flag FIRST - this signals any concurrent processing to stop
|
|
287
|
+
// This is checked by shouldAgentBeActive() and processAgent()
|
|
288
|
+
agent.stopRequested = true;
|
|
289
|
+
|
|
290
|
+
// CRITICAL: Abort any active streaming request to Azure backend
|
|
291
|
+
// This immediately stops the HTTP connection and prevents further chunk processing
|
|
292
|
+
if (this.aiService && this.aiService.abortRequest) {
|
|
293
|
+
const aborted = this.aiService.abortRequest(agentId);
|
|
294
|
+
if (aborted) {
|
|
295
|
+
this.logger.info(`Aborted active request for agent: ${agentId}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Set agent mode back to CHAT - this will cause shouldAgentBeActive to return false
|
|
300
|
+
agent.mode = AGENT_MODES.CHAT;
|
|
301
|
+
|
|
302
|
+
// Clear any delays
|
|
303
|
+
agent.delayEndTime = null;
|
|
304
|
+
|
|
305
|
+
// Clear stop request flag after mode is set (it has been honored)
|
|
306
|
+
agent.stopRequested = false;
|
|
307
|
+
|
|
308
|
+
// Persist the change
|
|
309
|
+
await this.agentPool.persistAgentState(agentId);
|
|
310
|
+
|
|
311
|
+
// Clean up session tracking
|
|
312
|
+
this.removeAgent(agentId, 'stopped-by-user');
|
|
313
|
+
|
|
314
|
+
// Get session ID for broadcast
|
|
315
|
+
const sessionId = this.getAgentSession(agentId) || agent.sessionId;
|
|
316
|
+
|
|
317
|
+
// Broadcast stop event if we have a valid session ID
|
|
318
|
+
if (sessionId && this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
319
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
320
|
+
type: 'execution_stopped',
|
|
321
|
+
data: {
|
|
322
|
+
agentId,
|
|
323
|
+
type: 'execution_stopped',
|
|
324
|
+
mode: AGENT_MODES.CHAT, // Include mode change for UI toggle
|
|
325
|
+
timestamp: new Date().toISOString()
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.logger.info(`Agent execution stopped: ${agentId}`, {
|
|
331
|
+
mode: agent.mode
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
success: true,
|
|
336
|
+
agent: {
|
|
337
|
+
id: agent.id,
|
|
338
|
+
name: agent.name,
|
|
339
|
+
mode: agent.mode
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.logger.error(`Failed to stop agent execution: ${agentId}`, {
|
|
345
|
+
error: error.message
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
error: error.message
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Main processing cycle - queries active agents and processes them
|
|
357
|
+
*
|
|
358
|
+
* KEY CHANGE: Instead of iterating over a managed activeAgents Map,
|
|
359
|
+
* we now query AgentActivityService.getActiveAgents() each cycle.
|
|
360
|
+
* This is the centralized approach that eliminates scattered add/remove logic.
|
|
361
|
+
*
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
async processingCycle() {
|
|
365
|
+
// Get all agents from pool and determine which should be active
|
|
366
|
+
const allAgents = await this.agentPool.getAllAgents();
|
|
367
|
+
const activeAgentResults = getActiveAgents(allAgents);
|
|
368
|
+
|
|
369
|
+
if (activeAgentResults.length === 0) {
|
|
370
|
+
return; // No agents to process
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Concurrency enforcement: count how many agents are already in-flight
|
|
374
|
+
// from previous cycles, and only launch new ones up to the global cap.
|
|
375
|
+
const maxConcurrent = SCHEDULER_CONFIG.MAX_CONCURRENT_AGENTS || 3;
|
|
376
|
+
const currentlyInFlight = this.agentProcessingLocks.size;
|
|
377
|
+
|
|
378
|
+
// Filter out agents already being processed
|
|
379
|
+
const unlockedAgents = activeAgentResults.filter(r => {
|
|
380
|
+
if (this.agentProcessingLocks.get(r.agentId)) {
|
|
381
|
+
this.logger.debug(`Agent ${r.agentId} still processing from previous cycle, skipping`);
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (unlockedAgents.length === 0) {
|
|
388
|
+
return; // All active agents already being processed
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Round-robin fairness: agents that launched in a recent cycle go to the back
|
|
392
|
+
// so that waiting agents get priority for available slots.
|
|
393
|
+
unlockedAgents.sort((a, b) => {
|
|
394
|
+
const aRan = this._lastLaunchedAgentIds.has(a.agentId) ? 1 : 0;
|
|
395
|
+
const bRan = this._lastLaunchedAgentIds.has(b.agentId) ? 1 : 0;
|
|
396
|
+
return aRan - bRan; // agents that didn't run recently come first
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Cap new launches so total in-flight never exceeds MAX_CONCURRENT_AGENTS
|
|
400
|
+
const slotsAvailable = Math.max(0, maxConcurrent - currentlyInFlight);
|
|
401
|
+
const agentsToLaunch = unlockedAgents.slice(0, slotsAvailable);
|
|
402
|
+
|
|
403
|
+
if (agentsToLaunch.length === 0) {
|
|
404
|
+
this.logger.debug(`Concurrency cap reached: ${currentlyInFlight}/${maxConcurrent} agents in-flight, ${unlockedAgents.length} waiting`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.logger.debug(`Processing cycle: launching ${agentsToLaunch.length} agents (${currentlyInFlight} in-flight, ${maxConcurrent} max)`, {
|
|
409
|
+
agents: agentsToLaunch.map(r => ({ id: r.agentId, reason: r.reason })),
|
|
410
|
+
waiting: unlockedAgents.length - agentsToLaunch.length
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Track which agents we're launching for round-robin fairness
|
|
414
|
+
this._lastLaunchedAgentIds = new Set(agentsToLaunch.map(r => r.agentId));
|
|
415
|
+
|
|
416
|
+
// Fire-and-forget: launch processing without awaiting.
|
|
417
|
+
// Each agent is protected by its own lock in processAgent().
|
|
418
|
+
// Next cycle (1s later) will pick up remaining agents when slots free up.
|
|
419
|
+
this.processAgentsInParallel(agentsToLaunch).catch(error => {
|
|
420
|
+
this.logger.error('Parallel agent processing failed:', error);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Process multiple agents in parallel.
|
|
426
|
+
* Concurrency is capped at the cycle level (processingCycle slices to
|
|
427
|
+
* MAX_CONCURRENT_AGENTS), so this method simply launches all given agents
|
|
428
|
+
* concurrently via Promise.all.
|
|
429
|
+
*
|
|
430
|
+
* @param {Array} activeAgentResults - Array of { agent, agentId, reason }
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
async processAgentsInParallel(activeAgentResults) {
|
|
434
|
+
this.logger.debug(`Launching ${activeAgentResults.length} agents in parallel`, {
|
|
435
|
+
agentIds: activeAgentResults.map(r => r.agentId)
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await Promise.all(
|
|
439
|
+
activeAgentResults.map(async ({ agentId, reason }) => {
|
|
440
|
+
try {
|
|
441
|
+
await this.processAgent(agentId);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
this.logger.error(`Agent processing failed: ${agentId}`, {
|
|
444
|
+
error: error.message,
|
|
445
|
+
stack: error.stack,
|
|
446
|
+
activationReason: reason
|
|
447
|
+
});
|
|
448
|
+
// Clean up on error - don't leave stale locks
|
|
449
|
+
this.agentProcessingLocks.delete(agentId);
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Process a single agent - handle queues and get AI response
|
|
457
|
+
*
|
|
458
|
+
* NOTE: This method no longer returns whether the agent should continue.
|
|
459
|
+
* The decision is made by AgentActivityService at the start of each cycle.
|
|
460
|
+
*
|
|
461
|
+
* @param {string} agentId - Agent ID to process
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
async processAgent(agentId) {
|
|
465
|
+
// Check if this agent is already being processed
|
|
466
|
+
if (this.agentProcessingLocks.get(agentId)) {
|
|
467
|
+
this.logger.debug(`Agent ${agentId} is already being processed, skipping`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
472
|
+
if (!agent) {
|
|
473
|
+
return; // Agent no longer exists
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Set processing lock (prevents concurrent processing of the same agent)
|
|
477
|
+
this.agentProcessingLocks.set(agentId, true);
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
// Use centralized service to check if we should skip this iteration
|
|
481
|
+
const skipCheck = shouldSkipIteration(agent);
|
|
482
|
+
if (skipCheck.skip) {
|
|
483
|
+
this.logger.debug(`Agent ${agentId} skipping iteration: ${skipCheck.reason}`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Generate current state hash for repetition detection
|
|
488
|
+
const currentStateHash = this.generateAgentStateHash(agent);
|
|
489
|
+
|
|
490
|
+
// Check for repetitive loop using sliding window
|
|
491
|
+
const loopDetection = this.detectRepetitiveLoop(agentId, currentStateHash);
|
|
492
|
+
if (loopDetection.isLoop) {
|
|
493
|
+
this.logger.warn(`Agent ${agentId} detected in repetitive loop - terminating`, {
|
|
494
|
+
stateHash: currentStateHash,
|
|
495
|
+
occurrences: loopDetection.occurrences,
|
|
496
|
+
windowSize: SCHEDULER_CONFIG.STATE_HASH_WINDOW_SIZE,
|
|
497
|
+
threshold: SCHEDULER_CONFIG.REPETITION_THRESHOLD
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Notify user about the loop and stop the agent
|
|
501
|
+
await this.handleRepetitiveLoop(agentId, loopDetection);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Check if this exact state was just processed (immediate duplicate)
|
|
506
|
+
if (loopDetection.isImmediateDuplicate) {
|
|
507
|
+
this.logger.debug(`Agent ${agentId} state unchanged from last iteration, skipping`, {
|
|
508
|
+
stateHash: currentStateHash,
|
|
509
|
+
agentMode: agent.mode
|
|
510
|
+
});
|
|
511
|
+
return; // Skip - nothing new to process
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Get message queue status
|
|
515
|
+
const queues = agent.messageQueues || {};
|
|
516
|
+
const totalMessages = (queues.toolResults?.length || 0) +
|
|
517
|
+
(queues.interAgentMessages?.length || 0) +
|
|
518
|
+
(queues.userMessages?.length || 0);
|
|
519
|
+
|
|
520
|
+
// Log if user messages are present (highest priority)
|
|
521
|
+
if (queues.userMessages?.length > 0) {
|
|
522
|
+
this.logger.info(`User message detected for agent ${agentId} - will be prioritized`, {
|
|
523
|
+
userMessageCount: queues.userMessages.length,
|
|
524
|
+
agentMode: agent.mode
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check if stop was requested - exit early if so
|
|
529
|
+
if (agent.stopRequested) {
|
|
530
|
+
this.logger.info(`Agent ${agentId} stop requested - aborting processing`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Track whether we actually got a new AI response this cycle
|
|
535
|
+
let gotNewAIResponse = false;
|
|
536
|
+
|
|
537
|
+
// Process based on whether there are messages or agent needs autonomous processing
|
|
538
|
+
if (totalMessages === 0 && agent.mode === AGENT_MODES.AGENT) {
|
|
539
|
+
// AGENT mode with no messages - check for task-based work
|
|
540
|
+
await this.autoCreateInitialTaskIfNeeded(agentId);
|
|
541
|
+
gotNewAIResponse = await this.processAgentAutonomously(agentId);
|
|
542
|
+
} else if (totalMessages > 0) {
|
|
543
|
+
// Has messages to process
|
|
544
|
+
const processedMessages = await this.processAgentQueues(agentId);
|
|
545
|
+
|
|
546
|
+
if (processedMessages > 0 || agent.mode === AGENT_MODES.AGENT) {
|
|
547
|
+
// Get AI response after processing queued messages
|
|
548
|
+
const aiResponse = await this.getAgentAIResponse(agentId);
|
|
549
|
+
|
|
550
|
+
if (aiResponse) {
|
|
551
|
+
// Process AI response and execute any tools
|
|
552
|
+
await this.processAIResponse(agentId, aiResponse);
|
|
553
|
+
gotNewAIResponse = true;
|
|
554
|
+
|
|
555
|
+
// Clear token limit retry tracker on successful AI response
|
|
556
|
+
this.clearTokenLimitRetryTracker(agentId);
|
|
557
|
+
} else {
|
|
558
|
+
this.logger.warn(`No AI response for agent ${agentId}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// CHAT mode with no messages: do nothing - activity service will mark as inactive
|
|
563
|
+
|
|
564
|
+
// Only record state hash when we actually got a new AI response
|
|
565
|
+
// This prevents false loop detection when agent is idle
|
|
566
|
+
if (gotNewAIResponse) {
|
|
567
|
+
this.recordStateHash(agentId, currentStateHash);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Decrement TTL (Time-to-Live) if set
|
|
571
|
+
// TTL gives agents extra processing cycles after clearing tasks
|
|
572
|
+
const agentForTtl = await this.agentPool.getAgent(agentId);
|
|
573
|
+
if (agentForTtl && agentForTtl.ttl !== null && agentForTtl.ttl !== undefined && agentForTtl.ttl > 0) {
|
|
574
|
+
agentForTtl.ttl--;
|
|
575
|
+
if (agentForTtl.ttl <= 0) {
|
|
576
|
+
agentForTtl.ttl = null; // Clear expired TTL
|
|
577
|
+
}
|
|
578
|
+
await this.agentPool.persistAgentState(agentId);
|
|
579
|
+
this.logger.debug(`TTL decremented for agent ${agentId}`, { newTtl: agentForTtl.ttl });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
} finally {
|
|
583
|
+
// Always clear processing lock, even on errors
|
|
584
|
+
this.agentProcessingLocks.delete(agentId);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Process agent's message queues with consolidated single-message approach
|
|
590
|
+
* @param {string} agentId - Agent ID
|
|
591
|
+
* @returns {Promise<number>} Number of messages processed
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
async processAgentQueues(agentId) {
|
|
595
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
596
|
+
if (!agent) return 0;
|
|
597
|
+
|
|
598
|
+
const queues = agent.messageQueues;
|
|
599
|
+
|
|
600
|
+
// Task boundary: if agent just completed work (jobdone) and has stale tool
|
|
601
|
+
// results from the previous task alongside new messages, drain the stale
|
|
602
|
+
// results into conversation history as a separate boundary entry so they
|
|
603
|
+
// don't get mixed into the new task's consolidated input.
|
|
604
|
+
const hasNewMessages = (queues.userMessages?.length > 0) || (queues.interAgentMessages?.length > 0);
|
|
605
|
+
if (agent.autonomousWorkComplete && queues.toolResults?.length > 0 && hasNewMessages) {
|
|
606
|
+
const staleCount = queues.toolResults.length;
|
|
607
|
+
let boundaryContent = '[Previous Task — Final Tool Results]\n';
|
|
608
|
+
queues.toolResults.forEach(msg => {
|
|
609
|
+
boundaryContent += `${this.formatToolResult(msg)}\n`;
|
|
610
|
+
});
|
|
611
|
+
boundaryContent += '\n--- Previous task completed. New task follows. ---';
|
|
612
|
+
|
|
613
|
+
await this.addMessageToConversation(agentId, {
|
|
614
|
+
id: `task-boundary-${Date.now()}`,
|
|
615
|
+
role: MESSAGE_ROLES.USER,
|
|
616
|
+
content: boundaryContent.trim(),
|
|
617
|
+
timestamp: new Date().toISOString(),
|
|
618
|
+
type: 'task-boundary'
|
|
619
|
+
}, false);
|
|
620
|
+
|
|
621
|
+
queues.toolResults.length = 0;
|
|
622
|
+
agent.autonomousWorkComplete = false;
|
|
623
|
+
this.logger.info(`Task boundary: drained ${staleCount} stale tool results for agent ${agentId}`);
|
|
624
|
+
} else if (agent.autonomousWorkComplete && hasNewMessages) {
|
|
625
|
+
// No stale tool results but new messages arrived — just reset the flag
|
|
626
|
+
agent.autonomousWorkComplete = false;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Collect all messages with timestamps for proper ordering
|
|
630
|
+
const allMessages = [
|
|
631
|
+
...queues.toolResults.map(msg => ({ ...msg, queueType: 'toolResults' })),
|
|
632
|
+
...queues.interAgentMessages.map(msg => ({ ...msg, queueType: 'interAgentMessages' })),
|
|
633
|
+
...queues.userMessages.map(msg => ({ ...msg, queueType: 'userMessages' }))
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
if (allMessages.length === 0) return 0;
|
|
637
|
+
|
|
638
|
+
// Sort by arrival time (timestamp)
|
|
639
|
+
allMessages.sort((a, b) => new Date(a.timestamp || a.queuedAt || 0) - new Date(b.timestamp || b.queuedAt || 0));
|
|
640
|
+
|
|
641
|
+
// CRITICAL FIX: Consolidate all messages into single AI request
|
|
642
|
+
let consolidatedContent = '';
|
|
643
|
+
const hasUserMessages = allMessages.some(m => m.queueType === 'userMessages');
|
|
644
|
+
const hasInterAgentMessages = allMessages.some(m => m.queueType === 'interAgentMessages');
|
|
645
|
+
const hasToolResults = allMessages.some(m => m.queueType === 'toolResults');
|
|
646
|
+
|
|
647
|
+
// Add user messages first (highest priority)
|
|
648
|
+
const userMessages = allMessages.filter(m => m.queueType === 'userMessages');
|
|
649
|
+
if (userMessages.length > 0) {
|
|
650
|
+
userMessages.forEach(msg => {
|
|
651
|
+
if (consolidatedContent) consolidatedContent += '\n\n';
|
|
652
|
+
consolidatedContent += msg.content;
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Add inter-agent messages as context (not as separate system messages)
|
|
657
|
+
const interAgentMessages = allMessages.filter(m => m.queueType === 'interAgentMessages');
|
|
658
|
+
if (interAgentMessages.length > 0) {
|
|
659
|
+
if (consolidatedContent) consolidatedContent += '\n\n';
|
|
660
|
+
consolidatedContent += '[Agent Messages]\n';
|
|
661
|
+
interAgentMessages.forEach(msg => {
|
|
662
|
+
const senderName = msg.senderName || msg.sender || 'Unknown Agent';
|
|
663
|
+
consolidatedContent += `${senderName}: ${msg.content}\n`;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Add tool results as context, grouped by the AI turn that triggered them
|
|
668
|
+
const toolResults = allMessages.filter(m => m.queueType === 'toolResults');
|
|
669
|
+
if (toolResults.length > 0) {
|
|
670
|
+
if (consolidatedContent) consolidatedContent += '\n\n';
|
|
671
|
+
|
|
672
|
+
// Group results by responseTurnId (the AI message that triggered them)
|
|
673
|
+
const turnGroups = new Map();
|
|
674
|
+
for (const msg of toolResults) {
|
|
675
|
+
const turnKey = msg.responseTurnId || 'unknown';
|
|
676
|
+
if (!turnGroups.has(turnKey)) turnGroups.set(turnKey, []);
|
|
677
|
+
turnGroups.get(turnKey).push(msg);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Manifest: tell agent what to expect
|
|
681
|
+
const turnCount = turnGroups.size;
|
|
682
|
+
const toolCount = toolResults.length;
|
|
683
|
+
const toolIds = [...new Set(toolResults.map(m => m.toolId).filter(Boolean))];
|
|
684
|
+
consolidatedContent += `[Tool Results — ${toolCount} result${toolCount > 1 ? 's' : ''} from ${turnCount} tool batch${turnCount > 1 ? 'es' : ''}: ${toolIds.join(', ')}]\n`;
|
|
685
|
+
|
|
686
|
+
if (turnCount === 1) {
|
|
687
|
+
// Single batch — flat list (no sub-headers needed)
|
|
688
|
+
toolResults.forEach(msg => {
|
|
689
|
+
consolidatedContent += `${this.formatToolResult(msg)}\n`;
|
|
690
|
+
});
|
|
691
|
+
} else {
|
|
692
|
+
// Multiple batches — group with labeled sub-headers
|
|
693
|
+
let batchIndex = 1;
|
|
694
|
+
for (const [, group] of turnGroups) {
|
|
695
|
+
consolidatedContent += `\n--- Batch ${batchIndex} of ${turnCount} ---\n`;
|
|
696
|
+
group.forEach(msg => {
|
|
697
|
+
consolidatedContent += `${this.formatToolResult(msg)}\n`;
|
|
698
|
+
});
|
|
699
|
+
batchIndex++;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Add processing instructions only if needed
|
|
705
|
+
if (hasInterAgentMessages) {
|
|
706
|
+
consolidatedContent += '\nNote: Use the agentcommunication tool if you need to respond to other agents.';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// PHASE 2: Auto-create tasks for incoming messages
|
|
710
|
+
await this.autoCreateTasksForMessages(agentId, userMessages, interAgentMessages);
|
|
711
|
+
|
|
712
|
+
// Create single consolidated user message for AI processing
|
|
713
|
+
const consolidatedMessage = {
|
|
714
|
+
id: `consolidated-${Date.now()}`,
|
|
715
|
+
role: MESSAGE_ROLES.USER,
|
|
716
|
+
content: consolidatedContent.trim(),
|
|
717
|
+
timestamp: new Date().toISOString(),
|
|
718
|
+
type: 'consolidated-input',
|
|
719
|
+
originalMessageCount: allMessages.length
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// Add to conversation history (don't broadcast - this is internal)
|
|
723
|
+
await this.addMessageToConversation(agentId, consolidatedMessage, false);
|
|
724
|
+
|
|
725
|
+
// CRITICAL: Update conversation tracking when inter-agent messages are processed
|
|
726
|
+
if (agent && interAgentMessages.length > 0) {
|
|
727
|
+
// Ensure interAgentTracking is a Map (defensive - may be plain object from JSON)
|
|
728
|
+
if (!agent.interAgentTracking || !(agent.interAgentTracking instanceof Map)) {
|
|
729
|
+
if (agent.interAgentTracking && typeof agent.interAgentTracking === 'object') {
|
|
730
|
+
agent.interAgentTracking = new Map(Object.entries(agent.interAgentTracking));
|
|
731
|
+
} else {
|
|
732
|
+
agent.interAgentTracking = new Map();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
for (const msg of interAgentMessages) {
|
|
737
|
+
if (msg.sender) {
|
|
738
|
+
// Mark that this agent received a message from the sender
|
|
739
|
+
if (!agent.interAgentTracking.has(msg.sender)) {
|
|
740
|
+
agent.interAgentTracking.set(msg.sender, {
|
|
741
|
+
lastSent: null,
|
|
742
|
+
lastReceived: null,
|
|
743
|
+
lastType: null
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const tracking = agent.interAgentTracking.get(msg.sender);
|
|
748
|
+
tracking.lastReceived = Date.now();
|
|
749
|
+
tracking.lastType = 'received';
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Persist updated tracking
|
|
753
|
+
await this.agentPool.persistAgentState(agentId);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Clear all processed queues
|
|
757
|
+
queues.toolResults.length = 0;
|
|
758
|
+
queues.interAgentMessages.length = 0;
|
|
759
|
+
queues.userMessages.length = 0;
|
|
760
|
+
|
|
761
|
+
// Persist updated agent state
|
|
762
|
+
await this.agentPool.persistAgentState(agentId);
|
|
763
|
+
|
|
764
|
+
this.logger.debug(`Consolidated ${allMessages.length} queued messages for agent ${agentId}`);
|
|
765
|
+
return allMessages.length;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Add message to agent's conversation history with proper formatting
|
|
770
|
+
* @param {string} agentId - Agent ID
|
|
771
|
+
* @param {Object} message - Message to add
|
|
772
|
+
* @param {boolean} broadcast - Whether to broadcast message to UI (default true)
|
|
773
|
+
* @private
|
|
774
|
+
*/
|
|
775
|
+
async addMessageToConversation(agentId, message, broadcast = true) {
|
|
776
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
777
|
+
if (!agent) return;
|
|
778
|
+
|
|
779
|
+
// Format message based on queue type
|
|
780
|
+
let formattedMessage;
|
|
781
|
+
|
|
782
|
+
switch (message.queueType) {
|
|
783
|
+
case 'toolResults': // Tool results
|
|
784
|
+
formattedMessage = {
|
|
785
|
+
...message,
|
|
786
|
+
role: 'tool',
|
|
787
|
+
content: this.formatToolResult(message)
|
|
788
|
+
};
|
|
789
|
+
break;
|
|
790
|
+
|
|
791
|
+
case 'interAgentMessages': // Inter-agent messages
|
|
792
|
+
formattedMessage = {
|
|
793
|
+
...message,
|
|
794
|
+
role: MESSAGE_ROLES.SYSTEM,
|
|
795
|
+
content: `Message from ${message.senderName || message.sender}: ${message.content}`
|
|
796
|
+
};
|
|
797
|
+
break;
|
|
798
|
+
|
|
799
|
+
case 'userMessages': // User messages
|
|
800
|
+
formattedMessage = {
|
|
801
|
+
...message,
|
|
802
|
+
role: MESSAGE_ROLES.USER
|
|
803
|
+
};
|
|
804
|
+
break;
|
|
805
|
+
|
|
806
|
+
default:
|
|
807
|
+
formattedMessage = message;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Add timestamp if not present
|
|
811
|
+
if (!formattedMessage.timestamp) {
|
|
812
|
+
formattedMessage.timestamp = new Date().toISOString();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// GUARD: Skip empty messages - they should never be added to history
|
|
816
|
+
const messageContent = formattedMessage.content;
|
|
817
|
+
if (!messageContent || (typeof messageContent === 'string' && !messageContent.trim())) {
|
|
818
|
+
this.logger.warn(`Skipping empty message for agent ${agentId}`, {
|
|
819
|
+
role: formattedMessage.role,
|
|
820
|
+
queueType: message.queueType,
|
|
821
|
+
hasContent: !!messageContent
|
|
822
|
+
});
|
|
823
|
+
return; // Don't add empty messages
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Add to conversation history
|
|
827
|
+
agent.conversations.full.messages.push(formattedMessage);
|
|
828
|
+
agent.conversations.full.lastUpdated = new Date().toISOString();
|
|
829
|
+
|
|
830
|
+
// Add to current model conversation if exists
|
|
831
|
+
if (agent.currentModel && agent.conversations[agent.currentModel]) {
|
|
832
|
+
agent.conversations[agent.currentModel].messages.push(formattedMessage);
|
|
833
|
+
agent.conversations[agent.currentModel].lastUpdated = new Date().toISOString();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// FIX: Only broadcast user-visible messages to UI (not internal system prompts)
|
|
837
|
+
if (broadcast && this.shouldBroadcastMessage(formattedMessage)) {
|
|
838
|
+
this.broadcastMessageUpdate(agentId, formattedMessage);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Process agent autonomously (for AGENT mode with no queued messages)
|
|
844
|
+
* @param {string} agentId - Agent ID
|
|
845
|
+
* @private
|
|
846
|
+
*/
|
|
847
|
+
async processAgentAutonomously(agentId) {
|
|
848
|
+
// Auto-mark the highest priority pending task as in-progress
|
|
849
|
+
await this.autoProgressHighestPriorityTask(agentId);
|
|
850
|
+
|
|
851
|
+
// Get AI response without new messages
|
|
852
|
+
const aiResponse = await this.getAgentAIResponse(agentId);
|
|
853
|
+
|
|
854
|
+
if (!aiResponse) {
|
|
855
|
+
return false; // No response - activity service will determine if we should continue
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Process AI response and execute tools
|
|
859
|
+
await this.processAIResponse(agentId, aiResponse);
|
|
860
|
+
|
|
861
|
+
// Clear token limit retry tracker on successful AI response
|
|
862
|
+
this.clearTokenLimitRetryTracker(agentId);
|
|
863
|
+
|
|
864
|
+
return true; // We got a new AI response
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Check if compaction is needed and perform it
|
|
869
|
+
* @param {string} agentId - Agent ID
|
|
870
|
+
* @param {string} targetModel - Target model for AI request
|
|
871
|
+
* @param {string} sessionId - Session ID for context
|
|
872
|
+
* @returns {Promise<Object>} Result with shouldContinue flag
|
|
873
|
+
* @private
|
|
874
|
+
*/
|
|
875
|
+
async checkAndPerformCompaction(agentId, targetModel, sessionId) {
|
|
876
|
+
const ENABLE_COMPACT_DEBUG = process.env.COMPACT_DEBUG === 'true';
|
|
877
|
+
|
|
878
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
879
|
+
console.log('[COMPACT-CHECK-START]', {
|
|
880
|
+
agentId,
|
|
881
|
+
targetModel,
|
|
882
|
+
sessionId,
|
|
883
|
+
timestamp: new Date().toISOString()
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
889
|
+
if (!agent) {
|
|
890
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
891
|
+
console.log('[COMPACT-ERROR]', { agentId, reason: 'Agent not found' });
|
|
892
|
+
}
|
|
893
|
+
return { shouldContinue: false, error: 'Agent not found' };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// For model switching, check the current model's conversation, not the target model's
|
|
897
|
+
// This handles scenarios where we're switching from a larger context to a smaller one
|
|
898
|
+
let modelToCheck = agent.currentModel && agent.currentModel !== targetModel
|
|
899
|
+
? agent.currentModel
|
|
900
|
+
: targetModel;
|
|
901
|
+
|
|
902
|
+
// DEFENSIVE: If modelToCheck is undefined, try to find a valid conversation key
|
|
903
|
+
if (!modelToCheck) {
|
|
904
|
+
this.logger.warn(`Agent ${agentId} has no currentModel or targetModel set, attempting to use available conversation`, {
|
|
905
|
+
agentId,
|
|
906
|
+
currentModel: agent.currentModel,
|
|
907
|
+
targetModel,
|
|
908
|
+
preferredModel: agent.preferredModel,
|
|
909
|
+
availableConversations: Object.keys(agent.conversations || {})
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Notify user via WebSocket
|
|
913
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
914
|
+
status: 'warning',
|
|
915
|
+
message: 'Agent model configuration issue detected - using fallback',
|
|
916
|
+
details: `Agent has no currentModel set. Using fallback model for compaction check.`,
|
|
917
|
+
agentName: agent.name
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// Try preferredModel first
|
|
921
|
+
if (agent.preferredModel && agent.conversations[agent.preferredModel]) {
|
|
922
|
+
modelToCheck = agent.preferredModel;
|
|
923
|
+
this.logger.info(`Using preferredModel as fallback for compaction check: ${modelToCheck}`);
|
|
924
|
+
} else {
|
|
925
|
+
// Find any non-'full' conversation key
|
|
926
|
+
const conversationKeys = Object.keys(agent.conversations || {}).filter(key => key !== 'full');
|
|
927
|
+
if (conversationKeys.length > 0) {
|
|
928
|
+
modelToCheck = conversationKeys[0];
|
|
929
|
+
this.logger.warn(`Using first available conversation key for compaction check: ${modelToCheck}`);
|
|
930
|
+
} else {
|
|
931
|
+
this.logger.error(`No valid conversation found for agent ${agentId}, skipping compaction`);
|
|
932
|
+
|
|
933
|
+
// Notify user of critical error
|
|
934
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
935
|
+
status: 'error',
|
|
936
|
+
message: 'No valid conversation found for compaction',
|
|
937
|
+
details: `Agent ${agent.name} has no valid conversation data. Compaction skipped.`,
|
|
938
|
+
agentName: agent.name
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
return { shouldContinue: true, error: 'No valid conversation found' };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Get conversation metadata
|
|
947
|
+
let metadata = await this.agentPool.getCompactionMetadata(agentId, modelToCheck);
|
|
948
|
+
|
|
949
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
950
|
+
console.log('[COMPACT-METADATA]', {
|
|
951
|
+
agentId,
|
|
952
|
+
modelToCheck,
|
|
953
|
+
hasMetadata: !!metadata,
|
|
954
|
+
isCompacted: metadata?.isCompacted,
|
|
955
|
+
originalMessageCount: metadata?.originalMessages?.length || 0,
|
|
956
|
+
compactedMessageCount: metadata?.compactedMessages?.length || 0,
|
|
957
|
+
lastCompactization: metadata?.lastCompactization || 'never',
|
|
958
|
+
compactizationCount: metadata?.compactizationCount || 0,
|
|
959
|
+
originalTokenCount: metadata?.originalTokenCount || 0,
|
|
960
|
+
compactedTokenCount: metadata?.compactedTokenCount || 0
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// If no conversation exists for this model yet, return early
|
|
965
|
+
if (!metadata || (!metadata.originalMessages && !metadata.compactedMessages)) {
|
|
966
|
+
this.logger.debug(`Compaction skipped: no conversation metadata for agent ${agentId}, model ${modelToCheck}`);
|
|
967
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
968
|
+
console.log('[COMPACT-SKIPPED]', { agentId, reason: 'No conversation metadata' });
|
|
969
|
+
}
|
|
970
|
+
return { shouldContinue: true }; // No conversation to compact
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// CRITICAL: Sync pending messages BEFORE token counting.
|
|
974
|
+
// Without this, the token count uses stale compactedMessages that miss new messages
|
|
975
|
+
// (tool results, user inputs) added since the last getMessagesForAI call.
|
|
976
|
+
// This was causing severe underestimation (e.g., 50K estimated vs 212K actual).
|
|
977
|
+
if (metadata.isCompacted) {
|
|
978
|
+
const earlySyncResult = await this.agentPool.syncPendingMessages(agentId, modelToCheck);
|
|
979
|
+
if (earlySyncResult.synced > 0) {
|
|
980
|
+
this.logger.info(`Pre-check sync: ${earlySyncResult.synced} pending messages synced for accurate token count`, {
|
|
981
|
+
agentId,
|
|
982
|
+
modelToCheck
|
|
983
|
+
});
|
|
984
|
+
// Re-fetch metadata to get updated compactedMessages
|
|
985
|
+
const updatedMetadata = await this.agentPool.getCompactionMetadata(agentId, modelToCheck);
|
|
986
|
+
metadata = updatedMetadata;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Determine which messages to use for token counting
|
|
991
|
+
let messages = metadata.isCompacted
|
|
992
|
+
? metadata.compactedMessages
|
|
993
|
+
: metadata.originalMessages;
|
|
994
|
+
|
|
995
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
996
|
+
console.log('[COMPACT-MESSAGES-SELECTED]', {
|
|
997
|
+
agentId,
|
|
998
|
+
selectedArray: metadata.isCompacted ? 'compactedMessages' : 'originalMessages',
|
|
999
|
+
messageCount: messages?.length || 0,
|
|
1000
|
+
reason: metadata.isCompacted ? 'Compaction exists, using compacted version' : 'No compaction yet, using original'
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Check if any messages are oversized — if so, always allow compaction
|
|
1005
|
+
// because the splitting logic inside compactConversation will create enough messages
|
|
1006
|
+
const hasOversizedMessages = messages && messages.some(m => {
|
|
1007
|
+
const content = typeof m.content === 'string' ? m.content : '';
|
|
1008
|
+
return content.length > COMPACTION_CONFIG.OVERSIZED_MESSAGE_THRESHOLD;
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
if (!hasOversizedMessages && (!messages || messages.length < COMPACTION_CONFIG.MIN_MESSAGES_FOR_COMPACTION)) {
|
|
1012
|
+
this.logger.debug(`Compaction skipped: too few messages (${messages?.length || 0}) for agent ${agentId}`);
|
|
1013
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
1014
|
+
console.log('[COMPACT-SKIPPED]', { agentId, reason: 'Too few messages', messageCount: messages?.length || 0, minRequired: COMPACTION_CONFIG.MIN_MESSAGES_FOR_COMPACTION });
|
|
1015
|
+
}
|
|
1016
|
+
return { shouldContinue: true }; // Conversation too short to compact
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Count current tokens using AI response metadata (system prompt included)
|
|
1020
|
+
const currentTokens = this.tokenCountingService.getConversationTokenCount(
|
|
1021
|
+
messages,
|
|
1022
|
+
targetModel,
|
|
1023
|
+
agent.systemPrompt
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
// Get model specifications
|
|
1027
|
+
const contextWindow = this.tokenCountingService.getModelContextWindow(targetModel);
|
|
1028
|
+
const maxOutputTokens = this.tokenCountingService.getModelMaxOutputTokens(targetModel);
|
|
1029
|
+
|
|
1030
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
1031
|
+
console.log('[COMPACT-TOKEN-COUNT]', {
|
|
1032
|
+
agentId,
|
|
1033
|
+
currentTokens,
|
|
1034
|
+
maxOutputTokens,
|
|
1035
|
+
contextWindow,
|
|
1036
|
+
model: targetModel,
|
|
1037
|
+
countingMode: 'response-data-based'
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Check if compaction is needed
|
|
1042
|
+
const threshold = agent.compactionThreshold || COMPACTION_CONFIG.DEFAULT_THRESHOLD;
|
|
1043
|
+
const shouldCompact = this.tokenCountingService.shouldTriggerCompaction(
|
|
1044
|
+
currentTokens,
|
|
1045
|
+
maxOutputTokens,
|
|
1046
|
+
contextWindow,
|
|
1047
|
+
threshold
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
1051
|
+
const requiredTokens = currentTokens + maxOutputTokens;
|
|
1052
|
+
const thresholdTokens = threshold * contextWindow;
|
|
1053
|
+
console.log('[COMPACT-TRIGGER-CHECK]', {
|
|
1054
|
+
agentId,
|
|
1055
|
+
currentTokens,
|
|
1056
|
+
maxOutputTokens,
|
|
1057
|
+
requiredTokens,
|
|
1058
|
+
contextWindow,
|
|
1059
|
+
threshold,
|
|
1060
|
+
thresholdTokens,
|
|
1061
|
+
shouldCompact,
|
|
1062
|
+
formula: `${currentTokens} + ${maxOutputTokens} = ${requiredTokens} ${shouldCompact ? '>=' : '<'} ${thresholdTokens} (${threshold * 100}% of ${contextWindow})`,
|
|
1063
|
+
decision: shouldCompact ? 'TRIGGER COMPACTION' : 'SKIP - below threshold'
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!shouldCompact) {
|
|
1068
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
1069
|
+
console.log('[COMPACT-SKIPPED]', { agentId, reason: 'Below threshold', utilizationPct: ((currentTokens + maxOutputTokens) / contextWindow * 100).toFixed(1) });
|
|
1070
|
+
}
|
|
1071
|
+
return { shouldContinue: true }; // No compaction needed
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
this.logger.info(`Compaction triggered for agent ${agentId}`, {
|
|
1075
|
+
currentTokens,
|
|
1076
|
+
contextWindow,
|
|
1077
|
+
threshold: `${(threshold * 100).toFixed(0)}%`,
|
|
1078
|
+
utilization: `${((currentTokens + maxOutputTokens) / contextWindow * 100).toFixed(1)}%`,
|
|
1079
|
+
targetModel
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
1083
|
+
console.log('[COMPACT-TRIGGERED]', {
|
|
1084
|
+
agentId,
|
|
1085
|
+
reason: 'Threshold exceeded',
|
|
1086
|
+
currentTokens,
|
|
1087
|
+
maxOutputTokens,
|
|
1088
|
+
requiredTokens: currentTokens + maxOutputTokens,
|
|
1089
|
+
contextWindow,
|
|
1090
|
+
threshold,
|
|
1091
|
+
utilizationPct: ((currentTokens + maxOutputTokens) / contextWindow * 100).toFixed(1)
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Mark compaction in progress
|
|
1096
|
+
this.compactionInProgress.set(agentId, COMPACTION_STATUS.STARTING);
|
|
1097
|
+
|
|
1098
|
+
// Broadcast compaction started event
|
|
1099
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
1100
|
+
status: COMPACTION_STATUS.STARTING,
|
|
1101
|
+
currentTokens,
|
|
1102
|
+
targetTokens: this.tokenCountingService.calculateTargetTokenCount(contextWindow),
|
|
1103
|
+
contextWindow,
|
|
1104
|
+
model: targetModel
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Always use summarization — multi-pass is handled inside compaction service
|
|
1108
|
+
const currentModel = agent.currentModel;
|
|
1109
|
+
const isModelSwitch = currentModel && currentModel !== targetModel;
|
|
1110
|
+
|
|
1111
|
+
// Update status to in-progress
|
|
1112
|
+
this.compactionInProgress.set(agentId, COMPACTION_STATUS.IN_PROGRESS);
|
|
1113
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
1114
|
+
status: COMPACTION_STATUS.IN_PROGRESS,
|
|
1115
|
+
strategy: COMPACTION_STRATEGIES.SUMMARIZATION,
|
|
1116
|
+
messageCount: messages.length
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Final sync before compaction: catch any messages that arrived AFTER the early pre-check
|
|
1120
|
+
// sync but BEFORE compaction starts (e.g., tool results completing during threshold decision).
|
|
1121
|
+
if (metadata.isCompacted) {
|
|
1122
|
+
const syncResult = await this.agentPool.syncPendingMessages(agentId, modelToCheck);
|
|
1123
|
+
if (syncResult.synced > 0) {
|
|
1124
|
+
this.logger.info(`Pre-compaction sync: ${syncResult.synced} additional messages synced`, {
|
|
1125
|
+
agentId,
|
|
1126
|
+
modelToCheck
|
|
1127
|
+
});
|
|
1128
|
+
const updatedMetadata = await this.agentPool.getCompactionMetadata(agentId, modelToCheck);
|
|
1129
|
+
messages = updatedMetadata.compactedMessages;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Record message count BEFORE compaction starts.
|
|
1134
|
+
// This is the watermark: messages at indices < this count are considered "already compacted".
|
|
1135
|
+
// Messages added DURING compaction (e.g., user messages via WebSocket while the
|
|
1136
|
+
// summarization API call is in flight) will be at indices >= this count, and will be
|
|
1137
|
+
// detected as "new" by getMessagesForAI's sync logic after compaction completes.
|
|
1138
|
+
const preCompactionMessageCount = agent.conversations[targetModel]?.messages?.length || 0;
|
|
1139
|
+
|
|
1140
|
+
// Gather compacted conversations for model-switch scenarios
|
|
1141
|
+
const compactedConversations = isModelSwitch
|
|
1142
|
+
? this._gatherCompactedConversations(agent)
|
|
1143
|
+
: null;
|
|
1144
|
+
|
|
1145
|
+
// Single call — multi-pass retry is now inside the compaction service
|
|
1146
|
+
const compactionResult = await this.compactionService.compactConversation(
|
|
1147
|
+
messages,
|
|
1148
|
+
currentModel || targetModel,
|
|
1149
|
+
targetModel,
|
|
1150
|
+
{
|
|
1151
|
+
targetTokenCount: this.tokenCountingService.calculateTargetTokenCount(contextWindow),
|
|
1152
|
+
sessionId,
|
|
1153
|
+
compactedConversations,
|
|
1154
|
+
onAllModelsExhausted: (errorInfo) => {
|
|
1155
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
1156
|
+
type: 'compaction_models_exhausted',
|
|
1157
|
+
status: 'warning',
|
|
1158
|
+
message: errorInfo.message,
|
|
1159
|
+
modelsAttempted: errorInfo.models,
|
|
1160
|
+
error: errorInfo.error
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
// Update AgentPool with compacted messages — pass pre-compaction watermark
|
|
1167
|
+
await this.agentPool.updateCompactedMessages(agentId, targetModel, compactionResult, preCompactionMessageCount);
|
|
1168
|
+
|
|
1169
|
+
if (ENABLE_COMPACT_DEBUG) {
|
|
1170
|
+
console.log('[COMPACT-COMPLETED]', {
|
|
1171
|
+
agentId,
|
|
1172
|
+
strategy: compactionResult.strategy,
|
|
1173
|
+
originalMessageCount: compactionResult.originalMessages?.length || 0,
|
|
1174
|
+
compactedMessageCount: compactionResult.compactedMessages?.length || 0,
|
|
1175
|
+
originalTokens: compactionResult.originalTokenCount,
|
|
1176
|
+
compactedTokens: compactionResult.compactedTokenCount,
|
|
1177
|
+
reductionPercent: compactionResult.reductionPercent.toFixed(1),
|
|
1178
|
+
executionTimeMs: compactionResult.executionTime,
|
|
1179
|
+
model: targetModel
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
this.logger.info(`Compaction completed for agent ${agentId}`, {
|
|
1184
|
+
strategy: compactionResult.strategy,
|
|
1185
|
+
originalTokens: compactionResult.originalTokenCount,
|
|
1186
|
+
compactedTokens: compactionResult.compactedTokenCount,
|
|
1187
|
+
reduction: `${compactionResult.reductionPercent.toFixed(1)}%`,
|
|
1188
|
+
executionTime: `${compactionResult.executionTime}ms`
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// Update status to completed
|
|
1192
|
+
this.compactionInProgress.delete(agentId);
|
|
1193
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
1194
|
+
status: COMPACTION_STATUS.COMPLETED,
|
|
1195
|
+
originalTokens: compactionResult.originalTokenCount,
|
|
1196
|
+
compactedTokens: compactionResult.compactedTokenCount,
|
|
1197
|
+
reductionPercent: compactionResult.reductionPercent,
|
|
1198
|
+
strategy: compactionResult.strategy,
|
|
1199
|
+
executionTime: compactionResult.executionTime
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
return { shouldContinue: true, compactionPerformed: true };
|
|
1203
|
+
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
this.logger.error(`Compaction failed for agent ${agentId}`, {
|
|
1206
|
+
error: error.message,
|
|
1207
|
+
stack: error.stack
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// Update status to failed
|
|
1211
|
+
this.compactionInProgress.delete(agentId);
|
|
1212
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
1213
|
+
status: COMPACTION_STATUS.FAILED,
|
|
1214
|
+
error: error.message
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Don't block AI request on compaction failure
|
|
1218
|
+
return { shouldContinue: true, error: error.message };
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Gather compacted conversations from all model conversations for an agent.
|
|
1224
|
+
* Used during model switching to find the best existing conversation.
|
|
1225
|
+
* @param {Object} agent - Agent object with conversations
|
|
1226
|
+
* @returns {Map|null} Map of modelId → compactedMessages, or null
|
|
1227
|
+
* @private
|
|
1228
|
+
*/
|
|
1229
|
+
_gatherCompactedConversations(agent) {
|
|
1230
|
+
if (!agent.conversations) return null;
|
|
1231
|
+
|
|
1232
|
+
const result = new Map();
|
|
1233
|
+
for (const [modelId, conv] of Object.entries(agent.conversations)) {
|
|
1234
|
+
if (modelId === 'full') continue;
|
|
1235
|
+
// Use compactizedMessages (correct field name) if available, otherwise messages
|
|
1236
|
+
const msgs = conv.compactizedMessages || conv.messages;
|
|
1237
|
+
if (Array.isArray(msgs) && msgs.length > 0) {
|
|
1238
|
+
result.set(modelId, msgs);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return result.size > 0 ? result : null;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Broadcast compaction event to UI
|
|
1247
|
+
* @param {string} agentId - Agent ID
|
|
1248
|
+
* @param {string} sessionId - Session ID
|
|
1249
|
+
* @param {Object} data - Event data
|
|
1250
|
+
* @private
|
|
1251
|
+
*/
|
|
1252
|
+
broadcastCompactionEvent(agentId, sessionId, data) {
|
|
1253
|
+
if (!this.webSocketManager || !this.webSocketManager.broadcastToSession) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1258
|
+
type: 'compaction_event',
|
|
1259
|
+
data: {
|
|
1260
|
+
agentId,
|
|
1261
|
+
timestamp: new Date().toISOString(),
|
|
1262
|
+
...data
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Get AI response for agent with proper error handling
|
|
1269
|
+
* @param {string} agentId - Agent ID
|
|
1270
|
+
* @returns {Promise<Object|null>} AI response or null if failed
|
|
1271
|
+
* @private
|
|
1272
|
+
*/
|
|
1273
|
+
async getAgentAIResponse(agentId) {
|
|
1274
|
+
try {
|
|
1275
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
1276
|
+
if (!agent) return null;
|
|
1277
|
+
|
|
1278
|
+
const conversationHistory = agent.conversations?.full?.messages || [];
|
|
1279
|
+
|
|
1280
|
+
// Get the session ID from the session map or agent's stored sessionId
|
|
1281
|
+
const sessionId = this.getAgentSession(agentId) || agent.sessionId;
|
|
1282
|
+
|
|
1283
|
+
if (!sessionId) {
|
|
1284
|
+
this.logger.error(`Agent ${agentId} has no session ID - API key resolution will fail`, {
|
|
1285
|
+
agentName: agent.name,
|
|
1286
|
+
agentSessionId: agent.sessionId,
|
|
1287
|
+
sessionMapHas: this.agentSessionMap.has(agentId)
|
|
1288
|
+
});
|
|
1289
|
+
// Return null to avoid making requests that will fail
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// DYNAMIC ROUTING: Check if agent has dynamic model routing enabled
|
|
1294
|
+
let targetModel = agent.currentModel;
|
|
1295
|
+
|
|
1296
|
+
// DEFENSIVE: Ensure targetModel is set, fallback to preferredModel if currentModel is undefined
|
|
1297
|
+
if (!targetModel) {
|
|
1298
|
+
this.logger.warn(`Agent ${agentId} has no currentModel set, using preferredModel as fallback`, {
|
|
1299
|
+
agentId,
|
|
1300
|
+
preferredModel: agent.preferredModel,
|
|
1301
|
+
availableConversations: Object.keys(agent.conversations || {})
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
// Notify user via WebSocket
|
|
1305
|
+
if (this.webSocketManager && sessionId) {
|
|
1306
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1307
|
+
type: 'agent_warning',
|
|
1308
|
+
agentId,
|
|
1309
|
+
agentName: agent.name,
|
|
1310
|
+
message: 'Agent model configuration restored',
|
|
1311
|
+
details: `Agent "${agent.name}" had no currentModel set. Automatically restored to ${agent.preferredModel || 'default model'}.`,
|
|
1312
|
+
severity: 'warning',
|
|
1313
|
+
timestamp: new Date().toISOString()
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
targetModel = agent.preferredModel;
|
|
1318
|
+
|
|
1319
|
+
// Update agent's currentModel to preferredModel for future iterations
|
|
1320
|
+
if (targetModel) {
|
|
1321
|
+
agent.currentModel = targetModel;
|
|
1322
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1323
|
+
this.logger.info(`Set agent currentModel to ${targetModel}`);
|
|
1324
|
+
} else {
|
|
1325
|
+
this.logger.error(`Agent ${agentId} has no preferredModel or currentModel, cannot continue`);
|
|
1326
|
+
|
|
1327
|
+
// Notify user of critical error
|
|
1328
|
+
if (this.webSocketManager && sessionId) {
|
|
1329
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1330
|
+
type: 'agent_error',
|
|
1331
|
+
agentId,
|
|
1332
|
+
agentName: agent.name,
|
|
1333
|
+
message: 'Agent model configuration error',
|
|
1334
|
+
details: `Agent "${agent.name}" has no valid model configuration. Cannot process messages.`,
|
|
1335
|
+
severity: 'error',
|
|
1336
|
+
timestamp: new Date().toISOString()
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (agent.dynamicModelRouting && this.modelRouterService) {
|
|
1345
|
+
try {
|
|
1346
|
+
// Get the last user message for routing decision
|
|
1347
|
+
const lastUserMessage = [...conversationHistory].reverse().find(m => m.role === 'user');
|
|
1348
|
+
|
|
1349
|
+
if (lastUserMessage) {
|
|
1350
|
+
// Get available models from ModelsService
|
|
1351
|
+
let availableModels = [];
|
|
1352
|
+
if (this.modelsService) {
|
|
1353
|
+
try {
|
|
1354
|
+
this.logger.debug('ModelsService type check', {
|
|
1355
|
+
hasModelsService: !!this.modelsService,
|
|
1356
|
+
type: typeof this.modelsService,
|
|
1357
|
+
methods: Object.getOwnPropertyNames(Object.getPrototypeOf(this.modelsService))
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// Try to get models, if empty/stale, fetch with current sessionId
|
|
1361
|
+
let allModels = this.modelsService.getModels();
|
|
1362
|
+
|
|
1363
|
+
// Check if models need refresh (safer check)
|
|
1364
|
+
const needsRefresh = !this.modelsService.lastFetched ||
|
|
1365
|
+
(this.modelsService.lastFetched &&
|
|
1366
|
+
(Date.now() - new Date(this.modelsService.lastFetched).getTime()) > (5 * 60 * 1000));
|
|
1367
|
+
|
|
1368
|
+
if (!allModels || allModels.length === 0 || needsRefresh) {
|
|
1369
|
+
this.logger.info('Models list empty or stale, fetching from backend with sessionId');
|
|
1370
|
+
await this.modelsService.fetchModels({ sessionId });
|
|
1371
|
+
allModels = this.modelsService.getModels();
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Filter to only include models that are available (not router models)
|
|
1375
|
+
availableModels = allModels
|
|
1376
|
+
.filter(model => model.id !== 'autopilot-model-router' && model.name !== 'autopilot-model-router');
|
|
1377
|
+
|
|
1378
|
+
this.logger.debug(`Available models for routing: ${availableModels.map(m => m.id || m.name).join(', ')}`);
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
this.logger.warn(`Failed to get available models for routing: ${error.message}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const routingResult = await this.modelRouterService.routeMessage(
|
|
1385
|
+
lastUserMessage.content,
|
|
1386
|
+
conversationHistory.slice(-5), // Last 5 messages for context
|
|
1387
|
+
agent.currentModel,
|
|
1388
|
+
availableModels, // Pass actual available models
|
|
1389
|
+
{ agentId, sessionId, routingStrategy: agent.routingStrategy }
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
this.logger.info('Routing result analysis', {
|
|
1393
|
+
selectedModel: routingResult.selectedModel,
|
|
1394
|
+
currentModel: agent.currentModel,
|
|
1395
|
+
areEqual: routingResult.selectedModel === agent.currentModel,
|
|
1396
|
+
willSwitch: routingResult.selectedModel && routingResult.selectedModel !== agent.currentModel
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
if (routingResult.selectedModel && routingResult.selectedModel !== agent.currentModel) {
|
|
1400
|
+
this.logger.info(`Dynamic routing: switching from ${agent.currentModel} to ${routingResult.selectedModel}`, {
|
|
1401
|
+
agentId,
|
|
1402
|
+
reason: routingResult.reasoning
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
targetModel = routingResult.selectedModel;
|
|
1406
|
+
|
|
1407
|
+
// Update agent's current model
|
|
1408
|
+
agent.currentModel = targetModel;
|
|
1409
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1410
|
+
|
|
1411
|
+
this.logger.info(`Model updated: targetModel=${targetModel}, agent.currentModel=${agent.currentModel}`);
|
|
1412
|
+
} else {
|
|
1413
|
+
this.logger.info('No model switch needed', {
|
|
1414
|
+
selectedModel: routingResult.selectedModel,
|
|
1415
|
+
currentModel: agent.currentModel,
|
|
1416
|
+
hasSelectedModel: !!routingResult.selectedModel
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} catch (routingError) {
|
|
1421
|
+
this.logger.warn(`Dynamic routing failed, using current model: ${routingError.message}`);
|
|
1422
|
+
// Fall back to current model on routing failure
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
this.logger.info(`About to send message to model: ${targetModel}`, {
|
|
1427
|
+
agentId,
|
|
1428
|
+
targetModel,
|
|
1429
|
+
originalModel: agent.currentModel
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// PHASE 4: Check and perform compaction if needed BEFORE sending to AI
|
|
1433
|
+
const compactionResult = await this.checkAndPerformCompaction(agentId, targetModel, sessionId);
|
|
1434
|
+
|
|
1435
|
+
if (!compactionResult.shouldContinue) {
|
|
1436
|
+
this.logger.warn(`Compaction check returned shouldContinue=false for agent ${agentId}`);
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (compactionResult.compactionPerformed) {
|
|
1441
|
+
this.logger.info(`Compaction performed for agent ${agentId}, proceeding with AI request`);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// After compaction, retrieve messages from AgentPool (will use compacted if available)
|
|
1445
|
+
const messagesToSend = await this.agentPool.getMessagesForAI(agentId, targetModel);
|
|
1446
|
+
|
|
1447
|
+
// Inject TaskManager instructions for AGENT mode
|
|
1448
|
+
let enhancedSystemPrompt = agent.systemPrompt;
|
|
1449
|
+
if (agent.mode === AGENT_MODES.AGENT) {
|
|
1450
|
+
const taskManagerInstruction = "\n\nIMPORTANT: You are in AGENT mode. The use of TaskManager tool is mandatory. Always create and update tasks to track your work progress. Update the user about task-list status periodically. Use the jobdone tool when your main task is complete. While in agent mode: no thank-you's, no compliments, no rhetorical questions, no self-commentary - stay focused on executing tasks. Only ask questions through dedicated tools designed for user interaction (if available).";
|
|
1451
|
+
enhancedSystemPrompt = (agent.systemPrompt || '') + taskManagerInstruction;
|
|
1452
|
+
|
|
1453
|
+
// Note: Consecutive no-tool reminders are now sent as tool results (see _processAIResponse)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Inject dynamic file attachment context
|
|
1457
|
+
try {
|
|
1458
|
+
const fileAttachmentContext = await this.contextInjectionService.buildDynamicContext(agentId);
|
|
1459
|
+
if (fileAttachmentContext) {
|
|
1460
|
+
enhancedSystemPrompt = (enhancedSystemPrompt || '') + fileAttachmentContext;
|
|
1461
|
+
this.logger.debug(`Injected file attachment context for agent ${agentId}`, {
|
|
1462
|
+
contextLength: fileAttachmentContext.length
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
this.logger.warn(`Failed to inject file attachment context for agent ${agentId}`, {
|
|
1467
|
+
error: error.message
|
|
1468
|
+
});
|
|
1469
|
+
// Continue without file attachments if service fails
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Inject system environment constraints (reserved ports, process safety)
|
|
1473
|
+
const systemConstraints = this.contextInjectionService.buildSystemConstraints();
|
|
1474
|
+
if (systemConstraints) {
|
|
1475
|
+
enhancedSystemPrompt = (enhancedSystemPrompt || '') + systemConstraints;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Inject flow execution context if this is part of a flow
|
|
1479
|
+
try {
|
|
1480
|
+
const lastUserMsg = [...conversationHistory].reverse().find(m => m.role === 'user');
|
|
1481
|
+
if (lastUserMsg?.isFlowExecution && lastUserMsg?.flowMetadata) {
|
|
1482
|
+
const flowContext = this.flowContextService.buildFlowAgentContext(
|
|
1483
|
+
lastUserMsg.flowMetadata,
|
|
1484
|
+
lastUserMsg.previousAgentData
|
|
1485
|
+
);
|
|
1486
|
+
|
|
1487
|
+
if (flowContext) {
|
|
1488
|
+
enhancedSystemPrompt = (enhancedSystemPrompt || '') + flowContext;
|
|
1489
|
+
this.logger.info(`Injected flow execution context for agent ${agentId}`, {
|
|
1490
|
+
flowName: lastUserMsg.flowMetadata.flowName,
|
|
1491
|
+
nodePosition: `${lastUserMsg.flowMetadata.nodePosition}/${lastUserMsg.flowMetadata.totalNodes}`,
|
|
1492
|
+
hasPreviousAgent: !!lastUserMsg.previousAgentData
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
this.logger.warn(`Failed to inject flow execution context for agent ${agentId}`, {
|
|
1498
|
+
error: error.message
|
|
1499
|
+
});
|
|
1500
|
+
// Continue without flow context if service fails
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Check if streaming is enabled - consider both agent config and user message preference
|
|
1504
|
+
// Get the last user message to check for streaming preference
|
|
1505
|
+
const lastUserMsg = [...conversationHistory].reverse().find(m => m.role === 'user');
|
|
1506
|
+
const userStreamingPref = lastUserMsg?.streamingEnabled;
|
|
1507
|
+
// Use user preference if explicitly set, otherwise use agent config
|
|
1508
|
+
const streamingEnabled = userStreamingPref !== undefined
|
|
1509
|
+
? userStreamingPref !== false
|
|
1510
|
+
: agent.streamingEnabled !== false; // Default to true
|
|
1511
|
+
|
|
1512
|
+
if (streamingEnabled && this.aiService.sendMessageStream) {
|
|
1513
|
+
// Build flow context if this is part of a flow execution
|
|
1514
|
+
const flowContext = lastUserMsg?.isFlowExecution ? {
|
|
1515
|
+
flowRunId: lastUserMsg.flowRunId,
|
|
1516
|
+
flowNodeId: lastUserMsg.flowNodeId
|
|
1517
|
+
} : null;
|
|
1518
|
+
|
|
1519
|
+
// Use streaming response
|
|
1520
|
+
return await this._getStreamingResponse(
|
|
1521
|
+
agentId,
|
|
1522
|
+
targetModel,
|
|
1523
|
+
messagesToSend,
|
|
1524
|
+
enhancedSystemPrompt,
|
|
1525
|
+
sessionId,
|
|
1526
|
+
agent.platformProvided,
|
|
1527
|
+
flowContext
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Non-streaming fallback
|
|
1532
|
+
const response = await this.aiService.sendMessage(
|
|
1533
|
+
targetModel,
|
|
1534
|
+
messagesToSend,
|
|
1535
|
+
{
|
|
1536
|
+
agentId: agentId,
|
|
1537
|
+
systemPrompt: enhancedSystemPrompt,
|
|
1538
|
+
sessionId: sessionId,
|
|
1539
|
+
platformProvided: agent.platformProvided
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
|
|
1543
|
+
return response;
|
|
1544
|
+
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
this.logger.error(`AI response failed for agent ${agentId}:`, error);
|
|
1547
|
+
|
|
1548
|
+
// Handle different types of AI service failures
|
|
1549
|
+
await this.handleAIServiceFailure(agentId, error);
|
|
1550
|
+
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Get AI response using streaming with WebSocket broadcast
|
|
1557
|
+
* @param {string} agentId - Agent ID
|
|
1558
|
+
* @param {string} targetModel - Model to use
|
|
1559
|
+
* @param {Array} messagesToSend - Messages to send
|
|
1560
|
+
* @param {string} systemPrompt - System prompt
|
|
1561
|
+
* @param {string} sessionId - Session ID for WebSocket
|
|
1562
|
+
* @param {boolean} platformProvided - Whether using platform keys
|
|
1563
|
+
* @param {Object} flowContext - Optional flow execution context
|
|
1564
|
+
* @returns {Promise<Object>} Response object
|
|
1565
|
+
* @private
|
|
1566
|
+
*/
|
|
1567
|
+
async _getStreamingResponse(agentId, targetModel, messagesToSend, systemPrompt, sessionId, platformProvided, flowContext = null) {
|
|
1568
|
+
// Generate a unique message ID for this streaming response
|
|
1569
|
+
const streamMessageId = `stream-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1570
|
+
|
|
1571
|
+
// Flow progress tracking
|
|
1572
|
+
let flowProgress = null;
|
|
1573
|
+
if (flowContext) {
|
|
1574
|
+
flowProgress = {
|
|
1575
|
+
charactersStreamed: 0,
|
|
1576
|
+
chunkCount: 0,
|
|
1577
|
+
lastBroadcast: Date.now(),
|
|
1578
|
+
flowRunId: flowContext.flowRunId,
|
|
1579
|
+
flowNodeId: flowContext.flowNodeId
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// Broadcast stream start event
|
|
1584
|
+
this._broadcastStreamEvent(sessionId, {
|
|
1585
|
+
type: 'stream_start',
|
|
1586
|
+
agentId,
|
|
1587
|
+
messageId: streamMessageId,
|
|
1588
|
+
model: targetModel,
|
|
1589
|
+
timestamp: new Date().toISOString(),
|
|
1590
|
+
// Include flow context if present
|
|
1591
|
+
...(flowContext && {
|
|
1592
|
+
flowRunId: flowContext.flowRunId,
|
|
1593
|
+
flowNodeId: flowContext.flowNodeId
|
|
1594
|
+
})
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
try {
|
|
1598
|
+
const response = await this.aiService.sendMessageStream(
|
|
1599
|
+
targetModel,
|
|
1600
|
+
messagesToSend,
|
|
1601
|
+
{
|
|
1602
|
+
agentId: agentId,
|
|
1603
|
+
systemPrompt: systemPrompt,
|
|
1604
|
+
sessionId: sessionId,
|
|
1605
|
+
platformProvided: platformProvided,
|
|
1606
|
+
onChunk: (chunk) => {
|
|
1607
|
+
// Update flow progress if in flow execution
|
|
1608
|
+
if (flowProgress) {
|
|
1609
|
+
flowProgress.charactersStreamed += chunk.length;
|
|
1610
|
+
flowProgress.chunkCount++;
|
|
1611
|
+
|
|
1612
|
+
// Broadcast flow progress every 500ms or 50 chunks
|
|
1613
|
+
const now = Date.now();
|
|
1614
|
+
if (now - flowProgress.lastBroadcast > 500 || flowProgress.chunkCount % 50 === 0) {
|
|
1615
|
+
this._broadcastFlowProgress(sessionId, agentId, flowProgress);
|
|
1616
|
+
flowProgress.lastBroadcast = now;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Broadcast each chunk to the UI
|
|
1621
|
+
this._broadcastStreamEvent(sessionId, {
|
|
1622
|
+
type: 'stream_chunk',
|
|
1623
|
+
agentId,
|
|
1624
|
+
messageId: streamMessageId,
|
|
1625
|
+
content: chunk,
|
|
1626
|
+
timestamp: new Date().toISOString(),
|
|
1627
|
+
// Include flow context if present
|
|
1628
|
+
...(flowContext && {
|
|
1629
|
+
flowRunId: flowContext.flowRunId,
|
|
1630
|
+
flowNodeId: flowContext.flowNodeId
|
|
1631
|
+
})
|
|
1632
|
+
});
|
|
1633
|
+
},
|
|
1634
|
+
onDone: (result) => {
|
|
1635
|
+
// Final flow progress broadcast
|
|
1636
|
+
if (flowProgress) {
|
|
1637
|
+
this._broadcastFlowProgress(sessionId, agentId, flowProgress, true);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Broadcast stream completion
|
|
1641
|
+
this._broadcastStreamEvent(sessionId, {
|
|
1642
|
+
type: 'stream_complete',
|
|
1643
|
+
agentId,
|
|
1644
|
+
messageId: streamMessageId,
|
|
1645
|
+
content: result.content,
|
|
1646
|
+
usage: result.usage,
|
|
1647
|
+
model: result.model || targetModel,
|
|
1648
|
+
finishReason: result.finishReason,
|
|
1649
|
+
timestamp: new Date().toISOString(),
|
|
1650
|
+
// Include flow context if present
|
|
1651
|
+
...(flowContext && {
|
|
1652
|
+
flowRunId: flowContext.flowRunId,
|
|
1653
|
+
flowNodeId: flowContext.flowNodeId
|
|
1654
|
+
})
|
|
1655
|
+
});
|
|
1656
|
+
},
|
|
1657
|
+
onError: (error) => {
|
|
1658
|
+
// Check if this is a model-related error that should show suggestions
|
|
1659
|
+
const isModelError = this.aiService?.isModelRelatedError?.(error);
|
|
1660
|
+
const modelSuggestions = isModelError && this.aiService?.getModelSuggestions?.(targetModel, error);
|
|
1661
|
+
|
|
1662
|
+
// Broadcast stream error with model suggestions if applicable
|
|
1663
|
+
this._broadcastStreamEvent(sessionId, {
|
|
1664
|
+
type: isModelError ? 'model_error' : 'stream_error',
|
|
1665
|
+
agentId,
|
|
1666
|
+
messageId: streamMessageId,
|
|
1667
|
+
error: error.message,
|
|
1668
|
+
model: targetModel,
|
|
1669
|
+
timestamp: new Date().toISOString(),
|
|
1670
|
+
// Include model suggestions for model-related errors
|
|
1671
|
+
...(modelSuggestions && { modelSuggestions }),
|
|
1672
|
+
// Include flow context if present
|
|
1673
|
+
...(flowContext && {
|
|
1674
|
+
flowRunId: flowContext.flowRunId,
|
|
1675
|
+
flowNodeId: flowContext.flowNodeId
|
|
1676
|
+
})
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
);
|
|
1681
|
+
|
|
1682
|
+
this.logger.info(`Streaming response completed for agent ${agentId}`, {
|
|
1683
|
+
contentLength: response.content?.length || 0,
|
|
1684
|
+
model: response.model,
|
|
1685
|
+
...(flowProgress && { flowCharsStreamed: flowProgress.charactersStreamed })
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
return response;
|
|
1689
|
+
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
// Check if this is a model-related error that should show suggestions
|
|
1692
|
+
const isModelError = this.aiService?.isModelRelatedError?.(error);
|
|
1693
|
+
const modelSuggestions = isModelError && this.aiService?.getModelSuggestions?.(targetModel, error);
|
|
1694
|
+
|
|
1695
|
+
// Broadcast error event with model suggestions if applicable
|
|
1696
|
+
this._broadcastStreamEvent(sessionId, {
|
|
1697
|
+
type: isModelError ? 'model_error' : 'stream_error',
|
|
1698
|
+
agentId,
|
|
1699
|
+
messageId: streamMessageId,
|
|
1700
|
+
error: error.message,
|
|
1701
|
+
model: targetModel,
|
|
1702
|
+
timestamp: new Date().toISOString(),
|
|
1703
|
+
// Include model suggestions for model-related errors
|
|
1704
|
+
...(modelSuggestions && { modelSuggestions }),
|
|
1705
|
+
// Include flow context if present
|
|
1706
|
+
...(flowContext && {
|
|
1707
|
+
flowRunId: flowContext.flowRunId,
|
|
1708
|
+
flowNodeId: flowContext.flowNodeId
|
|
1709
|
+
})
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Broadcast flow node progress event
|
|
1718
|
+
* @param {string} sessionId - Session ID
|
|
1719
|
+
* @param {string} agentId - Agent ID
|
|
1720
|
+
* @param {Object} progress - Progress data
|
|
1721
|
+
* @param {boolean} isFinal - Whether this is the final progress update
|
|
1722
|
+
* @private
|
|
1723
|
+
*/
|
|
1724
|
+
_broadcastFlowProgress(sessionId, agentId, progress, isFinal = false) {
|
|
1725
|
+
if (!this.webSocketManager) return;
|
|
1726
|
+
|
|
1727
|
+
try {
|
|
1728
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1729
|
+
type: 'flow_update',
|
|
1730
|
+
data: {
|
|
1731
|
+
type: 'flow_node_progress',
|
|
1732
|
+
runId: progress.flowRunId,
|
|
1733
|
+
nodeId: progress.flowNodeId,
|
|
1734
|
+
agentId,
|
|
1735
|
+
charactersStreamed: progress.charactersStreamed,
|
|
1736
|
+
chunkCount: progress.chunkCount,
|
|
1737
|
+
isFinal,
|
|
1738
|
+
timestamp: new Date().toISOString()
|
|
1739
|
+
},
|
|
1740
|
+
timestamp: new Date().toISOString()
|
|
1741
|
+
});
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
this.logger.warn('Failed to broadcast flow progress', { error: error.message });
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Broadcast streaming event via WebSocket
|
|
1749
|
+
* @param {string} sessionId - Session ID
|
|
1750
|
+
* @param {Object} eventData - Event data to broadcast
|
|
1751
|
+
* @private
|
|
1752
|
+
*/
|
|
1753
|
+
_broadcastStreamEvent(sessionId, eventData) {
|
|
1754
|
+
if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
1755
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1756
|
+
type: eventData.type,
|
|
1757
|
+
data: eventData
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Handle AI service failures with appropriate recovery strategies
|
|
1764
|
+
* @param {string} agentId - Agent ID that failed
|
|
1765
|
+
* @param {Error} error - The error that occurred
|
|
1766
|
+
* @private
|
|
1767
|
+
*/
|
|
1768
|
+
async handleAIServiceFailure(agentId, error) {
|
|
1769
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
1770
|
+
if (!agent) return;
|
|
1771
|
+
|
|
1772
|
+
const sessionId = this.getAgentSession(agentId) || agent.sessionId || 'scheduler-session';
|
|
1773
|
+
|
|
1774
|
+
// PRIORITY: Handle timeout errors that should return to chat mode
|
|
1775
|
+
if (error.shouldReturnToChat || error.isTimeout) {
|
|
1776
|
+
this.logger.warn(`Agent ${agentId} returning to chat mode due to timeout`, {
|
|
1777
|
+
errorMessage: error.message,
|
|
1778
|
+
status: error.status
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
// Switch agent to CHAT mode - no delay, user can retry when ready
|
|
1782
|
+
agent.mode = AGENT_MODES.CHAT;
|
|
1783
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1784
|
+
|
|
1785
|
+
// Broadcast timeout notification to UI (toast + console log only, no chat message)
|
|
1786
|
+
if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
1787
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1788
|
+
type: 'agent_timeout',
|
|
1789
|
+
data: {
|
|
1790
|
+
agentId: agentId,
|
|
1791
|
+
agentName: agent.name,
|
|
1792
|
+
action: 'returned_to_chat',
|
|
1793
|
+
timestamp: new Date().toISOString()
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
return; // Don't proceed with other error handling
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Determine failure type and response
|
|
1802
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
1803
|
+
|
|
1804
|
+
if (errorMessage.includes('api key') || errorMessage.includes('authentication')) {
|
|
1805
|
+
// API key issues - pause agent to prevent infinite retries
|
|
1806
|
+
this.logger.warn(`Agent ${agentId} paused due to API key issue`);
|
|
1807
|
+
|
|
1808
|
+
agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.API_KEY_ERROR_DELAY_MS).toISOString();
|
|
1809
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1810
|
+
|
|
1811
|
+
// Add error message to agent's queue
|
|
1812
|
+
await this.agentPool.addToolResult(agentId, {
|
|
1813
|
+
toolId: 'system-error',
|
|
1814
|
+
status: 'failed',
|
|
1815
|
+
error: 'API key authentication failed. Please check your API key configuration in Settings.',
|
|
1816
|
+
timestamp: new Date().toISOString()
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
} else if (errorMessage.includes('rate limit') || errorMessage.includes('too many requests')) {
|
|
1820
|
+
// Rate limit - delay agent
|
|
1821
|
+
this.logger.warn(`Agent ${agentId} delayed due to rate limiting`);
|
|
1822
|
+
|
|
1823
|
+
agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.RATE_LIMIT_DELAY_MS).toISOString();
|
|
1824
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1825
|
+
|
|
1826
|
+
} else if (errorMessage.includes('network') || errorMessage.includes('connection')) {
|
|
1827
|
+
// Network issues (non-timeout) - shorter delay and retry
|
|
1828
|
+
this.logger.warn(`Agent ${agentId} delayed due to network issues`);
|
|
1829
|
+
|
|
1830
|
+
agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.NETWORK_ERROR_DELAY_MS).toISOString();
|
|
1831
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1832
|
+
|
|
1833
|
+
} else if (this.isTokenLimitError(errorMessage)) {
|
|
1834
|
+
// Token/context limit error - trigger emergency compaction and retry
|
|
1835
|
+
await this.handleTokenLimitError(agentId, agent, error);
|
|
1836
|
+
return; // Don't add error message or broadcast - will retry after compaction
|
|
1837
|
+
|
|
1838
|
+
} else {
|
|
1839
|
+
// Unknown error - pause agent and notify
|
|
1840
|
+
this.logger.error(`Agent ${agentId} paused due to unknown AI service error: ${error.message}`);
|
|
1841
|
+
|
|
1842
|
+
agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.UNKNOWN_ERROR_DELAY_MS).toISOString();
|
|
1843
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1844
|
+
|
|
1845
|
+
// Add error message to agent's queue
|
|
1846
|
+
await this.agentPool.addToolResult(agentId, {
|
|
1847
|
+
toolId: 'system-error',
|
|
1848
|
+
status: 'failed',
|
|
1849
|
+
error: `AI service error: ${error.message}. Agent temporarily paused.`,
|
|
1850
|
+
timestamp: new Date().toISOString()
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Broadcast error to UI
|
|
1855
|
+
if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
1856
|
+
const sessionId = this.getAgentSession(agentId) || agent.sessionId || 'scheduler-session';
|
|
1857
|
+
|
|
1858
|
+
// FIX: Wrap payload in 'data' field to match UI expectations
|
|
1859
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1860
|
+
type: 'agent_error',
|
|
1861
|
+
data: {
|
|
1862
|
+
agentId: agentId,
|
|
1863
|
+
error: error.message,
|
|
1864
|
+
recovery: 'Agent temporarily paused for recovery',
|
|
1865
|
+
timestamp: new Date().toISOString()
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Check if error message indicates a token/context limit error
|
|
1873
|
+
* @param {string} errorMessage - Lowercased error message
|
|
1874
|
+
* @returns {boolean}
|
|
1875
|
+
* @private
|
|
1876
|
+
*/
|
|
1877
|
+
isTokenLimitError(errorMessage) {
|
|
1878
|
+
const tokenLimitPatterns = [
|
|
1879
|
+
'prompt is too long',
|
|
1880
|
+
'tokens',
|
|
1881
|
+
'context length',
|
|
1882
|
+
'context window',
|
|
1883
|
+
'maximum context',
|
|
1884
|
+
'token limit',
|
|
1885
|
+
'max_tokens',
|
|
1886
|
+
'context_length_exceeded',
|
|
1887
|
+
'maximum.*exceeded'
|
|
1888
|
+
];
|
|
1889
|
+
|
|
1890
|
+
return tokenLimitPatterns.some(pattern => {
|
|
1891
|
+
if (pattern.includes('.*')) {
|
|
1892
|
+
return new RegExp(pattern).test(errorMessage);
|
|
1893
|
+
}
|
|
1894
|
+
return errorMessage.includes(pattern);
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Handle token limit errors with emergency compaction and retry
|
|
1900
|
+
* @param {string} agentId - Agent ID
|
|
1901
|
+
* @param {Object} agent - Agent object
|
|
1902
|
+
* @param {Error} error - The token limit error
|
|
1903
|
+
* @private
|
|
1904
|
+
*/
|
|
1905
|
+
async handleTokenLimitError(agentId, agent, error) {
|
|
1906
|
+
const sessionId = this.getAgentSession(agentId) || agent.sessionId || 'scheduler-session';
|
|
1907
|
+
|
|
1908
|
+
// Get or initialize retry tracker for this agent
|
|
1909
|
+
let tracker = this.tokenLimitRetryTracker.get(agentId);
|
|
1910
|
+
if (!tracker) {
|
|
1911
|
+
tracker = { attempts: 0, lastError: '', timestamp: new Date() };
|
|
1912
|
+
this.tokenLimitRetryTracker.set(agentId, tracker);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// Increment retry count
|
|
1916
|
+
tracker.attempts++;
|
|
1917
|
+
tracker.lastError = error.message;
|
|
1918
|
+
tracker.timestamp = new Date();
|
|
1919
|
+
|
|
1920
|
+
this.logger.warn(`Token limit error for agent ${agentId} (attempt ${tracker.attempts}/${this.MAX_TOKEN_LIMIT_RETRIES})`, {
|
|
1921
|
+
error: error.message,
|
|
1922
|
+
agentName: agent.name
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// Check if we've exceeded max retries
|
|
1926
|
+
if (tracker.attempts > this.MAX_TOKEN_LIMIT_RETRIES) {
|
|
1927
|
+
this.logger.error(`Agent ${agentId} exceeded max token limit retries, pausing agent`, {
|
|
1928
|
+
attempts: tracker.attempts,
|
|
1929
|
+
lastError: error.message
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
// Clear the retry tracker
|
|
1933
|
+
this.tokenLimitRetryTracker.delete(agentId);
|
|
1934
|
+
|
|
1935
|
+
// Now show the error to the user
|
|
1936
|
+
agent.delayEndTime = new Date(Date.now() + SCHEDULER_CONFIG.UNKNOWN_ERROR_DELAY_MS).toISOString();
|
|
1937
|
+
await this.agentPool.persistAgentState(agentId);
|
|
1938
|
+
|
|
1939
|
+
// Broadcast error to UI
|
|
1940
|
+
if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
1941
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
1942
|
+
type: 'agent_error',
|
|
1943
|
+
data: {
|
|
1944
|
+
agentId: agentId,
|
|
1945
|
+
error: `Context limit exceeded after ${this.MAX_TOKEN_LIMIT_RETRIES} compaction attempts. The conversation may be too large. Consider starting a new conversation.`,
|
|
1946
|
+
recovery: 'Agent temporarily paused',
|
|
1947
|
+
timestamp: new Date().toISOString()
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Add a user-friendly error message to the queue
|
|
1953
|
+
await this.agentPool.addToolResult(agentId, {
|
|
1954
|
+
toolId: 'system-error',
|
|
1955
|
+
status: 'failed',
|
|
1956
|
+
error: `Context limit exceeded after automatic compaction. The conversation is too large for the current model. Please consider clearing the conversation or switching to a model with larger context window.`,
|
|
1957
|
+
timestamp: new Date().toISOString()
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Determine compaction strategy based on attempt number
|
|
1964
|
+
// Attempt 1: Regular compaction (multi-pass summarization)
|
|
1965
|
+
// Attempt 2: Emergency compaction (summarization with lower min messages)
|
|
1966
|
+
const useEmergencyCompaction = tracker.attempts >= 2;
|
|
1967
|
+
const strategyName = useEmergencyCompaction ? 'emergency' : 'regular';
|
|
1968
|
+
|
|
1969
|
+
this.logger.info(`Triggering ${strategyName} compaction for agent ${agentId} due to token limit error`, {
|
|
1970
|
+
attempt: tracker.attempts,
|
|
1971
|
+
maxAttempts: this.MAX_TOKEN_LIMIT_RETRIES,
|
|
1972
|
+
strategy: strategyName
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
// Notify UI that compaction is happening
|
|
1976
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
1977
|
+
status: COMPACTION_STATUS.STARTING,
|
|
1978
|
+
message: useEmergencyCompaction
|
|
1979
|
+
? 'Emergency compaction triggered'
|
|
1980
|
+
: 'Compaction triggered due to context limit',
|
|
1981
|
+
emergency: useEmergencyCompaction,
|
|
1982
|
+
retryAttempt: tracker.attempts,
|
|
1983
|
+
timestamp: new Date().toISOString()
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
try {
|
|
1987
|
+
const targetModel = agent.currentModel || agent.preferredModel;
|
|
1988
|
+
let compactionResult;
|
|
1989
|
+
|
|
1990
|
+
if (useEmergencyCompaction) {
|
|
1991
|
+
// Second attempt: emergency compaction with lower minimum messages
|
|
1992
|
+
compactionResult = await this.performEmergencyCompaction(agentId, targetModel, sessionId);
|
|
1993
|
+
} else {
|
|
1994
|
+
// First attempt: regular multi-pass compaction
|
|
1995
|
+
compactionResult = await this.checkAndPerformCompaction(agentId, targetModel, sessionId);
|
|
1996
|
+
// Convert the result format to match emergency compaction output
|
|
1997
|
+
if (compactionResult.compactionPerformed) {
|
|
1998
|
+
compactionResult = {
|
|
1999
|
+
success: true,
|
|
2000
|
+
...compactionResult
|
|
2001
|
+
};
|
|
2002
|
+
} else if (compactionResult.shouldContinue) {
|
|
2003
|
+
compactionResult = { success: true, skipped: true };
|
|
2004
|
+
} else {
|
|
2005
|
+
compactionResult = { success: false, error: compactionResult.error };
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (compactionResult.success) {
|
|
2010
|
+
if (compactionResult.skipped) {
|
|
2011
|
+
this.logger.info(`Regular compaction skipped for agent ${agentId} (not needed or too few messages)`);
|
|
2012
|
+
} else {
|
|
2013
|
+
this.logger.info(`${strategyName} compaction successful for agent ${agentId}`, {
|
|
2014
|
+
reductionPercent: compactionResult.reductionPercent,
|
|
2015
|
+
originalTokens: compactionResult.originalTokenCount,
|
|
2016
|
+
compactedTokens: compactionResult.compactedTokenCount
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Broadcast successful compaction
|
|
2021
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
2022
|
+
status: COMPACTION_STATUS.COMPLETED,
|
|
2023
|
+
originalTokens: compactionResult.originalTokenCount,
|
|
2024
|
+
compactedTokens: compactionResult.compactedTokenCount,
|
|
2025
|
+
reductionPercent: compactionResult.reductionPercent,
|
|
2026
|
+
strategy: useEmergencyCompaction ? 'emergency_aggressive' : 'regular',
|
|
2027
|
+
emergency: useEmergencyCompaction,
|
|
2028
|
+
message: compactionResult.skipped
|
|
2029
|
+
? 'Compaction check complete'
|
|
2030
|
+
: `Compaction complete. Reduced by ${compactionResult.reductionPercent?.toFixed(1) || 0}%`,
|
|
2031
|
+
timestamp: new Date().toISOString()
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
// Don't add any delay - the scheduler will naturally retry on next cycle
|
|
2035
|
+
this.logger.info(`Agent ${agentId} ready for retry after ${strategyName} compaction`);
|
|
2036
|
+
|
|
2037
|
+
} else {
|
|
2038
|
+
this.logger.error(`${strategyName} compaction failed for agent ${agentId}`, {
|
|
2039
|
+
error: compactionResult.error
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
// Broadcast failed compaction
|
|
2043
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
2044
|
+
status: COMPACTION_STATUS.FAILED,
|
|
2045
|
+
error: compactionResult.error || `${strategyName} compaction failed`,
|
|
2046
|
+
emergency: useEmergencyCompaction,
|
|
2047
|
+
timestamp: new Date().toISOString()
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
// Will retry on next attempt until max retries reached
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
} catch (compactionError) {
|
|
2054
|
+
this.logger.error(`${strategyName} compaction threw error for agent ${agentId}`, {
|
|
2055
|
+
error: compactionError.message
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
2059
|
+
status: COMPACTION_STATUS.FAILED,
|
|
2060
|
+
error: compactionError.message,
|
|
2061
|
+
emergency: useEmergencyCompaction,
|
|
2062
|
+
timestamp: new Date().toISOString()
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Perform emergency compaction with more aggressive settings
|
|
2069
|
+
* @param {string} agentId - Agent ID
|
|
2070
|
+
* @param {string} targetModel - Target model
|
|
2071
|
+
* @param {string} sessionId - Session ID
|
|
2072
|
+
* @returns {Promise<Object>} Compaction result
|
|
2073
|
+
* @private
|
|
2074
|
+
*/
|
|
2075
|
+
async performEmergencyCompaction(agentId, targetModel, sessionId) {
|
|
2076
|
+
try {
|
|
2077
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2078
|
+
if (!agent) {
|
|
2079
|
+
return { success: false, error: 'Agent not found' };
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// Get the current conversation
|
|
2083
|
+
const modelConversation = agent.conversations[targetModel];
|
|
2084
|
+
if (!modelConversation) {
|
|
2085
|
+
return { success: false, error: 'No conversation found for model' };
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// CRITICAL: Sync pending messages before reading compactizedMessages.
|
|
2089
|
+
// The scheduler's addMessageToConversation only pushes to conversation.messages.
|
|
2090
|
+
if (modelConversation.compactizedMessages) {
|
|
2091
|
+
const originalLength = modelConversation.messages.length;
|
|
2092
|
+
const originalCount = modelConversation.originalMessageCountAtCompaction || originalLength;
|
|
2093
|
+
if (originalLength > originalCount) {
|
|
2094
|
+
const newCount = originalLength - originalCount;
|
|
2095
|
+
const newMessages = modelConversation.messages.slice(-newCount);
|
|
2096
|
+
modelConversation.compactizedMessages.push(...newMessages);
|
|
2097
|
+
modelConversation.originalMessageCountAtCompaction = originalLength;
|
|
2098
|
+
this.logger.info(`Emergency compaction: pre-synced ${newCount} pending messages`, { agentId });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Get the messages to compact — use compactizedMessages (correct field name)
|
|
2103
|
+
const messages = modelConversation.compactizedMessages || modelConversation.messages;
|
|
2104
|
+
|
|
2105
|
+
// Allow compaction even with few messages if any are oversized
|
|
2106
|
+
// (splitting inside compactConversation will create enough messages)
|
|
2107
|
+
const hasOversized = messages && messages.some(m => {
|
|
2108
|
+
const content = typeof m.content === 'string' ? m.content : '';
|
|
2109
|
+
return content.length > COMPACTION_CONFIG.OVERSIZED_MESSAGE_THRESHOLD;
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
if (!hasOversized && (!messages || messages.length < 5)) {
|
|
2113
|
+
return { success: false, error: 'Not enough messages to compact' };
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Record watermark BEFORE compaction starts
|
|
2117
|
+
const preCompactionMessageCount = modelConversation.messages.length;
|
|
2118
|
+
|
|
2119
|
+
const contextWindow = this.tokenCountingService.getModelContextWindow(targetModel);
|
|
2120
|
+
|
|
2121
|
+
// Use aggressive settings - aim for 50% of context window instead of normal threshold
|
|
2122
|
+
const targetTokens = Math.floor(contextWindow * 0.5);
|
|
2123
|
+
|
|
2124
|
+
this.logger.info(`Emergency compaction: targeting ${targetTokens} tokens (50% of ${contextWindow})`, {
|
|
2125
|
+
agentId,
|
|
2126
|
+
messageCount: messages.length,
|
|
2127
|
+
targetModel
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
// Call compaction service with summarization (multi-pass handles reduction)
|
|
2131
|
+
const compactionResult = await this.compactionService.compactConversation(
|
|
2132
|
+
messages,
|
|
2133
|
+
targetModel,
|
|
2134
|
+
targetModel,
|
|
2135
|
+
{
|
|
2136
|
+
sessionId,
|
|
2137
|
+
agentId,
|
|
2138
|
+
emergency: true,
|
|
2139
|
+
onAllModelsExhausted: (errorInfo) => {
|
|
2140
|
+
this.broadcastCompactionEvent(agentId, sessionId, {
|
|
2141
|
+
type: 'compaction_models_exhausted',
|
|
2142
|
+
status: 'warning',
|
|
2143
|
+
message: errorInfo.message,
|
|
2144
|
+
modelsAttempted: errorInfo.models,
|
|
2145
|
+
error: errorInfo.error
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
);
|
|
2150
|
+
|
|
2151
|
+
// Compaction service returns compactedMessages directly (no success flag)
|
|
2152
|
+
// Check for compactedMessages array with length > 0 and not skipped
|
|
2153
|
+
const compactionSucceeded = compactionResult.compactedMessages?.length > 0 && !compactionResult.skipped;
|
|
2154
|
+
|
|
2155
|
+
if (compactionSucceeded) {
|
|
2156
|
+
// Update the agent's conversation — use correct field name: compactizedMessages
|
|
2157
|
+
modelConversation.compactizedMessages = compactionResult.compactedMessages;
|
|
2158
|
+
modelConversation.originalMessageCountAtCompaction = preCompactionMessageCount;
|
|
2159
|
+
modelConversation.lastCompactization = new Date().toISOString();
|
|
2160
|
+
modelConversation.compactizationCount = (modelConversation.compactizationCount || 0) + 1;
|
|
2161
|
+
modelConversation.compactedTokenCount = compactionResult.compactedTokenCount;
|
|
2162
|
+
modelConversation.originalTokenCount = compactionResult.originalTokenCount;
|
|
2163
|
+
|
|
2164
|
+
// Persist the changes
|
|
2165
|
+
await this.agentPool.persistAgentState(agentId);
|
|
2166
|
+
|
|
2167
|
+
return {
|
|
2168
|
+
success: true,
|
|
2169
|
+
originalTokenCount: compactionResult.originalTokenCount,
|
|
2170
|
+
compactedTokenCount: compactionResult.compactedTokenCount,
|
|
2171
|
+
reductionPercent: compactionResult.reductionPercent
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
return { success: false, error: compactionResult.error || 'Compaction returned no messages' };
|
|
2176
|
+
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
this.logger.error(`Emergency compaction error for agent ${agentId}`, {
|
|
2179
|
+
error: error.message,
|
|
2180
|
+
stack: error.stack
|
|
2181
|
+
});
|
|
2182
|
+
return { success: false, error: error.message };
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/**
|
|
2187
|
+
* Clear token limit retry tracker for an agent (call after successful AI response)
|
|
2188
|
+
* @param {string} agentId - Agent ID
|
|
2189
|
+
* @private
|
|
2190
|
+
*/
|
|
2191
|
+
clearTokenLimitRetryTracker(agentId) {
|
|
2192
|
+
if (this.tokenLimitRetryTracker.has(agentId)) {
|
|
2193
|
+
this.tokenLimitRetryTracker.delete(agentId);
|
|
2194
|
+
this.logger.debug(`Cleared token limit retry tracker for agent ${agentId}`);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Process AI response and execute any tools
|
|
2200
|
+
* @param {string} agentId - Agent ID
|
|
2201
|
+
* @param {Object} aiResponse - AI service response
|
|
2202
|
+
* @private
|
|
2203
|
+
*/
|
|
2204
|
+
async processAIResponse(agentId, aiResponse) {
|
|
2205
|
+
// Get the session ID from the session map
|
|
2206
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2207
|
+
const sessionId = this.getAgentSession(agentId) || agent?.sessionId || 'scheduler-session';
|
|
2208
|
+
|
|
2209
|
+
// Safety check: agent must exist
|
|
2210
|
+
if (!agent) {
|
|
2211
|
+
this.logger.warn(`Cannot process AI response - agent ${agentId} not found`);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Check if response contains tool calls
|
|
2216
|
+
const hasTools = this._hasToolCalls(aiResponse.content);
|
|
2217
|
+
|
|
2218
|
+
// Track consecutive messages without tools (AGENT mode only)
|
|
2219
|
+
if (agent && agent.mode === AGENT_MODES.AGENT && SCHEDULER_CONFIG.CONSECUTIVE_NO_TOOL_ENABLED) {
|
|
2220
|
+
if (!hasTools) {
|
|
2221
|
+
// Increment consecutive no-tool counter
|
|
2222
|
+
const currentCount = this.consecutiveNoToolMessages.get(agentId) || 0;
|
|
2223
|
+
const newCount = currentCount + 1;
|
|
2224
|
+
this.consecutiveNoToolMessages.set(agentId, newCount);
|
|
2225
|
+
this.logger.warn(`[NO-TOOL-TRACKER] Agent ${agentId}: ${newCount} consecutive messages without tools (threshold: ${SCHEDULER_CONFIG.CONSECUTIVE_NO_TOOL_THRESHOLD})`);
|
|
2226
|
+
|
|
2227
|
+
// If threshold exceeded, queue a tool result reminder (more noticeable than system prompt)
|
|
2228
|
+
if (newCount >= SCHEDULER_CONFIG.CONSECUTIVE_NO_TOOL_THRESHOLD) {
|
|
2229
|
+
const toolResultReminder = {
|
|
2230
|
+
id: `no-tool-reminder-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
2231
|
+
toolId: 'system_reminder',
|
|
2232
|
+
status: 'warning',
|
|
2233
|
+
result: `[NO-TOOL WARNING] You have sent ${newCount} consecutive messages without using any tools. You MUST now either: (1) Update your task list using the TaskManager tool, or (2) Call the jobdone tool if all tasks are complete. Do not respond with text only - take action with a tool.`,
|
|
2234
|
+
timestamp: new Date().toISOString(),
|
|
2235
|
+
queuedAt: new Date().toISOString(),
|
|
2236
|
+
isSystemGenerated: true
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
// Queue as tool result for next processing cycle
|
|
2240
|
+
agent.messageQueues.toolResults.push(toolResultReminder);
|
|
2241
|
+
|
|
2242
|
+
this.logger.warn(`[NO-TOOL-TRACKER] Agent ${agentId}: *** QUEUED NO-TOOL REMINDER AS TOOL RESULT ***`);
|
|
2243
|
+
}
|
|
2244
|
+
} else {
|
|
2245
|
+
this.logger.warn(`[NO-TOOL-TRACKER] Agent ${agentId}: Response HAS tools, counter will reset in _executeToolsAsync`);
|
|
2246
|
+
}
|
|
2247
|
+
// Note: Counter is reset in _executeToolsAsync when tools ARE executed
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Normalize token usage field names (backend may send input_tokens/output_tokens
|
|
2251
|
+
// instead of prompt_tokens/completion_tokens depending on provider)
|
|
2252
|
+
let normalizedTokenUsage = null;
|
|
2253
|
+
if (aiResponse.tokenUsage) {
|
|
2254
|
+
normalizedTokenUsage = {
|
|
2255
|
+
prompt_tokens: aiResponse.tokenUsage.prompt_tokens || aiResponse.tokenUsage.input_tokens || 0,
|
|
2256
|
+
completion_tokens: aiResponse.tokenUsage.completion_tokens || aiResponse.tokenUsage.output_tokens || 0,
|
|
2257
|
+
total_tokens: aiResponse.tokenUsage.total_tokens || 0
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Create response message
|
|
2262
|
+
const responseMessage = {
|
|
2263
|
+
id: `ai-response-${Date.now()}`,
|
|
2264
|
+
agentId: agentId,
|
|
2265
|
+
role: MESSAGE_ROLES.ASSISTANT,
|
|
2266
|
+
content: aiResponse.content,
|
|
2267
|
+
timestamp: new Date().toISOString(),
|
|
2268
|
+
model: aiResponse.model,
|
|
2269
|
+
tokenUsage: normalizedTokenUsage,
|
|
2270
|
+
sessionId: sessionId,
|
|
2271
|
+
// Mark if tools will be executed (UI can show loading indicator)
|
|
2272
|
+
pendingToolExecution: hasTools
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
await this.addMessageToConversation(agentId, responseMessage, false);
|
|
2276
|
+
|
|
2277
|
+
// IMMEDIATELY broadcast the AI response to UI (don't wait for tool execution)
|
|
2278
|
+
if (this.shouldBroadcastMessage(responseMessage)) {
|
|
2279
|
+
const updatedAgent = await this.agentPool.getAgent(agentId);
|
|
2280
|
+
this.broadcastMessageUpdate(agentId, responseMessage, {
|
|
2281
|
+
agentCurrentModel: updatedAgent?.currentModel
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// Execute tools ASYNCHRONOUSLY - don't block the response
|
|
2286
|
+
this._executeToolsAsync(agentId, aiResponse.content, sessionId, responseMessage.id);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
/**
|
|
2290
|
+
* Check if AI response contains tool calls
|
|
2291
|
+
* @param {string} content - AI response content
|
|
2292
|
+
* @returns {boolean} Whether content has tool calls
|
|
2293
|
+
* @private
|
|
2294
|
+
*/
|
|
2295
|
+
_hasToolCalls(content) {
|
|
2296
|
+
if (!content) return false;
|
|
2297
|
+
// Check for JSON code block tool format (primary format used by tagParser)
|
|
2298
|
+
if (content.includes('```json')) {
|
|
2299
|
+
// Quick check for toolId in JSON block
|
|
2300
|
+
const jsonBlockPattern = /```json\s*\{[\s\S]*?"toolId"\s*:/;
|
|
2301
|
+
if (jsonBlockPattern.test(content)) return true;
|
|
2302
|
+
}
|
|
2303
|
+
// Also check for legacy patterns
|
|
2304
|
+
return content.includes('<tool>') ||
|
|
2305
|
+
content.includes('<function_call>') ||
|
|
2306
|
+
content.includes('```tool') ||
|
|
2307
|
+
/<\w+_tool>/i.test(content);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/**
|
|
2311
|
+
* Execute tools asynchronously and stream results to UI
|
|
2312
|
+
* @param {string} agentId - Agent ID
|
|
2313
|
+
* @param {string} content - AI response content
|
|
2314
|
+
* @param {string} sessionId - Session ID
|
|
2315
|
+
* @param {string} responseMessageId - Original response message ID for correlation
|
|
2316
|
+
* @private
|
|
2317
|
+
*/
|
|
2318
|
+
async _executeToolsAsync(agentId, content, sessionId, responseMessageId) {
|
|
2319
|
+
// Check tools for builtinDelay via registry and apply the maximum delay
|
|
2320
|
+
try {
|
|
2321
|
+
const extractedTools = await this.messageProcessor.extractToolCommands(content);
|
|
2322
|
+
const toolsRegistry = this.messageProcessor.toolsRegistry;
|
|
2323
|
+
|
|
2324
|
+
// Find the maximum builtinDelay among all tools being executed
|
|
2325
|
+
let maxDelay = 0;
|
|
2326
|
+
for (const cmd of extractedTools) {
|
|
2327
|
+
const tool = toolsRegistry?.getTool(cmd.toolId);
|
|
2328
|
+
if (tool?.builtinDelay > maxDelay) {
|
|
2329
|
+
maxDelay = tool.builtinDelay;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (maxDelay > 0) {
|
|
2334
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2335
|
+
if (agent) {
|
|
2336
|
+
agent.delayEndTime = new Date(Date.now() + maxDelay).toISOString();
|
|
2337
|
+
await this.agentPool.persistAgentState(agentId);
|
|
2338
|
+
this.logger.debug(`Agent ${agentId} - applying ${maxDelay}ms builtin delay for tool execution`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
} catch (extractError) {
|
|
2342
|
+
this.logger.warn(`Agent ${agentId} - failed to check tool delays:`, extractError.message);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
try {
|
|
2346
|
+
const toolResults = await this.messageProcessor.extractAndExecuteTools(
|
|
2347
|
+
content,
|
|
2348
|
+
agentId,
|
|
2349
|
+
{ sessionId: sessionId }
|
|
2350
|
+
);
|
|
2351
|
+
|
|
2352
|
+
// Queue tool results in T queue for next iteration
|
|
2353
|
+
if (toolResults.length > 0) {
|
|
2354
|
+
// Reset consecutive no-tool counter since tools were executed
|
|
2355
|
+
if (this.consecutiveNoToolMessages.has(agentId)) {
|
|
2356
|
+
this.logger.debug(`Agent ${agentId} used tools - resetting consecutive no-tool counter`);
|
|
2357
|
+
this.consecutiveNoToolMessages.set(agentId, 0);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2361
|
+
if (agent) {
|
|
2362
|
+
const toolExecutions = [];
|
|
2363
|
+
const fullToolResults = [];
|
|
2364
|
+
|
|
2365
|
+
for (const result of toolResults) {
|
|
2366
|
+
const toolResultEntry = {
|
|
2367
|
+
id: `tool-result-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
2368
|
+
toolId: result.toolId,
|
|
2369
|
+
status: result.status,
|
|
2370
|
+
result: result.result,
|
|
2371
|
+
error: result.error,
|
|
2372
|
+
executionTime: result.executionTime,
|
|
2373
|
+
timestamp: new Date().toISOString(),
|
|
2374
|
+
queuedAt: new Date().toISOString(),
|
|
2375
|
+
responseTurnId: responseMessageId // Track which AI turn triggered this result
|
|
2376
|
+
};
|
|
2377
|
+
|
|
2378
|
+
agent.messageQueues.toolResults.push(toolResultEntry);
|
|
2379
|
+
|
|
2380
|
+
toolExecutions.push({
|
|
2381
|
+
toolId: result.toolId,
|
|
2382
|
+
status: result.status,
|
|
2383
|
+
error: result.error,
|
|
2384
|
+
executionTime: result.executionTime
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
fullToolResults.push({
|
|
2388
|
+
id: toolResultEntry.id,
|
|
2389
|
+
toolId: result.toolId,
|
|
2390
|
+
status: result.status,
|
|
2391
|
+
result: result.result,
|
|
2392
|
+
error: result.error,
|
|
2393
|
+
executionTime: result.executionTime,
|
|
2394
|
+
timestamp: toolResultEntry.timestamp
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Attach toolResults and toolExecutions to the original assistant message
|
|
2399
|
+
// so they persist in conversation history and are available when loading old conversations
|
|
2400
|
+
this._attachToolResultsToMessage(agent, responseMessageId, toolExecutions, fullToolResults);
|
|
2401
|
+
|
|
2402
|
+
await this.agentPool.persistAgentState(agentId);
|
|
2403
|
+
|
|
2404
|
+
// Broadcast tool execution completion to UI
|
|
2405
|
+
this._broadcastToolResults(agentId, sessionId, responseMessageId, toolExecutions, fullToolResults);
|
|
2406
|
+
}
|
|
2407
|
+
} else {
|
|
2408
|
+
// No tools to execute - broadcast completion with empty results
|
|
2409
|
+
this._broadcastToolResults(agentId, sessionId, responseMessageId, [], []);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
this.logger.error(`Tool execution failed for agent ${agentId}:`, error);
|
|
2414
|
+
// Broadcast error to UI
|
|
2415
|
+
this._broadcastToolResults(agentId, sessionId, responseMessageId, [], [], error.message);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/**
|
|
2420
|
+
* Broadcast tool execution results to UI
|
|
2421
|
+
* @param {string} agentId - Agent ID
|
|
2422
|
+
* @param {string} sessionId - Session ID
|
|
2423
|
+
* @param {string} responseMessageId - Original response message ID
|
|
2424
|
+
* @param {Array} toolExecutions - Summary of tool executions
|
|
2425
|
+
* @param {Array} toolResults - Full tool results
|
|
2426
|
+
* @param {string} error - Error message if execution failed
|
|
2427
|
+
* @private
|
|
2428
|
+
*/
|
|
2429
|
+
_broadcastToolResults(agentId, sessionId, responseMessageId, toolExecutions, toolResults, error = null) {
|
|
2430
|
+
if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
2431
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
2432
|
+
type: 'tool_execution_complete',
|
|
2433
|
+
data: {
|
|
2434
|
+
agentId: agentId,
|
|
2435
|
+
responseMessageId: responseMessageId,
|
|
2436
|
+
toolExecutions: toolExecutions,
|
|
2437
|
+
toolResults: toolResults,
|
|
2438
|
+
error: error,
|
|
2439
|
+
timestamp: new Date().toISOString()
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
this.logger.debug('Broadcast tool execution results', {
|
|
2444
|
+
agentId,
|
|
2445
|
+
sessionId,
|
|
2446
|
+
toolCount: toolExecutions.length,
|
|
2447
|
+
hasError: !!error
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Attach tool execution results to the original assistant message in conversation history.
|
|
2454
|
+
* This ensures results persist and are available when loading old conversations,
|
|
2455
|
+
* rather than only being available via the transient WebSocket event.
|
|
2456
|
+
* @param {Object} agent - Agent object with conversations
|
|
2457
|
+
* @param {string} responseMessageId - ID of the assistant message that triggered tool execution
|
|
2458
|
+
* @param {Array} toolExecutions - Summary of tool executions (toolId, status, error, executionTime)
|
|
2459
|
+
* @param {Array} toolResults - Full tool results with result data
|
|
2460
|
+
* @private
|
|
2461
|
+
*/
|
|
2462
|
+
_attachToolResultsToMessage(agent, responseMessageId, toolExecutions, toolResults) {
|
|
2463
|
+
if (!agent || !responseMessageId) return;
|
|
2464
|
+
|
|
2465
|
+
try {
|
|
2466
|
+
// Update in full conversation history
|
|
2467
|
+
if (agent.conversations?.full?.messages) {
|
|
2468
|
+
const fullMsg = agent.conversations.full.messages.find(m => m.id === responseMessageId);
|
|
2469
|
+
if (fullMsg) {
|
|
2470
|
+
fullMsg.toolExecutions = toolExecutions;
|
|
2471
|
+
fullMsg.toolResults = toolResults;
|
|
2472
|
+
fullMsg.pendingToolExecution = false;
|
|
2473
|
+
fullMsg.hasToolExecutions = true;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Also update in all model-specific conversations
|
|
2478
|
+
for (const [key, conv] of Object.entries(agent.conversations || {})) {
|
|
2479
|
+
if (key === 'full' || !conv?.messages) continue;
|
|
2480
|
+
const modelMsg = conv.messages.find(m => m.id === responseMessageId);
|
|
2481
|
+
if (modelMsg) {
|
|
2482
|
+
modelMsg.toolExecutions = toolExecutions;
|
|
2483
|
+
modelMsg.toolResults = toolResults;
|
|
2484
|
+
modelMsg.pendingToolExecution = false;
|
|
2485
|
+
modelMsg.hasToolExecutions = true;
|
|
2486
|
+
}
|
|
2487
|
+
// Also check compactizedMessages if conversation was compacted
|
|
2488
|
+
if (conv.compactizedMessages) {
|
|
2489
|
+
const compactMsg = conv.compactizedMessages.find(m => m.id === responseMessageId);
|
|
2490
|
+
if (compactMsg) {
|
|
2491
|
+
compactMsg.toolExecutions = toolExecutions;
|
|
2492
|
+
compactMsg.toolResults = toolResults;
|
|
2493
|
+
compactMsg.pendingToolExecution = false;
|
|
2494
|
+
compactMsg.hasToolExecutions = true;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
this.logger.warn(`Failed to attach tool results to message ${responseMessageId}:`, err.message);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* Format tool result for conversation
|
|
2505
|
+
* @param {Object} toolResult - Tool result message
|
|
2506
|
+
* @returns {string} Formatted content
|
|
2507
|
+
* @private
|
|
2508
|
+
*/
|
|
2509
|
+
formatToolResult(toolResult) {
|
|
2510
|
+
const toolLabel = toolResult.toolId ? `[${toolResult.toolId}] ` : '';
|
|
2511
|
+
if (toolResult.status === 'completed') {
|
|
2512
|
+
if (typeof toolResult.result === 'object') {
|
|
2513
|
+
return `${toolLabel}${JSON.stringify(toolResult.result, null, 2)}`;
|
|
2514
|
+
}
|
|
2515
|
+
return `${toolLabel}${String(toolResult.result || 'Tool executed successfully')}`;
|
|
2516
|
+
} else if (toolResult.status === 'failed') {
|
|
2517
|
+
return `${toolLabel}Tool execution failed: ${toolResult.error || 'Unknown error'}`;
|
|
2518
|
+
} else if (toolResult.result) {
|
|
2519
|
+
// Warning or other status with a result message (e.g. no-tool reminders)
|
|
2520
|
+
return `${toolLabel}${String(toolResult.result)}`;
|
|
2521
|
+
}
|
|
2522
|
+
return `${toolLabel}Tool status: ${toolResult.status}`;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/**
|
|
2526
|
+
* Check if message should be broadcast to UI
|
|
2527
|
+
* @param {Object} message - Message to check
|
|
2528
|
+
* @returns {boolean} Whether to broadcast
|
|
2529
|
+
* @private
|
|
2530
|
+
*/
|
|
2531
|
+
shouldBroadcastMessage(message) {
|
|
2532
|
+
// Don't broadcast internal scheduler prompts
|
|
2533
|
+
if (message.type === 'scheduler-prompt') {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// Don't broadcast consolidated-input messages (internal AI context with tool results)
|
|
2538
|
+
if (message.type === 'consolidated-input') {
|
|
2539
|
+
return false;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// Don't broadcast pure system instructions (but allow system messages from inter-agent communication)
|
|
2543
|
+
if (message.role === MESSAGE_ROLES.SYSTEM && !message.queueType) {
|
|
2544
|
+
return false;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// Broadcast all other messages (user, assistant, tool, inter-agent)
|
|
2548
|
+
return true;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/**
|
|
2552
|
+
* Broadcast message update to UI
|
|
2553
|
+
* @param {string} agentId - Agent ID
|
|
2554
|
+
* @param {Object} message - Message that was added
|
|
2555
|
+
* @param {Object} agentInfo - Additional agent information for UI sync
|
|
2556
|
+
* @private
|
|
2557
|
+
*/
|
|
2558
|
+
broadcastMessageUpdate(agentId, message, agentInfo = {}) {
|
|
2559
|
+
if (this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
2560
|
+
// Get the session ID from session map, message, or fallback
|
|
2561
|
+
let sessionId = this.getAgentSession(agentId);
|
|
2562
|
+
|
|
2563
|
+
// Try to get sessionId from message if not in session map
|
|
2564
|
+
if (!sessionId && message.sessionId) {
|
|
2565
|
+
sessionId = message.sessionId;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// Final fallback
|
|
2569
|
+
if (!sessionId) {
|
|
2570
|
+
sessionId = 'scheduler-session';
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
this.logger.debug('Broadcasting message to session', {
|
|
2574
|
+
agentId: agentId,
|
|
2575
|
+
sessionId: sessionId,
|
|
2576
|
+
messageRole: message.role,
|
|
2577
|
+
messageType: message.type
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
2581
|
+
type: 'message_added',
|
|
2582
|
+
data: {
|
|
2583
|
+
agentId: agentId,
|
|
2584
|
+
message: message,
|
|
2585
|
+
timestamp: new Date().toISOString(),
|
|
2586
|
+
agentCurrentModel: agentInfo.agentCurrentModel
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
/**
|
|
2593
|
+
* Auto-create initial task if agent just switched to AGENT mode
|
|
2594
|
+
* @param {string} agentId - Agent ID
|
|
2595
|
+
* @private
|
|
2596
|
+
*/
|
|
2597
|
+
async autoCreateInitialTaskIfNeeded(agentId) {
|
|
2598
|
+
try {
|
|
2599
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2600
|
+
if (!agent || agent.mode !== AGENT_MODES.AGENT) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// Ensure taskList exists
|
|
2605
|
+
if (!agent.taskList) {
|
|
2606
|
+
agent.taskList = {
|
|
2607
|
+
tasks: [],
|
|
2608
|
+
lastUpdated: new Date().toISOString()
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// Check if we already have tasks
|
|
2613
|
+
if (agent.taskList.tasks && agent.taskList.tasks.length > 0) {
|
|
2614
|
+
return; // Already has tasks
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// If agent just completed work via jobdone, don't recreate a task
|
|
2618
|
+
// from old conversation history — wait for a genuinely new message.
|
|
2619
|
+
if (agent.autonomousWorkComplete) {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// Look for the last user message in conversation history
|
|
2624
|
+
const conversations = agent.conversations?.full?.messages || [];
|
|
2625
|
+
const lastUserMessage = [...conversations].reverse().find(m => m.role === MESSAGE_ROLES.USER);
|
|
2626
|
+
|
|
2627
|
+
if (lastUserMessage) {
|
|
2628
|
+
const taskTitle = `Process initial request: ${this.extractTaskTitle(lastUserMessage.content)}`;
|
|
2629
|
+
const taskDescription = `Handle user request: "${this.truncateContent(lastUserMessage.content, 200)}"`;
|
|
2630
|
+
|
|
2631
|
+
const task = {
|
|
2632
|
+
id: `task-initial-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
2633
|
+
title: taskTitle,
|
|
2634
|
+
description: taskDescription,
|
|
2635
|
+
status: 'pending',
|
|
2636
|
+
priority: 'high',
|
|
2637
|
+
createdAt: new Date().toISOString(),
|
|
2638
|
+
updatedAt: new Date().toISOString(),
|
|
2639
|
+
source: 'auto-created-initial',
|
|
2640
|
+
messageId: lastUserMessage.id
|
|
2641
|
+
};
|
|
2642
|
+
|
|
2643
|
+
agent.taskList.tasks.push(task);
|
|
2644
|
+
agent.taskList.lastUpdated = new Date().toISOString();
|
|
2645
|
+
|
|
2646
|
+
await this.agentPool.persistAgentState(agentId);
|
|
2647
|
+
|
|
2648
|
+
this.logger.info(`Auto-created initial task for agent ${agentId}`, {
|
|
2649
|
+
taskId: task.id,
|
|
2650
|
+
title: task.title,
|
|
2651
|
+
agentName: agent.name
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
this.logger.error(`Failed to auto-create initial task for agent ${agentId}`, {
|
|
2656
|
+
error: error.message
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* Auto-create tasks for incoming messages (Phase 2)
|
|
2663
|
+
* @param {string} agentId - Agent ID
|
|
2664
|
+
* @param {Array} userMessages - User messages to process
|
|
2665
|
+
* @param {Array} interAgentMessages - Inter-agent messages to process
|
|
2666
|
+
* @private
|
|
2667
|
+
*/
|
|
2668
|
+
async autoCreateTasksForMessages(agentId, userMessages, interAgentMessages) {
|
|
2669
|
+
try {
|
|
2670
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2671
|
+
if (!agent || agent.mode !== AGENT_MODES.AGENT) {
|
|
2672
|
+
return; // Only auto-create tasks for AGENT mode agents
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// Ensure taskList exists
|
|
2676
|
+
if (!agent.taskList) {
|
|
2677
|
+
agent.taskList = {
|
|
2678
|
+
tasks: [],
|
|
2679
|
+
lastUpdated: new Date().toISOString()
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// Create tasks for user messages
|
|
2684
|
+
for (const msg of userMessages) {
|
|
2685
|
+
const taskTitle = `Process user request: ${this.extractTaskTitle(msg.content)}`;
|
|
2686
|
+
const taskDescription = `Handle user message: "${this.truncateContent(msg.content, 200)}"`;
|
|
2687
|
+
|
|
2688
|
+
// Check if similar task already exists
|
|
2689
|
+
const existingTask = agent.taskList.tasks.find(task =>
|
|
2690
|
+
task.status === 'pending' &&
|
|
2691
|
+
task.title.includes('Process user request') &&
|
|
2692
|
+
this.calculateContentSimilarity(task.description, taskDescription) > 0.7
|
|
2693
|
+
);
|
|
2694
|
+
|
|
2695
|
+
if (!existingTask) {
|
|
2696
|
+
const task = {
|
|
2697
|
+
id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
2698
|
+
title: taskTitle,
|
|
2699
|
+
description: taskDescription,
|
|
2700
|
+
status: 'pending',
|
|
2701
|
+
priority: 'high', // User messages get high priority
|
|
2702
|
+
createdAt: new Date().toISOString(),
|
|
2703
|
+
updatedAt: new Date().toISOString(),
|
|
2704
|
+
source: 'auto-created',
|
|
2705
|
+
messageId: msg.id
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
agent.taskList.tasks.push(task);
|
|
2709
|
+
|
|
2710
|
+
this.logger?.info(`Auto-created task for user message`, {
|
|
2711
|
+
agentId,
|
|
2712
|
+
taskId: task.id,
|
|
2713
|
+
title: task.title
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// Create tasks for inter-agent messages
|
|
2719
|
+
for (const msg of interAgentMessages) {
|
|
2720
|
+
const senderName = msg.senderName || msg.sender || 'Unknown Agent';
|
|
2721
|
+
const taskTitle = `Respond to ${senderName}: ${this.extractTaskTitle(msg.content)}`;
|
|
2722
|
+
const taskDescription = `Handle message from ${senderName}: "${this.truncateContent(msg.content, 200)}"`;
|
|
2723
|
+
|
|
2724
|
+
// Check if similar task already exists
|
|
2725
|
+
const existingTask = agent.taskList.tasks.find(task =>
|
|
2726
|
+
task.status === 'pending' &&
|
|
2727
|
+
task.title.includes(`Respond to ${senderName}`) &&
|
|
2728
|
+
this.calculateContentSimilarity(task.description, taskDescription) > 0.7
|
|
2729
|
+
);
|
|
2730
|
+
|
|
2731
|
+
if (!existingTask) {
|
|
2732
|
+
const task = {
|
|
2733
|
+
id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
2734
|
+
title: taskTitle,
|
|
2735
|
+
description: taskDescription,
|
|
2736
|
+
status: 'pending',
|
|
2737
|
+
priority: 'medium', // Inter-agent messages get medium priority
|
|
2738
|
+
createdAt: new Date().toISOString(),
|
|
2739
|
+
updatedAt: new Date().toISOString(),
|
|
2740
|
+
source: 'auto-created',
|
|
2741
|
+
messageId: msg.id,
|
|
2742
|
+
senderAgent: msg.sender
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2745
|
+
agent.taskList.tasks.push(task);
|
|
2746
|
+
|
|
2747
|
+
this.logger?.info(`Auto-created task for inter-agent message`, {
|
|
2748
|
+
agentId,
|
|
2749
|
+
taskId: task.id,
|
|
2750
|
+
title: task.title,
|
|
2751
|
+
sender: senderName
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// Update task list timestamp
|
|
2757
|
+
if (userMessages.length > 0 || interAgentMessages.length > 0) {
|
|
2758
|
+
agent.taskList.lastUpdated = new Date().toISOString();
|
|
2759
|
+
await this.agentPool.persistAgentState(agentId);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
this.logger?.error(`Failed to auto-create tasks for agent ${agentId}`, {
|
|
2764
|
+
error: error.message,
|
|
2765
|
+
userMessageCount: userMessages.length,
|
|
2766
|
+
interAgentMessageCount: interAgentMessages.length
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* Extract a concise title from message content
|
|
2773
|
+
* @param {string} content - Message content
|
|
2774
|
+
* @returns {string} Extracted title
|
|
2775
|
+
* @private
|
|
2776
|
+
*/
|
|
2777
|
+
extractTaskTitle(content) {
|
|
2778
|
+
// Extract first meaningful sentence or phrase, max 50 chars
|
|
2779
|
+
const cleaned = content.trim().replace(/\n+/g, ' ').replace(/\s+/g, ' ');
|
|
2780
|
+
const firstSentence = cleaned.split(/[.!?]/)[0].trim();
|
|
2781
|
+
|
|
2782
|
+
if (firstSentence.length > 50) {
|
|
2783
|
+
return firstSentence.substring(0, 47) + '...';
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
return firstSentence || 'Process message';
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
/**
|
|
2790
|
+
* Truncate content to specified length
|
|
2791
|
+
* @param {string} content - Content to truncate
|
|
2792
|
+
* @param {number} maxLength - Maximum length
|
|
2793
|
+
* @returns {string} Truncated content
|
|
2794
|
+
* @private
|
|
2795
|
+
*/
|
|
2796
|
+
truncateContent(content, maxLength) {
|
|
2797
|
+
if (content.length <= maxLength) return content;
|
|
2798
|
+
return content.substring(0, maxLength - 3) + '...';
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* Calculate content similarity (simple implementation)
|
|
2803
|
+
* @param {string} content1 - First content
|
|
2804
|
+
* @param {string} content2 - Second content
|
|
2805
|
+
* @returns {number} Similarity score (0-1)
|
|
2806
|
+
* @private
|
|
2807
|
+
*/
|
|
2808
|
+
calculateContentSimilarity(content1, content2) {
|
|
2809
|
+
// Simple word-based similarity
|
|
2810
|
+
const words1 = content1.toLowerCase().split(/\s+/);
|
|
2811
|
+
const words2 = content2.toLowerCase().split(/\s+/);
|
|
2812
|
+
|
|
2813
|
+
const commonWords = words1.filter(word => words2.includes(word));
|
|
2814
|
+
const totalWords = new Set([...words1, ...words2]).size;
|
|
2815
|
+
|
|
2816
|
+
return commonWords.length / totalWords;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
/**
|
|
2820
|
+
* Auto-mark highest priority pending task as in-progress (Phase 2)
|
|
2821
|
+
* @param {string} agentId - Agent ID
|
|
2822
|
+
* @private
|
|
2823
|
+
*/
|
|
2824
|
+
async autoProgressHighestPriorityTask(agentId) {
|
|
2825
|
+
try {
|
|
2826
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
2827
|
+
if (!agent || !agent.taskList || !agent.taskList.tasks) {
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
// Find highest priority pending task that can be started (respecting dependencies)
|
|
2832
|
+
let pendingTasks = agent.taskList.tasks.filter(task => task.status === TASK_STATUS.PENDING);
|
|
2833
|
+
|
|
2834
|
+
if (pendingTasks.length === 0) {
|
|
2835
|
+
return; // No pending tasks
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// Phase 3: Filter out blocked tasks (dependencies not met)
|
|
2839
|
+
pendingTasks = this.filterAvailableTasks(agent.taskList.tasks, pendingTasks);
|
|
2840
|
+
|
|
2841
|
+
if (pendingTasks.length === 0) {
|
|
2842
|
+
this.logger?.info(`All pending tasks are blocked by dependencies for agent ${agentId}`);
|
|
2843
|
+
return; // All tasks are blocked
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// Sort by intelligent priority score, fallback to priority level, then creation date
|
|
2847
|
+
pendingTasks.sort((a, b) => {
|
|
2848
|
+
// Use priority score if available (higher score = higher priority)
|
|
2849
|
+
if (a.priorityScore !== undefined && b.priorityScore !== undefined) {
|
|
2850
|
+
const scoreDiff = b.priorityScore - a.priorityScore;
|
|
2851
|
+
if (Math.abs(scoreDiff) > 0.1) return scoreDiff; // Use score if significantly different
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// Fallback to traditional priority ordering using constants
|
|
2855
|
+
const priorityA = TASK_PRIORITY_ORDER[a.priority] ?? TASK_PRIORITY_ORDER.medium;
|
|
2856
|
+
const priorityB = TASK_PRIORITY_ORDER[b.priority] ?? TASK_PRIORITY_ORDER.medium;
|
|
2857
|
+
const priorityDiff = priorityA - priorityB;
|
|
2858
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
2859
|
+
|
|
2860
|
+
// Finally sort by creation date (older first)
|
|
2861
|
+
return new Date(a.createdAt) - new Date(b.createdAt);
|
|
2862
|
+
});
|
|
2863
|
+
|
|
2864
|
+
const taskToProgress = pendingTasks[0];
|
|
2865
|
+
|
|
2866
|
+
// Check if we already have a task in progress
|
|
2867
|
+
const inProgressTasks = agent.taskList.tasks.filter(task => task.status === TASK_STATUS.IN_PROGRESS);
|
|
2868
|
+
|
|
2869
|
+
if (inProgressTasks.length === 0) {
|
|
2870
|
+
// Mark highest priority task as in-progress
|
|
2871
|
+
taskToProgress.status = TASK_STATUS.IN_PROGRESS;
|
|
2872
|
+
taskToProgress.updatedAt = new Date().toISOString();
|
|
2873
|
+
taskToProgress.startedAt = new Date().toISOString();
|
|
2874
|
+
|
|
2875
|
+
agent.taskList.lastUpdated = new Date().toISOString();
|
|
2876
|
+
await this.agentPool.persistAgentState(agentId);
|
|
2877
|
+
|
|
2878
|
+
this.logger?.info(`Auto-progressed task to in-progress`, {
|
|
2879
|
+
agentId,
|
|
2880
|
+
taskId: taskToProgress.id,
|
|
2881
|
+
title: taskToProgress.title,
|
|
2882
|
+
priority: taskToProgress.priority
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
} catch (error) {
|
|
2887
|
+
this.logger?.error(`Failed to auto-progress task for agent ${agentId}`, {
|
|
2888
|
+
error: error.message
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/**
|
|
2894
|
+
* Filter tasks to only include those that can be started (Phase 3)
|
|
2895
|
+
* @param {Array} allTasks - All tasks for the agent
|
|
2896
|
+
* @param {Array} pendingTasks - Tasks with pending status
|
|
2897
|
+
* @returns {Array} Tasks that can be started (no blocking dependencies)
|
|
2898
|
+
* @private
|
|
2899
|
+
*/
|
|
2900
|
+
filterAvailableTasks(allTasks, pendingTasks) {
|
|
2901
|
+
return pendingTasks.filter(task => {
|
|
2902
|
+
// If task has no dependencies, it's available
|
|
2903
|
+
if (!task.dependencies || task.dependencies.length === 0) {
|
|
2904
|
+
return true;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// Check all blocking dependencies
|
|
2908
|
+
const blockingDeps = task.dependencies.filter(dep => dep.type === 'blocks');
|
|
2909
|
+
|
|
2910
|
+
for (const dep of blockingDeps) {
|
|
2911
|
+
const depTask = allTasks.find(t => t.id === dep.taskId);
|
|
2912
|
+
|
|
2913
|
+
// If dependency task doesn't exist or isn't completed, task is blocked
|
|
2914
|
+
if (!depTask || depTask.status !== 'completed') {
|
|
2915
|
+
return false;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
return true; // All blocking dependencies are satisfied
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
/**
|
|
2924
|
+
* Update task statuses based on dependency completion (Phase 3)
|
|
2925
|
+
* @param {Object} agent - Agent object
|
|
2926
|
+
* @param {string} completedTaskId - ID of the task that was just completed
|
|
2927
|
+
* @private
|
|
2928
|
+
*/
|
|
2929
|
+
async updateDependentTasks(agent, completedTaskId) {
|
|
2930
|
+
try {
|
|
2931
|
+
if (!agent.taskList || !agent.taskList.tasks) {
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
let updated = false;
|
|
2936
|
+
|
|
2937
|
+
// Find tasks that were blocked by the completed task
|
|
2938
|
+
for (const task of agent.taskList.tasks) {
|
|
2939
|
+
if (task.status === 'blocked' && task.dependencies) {
|
|
2940
|
+
const blockingDep = task.dependencies.find(
|
|
2941
|
+
dep => dep.type === 'blocks' && dep.taskId === completedTaskId
|
|
2942
|
+
);
|
|
2943
|
+
|
|
2944
|
+
if (blockingDep) {
|
|
2945
|
+
// Check if all other blocking dependencies are also completed
|
|
2946
|
+
const stillBlocked = task.dependencies.some(dep => {
|
|
2947
|
+
if (dep.type !== 'blocks') return false;
|
|
2948
|
+
const depTask = agent.taskList.tasks.find(t => t.id === dep.taskId);
|
|
2949
|
+
return depTask && depTask.status !== 'completed';
|
|
2950
|
+
});
|
|
2951
|
+
|
|
2952
|
+
if (!stillBlocked) {
|
|
2953
|
+
task.status = 'pending';
|
|
2954
|
+
task.updatedAt = new Date().toISOString();
|
|
2955
|
+
updated = true;
|
|
2956
|
+
|
|
2957
|
+
this.logger?.info(`Task unblocked due to dependency completion`, {
|
|
2958
|
+
taskId: task.id,
|
|
2959
|
+
title: task.title,
|
|
2960
|
+
completedDependency: completedTaskId
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
if (updated) {
|
|
2968
|
+
agent.taskList.lastUpdated = new Date().toISOString();
|
|
2969
|
+
await this.agentPool.persistAgentState(agent.id);
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
this.logger?.error(`Failed to update dependent tasks for agent ${agent.id}`, {
|
|
2974
|
+
error: error.message,
|
|
2975
|
+
completedTaskId
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* Generate a hash representing the agent's most recent output
|
|
2982
|
+
*
|
|
2983
|
+
* IMPORTANT: We only hash AGENT/ASSISTANT responses, not user inputs.
|
|
2984
|
+
* This is because:
|
|
2985
|
+
* - User inputs changing is normal and expected
|
|
2986
|
+
* - Agent producing the SAME OUTPUT repeatedly indicates a loop
|
|
2987
|
+
* - If agent keeps saying "I'll do X" without actually doing it = loop
|
|
2988
|
+
*
|
|
2989
|
+
* @param {Object} agent - Agent object
|
|
2990
|
+
* @returns {string} Hash of the agent's recent output
|
|
2991
|
+
* @private
|
|
2992
|
+
*/
|
|
2993
|
+
generateAgentStateHash(agent) {
|
|
2994
|
+
const stateComponents = [];
|
|
2995
|
+
|
|
2996
|
+
// Get the most recent ASSISTANT messages (agent outputs only)
|
|
2997
|
+
const allMessages = agent.conversations?.full?.messages || [];
|
|
2998
|
+
const assistantMessages = allMessages
|
|
2999
|
+
.filter(m => m.role === 'assistant')
|
|
3000
|
+
.slice(-3); // Last 3 assistant responses
|
|
3001
|
+
|
|
3002
|
+
// Hash the agent's actual output content
|
|
3003
|
+
const outputSummary = assistantMessages
|
|
3004
|
+
.map(m => {
|
|
3005
|
+
// Get the meaningful content - strip tool calls for cleaner comparison
|
|
3006
|
+
const content = m.content || '';
|
|
3007
|
+
// Truncate but include enough to detect patterns
|
|
3008
|
+
return content.substring(0, 500);
|
|
3009
|
+
})
|
|
3010
|
+
.join('|');
|
|
3011
|
+
|
|
3012
|
+
stateComponents.push(`output:${outputSummary}`);
|
|
3013
|
+
|
|
3014
|
+
// Also include tool calls from recent assistant messages (agent's actions)
|
|
3015
|
+
// If agent keeps trying to call the same tool = loop
|
|
3016
|
+
const recentToolCalls = assistantMessages
|
|
3017
|
+
.filter(m => m.toolCalls && m.toolCalls.length > 0)
|
|
3018
|
+
.flatMap(m => m.toolCalls)
|
|
3019
|
+
.slice(-5)
|
|
3020
|
+
.map(tc => `${tc.toolId || tc.name}:${JSON.stringify(tc.parameters || tc.params || {}).substring(0, 100)}`)
|
|
3021
|
+
.join(',');
|
|
3022
|
+
|
|
3023
|
+
if (recentToolCalls) {
|
|
3024
|
+
stateComponents.push(`tools:${recentToolCalls}`);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// Create hash from agent output only
|
|
3028
|
+
const stateString = stateComponents.join('||');
|
|
3029
|
+
|
|
3030
|
+
// Simple hash function
|
|
3031
|
+
let hash = 0;
|
|
3032
|
+
for (let i = 0; i < stateString.length; i++) {
|
|
3033
|
+
const char = stateString.charCodeAt(i);
|
|
3034
|
+
hash = ((hash << 5) - hash) + char;
|
|
3035
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
return `${hash}_${stateString.length}`;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
/**
|
|
3042
|
+
* Detect if agent is in a repetitive loop using sliding window approach
|
|
3043
|
+
*
|
|
3044
|
+
* @param {string} agentId - Agent ID
|
|
3045
|
+
* @param {string} stateHash - Current state hash
|
|
3046
|
+
* @returns {{ isLoop: boolean, isImmediateDuplicate: boolean, occurrences: number }}
|
|
3047
|
+
* @private
|
|
3048
|
+
*/
|
|
3049
|
+
detectRepetitiveLoop(agentId, stateHash) {
|
|
3050
|
+
const history = this.stateHashHistory.get(agentId) || [];
|
|
3051
|
+
const windowSize = SCHEDULER_CONFIG.STATE_HASH_WINDOW_SIZE;
|
|
3052
|
+
const threshold = SCHEDULER_CONFIG.REPETITION_THRESHOLD;
|
|
3053
|
+
|
|
3054
|
+
// Get the sliding window (last N entries)
|
|
3055
|
+
const window = history.slice(-windowSize);
|
|
3056
|
+
|
|
3057
|
+
// Check if this is an immediate duplicate (same as last hash)
|
|
3058
|
+
const isImmediateDuplicate = window.length > 0 && window[window.length - 1].hash === stateHash;
|
|
3059
|
+
|
|
3060
|
+
// Count occurrences of this hash in the window
|
|
3061
|
+
const occurrences = window.filter(entry => entry.hash === stateHash).length;
|
|
3062
|
+
|
|
3063
|
+
// It's a loop if the same hash appears threshold times or more
|
|
3064
|
+
const isLoop = occurrences >= threshold;
|
|
3065
|
+
|
|
3066
|
+
return {
|
|
3067
|
+
isLoop,
|
|
3068
|
+
isImmediateDuplicate,
|
|
3069
|
+
occurrences,
|
|
3070
|
+
windowSize: window.length
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/**
|
|
3075
|
+
* Record a state hash in the sliding window
|
|
3076
|
+
*
|
|
3077
|
+
* @param {string} agentId - Agent ID
|
|
3078
|
+
* @param {string} stateHash - State hash to record
|
|
3079
|
+
* @private
|
|
3080
|
+
*/
|
|
3081
|
+
recordStateHash(agentId, stateHash) {
|
|
3082
|
+
if (!this.stateHashHistory.has(agentId)) {
|
|
3083
|
+
this.stateHashHistory.set(agentId, []);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
const history = this.stateHashHistory.get(agentId);
|
|
3087
|
+
|
|
3088
|
+
// Add new entry with timestamp
|
|
3089
|
+
history.push({
|
|
3090
|
+
hash: stateHash,
|
|
3091
|
+
timestamp: Date.now()
|
|
3092
|
+
});
|
|
3093
|
+
|
|
3094
|
+
// Trim to keep only the sliding window (no arbitrary limits)
|
|
3095
|
+
// We keep slightly more than window size for context, but not unlimited
|
|
3096
|
+
const maxHistorySize = SCHEDULER_CONFIG.STATE_HASH_WINDOW_SIZE * 2;
|
|
3097
|
+
if (history.length > maxHistorySize) {
|
|
3098
|
+
// Remove oldest entries beyond double the window size
|
|
3099
|
+
history.splice(0, history.length - maxHistorySize);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
/**
|
|
3104
|
+
* Handle detected repetitive loop - notify user and stop agent
|
|
3105
|
+
*
|
|
3106
|
+
* @param {string} agentId - Agent ID
|
|
3107
|
+
* @param {Object} loopDetection - Loop detection result
|
|
3108
|
+
* @private
|
|
3109
|
+
*/
|
|
3110
|
+
async handleRepetitiveLoop(agentId, loopDetection) {
|
|
3111
|
+
const agent = await this.agentPool.getAgent(agentId);
|
|
3112
|
+
if (!agent) return;
|
|
3113
|
+
|
|
3114
|
+
// Create a user-friendly message
|
|
3115
|
+
const interventionMessage = `I notice I've been producing similar responses repeatedly (${loopDetection.occurrences} times). This usually means I'm stuck or need your guidance. I've switched to chat mode so you can provide direction.
|
|
3116
|
+
|
|
3117
|
+
What would you like me to do next? You can:
|
|
3118
|
+
- Give me new instructions or clarify the task
|
|
3119
|
+
- Switch me back to agent mode if you want me to continue
|
|
3120
|
+
- Ask me to try a different approach`;
|
|
3121
|
+
|
|
3122
|
+
// Add the intervention message as an assistant message so it shows in the chat
|
|
3123
|
+
const messageToAdd = {
|
|
3124
|
+
id: `msg-loop-intervention-${Date.now()}`,
|
|
3125
|
+
role: 'assistant',
|
|
3126
|
+
content: interventionMessage,
|
|
3127
|
+
timestamp: new Date().toISOString(),
|
|
3128
|
+
isSystemMessage: true,
|
|
3129
|
+
loopDetection: {
|
|
3130
|
+
occurrences: loopDetection.occurrences,
|
|
3131
|
+
windowSize: loopDetection.windowSize
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
|
|
3135
|
+
// Add to the full conversation history directly
|
|
3136
|
+
if (agent.conversations && agent.conversations.full) {
|
|
3137
|
+
agent.conversations.full.messages.push(messageToAdd);
|
|
3138
|
+
agent.conversations.full.lastUpdated = new Date().toISOString();
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// Stop the agent execution - switch to chat mode
|
|
3142
|
+
agent.mode = AGENT_MODES.CHAT;
|
|
3143
|
+
await this.agentPool.persistAgentState(agentId);
|
|
3144
|
+
|
|
3145
|
+
// Broadcast the message to UI so it appears in the chat
|
|
3146
|
+
const sessionId = this.getAgentSession(agentId) || agent.sessionId;
|
|
3147
|
+
if (sessionId && this.webSocketManager && this.webSocketManager.broadcastToSession) {
|
|
3148
|
+
// Broadcast the intervention message
|
|
3149
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
3150
|
+
type: 'message_added',
|
|
3151
|
+
data: {
|
|
3152
|
+
agentId,
|
|
3153
|
+
message: messageToAdd,
|
|
3154
|
+
type: 'loop_intervention'
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
3157
|
+
|
|
3158
|
+
// Also broadcast mode change
|
|
3159
|
+
this.webSocketManager.broadcastToSession(sessionId, {
|
|
3160
|
+
type: 'agent_mode_changed',
|
|
3161
|
+
data: {
|
|
3162
|
+
agentId,
|
|
3163
|
+
mode: AGENT_MODES.CHAT,
|
|
3164
|
+
reason: 'loop_detected',
|
|
3165
|
+
timestamp: new Date().toISOString()
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// Clear the hash history for this agent (fresh start when user responds)
|
|
3171
|
+
this.stateHashHistory.delete(agentId);
|
|
3172
|
+
|
|
3173
|
+
this.logger.warn(`Agent ${agentId} stopped due to repetitive loop - awaiting user intervention`, {
|
|
3174
|
+
occurrences: loopDetection.occurrences,
|
|
3175
|
+
agentName: agent.name
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
/**
|
|
3180
|
+
* Clear hash history for an agent (e.g., when conversation changes significantly)
|
|
3181
|
+
*
|
|
3182
|
+
* @param {string} agentId - Agent ID
|
|
3183
|
+
*/
|
|
3184
|
+
clearHashHistory(agentId) {
|
|
3185
|
+
if (this.stateHashHistory.has(agentId)) {
|
|
3186
|
+
this.stateHashHistory.set(agentId, []);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
/**
|
|
3191
|
+
* Get scheduler status
|
|
3192
|
+
* Uses AgentActivityService to get current active agents
|
|
3193
|
+
* @returns {Promise<Object>} Scheduler status
|
|
3194
|
+
*/
|
|
3195
|
+
async getStatus() {
|
|
3196
|
+
const allAgents = await this.agentPool.getAllAgents();
|
|
3197
|
+
const activeAgentResults = getActiveAgents(allAgents);
|
|
3198
|
+
|
|
3199
|
+
return {
|
|
3200
|
+
isRunning: this.isRunning,
|
|
3201
|
+
activeAgents: activeAgentResults.map(r => ({
|
|
3202
|
+
agentId: r.agentId,
|
|
3203
|
+
reason: r.reason,
|
|
3204
|
+
sessionId: this.getAgentSession(r.agentId)
|
|
3205
|
+
})),
|
|
3206
|
+
agentCount: activeAgentResults.length,
|
|
3207
|
+
sessionMapSize: this.agentSessionMap.size
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
export default AgentScheduler;
|