@falai/agent 2.2.4 → 2.4.2

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 (397) 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/SignalProcessor.d.ts.map +1 -1
  99. package/dist/cjs/core/SignalProcessor.js +28 -11
  100. package/dist/cjs/core/SignalProcessor.js.map +1 -1
  101. package/dist/cjs/core/Step.d.ts +3 -1
  102. package/dist/cjs/core/Step.d.ts.map +1 -1
  103. package/dist/cjs/core/Step.js +10 -3
  104. package/dist/cjs/core/Step.js.map +1 -1
  105. package/dist/cjs/core/StepLifecycle.d.ts +33 -0
  106. package/dist/cjs/core/StepLifecycle.d.ts.map +1 -0
  107. package/dist/cjs/core/StepLifecycle.js +97 -0
  108. package/dist/cjs/core/StepLifecycle.js.map +1 -0
  109. package/dist/cjs/core/ToolLoopExecutor.d.ts +104 -0
  110. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -0
  111. package/dist/cjs/core/ToolLoopExecutor.js +391 -0
  112. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -0
  113. package/dist/cjs/core/ToolManager.d.ts +1 -1
  114. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  115. package/dist/cjs/core/ToolManager.js.map +1 -1
  116. package/dist/cjs/index.d.ts +4 -5
  117. package/dist/cjs/index.d.ts.map +1 -1
  118. package/dist/cjs/index.js +7 -6
  119. package/dist/cjs/index.js.map +1 -1
  120. package/dist/cjs/providers/AnthropicProvider.d.ts +2 -0
  121. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  122. package/dist/cjs/providers/AnthropicProvider.js +28 -56
  123. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  124. package/dist/cjs/providers/DeepSeekProvider.d.ts +25 -21
  125. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
  126. package/dist/cjs/providers/DeepSeekProvider.js +48 -407
  127. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
  128. package/dist/cjs/providers/GeminiProvider.d.ts +2 -0
  129. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  130. package/dist/cjs/providers/GeminiProvider.js +27 -56
  131. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  132. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +129 -0
  133. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  134. package/dist/cjs/providers/OpenAICompatibleProvider.js +485 -0
  135. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -0
  136. package/dist/cjs/providers/OpenAIProvider.d.ts +9 -28
  137. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  138. package/dist/cjs/providers/OpenAIProvider.js +23 -417
  139. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  140. package/dist/cjs/providers/OpenRouterProvider.d.ts +10 -27
  141. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  142. package/dist/cjs/providers/OpenRouterProvider.js +28 -417
  143. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  144. package/dist/cjs/providers/errorClassification.d.ts +61 -0
  145. package/dist/cjs/providers/errorClassification.d.ts.map +1 -0
  146. package/dist/cjs/providers/errorClassification.js +123 -0
  147. package/dist/cjs/providers/errorClassification.js.map +1 -0
  148. package/dist/cjs/providers/index.d.ts +4 -0
  149. package/dist/cjs/providers/index.d.ts.map +1 -1
  150. package/dist/cjs/providers/index.js +7 -1
  151. package/dist/cjs/providers/index.js.map +1 -1
  152. package/dist/cjs/types/agent.d.ts +2 -1
  153. package/dist/cjs/types/agent.d.ts.map +1 -1
  154. package/dist/cjs/types/ai.d.ts +20 -0
  155. package/dist/cjs/types/ai.d.ts.map +1 -1
  156. package/dist/cjs/types/errors.d.ts +33 -0
  157. package/dist/cjs/types/errors.d.ts.map +1 -1
  158. package/dist/cjs/types/errors.js +37 -1
  159. package/dist/cjs/types/errors.js.map +1 -1
  160. package/dist/cjs/types/flow.d.ts +10 -5
  161. package/dist/cjs/types/flow.d.ts.map +1 -1
  162. package/dist/cjs/types/history.d.ts +1 -1
  163. package/dist/cjs/types/history.d.ts.map +1 -1
  164. package/dist/cjs/types/index.d.ts +5 -4
  165. package/dist/cjs/types/index.d.ts.map +1 -1
  166. package/dist/cjs/types/index.js +3 -1
  167. package/dist/cjs/types/index.js.map +1 -1
  168. package/dist/cjs/types/persistence.d.ts +43 -2
  169. package/dist/cjs/types/persistence.d.ts.map +1 -1
  170. package/dist/cjs/types/session.d.ts +8 -0
  171. package/dist/cjs/types/session.d.ts.map +1 -1
  172. package/dist/cjs/types/signals.d.ts +18 -2
  173. package/dist/cjs/types/signals.d.ts.map +1 -1
  174. package/dist/cjs/types/template.d.ts +3 -1
  175. package/dist/cjs/types/template.d.ts.map +1 -1
  176. package/dist/cjs/types/tool.d.ts +4 -4
  177. package/dist/cjs/types/tool.d.ts.map +1 -1
  178. package/dist/cjs/utils/condition.d.ts +19 -0
  179. package/dist/cjs/utils/condition.d.ts.map +1 -1
  180. package/dist/cjs/utils/condition.js +86 -15
  181. package/dist/cjs/utils/condition.js.map +1 -1
  182. package/dist/cjs/utils/session.d.ts +1 -0
  183. package/dist/cjs/utils/session.d.ts.map +1 -1
  184. package/dist/cjs/utils/session.js +1 -0
  185. package/dist/cjs/utils/session.js.map +1 -1
  186. package/dist/core/Agent.d.ts +28 -18
  187. package/dist/core/Agent.d.ts.map +1 -1
  188. package/dist/core/Agent.js +58 -68
  189. package/dist/core/Agent.js.map +1 -1
  190. package/dist/core/AutoChainExecutor.d.ts.map +1 -1
  191. package/dist/core/AutoChainExecutor.js +14 -20
  192. package/dist/core/AutoChainExecutor.js.map +1 -1
  193. package/dist/core/BranchEvaluator.d.ts +4 -3
  194. package/dist/core/BranchEvaluator.d.ts.map +1 -1
  195. package/dist/core/BranchEvaluator.js +18 -23
  196. package/dist/core/BranchEvaluator.js.map +1 -1
  197. package/dist/core/Flow.d.ts +2 -1
  198. package/dist/core/Flow.d.ts.map +1 -1
  199. package/dist/core/Flow.js +10 -3
  200. package/dist/core/Flow.js.map +1 -1
  201. package/dist/core/FlowRouter.d.ts +1 -0
  202. package/dist/core/FlowRouter.d.ts.map +1 -1
  203. package/dist/core/FlowRouter.js +28 -5
  204. package/dist/core/FlowRouter.js.map +1 -1
  205. package/dist/core/PersistenceManager.d.ts +3 -0
  206. package/dist/core/PersistenceManager.d.ts.map +1 -1
  207. package/dist/core/PersistenceManager.js +58 -6
  208. package/dist/core/PersistenceManager.js.map +1 -1
  209. package/dist/core/PromptComposer.d.ts.map +1 -1
  210. package/dist/core/PromptComposer.js +24 -10
  211. package/dist/core/PromptComposer.js.map +1 -1
  212. package/dist/core/ResponseGenerationError.d.ts +30 -0
  213. package/dist/core/ResponseGenerationError.d.ts.map +1 -0
  214. package/dist/core/ResponseGenerationError.js +33 -0
  215. package/dist/core/ResponseGenerationError.js.map +1 -0
  216. package/dist/core/ResponseModal.d.ts +43 -102
  217. package/dist/core/ResponseModal.d.ts.map +1 -1
  218. package/dist/core/ResponseModal.js +171 -1186
  219. package/dist/core/ResponseModal.js.map +1 -1
  220. package/dist/core/ResponsePipeline.d.ts +58 -152
  221. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  222. package/dist/core/ResponsePipeline.js +409 -458
  223. package/dist/core/ResponsePipeline.js.map +1 -1
  224. package/dist/core/SessionFinalizer.d.ts +34 -0
  225. package/dist/core/SessionFinalizer.d.ts.map +1 -0
  226. package/dist/core/SessionFinalizer.js +57 -0
  227. package/dist/core/SessionFinalizer.js.map +1 -0
  228. package/dist/core/SessionManager.d.ts +1 -1
  229. package/dist/core/SessionManager.d.ts.map +1 -1
  230. package/dist/core/SessionManager.js +18 -7
  231. package/dist/core/SessionManager.js.map +1 -1
  232. package/dist/core/SignalCoordinator.d.ts +103 -0
  233. package/dist/core/SignalCoordinator.d.ts.map +1 -0
  234. package/dist/core/SignalCoordinator.js +203 -0
  235. package/dist/core/SignalCoordinator.js.map +1 -0
  236. package/dist/core/SignalEvaluator.d.ts +2 -2
  237. package/dist/core/SignalEvaluator.d.ts.map +1 -1
  238. package/dist/core/SignalEvaluator.js +11 -26
  239. package/dist/core/SignalEvaluator.js.map +1 -1
  240. package/dist/core/SignalProcessor.d.ts.map +1 -1
  241. package/dist/core/SignalProcessor.js +28 -11
  242. package/dist/core/SignalProcessor.js.map +1 -1
  243. package/dist/core/Step.d.ts +3 -1
  244. package/dist/core/Step.d.ts.map +1 -1
  245. package/dist/core/Step.js +10 -3
  246. package/dist/core/Step.js.map +1 -1
  247. package/dist/core/StepLifecycle.d.ts +33 -0
  248. package/dist/core/StepLifecycle.d.ts.map +1 -0
  249. package/dist/core/StepLifecycle.js +93 -0
  250. package/dist/core/StepLifecycle.js.map +1 -0
  251. package/dist/core/ToolLoopExecutor.d.ts +104 -0
  252. package/dist/core/ToolLoopExecutor.d.ts.map +1 -0
  253. package/dist/core/ToolLoopExecutor.js +387 -0
  254. package/dist/core/ToolLoopExecutor.js.map +1 -0
  255. package/dist/core/ToolManager.d.ts +1 -1
  256. package/dist/core/ToolManager.d.ts.map +1 -1
  257. package/dist/core/ToolManager.js.map +1 -1
  258. package/dist/index.d.ts +4 -5
  259. package/dist/index.d.ts.map +1 -1
  260. package/dist/index.js +3 -3
  261. package/dist/index.js.map +1 -1
  262. package/dist/providers/AnthropicProvider.d.ts +2 -0
  263. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  264. package/dist/providers/AnthropicProvider.js +22 -50
  265. package/dist/providers/AnthropicProvider.js.map +1 -1
  266. package/dist/providers/DeepSeekProvider.d.ts +25 -21
  267. package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
  268. package/dist/providers/DeepSeekProvider.js +49 -408
  269. package/dist/providers/DeepSeekProvider.js.map +1 -1
  270. package/dist/providers/GeminiProvider.d.ts +2 -0
  271. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  272. package/dist/providers/GeminiProvider.js +21 -50
  273. package/dist/providers/GeminiProvider.js.map +1 -1
  274. package/dist/providers/OpenAICompatibleProvider.d.ts +129 -0
  275. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  276. package/dist/providers/OpenAICompatibleProvider.js +481 -0
  277. package/dist/providers/OpenAICompatibleProvider.js.map +1 -0
  278. package/dist/providers/OpenAIProvider.d.ts +9 -28
  279. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  280. package/dist/providers/OpenAIProvider.js +23 -417
  281. package/dist/providers/OpenAIProvider.js.map +1 -1
  282. package/dist/providers/OpenRouterProvider.d.ts +10 -27
  283. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  284. package/dist/providers/OpenRouterProvider.js +28 -417
  285. package/dist/providers/OpenRouterProvider.js.map +1 -1
  286. package/dist/providers/errorClassification.d.ts +61 -0
  287. package/dist/providers/errorClassification.d.ts.map +1 -0
  288. package/dist/providers/errorClassification.js +116 -0
  289. package/dist/providers/errorClassification.js.map +1 -0
  290. package/dist/providers/index.d.ts +4 -0
  291. package/dist/providers/index.d.ts.map +1 -1
  292. package/dist/providers/index.js +2 -0
  293. package/dist/providers/index.js.map +1 -1
  294. package/dist/types/agent.d.ts +2 -1
  295. package/dist/types/agent.d.ts.map +1 -1
  296. package/dist/types/ai.d.ts +20 -0
  297. package/dist/types/ai.d.ts.map +1 -1
  298. package/dist/types/errors.d.ts +33 -0
  299. package/dist/types/errors.d.ts.map +1 -1
  300. package/dist/types/errors.js +34 -0
  301. package/dist/types/errors.js.map +1 -1
  302. package/dist/types/flow.d.ts +10 -5
  303. package/dist/types/flow.d.ts.map +1 -1
  304. package/dist/types/history.d.ts +1 -1
  305. package/dist/types/history.d.ts.map +1 -1
  306. package/dist/types/index.d.ts +5 -4
  307. package/dist/types/index.d.ts.map +1 -1
  308. package/dist/types/index.js +1 -1
  309. package/dist/types/index.js.map +1 -1
  310. package/dist/types/persistence.d.ts +43 -2
  311. package/dist/types/persistence.d.ts.map +1 -1
  312. package/dist/types/session.d.ts +8 -0
  313. package/dist/types/session.d.ts.map +1 -1
  314. package/dist/types/signals.d.ts +18 -2
  315. package/dist/types/signals.d.ts.map +1 -1
  316. package/dist/types/template.d.ts +3 -1
  317. package/dist/types/template.d.ts.map +1 -1
  318. package/dist/types/tool.d.ts +4 -4
  319. package/dist/types/tool.d.ts.map +1 -1
  320. package/dist/utils/condition.d.ts +19 -0
  321. package/dist/utils/condition.d.ts.map +1 -1
  322. package/dist/utils/condition.js +84 -15
  323. package/dist/utils/condition.js.map +1 -1
  324. package/dist/utils/session.d.ts +1 -0
  325. package/dist/utils/session.d.ts.map +1 -1
  326. package/dist/utils/session.js +1 -0
  327. package/dist/utils/session.js.map +1 -1
  328. package/docs/README.md +1 -1
  329. package/docs/guides/branching.md +1 -1
  330. package/docs/guides/compaction.md +12 -5
  331. package/docs/guides/conditions.md +15 -2
  332. package/docs/guides/error-handling.md +59 -9
  333. package/docs/guides/instructions.md +2 -2
  334. package/docs/guides/persistence.md +69 -2
  335. package/docs/guides/streaming.md +2 -0
  336. package/docs/migration/README.md +5 -1
  337. package/docs/migration/v2-3-to-v2-4.md +316 -0
  338. package/docs/reference/adapters.md +55 -7
  339. package/docs/reference/branches.md +2 -2
  340. package/docs/reference/create-agent.md +3 -3
  341. package/docs/reference/errors.md +66 -1
  342. package/docs/reference/flow.md +1 -1
  343. package/docs/reference/instruction.md +4 -4
  344. package/docs/reference/providers.md +100 -3
  345. package/docs/reference/signals.md +14 -3
  346. package/docs/reference/step.md +2 -2
  347. package/docs/reference/tool.md +3 -1
  348. package/docs/start/05-go-to-production.md +3 -0
  349. package/package.json +1 -1
  350. package/src/adapters/MemoryAdapter.ts +15 -1
  351. package/src/adapters/MongoAdapter.ts +41 -2
  352. package/src/adapters/OpenSearchAdapter.ts +24 -1
  353. package/src/adapters/PostgreSQLAdapter.ts +45 -5
  354. package/src/adapters/PrismaAdapter.ts +82 -16
  355. package/src/adapters/RedisAdapter.ts +19 -1
  356. package/src/adapters/SQLiteAdapter.ts +47 -5
  357. package/src/core/Agent.ts +70 -85
  358. package/src/core/AutoChainExecutor.ts +27 -57
  359. package/src/core/BranchEvaluator.ts +24 -30
  360. package/src/core/Flow.ts +10 -3
  361. package/src/core/FlowRouter.ts +36 -4
  362. package/src/core/PersistenceManager.ts +79 -10
  363. package/src/core/PromptComposer.ts +25 -12
  364. package/src/core/ResponseGenerationError.ts +56 -0
  365. package/src/core/ResponseModal.ts +233 -1482
  366. package/src/core/ResponsePipeline.ts +492 -662
  367. package/src/core/SessionFinalizer.ts +78 -0
  368. package/src/core/SessionManager.ts +21 -9
  369. package/src/core/SignalCoordinator.ts +263 -0
  370. package/src/core/SignalEvaluator.ts +12 -29
  371. package/src/core/SignalProcessor.ts +34 -11
  372. package/src/core/Step.ts +11 -3
  373. package/src/core/StepLifecycle.ts +139 -0
  374. package/src/core/ToolLoopExecutor.ts +492 -0
  375. package/src/core/ToolManager.ts +2 -1
  376. package/src/index.ts +7 -5
  377. package/src/providers/AnthropicProvider.ts +30 -72
  378. package/src/providers/DeepSeekProvider.ts +74 -586
  379. package/src/providers/GeminiProvider.ts +29 -70
  380. package/src/providers/OpenAICompatibleProvider.ts +738 -0
  381. package/src/providers/OpenAIProvider.ts +29 -602
  382. package/src/providers/OpenRouterProvider.ts +38 -596
  383. package/src/providers/errorClassification.ts +172 -0
  384. package/src/providers/index.ts +13 -0
  385. package/src/types/agent.ts +2 -1
  386. package/src/types/ai.ts +22 -0
  387. package/src/types/errors.ts +57 -0
  388. package/src/types/flow.ts +10 -5
  389. package/src/types/history.ts +1 -2
  390. package/src/types/index.ts +5 -1
  391. package/src/types/persistence.ts +50 -2
  392. package/src/types/session.ts +9 -0
  393. package/src/types/signals.ts +20 -2
  394. package/src/types/template.ts +3 -1
  395. package/src/types/tool.ts +10 -10
  396. package/src/utils/condition.ts +115 -18
  397. package/src/utils/session.ts +2 -0
@@ -4,33 +4,78 @@
4
4
  */
5
5
 
6
6
  import type {
7
+ AgentOptions,
7
8
  AgentResponse,
8
9
  AgentResponseStreamChunk,
9
10
  History,
10
11
  SessionState,
11
12
  StepRef,
12
13
  HistoryItem,
13
- Tool,
14
14
  Event,
15
15
  AgentStructuredResponse,
16
16
  StoppedReason,
17
- ToolCallRequest,
18
17
  ScopedInstructions,
19
18
  AppliedInstruction,
20
- PrepareResult,
21
19
  Directive,
20
+ Instruction,
21
+ Term,
22
+ StructuredSchema,
23
+ CompactionOptions,
22
24
  } from "../types";
23
25
  import type { SignalFiring } from "../types/signals";
24
- import type { Agent } from "./Agent";
26
+ import type { SessionManager } from "./SessionManager";
27
+ import type { SignalProcessor } from "./SignalProcessor";
28
+ import type { PromptSectionCache } from "./PromptSectionCache";
29
+ import type { FlowRouter } from "./FlowRouter";
30
+ import type { PersistenceManager } from "./PersistenceManager";
25
31
  import type { Flow } from "./Flow";
26
32
  import { Step } from "./Step";
27
33
  import { ResponseEngine } from "./ResponseEngine";
28
- import { ResponsePipeline, hasDirectivePositionField } from "./ResponsePipeline";
34
+ import { ResponsePipeline } from "./ResponsePipeline";
29
35
  import { AutoChainExecutor, type AutoChainResult } from "./AutoChainExecutor";
30
- import { cloneDeep, mergeCollected, enterStep, enterFlow, getLastMessageFromHistory, logger, historyToEvents, eventsToHistory, serializeToolResult, completeCurrentFlow, render } from "../utils";
36
+ import { StepLifecycle } from "./StepLifecycle";
37
+ import { SessionFinalizer } from "./SessionFinalizer";
38
+ import { ToolLoopExecutor } from "./ToolLoopExecutor";
39
+ import { SignalCoordinator } from "./SignalCoordinator";
40
+ import { ResponseGenerationError } from "./ResponseGenerationError";
41
+ import { cloneDeep, mergeCollected, logger, historyToEvents, completeCurrentFlow, render } from "../utils";
31
42
  import { createTemplateContext } from "../utils/template";
32
43
  import type { ToolManager } from "./ToolManager";
33
44
 
45
+ /**
46
+ * The narrow surface ResponseModal (and its collaborators) need from the
47
+ * Agent. Agent implements this; the response layer is constructible and
48
+ * testable against this interface without a full Agent.
49
+ */
50
+ export interface ResponseModalDeps<TContext = unknown, TData = unknown> {
51
+ /** Session manager (history, live session, sync). */
52
+ readonly session: SessionManager<TData>;
53
+ /** Tool registry/resolver and single-tool executor. */
54
+ readonly tool: ToolManager<TContext, TData>;
55
+ readonly signalProcessor: SignalProcessor<TContext, TData> | undefined;
56
+ readonly promptSectionCache: PromptSectionCache;
57
+ readonly instructions: Instruction<TContext, TData>[];
58
+ readonly schema: StructuredSchema | undefined;
59
+ readonly maxAutoStepsPerTurn: number;
60
+ /** The agent's live session reference (read and replaced at finalize). */
61
+ currentSession: SessionState<TData> | undefined;
62
+ getAgentOptions(): AgentOptions<TContext, TData>;
63
+ getFlows(): Flow<TContext, TData>[];
64
+ getTerms(): Term<TContext, TData>[];
65
+ getFlowRouter(): FlowRouter<TContext, TData>;
66
+ getContext(): Promise<TContext | undefined>;
67
+ getCompactionOptions(): CompactionOptions | undefined;
68
+ getPersistenceManager(): PersistenceManager<TData> | undefined;
69
+ getUpdateDataMethod(): (
70
+ session: SessionState<TData>,
71
+ dataUpdate: Partial<TData>
72
+ ) => Promise<SessionState<TData>>;
73
+ updateContext(updates: Partial<TContext>): Promise<void>;
74
+ updateCollectedData(updates: Partial<TData>): Promise<void>;
75
+ /** Drain data staged before any session existed. */
76
+ consumePendingData(): Partial<TData>;
77
+ }
78
+
34
79
  /**
35
80
  * Configuration options for ResponseModal
36
81
  */
@@ -72,58 +117,6 @@ export interface GenerateOptions<TContext = unknown> {
72
117
  history?: History; // Optional: override session history
73
118
  }
74
119
 
75
- /**
76
- * Error details for response generation failures
77
- */
78
- interface ResponseGenerationErrorDetails {
79
- originalError?: unknown;
80
- params?: Record<string, unknown>;
81
- phase?: string;
82
- context?: Record<string, unknown>;
83
- }
84
-
85
- /**
86
- * Error class for response generation failures
87
- */
88
- export class ResponseGenerationError extends Error {
89
- constructor(
90
- message: string,
91
- public readonly details?: ResponseGenerationErrorDetails
92
- ) {
93
- super(message);
94
- this.name = 'ResponseGenerationError';
95
-
96
- // Preserve stack trace from original error if available
97
- if (details?.originalError instanceof Error && details.originalError.stack) {
98
- this.stack = `${this.stack}\nCaused by: ${details.originalError.stack}`;
99
- }
100
- }
101
-
102
- /**
103
- * Create a ResponseGenerationError from an unknown error
104
- */
105
- static fromError(
106
- error: unknown,
107
- phase: string,
108
- params?: Record<string, unknown>,
109
- context?: Record<string, unknown>
110
- ): ResponseGenerationError {
111
- const message = error instanceof Error ? error.message : String(error);
112
- return new ResponseGenerationError(
113
- `[ResponseGenerationError] Response generation failed in ${phase}: ${message}. ` +
114
- `Check provider configuration and the ${phase} phase handler.`,
115
- { originalError: error, params, phase, context }
116
- );
117
- }
118
-
119
- /**
120
- * Check if an error is a ResponseGenerationError
121
- */
122
- static isResponseGenerationError(error: unknown): error is ResponseGenerationError {
123
- return error instanceof ResponseGenerationError;
124
- }
125
- }
126
-
127
120
  /**
128
121
  * Common response context used across all response methods
129
122
  */
@@ -154,44 +147,90 @@ interface ResponseContext<TContext = unknown, TData = unknown> {
154
147
  export class ResponseModal<TContext = unknown, TData = unknown> {
155
148
  private readonly responseEngine: ResponseEngine<TContext, TData>;
156
149
  private readonly responsePipeline: ResponsePipeline<TContext, TData>;
150
+ private readonly stepLifecycle: StepLifecycle<TContext, TData>;
151
+ private readonly sessionFinalizer: SessionFinalizer<TContext, TData>;
152
+ private readonly toolLoopExecutor: ToolLoopExecutor<TContext, TData>;
153
+ private readonly signalCoordinator: SignalCoordinator<TContext, TData>;
157
154
 
158
155
  constructor(
159
- private readonly agent: Agent<TContext, TData>,
156
+ private readonly agent: ResponseModalDeps<TContext, TData>,
160
157
  private readonly options?: ResponseModalOptions
161
158
  ) {
162
159
  // Initialize response engine
163
160
  this.responseEngine = new ResponseEngine<TContext, TData>(this.agent.promptSectionCache);
164
161
 
162
+ // Signal pre/post phase orchestration
163
+ this.signalCoordinator = new SignalCoordinator<TContext, TData>({
164
+ getFlows: () => this.agent.getFlows(),
165
+ signalProcessor: this.agent.signalProcessor,
166
+ });
167
+
165
168
  // Initialize response pipeline with agent dependencies
166
169
  this.responsePipeline = new ResponsePipeline<TContext, TData>(
167
170
  this.agent.getAgentOptions(),
168
171
  () => this.agent.getFlows(), // Pass a function to get flows dynamically
169
- this.agent.getTools(),
170
172
  this.agent.getFlowRouter(),
171
- this.agent.updateContext.bind(this.agent),
172
- this.agent.getUpdateDataMethod(),
173
+ this.signalCoordinator,
173
174
  this.agent.updateCollectedData.bind(this.agent),
174
- this.getToolManager(),
175
- this.agent.signalProcessor
175
+ () => this.agent.schema
176
176
  );
177
+
178
+ // Step prepare/finalize execution, shared by the prepare phase and finalizer
179
+ this.stepLifecycle = new StepLifecycle<TContext, TData>({
180
+ getFlows: () => this.agent.getFlows(),
181
+ toolManager: this.getToolManager(),
182
+ updateContext: this.agent.updateContext.bind(this.agent),
183
+ updateData: this.agent.updateCollectedData.bind(this.agent),
184
+ });
185
+
186
+ // Single owner of end-of-turn finalization (compaction + persistence + sync)
187
+ this.sessionFinalizer = new SessionFinalizer<TContext, TData>({
188
+ getCompactionOptions: () => this.agent.getCompactionOptions(),
189
+ getPersistenceManager: () => this.agent.getPersistenceManager(),
190
+ getAgentOptions: () => this.agent.getAgentOptions(),
191
+ getCurrentSession: () => this.agent.currentSession,
192
+ setCurrentSession: (session) => { this.agent.currentSession = session; },
193
+ stepLifecycle: this.stepLifecycle,
194
+ enableAutoSave: this.options?.enableAutoSave,
195
+ });
196
+
197
+ // Tool follow-up loop (run tools, ask the LLM again) + streaming batch execution
198
+ this.toolLoopExecutor = new ToolLoopExecutor<TContext, TData>({
199
+ toolManager: this.getToolManager(),
200
+ getAgentOptions: () => this.agent.getAgentOptions(),
201
+ updateContext: this.agent.updateContext.bind(this.agent),
202
+ updateCollectedData: this.agent.updateCollectedData.bind(this.agent),
203
+ updateSessionData: this.agent.getUpdateDataMethod(),
204
+ maxToolLoops: this.options?.maxToolLoops,
205
+ });
206
+
177
207
  }
178
208
 
179
209
  /**
180
210
  * Generate a non-streaming response using unified logic
181
211
  */
182
212
  async respond(params: RespondParams<TContext, TData>): Promise<AgentResponse<TData>> {
213
+ // Snapshot the managed session so a failed turn has no in-memory effect:
214
+ // without this, mutations made before the failure leave the live session
215
+ // diverged from persisted state
216
+ const preTurnSession = this.agent.session.current
217
+ ? cloneDeep(this.agent.session.current)
218
+ : undefined;
183
219
  try {
184
220
  // Use unified response preparation and routing
185
221
  const responseContext = await this.prepareUnifiedResponseContext(params);
186
222
  // Generate response using unified logic
187
223
  const result = await this.generateUnifiedResponse(responseContext);
188
224
 
189
- // Finalize session
190
- await this.finalizeSession(result.session!, responseContext.effectiveContext);
225
+ // Finalize session — the non-streaming turn's single finalize
226
+ await this.sessionFinalizer.finalize(result.session!, responseContext.effectiveContext);
191
227
 
192
228
  return result;
193
229
 
194
230
  } catch (error) {
231
+ if (preTurnSession) {
232
+ this.agent.session.syncSession(preTurnSession);
233
+ }
195
234
  throw new ResponseGenerationError(
196
235
  `[ResponseGenerationError] Response generation failed: ${error instanceof Error ? error.message : String(error)}. ` +
197
236
  `Check provider configuration and network connectivity.`,
@@ -204,6 +243,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
204
243
  * Generate a streaming response using unified logic
205
244
  */
206
245
  async *respondStream(params: RespondParams<TContext, TData>): AsyncGenerator<AgentResponseStreamChunk<TData>> {
246
+ // Same failed-turn rollback semantics as respond()
247
+ const preTurnSession = this.agent.session.current
248
+ ? cloneDeep(this.agent.session.current)
249
+ : undefined;
207
250
  try {
208
251
  // Use unified response preparation and routing
209
252
  const responseContext = await this.prepareUnifiedResponseContext(params);
@@ -212,6 +255,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
212
255
  yield* this.generateUnifiedStreamingResponse(responseContext);
213
256
 
214
257
  } catch (error) {
258
+ if (preTurnSession) {
259
+ this.agent.session.syncSession(preTurnSession);
260
+ }
215
261
  // Stream error to caller
216
262
  yield {
217
263
  delta: "",
@@ -246,17 +292,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
246
292
  history = this.agent.session.getHistory();
247
293
  }
248
294
 
249
- // Get or create session
250
- let session = await this.agent.session.getOrCreate();
251
-
252
- // Merge agent's collected data into session (agent data takes precedence)
253
- const collectedData = this.agent.getCollectedData();
254
- if (Object.keys(collectedData).length > 0) {
255
- session = mergeCollected(session, collectedData);
256
- // Update the session manager with the merged data
257
- await this.agent.session.setData(collectedData);
258
- logger.debug("[ResponseModal] Merged agent collected data into stream session:", collectedData);
259
- }
295
+ // Get or create session — session.data is the single source of truth,
296
+ // so no agent-side data merge is needed
297
+ const session = await this.agent.session.getOrCreate();
260
298
 
261
299
  // Stream response using existing respondStream method
262
300
  let finalMessage = "";
@@ -308,17 +346,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
308
346
  history = this.agent.session.getHistory();
309
347
  }
310
348
 
311
- // Get or create session
312
- let session = await this.agent.session.getOrCreate();
313
-
314
- // Merge agent's collected data into session (agent data takes precedence)
315
- const collectedData = this.agent.getCollectedData();
316
- if (Object.keys(collectedData).length > 0) {
317
- session = mergeCollected(session, collectedData);
318
- // Update the session manager with the merged data
319
- await this.agent.session.setData(collectedData);
320
- logger.debug("[ResponseModal] Merged agent collected data into generate session:", collectedData);
321
- }
349
+ // Get or create session — session.data is the single source of truth,
350
+ // so no agent-side data merge is needed
351
+ const session = await this.agent.session.getOrCreate();
322
352
 
323
353
  // Generate response using existing respond method
324
354
  const result = await this.respond({
@@ -370,18 +400,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
370
400
  return this.agent.tool;
371
401
  }
372
402
 
373
- /**
374
- * Post-phase signal replies replace the user-visible message after the
375
- * response has otherwise completed. Undefined means "leave it unchanged";
376
- * an empty string is an explicit replacement.
377
- */
378
- private applyPostSignalReply(
379
- message: string,
380
- directive: Directive<TContext, TData> | undefined,
381
- ): string {
382
- return directive?.reply !== undefined ? directive.reply : message;
383
- }
384
-
385
403
  /**
386
404
  * Collect scoped instructions from agent, flow, and step into a ScopedInstructions value.
387
405
  * @private
@@ -423,52 +441,53 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
423
441
  // Keep original HistoryItem[] format for external APIs
424
442
  const history = simpleHistory;
425
443
 
426
- // Use ResponsePipeline for optimized context and session preparation
427
- // This leverages existing optimizations and avoids code duplication
444
+ // Use ResponsePipeline for context and session preparation; context
445
+ // and session are passed explicitly the pipeline holds no state
428
446
  let responseContext: {
429
447
  effectiveContext: TContext;
430
448
  session: SessionState<TData>;
449
+ contextAfterHook?: TContext;
431
450
  };
432
451
  try {
433
- // Set current context and session in pipeline for consistency
434
- this.responsePipeline.setContext(await this.agent.getContext());
435
- this.responsePipeline.setCurrentSession(this.agent.currentSession);
436
-
437
452
  responseContext = await this.responsePipeline.prepareResponseContext({
438
453
  contextOverride,
439
454
  session: params.session ? cloneDeep(params.session) : undefined,
455
+ currentContext: await this.agent.getContext(),
456
+ currentSession: this.agent.currentSession,
440
457
  });
441
458
  } catch (error) {
442
459
  throw ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
443
460
  }
444
461
 
445
- const { effectiveContext } = responseContext;
462
+ const { effectiveContext, contextAfterHook } = responseContext;
446
463
  let session = responseContext.session;
447
464
 
448
- // Update our stored context if it was modified by beforeRespond hook
449
- const storedContext = this.responsePipeline.getStoredContext();
450
- if (storedContext !== undefined) {
465
+ // Sync the beforeRespond hook's context result back to the agent
466
+ if (contextAfterHook !== undefined) {
451
467
  try {
452
- await this.agent.updateContext(storedContext as Partial<TContext>);
468
+ await this.agent.updateContext(contextAfterHook as Partial<TContext>);
453
469
  } catch (error) {
454
- throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { storedContext });
470
+ throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { contextAfterHook });
455
471
  }
456
472
  }
457
473
 
458
- // Merge agent's collected data into session (agent data takes precedence)
459
- const collectedData = this.agent.getCollectedData();
460
- if (Object.keys(collectedData).length > 0) {
474
+ // Apply data staged before any session existed (initialData,
475
+ // pre-session updateCollectedData calls). Reading the live session's
476
+ // data here would leak state across sessions when an explicit
477
+ // session is passed, so only the staging buffer is merged.
478
+ const stagedData = this.agent.consumePendingData();
479
+ if (Object.keys(stagedData).length > 0) {
461
480
  try {
462
- session = mergeCollected(session, collectedData);
463
- logger.debug("[ResponseModal] Merged agent collected data into session:", collectedData);
481
+ session = mergeCollected(session, stagedData);
482
+ logger.debug("[ResponseModal] Merged staged agent data into session:", stagedData);
464
483
  } catch (error) {
465
- throw ResponseGenerationError.fromError(error, 'data_merging', params, { collectedData });
484
+ throw ResponseGenerationError.fromError(error, 'data_merging', params, { stagedData });
466
485
  }
467
486
  }
468
487
 
469
488
  // PHASE 1: PREPARE - Execute prepare function if current step has one
470
489
  try {
471
- await this.executeStepPrepare(session, effectiveContext);
490
+ await this.stepLifecycle.runPrepare(session, effectiveContext);
472
491
  } catch (error) {
473
492
  throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
474
493
  }
@@ -487,7 +506,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
487
506
  signalHaltReply?: string;
488
507
  };
489
508
  try {
490
- routingResult = await this.handleUnifiedRoutingAndStepSelection({
509
+ routingResult = await this.responsePipeline.routeAndSelectStep({
491
510
  session,
492
511
  history: historyEvents,
493
512
  context: effectiveContext,
@@ -520,564 +539,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
520
539
  }
521
540
  }
522
541
 
523
- /**
524
- * Unified routing and step selection logic using ResponsePipeline for optimization
525
- * @private
526
- */
527
- private async handleUnifiedRoutingAndStepSelection(params: {
528
- session: SessionState<TData>;
529
- history: Event[]; // Use Event[] for internal processing
530
- context: TContext;
531
- signal?: AbortSignal;
532
- }): Promise<{
533
- selectedFlow?: Flow<TContext, TData>;
534
- selectedStep?: Step<TContext, TData>;
535
- responseDirectives?: string[];
536
- session: SessionState<TData>;
537
- isFlowComplete: boolean;
538
- /** Signal firings from the pre-phase (threaded through for response surface). */
539
- signalFirings?: SignalFiring<TContext, TData>[];
540
- /** Non-position signal directive for pre-LLM augmentation (appendPrompt, injectTools, etc). */
541
- signalPreDirective?: Directive<TContext, TData>;
542
- /** Pre-signal phase halted the turn. */
543
- signalHalted?: boolean;
544
- /** Reply text from the halt directive. */
545
- signalHaltReply?: string;
546
- }> {
547
- try {
548
- // Create a fresh chain tracker for this turn (Requirement 22.1)
549
- this.responsePipeline.createChainTracker();
550
-
551
- // ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
552
- // When the current step has collect fields AND pre-extraction populates at least
553
- // one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
554
- const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
555
- if (routingSkipResult) {
556
- // Even when routing is skipped, run pre-signal phase if processor is present
557
- if (this.agent.signalProcessor) {
558
- const signalResult = await this.responsePipeline.runPreSignalPhase(
559
- params.session, params.context, params.history,
560
- );
561
- // If signal halts, override the routing skip result
562
- if (signalResult.mergedDirective?.halt) {
563
- return {
564
- ...routingSkipResult,
565
- session: signalResult.updatedSession,
566
- signalFirings: signalResult.firings,
567
- signalHalted: true,
568
- signalHaltReply: signalResult.mergedDirective.reply,
569
- };
570
- }
571
- // If signal has position fields, override routing skip result
572
- if (hasDirectivePositionField(signalResult.mergedDirective)) {
573
- return this.applySignalPositionDirective(
574
- signalResult, params,
575
- );
576
- }
577
- // Non-position directive: propagate for pre-LLM augmentation
578
- return {
579
- ...routingSkipResult,
580
- session: signalResult.updatedSession,
581
- signalFirings: signalResult.firings,
582
- signalPreDirective: signalResult.mergedDirective || undefined,
583
- };
584
- }
585
- return routingSkipResult;
586
- }
587
-
588
- // ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
589
- // When signalProcessor is present, run pre-signals in parallel with routing.
590
- // When absent, call the router directly (zero overhead, preserve current behavior).
591
- if (this.agent.signalProcessor) {
592
- // Run pre-signal phase in parallel with routing (Requirement 8.1)
593
- const [signalResult, routingResult] = await Promise.all([
594
- this.responsePipeline.runPreSignalPhase(
595
- params.session, params.context, params.history,
596
- ),
597
- this.responsePipeline.handleRoutingAndStepSelection({
598
- session: params.session,
599
- history: params.history,
600
- context: params.context,
601
- signal: params.signal,
602
- }),
603
- ]);
604
-
605
- // ── Requirement 8.2: halt → discard routing, skip LLM ────────────
606
- if (signalResult.mergedDirective?.halt) {
607
- return {
608
- selectedFlow: undefined,
609
- selectedStep: undefined,
610
- session: signalResult.updatedSession,
611
- isFlowComplete: false,
612
- signalFirings: signalResult.firings,
613
- signalHalted: true,
614
- signalHaltReply: signalResult.mergedDirective.reply,
615
- };
616
- }
617
-
618
- // ── Requirement 8.3: position directive → discard routing, apply signal position ──
619
- if (hasDirectivePositionField(signalResult.mergedDirective)) {
620
- return this.applySignalPositionDirective(
621
- signalResult, params,
622
- );
623
- }
624
-
625
- // ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
626
- // ── Requirement 8.5: no directive → use routing as-is ─────────────
627
- let updatedSession = signalResult.updatedSession;
628
-
629
- // Apply data/context updates from signal to the routed session
630
- if (signalResult.mergedDirective?.dataUpdate) {
631
- updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
632
- }
633
-
634
- // Use routing result for flow/step, but carry signal session state
635
- // Merge routing session changes on top of signal session
636
- const routingSession = routingResult.session;
637
- updatedSession = {
638
- ...updatedSession,
639
- currentFlow: routingSession.currentFlow,
640
- currentStep: routingSession.currentStep,
641
- flowHistory: routingSession.flowHistory,
642
- pendingDirective: routingSession.pendingDirective,
643
- };
644
-
645
- const isFlowComplete = routingResult.isFlowComplete;
646
-
647
- // PRE-EXTRACTION: same logic as below — extract data from user message
648
- if (routingResult.selectedFlow && !isFlowComplete) {
649
- if (this.shouldPreExtractData(routingResult.selectedFlow)) {
650
- logger.debug(
651
- `[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
652
- );
653
- const extractedData = await this.preExtractFlowData({
654
- route: routingResult.selectedFlow,
655
- history: params.history,
656
- context: params.context,
657
- session: updatedSession,
658
- signal: params.signal,
659
- });
660
- if (extractedData && Object.keys(extractedData).length > 0) {
661
- logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
662
- updatedSession = mergeCollected(updatedSession, extractedData);
663
- await this.agent.updateCollectedData(extractedData);
664
- }
665
- }
666
- }
667
-
668
- // Determine next step
669
- const stepResult = await this.responsePipeline.determineNextStep({
670
- selectedFlow: routingResult.selectedFlow,
671
- selectedStep: routingResult.selectedStep,
672
- session: updatedSession,
673
- isFlowComplete,
674
- });
675
-
676
- return {
677
- selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
678
- selectedStep: stepResult.nextStep,
679
- responseDirectives: routingResult.responseDirectives,
680
- session: stepResult.session,
681
- isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
682
- signalFirings: signalResult.firings,
683
- signalPreDirective: signalResult.mergedDirective || undefined,
684
- };
685
- }
686
-
687
- // ── No signal processor: existing behavior (zero overhead) ────────────
688
- const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
689
- session: params.session,
690
- history: params.history,
691
- context: params.context,
692
- signal: params.signal,
693
- });
694
-
695
- let updatedSession = routingResult.session;
696
- const isFlowComplete = routingResult.isFlowComplete;
697
-
698
- // PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
699
- // This allows us to skip steps whose data is already provided
700
- if (routingResult.selectedFlow && !isFlowComplete) {
701
- // Always pre-extract when flow collects data (not just on new flow entry)
702
- // This ensures step selection has the most up-to-date data
703
- if (this.shouldPreExtractData(routingResult.selectedFlow)) {
704
- logger.debug(
705
- `[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
706
- );
707
-
708
- const extractedData = await this.preExtractFlowData({
709
- route: routingResult.selectedFlow,
710
- history: params.history,
711
- context: params.context,
712
- session: updatedSession,
713
- signal: params.signal,
714
- });
715
-
716
- if (extractedData && Object.keys(extractedData).length > 0) {
717
- logger.debug(
718
- `[ResponseModal] Pre-extracted data:`,
719
- extractedData
720
- );
721
- // Merge pre-extracted data into session before step selection
722
- updatedSession = mergeCollected(updatedSession, extractedData);
723
- // Also update agent's collected data
724
- await this.agent.updateCollectedData(extractedData);
725
- }
726
- }
727
- }
728
-
729
- // Determine next step using pipeline method for consistency
730
- const stepResult = await this.responsePipeline.determineNextStep({
731
- selectedFlow: routingResult.selectedFlow,
732
- selectedStep: routingResult.selectedStep,
733
- session: updatedSession, // Use updated session with pre-extracted data
734
- isFlowComplete, // Use updated completion status
735
- });
736
-
737
- return {
738
- selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
739
- selectedStep: stepResult.nextStep, // Use the determined next step
740
- responseDirectives: routingResult.responseDirectives,
741
- session: stepResult.session,
742
- // If a branch changed the flow, the original isFlowComplete no longer applies
743
- isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
744
- };
745
- } catch (error) {
746
- throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
747
- }
748
- }
749
-
750
- /**
751
- * Apply a signal's position directive (goTo, goToStep, complete, abort, reset).
752
- * Discards routing result and uses the signal's position decision.
753
- * @private
754
- * @requirements 8.3
755
- */
756
- private applySignalPositionDirective(
757
- signalResult: {
758
- firings: SignalFiring<TContext, TData>[];
759
- updatedSession: SessionState<TData>;
760
- mergedDirective: Directive<TContext, TData> | undefined;
761
- },
762
- _params: { session: SessionState<TData>; history: Event[]; context: TContext },
763
- ): {
764
- selectedFlow?: Flow<TContext, TData>;
765
- selectedStep?: Step<TContext, TData>;
766
- responseDirectives?: string[];
767
- session: SessionState<TData>;
768
- isFlowComplete: boolean;
769
- signalFirings?: SignalFiring<TContext, TData>[];
770
- signalPreDirective?: Directive<TContext, TData>;
771
- signalHalted?: boolean;
772
- signalHaltReply?: string;
773
- } {
774
- const directive = signalResult.mergedDirective!;
775
- let session = signalResult.updatedSession;
776
- const flows = this.agent.getFlows();
777
- let selectedFlow: Flow<TContext, TData> | undefined;
778
- let selectedStep: Step<TContext, TData> | undefined;
779
- let isFlowComplete = false;
780
-
781
- // Apply data updates if present alongside position
782
- if (directive.dataUpdate) {
783
- session = mergeCollected(session, directive.dataUpdate);
784
- }
785
-
786
- if (directive.goTo) {
787
- const flowTarget = typeof directive.goTo === 'string'
788
- ? directive.goTo
789
- : directive.goTo.flow ?? directive.goTo.step;
790
-
791
- if (flowTarget) {
792
- const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
793
- if (targetFlow) {
794
- session = enterFlow(session, targetFlow.id, targetFlow.title);
795
- selectedFlow = targetFlow;
796
-
797
- if (typeof directive.goTo === 'object' && directive.goTo.step) {
798
- const targetStep = targetFlow.getStep(directive.goTo.step);
799
- if (targetStep) {
800
- session = enterStep(session, targetStep.id, targetStep.description);
801
- selectedStep = targetStep;
802
- }
803
- }
804
- } else {
805
- logger.warn(`[Signals] Pre-phase goTo target not found: "${flowTarget}". Falling back to no flow.`);
806
- }
807
- }
808
- } else if (directive.goToStep) {
809
- const stepTarget = typeof directive.goToStep === 'string'
810
- ? directive.goToStep
811
- : directive.goToStep.step;
812
- const flowTarget = typeof directive.goToStep === 'object'
813
- ? directive.goToStep.flow
814
- : undefined;
815
-
816
- if (flowTarget) {
817
- const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
818
- if (targetFlow) {
819
- session = enterFlow(session, targetFlow.id, targetFlow.title);
820
- selectedFlow = targetFlow;
821
- const targetStep = targetFlow.getStep(stepTarget);
822
- if (targetStep) {
823
- session = enterStep(session, targetStep.id, targetStep.description);
824
- selectedStep = targetStep;
825
- }
826
- }
827
- } else if (session.currentFlow) {
828
- const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
829
- if (currentFlow) {
830
- selectedFlow = currentFlow;
831
- const targetStep = currentFlow.getStep(stepTarget);
832
- if (targetStep) {
833
- session = enterStep(session, targetStep.id, targetStep.description);
834
- selectedStep = targetStep;
835
- }
836
- }
837
- }
838
- } else if (directive.complete) {
839
- isFlowComplete = true;
840
- } else if (directive.abort) {
841
- // Abort — no flow, session cleared or marked
842
- isFlowComplete = true;
843
- } else if (directive.reset) {
844
- if (session.currentFlow) {
845
- const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
846
- if (currentFlow) {
847
- selectedFlow = currentFlow;
848
- const resetStep = typeof directive.reset === 'object' && directive.reset.step
849
- ? directive.reset.step
850
- : undefined;
851
- if (resetStep) {
852
- const targetStep = currentFlow.getStep(resetStep);
853
- if (targetStep) {
854
- session = enterStep(session, targetStep.id, targetStep.description);
855
- selectedStep = targetStep;
856
- }
857
- } else {
858
- const initialStep = currentFlow.initialStep;
859
- session = enterStep(session, initialStep.id, initialStep.description);
860
- selectedStep = initialStep;
861
- }
862
- }
863
- }
864
- }
865
-
866
- return {
867
- selectedFlow,
868
- selectedStep,
869
- session,
870
- isFlowComplete,
871
- signalFirings: signalResult.firings,
872
- signalPreDirective: signalResult.mergedDirective || undefined,
873
- };
874
- }
875
-
876
- /**
877
- * Routing skip optimization (Requirements 20.1, 20.2, 20.3):
878
- * When the current step declares `collect` fields AND pre-extraction populates
879
- * at least one of those fields from the user's message, skip routing for this turn.
880
- *
881
- * Returns the routing result if the skip applies, or undefined to fall through
882
- * to normal routing.
883
- * @private
884
- */
885
- private async attemptRoutingSkipForCollect(params: {
886
- session: SessionState<TData>;
887
- history: Event[];
888
- context: TContext;
889
- signal?: AbortSignal;
890
- }): Promise<{
891
- selectedFlow?: Flow<TContext, TData>;
892
- selectedStep?: Step<TContext, TData>;
893
- responseDirectives?: string[];
894
- session: SessionState<TData>;
895
- isFlowComplete: boolean;
896
- } | undefined> {
897
- const { session } = params;
898
-
899
- // Only applies when we already have a current flow and step
900
- if (!session.currentFlow || !session.currentStep) {
901
- return undefined;
902
- }
903
-
904
- // Also skip this optimization if there's a pending directive (it takes priority)
905
- if (session.pendingDirective) {
906
- return undefined;
907
- }
908
-
909
- // Look up the actual Flow and Step objects to access `collect`
910
- const currentFlow = this.agent.getFlows().find(
911
- (f) => f.id === session.currentFlow?.id
912
- );
913
- if (!currentFlow) {
914
- return undefined;
915
- }
916
-
917
- const currentStep = currentFlow.getStep(session.currentStep.id);
918
- if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
919
- return undefined;
920
- }
921
-
922
- // We have a step with collect fields. Run pre-extraction to see if the
923
- // user's message populates any of them.
924
- const collectFields = currentStep.collect;
925
-
926
- // Snapshot current data for comparison
927
- const dataBefore = { ...session.data };
928
-
929
- // Run pre-extraction against the current flow
930
- const extractedData = await this.preExtractFlowData({
931
- route: currentFlow,
932
- history: params.history,
933
- context: params.context,
934
- session,
935
- signal: params.signal,
936
- });
937
-
938
- if (!extractedData || Object.keys(extractedData).length === 0) {
939
- return undefined;
940
- }
941
-
942
- // Determine which collect fields were newly populated by pre-extraction
943
- const populatedCollectFields: string[] = [];
944
- for (const field of collectFields) {
945
- const key = field as string;
946
- const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
947
- const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
948
- if (hasNewValue && !hadValue) {
949
- populatedCollectFields.push(key);
950
- }
951
- }
952
-
953
- if (populatedCollectFields.length === 0) {
954
- // Pre-extraction didn't populate any declared collect field — no skip
955
- return undefined;
956
- }
957
-
958
- // ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
959
- logger.debug(
960
- `[ResponseModal] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`
961
- );
962
-
963
- // Merge extracted data into session
964
- const updatedSession = mergeCollected(session, extractedData);
965
- await this.agent.updateCollectedData(extractedData);
966
-
967
- // Determine next step using pipeline method for consistency
968
- // Pass the current flow/step as the routing result (retained)
969
- const stepResult = await this.responsePipeline.determineNextStep({
970
- selectedFlow: currentFlow,
971
- selectedStep: currentStep,
972
- session: updatedSession,
973
- isFlowComplete: false,
974
- });
975
-
976
- return {
977
- selectedFlow: stepResult.flowChanged || currentFlow,
978
- selectedStep: stepResult.nextStep,
979
- responseDirectives: undefined,
980
- session: stepResult.session,
981
- isFlowComplete: stepResult.flowChanged ? false : false,
982
- };
983
- }
984
-
985
- /**
986
- * Check if a flow should pre-extract data before determining the initial step
987
- * @private
988
- */
989
- private shouldPreExtractData(flow: Flow<TContext, TData>): boolean {
990
- // Pre-extract if flow has declared required or optional fields
991
- if (flow.requiredFields && flow.requiredFields.length > 0) {
992
- return true;
993
- }
994
- if (flow.optionalFields && flow.optionalFields.length > 0) {
995
- return true;
996
- }
997
-
998
- // Pre-extract if any step in the flow collects data
999
- const steps = flow.getAllSteps();
1000
- const hasDataCollectionSteps = steps.some(
1001
- step => step.collect && step.collect.length > 0
1002
- );
1003
-
1004
- return hasDataCollectionSteps;
1005
- }
1006
-
1007
- /**
1008
- * Pre-extract data from user message when entering a flow
1009
- * This allows skipping steps whose data is already provided
1010
- * @private
1011
- */
1012
- private async preExtractFlowData(params: {
1013
- route: Flow<TContext, TData>;
1014
- history: Event[];
1015
- context: TContext;
1016
- session: SessionState<TData>;
1017
- signal?: AbortSignal;
1018
- }): Promise<Partial<TData>> {
1019
- const { route: flow, history, signal } = params;
1020
-
1021
- // Build a schema for data extraction based on flow's fields
1022
- const extractionSchema = this.agent.schema;
1023
- if (!extractionSchema) {
1024
- logger.warn(`[ResponseModal] No schema available for pre-extraction`);
1025
- return {};
1026
- }
1027
-
1028
- // Get last user message
1029
- const lastMessage = getLastMessageFromHistory(history);
1030
-
1031
- // Build extraction prompt
1032
- const extractionPrompt = [
1033
- `Extract any relevant information from the user's message that matches the following data fields.`,
1034
- `Only extract information that is explicitly stated or clearly implied.`,
1035
- ``,
1036
- `User's message: "${lastMessage}"`,
1037
- ``,
1038
- `Extract data for these fields if present:`,
1039
- ];
1040
-
1041
- // Add field descriptions
1042
- if (flow.requiredFields) {
1043
- extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
1044
- }
1045
- if (flow.optionalFields) {
1046
- extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
1047
- }
1048
-
1049
- extractionPrompt.push(
1050
- ``,
1051
- `Return ONLY the extracted data as JSON. If no data can be extracted, return an empty object {}.`
1052
- );
1053
-
1054
- // Convert Event[] to HistoryItem[] for provider call
1055
- const historyItems = eventsToHistory(history);
1056
-
1057
- // Call AI to extract data
1058
- const agentOptions = this.agent.getAgentOptions();
1059
- try {
1060
- const result = await agentOptions.provider.generateMessage<TContext, Partial<TData>>({
1061
- prompt: extractionPrompt.join('\n'),
1062
- history: historyItems,
1063
- context: {} as TContext, // Passed as empty object so AI doesn't "extract" from context
1064
- // NOTE: context is intentionally NOT passed here.
1065
- // Passing context caused the AI to "extract" data from the lead's context
1066
- // (e.g., name, sector, city) instead of from what the user actually said.
1067
- signal,
1068
- parameters: {
1069
- jsonSchema: extractionSchema,
1070
- schemaName: 'data_extraction',
1071
- },
1072
- });
1073
-
1074
- return result.structured || {};
1075
- } catch (error) {
1076
- logger.error(`[ResponseModal] Pre-extraction failed:`, error);
1077
- return {};
1078
- }
1079
- }
1080
-
1081
542
  /**
1082
543
  * Unified response generation for non-streaming responses
1083
544
  * @private
@@ -1113,20 +574,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1113
574
  if (signalHalted) {
1114
575
  const haltMessage = signalHaltReply || '';
1115
576
  // Run post-signal phase even on halt (post-phase sees complete turn context)
1116
- const postResult = await this.responsePipeline.runPostSignalPhase(
1117
- session, effectiveContext, historyEvents,
1118
- );
1119
- session = postResult.updatedSession;
1120
- signalFirings.push(...postResult.firings);
1121
-
1122
- // Apply post-phase position directive as pendingDirective (Requirement 9.3)
1123
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1124
- session = { ...session, pendingDirective: postResult.mergedDirective };
1125
- }
1126
-
1127
- const message = this.applyPostSignalReply(haltMessage, postResult.mergedDirective);
577
+ const post = await this.signalCoordinator.applyPostPhase({
578
+ session, context: effectiveContext, historyEvents, message: haltMessage,
579
+ });
580
+ session = post.session;
581
+ signalFirings.push(...post.firings);
582
+ const message = post.message;
1128
583
 
1129
- await this.finalizeSession(session, effectiveContext);
1130
584
  return {
1131
585
  message,
1132
586
  session,
@@ -1134,7 +588,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1134
588
  isFlowComplete: false,
1135
589
  executedSteps: [],
1136
590
  stoppedReason: haltMessage ? 'reply' : 'halt',
1137
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
591
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1138
592
  };
1139
593
  }
1140
594
 
@@ -1144,8 +598,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1144
598
  let stoppedReason: StoppedReason | undefined;
1145
599
  let appliedInstructions: AppliedInstruction[] | undefined;
1146
600
 
1147
-
1148
-
1149
601
  if (selectedFlow && !isFlowComplete) {
1150
602
  // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
1151
603
  // If the current step is auto, the executor advances through it (and any
@@ -1167,13 +619,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1167
619
 
1168
620
  session = autoResult.session;
1169
621
 
1170
- // Handle halt: emit verbatim reply, persist, return — no LLM call.
622
+ // Handle halt: emit verbatim reply, return — no LLM call.
623
+ // respond() finalizes the returned session exactly once.
1171
624
  if (autoResult.stoppedReason === 'halt') {
1172
625
  message = autoResult.mergedDirective?.reply || '';
1173
626
  stoppedReason = 'halt';
1174
627
  executedSteps = [];
1175
628
 
1176
- await this.finalizeSession(session, effectiveContext);
1177
629
  return {
1178
630
  message,
1179
631
  session,
@@ -1197,7 +649,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1197
649
  history,
1198
650
  });
1199
651
 
1200
- await this.finalizeSession(session, effectiveContext);
1201
652
  return {
1202
653
  message: '',
1203
654
  session,
@@ -1286,22 +737,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1286
737
  // Runs after finalize/onComplete and before session persistence.
1287
738
  // Post-phase signals see the complete turn result: assistant message in
1288
739
  // history, collected data, tool results.
1289
- const postResult = await this.responsePipeline.runPostSignalPhase(
1290
- session, effectiveContext, historyEvents,
1291
- );
1292
- session = postResult.updatedSession;
1293
-
740
+ const post = await this.signalCoordinator.applyPostPhase({
741
+ session, context: effectiveContext, historyEvents, message,
742
+ });
743
+ session = post.session;
1294
744
  // Append post-phase firings to the accumulator (preserves fire order)
1295
- signalFirings.push(...postResult.firings);
1296
-
1297
- // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1298
- // (no mid-turn re-entry per D6 decision). Pre-LLM-only fields are already
1299
- // dropped inside runPostSignalPhase per Phase 4.5.
1300
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1301
- session = { ...session, pendingDirective: postResult.mergedDirective };
1302
- }
1303
-
1304
- message = this.applyPostSignalReply(message, postResult.mergedDirective);
745
+ signalFirings.push(...post.firings);
746
+ message = post.message;
1305
747
 
1306
748
  // Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
1307
749
  // - executedSteps: array of steps executed (empty array if none)
@@ -1315,67 +757,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1315
757
  executedSteps: executedSteps || [],
1316
758
  stoppedReason,
1317
759
  appliedInstructions,
1318
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
760
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1319
761
  };
1320
762
  }
1321
763
 
1322
- /**
1323
- * Execute prepare function for current step if available
1324
- * @private
1325
- */
1326
- private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
1327
- if (session.currentFlow && session.currentStep) {
1328
- const currentFlow = this.agent.getFlows().find(
1329
- (r) => r.id === session.currentFlow?.id
1330
- );
1331
- if (currentFlow) {
1332
- const currentStep = currentFlow.getStep(session.currentStep.id);
1333
- // Skip auto-steps — their prepare is handled by AutoChainExecutor
1334
- if (currentStep?.auto) {
1335
- logger.debug(`[ResponseModal] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
1336
- return;
1337
- }
1338
- if (currentStep?.prepare) {
1339
- logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
1340
- await this.executePrepareFinalize(
1341
- currentStep.prepare,
1342
- context,
1343
- session.data,
1344
- currentFlow,
1345
- currentStep
1346
- );
1347
- }
1348
- }
1349
- }
1350
- }
1351
-
1352
- /**
1353
- * Execute finalize function for current step if available
1354
- * @private
1355
- */
1356
- private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
1357
- if (session.currentFlow && session.currentStep) {
1358
- const currentFlow = this.agent.getFlows().find(
1359
- (r) => r.id === session.currentFlow?.id
1360
- );
1361
- if (currentFlow) {
1362
- const currentStep = currentFlow.getStep(session.currentStep.id);
1363
- if (currentStep?.finalize) {
1364
- logger.debug(
1365
- `[ResponseModal] Executing finalize for step: ${currentStep.id}`
1366
- );
1367
- await this.executePrepareFinalize(
1368
- currentStep.finalize,
1369
- context,
1370
- session.data,
1371
- currentFlow,
1372
- currentStep
1373
- );
1374
- }
1375
- }
1376
- }
1377
- }
1378
-
1379
764
  /**
1380
765
  * Process flow response with unified tool execution and data collection
1381
766
  * @private
@@ -1409,87 +794,20 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1409
794
  const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1410
795
  let session = params.session;
1411
796
 
1412
- // Determine next step
1413
- let nextStep: Step<TContext, TData>;
1414
- if (selectedStep) {
1415
- nextStep = selectedStep;
1416
- } else {
1417
- // Determine current step from session if we're already in this flow
1418
- const isInSameFlow = session.currentFlow?.id === selectedFlow.id;
1419
- const currentStep = isInSameFlow && session.currentStep
1420
- ? selectedFlow.getStep(session.currentStep.id)
1421
- : undefined;
1422
-
1423
- logger.debug(`[ResponseModal] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
1424
-
1425
- // STEP 1 (Algorithm 1): branches win over linear chain
1426
- if (currentStep?.branches && currentStep.branches.length > 0) {
1427
- const branchResult = await this.responsePipeline.evaluateStepBranches(
1428
- currentStep, selectedFlow, session, context
1429
- );
1430
- if (branchResult) {
1431
- if (branchResult.nextStep) {
1432
- nextStep = branchResult.nextStep;
1433
- session = branchResult.session;
1434
- } else {
1435
- // Flow transition or completion — no local step to render
1436
- // Return empty message with updated session; caller handles flow transition
1437
- return { message: '', session: branchResult.session };
1438
- }
1439
- }
1440
- }
1441
-
1442
- if (!nextStep!) {
1443
- // Get candidate steps based on current position in the flow
1444
- const flowRouter = this.agent.getFlowRouter();
1445
- const candidates = await flowRouter.getCandidateStepsWithConditions(
1446
- selectedFlow,
1447
- currentStep, // Pass current step instead of undefined to maintain progression
1448
- createTemplateContext({ data: session.data, session, context })
1449
- );
1450
-
1451
- logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
1452
-
1453
- if (candidates.length > 0) {
1454
- nextStep = candidates[0].step;
1455
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1456
- } else {
1457
- // Fallback to initial step even if it should be skipped
1458
- nextStep = selectedFlow.initialStep;
1459
- logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1460
- }
1461
- }
1462
- }
1463
-
1464
- // Update session with next step
1465
- // If the next step has requires fields that are missing, stay at the previous step
1466
- if (nextStep.requires && nextStep.requires.length > 0) {
1467
- const sessionData = session.data || {};
1468
- const missingRequires = nextStep.requires.filter(
1469
- field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1470
- );
1471
- if (missingRequires.length > 0) {
1472
- const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1473
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1474
- logger.warn(warning);
1475
- console.warn(warning);
1476
- // Stay at the current step - don't enter the next one
1477
- const currentStepId = session.currentStep?.id;
1478
- if (currentStepId) {
1479
- const currentStepInstance = selectedFlow.getStep(currentStepId);
1480
- if (currentStepInstance) {
1481
- nextStep = currentStepInstance;
1482
- logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
1483
- }
1484
- }
1485
- } else {
1486
- session = enterStep(session, nextStep.id, nextStep.description);
1487
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1488
- }
1489
- } else {
1490
- session = enterStep(session, nextStep.id, nextStep.description);
1491
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
797
+ // Resolve the step to render (branches win over linear chain; requires enforced)
798
+ const stepResolution = await this.responsePipeline.resolveRenderStep({
799
+ selectedFlow,
800
+ selectedStep,
801
+ session,
802
+ context,
803
+ });
804
+ if (stepResolution.flowTransition) {
805
+ // Flow transition or completion — no local step to render
806
+ // Return empty message with updated session; caller handles flow transition
807
+ return { message: '', session: stepResolution.session };
1492
808
  }
809
+ const nextStep = stepResolution.nextStep!;
810
+ session = stepResolution.session;
1493
811
 
1494
812
  // Build response schema for this flow (with collect fields from step)
1495
813
  const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
@@ -1573,7 +891,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1573
891
  });
1574
892
 
1575
893
  // Execute tools with unified loop handling
1576
- const toolResult = await this.executeUnifiedToolLoop({
894
+ const toolResult = await this.toolLoopExecutor.runLoop({
1577
895
  toolCalls,
1578
896
  context,
1579
897
  session,
@@ -1640,19 +958,14 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1640
958
  if (signalHalted) {
1641
959
  const haltMessage = signalHaltReply || '';
1642
960
  // Run post-signal phase even on halt
1643
- const postResult = await this.responsePipeline.runPostSignalPhase(
1644
- session, effectiveContext, historyEvents,
1645
- );
1646
- session = postResult.updatedSession;
1647
- signalFirings.push(...postResult.firings);
1648
-
1649
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1650
- session = { ...session, pendingDirective: postResult.mergedDirective };
1651
- }
1652
-
1653
- const message = this.applyPostSignalReply(haltMessage, postResult.mergedDirective);
961
+ const post = await this.signalCoordinator.applyPostPhase({
962
+ session, context: effectiveContext, historyEvents, message: haltMessage,
963
+ });
964
+ session = post.session;
965
+ signalFirings.push(...post.firings);
966
+ const message = post.message;
1654
967
 
1655
- await this.finalizeSession(session, effectiveContext);
968
+ await this.sessionFinalizer.finalize(session, effectiveContext);
1656
969
  yield {
1657
970
  delta: message,
1658
971
  accumulated: message,
@@ -1660,7 +973,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1660
973
  session,
1661
974
  stoppedReason: haltMessage ? 'reply' : 'halt',
1662
975
  executedSteps: [],
1663
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
976
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1664
977
  } as AgentResponseStreamChunk<TData>;
1665
978
  return;
1666
979
  }
@@ -1690,7 +1003,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1690
1003
  // Handle halt: emit verbatim reply as a single chunk, done.
1691
1004
  if (autoResult.stoppedReason === 'halt') {
1692
1005
  const reply = autoResult.mergedDirective?.reply || '';
1693
- await this.finalizeSession(session, effectiveContext);
1006
+ await this.sessionFinalizer.finalize(session, effectiveContext);
1694
1007
  yield {
1695
1008
  delta: reply,
1696
1009
  accumulated: reply,
@@ -1698,7 +1011,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1698
1011
  session,
1699
1012
  stoppedReason: 'halt',
1700
1013
  executedSteps: [],
1701
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1014
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1702
1015
  } as AgentResponseStreamChunk<TData>;
1703
1016
  return;
1704
1017
  }
@@ -1767,35 +1080,35 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1767
1080
  });
1768
1081
  }
1769
1082
 
1770
- // ── Intercept the inner stream to run post-signal phase on the final chunk ──
1771
- // This mirrors the non-streaming path: post-phase runs after finalize/onComplete
1772
- // and before session persistence, attaching triggeredSignals to the final chunk
1773
- // (Requirement 11.2).
1083
+ // ── Intercept the inner stream on the final chunk ──────────────────────
1084
+ // Mirrors the non-streaming path: post-signal phase runs first, then the
1085
+ // session (including post-phase mutations) is finalized exactly once,
1086
+ // attaching triggeredSignals to the final chunk (Requirement 11.2).
1774
1087
  for await (const chunk of innerStream!) {
1775
1088
  if (chunk.done) {
1776
1089
  // Run post-signal phase on final chunk (Requirement 9.1, 9.2)
1777
- const postResult = await this.responsePipeline.runPostSignalPhase(
1778
- chunk.session || session, effectiveContext, historyEvents,
1779
- );
1780
- let finalSession = postResult.updatedSession;
1781
- signalFirings.push(...postResult.firings);
1090
+ const post = await this.signalCoordinator.applyPostPhase({
1091
+ session: chunk.session || session,
1092
+ context: effectiveContext,
1093
+ historyEvents,
1094
+ message: chunk.accumulated,
1095
+ });
1096
+ const finalSession = post.session;
1097
+ signalFirings.push(...post.firings);
1782
1098
 
1783
- // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1784
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1785
- finalSession = { ...finalSession, pendingDirective: postResult.mergedDirective };
1786
- }
1099
+ const accumulated = post.message;
1100
+ const delta = post.replyOverridden ? accumulated : chunk.delta;
1787
1101
 
1788
- const accumulated = this.applyPostSignalReply(chunk.accumulated, postResult.mergedDirective);
1789
- const delta = postResult.mergedDirective?.reply !== undefined
1790
- ? accumulated
1791
- : chunk.delta;
1102
+ // Single streaming exit: finalize the post-phase session so
1103
+ // post-signal mutations (e.g. pendingDirective) are persisted
1104
+ await this.sessionFinalizer.finalize(finalSession, effectiveContext);
1792
1105
 
1793
1106
  yield {
1794
1107
  ...chunk,
1795
1108
  delta,
1796
1109
  accumulated,
1797
1110
  session: finalSession,
1798
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1111
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1799
1112
  } as AgentResponseStreamChunk<TData>;
1800
1113
  } else {
1801
1114
  yield chunk;
@@ -1830,86 +1143,25 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1830
1143
  const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1831
1144
  let session = params.session;
1832
1145
 
1833
- // Determine next step (same logic as non-streaming)
1834
- let nextStep: Step<TContext, TData>;
1835
- if (selectedStep) {
1836
- nextStep = selectedStep;
1837
- } else {
1838
- // Determine current step from session if we're already in this flow
1839
- const currentStep = session.currentFlow?.id === selectedFlow.id && session.currentStep
1840
- ? selectedFlow.getStep(session.currentStep.id)
1841
- : undefined;
1842
-
1843
- // STEP 1 (Algorithm 1): branches win over linear chain
1844
- if (currentStep?.branches && currentStep.branches.length > 0) {
1845
- const branchResult = await this.responsePipeline.evaluateStepBranches(
1846
- currentStep, selectedFlow, session, context
1847
- );
1848
- if (branchResult) {
1849
- // Branch resolved — yield a final chunk with the updated session and return
1850
- if (branchResult.nextStep) {
1851
- session = branchResult.session;
1852
- nextStep = branchResult.nextStep;
1853
- } else {
1854
- // Flow transition or completion — no step to render
1855
- yield {
1856
- delta: '',
1857
- accumulated: '',
1858
- done: true,
1859
- session: branchResult.session,
1860
- } as AgentResponseStreamChunk<TData>;
1861
- return;
1862
- }
1863
- }
1864
- }
1865
-
1866
- if (!nextStep!) {
1867
- // Get candidate steps based on current position in the flow
1868
- const flowRouter = this.agent.getFlowRouter();
1869
- const candidates = await flowRouter.getCandidateStepsWithConditions(
1870
- selectedFlow,
1871
- currentStep, // Pass current step instead of undefined to maintain progression
1872
- createTemplateContext({ data: session.data, session, context })
1873
- );
1874
-
1875
- if (candidates.length > 0) {
1876
- nextStep = candidates[0].step;
1877
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1878
- } else {
1879
- nextStep = selectedFlow.initialStep;
1880
- logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1881
- }
1882
- }
1883
- }
1884
-
1885
- // Update session with next step
1886
- // If the next step has requires fields that are missing, stay at the previous step
1887
- if (nextStep.requires && nextStep.requires.length > 0) {
1888
- const sessionData = session.data || {};
1889
- const missingRequires = nextStep.requires.filter(
1890
- field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1891
- );
1892
- if (missingRequires.length > 0) {
1893
- const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1894
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1895
- logger.warn(warning);
1896
- console.warn(warning);
1897
- const currentStepId = session.currentStep?.id;
1898
- if (currentStepId) {
1899
- const currentStepInstance = selectedFlow.getStep(currentStepId);
1900
- if (currentStepInstance) {
1901
- nextStep = currentStepInstance;
1902
- logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
1903
- }
1904
- }
1905
- } else {
1906
- session = enterStep(session, nextStep.id, nextStep.description);
1907
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1908
- }
1909
- } else {
1910
- session = enterStep(session, nextStep.id, nextStep.description);
1911
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1146
+ // Resolve the step to render (same logic as non-streaming)
1147
+ const stepResolution = await this.responsePipeline.resolveRenderStep({
1148
+ selectedFlow,
1149
+ selectedStep,
1150
+ session,
1151
+ context,
1152
+ });
1153
+ if (stepResolution.flowTransition) {
1154
+ // Flow transition or completion — no step to render
1155
+ yield {
1156
+ delta: '',
1157
+ accumulated: '',
1158
+ done: true,
1159
+ session: stepResolution.session,
1160
+ } as AgentResponseStreamChunk<TData>;
1161
+ return;
1912
1162
  }
1163
+ const nextStep = stepResolution.nextStep!;
1164
+ session = stepResolution.session;
1913
1165
 
1914
1166
  // Build response schema and prompt (same as non-streaming)
1915
1167
  const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
@@ -1921,7 +1173,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1921
1173
  const reply = mergedPreDirective.reply || '';
1922
1174
  const reason: StoppedReason = mergedPreDirective.reply ? 'reply' : 'halt';
1923
1175
  logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
1924
- await this.finalizeSession(session, context);
1925
1176
  yield {
1926
1177
  delta: reply,
1927
1178
  accumulated: reply,
@@ -1943,13 +1194,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1943
1194
  createTemplateContext({ data: session.data || {}, context, session })
1944
1195
  );
1945
1196
  logger.debug(`[ResponseModal] Step.reply (streaming) — skipping LLM call for step ${nextStep.id}`);
1946
- await this.finalizeSession(session, context);
1947
1197
  yield {
1948
1198
  delta: effectiveReply,
1949
1199
  accumulated: effectiveReply,
1950
1200
  done: true,
1951
1201
  session,
1952
- stoppedReason: 'reply' as StoppedReason,
1202
+ stoppedReason: 'reply',
1953
1203
  executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
1954
1204
  } as AgentResponseStreamChunk<TData>;
1955
1205
  return;
@@ -1998,73 +1248,23 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1998
1248
  if (chunk.done && chunk.structured?.toolCalls) {
1999
1249
  toolCalls = chunk.structured.toolCalls;
2000
1250
 
2001
- const toolManager = this.getToolManager();
2002
-
2003
- // Use concurrent execution for the initial batch of tool calls
2004
- {
2005
- const toolCallRequests: ToolCallRequest[] = toolCalls.map((tc, i) => ({
2006
- id: `${tc.toolName}-${i}-${Date.now()}`,
2007
- toolName: tc.toolName,
2008
- arguments: tc.arguments,
2009
- }));
2010
-
2011
- const historyEvents = historyToEvents(history);
2012
-
2013
- try {
2014
- for await (const update of toolManager.executeWithConcurrency({
2015
- toolCalls: toolCallRequests,
2016
- context,
2017
- data: session.data,
2018
- history: historyEvents,
2019
- signal,
2020
- flow: selectedFlow,
2021
- step: nextStep,
2022
- })) {
2023
- // Apply context updates
2024
- if (update.contextUpdate) {
2025
- try {
2026
- await this.agent.updateContext(update.contextUpdate as Partial<TContext>);
2027
- } catch (error) {
2028
- logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
2029
- }
2030
- }
2031
-
2032
- // Apply data updates
2033
- if (update.dataUpdate) {
2034
- try {
2035
- const updateDataMethod = this.agent.getUpdateDataMethod();
2036
- session = await updateDataMethod(session, update.dataUpdate);
2037
- } catch (error) {
2038
- logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
2039
- }
2040
- }
2041
-
2042
- // Yield progress updates immediately
2043
- if (update.progress) {
2044
- yield {
2045
- delta: '',
2046
- accumulated: chunk.accumulated,
2047
- done: false,
2048
- session,
2049
- toolCalls: undefined,
2050
- isFlowComplete: false,
2051
- metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
2052
- };
2053
- }
2054
- }
2055
-
2056
- logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
2057
- } catch (error) {
2058
- logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
2059
- // Fall back to the unified tool loop on failure
2060
- const toolResult = await this.executeUnifiedToolLoop({
2061
- toolCalls, context, session, history, selectedFlow,
2062
- responsePrompt, availableTools, responseSchema, signal,
2063
- });
2064
- session = toolResult.session;
2065
- toolCalls = toolResult.finalToolCalls;
2066
- }
2067
- }
1251
+ // Concurrent execution for the initial batch of tool calls,
1252
+ // yielding tool-progress chunks as they arrive
1253
+ const batchResult = yield* this.toolLoopExecutor.runStreamingBatch({
1254
+ toolCalls,
1255
+ context,
1256
+ session,
1257
+ history,
1258
+ selectedFlow,
1259
+ step: nextStep,
1260
+ accumulated: chunk.accumulated,
1261
+ responsePrompt,
1262
+ availableTools,
1263
+ responseSchema,
1264
+ signal,
1265
+ });
1266
+ session = batchResult.session;
1267
+ toolCalls = batchResult.toolCalls;
2068
1268
  }
2069
1269
 
2070
1270
  // Extract collected data on final chunk
@@ -2077,11 +1277,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2077
1277
  });
2078
1278
  }
2079
1279
 
2080
- // Handle session finalization on final chunk
2081
- if (chunk.done) {
2082
- await this.finalizeSession(session, context);
2083
- }
2084
-
2085
1280
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2086
1281
  // - executedSteps: single step executed in this response
2087
1282
  // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
@@ -2109,341 +1304,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2109
1304
  }
2110
1305
 
2111
1306
  /**
2112
- * Unified tool execution logic with loop handling
2113
- * Consolidates the complex tool execution logic from both streaming and non-streaming responses
2114
- * @private
2115
- */
2116
- private async executeUnifiedToolLoop(params: {
2117
- toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
2118
- context: TContext;
2119
- session: SessionState<TData>;
2120
- history: HistoryItem[];
2121
- selectedFlow?: Flow<TContext, TData>;
2122
- responsePrompt: string;
2123
- availableTools: Array<{
2124
- id: string;
2125
- name: string;
2126
- description?: string;
2127
- parameters?: unknown;
2128
- }>;
2129
- responseSchema?: Record<string, unknown>;
2130
- signal?: AbortSignal;
2131
- }): Promise<{
2132
- session: SessionState<TData>;
2133
- finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
2134
- finalMessage?: string;
2135
- structured?: AgentStructuredResponse;
2136
- }> {
2137
- try {
2138
- const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
2139
- let { toolCalls, session } = params;
2140
-
2141
- // Convert HistoryItem[] to Event[] for internal processing
2142
- const historyEvents = historyToEvents(history);
2143
-
2144
- // Map to store tool execution results for history
2145
- const toolResultsMap = new Map<string, string>();
2146
- // Map to store tool call arguments for history reconstruction
2147
- const toolArgsMap = new Map<string, Record<string, unknown>>();
2148
-
2149
- // Execute initial dynamic tool calls
2150
- if (toolCalls && toolCalls.length > 0) {
2151
- logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
2152
-
2153
- for (const toolCall of toolCalls) {
2154
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
2155
- if (!tool) {
2156
- logger.warn(`[ToolExecutionError] Tool not found: "${toolCall.toolName}" is not registered in any scope. Skipping this tool call. Register the tool or check the tool name.`);
2157
- continue;
2158
- }
2159
-
2160
- try {
2161
- // Use ToolManager for unified tool execution
2162
- const toolManager = this.getToolManager();
2163
- const toolResult = await toolManager.executeTool({
2164
- tool: tool,
2165
- context,
2166
- updateContext: this.agent.updateContext.bind(this.agent),
2167
- updateData: this.agent.updateCollectedData.bind(this.agent),
2168
- history: historyEvents, // Use Event[] for tool execution
2169
- data: session.data,
2170
- toolArguments: toolCall.arguments,
2171
- });
2172
-
2173
- // Store the actual tool result data for history
2174
- toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
2175
- toolArgsMap.set(toolCall.toolName, toolCall.arguments);
2176
-
2177
- // Check if tool execution was successful
2178
- if (!toolResult.success) {
2179
- logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
2180
- // Continue with other tools rather than failing completely
2181
- continue;
2182
- }
2183
-
2184
- // Update context with tool results
2185
- if (toolResult.contextUpdate) {
2186
- try {
2187
- await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
2188
- } catch (error) {
2189
- logger.error(`[ResponseModal] Failed to update context from tool ${toolCall.toolName}:`, error);
2190
- // Continue execution but log the error
2191
- }
2192
- }
2193
-
2194
- // Update collected data with tool results
2195
- if (toolResult.dataUpdate) {
2196
- try {
2197
- const updateDataMethod = this.agent.getUpdateDataMethod();
2198
- session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
2199
- logger.debug(`[ResponseModal] Tool updated collected data:`, toolResult.dataUpdate);
2200
- } catch (error) {
2201
- logger.error(`[ResponseModal] Failed to update data from tool ${toolCall.toolName}:`, error);
2202
- // Continue execution but log the error
2203
- }
2204
- }
2205
-
2206
- logger.debug(`[ResponseModal] Executed dynamic tool: ${toolCall.toolName} (success: ${toolResult.success})`);
2207
- } catch (error) {
2208
- logger.error(`[ResponseModal] Tool execution error for ${toolCall.toolName}:`, error);
2209
- // Continue with other tools rather than failing the entire response
2210
- continue;
2211
- }
2212
- }
2213
- }
2214
-
2215
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
2216
- const MAX_TOOL_LOOPS = this.options?.maxToolLoops || 5;
2217
- let toolLoopCount = 0;
2218
- let hasToolCalls = toolCalls && toolCalls.length > 0;
2219
- let finalMessage: string | undefined;
2220
- let followUpStructured: AgentStructuredResponse | undefined;
2221
-
2222
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
2223
- toolLoopCount++;
2224
- logger.debug(`[ResponseModal] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS} with ${toolCalls?.length || 0} tool calls`);
2225
-
2226
- // Create tool result history items
2227
- const toolResultHistoryItems: HistoryItem[] = [];
2228
- for (const toolCall of toolCalls || []) {
2229
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
2230
- if (tool) {
2231
- // Create HistoryItem format for tool results
2232
- // Add assistant message with tool_calls
2233
- toolResultHistoryItems.push({
2234
- role: "assistant" as const,
2235
- content: null,
2236
- tool_calls: [{
2237
- id: toolCall.toolName,
2238
- name: toolCall.toolName,
2239
- arguments: toolCall.arguments,
2240
- }],
2241
- });
2242
- // Add tool result
2243
- toolResultHistoryItems.push({
2244
- role: "tool" as const,
2245
- tool_call_id: toolCall.toolName,
2246
- name: toolCall.toolName,
2247
- content: toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
2248
- });
2249
- }
2250
- }
2251
-
2252
- // Create updated history with tool results
2253
- const updatedHistory = [...history, ...toolResultHistoryItems];
2254
-
2255
- // Make follow-up AI call to see if more tools are needed
2256
- // After first iteration, don't provide tools to force a text response
2257
- const agentOptions = this.agent.getAgentOptions();
2258
- const shouldProvideTools = toolLoopCount === 1;
2259
-
2260
- logger.debug(`[ResponseModal] Making follow-up AI call (loop ${toolLoopCount}):`, {
2261
- providingTools: shouldProvideTools,
2262
- toolsCount: shouldProvideTools ? availableTools.length : 0,
2263
- addingTextInstruction: toolLoopCount > 1,
2264
- });
2265
-
2266
- const followUpResult = await agentOptions.provider.generateMessage({
2267
- prompt: responsePrompt + (toolLoopCount > 1 ? "\n\nProvide a text response to the user based on the tool results." : ""),
2268
- history: updatedHistory, // Use HistoryItem[] for AI provider
2269
- context,
2270
- tools: shouldProvideTools ? availableTools : [], // Only provide tools on first iteration
2271
- parameters: responseSchema ? {
2272
- jsonSchema: responseSchema,
2273
- schemaName: "tool_followup",
2274
- } : undefined,
2275
- signal,
2276
- });
2277
-
2278
- // Check if follow-up call has more tool calls
2279
- const followUpToolCalls = followUpResult.structured?.toolCalls;
2280
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
2281
-
2282
- logger.debug(`[ResponseModal] Follow-up AI response (loop ${toolLoopCount}):`, {
2283
- hasMessage: !!followUpResult.message,
2284
- messageLength: followUpResult.message?.length || 0,
2285
- hasToolCalls,
2286
- toolCallsCount: followUpToolCalls?.length || 0,
2287
- toolNames: followUpToolCalls?.map(tc => tc.toolName) || [],
2288
- });
2289
-
2290
- if (hasToolCalls) {
2291
- logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls!.length} additional tool calls`);
2292
-
2293
- // Execute the follow-up tool calls
2294
- for (const toolCall of followUpToolCalls!) {
2295
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
2296
- if (!tool) {
2297
- logger.warn(`[ToolExecutionError] Tool not found in follow-up: "${toolCall.toolName}" is not registered in any scope. Skipping this tool call. Register the tool or check the tool name.`);
2298
- continue;
2299
- }
2300
-
2301
- try {
2302
- // Use ToolManager for unified tool execution
2303
- const toolManager = this.getToolManager();
2304
- const toolResult = await toolManager.executeTool({
2305
- tool: tool,
2306
- context,
2307
- updateContext: this.agent.updateContext.bind(this.agent),
2308
- updateData: this.agent.updateCollectedData.bind(this.agent),
2309
- history: historyToEvents(updatedHistory), // Convert to Event[] for tool execution
2310
- data: session.data,
2311
- toolArguments: toolCall.arguments,
2312
- });
2313
-
2314
- // Check if tool execution was successful
2315
- if (!toolResult.success) {
2316
- logger.error(`[ResponseModal] Follow-up tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
2317
- continue;
2318
- }
2319
-
2320
- // Update context with follow-up tool results
2321
- if (toolResult.contextUpdate) {
2322
- try {
2323
- await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
2324
- } catch (error) {
2325
- logger.error(`[ResponseModal] Failed to update context from follow-up tool ${toolCall.toolName}:`, error);
2326
- }
2327
- }
2328
-
2329
- if (toolResult.dataUpdate) {
2330
- try {
2331
- const updateDataMethod = this.agent.getUpdateDataMethod();
2332
- session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
2333
- logger.debug(`[ResponseModal] Follow-up tool updated collected data:`, toolResult.dataUpdate);
2334
- } catch (error) {
2335
- logger.error(`[ResponseModal] Failed to update data from follow-up tool ${toolCall.toolName}:`, error);
2336
- }
2337
- }
2338
-
2339
- // Store the follow-up tool result for potential next loop iteration
2340
- toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
2341
- toolArgsMap.set(toolCall.toolName, toolCall.arguments);
2342
-
2343
- logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
2344
- } catch (error) {
2345
- logger.error(`[ResponseModal] Follow-up tool execution error for ${toolCall.toolName}:`, error);
2346
- continue;
2347
- }
2348
- }
2349
-
2350
- // Update toolCalls for next iteration or final response
2351
- toolCalls = followUpToolCalls;
2352
- } else {
2353
- logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
2354
- // Update final message and toolCalls from follow-up result if no more tools
2355
- finalMessage = followUpResult.structured?.message || followUpResult.message;
2356
- followUpStructured = followUpResult.structured;
2357
- toolCalls = followUpToolCalls || [];
2358
- break;
2359
- }
2360
- }
2361
-
2362
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
2363
- logger.warn(`[ResponseGenerationError] Tool loop limit reached: ${toolLoopCount} iterations hit the cap (${MAX_TOOL_LOOPS}). Stopping tool execution. Increase MAX_TOOL_LOOPS or reduce recursive tool calls.`);
2364
- }
2365
-
2366
- // If tools were executed but no final text message was produced,
2367
- // make one more LLM call to generate a proper text response from tool results.
2368
- // This prevents the original tool-invocation message (e.g. "Let me check...")
2369
- // from being returned as the final user-facing response.
2370
- if (!finalMessage && toolLoopCount > 0) {
2371
- logger.debug(`[ResponseModal] No final message after tool loop, making additional LLM call for text response`);
2372
-
2373
- // Build tool result history from toolResultsMap which contains ALL
2374
- // tool executions (initial + follow-up). We can't use `toolCalls` here
2375
- // because it was reassigned to the (empty) follow-up tool calls when
2376
- // the while loop broke out.
2377
- const finalToolResultHistoryItems: HistoryItem[] = [];
2378
- for (const [toolName, toolResult] of toolResultsMap) {
2379
- finalToolResultHistoryItems.push({
2380
- role: "assistant" as const,
2381
- content: null,
2382
- tool_calls: [{
2383
- id: toolName,
2384
- name: toolName,
2385
- arguments: toolArgsMap.get(toolName) || {},
2386
- }],
2387
- });
2388
- finalToolResultHistoryItems.push({
2389
- role: "tool" as const,
2390
- tool_call_id: toolName,
2391
- name: toolName,
2392
- content: toolResult,
2393
- });
2394
- }
2395
-
2396
- const finalHistory = [...history, ...finalToolResultHistoryItems];
2397
- const agentOptions = this.agent.getAgentOptions();
2398
-
2399
- try {
2400
- const textResult = await agentOptions.provider.generateMessage({
2401
- prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
2402
- history: finalHistory,
2403
- context,
2404
- tools: [], // No tools - force text response
2405
- parameters: responseSchema ? {
2406
- jsonSchema: responseSchema,
2407
- schemaName: "tool_final_text",
2408
- } : undefined,
2409
- signal,
2410
- });
2411
-
2412
- finalMessage = textResult.structured?.message || textResult.message;
2413
- if (textResult.structured) {
2414
- followUpStructured = textResult.structured;
2415
- }
2416
-
2417
- logger.debug(`[ResponseModal] Generated final text response after tool loop:`, {
2418
- hasMessage: !!finalMessage,
2419
- messageLength: finalMessage?.length || 0,
2420
- });
2421
- } catch (error) {
2422
- logger.error(`[ResponseModal] Failed to generate final text response after tool loop:`, error);
2423
- // finalMessage remains undefined; caller will use original message as fallback
2424
- }
2425
- }
2426
-
2427
- logger.debug(`[ResponseModal] Tool loop completed:`, {
2428
- totalIterations: toolLoopCount,
2429
- hasFinalMessage: !!finalMessage,
2430
- finalMessageLength: finalMessage?.length || 0,
2431
- finalToolCallsCount: toolCalls?.length || 0,
2432
- });
2433
-
2434
- return {
2435
- session,
2436
- finalToolCalls: toolCalls,
2437
- finalMessage,
2438
- structured: followUpStructured,
2439
- };
2440
- } catch (error) {
2441
- throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
2442
- toolCallsCount: params.toolCalls?.length || 0,
2443
- availableToolsCount: params.availableTools.length
2444
- });
2445
- }
2446
- } /**
2447
1307
  * Unified data collection from AI response
2448
1308
  * @private
2449
1309
  */
@@ -2624,10 +1484,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2624
1484
  return nextSession;
2625
1485
  }
2626
1486
 
2627
- /**
2628
- * Stream flow completion response
2629
- * @private
2630
- */
2631
1487
  /**
2632
1488
  * Stream a flow completion as a single terminal chunk.
2633
1489
  *
@@ -2659,8 +1515,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2659
1515
  history,
2660
1516
  });
2661
1517
 
2662
- await this.finalizeSession(session, context);
2663
-
2664
1518
  yield {
2665
1519
  delta: '',
2666
1520
  accumulated: '',
@@ -2754,11 +1608,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2754
1608
  });
2755
1609
 
2756
1610
  for await (const chunk of stream) {
2757
- // Update current session if we have one
2758
- if (chunk.done) {
2759
- await this.finalizeSession(session, context);
2760
- }
2761
-
2762
1611
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2763
1612
  // - executedSteps: empty for fallback (no flow/step execution)
2764
1613
  // - stoppedReason: undefined for fallback (no flow context)
@@ -2779,49 +1628,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2779
1628
  }
2780
1629
  }
2781
1630
 
2782
- /**
2783
- * Handle session persistence and finalization
2784
- * @private
2785
- */
2786
- private async finalizeSession(session: SessionState<TData>, context: TContext): Promise<void> {
2787
- // Auto-save session step to persistence if configured
2788
- const persistenceManager = this.agent.getPersistenceManager();
2789
- const agentOptions = this.agent.getAgentOptions();
2790
- if (
2791
- persistenceManager &&
2792
- session.id &&
2793
- (this.options?.enableAutoSave !== false && agentOptions.persistence?.autoSave !== false)
2794
- ) {
2795
- await persistenceManager.saveSessionState(session.id, session);
2796
- logger.debug(`[ResponseModal] Auto-saved session step to persistence: ${session.id}`);
2797
- }
2798
-
2799
- // Execute finalize function
2800
- await this.executeStepFinalize(session, context);
2801
-
2802
- // Update current session if we have one
2803
- const currentSession = this.agent.currentSession;
2804
- if (currentSession) {
2805
- this.agent.currentSession = session;
2806
- }
2807
- }
2808
1631
  // ============================================================================
2809
1632
  // UTILITY METHODS - Helper methods for tool management and other utilities
2810
1633
  // ============================================================================
2811
1634
 
2812
1635
 
2813
- /**
2814
- * Find an available tool by name for the given flow.
2815
- * Delegates to ToolManager for unified tool resolution.
2816
- * @private
2817
- */
2818
- private findAvailableTool(
2819
- toolName: string,
2820
- flow?: Flow<TContext, TData>
2821
- ): Tool<TContext, TData> | undefined {
2822
- return this.getToolManager().find(toolName, undefined, undefined, flow);
2823
- }
2824
-
2825
1636
  /**
2826
1637
  * Collect all available tools for the given flow and step context.
2827
1638
  * Delegates to ToolManager for unified tool resolution and deduplication.
@@ -2845,64 +1656,4 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2845
1656
  }));
2846
1657
  }
2847
1658
 
2848
- /**
2849
- * Execute a prepare or finalize function/tool
2850
- * @private
2851
- */
2852
- private async executePrepareFinalize(
2853
- prepareOrFinalize:
2854
- | string
2855
- | Tool<TContext, TData>
2856
- | ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult>)
2857
- | undefined,
2858
- context: TContext,
2859
- data?: Partial<TData>,
2860
- flow?: Flow<TContext, TData>,
2861
- step?: Step<TContext, TData>
2862
- ): Promise<void> {
2863
- if (!prepareOrFinalize) return;
2864
-
2865
- if (typeof prepareOrFinalize === "function") {
2866
- // It's a function - call it directly
2867
- await prepareOrFinalize(context, data);
2868
- } else {
2869
- // It's a tool reference - find and execute the tool
2870
- let tool: Tool<TContext, TData> | undefined;
2871
-
2872
- if (typeof prepareOrFinalize === "string") {
2873
- // Tool ID - use ToolManager for unified resolution
2874
- tool = this.getToolManager().find(prepareOrFinalize, undefined, step, flow);
2875
- } else {
2876
- // Tool object - use directly
2877
- tool = prepareOrFinalize;
2878
- }
2879
-
2880
- if (tool) {
2881
- // Use ToolManager for unified tool execution
2882
- const result = await this.getToolManager().executeTool({
2883
- tool,
2884
- context,
2885
- updateContext: this.agent.updateContext.bind(this.agent),
2886
- updateData: this.agent.updateCollectedData.bind(this.agent),
2887
- history: [], // Empty history for prepare/finalize
2888
- data,
2889
- });
2890
-
2891
- if (!result.success) {
2892
- logger.error(
2893
- `[ResponseModal] Tool execution failed in prepare/finalize: ${result.error}`
2894
- );
2895
- throw new Error(`Tool execution failed: ${result.error}`);
2896
- }
2897
- } else {
2898
- logger.warn(
2899
- `[ResponseModal] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
2900
- ? prepareOrFinalize
2901
- : "inline tool"
2902
- }`
2903
- );
2904
- }
2905
- }
2906
- }
2907
-
2908
1659
  }