@framers/agentos 0.1.120 → 0.1.121

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.
Files changed (405) hide show
  1. package/README.md +21 -0
  2. package/dist/api/agency.d.ts.map +1 -1
  3. package/dist/api/agency.js +227 -84
  4. package/dist/api/agency.js.map +1 -1
  5. package/dist/api/analyzeVideo.d.ts +127 -0
  6. package/dist/api/analyzeVideo.d.ts.map +1 -0
  7. package/dist/api/analyzeVideo.js +136 -0
  8. package/dist/api/analyzeVideo.js.map +1 -0
  9. package/dist/api/detectScenes.d.ts +82 -0
  10. package/dist/api/detectScenes.d.ts.map +1 -0
  11. package/dist/api/detectScenes.js +67 -0
  12. package/dist/api/detectScenes.js.map +1 -0
  13. package/dist/api/generateImage.d.ts +7 -0
  14. package/dist/api/generateImage.d.ts.map +1 -1
  15. package/dist/api/generateImage.js +133 -9
  16. package/dist/api/generateImage.js.map +1 -1
  17. package/dist/api/generateMusic.d.ts +98 -0
  18. package/dist/api/generateMusic.d.ts.map +1 -0
  19. package/dist/api/generateMusic.js +319 -0
  20. package/dist/api/generateMusic.js.map +1 -0
  21. package/dist/api/generateSFX.d.ts +96 -0
  22. package/dist/api/generateSFX.d.ts.map +1 -0
  23. package/dist/api/generateSFX.js +317 -0
  24. package/dist/api/generateSFX.js.map +1 -0
  25. package/dist/api/generateVideo.d.ts +113 -0
  26. package/dist/api/generateVideo.d.ts.map +1 -0
  27. package/dist/api/generateVideo.js +342 -0
  28. package/dist/api/generateVideo.js.map +1 -0
  29. package/dist/api/model.d.ts.map +1 -1
  30. package/dist/api/model.js +8 -4
  31. package/dist/api/model.js.map +1 -1
  32. package/dist/api/performOCR.d.ts +169 -0
  33. package/dist/api/performOCR.d.ts.map +1 -0
  34. package/dist/api/performOCR.js +198 -0
  35. package/dist/api/performOCR.js.map +1 -0
  36. package/dist/api/provider-defaults.d.ts +7 -5
  37. package/dist/api/provider-defaults.d.ts.map +1 -1
  38. package/dist/api/provider-defaults.js +32 -10
  39. package/dist/api/provider-defaults.js.map +1 -1
  40. package/dist/api/strategies/debate.d.ts.map +1 -1
  41. package/dist/api/strategies/debate.js +1 -0
  42. package/dist/api/strategies/debate.js.map +1 -1
  43. package/dist/api/strategies/graph.d.ts.map +1 -1
  44. package/dist/api/strategies/graph.js +69 -13
  45. package/dist/api/strategies/graph.js.map +1 -1
  46. package/dist/api/strategies/hierarchical.d.ts.map +1 -1
  47. package/dist/api/strategies/hierarchical.js +1 -0
  48. package/dist/api/strategies/hierarchical.js.map +1 -1
  49. package/dist/api/strategies/parallel.d.ts.map +1 -1
  50. package/dist/api/strategies/parallel.js +1 -0
  51. package/dist/api/strategies/parallel.js.map +1 -1
  52. package/dist/api/strategies/review-loop.d.ts.map +1 -1
  53. package/dist/api/strategies/review-loop.js +1 -0
  54. package/dist/api/strategies/review-loop.js.map +1 -1
  55. package/dist/api/strategies/sequential.d.ts.map +1 -1
  56. package/dist/api/strategies/sequential.js +54 -48
  57. package/dist/api/strategies/sequential.js.map +1 -1
  58. package/dist/api/streamBuffer.d.ts +20 -0
  59. package/dist/api/streamBuffer.d.ts.map +1 -0
  60. package/dist/api/streamBuffer.js +81 -0
  61. package/dist/api/streamBuffer.js.map +1 -0
  62. package/dist/api/types.d.ts +145 -5
  63. package/dist/api/types.d.ts.map +1 -1
  64. package/dist/api/types.js.map +1 -1
  65. package/dist/channels/adapters/RedditChannelAdapter.js.map +1 -1
  66. package/dist/core/audio/AudioProcessor.d.ts.map +1 -1
  67. package/dist/core/audio/AudioProcessor.js +1 -0
  68. package/dist/core/audio/AudioProcessor.js.map +1 -1
  69. package/dist/core/audio/EnvironmentalCalibrator.d.ts.map +1 -1
  70. package/dist/core/audio/EnvironmentalCalibrator.js +1 -0
  71. package/dist/core/audio/EnvironmentalCalibrator.js.map +1 -1
  72. package/dist/core/audio/FallbackAudioProxy.d.ts +169 -0
  73. package/dist/core/audio/FallbackAudioProxy.d.ts.map +1 -0
  74. package/dist/core/audio/FallbackAudioProxy.js +236 -0
  75. package/dist/core/audio/FallbackAudioProxy.js.map +1 -0
  76. package/dist/core/audio/IAudioGenerator.d.ts +103 -0
  77. package/dist/core/audio/IAudioGenerator.d.ts.map +1 -0
  78. package/dist/core/audio/IAudioGenerator.js +24 -0
  79. package/dist/core/audio/IAudioGenerator.js.map +1 -0
  80. package/dist/core/audio/index.d.ts +54 -0
  81. package/dist/core/audio/index.d.ts.map +1 -1
  82. package/dist/core/audio/index.js +93 -0
  83. package/dist/core/audio/index.js.map +1 -1
  84. package/dist/core/audio/providers/AudioGenLocalProvider.d.ts +136 -0
  85. package/dist/core/audio/providers/AudioGenLocalProvider.d.ts.map +1 -0
  86. package/dist/core/audio/providers/AudioGenLocalProvider.js +235 -0
  87. package/dist/core/audio/providers/AudioGenLocalProvider.js.map +1 -0
  88. package/dist/core/audio/providers/ElevenLabsSFXProvider.d.ts +107 -0
  89. package/dist/core/audio/providers/ElevenLabsSFXProvider.d.ts.map +1 -0
  90. package/dist/core/audio/providers/ElevenLabsSFXProvider.js +154 -0
  91. package/dist/core/audio/providers/ElevenLabsSFXProvider.js.map +1 -0
  92. package/dist/core/audio/providers/FalAudioProvider.d.ts +207 -0
  93. package/dist/core/audio/providers/FalAudioProvider.d.ts.map +1 -0
  94. package/dist/core/audio/providers/FalAudioProvider.js +315 -0
  95. package/dist/core/audio/providers/FalAudioProvider.js.map +1 -0
  96. package/dist/core/audio/providers/MusicGenLocalProvider.d.ts +136 -0
  97. package/dist/core/audio/providers/MusicGenLocalProvider.d.ts.map +1 -0
  98. package/dist/core/audio/providers/MusicGenLocalProvider.js +235 -0
  99. package/dist/core/audio/providers/MusicGenLocalProvider.js.map +1 -0
  100. package/dist/core/audio/providers/ReplicateAudioProvider.d.ts +200 -0
  101. package/dist/core/audio/providers/ReplicateAudioProvider.d.ts.map +1 -0
  102. package/dist/core/audio/providers/ReplicateAudioProvider.js +346 -0
  103. package/dist/core/audio/providers/ReplicateAudioProvider.js.map +1 -0
  104. package/dist/core/audio/providers/StableAudioProvider.d.ts +138 -0
  105. package/dist/core/audio/providers/StableAudioProvider.d.ts.map +1 -0
  106. package/dist/core/audio/providers/StableAudioProvider.js +192 -0
  107. package/dist/core/audio/providers/StableAudioProvider.js.map +1 -0
  108. package/dist/core/audio/providers/SunoProvider.d.ts +182 -0
  109. package/dist/core/audio/providers/SunoProvider.d.ts.map +1 -0
  110. package/dist/core/audio/providers/SunoProvider.js +312 -0
  111. package/dist/core/audio/providers/SunoProvider.js.map +1 -0
  112. package/dist/core/audio/providers/UdioProvider.d.ts +177 -0
  113. package/dist/core/audio/providers/UdioProvider.d.ts.map +1 -0
  114. package/dist/core/audio/providers/UdioProvider.js +305 -0
  115. package/dist/core/audio/providers/UdioProvider.js.map +1 -0
  116. package/dist/core/audio/types.d.ts +257 -0
  117. package/dist/core/audio/types.d.ts.map +1 -0
  118. package/dist/core/audio/types.js +21 -0
  119. package/dist/core/audio/types.js.map +1 -0
  120. package/dist/core/images/FallbackImageProxy.d.ts +183 -0
  121. package/dist/core/images/FallbackImageProxy.d.ts.map +1 -0
  122. package/dist/core/images/FallbackImageProxy.js +283 -0
  123. package/dist/core/images/FallbackImageProxy.js.map +1 -0
  124. package/dist/core/images/IImageProvider.d.ts +1 -1
  125. package/dist/core/images/IImageProvider.d.ts.map +1 -1
  126. package/dist/core/images/index.d.ts +1 -0
  127. package/dist/core/images/index.d.ts.map +1 -1
  128. package/dist/core/images/index.js +1 -0
  129. package/dist/core/images/index.js.map +1 -1
  130. package/dist/core/llm/providers/AIModelProviderManager.d.ts +3 -1
  131. package/dist/core/llm/providers/AIModelProviderManager.d.ts.map +1 -1
  132. package/dist/core/llm/providers/AIModelProviderManager.js +8 -0
  133. package/dist/core/llm/providers/AIModelProviderManager.js.map +1 -1
  134. package/dist/core/llm/providers/errors/ClaudeCodeProviderError.d.ts +52 -0
  135. package/dist/core/llm/providers/errors/ClaudeCodeProviderError.d.ts.map +1 -0
  136. package/dist/core/llm/providers/errors/ClaudeCodeProviderError.js +36 -0
  137. package/dist/core/llm/providers/errors/ClaudeCodeProviderError.js.map +1 -0
  138. package/dist/core/llm/providers/errors/GeminiCLIProviderError.d.ts +32 -0
  139. package/dist/core/llm/providers/errors/GeminiCLIProviderError.d.ts.map +1 -0
  140. package/dist/core/llm/providers/errors/GeminiCLIProviderError.js +27 -0
  141. package/dist/core/llm/providers/errors/GeminiCLIProviderError.js.map +1 -0
  142. package/dist/core/llm/providers/implementations/ClaudeCodeCLIBridge.d.ts +38 -0
  143. package/dist/core/llm/providers/implementations/ClaudeCodeCLIBridge.d.ts.map +1 -0
  144. package/dist/core/llm/providers/implementations/ClaudeCodeCLIBridge.js +128 -0
  145. package/dist/core/llm/providers/implementations/ClaudeCodeCLIBridge.js.map +1 -0
  146. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts +107 -0
  147. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts.map +1 -0
  148. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js +504 -0
  149. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js.map +1 -0
  150. package/dist/core/llm/providers/implementations/GeminiCLIBridge.d.ts +60 -0
  151. package/dist/core/llm/providers/implementations/GeminiCLIBridge.d.ts.map +1 -0
  152. package/dist/core/llm/providers/implementations/GeminiCLIBridge.js +177 -0
  153. package/dist/core/llm/providers/implementations/GeminiCLIBridge.js.map +1 -0
  154. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts +55 -0
  155. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts.map +1 -0
  156. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js +447 -0
  157. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js.map +1 -0
  158. package/dist/core/media/ProviderPreferences.d.ts +158 -0
  159. package/dist/core/media/ProviderPreferences.d.ts.map +1 -0
  160. package/dist/core/media/ProviderPreferences.js +183 -0
  161. package/dist/core/media/ProviderPreferences.js.map +1 -0
  162. package/dist/core/subprocess/CLIRegistry.d.ts +71 -0
  163. package/dist/core/subprocess/CLIRegistry.d.ts.map +1 -0
  164. package/dist/core/subprocess/CLIRegistry.js +210 -0
  165. package/dist/core/subprocess/CLIRegistry.js.map +1 -0
  166. package/dist/core/subprocess/CLISubprocessBridge.d.ts +117 -0
  167. package/dist/core/subprocess/CLISubprocessBridge.d.ts.map +1 -0
  168. package/dist/core/subprocess/CLISubprocessBridge.js +199 -0
  169. package/dist/core/subprocess/CLISubprocessBridge.js.map +1 -0
  170. package/dist/core/subprocess/errors.d.ts +76 -0
  171. package/dist/core/subprocess/errors.d.ts.map +1 -0
  172. package/dist/core/subprocess/errors.js +75 -0
  173. package/dist/core/subprocess/errors.js.map +1 -0
  174. package/dist/core/subprocess/index.d.ts +11 -0
  175. package/dist/core/subprocess/index.d.ts.map +1 -0
  176. package/dist/core/subprocess/index.js +10 -0
  177. package/dist/core/subprocess/index.js.map +1 -0
  178. package/dist/core/subprocess/types.d.ts +100 -0
  179. package/dist/core/subprocess/types.d.ts.map +1 -0
  180. package/dist/core/subprocess/types.js +9 -0
  181. package/dist/core/subprocess/types.js.map +1 -0
  182. package/dist/core/video/FallbackVideoProxy.d.ts +166 -0
  183. package/dist/core/video/FallbackVideoProxy.d.ts.map +1 -0
  184. package/dist/core/video/FallbackVideoProxy.js +228 -0
  185. package/dist/core/video/FallbackVideoProxy.js.map +1 -0
  186. package/dist/core/video/IVideoAnalyzer.d.ts +29 -0
  187. package/dist/core/video/IVideoAnalyzer.d.ts.map +1 -0
  188. package/dist/core/video/IVideoAnalyzer.js +12 -0
  189. package/dist/core/video/IVideoAnalyzer.js.map +1 -0
  190. package/dist/core/video/IVideoGenerator.d.ts +76 -0
  191. package/dist/core/video/IVideoGenerator.d.ts.map +1 -0
  192. package/dist/core/video/IVideoGenerator.js +13 -0
  193. package/dist/core/video/IVideoGenerator.js.map +1 -0
  194. package/dist/core/video/VideoAnalyzer.d.ts +278 -0
  195. package/dist/core/video/VideoAnalyzer.d.ts.map +1 -0
  196. package/dist/core/video/VideoAnalyzer.js +648 -0
  197. package/dist/core/video/VideoAnalyzer.js.map +1 -0
  198. package/dist/core/video/index.d.ts +55 -0
  199. package/dist/core/video/index.d.ts.map +1 -0
  200. package/dist/core/video/index.js +78 -0
  201. package/dist/core/video/index.js.map +1 -0
  202. package/dist/core/video/providers/FalVideoProvider.d.ts +195 -0
  203. package/dist/core/video/providers/FalVideoProvider.d.ts.map +1 -0
  204. package/dist/core/video/providers/FalVideoProvider.js +322 -0
  205. package/dist/core/video/providers/FalVideoProvider.js.map +1 -0
  206. package/dist/core/video/providers/ReplicateVideoProvider.d.ts +194 -0
  207. package/dist/core/video/providers/ReplicateVideoProvider.d.ts.map +1 -0
  208. package/dist/core/video/providers/ReplicateVideoProvider.js +356 -0
  209. package/dist/core/video/providers/ReplicateVideoProvider.js.map +1 -0
  210. package/dist/core/video/providers/RunwayVideoProvider.d.ts +175 -0
  211. package/dist/core/video/providers/RunwayVideoProvider.d.ts.map +1 -0
  212. package/dist/core/video/providers/RunwayVideoProvider.js +293 -0
  213. package/dist/core/video/providers/RunwayVideoProvider.js.map +1 -0
  214. package/dist/core/video/types.d.ts +441 -0
  215. package/dist/core/video/types.d.ts.map +1 -0
  216. package/dist/core/video/types.js +10 -0
  217. package/dist/core/video/types.js.map +1 -0
  218. package/dist/core/vision/SceneDetector.d.ts +180 -0
  219. package/dist/core/vision/SceneDetector.d.ts.map +1 -0
  220. package/dist/core/vision/SceneDetector.js +366 -0
  221. package/dist/core/vision/SceneDetector.js.map +1 -0
  222. package/dist/core/vision/index.d.ts +2 -1
  223. package/dist/core/vision/index.d.ts.map +1 -1
  224. package/dist/core/vision/index.js +1 -0
  225. package/dist/core/vision/index.js.map +1 -1
  226. package/dist/core/vision/types.d.ts +125 -0
  227. package/dist/core/vision/types.d.ts.map +1 -1
  228. package/dist/discovery/CapabilityDiscoveryEngine.d.ts +32 -0
  229. package/dist/discovery/CapabilityDiscoveryEngine.d.ts.map +1 -1
  230. package/dist/discovery/CapabilityDiscoveryEngine.js +46 -0
  231. package/dist/discovery/CapabilityDiscoveryEngine.js.map +1 -1
  232. package/dist/extensions/MultiRegistryLoader.js.map +1 -1
  233. package/dist/index.d.ts +17 -2
  234. package/dist/index.d.ts.map +1 -1
  235. package/dist/index.js +12 -0
  236. package/dist/index.js.map +1 -1
  237. package/dist/memory/CognitiveMemoryManager.d.ts +40 -0
  238. package/dist/memory/CognitiveMemoryManager.d.ts.map +1 -1
  239. package/dist/memory/CognitiveMemoryManager.js +54 -1
  240. package/dist/memory/CognitiveMemoryManager.js.map +1 -1
  241. package/dist/memory/facade/Memory.d.ts +4 -0
  242. package/dist/memory/facade/Memory.d.ts.map +1 -1
  243. package/dist/memory/facade/Memory.js +140 -4
  244. package/dist/memory/facade/Memory.js.map +1 -1
  245. package/dist/memory/facade/types.d.ts +30 -2
  246. package/dist/memory/facade/types.d.ts.map +1 -1
  247. package/dist/memory/index.d.ts +1 -0
  248. package/dist/memory/index.d.ts.map +1 -1
  249. package/dist/memory/index.js +1 -0
  250. package/dist/memory/index.js.map +1 -1
  251. package/dist/memory/types.d.ts +15 -0
  252. package/dist/memory/types.d.ts.map +1 -1
  253. package/dist/query-router/QueryClassifier.d.ts +192 -21
  254. package/dist/query-router/QueryClassifier.d.ts.map +1 -1
  255. package/dist/query-router/QueryClassifier.js +604 -23
  256. package/dist/query-router/QueryClassifier.js.map +1 -1
  257. package/dist/query-router/QueryDispatcher.d.ts +106 -8
  258. package/dist/query-router/QueryDispatcher.d.ts.map +1 -1
  259. package/dist/query-router/QueryDispatcher.js +387 -8
  260. package/dist/query-router/QueryDispatcher.js.map +1 -1
  261. package/dist/query-router/QueryRouter.d.ts +198 -14
  262. package/dist/query-router/QueryRouter.d.ts.map +1 -1
  263. package/dist/query-router/QueryRouter.js +738 -50
  264. package/dist/query-router/QueryRouter.js.map +1 -1
  265. package/dist/query-router/index.d.ts +1 -1
  266. package/dist/query-router/index.d.ts.map +1 -1
  267. package/dist/query-router/index.js +1 -1
  268. package/dist/query-router/index.js.map +1 -1
  269. package/dist/query-router/types.d.ts +396 -3
  270. package/dist/query-router/types.d.ts.map +1 -1
  271. package/dist/query-router/types.js +35 -0
  272. package/dist/query-router/types.js.map +1 -1
  273. package/dist/rag/HydeRetriever.d.ts +108 -0
  274. package/dist/rag/HydeRetriever.d.ts.map +1 -1
  275. package/dist/rag/HydeRetriever.js +184 -0
  276. package/dist/rag/HydeRetriever.js.map +1 -1
  277. package/dist/rag/IRetrievalAugmentor.d.ts +15 -0
  278. package/dist/rag/IRetrievalAugmentor.d.ts.map +1 -1
  279. package/dist/rag/RetrievalAugmentor.d.ts +58 -0
  280. package/dist/rag/RetrievalAugmentor.d.ts.map +1 -1
  281. package/dist/rag/RetrievalAugmentor.js +200 -32
  282. package/dist/rag/RetrievalAugmentor.js.map +1 -1
  283. package/dist/rag/VectorStoreManager.js +1 -1
  284. package/dist/rag/audit/RAGAuditCollector.d.ts +7 -0
  285. package/dist/rag/audit/RAGAuditCollector.d.ts.map +1 -1
  286. package/dist/rag/audit/RAGAuditCollector.js +10 -0
  287. package/dist/rag/audit/RAGAuditCollector.js.map +1 -1
  288. package/dist/rag/audit/RAGAuditTypes.d.ts +10 -1
  289. package/dist/rag/audit/RAGAuditTypes.d.ts.map +1 -1
  290. package/dist/rag/chunking/SemanticChunker.d.ts +210 -0
  291. package/dist/rag/chunking/SemanticChunker.d.ts.map +1 -0
  292. package/dist/rag/chunking/SemanticChunker.js +460 -0
  293. package/dist/rag/chunking/SemanticChunker.js.map +1 -0
  294. package/dist/rag/chunking/index.d.ts +10 -0
  295. package/dist/rag/chunking/index.d.ts.map +1 -0
  296. package/dist/rag/chunking/index.js +10 -0
  297. package/dist/rag/chunking/index.js.map +1 -0
  298. package/dist/rag/implementations/vector_stores/PineconeVectorStore.d.ts +103 -0
  299. package/dist/rag/implementations/vector_stores/PineconeVectorStore.d.ts.map +1 -0
  300. package/dist/rag/implementations/vector_stores/PineconeVectorStore.js +315 -0
  301. package/dist/rag/implementations/vector_stores/PineconeVectorStore.js.map +1 -0
  302. package/dist/rag/implementations/vector_stores/PostgresVectorStore.d.ts +107 -0
  303. package/dist/rag/implementations/vector_stores/PostgresVectorStore.d.ts.map +1 -0
  304. package/dist/rag/implementations/vector_stores/PostgresVectorStore.js +438 -0
  305. package/dist/rag/implementations/vector_stores/PostgresVectorStore.js.map +1 -0
  306. package/dist/rag/index.d.ts +15 -1
  307. package/dist/rag/index.d.ts.map +1 -1
  308. package/dist/rag/index.js +32 -0
  309. package/dist/rag/index.js.map +1 -1
  310. package/dist/rag/migration/MigrationEngine.d.ts +47 -0
  311. package/dist/rag/migration/MigrationEngine.d.ts.map +1 -0
  312. package/dist/rag/migration/MigrationEngine.js +168 -0
  313. package/dist/rag/migration/MigrationEngine.js.map +1 -0
  314. package/dist/rag/migration/adapters/PineconeSourceAdapter.d.ts +23 -0
  315. package/dist/rag/migration/adapters/PineconeSourceAdapter.d.ts.map +1 -0
  316. package/dist/rag/migration/adapters/PineconeSourceAdapter.js +63 -0
  317. package/dist/rag/migration/adapters/PineconeSourceAdapter.js.map +1 -0
  318. package/dist/rag/migration/adapters/PostgresSourceAdapter.d.ts +30 -0
  319. package/dist/rag/migration/adapters/PostgresSourceAdapter.d.ts.map +1 -0
  320. package/dist/rag/migration/adapters/PostgresSourceAdapter.js +71 -0
  321. package/dist/rag/migration/adapters/PostgresSourceAdapter.js.map +1 -0
  322. package/dist/rag/migration/adapters/PostgresTargetAdapter.d.ts +38 -0
  323. package/dist/rag/migration/adapters/PostgresTargetAdapter.d.ts.map +1 -0
  324. package/dist/rag/migration/adapters/PostgresTargetAdapter.js +114 -0
  325. package/dist/rag/migration/adapters/PostgresTargetAdapter.js.map +1 -0
  326. package/dist/rag/migration/adapters/QdrantSourceAdapter.d.ts +36 -0
  327. package/dist/rag/migration/adapters/QdrantSourceAdapter.d.ts.map +1 -0
  328. package/dist/rag/migration/adapters/QdrantSourceAdapter.js +109 -0
  329. package/dist/rag/migration/adapters/QdrantSourceAdapter.js.map +1 -0
  330. package/dist/rag/migration/adapters/QdrantTargetAdapter.d.ts +35 -0
  331. package/dist/rag/migration/adapters/QdrantTargetAdapter.d.ts.map +1 -0
  332. package/dist/rag/migration/adapters/QdrantTargetAdapter.js +110 -0
  333. package/dist/rag/migration/adapters/QdrantTargetAdapter.js.map +1 -0
  334. package/dist/rag/migration/adapters/SqliteSourceAdapter.d.ts +37 -0
  335. package/dist/rag/migration/adapters/SqliteSourceAdapter.d.ts.map +1 -0
  336. package/dist/rag/migration/adapters/SqliteSourceAdapter.js +72 -0
  337. package/dist/rag/migration/adapters/SqliteSourceAdapter.js.map +1 -0
  338. package/dist/rag/migration/adapters/SqliteTargetAdapter.d.ts +47 -0
  339. package/dist/rag/migration/adapters/SqliteTargetAdapter.d.ts.map +1 -0
  340. package/dist/rag/migration/adapters/SqliteTargetAdapter.js +93 -0
  341. package/dist/rag/migration/adapters/SqliteTargetAdapter.js.map +1 -0
  342. package/dist/rag/migration/types.d.ts +108 -0
  343. package/dist/rag/migration/types.d.ts.map +1 -0
  344. package/dist/rag/migration/types.js +11 -0
  345. package/dist/rag/migration/types.js.map +1 -0
  346. package/dist/rag/multimodal/MultimodalIndexer.d.ts +35 -0
  347. package/dist/rag/multimodal/MultimodalIndexer.d.ts.map +1 -1
  348. package/dist/rag/multimodal/MultimodalIndexer.js +66 -1
  349. package/dist/rag/multimodal/MultimodalIndexer.js.map +1 -1
  350. package/dist/rag/multimodal/types.d.ts +24 -0
  351. package/dist/rag/multimodal/types.d.ts.map +1 -1
  352. package/dist/rag/raptor/RaptorTree.d.ts +268 -0
  353. package/dist/rag/raptor/RaptorTree.d.ts.map +1 -0
  354. package/dist/rag/raptor/RaptorTree.js +443 -0
  355. package/dist/rag/raptor/RaptorTree.js.map +1 -0
  356. package/dist/rag/raptor/index.d.ts +11 -0
  357. package/dist/rag/raptor/index.d.ts.map +1 -0
  358. package/dist/rag/raptor/index.js +11 -0
  359. package/dist/rag/raptor/index.js.map +1 -0
  360. package/dist/rag/reranking/providers/CohereReranker.js.map +1 -1
  361. package/dist/rag/search/BM25Index.d.ts +282 -0
  362. package/dist/rag/search/BM25Index.d.ts.map +1 -0
  363. package/dist/rag/search/BM25Index.js +344 -0
  364. package/dist/rag/search/BM25Index.js.map +1 -0
  365. package/dist/rag/search/HybridSearcher.d.ts +198 -0
  366. package/dist/rag/search/HybridSearcher.d.ts.map +1 -0
  367. package/dist/rag/search/HybridSearcher.js +316 -0
  368. package/dist/rag/search/HybridSearcher.js.map +1 -0
  369. package/dist/rag/search/index.d.ts +12 -0
  370. package/dist/rag/search/index.d.ts.map +1 -0
  371. package/dist/rag/search/index.js +12 -0
  372. package/dist/rag/search/index.js.map +1 -0
  373. package/dist/rag/setup/DockerDetector.d.ts +67 -0
  374. package/dist/rag/setup/DockerDetector.d.ts.map +1 -0
  375. package/dist/rag/setup/DockerDetector.js +125 -0
  376. package/dist/rag/setup/DockerDetector.js.map +1 -0
  377. package/dist/rag/setup/PostgresSetup.d.ts +20 -0
  378. package/dist/rag/setup/PostgresSetup.d.ts.map +1 -0
  379. package/dist/rag/setup/PostgresSetup.js +133 -0
  380. package/dist/rag/setup/PostgresSetup.js.map +1 -0
  381. package/dist/rag/setup/QdrantSetup.d.ts +26 -0
  382. package/dist/rag/setup/QdrantSetup.d.ts.map +1 -0
  383. package/dist/rag/setup/QdrantSetup.js +96 -0
  384. package/dist/rag/setup/QdrantSetup.js.map +1 -0
  385. package/dist/rag/setup/types.d.ts +55 -0
  386. package/dist/rag/setup/types.d.ts.map +1 -0
  387. package/dist/rag/setup/types.js +6 -0
  388. package/dist/rag/setup/types.js.map +1 -0
  389. package/dist/rag/unified/UnifiedRetriever.d.ts +472 -0
  390. package/dist/rag/unified/UnifiedRetriever.d.ts.map +1 -0
  391. package/dist/rag/unified/UnifiedRetriever.js +887 -0
  392. package/dist/rag/unified/UnifiedRetriever.js.map +1 -0
  393. package/dist/rag/unified/index.d.ts +24 -0
  394. package/dist/rag/unified/index.d.ts.map +1 -0
  395. package/dist/rag/unified/index.js +23 -0
  396. package/dist/rag/unified/index.js.map +1 -0
  397. package/dist/rag/unified/types.d.ts +546 -0
  398. package/dist/rag/unified/types.d.ts.map +1 -0
  399. package/dist/rag/unified/types.js +177 -0
  400. package/dist/rag/unified/types.js.map +1 -0
  401. package/dist/speech/providers/AssemblyAISTTProvider.js.map +1 -1
  402. package/dist/speech/providers/AzureSpeechSTTProvider.js.map +1 -1
  403. package/dist/speech/providers/BuiltInAdaptiveVadProvider.d.ts +1 -1
  404. package/dist/speech/providers/DeepgramBatchSTTProvider.js.map +1 -1
  405. package/package.json +5 -2
@@ -29,13 +29,14 @@
29
29
  * router falls back gracefully to {@link KeywordFallback} for all retrieval.
30
30
  */
31
31
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
32
- import { join, extname } from 'node:path';
32
+ import { dirname, extname, join, resolve } from 'node:path';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
34
  import { QueryClassifier } from './QueryClassifier.js';
34
35
  import { QueryDispatcher } from './QueryDispatcher.js';
35
36
  import { QueryGenerator } from './QueryGenerator.js';
36
37
  import { TopicExtractor } from './TopicExtractor.js';
37
38
  import { KeywordFallback } from './KeywordFallback.js';
38
- import { DEFAULT_QUERY_ROUTER_CONFIG } from './types.js';
39
+ import { DEFAULT_QUERY_ROUTER_CONFIG, DEFAULT_STRATEGY_CONFIG, } from './types.js';
39
40
  /** Regex for splitting markdown by h1-h3 headings. */
40
41
  const HEADING_REGEX = /^#{1,3}\s+(.+)/;
41
42
  /** Maximum character length for a single corpus chunk. */
@@ -44,6 +45,11 @@ const MAX_CHUNK_CHARS = 6000;
44
45
  const MIN_CHUNK_CHARS = 20;
45
46
  /** Supported markdown file extensions for corpus loading. */
46
47
  const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
48
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
49
+ const GITHUB_EXTENSION_LOCAL_ENTRY_CANDIDATES = [
50
+ resolve(MODULE_DIR, '../../../../packages/agentos-extensions/registry/curated/integrations/github/dist/index.js'),
51
+ resolve(MODULE_DIR, '../../../../apps/wunderland-sol/packages/agentos-extensions/registry/curated/integrations/github/dist/index.js'),
52
+ ];
47
53
  // ============================================================================
48
54
  // QueryRouter
49
55
  // ============================================================================
@@ -102,6 +108,19 @@ export class QueryRouter {
102
108
  this.providerManager = null;
103
109
  /** Embedding dimension for the configured model. Zero if embeddings unavailable. */
104
110
  this.embeddingDimension = 0;
111
+ /** Current embedding availability state for corpus retrieval. */
112
+ this.embeddingStatus = 'disabled-no-key';
113
+ /**
114
+ * Optional UnifiedRetriever for plan-based retrieval.
115
+ *
116
+ * When set via {@link setUnifiedRetriever}, the `route()` method uses
117
+ * the UnifiedRetriever instead of the legacy QueryDispatcher for the
118
+ * retrieval phase. The UnifiedRetriever executes a structured
119
+ * {@link RetrievalPlan} across all available sources in parallel.
120
+ *
121
+ * @see setUnifiedRetriever
122
+ */
123
+ this.unifiedRetriever = null;
105
124
  /**
106
125
  * The data source ID used for corpus embeddings in the vector store.
107
126
  * Matches the collection name configured during init().
@@ -112,9 +131,77 @@ export class QueryRouter {
112
131
  ...config,
113
132
  deepResearchEnabled: config.deepResearchEnabled ?? Boolean(process.env.SERPER_API_KEY),
114
133
  availableTools: config.availableTools ?? [...DEFAULT_QUERY_ROUTER_CONFIG.availableTools],
134
+ strategyConfig: {
135
+ ...DEFAULT_STRATEGY_CONFIG,
136
+ ...config.strategyConfig,
137
+ },
115
138
  };
116
139
  }
117
140
  // ==========================================================================
141
+ // UNIFIED RETRIEVER INTEGRATION
142
+ // ==========================================================================
143
+ /**
144
+ * Attach a {@link UnifiedRetriever} for plan-based retrieval.
145
+ *
146
+ * When set, the `route()` method uses the UnifiedRetriever instead of
147
+ * the legacy QueryDispatcher for the retrieval phase. The classifier
148
+ * automatically produces a {@link RetrievalPlan} via `classifyWithPlan()`
149
+ * and the retriever executes it across all available sources in parallel.
150
+ *
151
+ * Pass `null` to revert to the legacy QueryDispatcher pipeline.
152
+ *
153
+ * @param retriever - A configured UnifiedRetriever instance, or `null` to disable.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const retriever = new UnifiedRetriever({
158
+ * hybridSearcher, raptorTree, graphEngine, memoryManager,
159
+ * });
160
+ * router.setUnifiedRetriever(retriever);
161
+ * // Now route() uses plan-based retrieval automatically
162
+ * ```
163
+ */
164
+ setUnifiedRetriever(retriever) {
165
+ this.unifiedRetriever = retriever;
166
+ }
167
+ /**
168
+ * Get the attached UnifiedRetriever, or `null` if not configured.
169
+ *
170
+ * @returns The UnifiedRetriever instance, or `null`.
171
+ */
172
+ getUnifiedRetriever() {
173
+ return this.unifiedRetriever;
174
+ }
175
+ // ==========================================================================
176
+ // CAPABILITY DISCOVERY INTEGRATION
177
+ // ==========================================================================
178
+ /**
179
+ * Attach a {@link CapabilityDiscoveryEngine} for capability-aware classification.
180
+ *
181
+ * When set, the classifier injects Tier 0 capability summaries (~150 tokens)
182
+ * into its LLM prompt, enabling it to recommend which skills, tools, and
183
+ * extensions should be activated for each query. The recommendations are
184
+ * included in the {@link ExecutionPlan} returned by `classifyWithPlan()`.
185
+ *
186
+ * Pass `null` to detach and revert to keyword-based heuristic capability
187
+ * selection.
188
+ *
189
+ * @param engine - A configured and initialized CapabilityDiscoveryEngine, or `null` to detach.
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const engine = new CapabilityDiscoveryEngine(embeddingManager, vectorStore);
194
+ * await engine.initialize({ tools, skills, extensions, channels });
195
+ * router.setCapabilityDiscoveryEngine(engine);
196
+ * // Now route() includes skill/tool/extension recommendations in the execution plan
197
+ * ```
198
+ */
199
+ setCapabilityDiscoveryEngine(engine) {
200
+ if (this.classifier) {
201
+ this.classifier.setCapabilityDiscoveryEngine(engine);
202
+ }
203
+ }
204
+ // ==========================================================================
118
205
  // PUBLIC API
119
206
  // ==========================================================================
120
207
  /**
@@ -132,6 +219,9 @@ export class QueryRouter {
132
219
  async init() {
133
220
  // 1. Load corpus chunks from the configured knowledge directories
134
221
  this.corpus = this.loadCorpus(this.config.knowledgeCorpus);
222
+ if (this.corpus.length === 0) {
223
+ throw new Error(this.buildEmptyCorpusError(this.config.knowledgeCorpus));
224
+ }
135
225
  // 2. Extract topics for the classifier's system prompt
136
226
  const topicExtractor = new TopicExtractor();
137
227
  this.topics = topicExtractor.extract(this.corpus);
@@ -143,36 +233,125 @@ export class QueryRouter {
143
233
  // fallback will still work for all retrieval operations.
144
234
  await this.embedCorpus();
145
235
  // 5. Instantiate the classifier
146
- this.classifier = new QueryClassifier({
147
- model: this.config.classifierModel,
148
- provider: this.config.classifierProvider,
149
- confidenceThreshold: this.config.confidenceThreshold,
150
- maxTier: this.config.maxTier,
151
- topicList,
152
- toolList: this.formatToolList(this.config.availableTools),
153
- apiKey: this.config.apiKey,
154
- baseUrl: this.config.baseUrl,
155
- });
236
+ this.classifier = this.createClassifier(topicList);
156
237
  // 6. Instantiate the generator
157
238
  this.generator = new QueryGenerator({
158
239
  model: this.config.generationModel,
159
240
  modelDeep: this.config.generationModelDeep,
160
241
  provider: this.config.generationProvider,
161
- apiKey: this.config.apiKey,
162
- baseUrl: this.config.baseUrl,
242
+ apiKey: this.getLlmApiKey(),
243
+ baseUrl: this.getLlmBaseUrl(),
163
244
  maxContextTokens: this.config.maxContextTokens,
164
245
  });
165
246
  // 7. Instantiate the dispatcher with callback dependencies
166
247
  this.dispatcher = new QueryDispatcher({
167
248
  vectorSearch: (query, topK) => this.vectorSearch(query, topK),
168
- graphExpand: (seeds) => this.graphExpand(seeds),
169
- rerank: (query, chunks, topN) => this.rerank(query, chunks, topN),
170
- deepResearch: (query, sources) => this.deepResearch(query, sources),
249
+ hydeSearch: (query, topK) => this.hydeSearch(query, topK),
250
+ decompose: (query, maxSubQueries) => this.decomposeQuery(query, maxSubQueries),
251
+ graphExpand: (seeds) => this.config.graphExpand ? this.config.graphExpand(seeds) : this.graphExpand(seeds),
252
+ rerank: (query, chunks, topN) => this.config.rerank
253
+ ? this.config.rerank(query, chunks, topN)
254
+ : this.rerank(query, chunks, topN),
255
+ deepResearch: (query, sources) => this.config.deepResearch
256
+ ? this.config.deepResearch(query, sources)
257
+ : this.deepResearch(query, sources),
171
258
  emit: (event) => this.emit(event),
172
259
  graphEnabled: this.config.graphEnabled,
173
260
  deepResearchEnabled: this.config.deepResearchEnabled,
261
+ maxSubQueries: this.config.strategyConfig.maxSubQueries,
174
262
  });
175
263
  this.initialized = true;
264
+ // ----------------------------------------------------------------
265
+ // Background GitHub repo indexing (non-blocking)
266
+ // ----------------------------------------------------------------
267
+ // Runs after the router is fully initialized so that query routing
268
+ // is available immediately. Indexed chunks are merged into the
269
+ // corpus, then the live retrieval/classification state is refreshed
270
+ // once indexing completes.
271
+ const repoConfig = this.config.githubRepos ?? {};
272
+ const includeEcosystem = repoConfig.includeEcosystem ?? true;
273
+ if (includeEcosystem || (repoConfig.repos && repoConfig.repos.length > 0)) {
274
+ Promise.resolve().then(async () => {
275
+ try {
276
+ const { GitHubRepoIndexer, GitHubService } = await this.loadGitHubExtensionModule();
277
+ const indexedChunks = [];
278
+ const token = repoConfig.token || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '';
279
+ const service = new GitHubService(token);
280
+ // initialize() creates the Octokit and validates auth.
281
+ // If the token is empty/invalid, Octokit is still created before
282
+ // the auth check throws, so public-only API access still works.
283
+ try {
284
+ await service.initialize();
285
+ }
286
+ catch { /* public-only mode */ }
287
+ const indexer = new GitHubRepoIndexer(service);
288
+ if (includeEcosystem) {
289
+ const results = await indexer.indexEcosystem();
290
+ const totalChunks = results.reduce((s, r) => s + r.chunks.length, 0);
291
+ for (const result of results) {
292
+ indexedChunks.push(...result.chunks);
293
+ this.emit({
294
+ type: 'github:index:complete',
295
+ repo: result.repo,
296
+ chunksTotal: result.chunks.length,
297
+ durationMs: result.durationMs,
298
+ timestamp: Date.now(),
299
+ });
300
+ }
301
+ console.log(`[QueryRouter] GitHub ecosystem indexed: ${results.length} repos, ${totalChunks} chunks`);
302
+ }
303
+ if (repoConfig.repos?.length) {
304
+ for (const { owner, repo } of repoConfig.repos) {
305
+ try {
306
+ const result = await indexer.indexRepo(owner, repo);
307
+ indexedChunks.push(...result.chunks);
308
+ this.emit({
309
+ type: 'github:index:complete',
310
+ repo: result.repo,
311
+ chunksTotal: result.chunks.length,
312
+ durationMs: result.durationMs,
313
+ timestamp: Date.now(),
314
+ });
315
+ }
316
+ catch (err) {
317
+ this.emit({
318
+ type: 'github:index:error',
319
+ repo: `${owner}/${repo}`,
320
+ error: err instanceof Error ? err.message : String(err),
321
+ timestamp: Date.now(),
322
+ });
323
+ }
324
+ }
325
+ }
326
+ await this.syncIndexedCorpusChunks(indexedChunks);
327
+ }
328
+ catch (err) {
329
+ console.warn(`[QueryRouter] GitHub indexing failed: ${err instanceof Error ? err.message : String(err)}`);
330
+ }
331
+ });
332
+ }
333
+ }
334
+ async loadGitHubExtensionModule() {
335
+ const specifiers = [
336
+ '@framers/agentos-ext-github',
337
+ ...GITHUB_EXTENSION_LOCAL_ENTRY_CANDIDATES
338
+ .filter((candidate) => existsSync(candidate))
339
+ .map((candidate) => pathToFileURL(candidate).href),
340
+ ];
341
+ const failures = [];
342
+ for (const specifier of specifiers) {
343
+ try {
344
+ const module = (await import(/* @vite-ignore */ specifier));
345
+ if (module.GitHubRepoIndexer && module.GitHubService) {
346
+ return module;
347
+ }
348
+ failures.push(`${specifier} loaded without GitHubRepoIndexer/GitHubService exports`);
349
+ }
350
+ catch (error) {
351
+ failures.push(`${specifier}: ${error instanceof Error ? error.message : String(error)}`);
352
+ }
353
+ }
354
+ throw new Error(`Unable to load GitHub extension runtime for QueryRouter indexing. ${failures.join(' | ')}`);
176
355
  }
177
356
  /**
178
357
  * Classify a query into a complexity tier without dispatching or generating.
@@ -248,9 +427,48 @@ export class QueryRouter {
248
427
  if (this.config.onClassification) {
249
428
  this.config.onClassification(classification);
250
429
  }
251
- // --- Phase 2: Retrieval ---
430
+ // --- Phase 2: Retrieval (with capability recommendations) ---
431
+ // When a UnifiedRetriever is attached, use plan-based retrieval with
432
+ // full ExecutionPlan (including skill/tool/extension recommendations).
433
+ // Otherwise fall back to the legacy QueryDispatcher pipeline.
252
434
  const retrievalEventStart = this.events.length;
253
- const retrieval = await this.dispatcher.dispatch(query, classification.tier, classification.suggestedSources);
435
+ let retrieval;
436
+ if (this.unifiedRetriever && this.classifier) {
437
+ // Plan-based retrieval via UnifiedRetriever
438
+ const { buildDefaultExecutionPlan } = await import('../rag/unified/types.js');
439
+ let plan;
440
+ try {
441
+ const [, classifiedPlan] = await this.classifier.classifyWithPlan(query, conversationHistory);
442
+ plan = classifiedPlan;
443
+ }
444
+ catch {
445
+ plan = buildDefaultExecutionPlan(classification.strategy);
446
+ }
447
+ // Emit capabilities:activate event when the plan recommends capabilities.
448
+ // The agent runtime is responsible for deciding which to honor — the
449
+ // router only recommends, it does not activate.
450
+ if (plan.skills.length > 0 || plan.tools.length > 0 || plan.extensions.length > 0) {
451
+ this.emit({
452
+ type: 'capabilities:activate',
453
+ skills: plan.skills,
454
+ tools: plan.tools,
455
+ extensions: plan.extensions,
456
+ timestamp: Date.now(),
457
+ });
458
+ }
459
+ const unifiedResult = await this.unifiedRetriever.retrieve(query, plan);
460
+ retrieval = {
461
+ chunks: unifiedResult.chunks,
462
+ researchSynthesis: unifiedResult.researchSynthesis,
463
+ durationMs: unifiedResult.durationMs,
464
+ };
465
+ }
466
+ else {
467
+ // Legacy dispatcher pipeline (HyDE-aware strategy or tier-based)
468
+ retrieval = classification.strategy
469
+ ? await this.dispatcher.dispatchByStrategy(query, classification.strategy, classification.suggestedSources)
470
+ : await this.dispatcher.dispatch(query, classification.tier, classification.suggestedSources);
471
+ }
254
472
  const retrievalEvents = this.events.slice(retrievalEventStart);
255
473
  const fallbacksUsed = this.collectFallbacks(classification, retrievalEvents);
256
474
  const tiersUsed = this.collectTiersUsed(classification, fallbacksUsed);
@@ -285,6 +503,7 @@ export class QueryRouter {
285
503
  answer: generateResult.answer,
286
504
  classification,
287
505
  sources,
506
+ researchSynthesis: retrieval.researchSynthesis,
288
507
  durationMs: totalDuration,
289
508
  tiersUsed,
290
509
  fallbacksUsed,
@@ -329,6 +548,7 @@ export class QueryRouter {
329
548
  this.vectorStoreManager = null;
330
549
  this.providerManager = null;
331
550
  this.embeddingDimension = 0;
551
+ this.embeddingStatus = 'disabled-no-key';
332
552
  this.classifier = null;
333
553
  this.dispatcher = null;
334
554
  this.generator = null;
@@ -338,6 +558,45 @@ export class QueryRouter {
338
558
  this.events = [];
339
559
  this.initialized = false;
340
560
  }
561
+ /**
562
+ * Return lightweight corpus/index stats for observability and host startup
563
+ * logs.
564
+ *
565
+ * Useful after {@link init} so callers can confirm the router loaded a real
566
+ * corpus instead of only knowing that initialisation completed.
567
+ */
568
+ getCorpusStats() {
569
+ const vectorActive = Boolean(this.embeddingManager && this.vectorStoreManager);
570
+ const graphRuntimeMode = this.config.graphEnabled
571
+ ? (this.hasLiveGraphRuntime() ? 'active' : this.hasHeuristicGraphRuntime() ? 'heuristic' : 'placeholder')
572
+ : 'disabled';
573
+ const deepResearchRuntimeMode = this.config.deepResearchEnabled
574
+ ? (this.hasLiveDeepResearchRuntime()
575
+ ? 'active'
576
+ : this.hasHeuristicDeepResearchRuntime()
577
+ ? 'heuristic'
578
+ : 'placeholder')
579
+ : 'disabled';
580
+ return {
581
+ initialized: this.initialized,
582
+ configuredPathCount: this.config.knowledgeCorpus.length,
583
+ chunkCount: this.corpus.length,
584
+ topicCount: this.topics.length,
585
+ sourceCount: new Set(this.corpus.map((chunk) => chunk.sourcePath)).size,
586
+ retrievalMode: vectorActive ? 'vector+keyword-fallback' : 'keyword-only',
587
+ embeddingStatus: vectorActive ? 'active' : this.embeddingStatus,
588
+ embeddingDimension: vectorActive ? this.embeddingDimension : 0,
589
+ graphEnabled: this.config.graphEnabled,
590
+ deepResearchEnabled: this.config.deepResearchEnabled,
591
+ graphRuntimeMode,
592
+ rerankRuntimeMode: this.hasLiveRerankerRuntime()
593
+ ? 'active'
594
+ : this.hasHeuristicRerankerRuntime()
595
+ ? 'heuristic'
596
+ : 'placeholder',
597
+ deepResearchRuntimeMode,
598
+ };
599
+ }
341
600
  // ==========================================================================
342
601
  // PRIVATE — Corpus loading
343
602
  // ==========================================================================
@@ -386,6 +645,22 @@ export class QueryRouter {
386
645
  }
387
646
  return chunks;
388
647
  }
648
+ /**
649
+ * Build a clear init-time error for empty or unreadable corpora.
650
+ *
651
+ * The router can technically operate with keyword fallback only, but it
652
+ * should not silently mark itself ready when no corpus content was loaded
653
+ * at all. Callers usually interpret a successful `init()` as "docs loaded".
654
+ *
655
+ * @param paths - Configured knowledge corpus directory paths.
656
+ * @returns Human-readable error message for throwing from {@link init}.
657
+ */
658
+ buildEmptyCorpusError(paths) {
659
+ return ('QueryRouter init failed: no readable markdown corpus chunks were loaded. ' +
660
+ `Checked paths: ${paths.join(', ')}. ` +
661
+ 'Make sure at least one configured directory exists and contains readable ' +
662
+ '.md or .mdx files with non-trivial section content.');
663
+ }
389
664
  /**
390
665
  * Recursively walk a directory tree, invoking a callback for each file.
391
666
  *
@@ -480,8 +755,9 @@ export class QueryRouter {
480
755
  // Quick check: bail out early if there's obviously no API key configured.
481
756
  // This avoids the overhead of dynamic imports and provider initialization
482
757
  // in test environments and when no embedding provider is available.
483
- const apiKey = this.config.apiKey || process.env.OPENAI_API_KEY || '';
484
- if (!apiKey) {
758
+ const embeddingApiKey = this.getEmbeddingApiKey();
759
+ if (!embeddingApiKey) {
760
+ this.disableVectorRetrieval('disabled-no-key');
485
761
  console.debug('[QueryRouter] No embedding API key configured; skipping vector store embedding (keyword fallback active).');
486
762
  return;
487
763
  }
@@ -500,8 +776,8 @@ export class QueryRouter {
500
776
  providerId: this.config.embeddingProvider,
501
777
  enabled: true,
502
778
  config: {
503
- apiKey: this.config.apiKey || process.env.OPENAI_API_KEY || '',
504
- ...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
779
+ apiKey: embeddingApiKey,
780
+ ...(this.getEmbeddingBaseUrl() ? { baseUrl: this.getEmbeddingBaseUrl() } : {}),
505
781
  },
506
782
  isDefault: true,
507
783
  },
@@ -539,6 +815,7 @@ export class QueryRouter {
539
815
  }
540
816
  }
541
817
  this.embeddingDimension = dimension;
818
+ this.embeddingStatus = 'active';
542
819
  // --- 3. Initialise the vector store manager (in-memory) ---
543
820
  const vsm = new VectorStoreManagerClass();
544
821
  const collectionName = this.corpusDataSourceId;
@@ -596,12 +873,161 @@ export class QueryRouter {
596
873
  // Non-fatal: warn and continue — keyword fallback still works
597
874
  const message = error instanceof Error ? error.message : String(error);
598
875
  console.warn(`[QueryRouter] Embedding initialisation failed, falling back to keyword search: ${message}`);
599
- // Clean up any partial state
600
- this.embeddingManager = null;
601
- this.vectorStoreManager = null;
602
- this.providerManager = null;
603
- this.embeddingDimension = 0;
876
+ this.disableVectorRetrieval('failed-init');
877
+ }
878
+ }
879
+ async syncIndexedCorpusChunks(chunks) {
880
+ if (chunks.length === 0) {
881
+ return;
882
+ }
883
+ const appendedChunks = this.appendCorpusChunks(chunks);
884
+ await this.indexAdditionalCorpusChunks(appendedChunks);
885
+ this.rebuildCorpusSearchState();
886
+ }
887
+ appendCorpusChunks(chunks) {
888
+ const existingKeys = new Set(this.corpus.map((chunk) => `${chunk.sourcePath}\u0000${chunk.heading}\u0000${chunk.content}`));
889
+ const filteredChunks = chunks
890
+ .map((chunk) => ({
891
+ heading: chunk.heading,
892
+ sourcePath: chunk.sourcePath,
893
+ content: chunk.content.slice(0, MAX_CHUNK_CHARS).trim(),
894
+ }))
895
+ .filter((chunk) => {
896
+ if (chunk.content.length < MIN_CHUNK_CHARS) {
897
+ return false;
898
+ }
899
+ const dedupeKey = `${chunk.sourcePath}\u0000${chunk.heading}\u0000${chunk.content}`;
900
+ if (existingKeys.has(dedupeKey)) {
901
+ return false;
902
+ }
903
+ existingKeys.add(dedupeKey);
904
+ return true;
905
+ });
906
+ const startIndex = this.corpus.length;
907
+ const appendedChunks = filteredChunks.map((chunk, index) => ({
908
+ id: `gh_${startIndex + index}`,
909
+ heading: chunk.heading,
910
+ content: chunk.content,
911
+ sourcePath: chunk.sourcePath,
912
+ }));
913
+ this.corpus.push(...appendedChunks);
914
+ return appendedChunks;
915
+ }
916
+ rebuildCorpusSearchState() {
917
+ this.keywordFallback = new KeywordFallback(this.corpus);
918
+ const topicExtractor = new TopicExtractor();
919
+ this.topics = topicExtractor.extract(this.corpus);
920
+ if (this.classifier) {
921
+ this.classifier = this.createClassifier(topicExtractor.formatForPrompt(this.topics));
922
+ }
923
+ }
924
+ createClassifier(topicList) {
925
+ return new QueryClassifier({
926
+ model: this.config.classifierModel,
927
+ provider: this.config.classifierProvider,
928
+ confidenceThreshold: this.config.confidenceThreshold,
929
+ maxTier: this.config.maxTier,
930
+ topicList,
931
+ toolList: this.formatToolList(this.config.availableTools),
932
+ apiKey: this.getLlmApiKey(),
933
+ baseUrl: this.getLlmBaseUrl(),
934
+ });
935
+ }
936
+ async indexAdditionalCorpusChunks(chunks) {
937
+ if (chunks.length === 0 ||
938
+ !this.embeddingManager ||
939
+ !this.vectorStoreManager ||
940
+ this.embeddingStatus !== 'active') {
941
+ return;
942
+ }
943
+ try {
944
+ const { store, collectionName } = await this.vectorStoreManager.getStoreForDataSource(this.corpusDataSourceId);
945
+ const BATCH_SIZE = 50;
946
+ const allDocuments = [];
947
+ for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
948
+ const batch = chunks.slice(i, i + BATCH_SIZE);
949
+ const result = await this.embeddingManager.generateEmbeddings({
950
+ texts: batch.map((chunk) => chunk.content),
951
+ });
952
+ for (let j = 0; j < batch.length; j++) {
953
+ const embedding = result.embeddings[j];
954
+ if (!embedding || embedding.length === 0) {
955
+ continue;
956
+ }
957
+ batch[j].embedding = embedding;
958
+ allDocuments.push({
959
+ id: batch[j].id,
960
+ embedding,
961
+ textContent: batch[j].content,
962
+ metadata: {
963
+ heading: batch[j].heading,
964
+ sourcePath: batch[j].sourcePath,
965
+ },
966
+ });
967
+ }
968
+ }
969
+ if (allDocuments.length > 0) {
970
+ await store.upsert(collectionName, allDocuments);
971
+ }
972
+ }
973
+ catch (error) {
974
+ const message = error instanceof Error ? error.message : String(error);
975
+ console.warn(`[QueryRouter] Incremental GitHub corpus embedding failed; falling back to keyword search: ${message}`);
976
+ this.disableVectorRetrieval('failed-init');
977
+ }
978
+ }
979
+ disableVectorRetrieval(status) {
980
+ this.embeddingManager = null;
981
+ this.vectorStoreManager = null;
982
+ this.providerManager = null;
983
+ this.embeddingDimension = 0;
984
+ this.embeddingStatus = status;
985
+ }
986
+ getEmbeddingApiKey() {
987
+ return (this.config.embeddingApiKey ??
988
+ this.config.apiKey ??
989
+ process.env.OPENAI_API_KEY ??
990
+ process.env.OPENROUTER_API_KEY ??
991
+ '');
992
+ }
993
+ getEmbeddingBaseUrl() {
994
+ if (this.config.embeddingBaseUrl !== undefined) {
995
+ return this.config.embeddingBaseUrl;
996
+ }
997
+ if (this.config.embeddingApiKey !== undefined) {
998
+ return undefined;
999
+ }
1000
+ if (this.config.baseUrl !== undefined) {
1001
+ return this.config.baseUrl;
604
1002
  }
1003
+ if (this.config.apiKey !== undefined) {
1004
+ return undefined;
1005
+ }
1006
+ if (process.env.OPENAI_API_KEY) {
1007
+ return undefined;
1008
+ }
1009
+ if (process.env.OPENROUTER_API_KEY) {
1010
+ return 'https://openrouter.ai/api/v1';
1011
+ }
1012
+ return undefined;
1013
+ }
1014
+ getLlmApiKey() {
1015
+ return this.config.apiKey ?? process.env.OPENAI_API_KEY ?? process.env.OPENROUTER_API_KEY ?? '';
1016
+ }
1017
+ getLlmBaseUrl() {
1018
+ if (this.config.baseUrl !== undefined) {
1019
+ return this.config.baseUrl;
1020
+ }
1021
+ if (this.config.apiKey !== undefined) {
1022
+ return undefined;
1023
+ }
1024
+ if (process.env.OPENAI_API_KEY) {
1025
+ return undefined;
1026
+ }
1027
+ if (process.env.OPENROUTER_API_KEY) {
1028
+ return 'https://openrouter.ai/api/v1';
1029
+ }
1030
+ return undefined;
605
1031
  }
606
1032
  /**
607
1033
  * Return a known embedding dimension for common models.
@@ -620,9 +1046,93 @@ export class QueryRouter {
620
1046
  };
621
1047
  return KNOWN_DIMENSIONS[modelId] ?? 0;
622
1048
  }
1049
+ /**
1050
+ * Whether graph expansion is backed by a live implementation.
1051
+ *
1052
+ * Hosts should not treat `graphEnabled` as meaning GraphRAG is actually live.
1053
+ * `active` is reserved for a host-injected or future provider-backed graph
1054
+ * runtime rather than the built-in heuristic.
1055
+ */
1056
+ hasLiveGraphRuntime() {
1057
+ return typeof this.config.graphExpand === 'function';
1058
+ }
1059
+ /**
1060
+ * Whether graph expansion is backed by the built-in heuristic expansion.
1061
+ */
1062
+ hasHeuristicGraphRuntime() {
1063
+ return true;
1064
+ }
1065
+ /**
1066
+ * Whether reranking is backed by a live implementation.
1067
+ *
1068
+ * `active` is reserved for a host-injected or future provider-backed
1069
+ * reranker rather than the built-in lexical heuristic.
1070
+ */
1071
+ hasLiveRerankerRuntime() {
1072
+ return typeof this.config.rerank === 'function';
1073
+ }
1074
+ /**
1075
+ * Whether reranking is backed by the built-in lexical reranker.
1076
+ */
1077
+ hasHeuristicRerankerRuntime() {
1078
+ return true;
1079
+ }
1080
+ /**
1081
+ * Whether deep research is backed by a live implementation.
1082
+ *
1083
+ * `deepResearchEnabled` only means the branch may be attempted by config.
1084
+ * `active` is reserved for a host-injected or future provider-backed
1085
+ * research runtime rather than the built-in local-corpus heuristic.
1086
+ */
1087
+ hasLiveDeepResearchRuntime() {
1088
+ return typeof this.config.deepResearch === 'function';
1089
+ }
1090
+ /**
1091
+ * Whether deep research is backed by the built-in corpus-only heuristic.
1092
+ */
1093
+ hasHeuristicDeepResearchRuntime() {
1094
+ return true;
1095
+ }
623
1096
  // ==========================================================================
624
1097
  // PRIVATE — Retrieval callbacks (injected into QueryDispatcher)
625
1098
  // ==========================================================================
1099
+ /**
1100
+ * HyDE (Hypothetical Document Embeddings) search callback for the dispatcher.
1101
+ *
1102
+ * Generates a hypothetical answer to the query using the LLM, then searches
1103
+ * for documents similar to that hypothetical answer. Falls back to standard
1104
+ * vector search if no generation provider is available.
1105
+ *
1106
+ * @param query - The user's query string.
1107
+ * @param topK - Maximum number of chunks to return.
1108
+ * @returns Promise resolving to an array of matched chunks.
1109
+ */
1110
+ async hydeSearch(query, topK) {
1111
+ // HyDE falls back to standard vector search in the built-in implementation.
1112
+ // A host-injected HyDE retriever would generate a hypothetical document first.
1113
+ return this.vectorSearch(query, topK);
1114
+ }
1115
+ /**
1116
+ * Query decomposition callback for the dispatcher.
1117
+ *
1118
+ * Splits a complex multi-part query into independent sub-queries.
1119
+ * The built-in implementation uses simple sentence splitting as a heuristic.
1120
+ * A host-injected decomposer would use an LLM for semantic decomposition.
1121
+ *
1122
+ * @param query - The original multi-part user query.
1123
+ * @param maxSubQueries - Maximum number of sub-queries to generate.
1124
+ * @returns Array of decomposed sub-query strings.
1125
+ */
1126
+ async decomposeQuery(query, maxSubQueries) {
1127
+ // Built-in heuristic: split on sentence boundaries or question marks.
1128
+ const parts = query
1129
+ .split(/[?.!]\s+/)
1130
+ .map(s => s.trim())
1131
+ .filter(s => s.length > 0);
1132
+ if (parts.length <= 1)
1133
+ return [query];
1134
+ return parts.slice(0, maxSubQueries);
1135
+ }
626
1136
  /**
627
1137
  * Vector search callback for the dispatcher.
628
1138
  *
@@ -696,44 +1206,156 @@ export class QueryRouter {
696
1206
  /**
697
1207
  * Graph expansion callback for the dispatcher.
698
1208
  *
699
- * Placeholder returns empty array.
700
- * // Follow-up: wire GraphRAGEngine
1209
+ * Built-in heuristic graph expansion over the loaded corpus.
701
1210
  *
702
- * @param _seeds - Seed chunks to expand from (unused for now).
703
- * @returns Promise resolving to an empty array.
1211
+ * This is not yet a true GraphRAG engine. It expands from seed chunks by
1212
+ * preferring:
1213
+ * - chunks from the same source document
1214
+ * - heading overlap with seed headings/content
1215
+ * - content overlap with seed headings/content
1216
+ *
1217
+ * @param seeds - Seed chunks to expand from.
1218
+ * @returns Promise resolving to related chunks marked as `graph`.
704
1219
  */
705
- async graphExpand(_seeds) {
706
- // Follow-up: wire GraphRAGEngine
707
- return [];
1220
+ async graphExpand(seeds) {
1221
+ if (seeds.length === 0 || this.corpus.length === 0) {
1222
+ return [];
1223
+ }
1224
+ const seedIds = new Set(seeds.map((seed) => seed.id));
1225
+ const seedSourcePaths = new Set(seeds.map((seed) => seed.sourcePath));
1226
+ const seedTerms = new Set();
1227
+ for (const seed of seeds) {
1228
+ for (const term of this.tokenizeForRerank(`${seed.heading} ${seed.content}`)) {
1229
+ seedTerms.add(term);
1230
+ }
1231
+ }
1232
+ const scored = this.corpus
1233
+ .filter((chunk) => !seedIds.has(chunk.id))
1234
+ .map((chunk, index) => {
1235
+ const sameSourceBoost = seedSourcePaths.has(chunk.sourcePath) ? 0.48 : 0;
1236
+ const headingOverlap = this.computeTermOverlap(seedTerms, this.tokenizeForRerank(chunk.heading));
1237
+ const contentOverlap = this.computeTermOverlap(seedTerms, this.tokenizeForRerank(chunk.content));
1238
+ const score = sameSourceBoost + headingOverlap * 0.32 + contentOverlap * 0.2;
1239
+ return { chunk, index, score };
1240
+ })
1241
+ .filter((entry) => entry.score >= 0.18);
1242
+ scored.sort((left, right) => {
1243
+ if (right.score !== left.score) {
1244
+ return right.score - left.score;
1245
+ }
1246
+ return left.index - right.index;
1247
+ });
1248
+ return scored.slice(0, 8).map(({ chunk, score }) => ({
1249
+ id: chunk.id,
1250
+ heading: chunk.heading,
1251
+ content: chunk.content,
1252
+ sourcePath: chunk.sourcePath,
1253
+ relevanceScore: Math.min(0.99, score),
1254
+ matchType: 'graph',
1255
+ }));
708
1256
  }
709
1257
  /**
710
1258
  * Reranking callback for the dispatcher.
711
1259
  *
712
- * Placeholder returns the first topN chunks without actual reranking.
713
- * // Follow-up: wire RerankerService
1260
+ * Built-in heuristic reranker.
1261
+ *
1262
+ * This is not yet a cross-encoder. It reorders candidate chunks by combining:
1263
+ * - original retrieval score
1264
+ * - heading term overlap
1265
+ * - content term overlap
1266
+ * - exact phrase containment
1267
+ *
1268
+ * This gives tier-2 routing a real second-pass ranking step today without
1269
+ * pretending the deeper reranker service is already wired.
714
1270
  *
715
- * @param _query - The user's query (unused for now).
1271
+ * @param query - The user's query.
716
1272
  * @param chunks - Candidate chunks to rerank.
717
1273
  * @param topN - Maximum number of chunks to keep.
718
- * @returns Promise resolving to the first topN chunks.
1274
+ * @returns Promise resolving to the best-ranked chunks.
719
1275
  */
720
- async rerank(_query, chunks, topN) {
721
- // Follow-up: wire RerankerService
722
- return chunks.slice(0, topN);
1276
+ async rerank(query, chunks, topN) {
1277
+ if (chunks.length <= 1) {
1278
+ return chunks.slice(0, topN);
1279
+ }
1280
+ const queryTerms = this.tokenizeForRerank(query);
1281
+ const normalizedQuery = this.normalizeForRerank(query);
1282
+ const scored = chunks.map((chunk, index) => {
1283
+ const headingTerms = this.tokenizeForRerank(chunk.heading);
1284
+ const contentTerms = this.tokenizeForRerank(chunk.content);
1285
+ const headingOverlap = this.computeTermOverlap(queryTerms, headingTerms);
1286
+ const contentOverlap = this.computeTermOverlap(queryTerms, contentTerms);
1287
+ const normalizedText = this.normalizeForRerank(`${chunk.heading} ${chunk.content}`);
1288
+ const exactPhraseBoost = normalizedQuery.length >= 4 && normalizedText.includes(normalizedQuery) ? 0.2 : 0;
1289
+ const score = chunk.relevanceScore * 0.55 +
1290
+ headingOverlap * 0.25 +
1291
+ contentOverlap * 0.2 +
1292
+ exactPhraseBoost;
1293
+ return { chunk, index, score };
1294
+ });
1295
+ scored.sort((left, right) => {
1296
+ if (right.score !== left.score) {
1297
+ return right.score - left.score;
1298
+ }
1299
+ if (right.chunk.relevanceScore !== left.chunk.relevanceScore) {
1300
+ return right.chunk.relevanceScore - left.chunk.relevanceScore;
1301
+ }
1302
+ return left.index - right.index;
1303
+ });
1304
+ return scored.slice(0, topN).map((entry) => entry.chunk);
723
1305
  }
724
1306
  /**
725
1307
  * Deep research callback for the dispatcher.
726
1308
  *
727
- * Placeholder returns empty synthesis and empty sources.
728
- * // Follow-up: wire DeepResearchEngine
1309
+ * Built-in corpus-only research heuristic.
1310
+ *
1311
+ * This is not web-backed research. It runs a few local keyword-based passes
1312
+ * over the loaded corpus using slightly different query formulations, merges
1313
+ * the results, and returns a compact synthesis built from the top findings.
729
1314
  *
730
- * @param _query - The user's query (unused for now).
731
- * @param _sources - Source identifiers to consult (unused for now).
732
- * @returns Promise resolving to empty synthesis and sources.
1315
+ * @param query - The user's query.
1316
+ * @param sources - Optional source hints used to broaden local matching.
1317
+ * @returns Promise resolving to synthesized local-corpus findings.
733
1318
  */
734
- async deepResearch(_query, _sources) {
735
- // Follow-up: wire DeepResearchEngine
736
- return { synthesis: '', sources: [] };
1319
+ async deepResearch(query, sources) {
1320
+ if (!this.keywordFallback || this.corpus.length === 0) {
1321
+ return { synthesis: '', sources: [] };
1322
+ }
1323
+ const normalizedSourceHints = sources
1324
+ .map((source) => this.normalizeForRerank(source))
1325
+ .filter(Boolean);
1326
+ const queryTerms = [...this.tokenizeForRerank(query)];
1327
+ const narrowedTerms = queryTerms.slice(0, 4).join(' ');
1328
+ const researchQueries = [
1329
+ query,
1330
+ [query, normalizedSourceHints.join(' ')].filter(Boolean).join(' ').trim(),
1331
+ [query, 'architecture tradeoffs details'].join(' ').trim(),
1332
+ narrowedTerms,
1333
+ ].filter(Boolean);
1334
+ const merged = new Map();
1335
+ for (const researchQuery of researchQueries) {
1336
+ const hits = this.keywordFallback.search(researchQuery, 4);
1337
+ for (const hit of hits) {
1338
+ const existing = merged.get(hit.id);
1339
+ const researchChunk = {
1340
+ ...hit,
1341
+ relevanceScore: Math.min(0.99, hit.relevanceScore),
1342
+ matchType: 'research',
1343
+ };
1344
+ if (!existing || researchChunk.relevanceScore > existing.relevanceScore) {
1345
+ merged.set(researchChunk.id, researchChunk);
1346
+ }
1347
+ }
1348
+ }
1349
+ const researchChunks = [...merged.values()]
1350
+ .sort((left, right) => right.relevanceScore - left.relevanceScore)
1351
+ .slice(0, 5);
1352
+ if (researchChunks.length === 0) {
1353
+ return { synthesis: '', sources: [] };
1354
+ }
1355
+ const synthesis = researchChunks
1356
+ .map((chunk, index) => `${index + 1}. ${chunk.heading}: ${this.firstSentence(chunk.content)}`)
1357
+ .join('\n');
1358
+ return { synthesis, sources: researchChunks };
737
1359
  }
738
1360
  /**
739
1361
  * Format available tools for the classifier prompt.
@@ -741,6 +1363,72 @@ export class QueryRouter {
741
1363
  formatToolList(availableTools) {
742
1364
  return availableTools.length > 0 ? availableTools.join(', ') : '(none available)';
743
1365
  }
1366
+ /**
1367
+ * Normalize text for simple lexical reranking.
1368
+ */
1369
+ normalizeForRerank(text) {
1370
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
1371
+ }
1372
+ /**
1373
+ * Tokenize text for lexical reranking.
1374
+ */
1375
+ tokenizeForRerank(text) {
1376
+ const STOP_WORDS = new Set([
1377
+ 'a',
1378
+ 'an',
1379
+ 'and',
1380
+ 'are',
1381
+ 'as',
1382
+ 'at',
1383
+ 'be',
1384
+ 'by',
1385
+ 'for',
1386
+ 'from',
1387
+ 'how',
1388
+ 'in',
1389
+ 'is',
1390
+ 'it',
1391
+ 'of',
1392
+ 'on',
1393
+ 'or',
1394
+ 'that',
1395
+ 'the',
1396
+ 'this',
1397
+ 'to',
1398
+ 'what',
1399
+ 'when',
1400
+ 'where',
1401
+ 'which',
1402
+ 'why',
1403
+ 'with',
1404
+ ]);
1405
+ return new Set(this.normalizeForRerank(text)
1406
+ .split(/\s+/)
1407
+ .filter((term) => term.length >= 2 && !STOP_WORDS.has(term)));
1408
+ }
1409
+ /**
1410
+ * Compute overlap ratio between query terms and candidate terms.
1411
+ */
1412
+ computeTermOverlap(queryTerms, candidateTerms) {
1413
+ if (queryTerms.size === 0 || candidateTerms.size === 0) {
1414
+ return 0;
1415
+ }
1416
+ let matches = 0;
1417
+ for (const term of queryTerms) {
1418
+ if (candidateTerms.has(term)) {
1419
+ matches += 1;
1420
+ }
1421
+ }
1422
+ return matches / queryTerms.size;
1423
+ }
1424
+ /**
1425
+ * Extract a short first-sentence style summary from a chunk for synthesis.
1426
+ */
1427
+ firstSentence(text) {
1428
+ const normalized = text.replace(/\s+/g, ' ').trim();
1429
+ const sentence = normalized.match(/^(.{1,220}?[.!?])(?:\s|$)/);
1430
+ return sentence?.[1] ?? normalized.slice(0, 220);
1431
+ }
744
1432
  /**
745
1433
  * Derive fallback strategy names from classification + retrieval events.
746
1434
  */