@falai/agent 2.2.4 → 2.4.1

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 (390) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/MemoryAdapter.d.ts.map +1 -1
  3. package/dist/adapters/MemoryAdapter.js +10 -1
  4. package/dist/adapters/MemoryAdapter.js.map +1 -1
  5. package/dist/adapters/MongoAdapter.d.ts.map +1 -1
  6. package/dist/adapters/MongoAdapter.js +33 -2
  7. package/dist/adapters/MongoAdapter.js.map +1 -1
  8. package/dist/adapters/OpenSearchAdapter.d.ts.map +1 -1
  9. package/dist/adapters/OpenSearchAdapter.js +15 -1
  10. package/dist/adapters/OpenSearchAdapter.js.map +1 -1
  11. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  12. package/dist/adapters/PostgreSQLAdapter.js +35 -5
  13. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  14. package/dist/adapters/PrismaAdapter.d.ts.map +1 -1
  15. package/dist/adapters/PrismaAdapter.js +58 -15
  16. package/dist/adapters/PrismaAdapter.js.map +1 -1
  17. package/dist/adapters/RedisAdapter.d.ts.map +1 -1
  18. package/dist/adapters/RedisAdapter.js +10 -1
  19. package/dist/adapters/RedisAdapter.js.map +1 -1
  20. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  21. package/dist/adapters/SQLiteAdapter.js +38 -6
  22. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/MemoryAdapter.d.ts.map +1 -1
  24. package/dist/cjs/adapters/MemoryAdapter.js +10 -1
  25. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  26. package/dist/cjs/adapters/MongoAdapter.d.ts.map +1 -1
  27. package/dist/cjs/adapters/MongoAdapter.js +33 -2
  28. package/dist/cjs/adapters/MongoAdapter.js.map +1 -1
  29. package/dist/cjs/adapters/OpenSearchAdapter.d.ts.map +1 -1
  30. package/dist/cjs/adapters/OpenSearchAdapter.js +15 -1
  31. package/dist/cjs/adapters/OpenSearchAdapter.js.map +1 -1
  32. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  33. package/dist/cjs/adapters/PostgreSQLAdapter.js +35 -5
  34. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  35. package/dist/cjs/adapters/PrismaAdapter.d.ts.map +1 -1
  36. package/dist/cjs/adapters/PrismaAdapter.js +58 -15
  37. package/dist/cjs/adapters/PrismaAdapter.js.map +1 -1
  38. package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
  39. package/dist/cjs/adapters/RedisAdapter.js +10 -1
  40. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  41. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  42. package/dist/cjs/adapters/SQLiteAdapter.js +38 -6
  43. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  44. package/dist/cjs/core/Agent.d.ts +28 -18
  45. package/dist/cjs/core/Agent.d.ts.map +1 -1
  46. package/dist/cjs/core/Agent.js +58 -68
  47. package/dist/cjs/core/Agent.js.map +1 -1
  48. package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
  49. package/dist/cjs/core/AutoChainExecutor.js +14 -20
  50. package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
  51. package/dist/cjs/core/BranchEvaluator.d.ts +4 -3
  52. package/dist/cjs/core/BranchEvaluator.d.ts.map +1 -1
  53. package/dist/cjs/core/BranchEvaluator.js +18 -23
  54. package/dist/cjs/core/BranchEvaluator.js.map +1 -1
  55. package/dist/cjs/core/Flow.d.ts +2 -1
  56. package/dist/cjs/core/Flow.d.ts.map +1 -1
  57. package/dist/cjs/core/Flow.js +10 -3
  58. package/dist/cjs/core/Flow.js.map +1 -1
  59. package/dist/cjs/core/FlowRouter.d.ts +1 -0
  60. package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
  61. package/dist/cjs/core/FlowRouter.js +28 -5
  62. package/dist/cjs/core/FlowRouter.js.map +1 -1
  63. package/dist/cjs/core/PersistenceManager.d.ts +3 -0
  64. package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
  65. package/dist/cjs/core/PersistenceManager.js +57 -5
  66. package/dist/cjs/core/PersistenceManager.js.map +1 -1
  67. package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
  68. package/dist/cjs/core/PromptComposer.js +24 -10
  69. package/dist/cjs/core/PromptComposer.js.map +1 -1
  70. package/dist/cjs/core/ResponseGenerationError.d.ts +30 -0
  71. package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -0
  72. package/dist/cjs/core/ResponseGenerationError.js +37 -0
  73. package/dist/cjs/core/ResponseGenerationError.js.map +1 -0
  74. package/dist/cjs/core/ResponseModal.d.ts +43 -102
  75. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  76. package/dist/cjs/core/ResponseModal.js +178 -1194
  77. package/dist/cjs/core/ResponseModal.js.map +1 -1
  78. package/dist/cjs/core/ResponsePipeline.d.ts +58 -152
  79. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  80. package/dist/cjs/core/ResponsePipeline.js +408 -457
  81. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  82. package/dist/cjs/core/SessionFinalizer.d.ts +34 -0
  83. package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -0
  84. package/dist/cjs/core/SessionFinalizer.js +61 -0
  85. package/dist/cjs/core/SessionFinalizer.js.map +1 -0
  86. package/dist/cjs/core/SessionManager.d.ts +1 -1
  87. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  88. package/dist/cjs/core/SessionManager.js +18 -7
  89. package/dist/cjs/core/SessionManager.js.map +1 -1
  90. package/dist/cjs/core/SignalCoordinator.d.ts +103 -0
  91. package/dist/cjs/core/SignalCoordinator.d.ts.map +1 -0
  92. package/dist/cjs/core/SignalCoordinator.js +207 -0
  93. package/dist/cjs/core/SignalCoordinator.js.map +1 -0
  94. package/dist/cjs/core/SignalEvaluator.d.ts +2 -2
  95. package/dist/cjs/core/SignalEvaluator.d.ts.map +1 -1
  96. package/dist/cjs/core/SignalEvaluator.js +11 -26
  97. package/dist/cjs/core/SignalEvaluator.js.map +1 -1
  98. package/dist/cjs/core/Step.d.ts +3 -1
  99. package/dist/cjs/core/Step.d.ts.map +1 -1
  100. package/dist/cjs/core/Step.js +10 -3
  101. package/dist/cjs/core/Step.js.map +1 -1
  102. package/dist/cjs/core/StepLifecycle.d.ts +33 -0
  103. package/dist/cjs/core/StepLifecycle.d.ts.map +1 -0
  104. package/dist/cjs/core/StepLifecycle.js +97 -0
  105. package/dist/cjs/core/StepLifecycle.js.map +1 -0
  106. package/dist/cjs/core/ToolLoopExecutor.d.ts +104 -0
  107. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -0
  108. package/dist/cjs/core/ToolLoopExecutor.js +391 -0
  109. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -0
  110. package/dist/cjs/core/ToolManager.d.ts +1 -1
  111. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  112. package/dist/cjs/core/ToolManager.js.map +1 -1
  113. package/dist/cjs/index.d.ts +4 -5
  114. package/dist/cjs/index.d.ts.map +1 -1
  115. package/dist/cjs/index.js +7 -6
  116. package/dist/cjs/index.js.map +1 -1
  117. package/dist/cjs/providers/AnthropicProvider.d.ts +2 -0
  118. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  119. package/dist/cjs/providers/AnthropicProvider.js +28 -56
  120. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  121. package/dist/cjs/providers/DeepSeekProvider.d.ts +25 -21
  122. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
  123. package/dist/cjs/providers/DeepSeekProvider.js +48 -407
  124. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
  125. package/dist/cjs/providers/GeminiProvider.d.ts +2 -0
  126. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  127. package/dist/cjs/providers/GeminiProvider.js +27 -56
  128. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  129. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +129 -0
  130. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  131. package/dist/cjs/providers/OpenAICompatibleProvider.js +485 -0
  132. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -0
  133. package/dist/cjs/providers/OpenAIProvider.d.ts +9 -28
  134. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  135. package/dist/cjs/providers/OpenAIProvider.js +23 -417
  136. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  137. package/dist/cjs/providers/OpenRouterProvider.d.ts +10 -27
  138. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  139. package/dist/cjs/providers/OpenRouterProvider.js +28 -417
  140. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  141. package/dist/cjs/providers/errorClassification.d.ts +61 -0
  142. package/dist/cjs/providers/errorClassification.d.ts.map +1 -0
  143. package/dist/cjs/providers/errorClassification.js +123 -0
  144. package/dist/cjs/providers/errorClassification.js.map +1 -0
  145. package/dist/cjs/providers/index.d.ts +4 -0
  146. package/dist/cjs/providers/index.d.ts.map +1 -1
  147. package/dist/cjs/providers/index.js +7 -1
  148. package/dist/cjs/providers/index.js.map +1 -1
  149. package/dist/cjs/types/agent.d.ts +2 -1
  150. package/dist/cjs/types/agent.d.ts.map +1 -1
  151. package/dist/cjs/types/ai.d.ts +20 -0
  152. package/dist/cjs/types/ai.d.ts.map +1 -1
  153. package/dist/cjs/types/errors.d.ts +33 -0
  154. package/dist/cjs/types/errors.d.ts.map +1 -1
  155. package/dist/cjs/types/errors.js +37 -1
  156. package/dist/cjs/types/errors.js.map +1 -1
  157. package/dist/cjs/types/flow.d.ts +10 -5
  158. package/dist/cjs/types/flow.d.ts.map +1 -1
  159. package/dist/cjs/types/history.d.ts +1 -1
  160. package/dist/cjs/types/history.d.ts.map +1 -1
  161. package/dist/cjs/types/index.d.ts +5 -4
  162. package/dist/cjs/types/index.d.ts.map +1 -1
  163. package/dist/cjs/types/index.js +3 -1
  164. package/dist/cjs/types/index.js.map +1 -1
  165. package/dist/cjs/types/persistence.d.ts +43 -2
  166. package/dist/cjs/types/persistence.d.ts.map +1 -1
  167. package/dist/cjs/types/session.d.ts +8 -0
  168. package/dist/cjs/types/session.d.ts.map +1 -1
  169. package/dist/cjs/types/signals.d.ts +12 -2
  170. package/dist/cjs/types/signals.d.ts.map +1 -1
  171. package/dist/cjs/types/template.d.ts +3 -1
  172. package/dist/cjs/types/template.d.ts.map +1 -1
  173. package/dist/cjs/types/tool.d.ts +4 -4
  174. package/dist/cjs/types/tool.d.ts.map +1 -1
  175. package/dist/cjs/utils/condition.d.ts +19 -0
  176. package/dist/cjs/utils/condition.d.ts.map +1 -1
  177. package/dist/cjs/utils/condition.js +86 -15
  178. package/dist/cjs/utils/condition.js.map +1 -1
  179. package/dist/cjs/utils/session.d.ts +1 -0
  180. package/dist/cjs/utils/session.d.ts.map +1 -1
  181. package/dist/cjs/utils/session.js +1 -0
  182. package/dist/cjs/utils/session.js.map +1 -1
  183. package/dist/core/Agent.d.ts +28 -18
  184. package/dist/core/Agent.d.ts.map +1 -1
  185. package/dist/core/Agent.js +58 -68
  186. package/dist/core/Agent.js.map +1 -1
  187. package/dist/core/AutoChainExecutor.d.ts.map +1 -1
  188. package/dist/core/AutoChainExecutor.js +14 -20
  189. package/dist/core/AutoChainExecutor.js.map +1 -1
  190. package/dist/core/BranchEvaluator.d.ts +4 -3
  191. package/dist/core/BranchEvaluator.d.ts.map +1 -1
  192. package/dist/core/BranchEvaluator.js +18 -23
  193. package/dist/core/BranchEvaluator.js.map +1 -1
  194. package/dist/core/Flow.d.ts +2 -1
  195. package/dist/core/Flow.d.ts.map +1 -1
  196. package/dist/core/Flow.js +10 -3
  197. package/dist/core/Flow.js.map +1 -1
  198. package/dist/core/FlowRouter.d.ts +1 -0
  199. package/dist/core/FlowRouter.d.ts.map +1 -1
  200. package/dist/core/FlowRouter.js +28 -5
  201. package/dist/core/FlowRouter.js.map +1 -1
  202. package/dist/core/PersistenceManager.d.ts +3 -0
  203. package/dist/core/PersistenceManager.d.ts.map +1 -1
  204. package/dist/core/PersistenceManager.js +58 -6
  205. package/dist/core/PersistenceManager.js.map +1 -1
  206. package/dist/core/PromptComposer.d.ts.map +1 -1
  207. package/dist/core/PromptComposer.js +24 -10
  208. package/dist/core/PromptComposer.js.map +1 -1
  209. package/dist/core/ResponseGenerationError.d.ts +30 -0
  210. package/dist/core/ResponseGenerationError.d.ts.map +1 -0
  211. package/dist/core/ResponseGenerationError.js +33 -0
  212. package/dist/core/ResponseGenerationError.js.map +1 -0
  213. package/dist/core/ResponseModal.d.ts +43 -102
  214. package/dist/core/ResponseModal.d.ts.map +1 -1
  215. package/dist/core/ResponseModal.js +171 -1186
  216. package/dist/core/ResponseModal.js.map +1 -1
  217. package/dist/core/ResponsePipeline.d.ts +58 -152
  218. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  219. package/dist/core/ResponsePipeline.js +409 -458
  220. package/dist/core/ResponsePipeline.js.map +1 -1
  221. package/dist/core/SessionFinalizer.d.ts +34 -0
  222. package/dist/core/SessionFinalizer.d.ts.map +1 -0
  223. package/dist/core/SessionFinalizer.js +57 -0
  224. package/dist/core/SessionFinalizer.js.map +1 -0
  225. package/dist/core/SessionManager.d.ts +1 -1
  226. package/dist/core/SessionManager.d.ts.map +1 -1
  227. package/dist/core/SessionManager.js +18 -7
  228. package/dist/core/SessionManager.js.map +1 -1
  229. package/dist/core/SignalCoordinator.d.ts +103 -0
  230. package/dist/core/SignalCoordinator.d.ts.map +1 -0
  231. package/dist/core/SignalCoordinator.js +203 -0
  232. package/dist/core/SignalCoordinator.js.map +1 -0
  233. package/dist/core/SignalEvaluator.d.ts +2 -2
  234. package/dist/core/SignalEvaluator.d.ts.map +1 -1
  235. package/dist/core/SignalEvaluator.js +11 -26
  236. package/dist/core/SignalEvaluator.js.map +1 -1
  237. package/dist/core/Step.d.ts +3 -1
  238. package/dist/core/Step.d.ts.map +1 -1
  239. package/dist/core/Step.js +10 -3
  240. package/dist/core/Step.js.map +1 -1
  241. package/dist/core/StepLifecycle.d.ts +33 -0
  242. package/dist/core/StepLifecycle.d.ts.map +1 -0
  243. package/dist/core/StepLifecycle.js +93 -0
  244. package/dist/core/StepLifecycle.js.map +1 -0
  245. package/dist/core/ToolLoopExecutor.d.ts +104 -0
  246. package/dist/core/ToolLoopExecutor.d.ts.map +1 -0
  247. package/dist/core/ToolLoopExecutor.js +387 -0
  248. package/dist/core/ToolLoopExecutor.js.map +1 -0
  249. package/dist/core/ToolManager.d.ts +1 -1
  250. package/dist/core/ToolManager.d.ts.map +1 -1
  251. package/dist/core/ToolManager.js.map +1 -1
  252. package/dist/index.d.ts +4 -5
  253. package/dist/index.d.ts.map +1 -1
  254. package/dist/index.js +3 -3
  255. package/dist/index.js.map +1 -1
  256. package/dist/providers/AnthropicProvider.d.ts +2 -0
  257. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  258. package/dist/providers/AnthropicProvider.js +22 -50
  259. package/dist/providers/AnthropicProvider.js.map +1 -1
  260. package/dist/providers/DeepSeekProvider.d.ts +25 -21
  261. package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
  262. package/dist/providers/DeepSeekProvider.js +49 -408
  263. package/dist/providers/DeepSeekProvider.js.map +1 -1
  264. package/dist/providers/GeminiProvider.d.ts +2 -0
  265. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  266. package/dist/providers/GeminiProvider.js +21 -50
  267. package/dist/providers/GeminiProvider.js.map +1 -1
  268. package/dist/providers/OpenAICompatibleProvider.d.ts +129 -0
  269. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  270. package/dist/providers/OpenAICompatibleProvider.js +481 -0
  271. package/dist/providers/OpenAICompatibleProvider.js.map +1 -0
  272. package/dist/providers/OpenAIProvider.d.ts +9 -28
  273. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  274. package/dist/providers/OpenAIProvider.js +23 -417
  275. package/dist/providers/OpenAIProvider.js.map +1 -1
  276. package/dist/providers/OpenRouterProvider.d.ts +10 -27
  277. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  278. package/dist/providers/OpenRouterProvider.js +28 -417
  279. package/dist/providers/OpenRouterProvider.js.map +1 -1
  280. package/dist/providers/errorClassification.d.ts +61 -0
  281. package/dist/providers/errorClassification.d.ts.map +1 -0
  282. package/dist/providers/errorClassification.js +116 -0
  283. package/dist/providers/errorClassification.js.map +1 -0
  284. package/dist/providers/index.d.ts +4 -0
  285. package/dist/providers/index.d.ts.map +1 -1
  286. package/dist/providers/index.js +2 -0
  287. package/dist/providers/index.js.map +1 -1
  288. package/dist/types/agent.d.ts +2 -1
  289. package/dist/types/agent.d.ts.map +1 -1
  290. package/dist/types/ai.d.ts +20 -0
  291. package/dist/types/ai.d.ts.map +1 -1
  292. package/dist/types/errors.d.ts +33 -0
  293. package/dist/types/errors.d.ts.map +1 -1
  294. package/dist/types/errors.js +34 -0
  295. package/dist/types/errors.js.map +1 -1
  296. package/dist/types/flow.d.ts +10 -5
  297. package/dist/types/flow.d.ts.map +1 -1
  298. package/dist/types/history.d.ts +1 -1
  299. package/dist/types/history.d.ts.map +1 -1
  300. package/dist/types/index.d.ts +5 -4
  301. package/dist/types/index.d.ts.map +1 -1
  302. package/dist/types/index.js +1 -1
  303. package/dist/types/index.js.map +1 -1
  304. package/dist/types/persistence.d.ts +43 -2
  305. package/dist/types/persistence.d.ts.map +1 -1
  306. package/dist/types/session.d.ts +8 -0
  307. package/dist/types/session.d.ts.map +1 -1
  308. package/dist/types/signals.d.ts +12 -2
  309. package/dist/types/signals.d.ts.map +1 -1
  310. package/dist/types/template.d.ts +3 -1
  311. package/dist/types/template.d.ts.map +1 -1
  312. package/dist/types/tool.d.ts +4 -4
  313. package/dist/types/tool.d.ts.map +1 -1
  314. package/dist/utils/condition.d.ts +19 -0
  315. package/dist/utils/condition.d.ts.map +1 -1
  316. package/dist/utils/condition.js +84 -15
  317. package/dist/utils/condition.js.map +1 -1
  318. package/dist/utils/session.d.ts +1 -0
  319. package/dist/utils/session.d.ts.map +1 -1
  320. package/dist/utils/session.js +1 -0
  321. package/dist/utils/session.js.map +1 -1
  322. package/docs/README.md +1 -1
  323. package/docs/guides/branching.md +1 -1
  324. package/docs/guides/compaction.md +12 -5
  325. package/docs/guides/conditions.md +15 -2
  326. package/docs/guides/error-handling.md +59 -9
  327. package/docs/guides/instructions.md +2 -2
  328. package/docs/guides/persistence.md +69 -2
  329. package/docs/guides/streaming.md +2 -0
  330. package/docs/migration/README.md +5 -1
  331. package/docs/migration/v2-3-to-v2-4.md +316 -0
  332. package/docs/reference/adapters.md +55 -7
  333. package/docs/reference/branches.md +2 -2
  334. package/docs/reference/create-agent.md +3 -3
  335. package/docs/reference/errors.md +66 -1
  336. package/docs/reference/flow.md +1 -1
  337. package/docs/reference/instruction.md +4 -4
  338. package/docs/reference/providers.md +100 -3
  339. package/docs/reference/signals.md +11 -3
  340. package/docs/reference/step.md +2 -2
  341. package/docs/reference/tool.md +3 -1
  342. package/docs/start/05-go-to-production.md +3 -0
  343. package/package.json +1 -1
  344. package/src/adapters/MemoryAdapter.ts +15 -1
  345. package/src/adapters/MongoAdapter.ts +41 -2
  346. package/src/adapters/OpenSearchAdapter.ts +24 -1
  347. package/src/adapters/PostgreSQLAdapter.ts +45 -5
  348. package/src/adapters/PrismaAdapter.ts +82 -16
  349. package/src/adapters/RedisAdapter.ts +19 -1
  350. package/src/adapters/SQLiteAdapter.ts +47 -5
  351. package/src/core/Agent.ts +70 -85
  352. package/src/core/AutoChainExecutor.ts +27 -57
  353. package/src/core/BranchEvaluator.ts +24 -30
  354. package/src/core/Flow.ts +10 -3
  355. package/src/core/FlowRouter.ts +36 -4
  356. package/src/core/PersistenceManager.ts +79 -10
  357. package/src/core/PromptComposer.ts +25 -12
  358. package/src/core/ResponseGenerationError.ts +56 -0
  359. package/src/core/ResponseModal.ts +233 -1482
  360. package/src/core/ResponsePipeline.ts +492 -662
  361. package/src/core/SessionFinalizer.ts +78 -0
  362. package/src/core/SessionManager.ts +21 -9
  363. package/src/core/SignalCoordinator.ts +263 -0
  364. package/src/core/SignalEvaluator.ts +12 -29
  365. package/src/core/Step.ts +11 -3
  366. package/src/core/StepLifecycle.ts +139 -0
  367. package/src/core/ToolLoopExecutor.ts +492 -0
  368. package/src/core/ToolManager.ts +2 -1
  369. package/src/index.ts +7 -5
  370. package/src/providers/AnthropicProvider.ts +30 -72
  371. package/src/providers/DeepSeekProvider.ts +74 -586
  372. package/src/providers/GeminiProvider.ts +29 -70
  373. package/src/providers/OpenAICompatibleProvider.ts +738 -0
  374. package/src/providers/OpenAIProvider.ts +29 -602
  375. package/src/providers/OpenRouterProvider.ts +38 -596
  376. package/src/providers/errorClassification.ts +172 -0
  377. package/src/providers/index.ts +13 -0
  378. package/src/types/agent.ts +2 -1
  379. package/src/types/ai.ts +22 -0
  380. package/src/types/errors.ts +57 -0
  381. package/src/types/flow.ts +10 -5
  382. package/src/types/history.ts +1 -2
  383. package/src/types/index.ts +5 -1
  384. package/src/types/persistence.ts +50 -2
  385. package/src/types/session.ts +9 -0
  386. package/src/types/signals.ts +14 -2
  387. package/src/types/template.ts +3 -1
  388. package/src/types/tool.ts +10 -10
  389. package/src/utils/condition.ts +115 -18
  390. package/src/utils/session.ts +2 -0
@@ -6,31 +6,28 @@ import type {
6
6
  AgentOptions,
7
7
  Event,
8
8
  SessionState,
9
- AgentStructuredResponse,
10
- Tool,
11
9
  Directive,
10
+ StructuredSchema,
12
11
  } from "../types";
13
12
  import type { SignalFiring } from "../types/signals";
14
- import type { SignalProcessor } from "./SignalProcessor";
15
- import type { HistoryItem } from "../types/history";
16
13
  import {
17
14
  createSession,
18
15
  enterStep,
19
16
  mergeCollected,
20
- completeCurrentFlow,
21
17
  logger,
22
18
  historyToEvents,
23
- serializeToolResult,
19
+ eventsToHistory,
20
+ getLastMessageFromHistory,
24
21
  } from "../utils";
25
22
  import { enterFlow } from "../utils/session";
26
23
  import { createTemplateContext } from "../utils/template";
27
24
  import { Flow } from "./Flow";
28
25
  import { Step, FlowConfigurationError } from "../core/Step";
29
26
  import { FlowRouter } from "./FlowRouter";
30
- import type { ToolManager } from "../core/ToolManager";
31
27
  import { evaluateBranches, createAiConditionEvaluator } from "./BranchEvaluator";
32
28
  import { DirectiveChainTracker } from "./DirectiveChainTracker";
33
- import { DirectiveBus } from "./DirectiveBus";
29
+ import { ResponseGenerationError } from "./ResponseGenerationError";
30
+ import type { SignalCoordinator } from "./SignalCoordinator";
34
31
 
35
32
  /**
36
33
  * Position fields on a Directive that represent a navigation decision.
@@ -57,6 +54,8 @@ export function hasDirectivePositionField<TContext = unknown, TData = unknown>(
57
54
  export interface ResponsePreparationResult<TContext, TData = unknown> {
58
55
  effectiveContext: TContext;
59
56
  session: SessionState<TData>;
57
+ /** Context returned by the beforeRespond hook, for the caller to sync back to the agent. */
58
+ contextAfterHook?: TContext;
60
59
  }
61
60
 
62
61
  export interface RoutingResult<TContext, TData = unknown> {
@@ -68,20 +67,6 @@ export interface RoutingResult<TContext, TData = unknown> {
68
67
  completedFlows?: Flow<TContext, TData>[];
69
68
  }
70
69
 
71
- export interface ToolExecutionResult<TData = unknown> {
72
- session: SessionState<TData>;
73
- toolCalls:
74
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
75
- | undefined;
76
- /** Map of tool name to serialized result data for inclusion in conversation history */
77
- toolResults?: Map<string, string>;
78
- }
79
-
80
- export interface DataCollectionResult<TData = unknown> {
81
- session: SessionState<TData>;
82
- collectedData?: Partial<TData>;
83
- }
84
-
85
70
  /**
86
71
  * Shared response processing logic between respond() and respondStream() methods
87
72
  */
@@ -96,20 +81,10 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
96
81
  constructor(
97
82
  private readonly options: AgentOptions<TContext, TData>,
98
83
  private readonly getFlows: () => Flow<TContext, TData>[],
99
- private readonly tools: Tool<TContext, TData>[],
100
84
  private readonly flowRouter: FlowRouter<TContext, TData>,
101
- private readonly updateContext: (
102
- updates: Partial<TContext>
103
- ) => Promise<void>,
104
- private readonly updateData: (
105
- session: SessionState<TData>,
106
- dataUpdate: Partial<TData>
107
- ) => Promise<SessionState<TData>>,
108
- private readonly updateCollectedData?: (
109
- updates: Partial<TData>
110
- ) => Promise<void>,
111
- private readonly toolManager?: ToolManager<TContext, TData>,
112
- private readonly signalProcessor?: SignalProcessor<TContext, TData>
85
+ private readonly signalCoordinator: SignalCoordinator<TContext, TData>,
86
+ private readonly updateCollectedData: (updates: Partial<TData>) => Promise<void>,
87
+ private readonly getSchema: () => StructuredSchema | undefined
113
88
  ) { }
114
89
 
115
90
  /**
@@ -132,101 +107,27 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
132
107
  return this._chainTracker;
133
108
  }
134
109
 
135
- /**
136
- * Create a fresh DirectiveBus for a new turn.
137
- * The bus collects directives from hooks, tools, and branches during the turn
138
- * and merges them at phase boundaries via Algorithm 4.
139
- */
140
- createDirectiveBus(): DirectiveBus<TContext, TData> {
141
- return new DirectiveBus<TContext, TData>();
142
- }
143
-
144
- // ──────────────────────────────────────────────────────────────────────────
145
- // Signal pipeline phases
146
- // ──────────────────────────────────────────────────────────────────────────
147
-
148
- /**
149
- * PRE-SIGNAL PHASE — Evaluates pre/both signals in parallel with routing.
150
- *
151
- * Delegates to `signalProcessor.runPreSignalPhase(...)` when configured.
152
- * When no signal processor is present (zero-cost path), returns a stable
153
- * empty shape so callers don't need to branch.
154
- *
155
- * @requirements 2.1, 2.3, 8.6, 13.3
156
- */
157
- async runPreSignalPhase(
158
- session: SessionState<TData>,
159
- context: TContext,
160
- history: Event[],
161
- ): Promise<{
162
- firings: SignalFiring<TContext, TData>[];
163
- updatedSession: SessionState<TData>;
164
- mergedDirective: Directive<TContext, TData> | undefined;
165
- }> {
166
- if (!this.signalProcessor) {
167
- return { firings: [], updatedSession: session, mergedDirective: undefined };
168
- }
169
- const result = await this.signalProcessor.runPreSignalPhase({ session, history, context });
170
- return {
171
- firings: result.firings,
172
- updatedSession: result.updatedSession,
173
- mergedDirective: result.mergedDirective,
174
- };
175
- }
176
-
177
- /**
178
- * POST-SIGNAL PHASE — Evaluates post/both signals after finalize/onComplete.
179
- *
180
- * Delegates to `signalProcessor.runPostSignalPhase(...)` when configured.
181
- * When no signal processor is present, returns a stable empty shape.
182
- *
183
- * Post-phase signals see the complete turn result: assistant message in history,
184
- * collected data, tool results. Position directives from this phase set
185
- * `session.pendingDirective` (no mid-turn re-entry per D6 decision).
186
- *
187
- * Pre-LLM-only fields (`appendPrompt`, `injectTools`, `halt`) are already
188
- * dropped inside `runPostSignalPhase` per Phase 4.5 — this seam does NOT
189
- * re-introduce them.
190
- *
191
- * @requirements 9.1, 9.2, 9.3, 9.4
192
- */
193
- async runPostSignalPhase(
194
- session: SessionState<TData>,
195
- context: TContext,
196
- history: Event[],
197
- ): Promise<{
198
- firings: SignalFiring<TContext, TData>[];
199
- updatedSession: SessionState<TData>;
200
- mergedDirective: Directive<TContext, TData> | undefined;
201
- }> {
202
- if (!this.signalProcessor) {
203
- return { firings: [], updatedSession: session, mergedDirective: undefined };
204
- }
205
- const result = await this.signalProcessor.runPostSignalPhase({ session, history, context });
206
- return {
207
- firings: result.firings,
208
- updatedSession: result.updatedSession,
209
- mergedDirective: result.mergedDirective,
210
- };
211
- }
212
-
213
110
  /**
214
111
  * Prepare context and session for response generation
215
112
  */
216
113
  async prepareResponseContext(params: {
217
114
  contextOverride?: Partial<TContext>;
218
115
  session?: SessionState<TData>;
116
+ /** The agent's current context, resolved by the caller (contextProvider already applied). */
117
+ currentContext?: TContext;
118
+ /** The agent's live session, used when no explicit session is passed. */
119
+ currentSession?: SessionState<TData>;
219
120
  }): Promise<ResponsePreparationResult<TContext, TData>> {
220
121
  const { contextOverride, session } = params;
221
122
 
222
- // Get current context (may fetch from provider)
223
- let currentContext = await this.getContext();
123
+ let currentContext = params.currentContext;
124
+ let contextAfterHook: TContext | undefined;
224
125
 
225
126
  // Call beforeRespond hook if configured
226
127
  if (this.options.hooks?.beforeRespond && currentContext !== undefined) {
227
128
  currentContext = await this.options.hooks.beforeRespond(currentContext);
228
- // Update stored context with the result from beforeRespond
229
- this.context = currentContext;
129
+ // Surface the hook result so the caller can sync it back to the agent
130
+ contextAfterHook = currentContext;
230
131
  }
231
132
 
232
133
  // Merge context with override
@@ -235,12 +136,13 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
235
136
  ...(contextOverride as Record<string, unknown>),
236
137
  } as TContext;
237
138
 
238
- // Initialize or get session (use current session if available)
239
- const targetSession = session || this.currentSession || createSession<TData>();
139
+ // Initialize or get session (use the live session if available)
140
+ const targetSession = session || params.currentSession || createSession<TData>();
240
141
 
241
142
  return {
242
143
  effectiveContext,
243
144
  session: targetSession,
145
+ contextAfterHook,
244
146
  };
245
147
  }
246
148
 
@@ -270,7 +172,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
270
172
  if (targetSession.pendingDirective) {
271
173
  const directive = targetSession.pendingDirective;
272
174
  logger.debug(
273
- `[ResponseHandler] Applying pending directive at start of turn`
175
+ `[ResponsePipeline] Applying pending directive at start of turn`
274
176
  );
275
177
 
276
178
  // Track directive chain depth (Requirement 22.1)
@@ -306,7 +208,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
306
208
 
307
209
  if (targetFlow) {
308
210
  logger.debug(
309
- `[ResponseHandler] Pending directive goTo → flow: ${targetFlow.title}`
211
+ `[ResponsePipeline] Pending directive goTo → flow: ${targetFlow.title}`
310
212
  );
311
213
  targetSession = enterFlow(
312
214
  targetSession,
@@ -436,7 +338,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
436
338
  // Log if flow is complete
437
339
  if (isFlowComplete) {
438
340
  logger.debug(
439
- `[ResponseHandler] Flow complete: all required data collected or last step reached`
341
+ `[ResponsePipeline] Flow complete: all required data collected or last step reached`
440
342
  );
441
343
  }
442
344
  }
@@ -459,10 +361,12 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
459
361
  selectedStep: Step<TContext, TData> | undefined;
460
362
  session: SessionState<TData>;
461
363
  isFlowComplete: boolean;
364
+ /** The turn's effective context, passed explicitly (no stored pipeline state). */
365
+ context: TContext;
462
366
  /** Merged directive from the directive bus (pre-LLM + post-LLM phases). */
463
367
  busDirective?: Directive<TContext, TData>;
464
368
  }): Promise<{ nextStep: Step<TContext, TData> | undefined; session: SessionState<TData>; flowChanged?: Flow<TContext, TData> }> {
465
- const { selectedFlow, selectedStep, session, isFlowComplete, busDirective } = params;
369
+ const { selectedFlow, selectedStep, session, isFlowComplete, context, busDirective } = params;
466
370
 
467
371
  if (!selectedFlow) {
468
372
  return { nextStep: undefined, session };
@@ -475,7 +379,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
475
379
  // branches or linear/AI selection — the caller applies the bus directive.
476
380
  if (hasDirectivePositionField(busDirective)) {
477
381
  logger.debug(
478
- `[ResponseHandler] Directive bus winner has position field — skipping branch evaluation and linear/AI selection`,
382
+ `[ResponsePipeline] Directive bus winner has position field — skipping branch evaluation and linear/AI selection`,
479
383
  );
480
384
  return { nextStep: undefined, session };
481
385
  }
@@ -489,9 +393,8 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
489
393
  : undefined;
490
394
 
491
395
  if (currentStep?.branches && currentStep.branches.length > 0) {
492
- const contextToUse = this.getStoredContext();
493
396
  const branchResult = await this.evaluateStepBranches(
494
- currentStep, selectedFlow, session, contextToUse as TContext
397
+ currentStep, selectedFlow, session, context
495
398
  );
496
399
  if (branchResult) {
497
400
  return branchResult;
@@ -515,19 +418,17 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
515
418
  ? selectedFlow.getStep(session.currentStep.id)
516
419
  : undefined;
517
420
 
518
- const contextToUse = this.getStoredContext();
519
-
520
421
  // Get candidate steps based on current position in the flow
521
422
  const candidates = await this.flowRouter.getCandidateStepsWithConditions(
522
423
  selectedFlow,
523
424
  currentStep, // Pass current step instead of undefined to maintain progression
524
- createTemplateContext({ data: session.data, session, context: contextToUse })
425
+ createTemplateContext({ data: session.data, session, context })
525
426
  );
526
427
 
527
428
  if (candidates.length > 0) {
528
429
  nextStep = candidates[0].step;
529
430
  logger.debug(
530
- `[ResponseHandler] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`
431
+ `[ResponsePipeline] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`
531
432
  );
532
433
  } else {
533
434
  // Fallback to initial step even if it should be skipped
@@ -546,7 +447,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
546
447
  );
547
448
  if (missingRequires.length > 0) {
548
449
  logger.debug(
549
- `[ResponseHandler] Cannot enter step "${nextStep.id}": missing required fields [${missingRequires.join(', ')}]. Staying at current step.`
450
+ `[ResponsePipeline] Cannot enter step "${nextStep.id}": missing required fields [${missingRequires.join(', ')}]. Staying at current step.`
550
451
  );
551
452
  // Stay at current step - don't enter the next one
552
453
  const currentStepId = session.currentStep?.id;
@@ -566,7 +467,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
566
467
  nextStep.id,
567
468
  nextStep.description
568
469
  );
569
- logger.debug(`[ResponseHandler] Entered step: ${nextStep.id}`);
470
+ logger.debug(`[ResponsePipeline] Entered step: ${nextStep.id}`);
570
471
 
571
472
  return { nextStep, session: updatedSession };
572
473
  }
@@ -626,7 +527,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
626
527
  const localStep = selectedFlow.getStep(result);
627
528
  if (localStep) {
628
529
  const updatedSession = enterStep(session, localStep.id, localStep.description);
629
- logger.debug(`[ResponseHandler] Branch resolved to local step: ${localStep.id}`);
530
+ logger.debug(`[ResponsePipeline] Branch resolved to local step: ${localStep.id}`);
630
531
  return { nextStep: localStep, session: updatedSession };
631
532
  }
632
533
 
@@ -635,7 +536,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
635
536
  const targetFlow = flows.find(f => f.id === result || f.title === result);
636
537
  if (targetFlow) {
637
538
  // Treat as applyDirective({ goTo: result }) — enter the target flow
638
- logger.debug(`[ResponseHandler] Branch resolved to flow: ${targetFlow.title}`);
539
+ logger.debug(`[ResponsePipeline] Branch resolved to flow: ${targetFlow.title}`);
639
540
  const updatedSession = enterFlow(session, targetFlow.id, targetFlow.title);
640
541
  return { nextStep: undefined, session: updatedSession, flowChanged: targetFlow };
641
542
  }
@@ -685,7 +586,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
685
586
  updatedSession = enterStep(updatedSession, stepTarget);
686
587
  // Try to resolve the target step instance for the caller
687
588
  const targetStepInstance = targetFlow.getStep(stepTarget);
688
- logger.debug(`[ResponseHandler] Branch directive goToStep → ${flowTarget}.${stepTarget}`);
589
+ logger.debug(`[ResponsePipeline] Branch directive goToStep → ${flowTarget}.${stepTarget}`);
689
590
  return { nextStep: targetStepInstance || undefined, session: updatedSession, flowChanged: targetFlow };
690
591
  }
691
592
  throw new FlowConfigurationError(
@@ -698,7 +599,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
698
599
  const targetStep = selectedFlow.getStep(stepTarget);
699
600
  if (targetStep) {
700
601
  updatedSession = enterStep(updatedSession, targetStep.id, targetStep.description);
701
- logger.debug(`[ResponseHandler] Branch directive goToStep → ${targetStep.id}`);
602
+ logger.debug(`[ResponsePipeline] Branch directive goToStep → ${targetStep.id}`);
702
603
  return { nextStep: targetStep, session: updatedSession };
703
604
  }
704
605
  throw new FlowConfigurationError(
@@ -723,7 +624,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
723
624
  updatedSession = enterStep(updatedSession, directive.goTo.step);
724
625
  }
725
626
 
726
- logger.debug(`[ResponseHandler] Branch directive goTo → ${targetFlow.title}`);
627
+ logger.debug(`[ResponsePipeline] Branch directive goTo → ${targetFlow.title}`);
727
628
  return { nextStep: undefined, session: updatedSession, flowChanged: targetFlow };
728
629
  }
729
630
  throw new FlowConfigurationError(
@@ -734,12 +635,12 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
734
635
  }
735
636
 
736
637
  if (directive.complete) {
737
- logger.debug(`[ResponseHandler] Branch directive complete`);
638
+ logger.debug(`[ResponsePipeline] Branch directive complete`);
738
639
  return { nextStep: undefined, session: updatedSession };
739
640
  }
740
641
 
741
642
  if (directive.abort) {
742
- logger.debug(`[ResponseHandler] Branch directive abort`);
643
+ logger.debug(`[ResponsePipeline] Branch directive abort`);
743
644
  return { nextStep: undefined, session: updatedSession };
744
645
  }
745
646
 
@@ -757,7 +658,7 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
757
658
  // Reset to initial step
758
659
  const initialStep = selectedFlow.initialStep;
759
660
  updatedSession = enterStep(updatedSession, initialStep.id, initialStep.description);
760
- logger.debug(`[ResponseHandler] Branch directive reset → ${initialStep.id}`);
661
+ logger.debug(`[ResponsePipeline] Branch directive reset → ${initialStep.id}`);
761
662
  return { nextStep: initialStep, session: updatedSession };
762
663
  }
763
664
 
@@ -766,612 +667,541 @@ export class ResponsePipeline<TContext = unknown, TData = unknown> {
766
667
  return { nextStep: undefined, session: updatedSession };
767
668
  }
768
669
 
670
+
769
671
  /**
770
- * Execute tool calls and handle results
672
+ * TURN ROUTING + STEP SELECTION — the single entry point for deciding which
673
+ * flow and step a turn renders. Runs the routing-skip optimization, the
674
+ * pre-signal phase (parallel with routing when a processor is configured),
675
+ * pre-extraction, and next-step determination.
771
676
  */
772
- async executeToolCalls(params: {
773
- toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
774
- selectedFlow?: Flow<TContext, TData>;
677
+ async routeAndSelectStep(params: {
678
+ session: SessionState<TData>;
679
+ history: Event[]; // Use Event[] for internal processing
775
680
  context: TContext;
681
+ signal?: AbortSignal;
682
+ }): Promise<{
683
+ selectedFlow?: Flow<TContext, TData>;
684
+ selectedStep?: Step<TContext, TData>;
685
+ responseDirectives?: string[];
776
686
  session: SessionState<TData>;
777
- history: Event[];
778
- isStreaming?: boolean;
779
- }): Promise<ToolExecutionResult<TData>> {
780
- const {
781
- toolCalls,
782
- selectedFlow,
783
- context,
784
- session,
785
- history,
786
- isStreaming = false,
787
- } = params;
788
-
789
- if (toolCalls.length === 0) {
790
- return { session, toolCalls: undefined };
791
- }
792
-
793
- logger.debug(
794
- `[ResponseHandler] Executing ${toolCalls.length} ${isStreaming ? "streaming " : ""
795
- }tool calls`
796
- );
797
-
798
- let updatedSession = session;
799
- const executedToolCalls: Array<{
800
- toolName: string;
801
- arguments: Record<string, unknown>;
802
- }> = [];
803
- const toolResults = new Map<string, string>();
804
-
805
- for (const toolCall of toolCalls) {
806
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
807
- if (!tool) {
808
- logger.warn(`[ToolExecutionError] Tool not found: "${toolCall.toolName}" is not registered in any scope (transient, step, flow, or agent). Skipping this tool call. Register the tool or check the tool name.`);
809
- continue;
810
- }
811
-
812
- // Use ToolManager for unified tool execution
813
- let result;
814
- if (this.toolManager) {
815
- result = await this.toolManager.executeTool({
816
- tool,
817
- context,
818
- updateContext: this.updateContext,
819
- updateData: this.updateCollectedData || (async () => { }),
820
- history,
821
- data: updatedSession.data,
822
- });
823
- } else {
824
- // Fallback: execute tool directly if ToolManager not available
825
- throw new Error(`ToolManager not available for tool execution: ${toolCall.toolName}`);
826
- }
827
-
828
- executedToolCalls.push(toolCall);
829
-
830
- // Store the actual tool result data for history
831
- toolResults.set(toolCall.toolName, serializeToolResult(result));
832
-
833
- // Update context with tool results
834
- if (result.contextUpdate) {
835
- await this.updateContext(result.contextUpdate as Partial<TContext>);
836
- }
837
-
838
- // Update collected data with tool results
839
- if (result.dataUpdate) {
840
- updatedSession = await this.updateData(
841
- updatedSession,
842
- result.dataUpdate as Partial<TData>
843
- );
844
- logger.debug(
845
- `[ResponseHandler] ${isStreaming ? "Streaming " : ""
846
- }Tool updated collected data:`,
847
- result.dataUpdate
848
- );
687
+ isFlowComplete: boolean;
688
+ /** Signal firings from the pre-phase (threaded through for response surface). */
689
+ signalFirings?: SignalFiring<TContext, TData>[];
690
+ /** Non-position signal directive for pre-LLM augmentation (appendPrompt, injectTools, etc). */
691
+ signalPreDirective?: Directive<TContext, TData>;
692
+ /** Pre-signal phase halted the turn. */
693
+ signalHalted?: boolean;
694
+ /** Reply text from the halt directive. */
695
+ signalHaltReply?: string;
696
+ }> {
697
+ try {
698
+ // Create a fresh chain tracker for this turn (Requirement 22.1)
699
+ this.createChainTracker();
700
+
701
+ // ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
702
+ // When the current step has collect fields AND pre-extraction populates at least
703
+ // one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
704
+ const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
705
+ if (routingSkipResult) {
706
+ // Even when routing is skipped, run pre-signal phase if processor is present
707
+ if (this.signalCoordinator.enabled) {
708
+ const signalResult = await this.signalCoordinator.runPrePhase(
709
+ params.session, params.context, params.history,
710
+ );
711
+ // If signal halts, override the routing skip result
712
+ if (signalResult.mergedDirective?.halt) {
713
+ return {
714
+ ...routingSkipResult,
715
+ session: signalResult.updatedSession,
716
+ signalFirings: signalResult.firings,
717
+ signalHalted: true,
718
+ signalHaltReply: signalResult.mergedDirective.reply,
719
+ };
720
+ }
721
+ // If signal has position fields, override routing skip result
722
+ if (hasDirectivePositionField(signalResult.mergedDirective)) {
723
+ return this.signalCoordinator.applyPositionDirective(signalResult);
724
+ }
725
+ // Non-position directive: propagate for pre-LLM augmentation
726
+ return {
727
+ ...routingSkipResult,
728
+ session: signalResult.updatedSession,
729
+ signalFirings: signalResult.firings,
730
+ signalPreDirective: signalResult.mergedDirective || undefined,
731
+ };
732
+ }
733
+ return routingSkipResult;
849
734
  }
850
735
 
851
- logger.debug(
852
- `[ResponseHandler] Executed ${isStreaming ? "streaming " : ""}tool: ${String(tool.id || tool.id || 'unknown')
853
- } (success: ${result.success})`
854
- );
855
- }
736
+ // ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
737
+ // When signalProcessor is present, run pre-signals in parallel with routing.
738
+ // When absent, call the router directly (zero overhead, preserve current behavior).
739
+ if (this.signalCoordinator.enabled) {
740
+ // Run pre-signal phase in parallel with routing (Requirement 8.1)
741
+ const [signalResult, routingResult] = await Promise.all([
742
+ this.signalCoordinator.runPrePhase(
743
+ params.session, params.context, params.history,
744
+ ),
745
+ this.handleRoutingAndStepSelection({
746
+ session: params.session,
747
+ history: params.history,
748
+ context: params.context,
749
+ signal: params.signal,
750
+ }),
751
+ ]);
752
+
753
+ // ── Requirement 8.2: halt → discard routing, skip LLM ────────────
754
+ if (signalResult.mergedDirective?.halt) {
755
+ return {
756
+ selectedFlow: undefined,
757
+ selectedStep: undefined,
758
+ session: signalResult.updatedSession,
759
+ isFlowComplete: false,
760
+ signalFirings: signalResult.firings,
761
+ signalHalted: true,
762
+ signalHaltReply: signalResult.mergedDirective.reply,
763
+ };
764
+ }
856
765
 
857
- return {
858
- session: updatedSession,
859
- toolCalls: executedToolCalls.length > 0 ? executedToolCalls : undefined,
860
- toolResults,
861
- };
862
- }
766
+ // ── Requirement 8.3: position directive → discard routing, apply signal position ──
767
+ if (hasDirectivePositionField(signalResult.mergedDirective)) {
768
+ return this.signalCoordinator.applyPositionDirective(signalResult);
769
+ }
863
770
 
864
- /**
865
- * Execute tool loop for follow-up tool calls
866
- */
867
- async executeToolLoop(params: {
868
- initialToolCalls:
869
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
870
- | undefined;
871
- selectedFlow?: Flow<TContext, TData>;
872
- nextStep: Step<TContext, TData>;
873
- responsePrompt: string;
874
- history: HistoryItem[];
875
- context: TContext;
876
- session: SessionState<TData>;
877
- responseSchema: Record<string, unknown>;
878
- isStreaming?: boolean;
879
- }): Promise<ToolExecutionResult<TData>> {
880
- const {
881
- initialToolCalls,
882
- selectedFlow,
883
- nextStep,
884
- responsePrompt,
885
- history,
886
- context,
887
- session,
888
- responseSchema,
889
- isStreaming = false,
890
- } = params;
891
-
892
- const MAX_TOOL_LOOPS = 5;
893
- let toolLoopCount = 0;
894
- let currentToolCalls = initialToolCalls;
895
- let hasToolCalls = currentToolCalls && currentToolCalls.length > 0;
896
- let currentSession = session;
897
- // Track tool results across loop iterations for history
898
- let currentToolResults = new Map<string, string>();
899
-
900
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
901
- toolLoopCount++;
902
- logger.debug(
903
- `[ResponseHandler] Starting ${isStreaming ? "streaming " : ""
904
- }tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`
905
- );
771
+ // ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
772
+ // ── Requirement 8.5: no directive → use routing as-is ─────────────
773
+ let updatedSession = signalResult.updatedSession;
906
774
 
907
- // Add tool execution results to history so AI knows what happened
908
- const toolResultItems: HistoryItem[] = [];
909
- for (const toolCall of currentToolCalls || []) {
910
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
911
- if (tool) {
912
- toolResultItems.push({
913
- role: "assistant" as const,
914
- content: null,
915
- tool_calls: [{
916
- id: toolCall.toolName,
917
- name: toolCall.toolName,
918
- arguments: toolCall.arguments,
919
- }],
920
- });
921
- toolResultItems.push({
922
- role: "tool" as const,
923
- tool_call_id: toolCall.toolName,
924
- name: toolCall.toolName,
925
- content: currentToolResults.get(toolCall.toolName) || "Tool executed successfully",
926
- });
775
+ // Apply data/context updates from signal to the routed session
776
+ if (signalResult.mergedDirective?.dataUpdate) {
777
+ updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
927
778
  }
928
- }
929
-
930
- // Create updated history with tool results
931
- const updatedHistory = [...history, ...toolResultItems];
932
779
 
933
- // Make follow-up AI call to see if more tools are needed
934
- const followUpResult = await this.options.provider.generateMessage({
935
- prompt: responsePrompt,
936
- history: updatedHistory,
937
- context,
938
- tools: this.collectAvailableTools(selectedFlow, nextStep),
939
- parameters: {
940
- jsonSchema: responseSchema,
941
- schemaName: isStreaming ? "tool_followup_streaming" : "tool_followup",
942
- },
943
- });
780
+ // Use routing result for flow/step, but carry signal session state
781
+ // Merge routing session changes on top of signal session
782
+ const routingSession = routingResult.session;
783
+ updatedSession = {
784
+ ...updatedSession,
785
+ currentFlow: routingSession.currentFlow,
786
+ currentStep: routingSession.currentStep,
787
+ flowHistory: routingSession.flowHistory,
788
+ pendingDirective: routingSession.pendingDirective,
789
+ };
944
790
 
945
- // Check if follow-up call has more tool calls
946
- const followUpToolCalls = followUpResult.structured?.toolCalls;
947
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
791
+ const isFlowComplete = routingResult.isFlowComplete;
948
792
 
949
- if (hasToolCalls) {
950
- logger.debug(
951
- `[ResponseHandler] Follow-up call produced ${followUpToolCalls!.length
952
- } additional tool calls`
953
- );
793
+ // PRE-EXTRACTION: same logic as below — extract data from user message
794
+ if (routingResult.selectedFlow && !isFlowComplete) {
795
+ if (this.shouldPreExtractData(routingResult.selectedFlow)) {
796
+ logger.debug(
797
+ `[ResponsePipeline] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
798
+ );
799
+ const extractedData = await this.preExtractFlowData({
800
+ route: routingResult.selectedFlow,
801
+ history: params.history,
802
+ context: params.context,
803
+ session: updatedSession,
804
+ signal: params.signal,
805
+ });
806
+ if (extractedData && Object.keys(extractedData).length > 0) {
807
+ logger.debug(`[ResponsePipeline] Pre-extracted data:`, extractedData);
808
+ updatedSession = mergeCollected(updatedSession, extractedData);
809
+ await this.updateCollectedData(extractedData);
810
+ }
811
+ }
812
+ }
954
813
 
955
- // Execute the follow-up tool calls
956
- const toolResult = await this.executeToolCalls({
957
- toolCalls: followUpToolCalls!,
958
- selectedFlow,
959
- context,
960
- session: currentSession,
961
- history: historyToEvents(updatedHistory),
962
- isStreaming,
814
+ // Determine next step
815
+ const stepResult = await this.determineNextStep({
816
+ selectedFlow: routingResult.selectedFlow,
817
+ selectedStep: routingResult.selectedStep,
818
+ session: updatedSession,
819
+ isFlowComplete,
820
+ context: params.context,
963
821
  });
964
822
 
965
- currentSession = toolResult.session;
966
- currentToolCalls = followUpToolCalls;
967
- currentToolResults = toolResult.toolResults || new Map();
968
- } else {
969
- logger.debug(
970
- `[ResponseHandler] ${isStreaming ? "Streaming " : ""
971
- }Tool loop completed after ${toolLoopCount} iterations`
972
- );
973
- // Update final toolCalls from follow-up result if no more tools
974
- currentToolCalls = followUpToolCalls || [];
975
- break;
823
+ return {
824
+ selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
825
+ selectedStep: stepResult.nextStep,
826
+ responseDirectives: routingResult.responseDirectives,
827
+ session: stepResult.session,
828
+ isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
829
+ signalFirings: signalResult.firings,
830
+ signalPreDirective: signalResult.mergedDirective || undefined,
831
+ };
976
832
  }
977
- }
978
-
979
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
980
- logger.warn(
981
- `[ResponseGenerationError] ${isStreaming ? "Streaming t" : "T"}ool loop limit reached: ${toolLoopCount} iterations hit the cap (${MAX_TOOL_LOOPS}). Stopping tool execution. Increase MAX_TOOL_LOOPS or reduce recursive tool calls.`
982
- );
983
- }
984
-
985
- return {
986
- session: currentSession,
987
- toolCalls: currentToolCalls,
988
- };
989
- }
990
833
 
991
- /**
992
- * Handle data collection from structured response
993
- */
994
- async handleDataCollection(params: {
995
- structured: AgentStructuredResponse | undefined;
996
- nextStep: Step<TContext, TData>;
997
- session: SessionState<TData>;
998
- }): Promise<DataCollectionResult<TData>> {
999
- const { structured, nextStep, session } = params;
1000
-
1001
- if (!structured || !nextStep.collect) {
1002
- return { session };
1003
- }
834
+ // ── No signal processor: existing behavior (zero overhead) ────────────
835
+ const routingResult = await this.handleRoutingAndStepSelection({
836
+ session: params.session,
837
+ history: params.history,
838
+ context: params.context,
839
+ signal: params.signal,
840
+ });
1004
841
 
1005
- const collectedData: Partial<TData> = {};
1006
- // The structured response includes both base fields and collected extraction fields
1007
- const structuredData = structured as AgentStructuredResponse &
1008
- Record<string, unknown>;
842
+ let updatedSession = routingResult.session;
843
+ const isFlowComplete = routingResult.isFlowComplete;
844
+
845
+ // PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
846
+ // This allows us to skip steps whose data is already provided
847
+ if (routingResult.selectedFlow && !isFlowComplete) {
848
+ // Always pre-extract when flow collects data (not just on new flow entry)
849
+ // This ensures step selection has the most up-to-date data
850
+ if (this.shouldPreExtractData(routingResult.selectedFlow)) {
851
+ logger.debug(
852
+ `[ResponsePipeline] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
853
+ );
1009
854
 
1010
- for (const field of nextStep.collect) {
1011
- const fieldKey = field as string;
1012
- if (fieldKey in structuredData) {
1013
- (collectedData as Record<string, unknown>)[fieldKey] = structuredData[fieldKey];
1014
- }
1015
- }
855
+ const extractedData = await this.preExtractFlowData({
856
+ route: routingResult.selectedFlow,
857
+ history: params.history,
858
+ context: params.context,
859
+ session: updatedSession,
860
+ signal: params.signal,
861
+ });
1016
862
 
1017
- let updatedSession = session;
1018
- if (Object.keys(collectedData).length > 0) {
1019
- // Update agent-level collected data with validation if available
1020
- if (this.updateCollectedData) {
1021
- await this.updateCollectedData(collectedData);
863
+ if (extractedData && Object.keys(extractedData).length > 0) {
864
+ logger.debug(
865
+ `[ResponsePipeline] Pre-extracted data:`,
866
+ extractedData
867
+ );
868
+ // Merge pre-extracted data into session before step selection
869
+ updatedSession = mergeCollected(updatedSession, extractedData);
870
+ // Also update agent's collected data
871
+ await this.updateCollectedData(extractedData);
872
+ }
873
+ }
1022
874
  }
1023
875
 
1024
- // Update session with validated data
1025
- updatedSession = await this.updateData(session, collectedData);
1026
- logger.debug(`[ResponseHandler] Collected data:`, collectedData);
1027
- }
1028
-
1029
- return {
1030
- session: updatedSession,
1031
- collectedData,
1032
- };
1033
- }
876
+ // Determine next step using pipeline method for consistency
877
+ const stepResult = await this.determineNextStep({
878
+ selectedFlow: routingResult.selectedFlow,
879
+ selectedStep: routingResult.selectedStep,
880
+ session: updatedSession, // Use updated session with pre-extracted data
881
+ isFlowComplete, // Use updated completion status
882
+ context: params.context,
883
+ });
1034
884
 
1035
- /**
1036
- * Handle context updates from structured response
1037
- */
1038
- async handleContextUpdate(
1039
- structured: AgentStructuredResponse | undefined
1040
- ): Promise<void> {
1041
- if (
1042
- structured &&
1043
- typeof structured === "object" &&
1044
- "contextUpdate" in structured
1045
- ) {
1046
- await this.updateContext(
1047
- (structured as { contextUpdate?: Partial<TContext> })
1048
- .contextUpdate as Partial<TContext>
1049
- );
885
+ return {
886
+ selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
887
+ selectedStep: stepResult.nextStep, // Use the determined next step
888
+ responseDirectives: routingResult.responseDirectives,
889
+ session: stepResult.session,
890
+ // If a branch changed the flow, the original isFlowComplete no longer applies
891
+ isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
892
+ };
893
+ } catch (error) {
894
+ throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
1050
895
  }
1051
896
  }
1052
897
 
1053
898
  /**
1054
- * Handle flow completion: pure state transition.
899
+ * RENDER-STEP RESOLUTION — resolve the step a flow response will render,
900
+ * shared by the streaming and non-streaming paths. When no step was
901
+ * pre-selected: branches win over the linear chain, then candidate steps,
902
+ * then the initial-step fallback. Enforces `requires` (stays at the current
903
+ * step when required fields are missing) and enters the resolved step.
1055
904
  *
1056
- * Releases the session to idle (`currentFlow`/`currentStep` cleared),
1057
- * marks the active `flowHistory` entry completed, and (when `onComplete`
1058
- * is set) wires `pendingDirective` for the next turn. The framework
1059
- * emits no message of its own at the completion boundary; callers that
1060
- * need closing copy should add a final interactive step.
905
+ * Returns `flowTransition: true` when a branch resolved to a flow
906
+ * transition or completion there is no local step to render and the
907
+ * caller handles the transition.
1061
908
  */
1062
- async handleFlowCompletion(params: {
909
+ async resolveRenderStep(params: {
1063
910
  selectedFlow: Flow<TContext, TData>;
911
+ selectedStep?: Step<TContext, TData>;
1064
912
  session: SessionState<TData>;
1065
913
  context: TContext;
1066
- history: Event[];
1067
- }): Promise<{ session: SessionState<TData>; hasTransition: boolean }> {
1068
- const { selectedFlow, session, context } = params;
1069
-
1070
- // Check for onComplete transition
1071
- const transitionConfig = await selectedFlow.evaluateOnComplete(
1072
- { data: session.data },
1073
- context
1074
- );
1075
-
1076
- // Release to idle. When the flow is reentrant, scrub its owned
1077
- // fields so a future re-selection doesn't short-circuit on stale data.
1078
- const ownedFields = selectedFlow.reentrant
1079
- ? [
1080
- ...(selectedFlow.requiredFields ?? []),
1081
- ...(selectedFlow.optionalFields ?? []),
1082
- ]
1083
- : undefined;
1084
- let updatedSession = completeCurrentFlow(session, {
1085
- clearOwnedFields: ownedFields,
1086
- });
914
+ }): Promise<{
915
+ nextStep?: Step<TContext, TData>;
916
+ session: SessionState<TData>;
917
+ flowTransition: boolean;
918
+ }> {
919
+ const { selectedFlow, selectedStep, context } = params;
920
+ let session = params.session;
1087
921
 
1088
- if (transitionConfig) {
1089
- // Extract target from directive's goTo field
1090
- const goToTarget = typeof transitionConfig.goTo === 'string'
1091
- ? transitionConfig.goTo
1092
- : transitionConfig.goTo?.flow;
922
+ // Determine next step
923
+ let nextStep: Step<TContext, TData>;
924
+ if (selectedStep) {
925
+ nextStep = selectedStep;
926
+ } else {
927
+ // Determine current step from session if we're already in this flow
928
+ const isInSameFlow = session.currentFlow?.id === selectedFlow.id;
929
+ const currentStep = isInSameFlow && session.currentStep
930
+ ? selectedFlow.getStep(session.currentStep.id)
931
+ : undefined;
1093
932
 
1094
- // Find target flow by ID or title
1095
- const targetFlow = goToTarget ? this.getFlows().find(
1096
- (r) =>
1097
- r.id === goToTarget ||
1098
- r.title === goToTarget
1099
- ) : undefined;
933
+ logger.debug(`[ResponsePipeline] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
1100
934
 
1101
- if (targetFlow) {
1102
- updatedSession = {
1103
- ...updatedSession,
1104
- pendingDirective: {
1105
- goTo: targetFlow.id,
1106
- },
1107
- };
1108
- logger.debug(
1109
- `[ResponseHandler] Flow ${selectedFlow.title} completed with pending directive to: ${targetFlow.title}`
935
+ // STEP 1 (Algorithm 1): branches win over linear chain
936
+ if (currentStep?.branches && currentStep.branches.length > 0) {
937
+ const branchResult = await this.evaluateStepBranches(
938
+ currentStep, selectedFlow, session, context
1110
939
  );
1111
- return { session: updatedSession, hasTransition: true };
1112
- } else if (goToTarget) {
1113
- logger.warn(
1114
- `[FlowConfigurationError] onComplete target not found: flow "${selectedFlow.title}" completed but onComplete target "${goToTarget}" does not match any flow. ` +
1115
- `Fix the onComplete value to reference an existing flow id/title, or remove onComplete to release the session to idle.`
940
+ if (branchResult) {
941
+ if (branchResult.nextStep) {
942
+ nextStep = branchResult.nextStep;
943
+ session = branchResult.session;
944
+ } else {
945
+ // Flow transition or completion — no local step to render
946
+ return { nextStep: undefined, session: branchResult.session, flowTransition: true };
947
+ }
948
+ }
949
+ }
950
+
951
+ if (!nextStep!) {
952
+ // Get candidate steps based on current position in the flow
953
+ const candidates = await this.flowRouter.getCandidateStepsWithConditions(
954
+ selectedFlow,
955
+ currentStep, // Pass current step instead of undefined to maintain progression
956
+ createTemplateContext({ data: session.data, session, context })
1116
957
  );
958
+
959
+ logger.debug(`[ResponsePipeline] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
960
+
961
+ if (candidates.length > 0) {
962
+ nextStep = candidates[0].step;
963
+ logger.debug(`[ResponsePipeline] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
964
+ } else {
965
+ // Fallback to initial step even if it should be skipped
966
+ nextStep = selectedFlow.initialStep;
967
+ logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
968
+ }
1117
969
  }
1118
970
  }
1119
971
 
1120
- logger.debug(
1121
- `[ResponseHandler] Flow ${selectedFlow.title} completed; session released to idle.`
1122
- );
972
+ // Update session with next step
973
+ // If the next step has requires fields that are missing, stay at the previous step
974
+ if (nextStep.requires && nextStep.requires.length > 0) {
975
+ const sessionData = session.data || {};
976
+ const missingRequires = nextStep.requires.filter(
977
+ field => (sessionData as Record<string, unknown>)[String(field)] === undefined
978
+ );
979
+ if (missingRequires.length > 0) {
980
+ const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
981
+ `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
982
+ logger.warn(warning);
983
+ console.warn(warning);
984
+ // Stay at the current step - don't enter the next one
985
+ const currentStepId = session.currentStep?.id;
986
+ if (currentStepId) {
987
+ const currentStepInstance = selectedFlow.getStep(currentStepId);
988
+ if (currentStepInstance) {
989
+ nextStep = currentStepInstance;
990
+ logger.debug(`[ResponsePipeline] Staying at current step: ${nextStep.id} due to missing requires`);
991
+ }
992
+ }
993
+ } else {
994
+ session = enterStep(session, nextStep.id, nextStep.description);
995
+ logger.debug(`[ResponsePipeline] Entered step: ${nextStep.id}`);
996
+ }
997
+ } else {
998
+ session = enterStep(session, nextStep.id, nextStep.description);
999
+ logger.debug(`[ResponsePipeline] Entered step: ${nextStep.id}`);
1000
+ }
1123
1001
 
1124
- return { session: updatedSession, hasTransition: false };
1002
+ return { nextStep, session, flowTransition: false };
1125
1003
  }
1126
1004
 
1127
-
1128
1005
  /**
1129
- * Find an available tool by name for the given flow using ToolManager
1130
- * Delegates to ToolManager for unified tool resolution
1006
+ * Routing skip optimization (Requirements 20.1, 20.2, 20.3):
1007
+ * When the current step declares `collect` fields AND pre-extraction populates
1008
+ * at least one of those fields from the user's message, skip routing for this turn.
1009
+ *
1010
+ * Returns the routing result if the skip applies, or undefined to fall through
1011
+ * to normal routing.
1131
1012
  */
1132
- private findAvailableTool(
1133
- toolName: string,
1134
- flow?: Flow<TContext, TData>
1135
- ): Tool<TContext, TData> | undefined {
1136
- // Use ToolManager for unified tool resolution if available
1137
- if (this.toolManager) {
1138
- return this.toolManager.find(toolName, undefined, undefined, flow);
1139
- }
1013
+ private async attemptRoutingSkipForCollect(params: {
1014
+ session: SessionState<TData>;
1015
+ history: Event[];
1016
+ context: TContext;
1017
+ signal?: AbortSignal;
1018
+ }): Promise<{
1019
+ selectedFlow?: Flow<TContext, TData>;
1020
+ selectedStep?: Step<TContext, TData>;
1021
+ responseDirectives?: string[];
1022
+ session: SessionState<TData>;
1023
+ isFlowComplete: boolean;
1024
+ } | undefined> {
1025
+ const { session } = params;
1140
1026
 
1141
- // Fallback to legacy resolution if ToolManager not available
1142
- logger.warn(`[ResponsePipeline] ToolManager not available, using legacy tool resolution for: ${toolName}`);
1027
+ // Only applies when we already have a current flow and step
1028
+ if (!session.currentFlow || !session.currentStep) {
1029
+ return undefined;
1030
+ }
1143
1031
 
1144
- // Check flow-level tools first (if flow provided)
1145
- if (flow) {
1146
- const flowTool = flow
1147
- .getTools()
1148
- .find((tool) => tool.id === toolName || tool.id === toolName);
1149
- if (flowTool) return flowTool;
1032
+ // Also skip this optimization if there's a pending directive (it takes priority)
1033
+ if (session.pendingDirective) {
1034
+ return undefined;
1150
1035
  }
1151
1036
 
1152
- // Fall back to agent-level tools
1153
- return this.tools.find(
1154
- (tool) => tool.id === toolName || tool.id === toolName
1037
+ // Look up the actual Flow and Step objects to access `collect`
1038
+ const currentFlow = this.getFlows().find(
1039
+ (f) => f.id === session.currentFlow?.id
1155
1040
  );
1156
- }
1041
+ if (!currentFlow) {
1042
+ return undefined;
1043
+ }
1157
1044
 
1158
- /**
1159
- * Collect all available tools for the given flow and step context using ToolManager
1160
- * Delegates to ToolManager for unified tool resolution and deduplication
1161
- */
1162
- private collectAvailableTools(
1163
- flow?: Flow<TContext, TData>,
1164
- step?: Step<TContext, TData>
1165
- ): Array<{ id: string; description?: string; parameters?: unknown }> {
1166
- // Use ToolManager for unified tool collection if available
1167
- if (this.toolManager) {
1168
- const availableTools = this.toolManager.getAvailable(undefined, step, flow);
1169
- return availableTools.map((tool) => ({
1170
- id: tool.id,
1171
- description: tool.description,
1172
- parameters: tool.parameters,
1173
- }));
1045
+ const currentStep = currentFlow.getStep(session.currentStep.id);
1046
+ if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
1047
+ return undefined;
1174
1048
  }
1175
1049
 
1176
- // Fallback to legacy collection logic if ToolManager not available
1177
- logger.warn(`[ResponsePipeline] ToolManager not available, using legacy tool collection`);
1050
+ // We have a step with collect fields. Run pre-extraction to see if the
1051
+ // user's message populates any of them.
1052
+ const collectFields = currentStep.collect;
1178
1053
 
1179
- const availableTools = new Map<
1180
- string,
1181
- Tool<TContext, TData>
1182
- >();
1054
+ // Snapshot current data for comparison
1055
+ const dataBefore = { ...session.data };
1183
1056
 
1184
- // Add agent-level tools
1185
- this.tools.forEach((tool) => {
1186
- availableTools.set(tool.id, tool);
1057
+ // Run pre-extraction against the current flow
1058
+ const extractedData = await this.preExtractFlowData({
1059
+ route: currentFlow,
1060
+ history: params.history,
1061
+ context: params.context,
1062
+ session,
1063
+ signal: params.signal,
1187
1064
  });
1188
1065
 
1189
- // Add flow-level tools (these take precedence)
1190
- if (flow) {
1191
- flow.getTools().forEach((tool) => {
1192
- availableTools.set(tool.id, tool);
1193
- });
1066
+ if (!extractedData || Object.keys(extractedData).length === 0) {
1067
+ return undefined;
1194
1068
  }
1195
1069
 
1196
- // Filter by step-level allowed tools if specified
1197
- if (step?.tools) {
1198
- const allowedToolIds = new Set<string>();
1199
- const stepTools: Tool<TContext, TData>[] = [];
1200
-
1201
- for (const toolRef of step.tools) {
1202
- if (typeof toolRef === "string") {
1203
- // Reference to registered tool
1204
- allowedToolIds.add(toolRef);
1205
- } else {
1206
- // Inline tool definition
1207
- if (toolRef.id) {
1208
- allowedToolIds.add(toolRef.id);
1209
- stepTools.push(toolRef);
1210
- }
1211
- }
1212
- }
1213
-
1214
- // If step specifies tools, only include those
1215
- if (allowedToolIds.size > 0) {
1216
- const filteredTools = new Map<
1217
- string,
1218
- Tool<TContext, TData>
1219
- >();
1220
- for (const toolId of Array.from(allowedToolIds)) {
1221
- const tool = availableTools.get(toolId);
1222
- if (tool) {
1223
- filteredTools.set(toolId, tool);
1224
- }
1225
- }
1226
- // Add inline tools
1227
- stepTools.forEach((tool) => {
1228
- if (tool.id) {
1229
- filteredTools.set(tool.id, tool);
1230
- }
1231
- });
1232
- availableTools.clear();
1233
- filteredTools.forEach((tool, id) => availableTools.set(id, tool));
1070
+ // Determine which collect fields were newly populated by pre-extraction
1071
+ const populatedCollectFields: string[] = [];
1072
+ for (const field of collectFields) {
1073
+ const key = field as string;
1074
+ const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
1075
+ const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
1076
+ if (hasNewValue && !hadValue) {
1077
+ populatedCollectFields.push(key);
1234
1078
  }
1235
1079
  }
1236
1080
 
1237
- // Convert to the format expected by AI providers
1238
- return Array.from(availableTools.values()).map((tool) => ({
1239
- id: tool.id,
1240
- description: tool.description,
1241
- parameters: tool.parameters,
1242
- }));
1243
- }
1244
-
1245
- /**
1246
- * Get current context (fetches from provider if configured)
1247
- */
1248
- private async getContext(): Promise<TContext | undefined> {
1249
- // If context provider is configured, use it to fetch fresh context
1250
- if (this.options.contextProvider) {
1251
- return await this.options.contextProvider();
1081
+ if (populatedCollectFields.length === 0) {
1082
+ // Pre-extraction didn't populate any declared collect field — no skip
1083
+ return undefined;
1252
1084
  }
1253
1085
 
1254
- // Otherwise return the stored context
1255
- return this.context;
1256
- }
1257
-
1258
- // These need to be passed in or accessed differently since ResponseHandler is not part of Agent
1259
- private context?: TContext;
1260
- private currentSession?: SessionState<TData>;
1261
-
1262
- // Setters for context and current session (needed for beforeRespond hook)
1263
- setContext(context: TContext | undefined): void {
1264
- this.context = context;
1265
- }
1086
+ // ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
1087
+ logger.debug(
1088
+ `[ResponsePipeline] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`
1089
+ );
1266
1090
 
1267
- setCurrentSession(session: SessionState<TData> | undefined): void {
1268
- this.currentSession = session;
1269
- }
1091
+ // Merge extracted data into session
1092
+ const updatedSession = mergeCollected(session, extractedData);
1093
+ await this.updateCollectedData(extractedData);
1270
1094
 
1271
- public getStoredContext(): TContext | undefined {
1272
- return this.context;
1273
- }
1095
+ // Determine next step using pipeline method for consistency
1096
+ // Pass the current flow/step as the routing result (retained)
1097
+ const stepResult = await this.determineNextStep({
1098
+ selectedFlow: currentFlow,
1099
+ selectedStep: currentStep,
1100
+ session: updatedSession,
1101
+ isFlowComplete: false,
1102
+ context: params.context,
1103
+ });
1274
1104
 
1275
- public getCurrentSession(): SessionState<TData> | undefined {
1276
- return this.currentSession;
1105
+ return {
1106
+ selectedFlow: stepResult.flowChanged || currentFlow,
1107
+ selectedStep: stepResult.nextStep,
1108
+ responseDirectives: undefined,
1109
+ session: stepResult.session,
1110
+ isFlowComplete: false,
1111
+ };
1277
1112
  }
1278
1113
 
1279
1114
  /**
1280
- * Handle cross-flow completion evaluation and notifications
1281
- * This method evaluates all flows for completion and can trigger completion handlers
1115
+ * Check if a flow should pre-extract data before determining the initial step
1282
1116
  */
1283
- async handleCrossFlowCompletion(params: {
1284
- flows: Flow<TContext, TData>[];
1285
- session: SessionState<TData>;
1286
- context: TContext;
1287
- history: Event[];
1288
- }): Promise<{
1289
- session: SessionState<TData>;
1290
- completedFlows: Flow<TContext, TData>[];
1291
- pendingTransitions: Array<{
1292
- flow: Flow<TContext, TData>;
1293
- transitionConfig: Directive<TContext, TData>;
1294
- }>;
1295
- }> {
1296
- const { flows, session, context } = params;
1297
-
1298
- // Evaluate all flows for completion
1299
- const completedFlows: Flow<TContext, TData>[] = [];
1300
- const pendingTransitions: Array<{
1301
- flow: Flow<TContext, TData>;
1302
- transitionConfig: Directive<TContext, TData>;
1303
- }> = [];
1304
-
1305
- for (const flow of flows) {
1306
- if (flow.isComplete(session.data || {})) {
1307
- completedFlows.push(flow);
1308
-
1309
- // Check for onComplete transitions
1310
- const transitionConfig = await flow.evaluateOnComplete(
1311
- { data: session.data },
1312
- context
1313
- );
1314
-
1315
- if (transitionConfig) {
1316
- pendingTransitions.push({ flow, transitionConfig });
1317
- }
1318
-
1319
- logger.debug(
1320
- `[ResponsePipeline] Flow completed: ${flow.title} ` +
1321
- `(${Math.round(flow.getCompletionProgress(session.data || {}) * 100)}%)`
1322
- );
1323
- }
1117
+ private shouldPreExtractData(flow: Flow<TContext, TData>): boolean {
1118
+ // Pre-extract if flow has declared required or optional fields
1119
+ if (flow.requiredFields && flow.requiredFields.length > 0) {
1120
+ return true;
1324
1121
  }
1325
-
1326
- // Log completion status for all flows
1327
- if (completedFlows.length > 0) {
1328
- logger.debug(
1329
- `[ResponsePipeline] Cross-flow completion evaluation: ` +
1330
- `${completedFlows.length}/${flows.length} flows complete`
1331
- );
1122
+ if (flow.optionalFields && flow.optionalFields.length > 0) {
1123
+ return true;
1332
1124
  }
1333
1125
 
1334
- return {
1335
- session,
1336
- completedFlows,
1337
- pendingTransitions,
1338
- };
1126
+ // Pre-extract if any step in the flow collects data
1127
+ const steps = flow.getAllSteps();
1128
+ const hasDataCollectionSteps = steps.some(
1129
+ step => step.collect && step.collect.length > 0
1130
+ );
1131
+
1132
+ return hasDataCollectionSteps;
1339
1133
  }
1340
1134
 
1341
1135
  /**
1342
- * Update data flow to ensure agent-level data consistency
1343
- * This method ensures that data updates are properly validated and propagated
1136
+ * Pre-extract data from user message when entering a flow
1137
+ * This allows skipping steps whose data is already provided
1344
1138
  */
1345
- async updateDataFlow(params: {
1139
+ private async preExtractFlowData(params: {
1140
+ route: Flow<TContext, TData>;
1141
+ history: Event[];
1142
+ context: TContext;
1346
1143
  session: SessionState<TData>;
1347
- dataUpdate: Partial<TData>;
1348
- flows: Flow<TContext, TData>[];
1349
- }): Promise<SessionState<TData>> {
1350
- const { session, dataUpdate, flows } = params;
1351
-
1352
- // Update session data
1353
- const updatedSession = await this.updateData(session, dataUpdate);
1144
+ signal?: AbortSignal;
1145
+ }): Promise<Partial<TData>> {
1146
+ const { route: flow, history, signal } = params;
1147
+
1148
+ // Build a schema for data extraction based on flow's fields
1149
+ const extractionSchema = this.getSchema();
1150
+ if (!extractionSchema) {
1151
+ logger.warn(`[ResponsePipeline] No schema available for pre-extraction`);
1152
+ return {};
1153
+ }
1354
1154
 
1355
- // Update agent-level data if handler is available
1356
- if (this.updateCollectedData) {
1357
- await this.updateCollectedData(dataUpdate);
1155
+ // Get last user message
1156
+ const lastMessage = getLastMessageFromHistory(history);
1157
+
1158
+ // Build extraction prompt
1159
+ const extractionPrompt = [
1160
+ `Extract any relevant information from the user's message that matches the following data fields.`,
1161
+ `Only extract information that is explicitly stated or clearly implied.`,
1162
+ ``,
1163
+ `User's message: "${lastMessage}"`,
1164
+ ``,
1165
+ `Extract data for these fields if present:`,
1166
+ ];
1167
+
1168
+ // Add field descriptions
1169
+ if (flow.requiredFields) {
1170
+ extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
1171
+ }
1172
+ if (flow.optionalFields) {
1173
+ extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
1358
1174
  }
1359
1175
 
1360
- // Evaluate flow completions after data update
1361
- const completionResults = await this.handleCrossFlowCompletion({
1362
- flows,
1363
- session: updatedSession,
1364
- context: this.context!,
1365
- history: [],
1366
- });
1176
+ extractionPrompt.push(
1177
+ ``,
1178
+ `Return ONLY the extracted data as JSON. If no data can be extracted, return an empty object {}.`
1179
+ );
1367
1180
 
1368
- // Log any newly completed flows
1369
- if (completionResults.completedFlows.length > 0) {
1370
- logger.debug(
1371
- `[ResponsePipeline] Data update resulted in ${completionResults.completedFlows.length} completed flows`
1372
- );
1373
- }
1181
+ // Convert Event[] to HistoryItem[] for provider call
1182
+ const historyItems = eventsToHistory(history);
1183
+
1184
+ // Call AI to extract data
1185
+ try {
1186
+ const result = await this.options.provider.generateMessage<TContext, Partial<TData>>({
1187
+ prompt: extractionPrompt.join('\n'),
1188
+ history: historyItems,
1189
+ context: {} as TContext, // Passed as empty object so AI doesn't "extract" from context
1190
+ // NOTE: context is intentionally NOT passed here.
1191
+ // Passing context caused the AI to "extract" data from the lead's context
1192
+ // (e.g., name, sector, city) instead of from what the user actually said.
1193
+ signal,
1194
+ parameters: {
1195
+ jsonSchema: extractionSchema,
1196
+ schemaName: 'data_extraction',
1197
+ },
1198
+ });
1374
1199
 
1375
- return completionResults.session;
1200
+ return result.structured || {};
1201
+ } catch (error) {
1202
+ logger.error(`[ResponsePipeline] Pre-extraction failed:`, error);
1203
+ return {};
1204
+ }
1376
1205
  }
1206
+
1377
1207
  }