onbuzz 4.9.13 → 4.10.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/node_modules/glob/README.md +31 -5
- package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/glob.js +2 -1
- package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
- package/node_modules/glob/dist/commonjs/index.min.js +3 -3
- package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/pattern.js +4 -0
- package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
- package/node_modules/glob/dist/esm/glob.d.ts +8 -0
- package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/glob.js +2 -1
- package/node_modules/glob/dist/esm/glob.js.map +1 -1
- package/node_modules/glob/dist/esm/index.min.js +3 -3
- package/node_modules/glob/dist/esm/index.min.js.map +4 -4
- package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
- package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/pattern.js +4 -0
- package/node_modules/glob/dist/esm/pattern.js.map +1 -1
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
- package/node_modules/glob/node_modules/minimatch/README.md +76 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/package.json +17 -11
- package/node_modules/glob/package.json +10 -13
- package/node_modules/minipass/LICENSE.md +55 -0
- package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
- package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/commonjs/index.js +13 -3
- package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
- package/node_modules/minipass/dist/esm/index.d.ts +12 -16
- package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/esm/index.js +3 -1
- package/node_modules/minipass/dist/esm/index.js.map +1 -1
- package/node_modules/minipass/package.json +9 -14
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
- package/node_modules/path-scurry/package.json +8 -24
- package/package.json +1 -1
- package/scripts/debug-balance-probe.mjs +35 -35
- package/scripts/push-image.sh +43 -43
- package/scripts/setup-acr.sh +65 -65
- package/scripts/verify-optional-deps.js +96 -1
- package/src/__tests__/composioCliFlags.test.js +239 -239
- package/src/analyzers/CSSAnalyzer.js +298 -297
- package/src/analyzers/ConfigValidator.js +691 -690
- package/src/analyzers/ESLintAnalyzer.js +320 -320
- package/src/analyzers/JavaScriptAnalyzer.js +260 -261
- package/src/analyzers/PrettierFormatter.js +246 -247
- package/src/analyzers/PythonAnalyzer.js +283 -283
- package/src/analyzers/SecurityAnalyzer.js +729 -729
- package/src/analyzers/SparrowAnalyzer.js +341 -341
- package/src/analyzers/TypeScriptAnalyzer.js +247 -247
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
- package/src/analyzers/codeCloneDetector/detector.js +250 -250
- package/src/analyzers/codeCloneDetector/index.js +194 -192
- package/src/analyzers/codeCloneDetector/parser.js +199 -199
- package/src/core/__tests__/agentPool.test.js +866 -866
- package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
- package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
- package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
- package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
- package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
- package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
- package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
- package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
- package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
- package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
- package/src/core/__tests__/flowEndToEnd.test.js +565 -565
- package/src/core/__tests__/flowFieldMapping.test.js +188 -189
- package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
- package/src/core/__tests__/flowSavePayload.test.js +170 -169
- package/src/core/__tests__/flowTemplates.test.js +311 -311
- package/src/core/__tests__/flowVersionStore.test.js +123 -123
- package/src/core/__tests__/messageProcessor.test.js +669 -669
- package/src/core/__tests__/stateManager.test.js +0 -1
- package/src/core/agentPool.js +2474 -2475
- package/src/core/agentScheduler.js +1 -4
- package/src/core/contextManager.js +708 -708
- package/src/core/flowExecutor.js +1510 -1510
- package/src/core/flowFieldMapping.js +136 -138
- package/src/core/messageProcessor.js +953 -954
- package/src/core/orchestrator.js +593 -595
- package/src/core/stateManager.js +1765 -1752
- package/src/index.js +1221 -1221
- package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
- package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
- package/src/interfaces/__tests__/imageServing.test.js +228 -228
- package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
- package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
- package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
- package/src/interfaces/schedulerRoutes.js +50 -50
- package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
- package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
- package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
- package/src/interfaces/terminal/api/session.js +265 -266
- package/src/interfaces/terminal/api/websocket.js +496 -497
- package/src/interfaces/terminal/components/AgentCreator.js +691 -705
- package/src/interfaces/terminal/components/AgentEditor.js +676 -678
- package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
- package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
- package/src/interfaces/terminal/components/Header.js +28 -28
- package/src/interfaces/terminal/components/Layout.js +598 -603
- package/src/interfaces/terminal/components/MessageList.js +280 -281
- package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
- package/src/interfaces/terminal/components/StatusBar.js +2 -0
- package/src/interfaces/terminal/index.js +168 -168
- package/src/interfaces/terminal/state/useAgentControl.js +496 -496
- package/src/interfaces/terminal/state/useAgents.js +537 -537
- package/src/interfaces/terminal/state/useMessages.js +629 -630
- package/src/interfaces/terminal/state/useTools.js +554 -554
- package/src/interfaces/terminal/utils/debugLogger.js +44 -44
- package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
- package/src/interfaces/webServer.js +7578 -7579
- package/src/interfaces/webServer.js.bak +7046 -7046
- package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
- package/src/modules/fileExplorer/controller.js +470 -469
- package/src/modules/fileExplorer/routes.js +285 -286
- package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
- package/src/modules/widget/__tests__/routes.test.js +677 -678
- package/src/modules/widget/__tests__/runtime.test.js +401 -401
- package/src/modules/widget/__tests__/versioning.test.js +309 -309
- package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
- package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
- package/src/modules/widget/routes.js +435 -435
- package/src/modules/widget/runtime/bundle.js +640 -640
- package/src/modules/widget/runtime/webComponentBundle.js +470 -470
- package/src/modules/widget/schema.js +182 -181
- package/src/modules/widget/widgetTool.js +1389 -1389
- package/src/services/__tests__/agentActivityService.test.js +401 -402
- package/src/services/__tests__/benchmarkService.test.js +184 -184
- package/src/services/__tests__/contextInjectionService.test.js +246 -246
- package/src/services/__tests__/conversationQuery.test.js +721 -723
- package/src/services/__tests__/credentialVault.test.js +469 -469
- package/src/services/__tests__/discordService.integration.test.js +638 -639
- package/src/services/__tests__/flowContextService.test.js +590 -590
- package/src/services/__tests__/memoryService.test.js +1 -1
- package/src/services/__tests__/messageSource.test.js +380 -380
- package/src/services/__tests__/modelRouterNaming.test.js +111 -111
- package/src/services/__tests__/projectDetector.test.js +34 -34
- package/src/services/__tests__/promptService.test.js +242 -242
- package/src/services/__tests__/telegramService.test.js +941 -941
- package/src/services/__tests__/tokenCountingService.test.js +48 -48
- package/src/services/agentActivityService.js +419 -420
- package/src/services/aiService.js +2997 -3001
- package/src/services/apiKeyManager.js +359 -359
- package/src/services/benchmarkService.js +196 -196
- package/src/services/codebaseKnowledgeService.js +2 -2
- package/src/services/composioService.js +738 -738
- package/src/services/conversationCompactionService.js +1258 -1257
- package/src/services/credentialVault.js +685 -685
- package/src/services/discordService.js +792 -793
- package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
- package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
- package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
- package/src/services/embeddings/autoRecall.js +218 -219
- package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
- package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
- package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
- package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
- package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
- package/src/services/embeddings/indexers/composioIndexer.js +279 -279
- package/src/services/embeddings/providerInterface.js +206 -206
- package/src/services/embeddings/providers/localProvider.js +11 -7
- package/src/services/embeddings/providers/openaiProvider.js +101 -101
- package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
- package/src/services/errorHandler.js +809 -809
- package/src/services/flowContextService.js +586 -586
- package/src/services/grounding/MockAdapter.js +125 -125
- package/src/services/modelRouterService.js +26 -31
- package/src/services/modelsService.js +322 -322
- package/src/services/ollamaService.js +452 -452
- package/src/services/projectDetector.js +403 -404
- package/src/services/promptService.js +418 -418
- package/src/services/qualityInspector.js +795 -795
- package/src/services/scheduleService.js +726 -726
- package/src/services/serviceRegistry.js +386 -386
- package/src/services/telegrafBot.js +174 -174
- package/src/services/telegramService.js +1972 -1972
- package/src/services/visualEditorBridge.js +1033 -1033
- package/src/services/visualEditorServer.js +1769 -1774
- package/src/services/whatsappService.js +667 -668
- package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
- package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
- package/src/tools/__tests__/agentDelayTool.test.js +342 -342
- package/src/tools/__tests__/baseTool.test.js +3 -3
- package/src/tools/__tests__/codeMapTool.test.js +915 -915
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
- package/src/tools/__tests__/fileTreeTool.test.js +274 -274
- package/src/tools/__tests__/filesystemTool.test.js +815 -815
- package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
- package/src/tools/__tests__/imageTool.validator.test.js +194 -194
- package/src/tools/__tests__/jobDoneTool.test.js +580 -581
- package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
- package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
- package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
- package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
- package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
- package/src/tools/__tests__/memoryTool.test.js +1 -1
- package/src/tools/__tests__/seekTool.test.js +282 -282
- package/src/tools/__tests__/skillsTool.search.test.js +164 -164
- package/src/tools/__tests__/skillsTool.test.js +226 -226
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
- package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
- package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
- package/src/tools/__tests__/taskManagerTool.test.js +866 -866
- package/src/tools/__tests__/terminalTool.test.js +448 -448
- package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
- package/src/tools/__tests__/userPromptTool.test.js +297 -297
- package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
- package/src/tools/__tests__/webTool.e2e.test.js +609 -603
- package/src/tools/__tests__/webTool.unit.test.js +195 -195
- package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
- package/src/tools/agentCommunicationTool.js +8 -10
- package/src/tools/agentDelayTool.js +496 -497
- package/src/tools/asyncToolManager.js +602 -603
- package/src/tools/baseTool.js +12 -11
- package/src/tools/cloneDetectionTool.js +576 -581
- package/src/tools/codeMapTool.js +0 -6
- package/src/tools/composioTool.js +617 -617
- package/src/tools/dependencyResolverTool.js +1211 -1212
- package/src/tools/desktop/DesktopTool.js +629 -638
- package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
- package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
- package/src/tools/desktop/__tests__/osController.test.js +364 -364
- package/src/tools/desktop/osController.js +491 -491
- package/src/tools/docxTool.js +623 -623
- package/src/tools/excelTool.js +636 -636
- package/src/tools/fileContentReplaceTool.js +5 -7
- package/src/tools/fileSystemTool.js +12 -19
- package/src/tools/fileTreeTool.js +840 -840
- package/src/tools/foundryWebSearchTool.js +273 -273
- package/src/tools/helpTool.js +198 -198
- package/src/tools/imageTool.js +1397 -1397
- package/src/tools/importAnalyzerTool.js +1056 -1056
- package/src/tools/jobDoneTool.js +495 -495
- package/src/tools/memoryTool.js +1 -1
- package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
- package/src/tools/office/pres/archetypes/agenda.js +61 -61
- package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
- package/src/tools/office/pres/archetypes/bigStat.js +140 -142
- package/src/tools/office/pres/archetypes/closing.js +70 -70
- package/src/tools/office/pres/archetypes/hero.js +70 -70
- package/src/tools/office/pres/archetypes/productHero.js +93 -94
- package/src/tools/office/pres/archetypes/table.js +73 -74
- package/src/tools/office/pres/backgrounds/orb.js +66 -66
- package/src/tools/office/pres/components.js +422 -423
- package/src/tools/officeTool.js +441 -441
- package/src/tools/pdfTool.js +625 -627
- package/src/tools/platformControlTool.js +1081 -1081
- package/src/tools/seekTool.js +917 -918
- package/src/tools/skillsTool.js +1 -1
- package/src/tools/staticAnalysisTool.js +2143 -2146
- package/src/tools/taskManagerTool.js +3324 -3324
- package/src/tools/terminalTool.js +2615 -2618
- package/src/tools/videoTool.js +1303 -1303
- package/src/tools/visionTool.js +508 -508
- package/src/tools/visualEditorTool.js +1289 -1290
- package/src/tools/webTool.js +3368 -3368
- package/src/tools/whatsappTool.js +464 -464
- package/src/types/__tests__/agent.test.js +499 -499
- package/src/types/__tests__/contextReference.test.js +606 -606
- package/src/types/__tests__/conversation.test.js +555 -555
- package/src/types/__tests__/toolCommand.test.js +584 -584
- package/src/types/contextReference.js +974 -971
- package/src/types/conversation.js +729 -729
- package/src/types/toolCommand.js +746 -746
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
- package/src/utilities/__tests__/auditReport.test.js +328 -328
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
- package/src/utilities/__tests__/jsonRepair.test.js +103 -104
- package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
- package/src/utilities/__tests__/platformUtils.test.js +80 -87
- package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
- package/src/utilities/__tests__/toolConstants.test.js +92 -94
- package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
- package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
- package/src/utilities/attachmentValidator.js +284 -288
- package/src/utilities/authCache.js.backup-1779570472481 +121 -121
- package/src/utilities/browserStealth.js +631 -630
- package/src/utilities/configManager.js +616 -617
- package/src/utilities/directoryAccessManager.js +564 -565
- package/src/utilities/fileProcessor.js +308 -307
- package/src/utilities/humanBehavior.js +454 -453
- package/src/utilities/logger.js +479 -479
- package/src/utilities/structuredFileValidator.js +696 -699
- package/src/utilities/tagParser.js +5 -10
- package/src/utilities/userDataDir.js +308 -308
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
- package/node_modules/minipass/LICENSE +0 -15
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
|
@@ -1,1257 +1,1258 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConversationCompactionService - Intelligent conversation compactization
|
|
3
|
-
*
|
|
4
|
-
* Purpose:
|
|
5
|
-
* - Compress long conversations while preserving critical information
|
|
6
|
-
* - AI-based summarization with sandwich approach (beginning + summary + end)
|
|
7
|
-
* - Multi-pass compaction (up to 3 passes) when a single pass isn't enough
|
|
8
|
-
* - Model switching support via best-existing-conversation selection
|
|
9
|
-
* - Compaction model validation against live model catalog
|
|
10
|
-
*
|
|
11
|
-
* Strategy:
|
|
12
|
-
* - Summarization only (sandwich approach):
|
|
13
|
-
* Keep beginning messages + AI summary of middle + end messages
|
|
14
|
-
* Middle segment always >= 50% of total messages
|
|
15
|
-
* Multi-pass: if result is still too large, re-summarize up to MAX_COMPACTION_PASSES times
|
|
16
|
-
*
|
|
17
|
-
* Model Switch Behavior:
|
|
18
|
-
* - Instead of truncation, find the best existing compacted conversation
|
|
19
|
-
* from another model and summarize that for the target model
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
COMPACTION_CONFIG,
|
|
24
|
-
COMPACTION_STRATEGIES,
|
|
25
|
-
} from '../utilities/constants.js';
|
|
26
|
-
|
|
27
|
-
class ConversationCompactionService {
|
|
28
|
-
constructor(tokenCountingService, aiService, logger) {
|
|
29
|
-
this.tokenCountingService = tokenCountingService;
|
|
30
|
-
this.aiService = aiService;
|
|
31
|
-
this.logger = logger;
|
|
32
|
-
|
|
33
|
-
// Models service for runtime validation (injected after construction)
|
|
34
|
-
this.modelsService = null;
|
|
35
|
-
|
|
36
|
-
// Round-robin index for compaction model selection
|
|
37
|
-
this.compactionModelIndex = 0;
|
|
38
|
-
|
|
39
|
-
// Summary generation prompt template
|
|
40
|
-
this.summaryPromptTemplate = this._createSummaryPromptTemplate();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Inject models service for runtime model validation
|
|
45
|
-
* @param {ModelsService} modelsService - Models service instance
|
|
46
|
-
*/
|
|
47
|
-
setModelsService(modelsService) {
|
|
48
|
-
this.modelsService = modelsService;
|
|
49
|
-
this.logger.info('ModelsService injected into compaction service');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get validated compaction models — filters COMPACTION_MODELS against live catalog
|
|
54
|
-
* @returns {string[]} Array of model names that are both recommended and available
|
|
55
|
-
* @private
|
|
56
|
-
*/
|
|
57
|
-
_getValidatedCompactionModels() {
|
|
58
|
-
const recommendedModels = COMPACTION_CONFIG.COMPACTION_MODELS || [];
|
|
59
|
-
|
|
60
|
-
if (!this.modelsService) {
|
|
61
|
-
this.logger.debug('No modelsService available, using all recommended compaction models');
|
|
62
|
-
return recommendedModels;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const availableNames = this.modelsService.getAvailableModelNames();
|
|
67
|
-
if (!availableNames || availableNames.length === 0) {
|
|
68
|
-
this.logger.warn('ModelsService returned no models, using all recommended compaction models');
|
|
69
|
-
return recommendedModels;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const availableSet = new Set(availableNames);
|
|
73
|
-
const validated = recommendedModels.filter(m => availableSet.has(m));
|
|
74
|
-
|
|
75
|
-
if (validated.length > 0) {
|
|
76
|
-
this.logger.debug('Compaction models validated against live catalog', {
|
|
77
|
-
recommended: recommendedModels.length,
|
|
78
|
-
available: validated.length,
|
|
79
|
-
validated
|
|
80
|
-
});
|
|
81
|
-
return validated;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// No recommended models match — pick the available model with the largest context window
|
|
85
|
-
this.logger.warn('No recommended compaction models found in catalog, selecting largest-context available model');
|
|
86
|
-
const models = this.modelsService.getModels();
|
|
87
|
-
const chatModels = models.filter(m => m.type === 'chat' || !m.type);
|
|
88
|
-
|
|
89
|
-
if (chatModels.length === 0) {
|
|
90
|
-
this.logger.error('No chat models available at all, falling back to recommended list');
|
|
91
|
-
return recommendedModels;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Sort by context window descending
|
|
95
|
-
chatModels.sort((a, b) => (b.contextWindow || 0) - (a.contextWindow || 0));
|
|
96
|
-
const fallbackModel = chatModels[0].name;
|
|
97
|
-
|
|
98
|
-
this.logger.info('Using fallback compaction model from catalog', {
|
|
99
|
-
model: fallbackModel,
|
|
100
|
-
contextWindow: chatModels[0].contextWindow
|
|
101
|
-
});
|
|
102
|
-
return [fallbackModel];
|
|
103
|
-
|
|
104
|
-
} catch (error) {
|
|
105
|
-
this.logger.warn('Failed to validate compaction models against catalog', {
|
|
106
|
-
error: error.message
|
|
107
|
-
});
|
|
108
|
-
return recommendedModels;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get next compaction model using round-robin from validated models
|
|
114
|
-
* @param {string[]} models - Validated model list
|
|
115
|
-
* @param {number} offset - Offset from current index
|
|
116
|
-
* @returns {string} Model name
|
|
117
|
-
* @private
|
|
118
|
-
*/
|
|
119
|
-
_getNextCompactionModel(models, offset = 0) {
|
|
120
|
-
if (!models || models.length === 0) {
|
|
121
|
-
throw new Error('No compaction models available');
|
|
122
|
-
}
|
|
123
|
-
const index = (this.compactionModelIndex + offset) % models.length;
|
|
124
|
-
return models[index];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Advance the round-robin index
|
|
129
|
-
* @private
|
|
130
|
-
*/
|
|
131
|
-
_advanceCompactionModelIndex() {
|
|
132
|
-
const models = COMPACTION_CONFIG.COMPACTION_MODELS || [];
|
|
133
|
-
if (models.length > 0) {
|
|
134
|
-
this.compactionModelIndex = (this.compactionModelIndex + 1) % models.length;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Main compaction entry point
|
|
140
|
-
* @param {Array} messages - Original messages array
|
|
141
|
-
* @param {string} currentModel - Current model being used
|
|
142
|
-
* @param {string} targetModel - Target model (may differ if switching)
|
|
143
|
-
* @param {Object} options - Compaction options
|
|
144
|
-
* @param {Map} [options.compactedConversations] - Map of modelId → compactedMessages (for model switch)
|
|
145
|
-
* @returns {Promise<Object>} Compaction result with messages and metadata
|
|
146
|
-
*/
|
|
147
|
-
async compactConversation(messages, currentModel, targetModel, options = {}) {
|
|
148
|
-
const startTime = Date.now();
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
// Validate inputs
|
|
152
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
153
|
-
throw new Error('Messages array is required and cannot be empty');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Split oversized messages into smaller chunks before compaction.
|
|
157
|
-
// This increases message count so the sandwich strategy can push
|
|
158
|
-
// oversized content into the summarizable middle segment.
|
|
159
|
-
const splitResult = this._splitOversizedMessages(messages);
|
|
160
|
-
let wasSplit = splitResult.wasSplit;
|
|
161
|
-
|
|
162
|
-
const minMessages = options.emergency ? 4 : COMPACTION_CONFIG.MIN_MESSAGES_FOR_COMPACTION;
|
|
163
|
-
|
|
164
|
-
if (splitResult.messages.length < minMessages) {
|
|
165
|
-
this.logger.warn('Too few messages for compaction', {
|
|
166
|
-
messageCount: splitResult.messages.length,
|
|
167
|
-
originalCount: messages.length,
|
|
168
|
-
minimum: minMessages,
|
|
169
|
-
emergency: !!options.emergency
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
compactedMessages: messages,
|
|
174
|
-
strategy: 'none',
|
|
175
|
-
originalTokenCount: 0,
|
|
176
|
-
compactedTokenCount: 0,
|
|
177
|
-
reductionPercent: 0,
|
|
178
|
-
skipped: true,
|
|
179
|
-
reason: 'Too few messages'
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Determine if model switch and pick best starting conversation
|
|
184
|
-
const isModelSwitch = currentModel !== targetModel;
|
|
185
|
-
let messagesToCompact = splitResult.messages;
|
|
186
|
-
|
|
187
|
-
if (isModelSwitch && options.compactedConversations) {
|
|
188
|
-
const bestConversation = this._findBestConversationForModelSwitch(
|
|
189
|
-
options.compactedConversations,
|
|
190
|
-
targetModel
|
|
191
|
-
);
|
|
192
|
-
if (bestConversation) {
|
|
193
|
-
// Split the best conversation too (it may contain oversized messages)
|
|
194
|
-
const bestSplit = this._splitOversizedMessages(bestConversation);
|
|
195
|
-
messagesToCompact = bestSplit.messages;
|
|
196
|
-
wasSplit = wasSplit || bestSplit.wasSplit;
|
|
197
|
-
this.logger.info('Using best existing conversation for model switch', {
|
|
198
|
-
originalMessages: messages.length,
|
|
199
|
-
bestConversationMessages: bestConversation.length,
|
|
200
|
-
currentModel,
|
|
201
|
-
targetModel
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
this.logger.info('Starting conversation compaction', {
|
|
207
|
-
messageCount: messagesToCompact.length,
|
|
208
|
-
currentModel,
|
|
209
|
-
targetModel,
|
|
210
|
-
strategy: COMPACTION_STRATEGIES.SUMMARIZATION,
|
|
211
|
-
isModelSwitch
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Execute multi-pass summarization
|
|
215
|
-
const result = await this._compactWithMultiPassSummarization(
|
|
216
|
-
messagesToCompact,
|
|
217
|
-
targetModel,
|
|
218
|
-
{ ...options, wasSplit }
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// Add execution metadata
|
|
222
|
-
const executionTime = Date.now() - startTime;
|
|
223
|
-
result.executionTime = executionTime;
|
|
224
|
-
result.timestamp = new Date().toISOString();
|
|
225
|
-
|
|
226
|
-
this.logger.info('Compaction completed successfully', {
|
|
227
|
-
strategy: result.strategy,
|
|
228
|
-
originalMessages: messagesToCompact.length,
|
|
229
|
-
compactedMessages: result.compactedMessages.length,
|
|
230
|
-
originalTokens: result.originalTokenCount,
|
|
231
|
-
compactedTokens: result.compactedTokenCount,
|
|
232
|
-
reductionPercent: result.reductionPercent.toFixed(2),
|
|
233
|
-
passes: result.passes,
|
|
234
|
-
executionTime: `${executionTime}ms`
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return result;
|
|
238
|
-
|
|
239
|
-
} catch (error) {
|
|
240
|
-
const executionTime = Date.now() - startTime;
|
|
241
|
-
|
|
242
|
-
this.logger.error('Compaction failed', {
|
|
243
|
-
error: error.message,
|
|
244
|
-
messageCount: messages.length,
|
|
245
|
-
currentModel,
|
|
246
|
-
targetModel,
|
|
247
|
-
executionTime: `${executionTime}ms`
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
throw error;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Find the best existing compacted conversation for model switching.
|
|
256
|
-
* Prefers the conversation from a model whose context window is the largest
|
|
257
|
-
* C where C < targetModel's context window.
|
|
258
|
-
* Falls back to the shortest compacted conversation.
|
|
259
|
-
*
|
|
260
|
-
* @param {Map} compactedConversations - Map of modelId → compactedMessages
|
|
261
|
-
* @param {string} targetModel - Target model name
|
|
262
|
-
* @returns {Array|null} Best conversation messages, or null
|
|
263
|
-
* @private
|
|
264
|
-
*/
|
|
265
|
-
_findBestConversationForModelSwitch(compactedConversations, targetModel) {
|
|
266
|
-
if (!compactedConversations || compactedConversations.size === 0) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const targetContextWindow = this.tokenCountingService.getModelContextWindow(targetModel);
|
|
271
|
-
|
|
272
|
-
// Collect candidates: conversations that have messages
|
|
273
|
-
const candidates = [];
|
|
274
|
-
for (const [modelId, msgs] of compactedConversations) {
|
|
275
|
-
if (Array.isArray(msgs) && msgs.length > 0) {
|
|
276
|
-
const contextWindow = this.tokenCountingService.getModelContextWindow(modelId);
|
|
277
|
-
candidates.push({ modelId, messages: msgs, contextWindow });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (candidates.length === 0) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Prefer: largest context window that is < targetModel's context window
|
|
286
|
-
// (these conversations were already compacted to fit a smaller window, so they'll fit the target)
|
|
287
|
-
const smallerCandidates = candidates
|
|
288
|
-
.filter(c => c.contextWindow < targetContextWindow)
|
|
289
|
-
.sort((a, b) => b.contextWindow - a.contextWindow);
|
|
290
|
-
|
|
291
|
-
if (smallerCandidates.length > 0) {
|
|
292
|
-
const best = smallerCandidates[0];
|
|
293
|
-
this.logger.debug('Best conversation for model switch: largest smaller context', {
|
|
294
|
-
selectedModel: best.modelId,
|
|
295
|
-
selectedContextWindow: best.contextWindow,
|
|
296
|
-
targetContextWindow,
|
|
297
|
-
messageCount: best.messages.length
|
|
298
|
-
});
|
|
299
|
-
return best.messages;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Fallback: shortest conversation (fewest messages)
|
|
303
|
-
candidates.sort((a, b) => a.messages.length - b.messages.length);
|
|
304
|
-
const shortest = candidates[0];
|
|
305
|
-
this.logger.debug('Best conversation for model switch: shortest', {
|
|
306
|
-
selectedModel: shortest.modelId,
|
|
307
|
-
messageCount: shortest.messages.length
|
|
308
|
-
});
|
|
309
|
-
return shortest.messages;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Multi-pass summarization: runs up to MAX_COMPACTION_PASSES passes.
|
|
314
|
-
* After each pass, checks if the result fits within the compaction threshold.
|
|
315
|
-
* If it fits, returns immediately; otherwise re-compacts the compacted result.
|
|
316
|
-
*
|
|
317
|
-
* @param {Array} messages - Messages to compact
|
|
318
|
-
* @param {string} model - Target model name
|
|
319
|
-
* @param {Object} options - Compaction options
|
|
320
|
-
* @returns {Promise<Object>} Compaction result
|
|
321
|
-
* @private
|
|
322
|
-
*/
|
|
323
|
-
async _compactWithMultiPassSummarization(messages, model, options) {
|
|
324
|
-
const maxPasses = COMPACTION_CONFIG.MAX_COMPACTION_PASSES;
|
|
325
|
-
const contextWindow = this.tokenCountingService.getModelContextWindow(model);
|
|
326
|
-
const maxOutputTokens = this.tokenCountingService.getModelMaxOutputTokens(model);
|
|
327
|
-
const threshold = COMPACTION_CONFIG.DEFAULT_THRESHOLD;
|
|
328
|
-
const wasSplit = options.wasSplit || false;
|
|
329
|
-
|
|
330
|
-
let currentMessages = messages;
|
|
331
|
-
let result = null;
|
|
332
|
-
|
|
333
|
-
for (let pass = 1; pass <= maxPasses; pass++) {
|
|
334
|
-
this.logger.info(`Compaction pass ${pass}/${maxPasses}`, {
|
|
335
|
-
inputMessages: currentMessages.length,
|
|
336
|
-
model,
|
|
337
|
-
contextWindow,
|
|
338
|
-
wasSplit
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
result = await this._executeSingleSummarizationPass(currentMessages, model, options, pass);
|
|
342
|
-
|
|
343
|
-
// Check if result fits within threshold
|
|
344
|
-
const compactedTokens = this.tokenCountingService.getConversationTokenCount(
|
|
345
|
-
result.compactedMessages,
|
|
346
|
-
model
|
|
347
|
-
);
|
|
348
|
-
result.compactedTokenCount = compactedTokens;
|
|
349
|
-
|
|
350
|
-
const fitsWithinThreshold = !this.tokenCountingService.shouldTriggerCompaction(
|
|
351
|
-
compactedTokens,
|
|
352
|
-
maxOutputTokens,
|
|
353
|
-
contextWindow,
|
|
354
|
-
threshold
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
this.logger.info(`Compaction pass ${pass} result`, {
|
|
358
|
-
compactedMessages: result.compactedMessages.length,
|
|
359
|
-
compactedTokens,
|
|
360
|
-
fitsWithinThreshold,
|
|
361
|
-
contextWindow,
|
|
362
|
-
threshold
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
if (fitsWithinThreshold) {
|
|
366
|
-
result.passes = pass;
|
|
367
|
-
return result;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// If not the last pass, use compacted result as input for next pass
|
|
371
|
-
// Split any oversized messages that survived compaction
|
|
372
|
-
if (pass < maxPasses) {
|
|
373
|
-
const reSplit = this._splitOversizedMessages(result.compactedMessages);
|
|
374
|
-
currentMessages = reSplit.messages;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Best effort after all passes
|
|
379
|
-
this.logger.warn('Compaction did not fit within threshold after all passes', {
|
|
380
|
-
passes: maxPasses,
|
|
381
|
-
finalTokens: result.compactedTokenCount,
|
|
382
|
-
contextWindow,
|
|
383
|
-
threshold
|
|
384
|
-
});
|
|
385
|
-
result.passes = maxPasses;
|
|
386
|
-
return result;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Execute a single summarization pass (sandwich approach).
|
|
391
|
-
* Preserves beginning + AI summary of middle + end.
|
|
392
|
-
*
|
|
393
|
-
* @param {Array} messages - Messages to compact
|
|
394
|
-
* @param {string} model - Target model name
|
|
395
|
-
* @param {Object} options - Compaction options
|
|
396
|
-
* @param {number} passNumber - Current pass number (1-based)
|
|
397
|
-
* @returns {Promise<Object>} Compaction result
|
|
398
|
-
* @private
|
|
399
|
-
*/
|
|
400
|
-
async _executeSingleSummarizationPass(messages, model, options, passNumber) {
|
|
401
|
-
const strategy = COMPACTION_STRATEGIES.SUMMARIZATION;
|
|
402
|
-
|
|
403
|
-
// Estimate original token count
|
|
404
|
-
const originalTokenCount = this.tokenCountingService.getConversationTokenCount(
|
|
405
|
-
messages,
|
|
406
|
-
model
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
// Identify segments (message-count-based)
|
|
410
|
-
// When oversized messages were split, use a small end segment so
|
|
411
|
-
// most chunks land in the middle for summarization
|
|
412
|
-
const segments = this._identifySegments(messages, { wasSplit: options.wasSplit });
|
|
413
|
-
|
|
414
|
-
this.logger.info(`Pass ${passNumber}: segments identified`, {
|
|
415
|
-
summarizedMessages: segments.middle.length,
|
|
416
|
-
keptMessages: segments.end.length,
|
|
417
|
-
totalMessages: messages.length
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Generate summary of middle segment
|
|
421
|
-
let summary;
|
|
422
|
-
try {
|
|
423
|
-
summary = await this._generateSummary(
|
|
424
|
-
segments.middle,
|
|
425
|
-
model,
|
|
426
|
-
{
|
|
427
|
-
...options,
|
|
428
|
-
middleStartIndex: segments.middleStartIndex,
|
|
429
|
-
middleEndIndex: segments.middleEndIndex,
|
|
430
|
-
passNumber
|
|
431
|
-
}
|
|
432
|
-
);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
if (error.code === 'ALL_MODELS_EXHAUSTED') {
|
|
435
|
-
// All AI models failed — use structural fallback
|
|
436
|
-
this.logger.warn('All summarization models exhausted, using structural fallback compaction');
|
|
437
|
-
const fallbackResult = this._performFallbackCompaction(messages);
|
|
438
|
-
|
|
439
|
-
const compactedTokenCount = this.tokenCountingService.getConversationTokenCount(
|
|
440
|
-
fallbackResult.compactedMessages,
|
|
441
|
-
model
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
const reductionPercent = originalTokenCount > 0
|
|
445
|
-
? ((originalTokenCount - compactedTokenCount) / originalTokenCount) * 100
|
|
446
|
-
: 0;
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
compactedMessages: fallbackResult.compactedMessages,
|
|
450
|
-
strategy: 'structural_fallback',
|
|
451
|
-
originalTokenCount,
|
|
452
|
-
compactedTokenCount,
|
|
453
|
-
reductionPercent,
|
|
454
|
-
segments: {
|
|
455
|
-
beginningCount: segments.beginning.length,
|
|
456
|
-
middleCount: segments.middle.length,
|
|
457
|
-
endCount: segments.end.length,
|
|
458
|
-
summaryInserted: true,
|
|
459
|
-
fallback: true
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
throw error;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Construct compacted conversation
|
|
467
|
-
const compactedMessages = [
|
|
468
|
-
...segments.beginning,
|
|
469
|
-
summary,
|
|
470
|
-
...segments.end
|
|
471
|
-
];
|
|
472
|
-
|
|
473
|
-
// Count tokens in compacted conversation
|
|
474
|
-
const compactedTokenCount = this.tokenCountingService.getConversationTokenCount(
|
|
475
|
-
compactedMessages,
|
|
476
|
-
model
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
// Calculate reduction
|
|
480
|
-
const reductionPercent = originalTokenCount > 0
|
|
481
|
-
? ((originalTokenCount - compactedTokenCount) / originalTokenCount) * 100
|
|
482
|
-
: 0;
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
compactedMessages,
|
|
486
|
-
strategy,
|
|
487
|
-
originalTokenCount,
|
|
488
|
-
compactedTokenCount,
|
|
489
|
-
reductionPercent,
|
|
490
|
-
segments: {
|
|
491
|
-
beginningCount: segments.beginning.length,
|
|
492
|
-
middleCount: segments.middle.length,
|
|
493
|
-
endCount: segments.end.length,
|
|
494
|
-
summaryInserted: true
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Calculate the maximum characters the summarizer can handle in a single call.
|
|
501
|
-
* Uses the largest available compaction model's context window minus overhead.
|
|
502
|
-
*
|
|
503
|
-
* @returns {number} Maximum characters per summarization call
|
|
504
|
-
* @private
|
|
505
|
-
*/
|
|
506
|
-
_calculateSummarizerCapacity() {
|
|
507
|
-
const models = this._getValidatedCompactionModels();
|
|
508
|
-
const contextWindows = { ...(COMPACTION_CONFIG.MODEL_CONTEXT_WINDOWS || {}) };
|
|
509
|
-
|
|
510
|
-
// Augment with live data from modelsService if available
|
|
511
|
-
if (this.modelsService) {
|
|
512
|
-
try {
|
|
513
|
-
const allModels = this.modelsService.getModels();
|
|
514
|
-
for (const m of allModels) {
|
|
515
|
-
if (m.contextWindow) {
|
|
516
|
-
contextWindows[m.name] = m.contextWindow;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
} catch
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const largestContext = Math.max(...models.map(m => contextWindows[m] || 128000));
|
|
523
|
-
|
|
524
|
-
const usableTokens = largestContext
|
|
525
|
-
- (COMPACTION_CONFIG.SUMMARIZER_SYSTEM_PROMPT_OVERHEAD || 500)
|
|
526
|
-
- (COMPACTION_CONFIG.SUMMARIZER_TEMPLATE_OVERHEAD || 800)
|
|
527
|
-
- (COMPACTION_CONFIG.MAX_SUMMARY_TOKENS || 8000)
|
|
528
|
-
- (COMPACTION_CONFIG.SUMMARIZER_SAFETY_MARGIN || 5000);
|
|
529
|
-
|
|
530
|
-
const effectiveTokens = Math.max(10000, usableTokens);
|
|
531
|
-
return effectiveTokens * (COMPACTION_CONFIG.CHARS_PER_TOKEN_ESTIMATE || 3);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Identify conversation segments using token-budget sizing.
|
|
536
|
-
* The middle segment starts at the 15% char mark and extends forward
|
|
537
|
-
* until hitting either the summarizer's capacity or 35% of total chars.
|
|
538
|
-
* This guarantees the middle always fits within the summarizer's context window.
|
|
539
|
-
*
|
|
540
|
-
* @param {Array} messages - Messages array
|
|
541
|
-
* @param {Object} [options] - Segmentation options
|
|
542
|
-
* @param {boolean} [options.wasSplit] - Whether oversized messages were split (unused in new logic)
|
|
543
|
-
* @returns {Object} { beginning, middle, end, middleStartIndex, middleEndIndex }
|
|
544
|
-
* @private
|
|
545
|
-
*/
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
let
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
let
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
*
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
.filter(m => (m.
|
|
818
|
-
.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
this.
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
models:
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
exhaustedError
|
|
902
|
-
exhaustedError.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
* @
|
|
915
|
-
* @
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (m
|
|
925
|
-
if (m.
|
|
926
|
-
return
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
*
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
parts
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
* @
|
|
1030
|
-
* @
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
* @param {
|
|
1094
|
-
* @
|
|
1095
|
-
* @
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
remaining
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
* @
|
|
1163
|
-
* @
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if (msg.role === '
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (c.startsWith('[
|
|
1172
|
-
return '
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
[
|
|
1207
|
-
[
|
|
1208
|
-
[
|
|
1209
|
-
[
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
- Do NOT
|
|
1226
|
-
- Do NOT
|
|
1227
|
-
-
|
|
1228
|
-
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ConversationCompactionService - Intelligent conversation compactization
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Compress long conversations while preserving critical information
|
|
6
|
+
* - AI-based summarization with sandwich approach (beginning + summary + end)
|
|
7
|
+
* - Multi-pass compaction (up to 3 passes) when a single pass isn't enough
|
|
8
|
+
* - Model switching support via best-existing-conversation selection
|
|
9
|
+
* - Compaction model validation against live model catalog
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* - Summarization only (sandwich approach):
|
|
13
|
+
* Keep beginning messages + AI summary of middle + end messages
|
|
14
|
+
* Middle segment always >= 50% of total messages
|
|
15
|
+
* Multi-pass: if result is still too large, re-summarize up to MAX_COMPACTION_PASSES times
|
|
16
|
+
*
|
|
17
|
+
* Model Switch Behavior:
|
|
18
|
+
* - Instead of truncation, find the best existing compacted conversation
|
|
19
|
+
* from another model and summarize that for the target model
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
COMPACTION_CONFIG,
|
|
24
|
+
COMPACTION_STRATEGIES,
|
|
25
|
+
} from '../utilities/constants.js';
|
|
26
|
+
|
|
27
|
+
class ConversationCompactionService {
|
|
28
|
+
constructor(tokenCountingService, aiService, logger) {
|
|
29
|
+
this.tokenCountingService = tokenCountingService;
|
|
30
|
+
this.aiService = aiService;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
|
|
33
|
+
// Models service for runtime validation (injected after construction)
|
|
34
|
+
this.modelsService = null;
|
|
35
|
+
|
|
36
|
+
// Round-robin index for compaction model selection
|
|
37
|
+
this.compactionModelIndex = 0;
|
|
38
|
+
|
|
39
|
+
// Summary generation prompt template
|
|
40
|
+
this.summaryPromptTemplate = this._createSummaryPromptTemplate();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inject models service for runtime model validation
|
|
45
|
+
* @param {ModelsService} modelsService - Models service instance
|
|
46
|
+
*/
|
|
47
|
+
setModelsService(modelsService) {
|
|
48
|
+
this.modelsService = modelsService;
|
|
49
|
+
this.logger.info('ModelsService injected into compaction service');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get validated compaction models — filters COMPACTION_MODELS against live catalog
|
|
54
|
+
* @returns {string[]} Array of model names that are both recommended and available
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
_getValidatedCompactionModels() {
|
|
58
|
+
const recommendedModels = COMPACTION_CONFIG.COMPACTION_MODELS || [];
|
|
59
|
+
|
|
60
|
+
if (!this.modelsService) {
|
|
61
|
+
this.logger.debug('No modelsService available, using all recommended compaction models');
|
|
62
|
+
return recommendedModels;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const availableNames = this.modelsService.getAvailableModelNames();
|
|
67
|
+
if (!availableNames || availableNames.length === 0) {
|
|
68
|
+
this.logger.warn('ModelsService returned no models, using all recommended compaction models');
|
|
69
|
+
return recommendedModels;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const availableSet = new Set(availableNames);
|
|
73
|
+
const validated = recommendedModels.filter(m => availableSet.has(m));
|
|
74
|
+
|
|
75
|
+
if (validated.length > 0) {
|
|
76
|
+
this.logger.debug('Compaction models validated against live catalog', {
|
|
77
|
+
recommended: recommendedModels.length,
|
|
78
|
+
available: validated.length,
|
|
79
|
+
validated
|
|
80
|
+
});
|
|
81
|
+
return validated;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// No recommended models match — pick the available model with the largest context window
|
|
85
|
+
this.logger.warn('No recommended compaction models found in catalog, selecting largest-context available model');
|
|
86
|
+
const models = this.modelsService.getModels();
|
|
87
|
+
const chatModels = models.filter(m => m.type === 'chat' || !m.type);
|
|
88
|
+
|
|
89
|
+
if (chatModels.length === 0) {
|
|
90
|
+
this.logger.error('No chat models available at all, falling back to recommended list');
|
|
91
|
+
return recommendedModels;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Sort by context window descending
|
|
95
|
+
chatModels.sort((a, b) => (b.contextWindow || 0) - (a.contextWindow || 0));
|
|
96
|
+
const fallbackModel = chatModels[0].name;
|
|
97
|
+
|
|
98
|
+
this.logger.info('Using fallback compaction model from catalog', {
|
|
99
|
+
model: fallbackModel,
|
|
100
|
+
contextWindow: chatModels[0].contextWindow
|
|
101
|
+
});
|
|
102
|
+
return [fallbackModel];
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.logger.warn('Failed to validate compaction models against catalog', {
|
|
106
|
+
error: error.message
|
|
107
|
+
});
|
|
108
|
+
return recommendedModels;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get next compaction model using round-robin from validated models
|
|
114
|
+
* @param {string[]} models - Validated model list
|
|
115
|
+
* @param {number} offset - Offset from current index
|
|
116
|
+
* @returns {string} Model name
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
_getNextCompactionModel(models, offset = 0) {
|
|
120
|
+
if (!models || models.length === 0) {
|
|
121
|
+
throw new Error('No compaction models available');
|
|
122
|
+
}
|
|
123
|
+
const index = (this.compactionModelIndex + offset) % models.length;
|
|
124
|
+
return models[index];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Advance the round-robin index
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
_advanceCompactionModelIndex() {
|
|
132
|
+
const models = COMPACTION_CONFIG.COMPACTION_MODELS || [];
|
|
133
|
+
if (models.length > 0) {
|
|
134
|
+
this.compactionModelIndex = (this.compactionModelIndex + 1) % models.length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Main compaction entry point
|
|
140
|
+
* @param {Array} messages - Original messages array
|
|
141
|
+
* @param {string} currentModel - Current model being used
|
|
142
|
+
* @param {string} targetModel - Target model (may differ if switching)
|
|
143
|
+
* @param {Object} options - Compaction options
|
|
144
|
+
* @param {Map} [options.compactedConversations] - Map of modelId → compactedMessages (for model switch)
|
|
145
|
+
* @returns {Promise<Object>} Compaction result with messages and metadata
|
|
146
|
+
*/
|
|
147
|
+
async compactConversation(messages, currentModel, targetModel, options = {}) {
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Validate inputs
|
|
152
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
153
|
+
throw new Error('Messages array is required and cannot be empty');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Split oversized messages into smaller chunks before compaction.
|
|
157
|
+
// This increases message count so the sandwich strategy can push
|
|
158
|
+
// oversized content into the summarizable middle segment.
|
|
159
|
+
const splitResult = this._splitOversizedMessages(messages);
|
|
160
|
+
let wasSplit = splitResult.wasSplit;
|
|
161
|
+
|
|
162
|
+
const minMessages = options.emergency ? 4 : COMPACTION_CONFIG.MIN_MESSAGES_FOR_COMPACTION;
|
|
163
|
+
|
|
164
|
+
if (splitResult.messages.length < minMessages) {
|
|
165
|
+
this.logger.warn('Too few messages for compaction', {
|
|
166
|
+
messageCount: splitResult.messages.length,
|
|
167
|
+
originalCount: messages.length,
|
|
168
|
+
minimum: minMessages,
|
|
169
|
+
emergency: !!options.emergency
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
compactedMessages: messages,
|
|
174
|
+
strategy: 'none',
|
|
175
|
+
originalTokenCount: 0,
|
|
176
|
+
compactedTokenCount: 0,
|
|
177
|
+
reductionPercent: 0,
|
|
178
|
+
skipped: true,
|
|
179
|
+
reason: 'Too few messages'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Determine if model switch and pick best starting conversation
|
|
184
|
+
const isModelSwitch = currentModel !== targetModel;
|
|
185
|
+
let messagesToCompact = splitResult.messages;
|
|
186
|
+
|
|
187
|
+
if (isModelSwitch && options.compactedConversations) {
|
|
188
|
+
const bestConversation = this._findBestConversationForModelSwitch(
|
|
189
|
+
options.compactedConversations,
|
|
190
|
+
targetModel
|
|
191
|
+
);
|
|
192
|
+
if (bestConversation) {
|
|
193
|
+
// Split the best conversation too (it may contain oversized messages)
|
|
194
|
+
const bestSplit = this._splitOversizedMessages(bestConversation);
|
|
195
|
+
messagesToCompact = bestSplit.messages;
|
|
196
|
+
wasSplit = wasSplit || bestSplit.wasSplit;
|
|
197
|
+
this.logger.info('Using best existing conversation for model switch', {
|
|
198
|
+
originalMessages: messages.length,
|
|
199
|
+
bestConversationMessages: bestConversation.length,
|
|
200
|
+
currentModel,
|
|
201
|
+
targetModel
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.logger.info('Starting conversation compaction', {
|
|
207
|
+
messageCount: messagesToCompact.length,
|
|
208
|
+
currentModel,
|
|
209
|
+
targetModel,
|
|
210
|
+
strategy: COMPACTION_STRATEGIES.SUMMARIZATION,
|
|
211
|
+
isModelSwitch
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Execute multi-pass summarization
|
|
215
|
+
const result = await this._compactWithMultiPassSummarization(
|
|
216
|
+
messagesToCompact,
|
|
217
|
+
targetModel,
|
|
218
|
+
{ ...options, wasSplit }
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Add execution metadata
|
|
222
|
+
const executionTime = Date.now() - startTime;
|
|
223
|
+
result.executionTime = executionTime;
|
|
224
|
+
result.timestamp = new Date().toISOString();
|
|
225
|
+
|
|
226
|
+
this.logger.info('Compaction completed successfully', {
|
|
227
|
+
strategy: result.strategy,
|
|
228
|
+
originalMessages: messagesToCompact.length,
|
|
229
|
+
compactedMessages: result.compactedMessages.length,
|
|
230
|
+
originalTokens: result.originalTokenCount,
|
|
231
|
+
compactedTokens: result.compactedTokenCount,
|
|
232
|
+
reductionPercent: result.reductionPercent.toFixed(2),
|
|
233
|
+
passes: result.passes,
|
|
234
|
+
executionTime: `${executionTime}ms`
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const executionTime = Date.now() - startTime;
|
|
241
|
+
|
|
242
|
+
this.logger.error('Compaction failed', {
|
|
243
|
+
error: error.message,
|
|
244
|
+
messageCount: messages.length,
|
|
245
|
+
currentModel,
|
|
246
|
+
targetModel,
|
|
247
|
+
executionTime: `${executionTime}ms`
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Find the best existing compacted conversation for model switching.
|
|
256
|
+
* Prefers the conversation from a model whose context window is the largest
|
|
257
|
+
* C where C < targetModel's context window.
|
|
258
|
+
* Falls back to the shortest compacted conversation.
|
|
259
|
+
*
|
|
260
|
+
* @param {Map} compactedConversations - Map of modelId → compactedMessages
|
|
261
|
+
* @param {string} targetModel - Target model name
|
|
262
|
+
* @returns {Array|null} Best conversation messages, or null
|
|
263
|
+
* @private
|
|
264
|
+
*/
|
|
265
|
+
_findBestConversationForModelSwitch(compactedConversations, targetModel) {
|
|
266
|
+
if (!compactedConversations || compactedConversations.size === 0) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const targetContextWindow = this.tokenCountingService.getModelContextWindow(targetModel);
|
|
271
|
+
|
|
272
|
+
// Collect candidates: conversations that have messages
|
|
273
|
+
const candidates = [];
|
|
274
|
+
for (const [modelId, msgs] of compactedConversations) {
|
|
275
|
+
if (Array.isArray(msgs) && msgs.length > 0) {
|
|
276
|
+
const contextWindow = this.tokenCountingService.getModelContextWindow(modelId);
|
|
277
|
+
candidates.push({ modelId, messages: msgs, contextWindow });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (candidates.length === 0) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Prefer: largest context window that is < targetModel's context window
|
|
286
|
+
// (these conversations were already compacted to fit a smaller window, so they'll fit the target)
|
|
287
|
+
const smallerCandidates = candidates
|
|
288
|
+
.filter(c => c.contextWindow < targetContextWindow)
|
|
289
|
+
.sort((a, b) => b.contextWindow - a.contextWindow);
|
|
290
|
+
|
|
291
|
+
if (smallerCandidates.length > 0) {
|
|
292
|
+
const best = smallerCandidates[0];
|
|
293
|
+
this.logger.debug('Best conversation for model switch: largest smaller context', {
|
|
294
|
+
selectedModel: best.modelId,
|
|
295
|
+
selectedContextWindow: best.contextWindow,
|
|
296
|
+
targetContextWindow,
|
|
297
|
+
messageCount: best.messages.length
|
|
298
|
+
});
|
|
299
|
+
return best.messages;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fallback: shortest conversation (fewest messages)
|
|
303
|
+
candidates.sort((a, b) => a.messages.length - b.messages.length);
|
|
304
|
+
const shortest = candidates[0];
|
|
305
|
+
this.logger.debug('Best conversation for model switch: shortest', {
|
|
306
|
+
selectedModel: shortest.modelId,
|
|
307
|
+
messageCount: shortest.messages.length
|
|
308
|
+
});
|
|
309
|
+
return shortest.messages;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Multi-pass summarization: runs up to MAX_COMPACTION_PASSES passes.
|
|
314
|
+
* After each pass, checks if the result fits within the compaction threshold.
|
|
315
|
+
* If it fits, returns immediately; otherwise re-compacts the compacted result.
|
|
316
|
+
*
|
|
317
|
+
* @param {Array} messages - Messages to compact
|
|
318
|
+
* @param {string} model - Target model name
|
|
319
|
+
* @param {Object} options - Compaction options
|
|
320
|
+
* @returns {Promise<Object>} Compaction result
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
async _compactWithMultiPassSummarization(messages, model, options) {
|
|
324
|
+
const maxPasses = COMPACTION_CONFIG.MAX_COMPACTION_PASSES;
|
|
325
|
+
const contextWindow = this.tokenCountingService.getModelContextWindow(model);
|
|
326
|
+
const maxOutputTokens = this.tokenCountingService.getModelMaxOutputTokens(model);
|
|
327
|
+
const threshold = COMPACTION_CONFIG.DEFAULT_THRESHOLD;
|
|
328
|
+
const wasSplit = options.wasSplit || false;
|
|
329
|
+
|
|
330
|
+
let currentMessages = messages;
|
|
331
|
+
let result = null;
|
|
332
|
+
|
|
333
|
+
for (let pass = 1; pass <= maxPasses; pass++) {
|
|
334
|
+
this.logger.info(`Compaction pass ${pass}/${maxPasses}`, {
|
|
335
|
+
inputMessages: currentMessages.length,
|
|
336
|
+
model,
|
|
337
|
+
contextWindow,
|
|
338
|
+
wasSplit
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
result = await this._executeSingleSummarizationPass(currentMessages, model, options, pass);
|
|
342
|
+
|
|
343
|
+
// Check if result fits within threshold
|
|
344
|
+
const compactedTokens = this.tokenCountingService.getConversationTokenCount(
|
|
345
|
+
result.compactedMessages,
|
|
346
|
+
model
|
|
347
|
+
);
|
|
348
|
+
result.compactedTokenCount = compactedTokens;
|
|
349
|
+
|
|
350
|
+
const fitsWithinThreshold = !this.tokenCountingService.shouldTriggerCompaction(
|
|
351
|
+
compactedTokens,
|
|
352
|
+
maxOutputTokens,
|
|
353
|
+
contextWindow,
|
|
354
|
+
threshold
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
this.logger.info(`Compaction pass ${pass} result`, {
|
|
358
|
+
compactedMessages: result.compactedMessages.length,
|
|
359
|
+
compactedTokens,
|
|
360
|
+
fitsWithinThreshold,
|
|
361
|
+
contextWindow,
|
|
362
|
+
threshold
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (fitsWithinThreshold) {
|
|
366
|
+
result.passes = pass;
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// If not the last pass, use compacted result as input for next pass
|
|
371
|
+
// Split any oversized messages that survived compaction
|
|
372
|
+
if (pass < maxPasses) {
|
|
373
|
+
const reSplit = this._splitOversizedMessages(result.compactedMessages);
|
|
374
|
+
currentMessages = reSplit.messages;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Best effort after all passes
|
|
379
|
+
this.logger.warn('Compaction did not fit within threshold after all passes', {
|
|
380
|
+
passes: maxPasses,
|
|
381
|
+
finalTokens: result.compactedTokenCount,
|
|
382
|
+
contextWindow,
|
|
383
|
+
threshold
|
|
384
|
+
});
|
|
385
|
+
result.passes = maxPasses;
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Execute a single summarization pass (sandwich approach).
|
|
391
|
+
* Preserves beginning + AI summary of middle + end.
|
|
392
|
+
*
|
|
393
|
+
* @param {Array} messages - Messages to compact
|
|
394
|
+
* @param {string} model - Target model name
|
|
395
|
+
* @param {Object} options - Compaction options
|
|
396
|
+
* @param {number} passNumber - Current pass number (1-based)
|
|
397
|
+
* @returns {Promise<Object>} Compaction result
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
async _executeSingleSummarizationPass(messages, model, options, passNumber) {
|
|
401
|
+
const strategy = COMPACTION_STRATEGIES.SUMMARIZATION;
|
|
402
|
+
|
|
403
|
+
// Estimate original token count
|
|
404
|
+
const originalTokenCount = this.tokenCountingService.getConversationTokenCount(
|
|
405
|
+
messages,
|
|
406
|
+
model
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Identify segments (message-count-based)
|
|
410
|
+
// When oversized messages were split, use a small end segment so
|
|
411
|
+
// most chunks land in the middle for summarization
|
|
412
|
+
const segments = this._identifySegments(messages, { wasSplit: options.wasSplit });
|
|
413
|
+
|
|
414
|
+
this.logger.info(`Pass ${passNumber}: segments identified`, {
|
|
415
|
+
summarizedMessages: segments.middle.length,
|
|
416
|
+
keptMessages: segments.end.length,
|
|
417
|
+
totalMessages: messages.length
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Generate summary of middle segment
|
|
421
|
+
let summary;
|
|
422
|
+
try {
|
|
423
|
+
summary = await this._generateSummary(
|
|
424
|
+
segments.middle,
|
|
425
|
+
model,
|
|
426
|
+
{
|
|
427
|
+
...options,
|
|
428
|
+
middleStartIndex: segments.middleStartIndex,
|
|
429
|
+
middleEndIndex: segments.middleEndIndex,
|
|
430
|
+
passNumber
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error.code === 'ALL_MODELS_EXHAUSTED') {
|
|
435
|
+
// All AI models failed — use structural fallback
|
|
436
|
+
this.logger.warn('All summarization models exhausted, using structural fallback compaction');
|
|
437
|
+
const fallbackResult = this._performFallbackCompaction(messages);
|
|
438
|
+
|
|
439
|
+
const compactedTokenCount = this.tokenCountingService.getConversationTokenCount(
|
|
440
|
+
fallbackResult.compactedMessages,
|
|
441
|
+
model
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const reductionPercent = originalTokenCount > 0
|
|
445
|
+
? ((originalTokenCount - compactedTokenCount) / originalTokenCount) * 100
|
|
446
|
+
: 0;
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
compactedMessages: fallbackResult.compactedMessages,
|
|
450
|
+
strategy: 'structural_fallback',
|
|
451
|
+
originalTokenCount,
|
|
452
|
+
compactedTokenCount,
|
|
453
|
+
reductionPercent,
|
|
454
|
+
segments: {
|
|
455
|
+
beginningCount: segments.beginning.length,
|
|
456
|
+
middleCount: segments.middle.length,
|
|
457
|
+
endCount: segments.end.length,
|
|
458
|
+
summaryInserted: true,
|
|
459
|
+
fallback: true
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Construct compacted conversation
|
|
467
|
+
const compactedMessages = [
|
|
468
|
+
...segments.beginning,
|
|
469
|
+
summary,
|
|
470
|
+
...segments.end
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
// Count tokens in compacted conversation
|
|
474
|
+
const compactedTokenCount = this.tokenCountingService.getConversationTokenCount(
|
|
475
|
+
compactedMessages,
|
|
476
|
+
model
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Calculate reduction
|
|
480
|
+
const reductionPercent = originalTokenCount > 0
|
|
481
|
+
? ((originalTokenCount - compactedTokenCount) / originalTokenCount) * 100
|
|
482
|
+
: 0;
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
compactedMessages,
|
|
486
|
+
strategy,
|
|
487
|
+
originalTokenCount,
|
|
488
|
+
compactedTokenCount,
|
|
489
|
+
reductionPercent,
|
|
490
|
+
segments: {
|
|
491
|
+
beginningCount: segments.beginning.length,
|
|
492
|
+
middleCount: segments.middle.length,
|
|
493
|
+
endCount: segments.end.length,
|
|
494
|
+
summaryInserted: true
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Calculate the maximum characters the summarizer can handle in a single call.
|
|
501
|
+
* Uses the largest available compaction model's context window minus overhead.
|
|
502
|
+
*
|
|
503
|
+
* @returns {number} Maximum characters per summarization call
|
|
504
|
+
* @private
|
|
505
|
+
*/
|
|
506
|
+
_calculateSummarizerCapacity() {
|
|
507
|
+
const models = this._getValidatedCompactionModels();
|
|
508
|
+
const contextWindows = { ...(COMPACTION_CONFIG.MODEL_CONTEXT_WINDOWS || {}) };
|
|
509
|
+
|
|
510
|
+
// Augment with live data from modelsService if available
|
|
511
|
+
if (this.modelsService) {
|
|
512
|
+
try {
|
|
513
|
+
const allModels = this.modelsService.getModels();
|
|
514
|
+
for (const m of allModels) {
|
|
515
|
+
if (m.contextWindow) {
|
|
516
|
+
contextWindows[m.name] = m.contextWindow;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} catch { /* use static fallback */ }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const largestContext = Math.max(...models.map(m => contextWindows[m] || 128000));
|
|
523
|
+
|
|
524
|
+
const usableTokens = largestContext
|
|
525
|
+
- (COMPACTION_CONFIG.SUMMARIZER_SYSTEM_PROMPT_OVERHEAD || 500)
|
|
526
|
+
- (COMPACTION_CONFIG.SUMMARIZER_TEMPLATE_OVERHEAD || 800)
|
|
527
|
+
- (COMPACTION_CONFIG.MAX_SUMMARY_TOKENS || 8000)
|
|
528
|
+
- (COMPACTION_CONFIG.SUMMARIZER_SAFETY_MARGIN || 5000);
|
|
529
|
+
|
|
530
|
+
const effectiveTokens = Math.max(10000, usableTokens);
|
|
531
|
+
return effectiveTokens * (COMPACTION_CONFIG.CHARS_PER_TOKEN_ESTIMATE || 3);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Identify conversation segments using token-budget sizing.
|
|
536
|
+
* The middle segment starts at the 15% char mark and extends forward
|
|
537
|
+
* until hitting either the summarizer's capacity or 35% of total chars.
|
|
538
|
+
* This guarantees the middle always fits within the summarizer's context window.
|
|
539
|
+
*
|
|
540
|
+
* @param {Array} messages - Messages array
|
|
541
|
+
* @param {Object} [options] - Segmentation options
|
|
542
|
+
* @param {boolean} [options.wasSplit] - Whether oversized messages were split (unused in new logic)
|
|
543
|
+
* @returns {Object} { beginning, middle, end, middleStartIndex, middleEndIndex }
|
|
544
|
+
* @private
|
|
545
|
+
*/
|
|
546
|
+
// eslint-disable-next-line no-unused-vars
|
|
547
|
+
_identifySegments(messages, options = {}) {
|
|
548
|
+
const totalMessages = messages.length;
|
|
549
|
+
|
|
550
|
+
// Edge case: very small conversations — summarize all but last message
|
|
551
|
+
if (totalMessages <= 4) {
|
|
552
|
+
return {
|
|
553
|
+
beginning: [],
|
|
554
|
+
middle: messages.slice(0, Math.max(1, totalMessages - 1)),
|
|
555
|
+
end: messages.slice(-1),
|
|
556
|
+
middleStartIndex: 0,
|
|
557
|
+
middleEndIndex: Math.max(0, totalMessages - 2)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Calculate char length of each message
|
|
562
|
+
const msgChars = messages.map(m => {
|
|
563
|
+
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
564
|
+
return content.length;
|
|
565
|
+
});
|
|
566
|
+
const totalChars = msgChars.reduce((sum, c) => sum + c, 0);
|
|
567
|
+
|
|
568
|
+
// Get summarizer capacity (max chars it can handle in one call)
|
|
569
|
+
const summarizerCapacity = this._calculateSummarizerCapacity();
|
|
570
|
+
|
|
571
|
+
// Walk BACKWARD from end to determine tail (kept verbatim)
|
|
572
|
+
// Recent messages are more relevant for continuation than old ones
|
|
573
|
+
const tailBudget = totalChars * COMPACTION_CONFIG.TAIL_PRESERVE_PERCENTAGE;
|
|
574
|
+
let tailChars = 0;
|
|
575
|
+
let keepStartIdx = totalMessages;
|
|
576
|
+
for (let i = totalMessages - 1; i >= 0; i--) {
|
|
577
|
+
if (tailChars + msgChars[i] > tailBudget && i < totalMessages - 1) {
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
tailChars += msgChars[i];
|
|
581
|
+
keepStartIdx = i;
|
|
582
|
+
}
|
|
583
|
+
// Ensure at least 1 message in the summarize segment
|
|
584
|
+
keepStartIdx = Math.max(1, keepStartIdx);
|
|
585
|
+
|
|
586
|
+
// Old segment: M[0..keepStartIdx-1] — to be summarized
|
|
587
|
+
// Cap by summarizer capacity (if too large, only summarize what fits this pass)
|
|
588
|
+
let oldChars = 0;
|
|
589
|
+
let summarizeEndIdx = keepStartIdx;
|
|
590
|
+
for (let i = 0; i < keepStartIdx; i++) {
|
|
591
|
+
if (oldChars + msgChars[i] > summarizerCapacity && i > 0) {
|
|
592
|
+
summarizeEndIdx = i;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
oldChars += msgChars[i];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const middle = messages.slice(0, summarizeEndIdx); // summarized
|
|
599
|
+
const end = messages.slice(summarizeEndIdx); // kept verbatim
|
|
600
|
+
|
|
601
|
+
this.logger.info('Segment identification (tail-preserving)', {
|
|
602
|
+
totalMessages,
|
|
603
|
+
totalChars,
|
|
604
|
+
summarizerCapacity,
|
|
605
|
+
middleCount: middle.length,
|
|
606
|
+
middleChars: oldChars,
|
|
607
|
+
endCount: end.length,
|
|
608
|
+
tailBudget,
|
|
609
|
+
keepStartIdx,
|
|
610
|
+
summarizeEndIdx
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
beginning: [],
|
|
615
|
+
middle: middle.length > 0 ? middle : [messages[0]],
|
|
616
|
+
end: end.length > 0 ? end : [messages[messages.length - 1]],
|
|
617
|
+
middleStartIndex: 0,
|
|
618
|
+
middleEndIndex: summarizeEndIdx - 1
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Generate AI summary of middle segment using validated compaction models
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
async _generateSummary(middleMessages, model, options = {}) {
|
|
627
|
+
if (middleMessages.length === 0) {
|
|
628
|
+
return {
|
|
629
|
+
role: 'system',
|
|
630
|
+
content: `${COMPACTION_CONFIG.COMPACTION_SUMMARY_PREFIX} No messages to summarize.`,
|
|
631
|
+
type: 'summary',
|
|
632
|
+
timestamp: new Date().toISOString()
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Format middle messages for summarization — PRE-TAG each message
|
|
637
|
+
// with a category the summarizer can trust without inference.
|
|
638
|
+
//
|
|
639
|
+
// Why pre-tag instead of letting the summarizer figure it out:
|
|
640
|
+
// tool-result wrappers carry `role: user` (they come back as
|
|
641
|
+
// user-role messages by convention in this codebase). A summarizer
|
|
642
|
+
// staring at raw `user:` prefixes can't reliably tell a literal
|
|
643
|
+
// user typing from a tool-result blob — and in our experiments
|
|
644
|
+
// both gpt-4.1-mini and gpt-4.1-nano routinely quoted tool blobs
|
|
645
|
+
// as if they were user messages, wasting budget and corrupting
|
|
646
|
+
// the user-voice section. Categorizing here eliminates that whole
|
|
647
|
+
// failure class. See _categorizeMessage for the rules.
|
|
648
|
+
let middleContent = middleMessages
|
|
649
|
+
.map(msg => {
|
|
650
|
+
const cat = this._categorizeMessage(msg);
|
|
651
|
+
const body = typeof msg.content === 'string'
|
|
652
|
+
? msg.content
|
|
653
|
+
: JSON.stringify(msg.content);
|
|
654
|
+
return `[${cat}] ${body}`;
|
|
655
|
+
})
|
|
656
|
+
.join('\n\n────────\n\n');
|
|
657
|
+
|
|
658
|
+
// Estimate input tokens
|
|
659
|
+
const estimatedInputTokens = Math.ceil(middleContent.length / COMPACTION_CONFIG.CHARS_PER_TOKEN_ESTIMATE);
|
|
660
|
+
|
|
661
|
+
// Get validated compaction models
|
|
662
|
+
const models = this._getValidatedCompactionModels();
|
|
663
|
+
|
|
664
|
+
// Get context windows for smart selection
|
|
665
|
+
const contextWindows = COMPACTION_CONFIG.MODEL_CONTEXT_WINDOWS || {};
|
|
666
|
+
|
|
667
|
+
// If modelsService available, augment context windows with live data
|
|
668
|
+
if (this.modelsService) {
|
|
669
|
+
try {
|
|
670
|
+
const allModels = this.modelsService.getModels();
|
|
671
|
+
for (const m of allModels) {
|
|
672
|
+
if (m.contextWindow) {
|
|
673
|
+
contextWindows[m.name] = m.contextWindow;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
// Ignore — use static fallback
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Find the largest context window available among compaction models
|
|
682
|
+
const largestContextWindow = Math.max(
|
|
683
|
+
...models.map(m => contextWindows[m] || 128000)
|
|
684
|
+
);
|
|
685
|
+
const maxInputTokens = largestContextWindow - 10000;
|
|
686
|
+
|
|
687
|
+
// Middle is now sized by _identifySegments to fit within the summarizer's capacity.
|
|
688
|
+
// No truncation needed — the segment is guaranteed to be within budget.
|
|
689
|
+
if (estimatedInputTokens > maxInputTokens) {
|
|
690
|
+
this.logger.info('Middle segment exceeds single model but sized to fit summarizer capacity', {
|
|
691
|
+
estimatedInputTokens,
|
|
692
|
+
maxInputTokens,
|
|
693
|
+
largestContextWindow,
|
|
694
|
+
middleChars: middleContent.length
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Build summary prompt
|
|
699
|
+
const summaryPrompt = this.summaryPromptTemplate
|
|
700
|
+
.replace('{middle_segment}', middleContent);
|
|
701
|
+
|
|
702
|
+
// Estimate tokens for model selection
|
|
703
|
+
const finalEstimatedTokens = Math.ceil(middleContent.length / COMPACTION_CONFIG.CHARS_PER_TOKEN_ESTIMATE);
|
|
704
|
+
|
|
705
|
+
// Filter to models with sufficient context
|
|
706
|
+
const requiredContext = finalEstimatedTokens + 10000;
|
|
707
|
+
const capableModels = models.filter(m => {
|
|
708
|
+
const modelContext = contextWindows[m] || 128000;
|
|
709
|
+
return modelContext >= requiredContext;
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const selectedModels = capableModels.length > 0 ? capableModels : models;
|
|
713
|
+
|
|
714
|
+
this.logger.info('Compaction model selection', {
|
|
715
|
+
finalEstimatedTokens,
|
|
716
|
+
requiredContext,
|
|
717
|
+
validatedModelsCount: models.length,
|
|
718
|
+
capableModelsCount: capableModels.length,
|
|
719
|
+
selectedModels
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const modelsAttempted = [];
|
|
723
|
+
let lastError = null;
|
|
724
|
+
|
|
725
|
+
for (let attempt = 0; attempt < selectedModels.length; attempt++) {
|
|
726
|
+
const compactionModel = selectedModels[attempt];
|
|
727
|
+
modelsAttempted.push(compactionModel);
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
this.logger.info('Generating summary', {
|
|
731
|
+
compactionModel,
|
|
732
|
+
attempt: attempt + 1,
|
|
733
|
+
totalModels: selectedModels.length,
|
|
734
|
+
middleMessageCount: middleMessages.length,
|
|
735
|
+
passNumber: options.passNumber || 1
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Call AI service (skipCircuitBreaker prevents compaction failures from blocking the agent)
|
|
739
|
+
const response = await this.aiService.sendMessage(
|
|
740
|
+
compactionModel,
|
|
741
|
+
summaryPrompt,
|
|
742
|
+
{
|
|
743
|
+
systemPrompt: 'You are a conversation summarization expert. Your goal is to compress conversations while preserving critical information for continued interaction.',
|
|
744
|
+
maxTokens: COMPACTION_CONFIG.MAX_SUMMARY_TOKENS,
|
|
745
|
+
temperature: 0.3,
|
|
746
|
+
sessionId: options.sessionId,
|
|
747
|
+
platformProvided: true,
|
|
748
|
+
skipCircuitBreaker: true
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
const summaryContent = response.content.trim();
|
|
753
|
+
|
|
754
|
+
// Build index range string
|
|
755
|
+
const indexRange = (options.middleStartIndex !== undefined && options.middleEndIndex !== undefined)
|
|
756
|
+
? `original messages ${options.middleStartIndex}-${options.middleEndIndex}`
|
|
757
|
+
: `${middleMessages.length} messages`;
|
|
758
|
+
|
|
759
|
+
this.logger.info('Summary generated successfully', {
|
|
760
|
+
compactionModel,
|
|
761
|
+
attempt: attempt + 1,
|
|
762
|
+
originalLength: middleContent.length,
|
|
763
|
+
summaryLength: summaryContent.length,
|
|
764
|
+
compressionRatio: (summaryContent.length / middleContent.length * 100).toFixed(2) + '%',
|
|
765
|
+
indexRange,
|
|
766
|
+
passNumber: options.passNumber || 1
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Advance round-robin index
|
|
770
|
+
this._advanceCompactionModelIndex();
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
role: 'system',
|
|
774
|
+
content: `${COMPACTION_CONFIG.COMPACTION_SUMMARY_PREFIX} - ${indexRange}]\n\n${summaryContent}\n\n${COMPACTION_CONFIG.COMPACTION_SUMMARY_SUFFIX}`,
|
|
775
|
+
type: 'summary',
|
|
776
|
+
timestamp: new Date().toISOString(),
|
|
777
|
+
metadata: {
|
|
778
|
+
originalMessageCount: middleMessages.length,
|
|
779
|
+
originalStartIndex: options.middleStartIndex,
|
|
780
|
+
originalEndIndex: options.middleEndIndex,
|
|
781
|
+
compactionModel,
|
|
782
|
+
passNumber: options.passNumber || 1
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
} catch (error) {
|
|
787
|
+
lastError = error;
|
|
788
|
+
const isRateLimit = error.message?.includes('429') || error.message?.includes('rate limit');
|
|
789
|
+
|
|
790
|
+
this.logger.warn('Summary generation failed, trying next model', {
|
|
791
|
+
compactionModel,
|
|
792
|
+
attempt: attempt + 1,
|
|
793
|
+
remainingModels: selectedModels.length - attempt - 1,
|
|
794
|
+
isRateLimit,
|
|
795
|
+
error: error.message
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Notify user that compaction is taking longer (only if more models to try)
|
|
799
|
+
if (attempt < selectedModels.length - 1 && options.onRetryAttempt) {
|
|
800
|
+
options.onRetryAttempt({
|
|
801
|
+
type: 'compaction_retry',
|
|
802
|
+
message: 'Compaction is taking longer than usual, hold on we\'ll be right there',
|
|
803
|
+
failedModel: compactionModel,
|
|
804
|
+
nextModel: selectedModels[attempt + 1],
|
|
805
|
+
attempt: attempt + 1,
|
|
806
|
+
totalModels: selectedModels.length
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// All recommended models failed — try one last-resort random model with sufficient context
|
|
813
|
+
if (this.modelsService) {
|
|
814
|
+
try {
|
|
815
|
+
const allModels = this.modelsService.getModels();
|
|
816
|
+
const suitableModels = allModels
|
|
817
|
+
.filter(m => (m.type === 'chat' || !m.type) && !modelsAttempted.includes(m.name))
|
|
818
|
+
.filter(m => (m.contextWindow || 128000) >= requiredContext)
|
|
819
|
+
.sort(() => Math.random() - 0.5);
|
|
820
|
+
|
|
821
|
+
if (suitableModels.length > 0) {
|
|
822
|
+
const lastResort = suitableModels[0].name;
|
|
823
|
+
modelsAttempted.push(lastResort);
|
|
824
|
+
|
|
825
|
+
this.logger.info('Trying last-resort random model for compaction', { lastResort, requiredContext });
|
|
826
|
+
|
|
827
|
+
if (options.onRetryAttempt) {
|
|
828
|
+
options.onRetryAttempt({
|
|
829
|
+
type: 'compaction_retry',
|
|
830
|
+
message: 'Compaction is taking longer than usual, hold on we\'ll be right there',
|
|
831
|
+
failedModel: modelsAttempted[modelsAttempted.length - 2],
|
|
832
|
+
nextModel: lastResort,
|
|
833
|
+
attempt: modelsAttempted.length,
|
|
834
|
+
totalModels: modelsAttempted.length
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const response = await this.aiService.sendMessage(
|
|
839
|
+
lastResort,
|
|
840
|
+
summaryPrompt,
|
|
841
|
+
{
|
|
842
|
+
systemPrompt: 'You are a conversation summarization expert. Your goal is to compress conversations while preserving critical information for continued interaction.',
|
|
843
|
+
maxTokens: COMPACTION_CONFIG.MAX_SUMMARY_TOKENS,
|
|
844
|
+
temperature: 0.3,
|
|
845
|
+
sessionId: options.sessionId,
|
|
846
|
+
platformProvided: true,
|
|
847
|
+
skipCircuitBreaker: true
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
const summaryContent = response.content.trim();
|
|
852
|
+
const indexRange = (options.middleStartIndex !== undefined && options.middleEndIndex !== undefined)
|
|
853
|
+
? `original messages ${options.middleStartIndex}-${options.middleEndIndex}`
|
|
854
|
+
: `${middleMessages.length} messages`;
|
|
855
|
+
|
|
856
|
+
this.logger.info('Last-resort model succeeded for compaction', { lastResort, summaryLength: summaryContent.length });
|
|
857
|
+
this._advanceCompactionModelIndex();
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
role: 'system',
|
|
861
|
+
content: `${COMPACTION_CONFIG.COMPACTION_SUMMARY_PREFIX} - ${indexRange}]\n\n${summaryContent}\n\n${COMPACTION_CONFIG.COMPACTION_SUMMARY_SUFFIX}`,
|
|
862
|
+
type: 'summary',
|
|
863
|
+
timestamp: new Date().toISOString(),
|
|
864
|
+
metadata: {
|
|
865
|
+
originalMessageCount: middleMessages.length,
|
|
866
|
+
originalStartIndex: options.middleStartIndex,
|
|
867
|
+
originalEndIndex: options.middleEndIndex,
|
|
868
|
+
compactionModel: lastResort,
|
|
869
|
+
passNumber: options.passNumber || 1,
|
|
870
|
+
lastResort: true
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
} catch (lastResortError) {
|
|
875
|
+
this.logger.warn('Last-resort model also failed', { error: lastResortError.message });
|
|
876
|
+
lastError = lastResortError;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ALL models exhausted (including last-resort) — now show error to user
|
|
881
|
+
const errorDetails = {
|
|
882
|
+
modelsAttempted,
|
|
883
|
+
lastError: lastError?.message,
|
|
884
|
+
middleMessageCount: middleMessages.length,
|
|
885
|
+
isRateLimitIssue: lastError?.message?.includes('429') || lastError?.message?.includes('rate limit')
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
this.logger.error('All compaction models exhausted (including last-resort)', errorDetails);
|
|
889
|
+
|
|
890
|
+
if (options.onAllModelsExhausted) {
|
|
891
|
+
options.onAllModelsExhausted({
|
|
892
|
+
type: 'compaction_models_exhausted',
|
|
893
|
+
message: `Conversation compaction failed: All ${modelsAttempted.length} models are currently unavailable. ${errorDetails.isRateLimitIssue ? 'Rate limits may be in effect.' : ''} Using structural fallback compaction.`,
|
|
894
|
+
models: modelsAttempted,
|
|
895
|
+
error: lastError?.message
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
this._advanceCompactionModelIndex();
|
|
900
|
+
|
|
901
|
+
const exhaustedError = new Error('ALL_MODELS_EXHAUSTED');
|
|
902
|
+
exhaustedError.code = 'ALL_MODELS_EXHAUSTED';
|
|
903
|
+
exhaustedError.details = errorDetails;
|
|
904
|
+
throw exhaustedError;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Perform structural fallback compaction when all AI models are unavailable.
|
|
909
|
+
* No AI call required - pure structural transformation:
|
|
910
|
+
* 1. Remove all system messages (except the first/main one)
|
|
911
|
+
* 2. Remove tool results
|
|
912
|
+
* 3. Keep beginning + end of remaining messages
|
|
913
|
+
* 4. Replace middle with a short paragraph
|
|
914
|
+
* @param {Array} allMessages - The full original messages array
|
|
915
|
+
* @returns {Object} { compactedMessages, metadata }
|
|
916
|
+
* @private
|
|
917
|
+
*/
|
|
918
|
+
_performFallbackCompaction(allMessages) {
|
|
919
|
+
// 1. Identify the main system message
|
|
920
|
+
const mainSystemMsg = allMessages.find(m => m.role === 'system' && m.type !== 'summary');
|
|
921
|
+
|
|
922
|
+
// 2. Filter out system messages (except main) and tool results
|
|
923
|
+
const filteredMessages = allMessages.filter(m => {
|
|
924
|
+
if (m === mainSystemMsg) return true;
|
|
925
|
+
if (m.role === 'system') return false;
|
|
926
|
+
if (m.type === 'tool_result' || m.type === 'tool-result' || m.role === 'tool') return false;
|
|
927
|
+
return true;
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const removedSystemCount = allMessages.filter(m => m.role === 'system' && m !== mainSystemMsg).length;
|
|
931
|
+
const removedToolCount = allMessages.filter(m => m.type === 'tool_result' || m.type === 'tool-result' || m.role === 'tool').length;
|
|
932
|
+
|
|
933
|
+
// 3. Apply sandwich using segment identification
|
|
934
|
+
const segments = this._identifySegments(filteredMessages);
|
|
935
|
+
|
|
936
|
+
// 4. Build replacement paragraph for middle section
|
|
937
|
+
const middleSummary = this._buildFallbackMiddleParagraph(segments.middle, {
|
|
938
|
+
removedSystemCount,
|
|
939
|
+
removedToolCount,
|
|
940
|
+
totalOriginalMessages: allMessages.length
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// 5. Create the summary message
|
|
944
|
+
const summaryMessage = {
|
|
945
|
+
role: 'system',
|
|
946
|
+
content: `${COMPACTION_CONFIG.COMPACTION_SUMMARY_PREFIX} - structural fallback]\n\n${middleSummary}\n\n${COMPACTION_CONFIG.COMPACTION_SUMMARY_SUFFIX}`,
|
|
947
|
+
type: 'summary',
|
|
948
|
+
timestamp: new Date().toISOString(),
|
|
949
|
+
metadata: {
|
|
950
|
+
fallback: true,
|
|
951
|
+
structural: true,
|
|
952
|
+
removedSystemMessages: removedSystemCount,
|
|
953
|
+
removedToolResults: removedToolCount,
|
|
954
|
+
middleMessagesCompacted: segments.middle.length
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// 6. Reconstruct conversation
|
|
959
|
+
const compactedMessages = [
|
|
960
|
+
...segments.beginning,
|
|
961
|
+
summaryMessage,
|
|
962
|
+
...segments.end
|
|
963
|
+
];
|
|
964
|
+
|
|
965
|
+
this.logger.info('Structural fallback compaction performed', {
|
|
966
|
+
originalMessages: allMessages.length,
|
|
967
|
+
afterFiltering: filteredMessages.length,
|
|
968
|
+
removedSystemMessages: removedSystemCount,
|
|
969
|
+
removedToolResults: removedToolCount,
|
|
970
|
+
beginningKept: segments.beginning.length,
|
|
971
|
+
middleCompacted: segments.middle.length,
|
|
972
|
+
endKept: segments.end.length,
|
|
973
|
+
finalMessages: compactedMessages.length
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
compactedMessages,
|
|
978
|
+
metadata: {
|
|
979
|
+
strategy: 'structural_fallback',
|
|
980
|
+
removedSystemMessages: removedSystemCount,
|
|
981
|
+
removedToolResults: removedToolCount,
|
|
982
|
+
middleMessagesCompacted: segments.middle.length
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Build a short paragraph summarizing the middle section for fallback compaction
|
|
989
|
+
* @private
|
|
990
|
+
*/
|
|
991
|
+
_buildFallbackMiddleParagraph(middleMessages, stats) {
|
|
992
|
+
const userMsgs = middleMessages.filter(m => m.role === 'user');
|
|
993
|
+
const assistantMsgs = middleMessages.filter(m => m.role === 'assistant');
|
|
994
|
+
|
|
995
|
+
// Extract file paths mentioned in messages
|
|
996
|
+
const filePathRegex = /(?:\/[\w.-]+)+\.\w+|(?:[A-Za-z]:)?(?:\\[\w.-]+)+\.\w+|(?:src|lib|test|config|public|dist|build|node_modules)\/[\w./-]+/g;
|
|
997
|
+
const filePaths = new Set();
|
|
998
|
+
for (const msg of middleMessages) {
|
|
999
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
1000
|
+
const matches = content.match(filePathRegex);
|
|
1001
|
+
if (matches) {
|
|
1002
|
+
matches.forEach(p => filePaths.add(p));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const parts = [];
|
|
1007
|
+
parts.push(`[${middleMessages.length} messages compacted (${userMsgs.length} user, ${assistantMsgs.length} assistant).`);
|
|
1008
|
+
|
|
1009
|
+
if (stats.removedSystemCount > 0 || stats.removedToolCount > 0) {
|
|
1010
|
+
parts.push(`Additionally removed: ${stats.removedSystemCount} system messages, ${stats.removedToolCount} tool results.`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (filePaths.size > 0) {
|
|
1014
|
+
const fileList = Array.from(filePaths).slice(0, 20).join(', ');
|
|
1015
|
+
parts.push(`Files referenced: ${fileList}${filePaths.size > 20 ? ` and ${filePaths.size - 20} more` : ''}.`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
parts.push('Summary generation was unavailable - content structurally compacted for context management.]');
|
|
1019
|
+
|
|
1020
|
+
return parts.join(' ');
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Split oversized messages into smaller chunks for effective compaction.
|
|
1025
|
+
* When a single message exceeds OVERSIZED_MESSAGE_THRESHOLD, it gets split
|
|
1026
|
+
* into chunks of MAX_CHUNK_SIZE, increasing message count so the sandwich
|
|
1027
|
+
* strategy can push the content into the summarizable middle segment.
|
|
1028
|
+
*
|
|
1029
|
+
* @param {Array} messages - Messages array
|
|
1030
|
+
* @returns {Array} Messages with oversized ones split into chunks
|
|
1031
|
+
* @private
|
|
1032
|
+
*/
|
|
1033
|
+
_splitOversizedMessages(messages) {
|
|
1034
|
+
const threshold = COMPACTION_CONFIG.OVERSIZED_MESSAGE_THRESHOLD;
|
|
1035
|
+
const maxChunk = COMPACTION_CONFIG.MAX_CHUNK_SIZE;
|
|
1036
|
+
|
|
1037
|
+
let splitCount = 0;
|
|
1038
|
+
const result = [];
|
|
1039
|
+
|
|
1040
|
+
for (const msg of messages) {
|
|
1041
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
1042
|
+
|
|
1043
|
+
if (content.length <= threshold) {
|
|
1044
|
+
result.push(msg);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Split this message
|
|
1049
|
+
splitCount++;
|
|
1050
|
+
const chunks = this._splitContentIntoChunks(content, maxChunk);
|
|
1051
|
+
|
|
1052
|
+
this.logger.info('Splitting oversized message for compaction', {
|
|
1053
|
+
role: msg.role,
|
|
1054
|
+
type: msg.type || 'none',
|
|
1055
|
+
originalChars: content.length,
|
|
1056
|
+
chunks: chunks.length
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1060
|
+
const chunkMsg = {
|
|
1061
|
+
...msg,
|
|
1062
|
+
content: `[Part ${i + 1}/${chunks.length}${i === 0 ? ' — oversized message split for compaction' : ' — continued'}]\n${chunks[i]}`,
|
|
1063
|
+
_splitMetadata: {
|
|
1064
|
+
originalLength: content.length,
|
|
1065
|
+
chunkIndex: i,
|
|
1066
|
+
totalChunks: chunks.length
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
// Give each chunk a unique ID to avoid conflicts
|
|
1070
|
+
if (msg.id) {
|
|
1071
|
+
chunkMsg.id = `${msg.id}-chunk-${i + 1}`;
|
|
1072
|
+
}
|
|
1073
|
+
result.push(chunkMsg);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (splitCount > 0) {
|
|
1078
|
+
this.logger.info('Oversized message splitting complete', {
|
|
1079
|
+
originalCount: messages.length,
|
|
1080
|
+
newCount: result.length,
|
|
1081
|
+
added: result.length - messages.length,
|
|
1082
|
+
messagesSplit: splitCount
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return { messages: result, wasSplit: splitCount > 0 };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Split content into chunks, respecting natural boundaries.
|
|
1091
|
+
* Priority: double newlines > single newlines > hard cut at maxChunk.
|
|
1092
|
+
*
|
|
1093
|
+
* @param {string} content - Content to split
|
|
1094
|
+
* @param {number} maxChunk - Maximum chunk size in chars
|
|
1095
|
+
* @returns {string[]} Array of content chunks
|
|
1096
|
+
* @private
|
|
1097
|
+
*/
|
|
1098
|
+
_splitContentIntoChunks(content, maxChunk) {
|
|
1099
|
+
if (content.length <= maxChunk) return [content];
|
|
1100
|
+
|
|
1101
|
+
const chunks = [];
|
|
1102
|
+
let remaining = content;
|
|
1103
|
+
|
|
1104
|
+
while (remaining.length > maxChunk) {
|
|
1105
|
+
let splitAt = -1;
|
|
1106
|
+
|
|
1107
|
+
// Try double newline within the chunk range
|
|
1108
|
+
const searchRange = remaining.substring(0, maxChunk);
|
|
1109
|
+
const lastDoubleNL = searchRange.lastIndexOf('\n\n');
|
|
1110
|
+
if (lastDoubleNL > maxChunk * 0.3) {
|
|
1111
|
+
splitAt = lastDoubleNL + 2;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Fallback: single newline
|
|
1115
|
+
if (splitAt === -1) {
|
|
1116
|
+
const lastNL = searchRange.lastIndexOf('\n');
|
|
1117
|
+
if (lastNL > maxChunk * 0.3) {
|
|
1118
|
+
splitAt = lastNL + 1;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Fallback: sentence boundary (". " — common in prose, rare in code)
|
|
1123
|
+
if (splitAt === -1) {
|
|
1124
|
+
const lastSentence = searchRange.lastIndexOf('. ');
|
|
1125
|
+
if (lastSentence > maxChunk * 0.3) {
|
|
1126
|
+
splitAt = lastSentence + 2;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Fallback: hard cut (e.g. minified code with no newlines)
|
|
1131
|
+
// TODO: Consider splitting at last space or semicolon before maxChunk
|
|
1132
|
+
// to avoid breaking mid-token/mid-word in minified code
|
|
1133
|
+
if (splitAt === -1) {
|
|
1134
|
+
splitAt = maxChunk;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
chunks.push(remaining.substring(0, splitAt));
|
|
1138
|
+
remaining = remaining.substring(splitAt);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (remaining.length > 0) {
|
|
1142
|
+
chunks.push(remaining);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return chunks;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Categorize one conversation message for compaction tagging.
|
|
1150
|
+
*
|
|
1151
|
+
* Returns one of:
|
|
1152
|
+
* REAL_USER — a literal user typing turn
|
|
1153
|
+
* TOOL_RESULT — a `[Tool Results …]` wrapper (carries role:user)
|
|
1154
|
+
* PREVIOUS_TASK — a `[Previous Task — Final Tool Results]` boundary
|
|
1155
|
+
* AGENT — assistant turn
|
|
1156
|
+
* SYSTEM — system message
|
|
1157
|
+
*
|
|
1158
|
+
* The categorization is deterministic — text-prefix sniffing on the
|
|
1159
|
+
* content, not heuristic. Matches the convention used everywhere
|
|
1160
|
+
* else in the CLI for marking tool-result envelopes.
|
|
1161
|
+
*
|
|
1162
|
+
* @param {object} msg - { role, content }
|
|
1163
|
+
* @returns {string} one of the categories above
|
|
1164
|
+
* @private
|
|
1165
|
+
*/
|
|
1166
|
+
_categorizeMessage(msg) {
|
|
1167
|
+
if (msg.role === 'assistant') return 'AGENT';
|
|
1168
|
+
if (msg.role === 'system') return 'SYSTEM';
|
|
1169
|
+
// role === 'user' — could be a real user message OR a tool-result wrapper.
|
|
1170
|
+
const c = typeof msg.content === 'string' ? msg.content.trimStart() : '';
|
|
1171
|
+
if (c.startsWith('[Tool Results')) return 'TOOL_RESULT';
|
|
1172
|
+
if (c.startsWith('[Previous Task')) return 'PREVIOUS_TASK';
|
|
1173
|
+
return 'REAL_USER';
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Create the compaction-summary prompt template.
|
|
1178
|
+
*
|
|
1179
|
+
* Why this prompt is shaped this way:
|
|
1180
|
+
* The previous "paraphrase-everything" template was found to drop
|
|
1181
|
+
* the user's literal asks during compaction (see the Talisman
|
|
1182
|
+
* case study: the agent paraphrased the user's 3-point UI request
|
|
1183
|
+
* into "redesign UI" and then went off and built a Settings
|
|
1184
|
+
* screen). Re-tested across 3 models × 5 prompt variants, this
|
|
1185
|
+
* two-pass shape was the highest-fidelity option that worked
|
|
1186
|
+
* uniformly well across gpt-4.1-mini, gpt-4.1-nano, and
|
|
1187
|
+
* FW-Kimi-K2.5. See tmp-compaction-experiment/ for the harness.
|
|
1188
|
+
*
|
|
1189
|
+
* PASS 1 is transcription. The summarizer is NOT allowed to filter
|
|
1190
|
+
* user messages by "I think the agent already handled this." That
|
|
1191
|
+
* determination belongs to the consumer agent reading the summary,
|
|
1192
|
+
* not to the summarizer itself — making the summarizer choose was
|
|
1193
|
+
* how completed-vs-open misjudgments crept in. The blockquote
|
|
1194
|
+
* format gives the consumer agent a strong visual signal to
|
|
1195
|
+
* anchor on those literal asks.
|
|
1196
|
+
*
|
|
1197
|
+
* PASS 2 is the narrative summary of the agent's work — files,
|
|
1198
|
+
* tools, decisions, state. Heavy compression OK here; only the
|
|
1199
|
+
* user-voice section is sacred.
|
|
1200
|
+
*
|
|
1201
|
+
* @private
|
|
1202
|
+
*/
|
|
1203
|
+
_createSummaryPromptTemplate() {
|
|
1204
|
+
return `You are compacting an earlier portion of an agent-user conversation. The input has been PRE-TAGGED — every message starts with one of:
|
|
1205
|
+
|
|
1206
|
+
[REAL_USER] — a literal user message; TRANSCRIBE VERBATIM in PASS 1
|
|
1207
|
+
[AGENT] — assistant turn (tool calls + reasoning)
|
|
1208
|
+
[TOOL_RESULT] — a tool's output; the consumer agent does NOT need these verbatim
|
|
1209
|
+
[PREVIOUS_TASK] — final tool-result block from a previous task boundary
|
|
1210
|
+
[SYSTEM] — system note
|
|
1211
|
+
|
|
1212
|
+
You DO NOT need to detect categories yourself. Trust the tags. The pre-tagging is deterministic.
|
|
1213
|
+
|
|
1214
|
+
Write the summary in THREE passes, in this exact order.
|
|
1215
|
+
|
|
1216
|
+
──────────────────────────────────────────────
|
|
1217
|
+
PASS 1 — USER VOICE (transcription only, no judgment)
|
|
1218
|
+
──────────────────────────────────────────────
|
|
1219
|
+
|
|
1220
|
+
For EVERY [REAL_USER] message — and ONLY [REAL_USER] messages — emit a blockquote:
|
|
1221
|
+
|
|
1222
|
+
> **User said (orig idx N):** "<exact text, word for word, all of it>"
|
|
1223
|
+
|
|
1224
|
+
Absolute rules:
|
|
1225
|
+
- Do NOT quote any [TOOL_RESULT], [AGENT], [PREVIOUS_TASK], or [SYSTEM] message here.
|
|
1226
|
+
- Do NOT condense, paraphrase, or omit any [REAL_USER] message.
|
|
1227
|
+
- Do NOT skip a [REAL_USER] message on the assumption "the agent already addressed it." That determination belongs to the consumer agent, not to you. Your job here is transcription.
|
|
1228
|
+
- Reproduce every [REAL_USER] message, in original order, including punctuation and typos.
|
|
1229
|
+
- If the input has no [REAL_USER] messages, write "(no user messages in this segment)" and proceed.
|
|
1230
|
+
|
|
1231
|
+
──────────────────────────────────────────────
|
|
1232
|
+
PASS 2 — EVENT LOG (chronological bullets, concrete details)
|
|
1233
|
+
──────────────────────────────────────────────
|
|
1234
|
+
|
|
1235
|
+
A bulleted list of every notable event between/after the user messages. ONE bullet per event:
|
|
1236
|
+
|
|
1237
|
+
- [orig idx N] <one-line description — include full file paths, tool names, line numbers, status, and outcome>
|
|
1238
|
+
|
|
1239
|
+
Cover: file writes, successful tool calls that changed state, decisions made by the agent, errors that affected outcome, task-list changes (especially destructive ones like 'removed: N tasks'). Skip: pure-read tool calls that didn't change state, repeated reads, pleasantries, verbose tool output dumps.
|
|
1240
|
+
|
|
1241
|
+
A consumer agent should be able to read this log and reconstruct the cause-and-effect chain — what happened to each [REAL_USER] request.
|
|
1242
|
+
|
|
1243
|
+
──────────────────────────────────────────────
|
|
1244
|
+
PASS 3 — STATE NARRATIVE (2–4 sentences)
|
|
1245
|
+
──────────────────────────────────────────────
|
|
1246
|
+
|
|
1247
|
+
Plain prose describing the situation at the end of this segment: what is done, what is mid-flight, what is open — and where possible, map back to which [REAL_USER] request each piece corresponds to. If [REAL_USER] requests are still open with no work toward them, say so explicitly. This is the place where lossy paraphrase is most dangerous — name the gaps clearly.
|
|
1248
|
+
|
|
1249
|
+
──────────────────────────────────────────────
|
|
1250
|
+
|
|
1251
|
+
CONVERSATION SEGMENT TO COMPACT:
|
|
1252
|
+
{middle_segment}
|
|
1253
|
+
|
|
1254
|
+
OUTPUT: PASS 1, PASS 2, PASS 3 in that order. Use exactly the section headers above. No preamble, no meta-commentary.`;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
export default ConversationCompactionService;
|