@falai/agent 2.2.3 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 -96
  75. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  76. package/dist/cjs/core/ResponseModal.js +184 -1183
  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 +13 -27
  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 +1 -3
  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 +21 -11
  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 -96
  217. package/dist/core/ResponseModal.d.ts.map +1 -1
  218. package/dist/core/ResponseModal.js +177 -1175
  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 +13 -27
  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 +1 -3
  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 +21 -11
  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 +3 -1
  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 +18 -8
  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 +241 -1465
  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 +14 -30
  371. package/src/core/SignalProcessor.ts +1 -4
  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 +23 -11
  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({
@@ -411,52 +441,53 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
411
441
  // Keep original HistoryItem[] format for external APIs
412
442
  const history = simpleHistory;
413
443
 
414
- // Use ResponsePipeline for optimized context and session preparation
415
- // 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
416
446
  let responseContext: {
417
447
  effectiveContext: TContext;
418
448
  session: SessionState<TData>;
449
+ contextAfterHook?: TContext;
419
450
  };
420
451
  try {
421
- // Set current context and session in pipeline for consistency
422
- this.responsePipeline.setContext(await this.agent.getContext());
423
- this.responsePipeline.setCurrentSession(this.agent.currentSession);
424
-
425
452
  responseContext = await this.responsePipeline.prepareResponseContext({
426
453
  contextOverride,
427
454
  session: params.session ? cloneDeep(params.session) : undefined,
455
+ currentContext: await this.agent.getContext(),
456
+ currentSession: this.agent.currentSession,
428
457
  });
429
458
  } catch (error) {
430
459
  throw ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
431
460
  }
432
461
 
433
- const { effectiveContext } = responseContext;
462
+ const { effectiveContext, contextAfterHook } = responseContext;
434
463
  let session = responseContext.session;
435
464
 
436
- // Update our stored context if it was modified by beforeRespond hook
437
- const storedContext = this.responsePipeline.getStoredContext();
438
- if (storedContext !== undefined) {
465
+ // Sync the beforeRespond hook's context result back to the agent
466
+ if (contextAfterHook !== undefined) {
439
467
  try {
440
- await this.agent.updateContext(storedContext as Partial<TContext>);
468
+ await this.agent.updateContext(contextAfterHook as Partial<TContext>);
441
469
  } catch (error) {
442
- throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { storedContext });
470
+ throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { contextAfterHook });
443
471
  }
444
472
  }
445
473
 
446
- // Merge agent's collected data into session (agent data takes precedence)
447
- const collectedData = this.agent.getCollectedData();
448
- 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) {
449
480
  try {
450
- session = mergeCollected(session, collectedData);
451
- 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);
452
483
  } catch (error) {
453
- throw ResponseGenerationError.fromError(error, 'data_merging', params, { collectedData });
484
+ throw ResponseGenerationError.fromError(error, 'data_merging', params, { stagedData });
454
485
  }
455
486
  }
456
487
 
457
488
  // PHASE 1: PREPARE - Execute prepare function if current step has one
458
489
  try {
459
- await this.executeStepPrepare(session, effectiveContext);
490
+ await this.stepLifecycle.runPrepare(session, effectiveContext);
460
491
  } catch (error) {
461
492
  throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
462
493
  }
@@ -475,7 +506,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
475
506
  signalHaltReply?: string;
476
507
  };
477
508
  try {
478
- routingResult = await this.handleUnifiedRoutingAndStepSelection({
509
+ routingResult = await this.responsePipeline.routeAndSelectStep({
479
510
  session,
480
511
  history: historyEvents,
481
512
  context: effectiveContext,
@@ -508,564 +539,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
508
539
  }
509
540
  }
510
541
 
511
- /**
512
- * Unified routing and step selection logic using ResponsePipeline for optimization
513
- * @private
514
- */
515
- private async handleUnifiedRoutingAndStepSelection(params: {
516
- session: SessionState<TData>;
517
- history: Event[]; // Use Event[] for internal processing
518
- context: TContext;
519
- signal?: AbortSignal;
520
- }): Promise<{
521
- selectedFlow?: Flow<TContext, TData>;
522
- selectedStep?: Step<TContext, TData>;
523
- responseDirectives?: string[];
524
- session: SessionState<TData>;
525
- isFlowComplete: boolean;
526
- /** Signal firings from the pre-phase (threaded through for response surface). */
527
- signalFirings?: SignalFiring<TContext, TData>[];
528
- /** Non-position signal directive for pre-LLM augmentation (appendPrompt, injectTools, etc). */
529
- signalPreDirective?: Directive<TContext, TData>;
530
- /** Pre-signal phase halted the turn. */
531
- signalHalted?: boolean;
532
- /** Reply text from the halt directive. */
533
- signalHaltReply?: string;
534
- }> {
535
- try {
536
- // Create a fresh chain tracker for this turn (Requirement 22.1)
537
- this.responsePipeline.createChainTracker();
538
-
539
- // ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
540
- // When the current step has collect fields AND pre-extraction populates at least
541
- // one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
542
- const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
543
- if (routingSkipResult) {
544
- // Even when routing is skipped, run pre-signal phase if processor is present
545
- if (this.agent.signalProcessor) {
546
- const signalResult = await this.responsePipeline.runPreSignalPhase(
547
- params.session, params.context, params.history,
548
- );
549
- // If signal halts, override the routing skip result
550
- if (signalResult.mergedDirective?.halt) {
551
- return {
552
- ...routingSkipResult,
553
- session: signalResult.updatedSession,
554
- signalFirings: signalResult.firings,
555
- signalHalted: true,
556
- signalHaltReply: signalResult.mergedDirective.reply,
557
- };
558
- }
559
- // If signal has position fields, override routing skip result
560
- if (hasDirectivePositionField(signalResult.mergedDirective)) {
561
- return this.applySignalPositionDirective(
562
- signalResult, params,
563
- );
564
- }
565
- // Non-position directive: propagate for pre-LLM augmentation
566
- return {
567
- ...routingSkipResult,
568
- session: signalResult.updatedSession,
569
- signalFirings: signalResult.firings,
570
- signalPreDirective: signalResult.mergedDirective || undefined,
571
- };
572
- }
573
- return routingSkipResult;
574
- }
575
-
576
- // ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
577
- // When signalProcessor is present, run pre-signals in parallel with routing.
578
- // When absent, call the router directly (zero overhead, preserve current behavior).
579
- if (this.agent.signalProcessor) {
580
- // Run pre-signal phase in parallel with routing (Requirement 8.1)
581
- const [signalResult, routingResult] = await Promise.all([
582
- this.responsePipeline.runPreSignalPhase(
583
- params.session, params.context, params.history,
584
- ),
585
- this.responsePipeline.handleRoutingAndStepSelection({
586
- session: params.session,
587
- history: params.history,
588
- context: params.context,
589
- signal: params.signal,
590
- }),
591
- ]);
592
-
593
- // ── Requirement 8.2: halt → discard routing, skip LLM ────────────
594
- if (signalResult.mergedDirective?.halt) {
595
- return {
596
- selectedFlow: undefined,
597
- selectedStep: undefined,
598
- session: signalResult.updatedSession,
599
- isFlowComplete: false,
600
- signalFirings: signalResult.firings,
601
- signalHalted: true,
602
- signalHaltReply: signalResult.mergedDirective.reply,
603
- };
604
- }
605
-
606
- // ── Requirement 8.3: position directive → discard routing, apply signal position ──
607
- if (hasDirectivePositionField(signalResult.mergedDirective)) {
608
- return this.applySignalPositionDirective(
609
- signalResult, params,
610
- );
611
- }
612
-
613
- // ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
614
- // ── Requirement 8.5: no directive → use routing as-is ─────────────
615
- let updatedSession = signalResult.updatedSession;
616
-
617
- // Apply data/context updates from signal to the routed session
618
- if (signalResult.mergedDirective?.dataUpdate) {
619
- updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
620
- }
621
-
622
- // Use routing result for flow/step, but carry signal session state
623
- // Merge routing session changes on top of signal session
624
- const routingSession = routingResult.session;
625
- updatedSession = {
626
- ...updatedSession,
627
- currentFlow: routingSession.currentFlow,
628
- currentStep: routingSession.currentStep,
629
- flowHistory: routingSession.flowHistory,
630
- pendingDirective: routingSession.pendingDirective,
631
- };
632
-
633
- const isFlowComplete = routingResult.isFlowComplete;
634
-
635
- // PRE-EXTRACTION: same logic as below — extract data from user message
636
- if (routingResult.selectedFlow && !isFlowComplete) {
637
- if (this.shouldPreExtractData(routingResult.selectedFlow)) {
638
- logger.debug(
639
- `[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
640
- );
641
- const extractedData = await this.preExtractFlowData({
642
- route: routingResult.selectedFlow,
643
- history: params.history,
644
- context: params.context,
645
- session: updatedSession,
646
- signal: params.signal,
647
- });
648
- if (extractedData && Object.keys(extractedData).length > 0) {
649
- logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
650
- updatedSession = mergeCollected(updatedSession, extractedData);
651
- await this.agent.updateCollectedData(extractedData);
652
- }
653
- }
654
- }
655
-
656
- // Determine next step
657
- const stepResult = await this.responsePipeline.determineNextStep({
658
- selectedFlow: routingResult.selectedFlow,
659
- selectedStep: routingResult.selectedStep,
660
- session: updatedSession,
661
- isFlowComplete,
662
- });
663
-
664
- return {
665
- selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
666
- selectedStep: stepResult.nextStep,
667
- responseDirectives: routingResult.responseDirectives,
668
- session: stepResult.session,
669
- isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
670
- signalFirings: signalResult.firings,
671
- signalPreDirective: signalResult.mergedDirective || undefined,
672
- };
673
- }
674
-
675
- // ── No signal processor: existing behavior (zero overhead) ────────────
676
- const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
677
- session: params.session,
678
- history: params.history,
679
- context: params.context,
680
- signal: params.signal,
681
- });
682
-
683
- let updatedSession = routingResult.session;
684
- const isFlowComplete = routingResult.isFlowComplete;
685
-
686
- // PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
687
- // This allows us to skip steps whose data is already provided
688
- if (routingResult.selectedFlow && !isFlowComplete) {
689
- // Always pre-extract when flow collects data (not just on new flow entry)
690
- // This ensures step selection has the most up-to-date data
691
- if (this.shouldPreExtractData(routingResult.selectedFlow)) {
692
- logger.debug(
693
- `[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
694
- );
695
-
696
- const extractedData = await this.preExtractFlowData({
697
- route: routingResult.selectedFlow,
698
- history: params.history,
699
- context: params.context,
700
- session: updatedSession,
701
- signal: params.signal,
702
- });
703
-
704
- if (extractedData && Object.keys(extractedData).length > 0) {
705
- logger.debug(
706
- `[ResponseModal] Pre-extracted data:`,
707
- extractedData
708
- );
709
- // Merge pre-extracted data into session before step selection
710
- updatedSession = mergeCollected(updatedSession, extractedData);
711
- // Also update agent's collected data
712
- await this.agent.updateCollectedData(extractedData);
713
- }
714
- }
715
- }
716
-
717
- // Determine next step using pipeline method for consistency
718
- const stepResult = await this.responsePipeline.determineNextStep({
719
- selectedFlow: routingResult.selectedFlow,
720
- selectedStep: routingResult.selectedStep,
721
- session: updatedSession, // Use updated session with pre-extracted data
722
- isFlowComplete, // Use updated completion status
723
- });
724
-
725
- return {
726
- selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
727
- selectedStep: stepResult.nextStep, // Use the determined next step
728
- responseDirectives: routingResult.responseDirectives,
729
- session: stepResult.session,
730
- // If a branch changed the flow, the original isFlowComplete no longer applies
731
- isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
732
- };
733
- } catch (error) {
734
- throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
735
- }
736
- }
737
-
738
- /**
739
- * Apply a signal's position directive (goTo, goToStep, complete, abort, reset).
740
- * Discards routing result and uses the signal's position decision.
741
- * @private
742
- * @requirements 8.3
743
- */
744
- private applySignalPositionDirective(
745
- signalResult: {
746
- firings: SignalFiring<TContext, TData>[];
747
- updatedSession: SessionState<TData>;
748
- mergedDirective: Directive<TContext, TData> | undefined;
749
- },
750
- _params: { session: SessionState<TData>; history: Event[]; context: TContext },
751
- ): {
752
- selectedFlow?: Flow<TContext, TData>;
753
- selectedStep?: Step<TContext, TData>;
754
- responseDirectives?: string[];
755
- session: SessionState<TData>;
756
- isFlowComplete: boolean;
757
- signalFirings?: SignalFiring<TContext, TData>[];
758
- signalPreDirective?: Directive<TContext, TData>;
759
- signalHalted?: boolean;
760
- signalHaltReply?: string;
761
- } {
762
- const directive = signalResult.mergedDirective!;
763
- let session = signalResult.updatedSession;
764
- const flows = this.agent.getFlows();
765
- let selectedFlow: Flow<TContext, TData> | undefined;
766
- let selectedStep: Step<TContext, TData> | undefined;
767
- let isFlowComplete = false;
768
-
769
- // Apply data updates if present alongside position
770
- if (directive.dataUpdate) {
771
- session = mergeCollected(session, directive.dataUpdate);
772
- }
773
-
774
- if (directive.goTo) {
775
- const flowTarget = typeof directive.goTo === 'string'
776
- ? directive.goTo
777
- : directive.goTo.flow ?? directive.goTo.step;
778
-
779
- if (flowTarget) {
780
- const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
781
- if (targetFlow) {
782
- session = enterFlow(session, targetFlow.id, targetFlow.title);
783
- selectedFlow = targetFlow;
784
-
785
- if (typeof directive.goTo === 'object' && directive.goTo.step) {
786
- const targetStep = targetFlow.getStep(directive.goTo.step);
787
- if (targetStep) {
788
- session = enterStep(session, targetStep.id, targetStep.description);
789
- selectedStep = targetStep;
790
- }
791
- }
792
- } else {
793
- logger.warn(`[Signals] Pre-phase goTo target not found: "${flowTarget}". Falling back to no flow.`);
794
- }
795
- }
796
- } else if (directive.goToStep) {
797
- const stepTarget = typeof directive.goToStep === 'string'
798
- ? directive.goToStep
799
- : directive.goToStep.step;
800
- const flowTarget = typeof directive.goToStep === 'object'
801
- ? directive.goToStep.flow
802
- : undefined;
803
-
804
- if (flowTarget) {
805
- const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
806
- if (targetFlow) {
807
- session = enterFlow(session, targetFlow.id, targetFlow.title);
808
- selectedFlow = targetFlow;
809
- const targetStep = targetFlow.getStep(stepTarget);
810
- if (targetStep) {
811
- session = enterStep(session, targetStep.id, targetStep.description);
812
- selectedStep = targetStep;
813
- }
814
- }
815
- } else if (session.currentFlow) {
816
- const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
817
- if (currentFlow) {
818
- selectedFlow = currentFlow;
819
- const targetStep = currentFlow.getStep(stepTarget);
820
- if (targetStep) {
821
- session = enterStep(session, targetStep.id, targetStep.description);
822
- selectedStep = targetStep;
823
- }
824
- }
825
- }
826
- } else if (directive.complete) {
827
- isFlowComplete = true;
828
- } else if (directive.abort) {
829
- // Abort — no flow, session cleared or marked
830
- isFlowComplete = true;
831
- } else if (directive.reset) {
832
- if (session.currentFlow) {
833
- const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
834
- if (currentFlow) {
835
- selectedFlow = currentFlow;
836
- const resetStep = typeof directive.reset === 'object' && directive.reset.step
837
- ? directive.reset.step
838
- : undefined;
839
- if (resetStep) {
840
- const targetStep = currentFlow.getStep(resetStep);
841
- if (targetStep) {
842
- session = enterStep(session, targetStep.id, targetStep.description);
843
- selectedStep = targetStep;
844
- }
845
- } else {
846
- const initialStep = currentFlow.initialStep;
847
- session = enterStep(session, initialStep.id, initialStep.description);
848
- selectedStep = initialStep;
849
- }
850
- }
851
- }
852
- }
853
-
854
- return {
855
- selectedFlow,
856
- selectedStep,
857
- session,
858
- isFlowComplete,
859
- signalFirings: signalResult.firings,
860
- signalPreDirective: signalResult.mergedDirective || undefined,
861
- };
862
- }
863
-
864
- /**
865
- * Routing skip optimization (Requirements 20.1, 20.2, 20.3):
866
- * When the current step declares `collect` fields AND pre-extraction populates
867
- * at least one of those fields from the user's message, skip routing for this turn.
868
- *
869
- * Returns the routing result if the skip applies, or undefined to fall through
870
- * to normal routing.
871
- * @private
872
- */
873
- private async attemptRoutingSkipForCollect(params: {
874
- session: SessionState<TData>;
875
- history: Event[];
876
- context: TContext;
877
- signal?: AbortSignal;
878
- }): Promise<{
879
- selectedFlow?: Flow<TContext, TData>;
880
- selectedStep?: Step<TContext, TData>;
881
- responseDirectives?: string[];
882
- session: SessionState<TData>;
883
- isFlowComplete: boolean;
884
- } | undefined> {
885
- const { session } = params;
886
-
887
- // Only applies when we already have a current flow and step
888
- if (!session.currentFlow || !session.currentStep) {
889
- return undefined;
890
- }
891
-
892
- // Also skip this optimization if there's a pending directive (it takes priority)
893
- if (session.pendingDirective) {
894
- return undefined;
895
- }
896
-
897
- // Look up the actual Flow and Step objects to access `collect`
898
- const currentFlow = this.agent.getFlows().find(
899
- (f) => f.id === session.currentFlow?.id
900
- );
901
- if (!currentFlow) {
902
- return undefined;
903
- }
904
-
905
- const currentStep = currentFlow.getStep(session.currentStep.id);
906
- if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
907
- return undefined;
908
- }
909
-
910
- // We have a step with collect fields. Run pre-extraction to see if the
911
- // user's message populates any of them.
912
- const collectFields = currentStep.collect;
913
-
914
- // Snapshot current data for comparison
915
- const dataBefore = { ...session.data };
916
-
917
- // Run pre-extraction against the current flow
918
- const extractedData = await this.preExtractFlowData({
919
- route: currentFlow,
920
- history: params.history,
921
- context: params.context,
922
- session,
923
- signal: params.signal,
924
- });
925
-
926
- if (!extractedData || Object.keys(extractedData).length === 0) {
927
- return undefined;
928
- }
929
-
930
- // Determine which collect fields were newly populated by pre-extraction
931
- const populatedCollectFields: string[] = [];
932
- for (const field of collectFields) {
933
- const key = field as string;
934
- const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
935
- const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
936
- if (hasNewValue && !hadValue) {
937
- populatedCollectFields.push(key);
938
- }
939
- }
940
-
941
- if (populatedCollectFields.length === 0) {
942
- // Pre-extraction didn't populate any declared collect field — no skip
943
- return undefined;
944
- }
945
-
946
- // ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
947
- logger.debug(
948
- `[ResponseModal] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`
949
- );
950
-
951
- // Merge extracted data into session
952
- const updatedSession = mergeCollected(session, extractedData);
953
- await this.agent.updateCollectedData(extractedData);
954
-
955
- // Determine next step using pipeline method for consistency
956
- // Pass the current flow/step as the routing result (retained)
957
- const stepResult = await this.responsePipeline.determineNextStep({
958
- selectedFlow: currentFlow,
959
- selectedStep: currentStep,
960
- session: updatedSession,
961
- isFlowComplete: false,
962
- });
963
-
964
- return {
965
- selectedFlow: stepResult.flowChanged || currentFlow,
966
- selectedStep: stepResult.nextStep,
967
- responseDirectives: undefined,
968
- session: stepResult.session,
969
- isFlowComplete: stepResult.flowChanged ? false : false,
970
- };
971
- }
972
-
973
- /**
974
- * Check if a flow should pre-extract data before determining the initial step
975
- * @private
976
- */
977
- private shouldPreExtractData(flow: Flow<TContext, TData>): boolean {
978
- // Pre-extract if flow has declared required or optional fields
979
- if (flow.requiredFields && flow.requiredFields.length > 0) {
980
- return true;
981
- }
982
- if (flow.optionalFields && flow.optionalFields.length > 0) {
983
- return true;
984
- }
985
-
986
- // Pre-extract if any step in the flow collects data
987
- const steps = flow.getAllSteps();
988
- const hasDataCollectionSteps = steps.some(
989
- step => step.collect && step.collect.length > 0
990
- );
991
-
992
- return hasDataCollectionSteps;
993
- }
994
-
995
- /**
996
- * Pre-extract data from user message when entering a flow
997
- * This allows skipping steps whose data is already provided
998
- * @private
999
- */
1000
- private async preExtractFlowData(params: {
1001
- route: Flow<TContext, TData>;
1002
- history: Event[];
1003
- context: TContext;
1004
- session: SessionState<TData>;
1005
- signal?: AbortSignal;
1006
- }): Promise<Partial<TData>> {
1007
- const { route: flow, history, signal } = params;
1008
-
1009
- // Build a schema for data extraction based on flow's fields
1010
- const extractionSchema = this.agent.schema;
1011
- if (!extractionSchema) {
1012
- logger.warn(`[ResponseModal] No schema available for pre-extraction`);
1013
- return {};
1014
- }
1015
-
1016
- // Get last user message
1017
- const lastMessage = getLastMessageFromHistory(history);
1018
-
1019
- // Build extraction prompt
1020
- const extractionPrompt = [
1021
- `Extract any relevant information from the user's message that matches the following data fields.`,
1022
- `Only extract information that is explicitly stated or clearly implied.`,
1023
- ``,
1024
- `User's message: "${lastMessage}"`,
1025
- ``,
1026
- `Extract data for these fields if present:`,
1027
- ];
1028
-
1029
- // Add field descriptions
1030
- if (flow.requiredFields) {
1031
- extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
1032
- }
1033
- if (flow.optionalFields) {
1034
- extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
1035
- }
1036
-
1037
- extractionPrompt.push(
1038
- ``,
1039
- `Return ONLY the extracted data as JSON. If no data can be extracted, return an empty object {}.`
1040
- );
1041
-
1042
- // Convert Event[] to HistoryItem[] for provider call
1043
- const historyItems = eventsToHistory(history);
1044
-
1045
- // Call AI to extract data
1046
- const agentOptions = this.agent.getAgentOptions();
1047
- try {
1048
- const result = await agentOptions.provider.generateMessage<TContext, Partial<TData>>({
1049
- prompt: extractionPrompt.join('\n'),
1050
- history: historyItems,
1051
- context: {} as TContext, // Passed as empty object so AI doesn't "extract" from context
1052
- // NOTE: context is intentionally NOT passed here.
1053
- // Passing context caused the AI to "extract" data from the lead's context
1054
- // (e.g., name, sector, city) instead of from what the user actually said.
1055
- signal,
1056
- parameters: {
1057
- jsonSchema: extractionSchema,
1058
- schemaName: 'data_extraction',
1059
- },
1060
- });
1061
-
1062
- return result.structured || {};
1063
- } catch (error) {
1064
- logger.error(`[ResponseModal] Pre-extraction failed:`, error);
1065
- return {};
1066
- }
1067
- }
1068
-
1069
542
  /**
1070
543
  * Unified response generation for non-streaming responses
1071
544
  * @private
@@ -1101,26 +574,21 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1101
574
  if (signalHalted) {
1102
575
  const haltMessage = signalHaltReply || '';
1103
576
  // Run post-signal phase even on halt (post-phase sees complete turn context)
1104
- const postResult = await this.responsePipeline.runPostSignalPhase(
1105
- session, effectiveContext, historyEvents,
1106
- );
1107
- session = postResult.updatedSession;
1108
- signalFirings.push(...postResult.firings);
1109
-
1110
- // Apply post-phase position directive as pendingDirective (Requirement 9.3)
1111
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1112
- session = { ...session, pendingDirective: postResult.mergedDirective };
1113
- }
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;
1114
583
 
1115
- await this.finalizeSession(session, effectiveContext);
1116
584
  return {
1117
- message: haltMessage,
585
+ message,
1118
586
  session,
1119
587
  toolCalls: undefined,
1120
588
  isFlowComplete: false,
1121
589
  executedSteps: [],
1122
- stoppedReason: signalHaltReply ? 'reply' : 'halt',
1123
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
590
+ stoppedReason: haltMessage ? 'reply' : 'halt',
591
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1124
592
  };
1125
593
  }
1126
594
 
@@ -1130,8 +598,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1130
598
  let stoppedReason: StoppedReason | undefined;
1131
599
  let appliedInstructions: AppliedInstruction[] | undefined;
1132
600
 
1133
-
1134
-
1135
601
  if (selectedFlow && !isFlowComplete) {
1136
602
  // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
1137
603
  // If the current step is auto, the executor advances through it (and any
@@ -1153,13 +619,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1153
619
 
1154
620
  session = autoResult.session;
1155
621
 
1156
- // 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.
1157
624
  if (autoResult.stoppedReason === 'halt') {
1158
625
  message = autoResult.mergedDirective?.reply || '';
1159
626
  stoppedReason = 'halt';
1160
627
  executedSteps = [];
1161
628
 
1162
- await this.finalizeSession(session, effectiveContext);
1163
629
  return {
1164
630
  message,
1165
631
  session,
@@ -1183,7 +649,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1183
649
  history,
1184
650
  });
1185
651
 
1186
- await this.finalizeSession(session, effectiveContext);
1187
652
  return {
1188
653
  message: '',
1189
654
  session,
@@ -1272,20 +737,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1272
737
  // Runs after finalize/onComplete and before session persistence.
1273
738
  // Post-phase signals see the complete turn result: assistant message in
1274
739
  // history, collected data, tool results.
1275
- const postResult = await this.responsePipeline.runPostSignalPhase(
1276
- session, effectiveContext, historyEvents,
1277
- );
1278
- session = postResult.updatedSession;
1279
-
740
+ const post = await this.signalCoordinator.applyPostPhase({
741
+ session, context: effectiveContext, historyEvents, message,
742
+ });
743
+ session = post.session;
1280
744
  // Append post-phase firings to the accumulator (preserves fire order)
1281
- signalFirings.push(...postResult.firings);
1282
-
1283
- // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1284
- // (no mid-turn re-entry per D6 decision). Pre-LLM-only fields are already
1285
- // dropped inside runPostSignalPhase per Phase 4.5.
1286
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1287
- session = { ...session, pendingDirective: postResult.mergedDirective };
1288
- }
745
+ signalFirings.push(...post.firings);
746
+ message = post.message;
1289
747
 
1290
748
  // Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
1291
749
  // - executedSteps: array of steps executed (empty array if none)
@@ -1299,67 +757,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1299
757
  executedSteps: executedSteps || [],
1300
758
  stoppedReason,
1301
759
  appliedInstructions,
1302
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
760
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1303
761
  };
1304
762
  }
1305
763
 
1306
- /**
1307
- * Execute prepare function for current step if available
1308
- * @private
1309
- */
1310
- private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
1311
- if (session.currentFlow && session.currentStep) {
1312
- const currentFlow = this.agent.getFlows().find(
1313
- (r) => r.id === session.currentFlow?.id
1314
- );
1315
- if (currentFlow) {
1316
- const currentStep = currentFlow.getStep(session.currentStep.id);
1317
- // Skip auto-steps — their prepare is handled by AutoChainExecutor
1318
- if (currentStep?.auto) {
1319
- logger.debug(`[ResponseModal] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
1320
- return;
1321
- }
1322
- if (currentStep?.prepare) {
1323
- logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
1324
- await this.executePrepareFinalize(
1325
- currentStep.prepare,
1326
- context,
1327
- session.data,
1328
- currentFlow,
1329
- currentStep
1330
- );
1331
- }
1332
- }
1333
- }
1334
- }
1335
-
1336
- /**
1337
- * Execute finalize function for current step if available
1338
- * @private
1339
- */
1340
- private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
1341
- if (session.currentFlow && session.currentStep) {
1342
- const currentFlow = this.agent.getFlows().find(
1343
- (r) => r.id === session.currentFlow?.id
1344
- );
1345
- if (currentFlow) {
1346
- const currentStep = currentFlow.getStep(session.currentStep.id);
1347
- if (currentStep?.finalize) {
1348
- logger.debug(
1349
- `[ResponseModal] Executing finalize for step: ${currentStep.id}`
1350
- );
1351
- await this.executePrepareFinalize(
1352
- currentStep.finalize,
1353
- context,
1354
- session.data,
1355
- currentFlow,
1356
- currentStep
1357
- );
1358
- }
1359
- }
1360
- }
1361
- }
1362
-
1363
764
  /**
1364
765
  * Process flow response with unified tool execution and data collection
1365
766
  * @private
@@ -1393,87 +794,20 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1393
794
  const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1394
795
  let session = params.session;
1395
796
 
1396
- // Determine next step
1397
- let nextStep: Step<TContext, TData>;
1398
- if (selectedStep) {
1399
- nextStep = selectedStep;
1400
- } else {
1401
- // Determine current step from session if we're already in this flow
1402
- const isInSameFlow = session.currentFlow?.id === selectedFlow.id;
1403
- const currentStep = isInSameFlow && session.currentStep
1404
- ? selectedFlow.getStep(session.currentStep.id)
1405
- : undefined;
1406
-
1407
- logger.debug(`[ResponseModal] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
1408
-
1409
- // STEP 1 (Algorithm 1): branches win over linear chain
1410
- if (currentStep?.branches && currentStep.branches.length > 0) {
1411
- const branchResult = await this.responsePipeline.evaluateStepBranches(
1412
- currentStep, selectedFlow, session, context
1413
- );
1414
- if (branchResult) {
1415
- if (branchResult.nextStep) {
1416
- nextStep = branchResult.nextStep;
1417
- session = branchResult.session;
1418
- } else {
1419
- // Flow transition or completion — no local step to render
1420
- // Return empty message with updated session; caller handles flow transition
1421
- return { message: '', session: branchResult.session };
1422
- }
1423
- }
1424
- }
1425
-
1426
- if (!nextStep!) {
1427
- // Get candidate steps based on current position in the flow
1428
- const flowRouter = this.agent.getFlowRouter();
1429
- const candidates = await flowRouter.getCandidateStepsWithConditions(
1430
- selectedFlow,
1431
- currentStep, // Pass current step instead of undefined to maintain progression
1432
- createTemplateContext({ data: session.data, session, context })
1433
- );
1434
-
1435
- logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
1436
-
1437
- if (candidates.length > 0) {
1438
- nextStep = candidates[0].step;
1439
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1440
- } else {
1441
- // Fallback to initial step even if it should be skipped
1442
- nextStep = selectedFlow.initialStep;
1443
- logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1444
- }
1445
- }
1446
- }
1447
-
1448
- // Update session with next step
1449
- // If the next step has requires fields that are missing, stay at the previous step
1450
- if (nextStep.requires && nextStep.requires.length > 0) {
1451
- const sessionData = session.data || {};
1452
- const missingRequires = nextStep.requires.filter(
1453
- field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1454
- );
1455
- if (missingRequires.length > 0) {
1456
- const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1457
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1458
- logger.warn(warning);
1459
- console.warn(warning);
1460
- // Stay at the current step - don't enter the next one
1461
- const currentStepId = session.currentStep?.id;
1462
- if (currentStepId) {
1463
- const currentStepInstance = selectedFlow.getStep(currentStepId);
1464
- if (currentStepInstance) {
1465
- nextStep = currentStepInstance;
1466
- logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
1467
- }
1468
- }
1469
- } else {
1470
- session = enterStep(session, nextStep.id, nextStep.description);
1471
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1472
- }
1473
- } else {
1474
- session = enterStep(session, nextStep.id, nextStep.description);
1475
- 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 };
1476
808
  }
809
+ const nextStep = stepResolution.nextStep!;
810
+ session = stepResolution.session;
1477
811
 
1478
812
  // Build response schema for this flow (with collect fields from step)
1479
813
  const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
@@ -1557,7 +891,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1557
891
  });
1558
892
 
1559
893
  // Execute tools with unified loop handling
1560
- const toolResult = await this.executeUnifiedToolLoop({
894
+ const toolResult = await this.toolLoopExecutor.runLoop({
1561
895
  toolCalls,
1562
896
  context,
1563
897
  session,
@@ -1624,25 +958,22 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1624
958
  if (signalHalted) {
1625
959
  const haltMessage = signalHaltReply || '';
1626
960
  // Run post-signal phase even on halt
1627
- const postResult = await this.responsePipeline.runPostSignalPhase(
1628
- session, effectiveContext, historyEvents,
1629
- );
1630
- session = postResult.updatedSession;
1631
- signalFirings.push(...postResult.firings);
1632
-
1633
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1634
- session = { ...session, pendingDirective: postResult.mergedDirective };
1635
- }
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;
1636
967
 
1637
- await this.finalizeSession(session, effectiveContext);
968
+ await this.sessionFinalizer.finalize(session, effectiveContext);
1638
969
  yield {
1639
- delta: haltMessage,
1640
- accumulated: haltMessage,
970
+ delta: message,
971
+ accumulated: message,
1641
972
  done: true,
1642
973
  session,
1643
974
  stoppedReason: haltMessage ? 'reply' : 'halt',
1644
975
  executedSteps: [],
1645
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
976
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1646
977
  } as AgentResponseStreamChunk<TData>;
1647
978
  return;
1648
979
  }
@@ -1672,7 +1003,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1672
1003
  // Handle halt: emit verbatim reply as a single chunk, done.
1673
1004
  if (autoResult.stoppedReason === 'halt') {
1674
1005
  const reply = autoResult.mergedDirective?.reply || '';
1675
- await this.finalizeSession(session, effectiveContext);
1006
+ await this.sessionFinalizer.finalize(session, effectiveContext);
1676
1007
  yield {
1677
1008
  delta: reply,
1678
1009
  accumulated: reply,
@@ -1680,7 +1011,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1680
1011
  session,
1681
1012
  stoppedReason: 'halt',
1682
1013
  executedSteps: [],
1683
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1014
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1684
1015
  } as AgentResponseStreamChunk<TData>;
1685
1016
  return;
1686
1017
  }
@@ -1749,28 +1080,35 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1749
1080
  });
1750
1081
  }
1751
1082
 
1752
- // ── Intercept the inner stream to run post-signal phase on the final chunk ──
1753
- // This mirrors the non-streaming path: post-phase runs after finalize/onComplete
1754
- // and before session persistence, attaching triggeredSignals to the final chunk
1755
- // (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).
1756
1087
  for await (const chunk of innerStream!) {
1757
1088
  if (chunk.done) {
1758
1089
  // Run post-signal phase on final chunk (Requirement 9.1, 9.2)
1759
- const postResult = await this.responsePipeline.runPostSignalPhase(
1760
- chunk.session || session, effectiveContext, historyEvents,
1761
- );
1762
- let finalSession = postResult.updatedSession;
1763
- 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);
1764
1098
 
1765
- // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1766
- if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1767
- finalSession = { ...finalSession, pendingDirective: postResult.mergedDirective };
1768
- }
1099
+ const accumulated = post.message;
1100
+ const delta = post.replyOverridden ? accumulated : chunk.delta;
1101
+
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);
1769
1105
 
1770
1106
  yield {
1771
1107
  ...chunk,
1108
+ delta,
1109
+ accumulated,
1772
1110
  session: finalSession,
1773
- triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1111
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1774
1112
  } as AgentResponseStreamChunk<TData>;
1775
1113
  } else {
1776
1114
  yield chunk;
@@ -1805,86 +1143,25 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1805
1143
  const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1806
1144
  let session = params.session;
1807
1145
 
1808
- // Determine next step (same logic as non-streaming)
1809
- let nextStep: Step<TContext, TData>;
1810
- if (selectedStep) {
1811
- nextStep = selectedStep;
1812
- } else {
1813
- // Determine current step from session if we're already in this flow
1814
- const currentStep = session.currentFlow?.id === selectedFlow.id && session.currentStep
1815
- ? selectedFlow.getStep(session.currentStep.id)
1816
- : undefined;
1817
-
1818
- // STEP 1 (Algorithm 1): branches win over linear chain
1819
- if (currentStep?.branches && currentStep.branches.length > 0) {
1820
- const branchResult = await this.responsePipeline.evaluateStepBranches(
1821
- currentStep, selectedFlow, session, context
1822
- );
1823
- if (branchResult) {
1824
- // Branch resolved — yield a final chunk with the updated session and return
1825
- if (branchResult.nextStep) {
1826
- session = branchResult.session;
1827
- nextStep = branchResult.nextStep;
1828
- } else {
1829
- // Flow transition or completion — no step to render
1830
- yield {
1831
- delta: '',
1832
- accumulated: '',
1833
- done: true,
1834
- session: branchResult.session,
1835
- } as AgentResponseStreamChunk<TData>;
1836
- return;
1837
- }
1838
- }
1839
- }
1840
-
1841
- if (!nextStep!) {
1842
- // Get candidate steps based on current position in the flow
1843
- const flowRouter = this.agent.getFlowRouter();
1844
- const candidates = await flowRouter.getCandidateStepsWithConditions(
1845
- selectedFlow,
1846
- currentStep, // Pass current step instead of undefined to maintain progression
1847
- createTemplateContext({ data: session.data, session, context })
1848
- );
1849
-
1850
- if (candidates.length > 0) {
1851
- nextStep = candidates[0].step;
1852
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1853
- } else {
1854
- nextStep = selectedFlow.initialStep;
1855
- logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1856
- }
1857
- }
1858
- }
1859
-
1860
- // Update session with next step
1861
- // If the next step has requires fields that are missing, stay at the previous step
1862
- if (nextStep.requires && nextStep.requires.length > 0) {
1863
- const sessionData = session.data || {};
1864
- const missingRequires = nextStep.requires.filter(
1865
- field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1866
- );
1867
- if (missingRequires.length > 0) {
1868
- const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1869
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1870
- logger.warn(warning);
1871
- console.warn(warning);
1872
- const currentStepId = session.currentStep?.id;
1873
- if (currentStepId) {
1874
- const currentStepInstance = selectedFlow.getStep(currentStepId);
1875
- if (currentStepInstance) {
1876
- nextStep = currentStepInstance;
1877
- logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
1878
- }
1879
- }
1880
- } else {
1881
- session = enterStep(session, nextStep.id, nextStep.description);
1882
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1883
- }
1884
- } else {
1885
- session = enterStep(session, nextStep.id, nextStep.description);
1886
- 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;
1887
1162
  }
1163
+ const nextStep = stepResolution.nextStep!;
1164
+ session = stepResolution.session;
1888
1165
 
1889
1166
  // Build response schema and prompt (same as non-streaming)
1890
1167
  const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
@@ -1896,7 +1173,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1896
1173
  const reply = mergedPreDirective.reply || '';
1897
1174
  const reason: StoppedReason = mergedPreDirective.reply ? 'reply' : 'halt';
1898
1175
  logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
1899
- await this.finalizeSession(session, context);
1900
1176
  yield {
1901
1177
  delta: reply,
1902
1178
  accumulated: reply,
@@ -1918,13 +1194,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1918
1194
  createTemplateContext({ data: session.data || {}, context, session })
1919
1195
  );
1920
1196
  logger.debug(`[ResponseModal] Step.reply (streaming) — skipping LLM call for step ${nextStep.id}`);
1921
- await this.finalizeSession(session, context);
1922
1197
  yield {
1923
1198
  delta: effectiveReply,
1924
1199
  accumulated: effectiveReply,
1925
1200
  done: true,
1926
1201
  session,
1927
- stoppedReason: 'reply' as StoppedReason,
1202
+ stoppedReason: 'reply',
1928
1203
  executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
1929
1204
  } as AgentResponseStreamChunk<TData>;
1930
1205
  return;
@@ -1973,73 +1248,23 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1973
1248
  if (chunk.done && chunk.structured?.toolCalls) {
1974
1249
  toolCalls = chunk.structured.toolCalls;
1975
1250
 
1976
- const toolManager = this.getToolManager();
1977
-
1978
- // Use concurrent execution for the initial batch of tool calls
1979
- {
1980
- const toolCallRequests: ToolCallRequest[] = toolCalls.map((tc, i) => ({
1981
- id: `${tc.toolName}-${i}-${Date.now()}`,
1982
- toolName: tc.toolName,
1983
- arguments: tc.arguments,
1984
- }));
1985
-
1986
- const historyEvents = historyToEvents(history);
1987
-
1988
- try {
1989
- for await (const update of toolManager.executeWithConcurrency({
1990
- toolCalls: toolCallRequests,
1991
- context,
1992
- data: session.data,
1993
- history: historyEvents,
1994
- signal,
1995
- flow: selectedFlow,
1996
- step: nextStep,
1997
- })) {
1998
- // Apply context updates
1999
- if (update.contextUpdate) {
2000
- try {
2001
- await this.agent.updateContext(update.contextUpdate as Partial<TContext>);
2002
- } catch (error) {
2003
- logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
2004
- }
2005
- }
2006
-
2007
- // Apply data updates
2008
- if (update.dataUpdate) {
2009
- try {
2010
- const updateDataMethod = this.agent.getUpdateDataMethod();
2011
- session = await updateDataMethod(session, update.dataUpdate);
2012
- } catch (error) {
2013
- logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
2014
- }
2015
- }
2016
-
2017
- // Yield progress updates immediately
2018
- if (update.progress) {
2019
- yield {
2020
- delta: '',
2021
- accumulated: chunk.accumulated,
2022
- done: false,
2023
- session,
2024
- toolCalls: undefined,
2025
- isFlowComplete: false,
2026
- metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
2027
- };
2028
- }
2029
- }
2030
-
2031
- logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
2032
- } catch (error) {
2033
- logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
2034
- // Fall back to the unified tool loop on failure
2035
- const toolResult = await this.executeUnifiedToolLoop({
2036
- toolCalls, context, session, history, selectedFlow,
2037
- responsePrompt, availableTools, responseSchema, signal,
2038
- });
2039
- session = toolResult.session;
2040
- toolCalls = toolResult.finalToolCalls;
2041
- }
2042
- }
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;
2043
1268
  }
2044
1269
 
2045
1270
  // Extract collected data on final chunk
@@ -2052,11 +1277,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2052
1277
  });
2053
1278
  }
2054
1279
 
2055
- // Handle session finalization on final chunk
2056
- if (chunk.done) {
2057
- await this.finalizeSession(session, context);
2058
- }
2059
-
2060
1280
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2061
1281
  // - executedSteps: single step executed in this response
2062
1282
  // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
@@ -2084,341 +1304,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2084
1304
  }
2085
1305
 
2086
1306
  /**
2087
- * Unified tool execution logic with loop handling
2088
- * Consolidates the complex tool execution logic from both streaming and non-streaming responses
2089
- * @private
2090
- */
2091
- private async executeUnifiedToolLoop(params: {
2092
- toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
2093
- context: TContext;
2094
- session: SessionState<TData>;
2095
- history: HistoryItem[];
2096
- selectedFlow?: Flow<TContext, TData>;
2097
- responsePrompt: string;
2098
- availableTools: Array<{
2099
- id: string;
2100
- name: string;
2101
- description?: string;
2102
- parameters?: unknown;
2103
- }>;
2104
- responseSchema?: Record<string, unknown>;
2105
- signal?: AbortSignal;
2106
- }): Promise<{
2107
- session: SessionState<TData>;
2108
- finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
2109
- finalMessage?: string;
2110
- structured?: AgentStructuredResponse;
2111
- }> {
2112
- try {
2113
- const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
2114
- let { toolCalls, session } = params;
2115
-
2116
- // Convert HistoryItem[] to Event[] for internal processing
2117
- const historyEvents = historyToEvents(history);
2118
-
2119
- // Map to store tool execution results for history
2120
- const toolResultsMap = new Map<string, string>();
2121
- // Map to store tool call arguments for history reconstruction
2122
- const toolArgsMap = new Map<string, Record<string, unknown>>();
2123
-
2124
- // Execute initial dynamic tool calls
2125
- if (toolCalls && toolCalls.length > 0) {
2126
- logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
2127
-
2128
- for (const toolCall of toolCalls) {
2129
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
2130
- if (!tool) {
2131
- 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.`);
2132
- continue;
2133
- }
2134
-
2135
- try {
2136
- // Use ToolManager for unified tool execution
2137
- const toolManager = this.getToolManager();
2138
- const toolResult = await toolManager.executeTool({
2139
- tool: tool,
2140
- context,
2141
- updateContext: this.agent.updateContext.bind(this.agent),
2142
- updateData: this.agent.updateCollectedData.bind(this.agent),
2143
- history: historyEvents, // Use Event[] for tool execution
2144
- data: session.data,
2145
- toolArguments: toolCall.arguments,
2146
- });
2147
-
2148
- // Store the actual tool result data for history
2149
- toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
2150
- toolArgsMap.set(toolCall.toolName, toolCall.arguments);
2151
-
2152
- // Check if tool execution was successful
2153
- if (!toolResult.success) {
2154
- logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
2155
- // Continue with other tools rather than failing completely
2156
- continue;
2157
- }
2158
-
2159
- // Update context with tool results
2160
- if (toolResult.contextUpdate) {
2161
- try {
2162
- await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
2163
- } catch (error) {
2164
- logger.error(`[ResponseModal] Failed to update context from tool ${toolCall.toolName}:`, error);
2165
- // Continue execution but log the error
2166
- }
2167
- }
2168
-
2169
- // Update collected data with tool results
2170
- if (toolResult.dataUpdate) {
2171
- try {
2172
- const updateDataMethod = this.agent.getUpdateDataMethod();
2173
- session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
2174
- logger.debug(`[ResponseModal] Tool updated collected data:`, toolResult.dataUpdate);
2175
- } catch (error) {
2176
- logger.error(`[ResponseModal] Failed to update data from tool ${toolCall.toolName}:`, error);
2177
- // Continue execution but log the error
2178
- }
2179
- }
2180
-
2181
- logger.debug(`[ResponseModal] Executed dynamic tool: ${toolCall.toolName} (success: ${toolResult.success})`);
2182
- } catch (error) {
2183
- logger.error(`[ResponseModal] Tool execution error for ${toolCall.toolName}:`, error);
2184
- // Continue with other tools rather than failing the entire response
2185
- continue;
2186
- }
2187
- }
2188
- }
2189
-
2190
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
2191
- const MAX_TOOL_LOOPS = this.options?.maxToolLoops || 5;
2192
- let toolLoopCount = 0;
2193
- let hasToolCalls = toolCalls && toolCalls.length > 0;
2194
- let finalMessage: string | undefined;
2195
- let followUpStructured: AgentStructuredResponse | undefined;
2196
-
2197
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
2198
- toolLoopCount++;
2199
- logger.debug(`[ResponseModal] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS} with ${toolCalls?.length || 0} tool calls`);
2200
-
2201
- // Create tool result history items
2202
- const toolResultHistoryItems: HistoryItem[] = [];
2203
- for (const toolCall of toolCalls || []) {
2204
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
2205
- if (tool) {
2206
- // Create HistoryItem format for tool results
2207
- // Add assistant message with tool_calls
2208
- toolResultHistoryItems.push({
2209
- role: "assistant" as const,
2210
- content: null,
2211
- tool_calls: [{
2212
- id: toolCall.toolName,
2213
- name: toolCall.toolName,
2214
- arguments: toolCall.arguments,
2215
- }],
2216
- });
2217
- // Add tool result
2218
- toolResultHistoryItems.push({
2219
- role: "tool" as const,
2220
- tool_call_id: toolCall.toolName,
2221
- name: toolCall.toolName,
2222
- content: toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
2223
- });
2224
- }
2225
- }
2226
-
2227
- // Create updated history with tool results
2228
- const updatedHistory = [...history, ...toolResultHistoryItems];
2229
-
2230
- // Make follow-up AI call to see if more tools are needed
2231
- // After first iteration, don't provide tools to force a text response
2232
- const agentOptions = this.agent.getAgentOptions();
2233
- const shouldProvideTools = toolLoopCount === 1;
2234
-
2235
- logger.debug(`[ResponseModal] Making follow-up AI call (loop ${toolLoopCount}):`, {
2236
- providingTools: shouldProvideTools,
2237
- toolsCount: shouldProvideTools ? availableTools.length : 0,
2238
- addingTextInstruction: toolLoopCount > 1,
2239
- });
2240
-
2241
- const followUpResult = await agentOptions.provider.generateMessage({
2242
- prompt: responsePrompt + (toolLoopCount > 1 ? "\n\nProvide a text response to the user based on the tool results." : ""),
2243
- history: updatedHistory, // Use HistoryItem[] for AI provider
2244
- context,
2245
- tools: shouldProvideTools ? availableTools : [], // Only provide tools on first iteration
2246
- parameters: responseSchema ? {
2247
- jsonSchema: responseSchema,
2248
- schemaName: "tool_followup",
2249
- } : undefined,
2250
- signal,
2251
- });
2252
-
2253
- // Check if follow-up call has more tool calls
2254
- const followUpToolCalls = followUpResult.structured?.toolCalls;
2255
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
2256
-
2257
- logger.debug(`[ResponseModal] Follow-up AI response (loop ${toolLoopCount}):`, {
2258
- hasMessage: !!followUpResult.message,
2259
- messageLength: followUpResult.message?.length || 0,
2260
- hasToolCalls,
2261
- toolCallsCount: followUpToolCalls?.length || 0,
2262
- toolNames: followUpToolCalls?.map(tc => tc.toolName) || [],
2263
- });
2264
-
2265
- if (hasToolCalls) {
2266
- logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls!.length} additional tool calls`);
2267
-
2268
- // Execute the follow-up tool calls
2269
- for (const toolCall of followUpToolCalls!) {
2270
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
2271
- if (!tool) {
2272
- 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.`);
2273
- continue;
2274
- }
2275
-
2276
- try {
2277
- // Use ToolManager for unified tool execution
2278
- const toolManager = this.getToolManager();
2279
- const toolResult = await toolManager.executeTool({
2280
- tool: tool,
2281
- context,
2282
- updateContext: this.agent.updateContext.bind(this.agent),
2283
- updateData: this.agent.updateCollectedData.bind(this.agent),
2284
- history: historyToEvents(updatedHistory), // Convert to Event[] for tool execution
2285
- data: session.data,
2286
- toolArguments: toolCall.arguments,
2287
- });
2288
-
2289
- // Check if tool execution was successful
2290
- if (!toolResult.success) {
2291
- logger.error(`[ResponseModal] Follow-up tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
2292
- continue;
2293
- }
2294
-
2295
- // Update context with follow-up tool results
2296
- if (toolResult.contextUpdate) {
2297
- try {
2298
- await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
2299
- } catch (error) {
2300
- logger.error(`[ResponseModal] Failed to update context from follow-up tool ${toolCall.toolName}:`, error);
2301
- }
2302
- }
2303
-
2304
- if (toolResult.dataUpdate) {
2305
- try {
2306
- const updateDataMethod = this.agent.getUpdateDataMethod();
2307
- session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
2308
- logger.debug(`[ResponseModal] Follow-up tool updated collected data:`, toolResult.dataUpdate);
2309
- } catch (error) {
2310
- logger.error(`[ResponseModal] Failed to update data from follow-up tool ${toolCall.toolName}:`, error);
2311
- }
2312
- }
2313
-
2314
- // Store the follow-up tool result for potential next loop iteration
2315
- toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
2316
- toolArgsMap.set(toolCall.toolName, toolCall.arguments);
2317
-
2318
- logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
2319
- } catch (error) {
2320
- logger.error(`[ResponseModal] Follow-up tool execution error for ${toolCall.toolName}:`, error);
2321
- continue;
2322
- }
2323
- }
2324
-
2325
- // Update toolCalls for next iteration or final response
2326
- toolCalls = followUpToolCalls;
2327
- } else {
2328
- logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
2329
- // Update final message and toolCalls from follow-up result if no more tools
2330
- finalMessage = followUpResult.structured?.message || followUpResult.message;
2331
- followUpStructured = followUpResult.structured;
2332
- toolCalls = followUpToolCalls || [];
2333
- break;
2334
- }
2335
- }
2336
-
2337
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
2338
- 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.`);
2339
- }
2340
-
2341
- // If tools were executed but no final text message was produced,
2342
- // make one more LLM call to generate a proper text response from tool results.
2343
- // This prevents the original tool-invocation message (e.g. "Let me check...")
2344
- // from being returned as the final user-facing response.
2345
- if (!finalMessage && toolLoopCount > 0) {
2346
- logger.debug(`[ResponseModal] No final message after tool loop, making additional LLM call for text response`);
2347
-
2348
- // Build tool result history from toolResultsMap which contains ALL
2349
- // tool executions (initial + follow-up). We can't use `toolCalls` here
2350
- // because it was reassigned to the (empty) follow-up tool calls when
2351
- // the while loop broke out.
2352
- const finalToolResultHistoryItems: HistoryItem[] = [];
2353
- for (const [toolName, toolResult] of toolResultsMap) {
2354
- finalToolResultHistoryItems.push({
2355
- role: "assistant" as const,
2356
- content: null,
2357
- tool_calls: [{
2358
- id: toolName,
2359
- name: toolName,
2360
- arguments: toolArgsMap.get(toolName) || {},
2361
- }],
2362
- });
2363
- finalToolResultHistoryItems.push({
2364
- role: "tool" as const,
2365
- tool_call_id: toolName,
2366
- name: toolName,
2367
- content: toolResult,
2368
- });
2369
- }
2370
-
2371
- const finalHistory = [...history, ...finalToolResultHistoryItems];
2372
- const agentOptions = this.agent.getAgentOptions();
2373
-
2374
- try {
2375
- const textResult = await agentOptions.provider.generateMessage({
2376
- prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
2377
- history: finalHistory,
2378
- context,
2379
- tools: [], // No tools - force text response
2380
- parameters: responseSchema ? {
2381
- jsonSchema: responseSchema,
2382
- schemaName: "tool_final_text",
2383
- } : undefined,
2384
- signal,
2385
- });
2386
-
2387
- finalMessage = textResult.structured?.message || textResult.message;
2388
- if (textResult.structured) {
2389
- followUpStructured = textResult.structured;
2390
- }
2391
-
2392
- logger.debug(`[ResponseModal] Generated final text response after tool loop:`, {
2393
- hasMessage: !!finalMessage,
2394
- messageLength: finalMessage?.length || 0,
2395
- });
2396
- } catch (error) {
2397
- logger.error(`[ResponseModal] Failed to generate final text response after tool loop:`, error);
2398
- // finalMessage remains undefined; caller will use original message as fallback
2399
- }
2400
- }
2401
-
2402
- logger.debug(`[ResponseModal] Tool loop completed:`, {
2403
- totalIterations: toolLoopCount,
2404
- hasFinalMessage: !!finalMessage,
2405
- finalMessageLength: finalMessage?.length || 0,
2406
- finalToolCallsCount: toolCalls?.length || 0,
2407
- });
2408
-
2409
- return {
2410
- session,
2411
- finalToolCalls: toolCalls,
2412
- finalMessage,
2413
- structured: followUpStructured,
2414
- };
2415
- } catch (error) {
2416
- throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
2417
- toolCallsCount: params.toolCalls?.length || 0,
2418
- availableToolsCount: params.availableTools.length
2419
- });
2420
- }
2421
- } /**
2422
1307
  * Unified data collection from AI response
2423
1308
  * @private
2424
1309
  */
@@ -2599,10 +1484,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2599
1484
  return nextSession;
2600
1485
  }
2601
1486
 
2602
- /**
2603
- * Stream flow completion response
2604
- * @private
2605
- */
2606
1487
  /**
2607
1488
  * Stream a flow completion as a single terminal chunk.
2608
1489
  *
@@ -2634,8 +1515,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2634
1515
  history,
2635
1516
  });
2636
1517
 
2637
- await this.finalizeSession(session, context);
2638
-
2639
1518
  yield {
2640
1519
  delta: '',
2641
1520
  accumulated: '',
@@ -2729,11 +1608,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2729
1608
  });
2730
1609
 
2731
1610
  for await (const chunk of stream) {
2732
- // Update current session if we have one
2733
- if (chunk.done) {
2734
- await this.finalizeSession(session, context);
2735
- }
2736
-
2737
1611
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2738
1612
  // - executedSteps: empty for fallback (no flow/step execution)
2739
1613
  // - stoppedReason: undefined for fallback (no flow context)
@@ -2754,49 +1628,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2754
1628
  }
2755
1629
  }
2756
1630
 
2757
- /**
2758
- * Handle session persistence and finalization
2759
- * @private
2760
- */
2761
- private async finalizeSession(session: SessionState<TData>, context: TContext): Promise<void> {
2762
- // Auto-save session step to persistence if configured
2763
- const persistenceManager = this.agent.getPersistenceManager();
2764
- const agentOptions = this.agent.getAgentOptions();
2765
- if (
2766
- persistenceManager &&
2767
- session.id &&
2768
- (this.options?.enableAutoSave !== false && agentOptions.persistence?.autoSave !== false)
2769
- ) {
2770
- await persistenceManager.saveSessionState(session.id, session);
2771
- logger.debug(`[ResponseModal] Auto-saved session step to persistence: ${session.id}`);
2772
- }
2773
-
2774
- // Execute finalize function
2775
- await this.executeStepFinalize(session, context);
2776
-
2777
- // Update current session if we have one
2778
- const currentSession = this.agent.currentSession;
2779
- if (currentSession) {
2780
- this.agent.currentSession = session;
2781
- }
2782
- }
2783
1631
  // ============================================================================
2784
1632
  // UTILITY METHODS - Helper methods for tool management and other utilities
2785
1633
  // ============================================================================
2786
1634
 
2787
1635
 
2788
- /**
2789
- * Find an available tool by name for the given flow.
2790
- * Delegates to ToolManager for unified tool resolution.
2791
- * @private
2792
- */
2793
- private findAvailableTool(
2794
- toolName: string,
2795
- flow?: Flow<TContext, TData>
2796
- ): Tool<TContext, TData> | undefined {
2797
- return this.getToolManager().find(toolName, undefined, undefined, flow);
2798
- }
2799
-
2800
1636
  /**
2801
1637
  * Collect all available tools for the given flow and step context.
2802
1638
  * Delegates to ToolManager for unified tool resolution and deduplication.
@@ -2820,64 +1656,4 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2820
1656
  }));
2821
1657
  }
2822
1658
 
2823
- /**
2824
- * Execute a prepare or finalize function/tool
2825
- * @private
2826
- */
2827
- private async executePrepareFinalize(
2828
- prepareOrFinalize:
2829
- | string
2830
- | Tool<TContext, TData>
2831
- | ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult>)
2832
- | undefined,
2833
- context: TContext,
2834
- data?: Partial<TData>,
2835
- flow?: Flow<TContext, TData>,
2836
- step?: Step<TContext, TData>
2837
- ): Promise<void> {
2838
- if (!prepareOrFinalize) return;
2839
-
2840
- if (typeof prepareOrFinalize === "function") {
2841
- // It's a function - call it directly
2842
- await prepareOrFinalize(context, data);
2843
- } else {
2844
- // It's a tool reference - find and execute the tool
2845
- let tool: Tool<TContext, TData> | undefined;
2846
-
2847
- if (typeof prepareOrFinalize === "string") {
2848
- // Tool ID - use ToolManager for unified resolution
2849
- tool = this.getToolManager().find(prepareOrFinalize, undefined, step, flow);
2850
- } else {
2851
- // Tool object - use directly
2852
- tool = prepareOrFinalize;
2853
- }
2854
-
2855
- if (tool) {
2856
- // Use ToolManager for unified tool execution
2857
- const result = await this.getToolManager().executeTool({
2858
- tool,
2859
- context,
2860
- updateContext: this.agent.updateContext.bind(this.agent),
2861
- updateData: this.agent.updateCollectedData.bind(this.agent),
2862
- history: [], // Empty history for prepare/finalize
2863
- data,
2864
- });
2865
-
2866
- if (!result.success) {
2867
- logger.error(
2868
- `[ResponseModal] Tool execution failed in prepare/finalize: ${result.error}`
2869
- );
2870
- throw new Error(`Tool execution failed: ${result.error}`);
2871
- }
2872
- } else {
2873
- logger.warn(
2874
- `[ResponseModal] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
2875
- ? prepareOrFinalize
2876
- : "inline tool"
2877
- }`
2878
- );
2879
- }
2880
- }
2881
- }
2882
-
2883
- }
1659
+ }