@falai/agent 1.2.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +40 -886
  2. package/dist/adapters/MemoryAdapter.js +2 -2
  3. package/dist/adapters/MemoryAdapter.js.map +1 -1
  4. package/dist/adapters/MongoAdapter.js +2 -2
  5. package/dist/adapters/MongoAdapter.js.map +1 -1
  6. package/dist/adapters/OpenSearchAdapter.d.ts.map +1 -1
  7. package/dist/adapters/OpenSearchAdapter.js +9 -7
  8. package/dist/adapters/OpenSearchAdapter.js.map +1 -1
  9. package/dist/adapters/PostgreSQLAdapter.d.ts +14 -0
  10. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  11. package/dist/adapters/PostgreSQLAdapter.js +25 -9
  12. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  13. package/dist/adapters/PrismaAdapter.js +5 -5
  14. package/dist/adapters/PrismaAdapter.js.map +1 -1
  15. package/dist/adapters/RedisAdapter.js +2 -2
  16. package/dist/adapters/RedisAdapter.js.map +1 -1
  17. package/dist/adapters/SQLiteAdapter.d.ts +17 -0
  18. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  19. package/dist/adapters/SQLiteAdapter.js +30 -11
  20. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  21. package/dist/cjs/adapters/MemoryAdapter.js +2 -2
  22. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/MongoAdapter.js +2 -2
  24. package/dist/cjs/adapters/MongoAdapter.js.map +1 -1
  25. package/dist/cjs/adapters/OpenSearchAdapter.d.ts.map +1 -1
  26. package/dist/cjs/adapters/OpenSearchAdapter.js +9 -7
  27. package/dist/cjs/adapters/OpenSearchAdapter.js.map +1 -1
  28. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts +14 -0
  29. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  30. package/dist/cjs/adapters/PostgreSQLAdapter.js +25 -9
  31. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  32. package/dist/cjs/adapters/PrismaAdapter.js +5 -5
  33. package/dist/cjs/adapters/PrismaAdapter.js.map +1 -1
  34. package/dist/cjs/adapters/RedisAdapter.js +2 -2
  35. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  36. package/dist/cjs/adapters/SQLiteAdapter.d.ts +17 -0
  37. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  38. package/dist/cjs/adapters/SQLiteAdapter.js +30 -11
  39. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  40. package/dist/cjs/constants/index.d.ts +0 -9
  41. package/dist/cjs/constants/index.d.ts.map +1 -1
  42. package/dist/cjs/constants/index.js +2 -11
  43. package/dist/cjs/constants/index.js.map +1 -1
  44. package/dist/cjs/core/Agent.d.ts +119 -153
  45. package/dist/cjs/core/Agent.d.ts.map +1 -1
  46. package/dist/cjs/core/Agent.js +471 -324
  47. package/dist/cjs/core/Agent.js.map +1 -1
  48. package/dist/cjs/core/AutoChainExecutor.d.ts +107 -0
  49. package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -0
  50. package/dist/cjs/core/AutoChainExecutor.js +297 -0
  51. package/dist/cjs/core/AutoChainExecutor.js.map +1 -0
  52. package/dist/cjs/core/BranchEvaluator.d.ts +54 -0
  53. package/dist/cjs/core/BranchEvaluator.d.ts.map +1 -0
  54. package/dist/cjs/core/BranchEvaluator.js +130 -0
  55. package/dist/cjs/core/BranchEvaluator.js.map +1 -0
  56. package/dist/cjs/core/DirectiveBus.d.ts +88 -0
  57. package/dist/cjs/core/DirectiveBus.d.ts.map +1 -0
  58. package/dist/cjs/core/DirectiveBus.js +196 -0
  59. package/dist/cjs/core/DirectiveBus.js.map +1 -0
  60. package/dist/cjs/core/DirectiveChainTracker.d.ts +49 -0
  61. package/dist/cjs/core/DirectiveChainTracker.d.ts.map +1 -0
  62. package/dist/cjs/core/DirectiveChainTracker.js +121 -0
  63. package/dist/cjs/core/DirectiveChainTracker.js.map +1 -0
  64. package/dist/cjs/core/Flow.d.ts +186 -0
  65. package/dist/cjs/core/Flow.d.ts.map +1 -0
  66. package/dist/cjs/core/Flow.js +550 -0
  67. package/dist/cjs/core/Flow.js.map +1 -0
  68. package/dist/cjs/core/FlowRouter.d.ts +182 -0
  69. package/dist/cjs/core/FlowRouter.d.ts.map +1 -0
  70. package/dist/cjs/core/{RoutingEngine.js → FlowRouter.js} +323 -306
  71. package/dist/cjs/core/FlowRouter.js.map +1 -0
  72. package/dist/cjs/core/PersistenceManager.d.ts +2 -2
  73. package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
  74. package/dist/cjs/core/PersistenceManager.js +7 -7
  75. package/dist/cjs/core/PersistenceManager.js.map +1 -1
  76. package/dist/cjs/core/PromptComposer.d.ts +21 -8
  77. package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
  78. package/dist/cjs/core/PromptComposer.js +182 -105
  79. package/dist/cjs/core/PromptComposer.js.map +1 -1
  80. package/dist/cjs/core/PromptSectionCache.d.ts +1 -1
  81. package/dist/cjs/core/PromptSectionCache.js +1 -1
  82. package/dist/cjs/core/ResponseEngine.d.ts +18 -8
  83. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  84. package/dist/cjs/core/ResponseEngine.js +38 -36
  85. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  86. package/dist/cjs/core/ResponseModal.d.ts +73 -56
  87. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  88. package/dist/cjs/core/ResponseModal.js +1196 -1015
  89. package/dist/cjs/core/ResponseModal.js.map +1 -1
  90. package/dist/cjs/core/ResponsePipeline.d.ts +124 -26
  91. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  92. package/dist/cjs/core/ResponsePipeline.js +524 -134
  93. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  94. package/dist/cjs/core/SignalEvaluator.d.ts +86 -0
  95. package/dist/cjs/core/SignalEvaluator.d.ts.map +1 -0
  96. package/dist/cjs/core/SignalEvaluator.js +333 -0
  97. package/dist/cjs/core/SignalEvaluator.js.map +1 -0
  98. package/dist/cjs/core/SignalProcessor.d.ts +152 -0
  99. package/dist/cjs/core/SignalProcessor.d.ts.map +1 -0
  100. package/dist/cjs/core/SignalProcessor.js +562 -0
  101. package/dist/cjs/core/SignalProcessor.js.map +1 -0
  102. package/dist/cjs/core/Step.d.ts +43 -32
  103. package/dist/cjs/core/Step.d.ts.map +1 -1
  104. package/dist/cjs/core/Step.js +221 -126
  105. package/dist/cjs/core/Step.js.map +1 -1
  106. package/dist/cjs/core/StreamingToolExecutor.d.ts +2 -2
  107. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
  108. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
  109. package/dist/cjs/core/ToolManager.d.ts +44 -13
  110. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  111. package/dist/cjs/core/ToolManager.js +174 -91
  112. package/dist/cjs/core/ToolManager.js.map +1 -1
  113. package/dist/cjs/core/createAgent.d.ts +35 -0
  114. package/dist/cjs/core/createAgent.d.ts.map +1 -0
  115. package/dist/cjs/core/createAgent.js +39 -0
  116. package/dist/cjs/core/createAgent.js.map +1 -0
  117. package/dist/cjs/core/flow-namespace.d.ts +49 -0
  118. package/dist/cjs/core/flow-namespace.d.ts.map +1 -0
  119. package/dist/cjs/core/flow-namespace.js +171 -0
  120. package/dist/cjs/core/flow-namespace.js.map +1 -0
  121. package/dist/cjs/index.d.ts +11 -14
  122. package/dist/cjs/index.d.ts.map +1 -1
  123. package/dist/cjs/index.js +18 -22
  124. package/dist/cjs/index.js.map +1 -1
  125. package/dist/cjs/providers/GeminiProvider.d.ts +3 -3
  126. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  127. package/dist/cjs/providers/GeminiProvider.js +16 -14
  128. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  129. package/dist/cjs/types/agent.d.ts +183 -54
  130. package/dist/cjs/types/agent.d.ts.map +1 -1
  131. package/dist/cjs/types/agent.js +0 -6
  132. package/dist/cjs/types/agent.js.map +1 -1
  133. package/dist/cjs/types/ai.d.ts +3 -3
  134. package/dist/cjs/types/ai.d.ts.map +1 -1
  135. package/dist/cjs/types/errors.d.ts +15 -0
  136. package/dist/cjs/types/errors.d.ts.map +1 -0
  137. package/dist/cjs/types/errors.js +22 -0
  138. package/dist/cjs/types/errors.js.map +1 -0
  139. package/dist/cjs/types/flow.d.ts +513 -0
  140. package/dist/cjs/types/flow.d.ts.map +1 -0
  141. package/dist/cjs/types/{route.js → flow.js} +2 -2
  142. package/dist/cjs/types/flow.js.map +1 -0
  143. package/dist/cjs/types/index.d.ts +7 -6
  144. package/dist/cjs/types/index.d.ts.map +1 -1
  145. package/dist/cjs/types/index.js +6 -2
  146. package/dist/cjs/types/index.js.map +1 -1
  147. package/dist/cjs/types/persistence.d.ts +11 -7
  148. package/dist/cjs/types/persistence.d.ts.map +1 -1
  149. package/dist/cjs/types/routing.d.ts +1 -1
  150. package/dist/cjs/types/routing.d.ts.map +1 -1
  151. package/dist/cjs/types/session.d.ts +24 -23
  152. package/dist/cjs/types/session.d.ts.map +1 -1
  153. package/dist/cjs/types/signals.d.ts +248 -0
  154. package/dist/cjs/types/signals.d.ts.map +1 -0
  155. package/dist/cjs/types/signals.js +11 -0
  156. package/dist/cjs/types/signals.js.map +1 -0
  157. package/dist/cjs/types/template.d.ts +2 -8
  158. package/dist/cjs/types/template.d.ts.map +1 -1
  159. package/dist/cjs/types/tool.d.ts +36 -29
  160. package/dist/cjs/types/tool.d.ts.map +1 -1
  161. package/dist/cjs/types/tool.js +1 -1
  162. package/dist/cjs/types/tool.js.map +1 -1
  163. package/dist/cjs/utils/condition.d.ts +7 -1
  164. package/dist/cjs/utils/condition.d.ts.map +1 -1
  165. package/dist/cjs/utils/condition.js.map +1 -1
  166. package/dist/cjs/utils/id.d.ts +13 -5
  167. package/dist/cjs/utils/id.d.ts.map +1 -1
  168. package/dist/cjs/utils/id.js +24 -10
  169. package/dist/cjs/utils/id.js.map +1 -1
  170. package/dist/cjs/utils/index.d.ts +2 -2
  171. package/dist/cjs/utils/index.d.ts.map +1 -1
  172. package/dist/cjs/utils/index.js +7 -3
  173. package/dist/cjs/utils/index.js.map +1 -1
  174. package/dist/cjs/utils/session.d.ts +44 -5
  175. package/dist/cjs/utils/session.d.ts.map +1 -1
  176. package/dist/cjs/utils/session.js +197 -38
  177. package/dist/cjs/utils/session.js.map +1 -1
  178. package/dist/constants/index.d.ts +0 -9
  179. package/dist/constants/index.d.ts.map +1 -1
  180. package/dist/constants/index.js +3 -9
  181. package/dist/constants/index.js.map +1 -1
  182. package/dist/core/Agent.d.ts +119 -153
  183. package/dist/core/Agent.d.ts.map +1 -1
  184. package/dist/core/Agent.js +472 -325
  185. package/dist/core/Agent.js.map +1 -1
  186. package/dist/core/AutoChainExecutor.d.ts +107 -0
  187. package/dist/core/AutoChainExecutor.d.ts.map +1 -0
  188. package/dist/core/AutoChainExecutor.js +293 -0
  189. package/dist/core/AutoChainExecutor.js.map +1 -0
  190. package/dist/core/BranchEvaluator.d.ts +54 -0
  191. package/dist/core/BranchEvaluator.d.ts.map +1 -0
  192. package/dist/core/BranchEvaluator.js +126 -0
  193. package/dist/core/BranchEvaluator.js.map +1 -0
  194. package/dist/core/DirectiveBus.d.ts +88 -0
  195. package/dist/core/DirectiveBus.d.ts.map +1 -0
  196. package/dist/core/DirectiveBus.js +192 -0
  197. package/dist/core/DirectiveBus.js.map +1 -0
  198. package/dist/core/DirectiveChainTracker.d.ts +49 -0
  199. package/dist/core/DirectiveChainTracker.d.ts.map +1 -0
  200. package/dist/core/DirectiveChainTracker.js +117 -0
  201. package/dist/core/DirectiveChainTracker.js.map +1 -0
  202. package/dist/core/Flow.d.ts +186 -0
  203. package/dist/core/Flow.d.ts.map +1 -0
  204. package/dist/core/Flow.js +546 -0
  205. package/dist/core/Flow.js.map +1 -0
  206. package/dist/core/FlowRouter.d.ts +182 -0
  207. package/dist/core/FlowRouter.d.ts.map +1 -0
  208. package/dist/core/{RoutingEngine.js → FlowRouter.js} +322 -305
  209. package/dist/core/FlowRouter.js.map +1 -0
  210. package/dist/core/PersistenceManager.d.ts +2 -2
  211. package/dist/core/PersistenceManager.d.ts.map +1 -1
  212. package/dist/core/PersistenceManager.js +7 -7
  213. package/dist/core/PersistenceManager.js.map +1 -1
  214. package/dist/core/PromptComposer.d.ts +21 -8
  215. package/dist/core/PromptComposer.d.ts.map +1 -1
  216. package/dist/core/PromptComposer.js +183 -106
  217. package/dist/core/PromptComposer.js.map +1 -1
  218. package/dist/core/PromptSectionCache.d.ts +1 -1
  219. package/dist/core/PromptSectionCache.js +1 -1
  220. package/dist/core/ResponseEngine.d.ts +18 -8
  221. package/dist/core/ResponseEngine.d.ts.map +1 -1
  222. package/dist/core/ResponseEngine.js +38 -36
  223. package/dist/core/ResponseEngine.js.map +1 -1
  224. package/dist/core/ResponseModal.d.ts +73 -56
  225. package/dist/core/ResponseModal.d.ts.map +1 -1
  226. package/dist/core/ResponseModal.js +1198 -1017
  227. package/dist/core/ResponseModal.js.map +1 -1
  228. package/dist/core/ResponsePipeline.d.ts +124 -26
  229. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  230. package/dist/core/ResponsePipeline.js +524 -135
  231. package/dist/core/ResponsePipeline.js.map +1 -1
  232. package/dist/core/SignalEvaluator.d.ts +86 -0
  233. package/dist/core/SignalEvaluator.d.ts.map +1 -0
  234. package/dist/core/SignalEvaluator.js +326 -0
  235. package/dist/core/SignalEvaluator.js.map +1 -0
  236. package/dist/core/SignalProcessor.d.ts +152 -0
  237. package/dist/core/SignalProcessor.d.ts.map +1 -0
  238. package/dist/core/SignalProcessor.js +555 -0
  239. package/dist/core/SignalProcessor.js.map +1 -0
  240. package/dist/core/Step.d.ts +43 -32
  241. package/dist/core/Step.d.ts.map +1 -1
  242. package/dist/core/Step.js +220 -126
  243. package/dist/core/Step.js.map +1 -1
  244. package/dist/core/StreamingToolExecutor.d.ts +2 -2
  245. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  246. package/dist/core/StreamingToolExecutor.js.map +1 -1
  247. package/dist/core/ToolManager.d.ts +44 -13
  248. package/dist/core/ToolManager.d.ts.map +1 -1
  249. package/dist/core/ToolManager.js +174 -91
  250. package/dist/core/ToolManager.js.map +1 -1
  251. package/dist/core/createAgent.d.ts +35 -0
  252. package/dist/core/createAgent.d.ts.map +1 -0
  253. package/dist/core/createAgent.js +36 -0
  254. package/dist/core/createAgent.js.map +1 -0
  255. package/dist/core/flow-namespace.d.ts +49 -0
  256. package/dist/core/flow-namespace.d.ts.map +1 -0
  257. package/dist/core/flow-namespace.js +168 -0
  258. package/dist/core/flow-namespace.js.map +1 -0
  259. package/dist/index.d.ts +11 -14
  260. package/dist/index.d.ts.map +1 -1
  261. package/dist/index.js +9 -12
  262. package/dist/index.js.map +1 -1
  263. package/dist/providers/GeminiProvider.d.ts +3 -3
  264. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  265. package/dist/providers/GeminiProvider.js +16 -14
  266. package/dist/providers/GeminiProvider.js.map +1 -1
  267. package/dist/types/agent.d.ts +183 -54
  268. package/dist/types/agent.d.ts.map +1 -1
  269. package/dist/types/agent.js +0 -6
  270. package/dist/types/agent.js.map +1 -1
  271. package/dist/types/ai.d.ts +3 -3
  272. package/dist/types/ai.d.ts.map +1 -1
  273. package/dist/types/errors.d.ts +15 -0
  274. package/dist/types/errors.d.ts.map +1 -0
  275. package/dist/types/errors.js +18 -0
  276. package/dist/types/errors.js.map +1 -0
  277. package/dist/types/flow.d.ts +513 -0
  278. package/dist/types/flow.d.ts.map +1 -0
  279. package/dist/types/flow.js +5 -0
  280. package/dist/types/flow.js.map +1 -0
  281. package/dist/types/index.d.ts +7 -6
  282. package/dist/types/index.d.ts.map +1 -1
  283. package/dist/types/index.js +4 -1
  284. package/dist/types/index.js.map +1 -1
  285. package/dist/types/persistence.d.ts +11 -7
  286. package/dist/types/persistence.d.ts.map +1 -1
  287. package/dist/types/routing.d.ts +1 -1
  288. package/dist/types/routing.d.ts.map +1 -1
  289. package/dist/types/session.d.ts +24 -23
  290. package/dist/types/session.d.ts.map +1 -1
  291. package/dist/types/signals.d.ts +248 -0
  292. package/dist/types/signals.d.ts.map +1 -0
  293. package/dist/types/signals.js +10 -0
  294. package/dist/types/signals.js.map +1 -0
  295. package/dist/types/template.d.ts +2 -8
  296. package/dist/types/template.d.ts.map +1 -1
  297. package/dist/types/tool.d.ts +36 -29
  298. package/dist/types/tool.d.ts.map +1 -1
  299. package/dist/types/tool.js +1 -1
  300. package/dist/types/tool.js.map +1 -1
  301. package/dist/utils/condition.d.ts +7 -1
  302. package/dist/utils/condition.d.ts.map +1 -1
  303. package/dist/utils/condition.js.map +1 -1
  304. package/dist/utils/id.d.ts +13 -5
  305. package/dist/utils/id.d.ts.map +1 -1
  306. package/dist/utils/id.js +22 -9
  307. package/dist/utils/id.js.map +1 -1
  308. package/dist/utils/index.d.ts +2 -2
  309. package/dist/utils/index.d.ts.map +1 -1
  310. package/dist/utils/index.js +2 -2
  311. package/dist/utils/index.js.map +1 -1
  312. package/dist/utils/session.d.ts +44 -5
  313. package/dist/utils/session.d.ts.map +1 -1
  314. package/dist/utils/session.js +193 -37
  315. package/dist/utils/session.js.map +1 -1
  316. package/docs/README.md +15 -202
  317. package/docs/concepts/architecture.md +281 -0
  318. package/docs/concepts/directives.md +400 -0
  319. package/docs/concepts/pipeline.md +399 -0
  320. package/docs/guides/branching.md +263 -0
  321. package/docs/guides/compaction.md +163 -0
  322. package/docs/guides/conditions.md +167 -0
  323. package/docs/guides/error-handling.md +176 -0
  324. package/docs/guides/flow-control.md +409 -0
  325. package/docs/guides/instructions.md +210 -0
  326. package/docs/guides/persistence.md +182 -0
  327. package/docs/guides/streaming.md +137 -0
  328. package/docs/migration/README.md +15 -0
  329. package/docs/migration/route-to-flow.md +560 -0
  330. package/docs/migration/v1-to-v2.md +909 -0
  331. package/docs/reference/adapters.md +481 -0
  332. package/docs/reference/branches.md +241 -0
  333. package/docs/reference/create-agent.md +186 -0
  334. package/docs/reference/directive.md +243 -0
  335. package/docs/reference/errors.md +122 -0
  336. package/docs/reference/flow.md +238 -0
  337. package/docs/reference/instruction.md +177 -0
  338. package/docs/reference/pre-directive.md +131 -0
  339. package/docs/reference/providers.md +227 -0
  340. package/docs/reference/signals.md +356 -0
  341. package/docs/reference/step.md +339 -0
  342. package/docs/reference/tool.md +269 -0
  343. package/docs/start/01-install.md +81 -0
  344. package/docs/start/02-first-agent.md +196 -0
  345. package/docs/start/03-collect-data.md +222 -0
  346. package/docs/start/04-add-tools.md +276 -0
  347. package/docs/start/05-go-to-production.md +216 -0
  348. package/examples/01-quickstart.ts +20 -0
  349. package/examples/02-data-extraction.ts +90 -0
  350. package/examples/03-tools.ts +136 -0
  351. package/examples/04-instructions.ts +100 -0
  352. package/examples/05-branching.ts +140 -0
  353. package/examples/06-flow-control.ts +103 -0
  354. package/examples/07-streaming.ts +69 -0
  355. package/examples/08-persistence.ts +98 -0
  356. package/examples/09-signals.ts +144 -0
  357. package/examples/tsconfig.json +30 -0
  358. package/package.json +2 -1
  359. package/src/adapters/MemoryAdapter.ts +3 -3
  360. package/src/adapters/MongoAdapter.ts +3 -3
  361. package/src/adapters/OpenSearchAdapter.ts +10 -8
  362. package/src/adapters/PostgreSQLAdapter.ts +26 -10
  363. package/src/adapters/PrismaAdapter.ts +6 -6
  364. package/src/adapters/RedisAdapter.ts +3 -3
  365. package/src/adapters/SQLiteAdapter.ts +31 -12
  366. package/src/constants/index.ts +2 -10
  367. package/src/core/Agent.ts +585 -374
  368. package/src/core/AutoChainExecutor.ts +440 -0
  369. package/src/core/BranchEvaluator.ts +167 -0
  370. package/src/core/DirectiveBus.ts +248 -0
  371. package/src/core/DirectiveChainTracker.ts +144 -0
  372. package/src/core/Flow.ts +666 -0
  373. package/src/core/{RoutingEngine.ts → FlowRouter.ts} +385 -365
  374. package/src/core/PersistenceManager.ts +8 -8
  375. package/src/core/PromptComposer.ts +209 -140
  376. package/src/core/PromptSectionCache.ts +1 -1
  377. package/src/core/ResponseEngine.ts +61 -46
  378. package/src/core/ResponseModal.ts +1458 -1241
  379. package/src/core/ResponsePipeline.ts +675 -173
  380. package/src/core/SignalEvaluator.ts +420 -0
  381. package/src/core/SignalProcessor.ts +723 -0
  382. package/src/core/Step.ts +279 -176
  383. package/src/core/StreamingToolExecutor.ts +4 -4
  384. package/src/core/ToolManager.ts +200 -97
  385. package/src/core/createAgent.ts +40 -0
  386. package/src/core/flow-namespace.ts +219 -0
  387. package/src/index.ts +42 -36
  388. package/src/providers/GeminiProvider.ts +17 -15
  389. package/src/types/agent.ts +182 -53
  390. package/src/types/ai.ts +3 -3
  391. package/src/types/errors.ts +18 -0
  392. package/src/types/flow.ts +590 -0
  393. package/src/types/index.ts +43 -16
  394. package/src/types/persistence.ts +12 -8
  395. package/src/types/routing.ts +1 -1
  396. package/src/types/session.ts +26 -23
  397. package/src/types/signals.ts +321 -0
  398. package/src/types/template.ts +3 -11
  399. package/src/types/tool.ts +50 -42
  400. package/src/utils/condition.ts +13 -4
  401. package/src/utils/id.ts +27 -9
  402. package/src/utils/index.ts +6 -2
  403. package/src/utils/session.ts +238 -42
  404. package/dist/cjs/core/BatchExecutor.d.ts +0 -359
  405. package/dist/cjs/core/BatchExecutor.d.ts.map +0 -1
  406. package/dist/cjs/core/BatchExecutor.js +0 -861
  407. package/dist/cjs/core/BatchExecutor.js.map +0 -1
  408. package/dist/cjs/core/BatchPromptBuilder.d.ts +0 -89
  409. package/dist/cjs/core/BatchPromptBuilder.d.ts.map +0 -1
  410. package/dist/cjs/core/BatchPromptBuilder.js +0 -223
  411. package/dist/cjs/core/BatchPromptBuilder.js.map +0 -1
  412. package/dist/cjs/core/Route.d.ts +0 -180
  413. package/dist/cjs/core/Route.d.ts.map +0 -1
  414. package/dist/cjs/core/Route.js +0 -542
  415. package/dist/cjs/core/Route.js.map +0 -1
  416. package/dist/cjs/core/RoutingEngine.d.ts +0 -185
  417. package/dist/cjs/core/RoutingEngine.d.ts.map +0 -1
  418. package/dist/cjs/core/RoutingEngine.js.map +0 -1
  419. package/dist/cjs/types/route.d.ts +0 -336
  420. package/dist/cjs/types/route.d.ts.map +0 -1
  421. package/dist/cjs/types/route.js.map +0 -1
  422. package/dist/core/BatchExecutor.d.ts +0 -359
  423. package/dist/core/BatchExecutor.d.ts.map +0 -1
  424. package/dist/core/BatchExecutor.js +0 -856
  425. package/dist/core/BatchExecutor.js.map +0 -1
  426. package/dist/core/BatchPromptBuilder.d.ts +0 -89
  427. package/dist/core/BatchPromptBuilder.d.ts.map +0 -1
  428. package/dist/core/BatchPromptBuilder.js +0 -219
  429. package/dist/core/BatchPromptBuilder.js.map +0 -1
  430. package/dist/core/Route.d.ts +0 -180
  431. package/dist/core/Route.d.ts.map +0 -1
  432. package/dist/core/Route.js +0 -538
  433. package/dist/core/Route.js.map +0 -1
  434. package/dist/core/RoutingEngine.d.ts +0 -185
  435. package/dist/core/RoutingEngine.d.ts.map +0 -1
  436. package/dist/core/RoutingEngine.js.map +0 -1
  437. package/dist/types/route.d.ts +0 -336
  438. package/dist/types/route.d.ts.map +0 -1
  439. package/dist/types/route.js +0 -5
  440. package/dist/types/route.js.map +0 -1
  441. package/docs/CONTRIBUTING.md +0 -521
  442. package/docs/api/README.md +0 -3299
  443. package/docs/api/overview.md +0 -1410
  444. package/docs/architecture/data-extraction-flow.md +0 -360
  445. package/docs/architecture/multi-step-execution.md +0 -277
  446. package/docs/core/agent/README.md +0 -938
  447. package/docs/core/agent/context-management.md +0 -796
  448. package/docs/core/agent/rules-and-prohibitions.md +0 -113
  449. package/docs/core/agent/session-management.md +0 -693
  450. package/docs/core/ai-integration/prompt-composition.md +0 -355
  451. package/docs/core/ai-integration/providers.md +0 -515
  452. package/docs/core/ai-integration/response-processing.md +0 -433
  453. package/docs/core/conversation-flows/data-collection.md +0 -772
  454. package/docs/core/conversation-flows/route-dsl.md +0 -509
  455. package/docs/core/conversation-flows/routes.md +0 -249
  456. package/docs/core/conversation-flows/step-transitions.md +0 -731
  457. package/docs/core/conversation-flows/steps.md +0 -268
  458. package/docs/core/error-handling.md +0 -830
  459. package/docs/core/persistence/adapters.md +0 -255
  460. package/docs/core/persistence/session-storage.md +0 -656
  461. package/docs/core/routing/intelligent-routing.md +0 -470
  462. package/docs/core/tools/enhanced-tool.md +0 -186
  463. package/docs/core/tools/streaming-execution.md +0 -161
  464. package/docs/core/tools/tool-definition.md +0 -970
  465. package/docs/core/tools/tool-scoping.md +0 -819
  466. package/docs/guides/advanced-patterns/publishing.md +0 -186
  467. package/docs/guides/context-compaction.md +0 -96
  468. package/docs/guides/error-handling-patterns.md +0 -578
  469. package/docs/guides/getting-started/README.md +0 -795
  470. package/docs/guides/migration/README.md +0 -101
  471. package/docs/guides/migration/flexible-routing-conditions.md +0 -375
  472. package/docs/guides/migration/multi-step-execution.md +0 -393
  473. package/docs/guides/migration/response-modal-refactor.md +0 -518
  474. package/docs/guides/prompt-optimization.md +0 -164
  475. package/examples/advanced-patterns/context-compaction.ts +0 -223
  476. package/examples/advanced-patterns/knowledge-based-agent.ts +0 -735
  477. package/examples/advanced-patterns/persistent-onboarding.ts +0 -728
  478. package/examples/advanced-patterns/route-lifecycle-hooks.ts +0 -556
  479. package/examples/advanced-patterns/streaming-responses.ts +0 -656
  480. package/examples/ai-providers/anthropic-integration.ts +0 -388
  481. package/examples/ai-providers/openai-integration.ts +0 -228
  482. package/examples/condition-patterns/function-only-conditions.ts +0 -365
  483. package/examples/condition-patterns/mixed-array-conditions.ts +0 -477
  484. package/examples/condition-patterns/route-skipif-patterns.ts +0 -468
  485. package/examples/condition-patterns/step-skipif-patterns.ts +0 -0
  486. package/examples/condition-patterns/string-only-conditions.ts +0 -296
  487. package/examples/conversation-flows/completion-transitions.ts +0 -318
  488. package/examples/core-concepts/basic-agent.ts +0 -503
  489. package/examples/core-concepts/modern-streaming-api.ts +0 -309
  490. package/examples/core-concepts/schema-driven-extraction.ts +0 -332
  491. package/examples/core-concepts/session-management.ts +0 -494
  492. package/examples/integrations/database-integration.ts +0 -631
  493. package/examples/integrations/healthcare-integration.ts +0 -595
  494. package/examples/integrations/search-integration.ts +0 -530
  495. package/examples/integrations/server-session-management.ts +0 -307
  496. package/examples/persistence/custom-adapter.ts +0 -526
  497. package/examples/persistence/database-persistence.ts +0 -583
  498. package/examples/persistence/memory-sessions.ts +0 -495
  499. package/examples/persistence/prisma-schema.example.prisma +0 -74
  500. package/examples/persistence/redis-persistence.ts +0 -488
  501. package/examples/tools/basic-tools.ts +0 -765
  502. package/examples/tools/data-enrichment-tools.ts +0 -593
  503. package/examples/tools/enhanced-tool-metadata.ts +0 -268
  504. package/examples/tools/streaming-tool-execution.ts +0 -283
  505. package/src/core/BatchExecutor.ts +0 -1187
  506. package/src/core/BatchPromptBuilder.ts +0 -299
  507. package/src/core/Route.ts +0 -678
  508. package/src/types/route.ts +0 -392
@@ -2,14 +2,11 @@
2
2
  * ResponseModal handles all response generation logic for the Agent
3
3
  * Provides both streaming and non-streaming response generation with unified logic
4
4
  */
5
- import { Step } from "./Step";
6
5
  import { ResponseEngine } from "./ResponseEngine";
7
- import { ResponsePipeline } from "./ResponsePipeline";
8
- import { BatchExecutor } from "./BatchExecutor";
9
- import { BatchPromptBuilder } from "./BatchPromptBuilder";
10
- import { cloneDeep, mergeCollected, enterStep, getLastMessageFromHistory, render, logger, historyToEvents, eventsToHistory, serializeToolResult } from "../utils";
6
+ import { ResponsePipeline, hasDirectivePositionField } from "./ResponsePipeline";
7
+ import { AutoChainExecutor } from "./AutoChainExecutor";
8
+ import { cloneDeep, mergeCollected, enterStep, enterFlow, getLastMessageFromHistory, logger, historyToEvents, eventsToHistory, serializeToolResult, completeCurrentFlow, render } from "../utils";
11
9
  import { createTemplateContext } from "../utils/template";
12
- import { END_ROUTE_ID } from "../constants";
13
10
  /**
14
11
  * Error class for response generation failures
15
12
  */
@@ -28,7 +25,8 @@ export class ResponseGenerationError extends Error {
28
25
  */
29
26
  static fromError(error, phase, params, context) {
30
27
  const message = error instanceof Error ? error.message : String(error);
31
- return new ResponseGenerationError(`Response generation failed in ${phase}: ${message}`, { originalError: error, params, phase, context });
28
+ return new ResponseGenerationError(`[ResponseGenerationError] Response generation failed in ${phase}: ${message}. ` +
29
+ `Check provider configuration and the ${phase} phase handler.`, { originalError: error, params, phase, context });
32
30
  }
33
31
  /**
34
32
  * Check if an error is a ResponseGenerationError
@@ -48,12 +46,8 @@ export class ResponseModal {
48
46
  // Initialize response engine
49
47
  this.responseEngine = new ResponseEngine(this.agent.promptSectionCache);
50
48
  // Initialize response pipeline with agent dependencies
51
- this.responsePipeline = new ResponsePipeline(this.agent.getAgentOptions(), () => this.agent.getRoutes(), // Pass a function to get routes dynamically
52
- this.agent.getTools(), this.agent.getRoutingEngine(), this.agent.updateContext.bind(this.agent), this.agent.getUpdateDataMethod(), this.agent.updateCollectedData.bind(this.agent), this.getToolManager());
53
- // Initialize batch executor for multi-step execution
54
- this.batchExecutor = new BatchExecutor();
55
- // Initialize batch prompt builder for combined prompts
56
- this.batchPromptBuilder = new BatchPromptBuilder(this.agent.promptSectionCache);
49
+ this.responsePipeline = new ResponsePipeline(this.agent.getAgentOptions(), () => this.agent.getFlows(), // Pass a function to get flows dynamically
50
+ this.agent.getTools(), this.agent.getFlowRouter(), this.agent.updateContext.bind(this.agent), this.agent.getUpdateDataMethod(), this.agent.updateCollectedData.bind(this.agent), this.getToolManager(), this.agent.signalProcessor);
57
51
  }
58
52
  /**
59
53
  * Generate a non-streaming response using unified logic
@@ -69,7 +63,8 @@ export class ResponseModal {
69
63
  return result;
70
64
  }
71
65
  catch (error) {
72
- throw new ResponseGenerationError(`Failed to generate response: ${error instanceof Error ? error.message : String(error)}`, { originalError: error, params, phase: 'response_generation' });
66
+ throw new ResponseGenerationError(`[ResponseGenerationError] Response generation failed: ${error instanceof Error ? error.message : String(error)}. ` +
67
+ `Check provider configuration and network connectivity.`, { originalError: error, params, phase: 'response_generation' });
73
68
  }
74
69
  }
75
70
  /**
@@ -222,6 +217,17 @@ export class ResponseModal {
222
217
  logger.warn(`[ResponseModal] ToolManager not available on agent - tool execution will use fallback methods`);
223
218
  return undefined;
224
219
  }
220
+ /**
221
+ * Collect scoped instructions from agent, flow, and step into a ScopedInstructions value.
222
+ * @private
223
+ */
224
+ collectScopedInstructions(flow, step) {
225
+ return {
226
+ global: this.agent.instructions,
227
+ flow: flow ? { flowTitle: flow.title, items: flow.instructions } : undefined,
228
+ step: step ? { stepId: step.id, items: step.getInstructions() } : undefined,
229
+ };
230
+ }
225
231
  // UNIFIED RESPONSE LOGIC - Consolidates common logic between streaming and non-streaming
226
232
  // ============================================================================
227
233
  /**
@@ -234,7 +240,8 @@ export class ResponseModal {
234
240
  const { history: simpleHistory, contextOverride, signal } = params;
235
241
  // Validate input parameters
236
242
  if (!simpleHistory) {
237
- throw new ResponseGenerationError('History is required for response generation', { params, phase: 'validation' });
243
+ throw new ResponseGenerationError('[ResponseGenerationError] Missing history: history is required for response generation. ' +
244
+ 'Pass a valid history array to the respond/stream method.', { params, phase: 'validation' });
238
245
  }
239
246
  // Convert HistoryItem[] to Event[] for internal processing
240
247
  const historyEvents = historyToEvents(simpleHistory);
@@ -246,7 +253,7 @@ export class ResponseModal {
246
253
  try {
247
254
  // Set current context and session in pipeline for consistency
248
255
  this.responsePipeline.setContext(await this.agent.getContext());
249
- this.responsePipeline.setCurrentSession(this.agent.getCurrentSession());
256
+ this.responsePipeline.setCurrentSession(this.agent.currentSession);
250
257
  responseContext = await this.responsePipeline.prepareResponseContext({
251
258
  contextOverride,
252
259
  session: params.session ? cloneDeep(params.session) : undefined,
@@ -285,8 +292,8 @@ export class ResponseModal {
285
292
  catch (error) {
286
293
  throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
287
294
  }
288
- // PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use
289
- // Also performs pre-extraction and batch determination
295
+ // PHASE 2: ROUTING + STEP SELECTION - Determine which flow and step to use
296
+ // Performs pre-extraction and step selection
290
297
  let routingResult;
291
298
  try {
292
299
  routingResult = await this.handleUnifiedRoutingAndStepSelection({
@@ -303,14 +310,15 @@ export class ResponseModal {
303
310
  effectiveContext,
304
311
  session: routingResult.session,
305
312
  history,
306
- selectedRoute: routingResult.selectedRoute,
313
+ selectedFlow: routingResult.selectedFlow,
307
314
  selectedStep: routingResult.selectedStep,
308
315
  responseDirectives: routingResult.responseDirectives,
309
- isRouteComplete: routingResult.isRouteComplete,
310
- batchSteps: routingResult.batchSteps,
311
- batchStoppedReason: routingResult.batchStoppedReason,
312
- batchStoppedAtStep: routingResult.batchStoppedAtStep,
316
+ isFlowComplete: routingResult.isFlowComplete,
313
317
  signal,
318
+ signalFirings: routingResult.signalFirings,
319
+ signalPreDirective: routingResult.signalPreDirective,
320
+ signalHalted: routingResult.signalHalted,
321
+ signalHaltReply: routingResult.signalHaltReply,
314
322
  };
315
323
  }
316
324
  catch (error) {
@@ -327,27 +335,141 @@ export class ResponseModal {
327
335
  */
328
336
  async handleUnifiedRoutingAndStepSelection(params) {
329
337
  try {
330
- // Use the ResponsePipeline for optimized routing and step selection
331
- // This avoids duplicate logic and leverages existing optimizations
332
- // ResponsePipeline expects Event[] for history
338
+ // Create a fresh chain tracker for this turn (Requirement 22.1)
339
+ this.responsePipeline.createChainTracker();
340
+ // ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
341
+ // When the current step has collect fields AND pre-extraction populates at least
342
+ // one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
343
+ const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
344
+ if (routingSkipResult) {
345
+ // Even when routing is skipped, run pre-signal phase if processor is present
346
+ if (this.agent.signalProcessor) {
347
+ const signalResult = await this.responsePipeline.runPreSignalPhase(params.session, params.context, params.history);
348
+ // If signal halts, override the routing skip result
349
+ if (signalResult.mergedDirective?.halt) {
350
+ return {
351
+ ...routingSkipResult,
352
+ session: signalResult.updatedSession,
353
+ signalFirings: signalResult.firings,
354
+ signalHalted: true,
355
+ signalHaltReply: signalResult.mergedDirective.reply,
356
+ };
357
+ }
358
+ // If signal has position fields, override routing skip result
359
+ if (hasDirectivePositionField(signalResult.mergedDirective)) {
360
+ return this.applySignalPositionDirective(signalResult, params);
361
+ }
362
+ // Non-position directive: propagate for pre-LLM augmentation
363
+ return {
364
+ ...routingSkipResult,
365
+ session: signalResult.updatedSession,
366
+ signalFirings: signalResult.firings,
367
+ signalPreDirective: signalResult.mergedDirective || undefined,
368
+ };
369
+ }
370
+ return routingSkipResult;
371
+ }
372
+ // ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
373
+ // When signalProcessor is present, run pre-signals in parallel with routing.
374
+ // When absent, call the router directly (zero overhead, preserve current behavior).
375
+ if (this.agent.signalProcessor) {
376
+ // Run pre-signal phase in parallel with routing (Requirement 8.1)
377
+ const [signalResult, routingResult] = await Promise.all([
378
+ this.responsePipeline.runPreSignalPhase(params.session, params.context, params.history),
379
+ this.responsePipeline.handleRoutingAndStepSelection({
380
+ session: params.session,
381
+ history: params.history,
382
+ context: params.context,
383
+ signal: params.signal,
384
+ }),
385
+ ]);
386
+ // ── Requirement 8.2: halt → discard routing, skip LLM ────────────
387
+ if (signalResult.mergedDirective?.halt) {
388
+ return {
389
+ selectedFlow: undefined,
390
+ selectedStep: undefined,
391
+ session: signalResult.updatedSession,
392
+ isFlowComplete: false,
393
+ signalFirings: signalResult.firings,
394
+ signalHalted: true,
395
+ signalHaltReply: signalResult.mergedDirective.reply,
396
+ };
397
+ }
398
+ // ── Requirement 8.3: position directive → discard routing, apply signal position ──
399
+ if (hasDirectivePositionField(signalResult.mergedDirective)) {
400
+ return this.applySignalPositionDirective(signalResult, params);
401
+ }
402
+ // ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
403
+ // ── Requirement 8.5: no directive → use routing as-is ─────────────
404
+ let updatedSession = signalResult.updatedSession;
405
+ // Apply data/context updates from signal to the routed session
406
+ if (signalResult.mergedDirective?.dataUpdate) {
407
+ updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
408
+ }
409
+ // Use routing result for flow/step, but carry signal session state
410
+ // Merge routing session changes on top of signal session
411
+ const routingSession = routingResult.session;
412
+ updatedSession = {
413
+ ...updatedSession,
414
+ currentFlow: routingSession.currentFlow,
415
+ currentStep: routingSession.currentStep,
416
+ flowHistory: routingSession.flowHistory,
417
+ pendingDirective: routingSession.pendingDirective,
418
+ };
419
+ const isFlowComplete = routingResult.isFlowComplete;
420
+ // PRE-EXTRACTION: same logic as below — extract data from user message
421
+ if (routingResult.selectedFlow && !isFlowComplete) {
422
+ if (this.shouldPreExtractData(routingResult.selectedFlow)) {
423
+ logger.debug(`[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`);
424
+ const extractedData = await this.preExtractFlowData({
425
+ route: routingResult.selectedFlow,
426
+ history: params.history,
427
+ context: params.context,
428
+ session: updatedSession,
429
+ signal: params.signal,
430
+ });
431
+ if (extractedData && Object.keys(extractedData).length > 0) {
432
+ logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
433
+ updatedSession = mergeCollected(updatedSession, extractedData);
434
+ await this.agent.updateCollectedData(extractedData);
435
+ }
436
+ }
437
+ }
438
+ // Determine next step
439
+ const stepResult = await this.responsePipeline.determineNextStep({
440
+ selectedFlow: routingResult.selectedFlow,
441
+ selectedStep: routingResult.selectedStep,
442
+ session: updatedSession,
443
+ isFlowComplete,
444
+ });
445
+ return {
446
+ selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
447
+ selectedStep: stepResult.nextStep,
448
+ responseDirectives: routingResult.responseDirectives,
449
+ session: stepResult.session,
450
+ isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
451
+ signalFirings: signalResult.firings,
452
+ signalPreDirective: signalResult.mergedDirective || undefined,
453
+ };
454
+ }
455
+ // ── No signal processor: existing behavior (zero overhead) ────────────
333
456
  const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
334
457
  session: params.session,
335
- history: params.history, // Already Event[]
458
+ history: params.history,
336
459
  context: params.context,
337
460
  signal: params.signal,
338
461
  });
339
462
  let updatedSession = routingResult.session;
340
- const isRouteComplete = routingResult.isRouteComplete;
341
- // PRE-EXTRACTION: If entering a route that collects data, extract data from user message first
463
+ const isFlowComplete = routingResult.isFlowComplete;
464
+ // PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
342
465
  // This allows us to skip steps whose data is already provided
343
- // Requirement 3.1: Perform Pre_Extraction before determining the Batch
344
- if (routingResult.selectedRoute && !isRouteComplete) {
345
- // Always pre-extract when route collects data (not just on new route entry)
346
- // This ensures batch determination has the most up-to-date data
347
- if (this.shouldPreExtractData(routingResult.selectedRoute)) {
348
- logger.debug(`[ResponseModal] Pre-extracting data for route: ${routingResult.selectedRoute.title}`);
349
- const extractedData = await this.preExtractRouteData({
350
- route: routingResult.selectedRoute,
466
+ if (routingResult.selectedFlow && !isFlowComplete) {
467
+ // Always pre-extract when flow collects data (not just on new flow entry)
468
+ // This ensures step selection has the most up-to-date data
469
+ if (this.shouldPreExtractData(routingResult.selectedFlow)) {
470
+ logger.debug(`[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`);
471
+ const extractedData = await this.preExtractFlowData({
472
+ route: routingResult.selectedFlow,
351
473
  history: params.history,
352
474
  context: params.context,
353
475
  session: updatedSession,
@@ -355,51 +477,27 @@ export class ResponseModal {
355
477
  });
356
478
  if (extractedData && Object.keys(extractedData).length > 0) {
357
479
  logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
358
- // Requirement 3.3: Merge pre-extracted data into session before batch determination
480
+ // Merge pre-extracted data into session before step selection
359
481
  updatedSession = mergeCollected(updatedSession, extractedData);
360
482
  // Also update agent's collected data
361
483
  await this.agent.updateCollectedData(extractedData);
362
484
  }
363
485
  }
364
486
  }
365
- // BATCH DETERMINATION: Use BatchExecutor to determine which steps can execute together
366
- // Requirement 3.4: Pre-extraction results affect batch determination
367
- let batchSteps;
368
- let batchStoppedReason;
369
- let batchStoppedAtStep;
370
- if (routingResult.selectedRoute && !isRouteComplete) {
371
- // Determine current step position for batch determination
372
- const currentStep = routingResult.selectedStep ||
373
- (updatedSession.currentStep ? routingResult.selectedRoute.getStep(updatedSession.currentStep.id) : undefined);
374
- logger.debug(`[ResponseModal] Determining batch starting from step: ${currentStep?.id || 'initial'}`);
375
- const batchResult = await this.batchExecutor.determineBatch({
376
- route: routingResult.selectedRoute,
377
- currentStep,
378
- sessionData: updatedSession.data || {},
379
- context: params.context,
380
- maxSteps: this.agent.getAgentOptions().maxStepsPerBatch,
381
- });
382
- batchSteps = batchResult.steps;
383
- batchStoppedReason = batchResult.stoppedReason;
384
- batchStoppedAtStep = batchResult.stoppedAtStep;
385
- logger.debug(`[ResponseModal] Batch determined: ${batchSteps.length} steps, stopped reason: ${batchStoppedReason}`);
386
- }
387
487
  // Determine next step using pipeline method for consistency
388
488
  const stepResult = await this.responsePipeline.determineNextStep({
389
- selectedRoute: routingResult.selectedRoute,
489
+ selectedFlow: routingResult.selectedFlow,
390
490
  selectedStep: routingResult.selectedStep,
391
491
  session: updatedSession, // Use updated session with pre-extracted data
392
- isRouteComplete, // Use updated completion status
492
+ isFlowComplete, // Use updated completion status
393
493
  });
394
494
  return {
395
- selectedRoute: routingResult.selectedRoute,
495
+ selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
396
496
  selectedStep: stepResult.nextStep, // Use the determined next step
397
497
  responseDirectives: routingResult.responseDirectives,
398
498
  session: stepResult.session,
399
- isRouteComplete, // Use updated completion status
400
- batchSteps,
401
- batchStoppedReason,
402
- batchStoppedAtStep,
499
+ // If a branch changed the flow, the original isFlowComplete no longer applies
500
+ isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
403
501
  };
404
502
  }
405
503
  catch (error) {
@@ -407,31 +505,219 @@ export class ResponseModal {
407
505
  }
408
506
  }
409
507
  /**
410
- * Check if a route should pre-extract data before determining the initial step
508
+ * Apply a signal's position directive (goTo, goToStep, complete, abort, reset).
509
+ * Discards routing result and uses the signal's position decision.
411
510
  * @private
511
+ * @requirements 8.3
412
512
  */
413
- shouldPreExtractData(route) {
414
- // Pre-extract if route has declared required or optional fields
415
- if (route.requiredFields && route.requiredFields.length > 0) {
513
+ applySignalPositionDirective(signalResult, _params) {
514
+ const directive = signalResult.mergedDirective;
515
+ let session = signalResult.updatedSession;
516
+ const flows = this.agent.getFlows();
517
+ let selectedFlow;
518
+ let selectedStep;
519
+ let isFlowComplete = false;
520
+ // Apply data updates if present alongside position
521
+ if (directive.dataUpdate) {
522
+ session = mergeCollected(session, directive.dataUpdate);
523
+ }
524
+ if (directive.goTo) {
525
+ const flowTarget = typeof directive.goTo === 'string'
526
+ ? directive.goTo
527
+ : directive.goTo.flow ?? directive.goTo.step;
528
+ if (flowTarget) {
529
+ const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
530
+ if (targetFlow) {
531
+ session = enterFlow(session, targetFlow.id, targetFlow.title);
532
+ selectedFlow = targetFlow;
533
+ if (typeof directive.goTo === 'object' && directive.goTo.step) {
534
+ const targetStep = targetFlow.getStep(directive.goTo.step);
535
+ if (targetStep) {
536
+ session = enterStep(session, targetStep.id, targetStep.description);
537
+ selectedStep = targetStep;
538
+ }
539
+ }
540
+ }
541
+ else {
542
+ logger.warn(`[Signals] Pre-phase goTo target not found: "${flowTarget}". Falling back to no flow.`);
543
+ }
544
+ }
545
+ }
546
+ else if (directive.goToStep) {
547
+ const stepTarget = typeof directive.goToStep === 'string'
548
+ ? directive.goToStep
549
+ : directive.goToStep.step;
550
+ const flowTarget = typeof directive.goToStep === 'object'
551
+ ? directive.goToStep.flow
552
+ : undefined;
553
+ if (flowTarget) {
554
+ const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
555
+ if (targetFlow) {
556
+ session = enterFlow(session, targetFlow.id, targetFlow.title);
557
+ selectedFlow = targetFlow;
558
+ const targetStep = targetFlow.getStep(stepTarget);
559
+ if (targetStep) {
560
+ session = enterStep(session, targetStep.id, targetStep.description);
561
+ selectedStep = targetStep;
562
+ }
563
+ }
564
+ }
565
+ else if (session.currentFlow) {
566
+ const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
567
+ if (currentFlow) {
568
+ selectedFlow = currentFlow;
569
+ const targetStep = currentFlow.getStep(stepTarget);
570
+ if (targetStep) {
571
+ session = enterStep(session, targetStep.id, targetStep.description);
572
+ selectedStep = targetStep;
573
+ }
574
+ }
575
+ }
576
+ }
577
+ else if (directive.complete) {
578
+ isFlowComplete = true;
579
+ }
580
+ else if (directive.abort) {
581
+ // Abort — no flow, session cleared or marked
582
+ isFlowComplete = true;
583
+ }
584
+ else if (directive.reset) {
585
+ if (session.currentFlow) {
586
+ const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
587
+ if (currentFlow) {
588
+ selectedFlow = currentFlow;
589
+ const resetStep = typeof directive.reset === 'object' && directive.reset.step
590
+ ? directive.reset.step
591
+ : undefined;
592
+ if (resetStep) {
593
+ const targetStep = currentFlow.getStep(resetStep);
594
+ if (targetStep) {
595
+ session = enterStep(session, targetStep.id, targetStep.description);
596
+ selectedStep = targetStep;
597
+ }
598
+ }
599
+ else {
600
+ const initialStep = currentFlow.initialStep;
601
+ session = enterStep(session, initialStep.id, initialStep.description);
602
+ selectedStep = initialStep;
603
+ }
604
+ }
605
+ }
606
+ }
607
+ return {
608
+ selectedFlow,
609
+ selectedStep,
610
+ session,
611
+ isFlowComplete,
612
+ signalFirings: signalResult.firings,
613
+ signalPreDirective: signalResult.mergedDirective || undefined,
614
+ };
615
+ }
616
+ /**
617
+ * Routing skip optimization (Requirements 20.1, 20.2, 20.3):
618
+ * When the current step declares `collect` fields AND pre-extraction populates
619
+ * at least one of those fields from the user's message, skip routing for this turn.
620
+ *
621
+ * Returns the routing result if the skip applies, or undefined to fall through
622
+ * to normal routing.
623
+ * @private
624
+ */
625
+ async attemptRoutingSkipForCollect(params) {
626
+ const { session } = params;
627
+ // Only applies when we already have a current flow and step
628
+ if (!session.currentFlow || !session.currentStep) {
629
+ return undefined;
630
+ }
631
+ // Also skip this optimization if there's a pending directive (it takes priority)
632
+ if (session.pendingDirective) {
633
+ return undefined;
634
+ }
635
+ // Look up the actual Flow and Step objects to access `collect`
636
+ const currentFlow = this.agent.getFlows().find((f) => f.id === session.currentFlow?.id);
637
+ if (!currentFlow) {
638
+ return undefined;
639
+ }
640
+ const currentStep = currentFlow.getStep(session.currentStep.id);
641
+ if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
642
+ return undefined;
643
+ }
644
+ // We have a step with collect fields. Run pre-extraction to see if the
645
+ // user's message populates any of them.
646
+ const collectFields = currentStep.collect;
647
+ // Snapshot current data for comparison
648
+ const dataBefore = { ...session.data };
649
+ // Run pre-extraction against the current flow
650
+ const extractedData = await this.preExtractFlowData({
651
+ route: currentFlow,
652
+ history: params.history,
653
+ context: params.context,
654
+ session,
655
+ signal: params.signal,
656
+ });
657
+ if (!extractedData || Object.keys(extractedData).length === 0) {
658
+ return undefined;
659
+ }
660
+ // Determine which collect fields were newly populated by pre-extraction
661
+ const populatedCollectFields = [];
662
+ for (const field of collectFields) {
663
+ const key = field;
664
+ const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
665
+ const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
666
+ if (hasNewValue && !hadValue) {
667
+ populatedCollectFields.push(key);
668
+ }
669
+ }
670
+ if (populatedCollectFields.length === 0) {
671
+ // Pre-extraction didn't populate any declared collect field — no skip
672
+ return undefined;
673
+ }
674
+ // ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
675
+ logger.debug(`[ResponseModal] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`);
676
+ // Merge extracted data into session
677
+ const updatedSession = mergeCollected(session, extractedData);
678
+ await this.agent.updateCollectedData(extractedData);
679
+ // Determine next step using pipeline method for consistency
680
+ // Pass the current flow/step as the routing result (retained)
681
+ const stepResult = await this.responsePipeline.determineNextStep({
682
+ selectedFlow: currentFlow,
683
+ selectedStep: currentStep,
684
+ session: updatedSession,
685
+ isFlowComplete: false,
686
+ });
687
+ return {
688
+ selectedFlow: stepResult.flowChanged || currentFlow,
689
+ selectedStep: stepResult.nextStep,
690
+ responseDirectives: undefined,
691
+ session: stepResult.session,
692
+ isFlowComplete: stepResult.flowChanged ? false : false,
693
+ };
694
+ }
695
+ /**
696
+ * Check if a flow should pre-extract data before determining the initial step
697
+ * @private
698
+ */
699
+ shouldPreExtractData(flow) {
700
+ // Pre-extract if flow has declared required or optional fields
701
+ if (flow.requiredFields && flow.requiredFields.length > 0) {
416
702
  return true;
417
703
  }
418
- if (route.optionalFields && route.optionalFields.length > 0) {
704
+ if (flow.optionalFields && flow.optionalFields.length > 0) {
419
705
  return true;
420
706
  }
421
- // Pre-extract if any step in the route collects data
422
- const steps = route.getAllSteps();
707
+ // Pre-extract if any step in the flow collects data
708
+ const steps = flow.getAllSteps();
423
709
  const hasDataCollectionSteps = steps.some(step => step.collect && step.collect.length > 0);
424
710
  return hasDataCollectionSteps;
425
711
  }
426
712
  /**
427
- * Pre-extract data from user message when entering a route
713
+ * Pre-extract data from user message when entering a flow
428
714
  * This allows skipping steps whose data is already provided
429
715
  * @private
430
716
  */
431
- async preExtractRouteData(params) {
432
- const { route, history, signal } = params;
433
- // Build a schema for data extraction based on route's fields
434
- const extractionSchema = this.agent.getSchema();
717
+ async preExtractFlowData(params) {
718
+ const { route: flow, history, signal } = params;
719
+ // Build a schema for data extraction based on flow's fields
720
+ const extractionSchema = this.agent.schema;
435
721
  if (!extractionSchema) {
436
722
  logger.warn(`[ResponseModal] No schema available for pre-extraction`);
437
723
  return {};
@@ -448,11 +734,11 @@ export class ResponseModal {
448
734
  `Extract data for these fields if present:`,
449
735
  ];
450
736
  // Add field descriptions
451
- if (route.requiredFields) {
452
- extractionPrompt.push(`Required fields: ${route.requiredFields.join(', ')}`);
737
+ if (flow.requiredFields) {
738
+ extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
453
739
  }
454
- if (route.optionalFields) {
455
- extractionPrompt.push(`Optional fields: ${route.optionalFields.join(', ')}`);
740
+ if (flow.optionalFields) {
741
+ extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
456
742
  }
457
743
  extractionPrompt.push(``, `Return ONLY the extracted data as JSON. If no data can be extracted, return an empty object {}.`);
458
744
  // Convert Event[] to HistoryItem[] for provider call
@@ -485,98 +771,176 @@ export class ResponseModal {
485
771
  * @private
486
772
  */
487
773
  async generateUnifiedResponse(responseContext) {
488
- const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete, batchSteps, batchStoppedReason, signal, } = responseContext;
774
+ const { effectiveContext, session: initialSession, history, selectedFlow, selectedStep, responseDirectives, isFlowComplete, signal, signalFirings: preSignalFirings, signalPreDirective, signalHalted, signalHaltReply, } = responseContext;
489
775
  let session = initialSession;
490
- // Get last user message (needed for both route and completion handling)
776
+ // Accumulator for signal firings across both phases (fire order)
777
+ const signalFirings = [...(preSignalFirings || [])];
778
+ // Get last user message (needed for both flow and completion handling)
491
779
  // Convert HistoryItem[] to Event[] for internal processing
492
780
  const historyEvents = historyToEvents(history);
781
+ // ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
782
+ // Pre-signal phase emitted halt → skip LLM call entirely.
783
+ if (signalHalted) {
784
+ const haltMessage = signalHaltReply || '';
785
+ // Run post-signal phase even on halt (post-phase sees complete turn context)
786
+ const postResult = await this.responsePipeline.runPostSignalPhase(session, effectiveContext, historyEvents);
787
+ session = postResult.updatedSession;
788
+ signalFirings.push(...postResult.firings);
789
+ // Apply post-phase position directive as pendingDirective (Requirement 9.3)
790
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
791
+ session = { ...session, pendingDirective: postResult.mergedDirective };
792
+ }
793
+ await this.finalizeSession(session, effectiveContext);
794
+ return {
795
+ message: haltMessage,
796
+ session,
797
+ toolCalls: undefined,
798
+ isFlowComplete: false,
799
+ executedSteps: [],
800
+ stoppedReason: signalHaltReply ? 'reply' : 'halt',
801
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
802
+ };
803
+ }
493
804
  let message;
494
805
  let toolCalls = undefined;
495
806
  let executedSteps;
496
807
  let stoppedReason;
497
- if (selectedRoute && !isRouteComplete) {
498
- // Check if we have batch steps to execute
499
- if (batchSteps && batchSteps.length > 0) {
500
- // BATCH EXECUTION: Execute multiple steps in a single LLM call
501
- logger.debug(`[ResponseModal] Executing batch of ${batchSteps.length} steps`);
502
- const batchResult = await this.executeBatchResponse({
503
- selectedRoute,
504
- batchSteps,
505
- responseDirectives,
506
- session,
507
- history,
508
- context: effectiveContext,
509
- historyEvents,
808
+ let appliedInstructions;
809
+ if (selectedFlow && !isFlowComplete) {
810
+ // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
811
+ // If the current step is auto, the executor advances through it (and any
812
+ // subsequent auto-steps) until an interactive step or terminal condition.
813
+ let resolvedStep = selectedStep;
814
+ const currentStepInstance = session.currentStep
815
+ ? selectedFlow.getStep(session.currentStep.id)
816
+ : selectedStep;
817
+ if (currentStepInstance?.auto) {
818
+ const autoChainExecutor = new AutoChainExecutor({
819
+ maxAutoStepsPerTurn: this.agent.maxAutoStepsPerTurn,
510
820
  });
511
- message = batchResult.message;
512
- toolCalls = batchResult.toolCalls;
513
- session = batchResult.session;
514
- executedSteps = batchResult.executedSteps;
515
- stoppedReason = batchStoppedReason;
516
- }
517
- else {
518
- // SINGLE STEP EXECUTION: Fall back to single-step processing
519
- // This happens when batch determination returns empty (first step needs input)
520
- const result = await this.processRouteResponse({
521
- selectedRoute,
522
- selectedStep,
523
- responseDirectives,
821
+ const autoResult = await autoChainExecutor.run({
524
822
  session,
525
- history,
526
823
  context: effectiveContext,
527
- historyEvents,
528
- signal,
824
+ flow: selectedFlow,
529
825
  });
530
- message = result.message;
531
- toolCalls = result.toolCalls;
532
- session = result.session;
533
- // Track executed step for single-step execution
534
- if (selectedStep) {
535
- executedSteps = [{
536
- id: selectedStep.id,
537
- routeId: selectedRoute.id,
538
- }];
826
+ session = autoResult.session;
827
+ // Handle halt: emit verbatim reply, persist, return — no LLM call.
828
+ if (autoResult.stoppedReason === 'halt') {
829
+ message = autoResult.mergedDirective?.reply || '';
830
+ stoppedReason = 'halt';
831
+ executedSteps = [];
832
+ await this.finalizeSession(session, effectiveContext);
833
+ return {
834
+ message,
835
+ session,
836
+ toolCalls: undefined,
837
+ isFlowComplete: false,
838
+ executedSteps,
839
+ stoppedReason,
840
+ };
539
841
  }
540
- stoppedReason = batchStoppedReason || 'needs_input';
541
- }
542
- }
543
- else if (isRouteComplete && selectedRoute) {
544
- // Handle route completion
545
- logger.debug(`[ResponseModal] Generating completion message for route: ${selectedRoute.title}`);
546
- try {
547
- message = await this.handleRouteCompletion({
548
- selectedRoute,
549
- session,
550
- context: effectiveContext,
551
- history,
552
- historyEvents,
553
- signal,
554
- });
555
- // Set step to END_ROUTE marker
556
- session = enterStep(session, END_ROUTE_ID, "Route completed");
557
- stoppedReason = 'route_complete';
558
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
842
+ // Handle flow completion or cross-flow redirect from auto-chain.
843
+ // The auto-chain ended without resolving to an interactive step.
844
+ // Possible reasons: last_step (no successor), completed (explicit
845
+ // complete directive), or goto (cross-flow redirect).
846
+ if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
847
+ logger.debug(`[ResponseModal] Auto-chain ended with ${autoResult.stoppedReason}`);
848
+ session = await this.applyFlowCompletion({
849
+ selectedFlow,
850
+ session,
851
+ context: effectiveContext,
852
+ history,
853
+ });
854
+ await this.finalizeSession(session, effectiveContext);
855
+ return {
856
+ message: '',
857
+ session,
858
+ toolCalls: undefined,
859
+ isFlowComplete: true,
860
+ executedSteps: [],
861
+ stoppedReason: autoResult.stoppedReason,
862
+ };
863
+ }
864
+ // Normal case: auto-chain resolved to an interactive step.
865
+ resolvedStep = autoResult.resolvedStep;
559
866
  }
560
- catch (error) {
561
- logger.error(`[ResponseModal] Error generating completion message:`, error);
562
- // Fallback to simple completion message
563
- message = `Thank you! I've recorded all the information for your ${selectedRoute.title.toLowerCase()}.`;
564
- session = enterStep(session, END_ROUTE_ID, "Route completed");
565
- stoppedReason = 'route_complete';
867
+ // SINGLE STEP EXECUTION: Process the resolved interactive step.
868
+ // The auto-chain (if it ran) already walked auto-steps. Only the
869
+ // interactive step remains for the LLM call.
870
+ const result = await this.processFlowResponse({
871
+ selectedFlow,
872
+ selectedStep: resolvedStep,
873
+ responseDirectives,
874
+ session,
875
+ history,
876
+ context: effectiveContext,
877
+ historyEvents,
878
+ signal,
879
+ // Propagate signal pre-directive's appendPrompt for this turn's LLM call (Requirement 8.4)
880
+ transientAppendage: signalPreDirective?.appendPrompt,
881
+ // Merge signal pre-directive (halt/reply/injectTools) into the pre-LLM bus
882
+ mergedPreDirective: signalPreDirective,
883
+ });
884
+ message = result.message;
885
+ toolCalls = result.toolCalls;
886
+ session = result.session;
887
+ appliedInstructions = result.appliedInstructions;
888
+ // Track executed step for single-step execution
889
+ if (resolvedStep) {
890
+ executedSteps = [{
891
+ id: resolvedStep.id,
892
+ flowId: selectedFlow.id,
893
+ }];
566
894
  }
895
+ // Use stoppedReason from processFlowResponse if set (halt/reply),
896
+ // otherwise default to 'needs_input' for normal LLM responses.
897
+ stoppedReason = result.stoppedReason || 'needs_input';
898
+ }
899
+ else if (isFlowComplete && selectedFlow) {
900
+ // Flow completion path: pure state transition, no LLM call.
901
+ // The framework emits no message of its own.
902
+ // stoppedReason is 'last_step' because this completion was detected by
903
+ // implicit terminus (no successor or all successors skipped), not by an
904
+ // explicit `complete` directive.
905
+ logger.debug(`[ResponseModal] Releasing session to idle for completed flow: ${selectedFlow.title}`);
906
+ session = await this.applyFlowCompletion({
907
+ selectedFlow,
908
+ session,
909
+ context: effectiveContext,
910
+ history,
911
+ });
912
+ message = '';
913
+ stoppedReason = 'last_step';
914
+ executedSteps = [];
567
915
  }
568
916
  else {
569
- // Fallback: No routes defined, generate a simple response
570
- message = await this.generateFallbackResponse({
917
+ // Fallback: No flows defined, generate a simple response
918
+ const fallbackResult = await this.generateFallbackResponse({
571
919
  history,
572
920
  context: effectiveContext,
573
921
  session,
574
922
  });
923
+ message = fallbackResult.message;
924
+ appliedInstructions = fallbackResult.appliedInstructions;
575
925
  // For fallback responses, set empty executedSteps and no stoppedReason
576
- // since there's no route/step execution happening
926
+ // since there's no flow/step execution happening
577
927
  executedSteps = [];
578
928
  stoppedReason = undefined;
579
929
  }
930
+ // POST-SIGNAL PHASE (Requirement 9.1, 9.2, 9.3, 9.4)
931
+ // Runs after finalize/onComplete and before session persistence.
932
+ // Post-phase signals see the complete turn result: assistant message in
933
+ // history, collected data, tool results.
934
+ const postResult = await this.responsePipeline.runPostSignalPhase(session, effectiveContext, historyEvents);
935
+ session = postResult.updatedSession;
936
+ // Append post-phase firings to the accumulator (preserves fire order)
937
+ signalFirings.push(...postResult.firings);
938
+ // Requirement 9.3: Post-phase position directive sets session.pendingDirective
939
+ // (no mid-turn re-entry per D6 decision). Pre-LLM-only fields are already
940
+ // dropped inside runPostSignalPhase per Phase 4.5.
941
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
942
+ session = { ...session, pendingDirective: postResult.mergedDirective };
943
+ }
580
944
  // Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
581
945
  // - executedSteps: array of steps executed (empty array if none)
582
946
  // - stoppedReason: why execution stopped (undefined for fallback)
@@ -585,400 +949,30 @@ export class ResponseModal {
585
949
  message,
586
950
  session,
587
951
  toolCalls,
588
- isRouteComplete,
589
- executedSteps: executedSteps || [],
590
- stoppedReason,
591
- };
592
- }
593
- /**
594
- * Execute a batch of steps with a single LLM call
595
- *
596
- * This method:
597
- * 1. Executes all prepare hooks for steps in the batch (in order)
598
- * 2. Builds a combined prompt using BatchPromptBuilder
599
- * 3. Makes a single LLM call
600
- * 4. Collects data from the response for all steps
601
- * 5. Executes all finalize hooks for steps in the batch (in order)
602
- *
603
- * @private
604
- * **Validates: Requirements 1.1, 4.4, 5.1, 5.2**
605
- */
606
- async executeBatchResponse(params) {
607
- const { selectedRoute, batchSteps, history, context, historyEvents, signal } = params;
608
- let session = params.session;
609
- logger.debug(`[ResponseModal] Starting batch execution for ${batchSteps.length} steps`);
610
- // Create hook executor function
611
- const executeHook = async (hook, hookContext, data, step) => {
612
- // Find the route for this step
613
- const route = selectedRoute;
614
- // Convert StepOptions to Step if needed for executePrepareFinalize
615
- const stepInstance = step?.id ? route.getStep(step.id) : undefined;
616
- await this.executePrepareFinalize(hook, hookContext, data, route, stepInstance);
617
- };
618
- // PHASE 1: Execute all prepare hooks (Requirement 5.1)
619
- logger.debug(`[ResponseModal] Executing prepare hooks for batch`);
620
- const prepareResult = await this.batchExecutor.executePrepareHooks({
621
- steps: batchSteps,
622
- context,
623
- data: session.data,
624
- executeHook,
625
- });
626
- if (!prepareResult.success) {
627
- // Prepare hook failed - return error response
628
- logger.error(`[ResponseModal] Prepare hook failed:`, prepareResult.error);
629
- throw new ResponseGenerationError(`Prepare hook failed: ${prepareResult.error?.message}`, {
630
- phase: 'prepare_hooks',
631
- context: {
632
- stepId: prepareResult.error?.stepId,
633
- executedSteps: prepareResult.executedSteps,
634
- }
635
- });
636
- }
637
- // PHASE 2: Build combined prompt using BatchPromptBuilder (Requirement 4.4)
638
- logger.debug(`[ResponseModal] Building batch prompt`);
639
- const batchPromptResult = await this.batchPromptBuilder.buildBatchPrompt({
640
- steps: batchSteps,
641
- route: selectedRoute,
642
- history: historyEvents,
643
- context,
644
- session,
645
- agentOptions: this.agent.getAgentOptions(),
646
- });
647
- logger.debug(`[ResponseModal] Batch prompt built with ${batchPromptResult.stepCount} steps, collecting: ${batchPromptResult.collectFields.join(', ')}`);
648
- // Build response schema for batch (includes all collect fields)
649
- const responseSchema = this.buildBatchResponseSchema(batchPromptResult.collectFields);
650
- // Collect available tools for AI (from all steps in batch)
651
- const availableTools = this.collectBatchAvailableTools(selectedRoute, batchSteps);
652
- // PHASE 3: Make single LLM call (Requirement 4.4)
653
- logger.debug(`[ResponseModal] Making LLM call for batch`);
654
- const agentOptions = this.agent.getAgentOptions();
655
- const result = await agentOptions.provider.generateMessage({
656
- prompt: batchPromptResult.prompt,
657
- history, // Use HistoryItem[] for AI provider
658
- context,
659
- tools: availableTools,
660
- signal,
661
- parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "batch_response" } : undefined,
662
- });
663
- let message = result.structured?.message || result.message;
664
- let toolCalls = result.structured?.toolCalls;
665
- logger.debug(`[ResponseModal] LLM response received for batch`);
666
- // Execute tools if any
667
- if (toolCalls && toolCalls.length > 0) {
668
- const toolResult = await this.executeUnifiedToolLoop({
669
- toolCalls,
670
- context,
671
- session,
672
- history,
673
- selectedRoute,
674
- responsePrompt: batchPromptResult.prompt,
675
- availableTools,
676
- responseSchema,
677
- signal,
678
- });
679
- session = toolResult.session;
680
- toolCalls = toolResult.finalToolCalls;
681
- if (toolResult.finalMessage) {
682
- message = toolResult.finalMessage;
683
- }
684
- }
685
- // PHASE 4: Collect data from response for all steps (Requirement 6.1, 6.2, 6.3)
686
- logger.debug(`[ResponseModal] Collecting batch data`);
687
- const collectResult = this.batchExecutor.collectBatchData({
688
- steps: batchSteps,
689
- llmResponse: result.structured || {},
690
- session,
691
- schema: this.agent.getSchema(),
692
- });
693
- session = collectResult.session;
694
- if (collectResult.collectedData && Object.keys(collectResult.collectedData).length > 0) {
695
- // Update agent's collected data
696
- await this.agent.updateCollectedData(collectResult.collectedData);
697
- logger.debug(`[ResponseModal] Batch collected data:`, collectResult.collectedData);
698
- }
699
- if (collectResult.validationErrors && collectResult.validationErrors.length > 0) {
700
- logger.warn(`[ResponseModal] Batch data validation errors:`, collectResult.validationErrors);
701
- }
702
- // Update session to final step position
703
- const lastStep = batchSteps[batchSteps.length - 1];
704
- if (lastStep?.id) {
705
- session = enterStep(session, lastStep.id, lastStep.description);
706
- logger.debug(`[ResponseModal] Updated session to final batch step: ${lastStep.id}`);
707
- }
708
- // PHASE 5: Execute all finalize hooks (Requirement 5.2)
709
- logger.debug(`[ResponseModal] Executing finalize hooks for batch`);
710
- const finalizeResult = await this.batchExecutor.executeFinalizeHooks({
711
- steps: batchSteps,
712
- context,
713
- data: session.data,
714
- executeHook,
715
- });
716
- if (finalizeResult.errors && finalizeResult.errors.length > 0) {
717
- // Log finalize errors but don't fail (Requirement 5.5)
718
- logger.warn(`[ResponseModal] Some finalize hooks failed:`, finalizeResult.errors);
719
- }
720
- // Build executed steps list
721
- const executedSteps = batchSteps
722
- .filter(step => step.id)
723
- .map(step => ({
724
- id: step.id,
725
- routeId: selectedRoute.id,
726
- }));
727
- logger.debug(`[ResponseModal] Batch execution complete. Executed ${executedSteps.length} steps`);
728
- return {
729
- message,
730
- toolCalls,
731
- session,
732
- executedSteps,
733
- };
734
- }
735
- /**
736
- * Build response schema for batch execution
737
- * @private
738
- */
739
- buildBatchResponseSchema(collectFields) {
740
- const properties = {
741
- message: {
742
- type: "string",
743
- description: "Natural, conversational response directed at the user. Must NOT contain field names, raw data, or internal information.",
744
- },
745
- };
746
- const agentSchema = this.agent.getSchema();
747
- // Add collect fields to schema, using agent schema definitions when available
748
- for (const field of collectFields) {
749
- if (agentSchema?.properties && agentSchema.properties[field]) {
750
- properties[field] = agentSchema.properties[field];
751
- }
752
- else {
753
- // Dynamic fallback when no agent schema is defined
754
- properties[field] = {
755
- type: "string",
756
- description: `Collected value for ${field}`,
757
- };
758
- }
759
- }
760
- return {
761
- type: "object",
762
- properties,
763
- required: ["message"],
764
- additionalProperties: true,
765
- };
766
- }
767
- /**
768
- * Collect available tools from all steps in the batch
769
- * @private
770
- */
771
- collectBatchAvailableTools(route, batchSteps) {
772
- const availableTools = new Map();
773
- // Add agent-level tools
774
- this.agent.getTools().forEach((tool) => {
775
- availableTools.set(tool.id, tool);
776
- });
777
- // Add route-level tools
778
- route.getTools().forEach((tool) => {
779
- availableTools.set(tool.id, tool);
780
- });
781
- // Add step-level tools from all batch steps
782
- for (const step of batchSteps) {
783
- if (step.tools) {
784
- for (const toolRef of step.tools) {
785
- if (typeof toolRef === "string") {
786
- // Reference to registered tool - already in availableTools
787
- }
788
- else if (typeof toolRef === 'object' && 'id' in toolRef && toolRef.id) {
789
- // Inline tool definition
790
- availableTools.set(toolRef.id, toolRef);
791
- }
792
- }
793
- }
794
- }
795
- // Convert to the format expected by AI providers
796
- return Array.from(availableTools.values()).map((tool) => ({
797
- id: tool.id,
798
- name: tool.name || tool.id,
799
- description: tool.description,
800
- parameters: tool.parameters,
801
- }));
802
- }
803
- /**
804
- * Unified streaming response generation
805
- * @private
806
- */
807
- async *generateUnifiedStreamingResponse(responseContext) {
808
- const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete, batchSteps, batchStoppedReason, } = responseContext;
809
- const session = initialSession;
810
- // Get last user message (needed for both route and completion handling)
811
- // Convert HistoryItem[] to Event[] for internal processing
812
- const historyEvents = historyToEvents(history);
813
- if (selectedRoute && !isRouteComplete) {
814
- // Check if we have batch steps to execute
815
- if (batchSteps && batchSteps.length > 0) {
816
- // BATCH EXECUTION: Execute multiple steps with streaming
817
- // Note: For streaming, we still use batch execution but stream the response
818
- logger.debug(`[ResponseModal] Streaming batch execution for ${batchSteps.length} steps`);
819
- yield* this.streamBatchResponse({
820
- selectedRoute,
821
- batchSteps,
822
- responseDirectives,
823
- session,
824
- history,
825
- context: effectiveContext,
826
- historyEvents,
827
- batchStoppedReason,
828
- });
829
- }
830
- else {
831
- // SINGLE STEP EXECUTION: Fall back to single-step streaming
832
- yield* this.processRouteStreamingResponse({
833
- selectedRoute,
834
- selectedStep,
835
- responseDirectives,
836
- session,
837
- history,
838
- context: effectiveContext,
839
- historyEvents,
840
- });
841
- }
842
- }
843
- else if (isRouteComplete && selectedRoute) {
844
- // Handle route completion streaming
845
- yield* this.streamRouteCompletion({
846
- selectedRoute,
847
- session,
848
- context: effectiveContext,
849
- history,
850
- historyEvents,
851
- });
852
- }
853
- else {
854
- // Fallback: No routes defined, stream a simple response
855
- yield* this.streamFallbackResponse({
856
- history,
857
- context: effectiveContext,
858
- session,
859
- });
860
- }
861
- }
862
- /**
863
- * Stream a batch response with multiple steps
864
- *
865
- * Similar to executeBatchResponse but streams the LLM response.
866
- *
867
- * @private
868
- */
869
- async *streamBatchResponse(params) {
870
- const { selectedRoute, batchSteps, history, context, historyEvents, batchStoppedReason, signal } = params;
871
- let session = params.session;
872
- // Create hook executor function
873
- const executeHook = async (hook, hookContext, data, step) => {
874
- const route = selectedRoute;
875
- const stepInstance = step?.id ? route.getStep(step.id) : undefined;
876
- await this.executePrepareFinalize(hook, hookContext, data, route, stepInstance);
877
- };
878
- // PHASE 1: Execute all prepare hooks
879
- const prepareResult = await this.batchExecutor.executePrepareHooks({
880
- steps: batchSteps,
881
- context,
882
- data: session.data,
883
- executeHook,
884
- });
885
- if (!prepareResult.success) {
886
- // Yield error chunk
887
- yield {
888
- delta: "",
889
- accumulated: "",
890
- done: true,
891
- session,
892
- error: new ResponseGenerationError(`Prepare hook failed: ${prepareResult.error?.message}`, { phase: 'prepare_hooks' }),
893
- };
894
- return;
895
- }
896
- // PHASE 2: Build combined prompt
897
- const batchPromptResult = await this.batchPromptBuilder.buildBatchPrompt({
898
- steps: batchSteps,
899
- route: selectedRoute,
900
- history: historyEvents,
901
- context,
902
- session,
903
- agentOptions: this.agent.getAgentOptions(),
904
- });
905
- const responseSchema = this.buildBatchResponseSchema(batchPromptResult.collectFields);
906
- const availableTools = this.collectBatchAvailableTools(selectedRoute, batchSteps);
907
- // PHASE 3: Stream LLM response
908
- const agentOptions = this.agent.getAgentOptions();
909
- const stream = agentOptions.provider.generateMessageStream({
910
- prompt: batchPromptResult.prompt,
911
- history, // Use HistoryItem[] for AI provider
912
- context,
913
- tools: availableTools,
914
- signal,
915
- parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "batch_stream_response" } : undefined,
916
- });
917
- // Build executed steps list
918
- const executedSteps = batchSteps
919
- .filter(step => step.id)
920
- .map(step => ({
921
- id: step.id,
922
- routeId: selectedRoute.id,
923
- }));
924
- // Stream chunks
925
- for await (const chunk of stream) {
926
- // On final chunk, collect data and execute finalize hooks
927
- if (chunk.done) {
928
- // Collect data from response
929
- if (chunk.structured) {
930
- const collectResult = this.batchExecutor.collectBatchData({
931
- steps: batchSteps,
932
- llmResponse: chunk.structured,
933
- session,
934
- schema: this.agent.getSchema(),
935
- });
936
- session = collectResult.session;
937
- if (collectResult.collectedData && Object.keys(collectResult.collectedData).length > 0) {
938
- await this.agent.updateCollectedData(collectResult.collectedData);
939
- }
940
- }
941
- // Update session to final step position
942
- const lastStep = batchSteps[batchSteps.length - 1];
943
- if (lastStep?.id) {
944
- session = enterStep(session, lastStep.id, lastStep.description);
945
- }
946
- // Execute finalize hooks
947
- await this.batchExecutor.executeFinalizeHooks({
948
- steps: batchSteps,
949
- context,
950
- data: session.data,
951
- executeHook,
952
- });
953
- // Finalize session
954
- await this.finalizeSession(session, context);
955
- }
956
- yield {
957
- delta: chunk.delta,
958
- accumulated: chunk.accumulated,
959
- done: chunk.done,
960
- session,
961
- toolCalls: chunk.structured?.toolCalls,
962
- isRouteComplete: false,
963
- executedSteps: chunk.done ? executedSteps : undefined,
964
- stoppedReason: chunk.done ? batchStoppedReason : undefined,
965
- metadata: chunk.metadata,
966
- structured: chunk.structured,
967
- };
968
- }
952
+ isFlowComplete: isFlowComplete,
953
+ executedSteps: executedSteps || [],
954
+ stoppedReason,
955
+ appliedInstructions,
956
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
957
+ };
969
958
  }
970
959
  /**
971
960
  * Execute prepare function for current step if available
972
961
  * @private
973
962
  */
974
963
  async executeStepPrepare(session, context) {
975
- if (session.currentRoute && session.currentStep) {
976
- const currentRoute = this.agent.getRoutes().find((r) => r.id === session.currentRoute?.id);
977
- if (currentRoute) {
978
- const currentStep = currentRoute.getStep(session.currentStep.id);
964
+ if (session.currentFlow && session.currentStep) {
965
+ const currentFlow = this.agent.getFlows().find((r) => r.id === session.currentFlow?.id);
966
+ if (currentFlow) {
967
+ const currentStep = currentFlow.getStep(session.currentStep.id);
968
+ // Skip auto-steps — their prepare is handled by AutoChainExecutor
969
+ if (currentStep?.auto) {
970
+ logger.debug(`[ResponseModal] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
971
+ return;
972
+ }
979
973
  if (currentStep?.prepare) {
980
974
  logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
981
- await this.executePrepareFinalize(currentStep.prepare, context, session.data, currentRoute, currentStep);
975
+ await this.executePrepareFinalize(currentStep.prepare, context, session.data, currentFlow, currentStep);
982
976
  }
983
977
  }
984
978
  }
@@ -988,23 +982,23 @@ export class ResponseModal {
988
982
  * @private
989
983
  */
990
984
  async executeStepFinalize(session, context) {
991
- if (session.currentRoute && session.currentStep) {
992
- const currentRoute = this.agent.getRoutes().find((r) => r.id === session.currentRoute?.id);
993
- if (currentRoute) {
994
- const currentStep = currentRoute.getStep(session.currentStep.id);
985
+ if (session.currentFlow && session.currentStep) {
986
+ const currentFlow = this.agent.getFlows().find((r) => r.id === session.currentFlow?.id);
987
+ if (currentFlow) {
988
+ const currentStep = currentFlow.getStep(session.currentStep.id);
995
989
  if (currentStep?.finalize) {
996
990
  logger.debug(`[ResponseModal] Executing finalize for step: ${currentStep.id}`);
997
- await this.executePrepareFinalize(currentStep.finalize, context, session.data, currentRoute, currentStep);
991
+ await this.executePrepareFinalize(currentStep.finalize, context, session.data, currentFlow, currentStep);
998
992
  }
999
993
  }
1000
994
  }
1001
995
  }
1002
996
  /**
1003
- * Process route response with unified tool execution and data collection
997
+ * Process flow response with unified tool execution and data collection
1004
998
  * @private
1005
999
  */
1006
- async processRouteResponse(params) {
1007
- const { selectedRoute, selectedStep, responseDirectives, history, context, historyEvents, signal } = params;
1000
+ async processFlowResponse(params) {
1001
+ const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1008
1002
  let session = params.session;
1009
1003
  // Determine next step
1010
1004
  let nextStep;
@@ -1012,25 +1006,42 @@ export class ResponseModal {
1012
1006
  nextStep = selectedStep;
1013
1007
  }
1014
1008
  else {
1015
- // Determine current step from session if we're already in this route
1016
- const isInSameRoute = session.currentRoute?.id === selectedRoute.id;
1017
- const currentStep = isInSameRoute && session.currentStep
1018
- ? selectedRoute.getStep(session.currentStep.id)
1009
+ // Determine current step from session if we're already in this flow
1010
+ const isInSameFlow = session.currentFlow?.id === selectedFlow.id;
1011
+ const currentStep = isInSameFlow && session.currentStep
1012
+ ? selectedFlow.getStep(session.currentStep.id)
1019
1013
  : undefined;
1020
- logger.debug(`[ResponseModal] Step determination: route match=${isInSameRoute}, currentRoute=${session.currentRoute?.id}, selectedRoute=${selectedRoute.id}, currentStep=${currentStep?.id || 'none'}`);
1021
- // Get candidate steps based on current position in the route
1022
- const routingEngine = this.agent.getRoutingEngine();
1023
- const candidates = await routingEngine.getCandidateStepsWithConditions(selectedRoute, currentStep, // Pass current step instead of undefined to maintain progression
1024
- createTemplateContext({ data: session.data, session, context }));
1025
- logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new route entry)'}`);
1026
- if (candidates.length > 0) {
1027
- nextStep = candidates[0].step;
1028
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new route'}`);
1014
+ logger.debug(`[ResponseModal] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
1015
+ // STEP 1 (Algorithm 1): branches win over linear chain
1016
+ if (currentStep?.branches && currentStep.branches.length > 0) {
1017
+ const branchResult = await this.responsePipeline.evaluateStepBranches(currentStep, selectedFlow, session, context);
1018
+ if (branchResult) {
1019
+ if (branchResult.nextStep) {
1020
+ nextStep = branchResult.nextStep;
1021
+ session = branchResult.session;
1022
+ }
1023
+ else {
1024
+ // Flow transition or completion — no local step to render
1025
+ // Return empty message with updated session; caller handles flow transition
1026
+ return { message: '', session: branchResult.session };
1027
+ }
1028
+ }
1029
1029
  }
1030
- else {
1031
- // Fallback to initial step even if it should be skipped
1032
- nextStep = selectedRoute.initialStep;
1033
- logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
1030
+ if (!nextStep) {
1031
+ // Get candidate steps based on current position in the flow
1032
+ const flowRouter = this.agent.getFlowRouter();
1033
+ const candidates = await flowRouter.getCandidateStepsWithConditions(selectedFlow, currentStep, // Pass current step instead of undefined to maintain progression
1034
+ createTemplateContext({ data: session.data, session, context }));
1035
+ logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
1036
+ if (candidates.length > 0) {
1037
+ nextStep = candidates[0].step;
1038
+ logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1039
+ }
1040
+ else {
1041
+ // Fallback to initial step even if it should be skipped
1042
+ nextStep = selectedFlow.initialStep;
1043
+ logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1044
+ }
1034
1045
  }
1035
1046
  }
1036
1047
  // Update session with next step
@@ -1039,14 +1050,14 @@ export class ResponseModal {
1039
1050
  const sessionData = session.data || {};
1040
1051
  const missingRequires = nextStep.requires.filter(field => sessionData[String(field)] === undefined);
1041
1052
  if (missingRequires.length > 0) {
1042
- const warning = `[Agent] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1043
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step.`;
1053
+ const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1054
+ `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1044
1055
  logger.warn(warning);
1045
1056
  console.warn(warning);
1046
1057
  // Stay at the current step - don't enter the next one
1047
1058
  const currentStepId = session.currentStep?.id;
1048
1059
  if (currentStepId) {
1049
- const currentStepInstance = selectedRoute.getStep(currentStepId);
1060
+ const currentStepInstance = selectedFlow.getStep(currentStepId);
1050
1061
  if (currentStepInstance) {
1051
1062
  nextStep = currentStepInstance;
1052
1063
  logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
@@ -1062,76 +1073,271 @@ export class ResponseModal {
1062
1073
  session = enterStep(session, nextStep.id, nextStep.description);
1063
1074
  logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1064
1075
  }
1065
- // Build response schema for this route (with collect fields from step)
1066
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
1067
- // Build response prompt
1068
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
1069
- route: selectedRoute,
1070
- currentStep: nextStep,
1071
- rules: selectedRoute.getRules(),
1072
- prohibitions: selectedRoute.getProhibitions(),
1073
- directives: responseDirectives,
1074
- history: historyEvents,
1075
- agentOptions: this.agent.getAgentOptions(),
1076
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
1077
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
1078
- context,
1079
- session,
1080
- agentSchema: this.agent.getSchema(),
1081
- });
1082
- // Collect available tools for AI
1083
- const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
1084
- // Generate message using AI provider
1085
- const agentOptions = this.agent.getAgentOptions();
1086
- const result = await agentOptions.provider.generateMessage({
1087
- prompt: responsePrompt,
1088
- history, // Use HistoryItem[] for AI provider
1089
- context,
1090
- tools: availableTools,
1091
- signal,
1092
- parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
1093
- });
1094
- let message = result.structured?.message || result.message;
1095
- let toolCalls = result.structured?.toolCalls;
1096
- // Debug: Log initial AI response
1097
- logger.debug(`[ResponseModal] Initial AI response:`, {
1098
- hasMessage: !!message,
1099
- messageLength: message?.length || 0,
1100
- hasToolCalls: !!toolCalls,
1101
- toolCallsCount: toolCalls?.length || 0,
1102
- toolNames: toolCalls?.map(tc => tc.toolName) || [],
1103
- });
1104
- // Execute tools with unified loop handling
1105
- const toolResult = await this.executeUnifiedToolLoop({
1106
- toolCalls,
1107
- context,
1108
- session,
1109
- history,
1110
- selectedRoute,
1111
- responsePrompt,
1112
- availableTools,
1113
- responseSchema,
1114
- signal,
1115
- });
1116
- session = toolResult.session;
1117
- toolCalls = toolResult.finalToolCalls;
1118
- if (toolResult.finalMessage) {
1119
- message = toolResult.finalMessage;
1120
- }
1121
- // Collect data from response
1122
- // Use follow-up structured data from tool loop when available, fall back to original result
1123
- const dataSource = toolResult.structured
1124
- ? { structured: toolResult.structured }
1125
- : result;
1126
- session = await this.collectDataFromResponse({ result: dataSource, selectedRoute, nextStep, session });
1127
- return { message, toolCalls, session };
1076
+ // Build response schema for this flow (with collect fields from step)
1077
+ const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
1078
+ // ── HALT SHORT-CIRCUIT (Requirement 2.5, 2.6, 2.7) ──────────────────────
1079
+ // After pre-LLM emissions are merged, if `halt: true` then skip the LLM
1080
+ // call entirely. The behavior depends on whether `reply` is also set.
1081
+ if (mergedPreDirective?.halt) {
1082
+ if (mergedPreDirective.reply) {
1083
+ // halt + reply: emit the reply as the assistant message
1084
+ logger.debug(`[ResponseModal] Halt with reply — skipping LLM call for step ${nextStep.id}`);
1085
+ return { message: mergedPreDirective.reply, session, stoppedReason: 'reply' };
1086
+ }
1087
+ else {
1088
+ // halt without reply: emit empty assistant content
1089
+ logger.debug(`[ResponseModal] Halt without reply — skipping LLM call for step ${nextStep.id}`);
1090
+ return { message: '', session, stoppedReason: 'halt' };
1091
+ }
1092
+ }
1093
+ // ── STEP.REPLY SHORT-CIRCUIT (Requirement 25.1–25.7, 17.9) ──────────────
1094
+ // A step with `reply` set emits a verbatim template response without LLM.
1095
+ // onEnter and prepare have already fired normally at this point.
1096
+ // If prepare returned a PreDirective with `reply`, that overrides
1097
+ // the step-declared reply (last-emission-wins per Algorithm 4).
1098
+ if (nextStep.reply != null) {
1099
+ // Determine the effective reply: prepare-emitted reply wins over step-declared
1100
+ const effectiveReply = mergedPreDirective?.reply ?? await render(nextStep.reply, createTemplateContext({ data: session.data || {}, context, session }));
1101
+ logger.debug(`[ResponseModal] Step.reply — skipping LLM call for step ${nextStep.id}`);
1102
+ return { message: effectiveReply, session, stoppedReason: 'reply' };
1103
+ }
1104
+ // Transient appendage: per-turn slot from PreDirective.appendPrompt.
1105
+ // Fresh each turn, never cached, never persisted.
1106
+ // Wrapped in try/finally to ensure cleanup even on abnormal termination.
1107
+ let turnTransientAppendage = transientAppendage;
1108
+ try {
1109
+ // Build response prompt
1110
+ const { prompt: responsePrompt, appliedInstructions } = await this.responseEngine.buildResponsePrompt({
1111
+ flow: selectedFlow,
1112
+ currentStep: nextStep,
1113
+ rules: [],
1114
+ prohibitions: [],
1115
+ directives: responseDirectives,
1116
+ history: historyEvents,
1117
+ agentOptions: this.agent.getAgentOptions(),
1118
+ instructions: this.collectScopedInstructions(selectedFlow, nextStep),
1119
+ combinedTerms: this.agent.getTerms(),
1120
+ context,
1121
+ session,
1122
+ agentSchema: this.agent.schema,
1123
+ transientAppendage: turnTransientAppendage,
1124
+ });
1125
+ // Collect available tools for AI
1126
+ const availableTools = this.collectAvailableTools(selectedFlow, nextStep);
1127
+ // Generate message using AI provider
1128
+ const agentOptions = this.agent.getAgentOptions();
1129
+ const result = await agentOptions.provider.generateMessage({
1130
+ prompt: responsePrompt,
1131
+ history, // Use HistoryItem[] for AI provider
1132
+ context,
1133
+ tools: availableTools,
1134
+ signal,
1135
+ parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
1136
+ });
1137
+ let message = result.structured?.message || result.message;
1138
+ let toolCalls = result.structured?.toolCalls;
1139
+ // Debug: Log initial AI response
1140
+ logger.debug(`[ResponseModal] Initial AI response:`, {
1141
+ hasMessage: !!message,
1142
+ messageLength: message?.length || 0,
1143
+ hasToolCalls: !!toolCalls,
1144
+ toolCallsCount: toolCalls?.length || 0,
1145
+ toolNames: toolCalls?.map(tc => tc.toolName) || [],
1146
+ });
1147
+ // Execute tools with unified loop handling
1148
+ const toolResult = await this.executeUnifiedToolLoop({
1149
+ toolCalls,
1150
+ context,
1151
+ session,
1152
+ history,
1153
+ selectedFlow,
1154
+ responsePrompt,
1155
+ availableTools,
1156
+ responseSchema,
1157
+ signal,
1158
+ });
1159
+ session = toolResult.session;
1160
+ toolCalls = toolResult.finalToolCalls;
1161
+ if (toolResult.finalMessage) {
1162
+ message = toolResult.finalMessage;
1163
+ }
1164
+ // Collect data from response
1165
+ // Use follow-up structured data from tool loop when available, fall back to original result
1166
+ const dataSource = toolResult.structured
1167
+ ? { structured: toolResult.structured }
1168
+ : result;
1169
+ session = await this.collectDataFromResponse({ result: dataSource, selectedFlow, nextStep, session });
1170
+ return { message, toolCalls, session, appliedInstructions };
1171
+ }
1172
+ finally {
1173
+ // Drain the transient appendage at end of turn.
1174
+ // This ensures PreDirective.appendPrompt does not leak to subsequent
1175
+ // turns even when the turn terminates abnormally (error, abort, reject).
1176
+ turnTransientAppendage = undefined;
1177
+ }
1178
+ }
1179
+ /**
1180
+ * Unified streaming response generation
1181
+ * @private
1182
+ */
1183
+ async *generateUnifiedStreamingResponse(responseContext) {
1184
+ const { effectiveContext, session: initialSession, history, selectedFlow, selectedStep, responseDirectives, isFlowComplete, signal, signalFirings: preSignalFirings, signalPreDirective, signalHalted, signalHaltReply, } = responseContext;
1185
+ let session = initialSession;
1186
+ // Accumulator for signal firings across both phases (fire order)
1187
+ const signalFirings = [...(preSignalFirings || [])];
1188
+ // Convert HistoryItem[] to Event[] for internal processing
1189
+ const historyEvents = historyToEvents(history);
1190
+ // ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
1191
+ if (signalHalted) {
1192
+ const haltMessage = signalHaltReply || '';
1193
+ // Run post-signal phase even on halt
1194
+ const postResult = await this.responsePipeline.runPostSignalPhase(session, effectiveContext, historyEvents);
1195
+ session = postResult.updatedSession;
1196
+ signalFirings.push(...postResult.firings);
1197
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1198
+ session = { ...session, pendingDirective: postResult.mergedDirective };
1199
+ }
1200
+ await this.finalizeSession(session, effectiveContext);
1201
+ yield {
1202
+ delta: haltMessage,
1203
+ accumulated: haltMessage,
1204
+ done: true,
1205
+ session,
1206
+ stoppedReason: haltMessage ? 'reply' : 'halt',
1207
+ executedSteps: [],
1208
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1209
+ };
1210
+ return;
1211
+ }
1212
+ // ── Determine the inner stream generator based on flow state ────────
1213
+ let innerStream;
1214
+ if (selectedFlow && !isFlowComplete) {
1215
+ // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work (streaming path).
1216
+ let resolvedStep = selectedStep;
1217
+ const currentStepInstance = session.currentStep
1218
+ ? selectedFlow.getStep(session.currentStep.id)
1219
+ : selectedStep;
1220
+ if (currentStepInstance?.auto) {
1221
+ const autoChainExecutor = new AutoChainExecutor({
1222
+ maxAutoStepsPerTurn: this.agent.maxAutoStepsPerTurn,
1223
+ });
1224
+ const autoResult = await autoChainExecutor.run({
1225
+ session,
1226
+ context: effectiveContext,
1227
+ flow: selectedFlow,
1228
+ });
1229
+ session = autoResult.session;
1230
+ // Handle halt: emit verbatim reply as a single chunk, done.
1231
+ if (autoResult.stoppedReason === 'halt') {
1232
+ const reply = autoResult.mergedDirective?.reply || '';
1233
+ await this.finalizeSession(session, effectiveContext);
1234
+ yield {
1235
+ delta: reply,
1236
+ accumulated: reply,
1237
+ done: true,
1238
+ session,
1239
+ stoppedReason: 'halt',
1240
+ executedSteps: [],
1241
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1242
+ };
1243
+ return;
1244
+ }
1245
+ // Handle flow completion or cross-flow redirect from auto-chain.
1246
+ if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
1247
+ innerStream = this.streamFlowCompletion({
1248
+ selectedFlow,
1249
+ session,
1250
+ context: effectiveContext,
1251
+ history,
1252
+ historyEvents,
1253
+ stoppedReason: autoResult.stoppedReason,
1254
+ });
1255
+ }
1256
+ else {
1257
+ // Normal case: resolved to an interactive step.
1258
+ resolvedStep = autoResult.resolvedStep;
1259
+ innerStream = this.processFlowStreamingResponse({
1260
+ selectedFlow,
1261
+ selectedStep: resolvedStep,
1262
+ responseDirectives,
1263
+ session,
1264
+ history,
1265
+ context: effectiveContext,
1266
+ historyEvents,
1267
+ signal,
1268
+ transientAppendage: signalPreDirective?.appendPrompt,
1269
+ mergedPreDirective: signalPreDirective,
1270
+ });
1271
+ }
1272
+ }
1273
+ else {
1274
+ // No auto-step: directly stream the interactive step.
1275
+ innerStream = this.processFlowStreamingResponse({
1276
+ selectedFlow,
1277
+ selectedStep: resolvedStep,
1278
+ responseDirectives,
1279
+ session,
1280
+ history,
1281
+ context: effectiveContext,
1282
+ historyEvents,
1283
+ signal,
1284
+ // Propagate signal pre-directive's appendPrompt for this turn's LLM call
1285
+ transientAppendage: signalPreDirective?.appendPrompt,
1286
+ mergedPreDirective: signalPreDirective,
1287
+ });
1288
+ }
1289
+ }
1290
+ else if (isFlowComplete && selectedFlow) {
1291
+ // Handle flow completion streaming — implicit terminus (no successor
1292
+ // or all successors skipped), so the reason is 'last_step'.
1293
+ innerStream = this.streamFlowCompletion({
1294
+ selectedFlow,
1295
+ session,
1296
+ context: effectiveContext,
1297
+ history,
1298
+ historyEvents,
1299
+ stoppedReason: 'last_step',
1300
+ });
1301
+ }
1302
+ else {
1303
+ // Fallback: No flows defined, stream a simple response
1304
+ innerStream = this.streamFallbackResponse({
1305
+ history,
1306
+ context: effectiveContext,
1307
+ session,
1308
+ });
1309
+ }
1310
+ // ── Intercept the inner stream to run post-signal phase on the final chunk ──
1311
+ // This mirrors the non-streaming path: post-phase runs after finalize/onComplete
1312
+ // and before session persistence, attaching triggeredSignals to the final chunk
1313
+ // (Requirement 11.2).
1314
+ for await (const chunk of innerStream) {
1315
+ if (chunk.done) {
1316
+ // Run post-signal phase on final chunk (Requirement 9.1, 9.2)
1317
+ const postResult = await this.responsePipeline.runPostSignalPhase(chunk.session || session, effectiveContext, historyEvents);
1318
+ let finalSession = postResult.updatedSession;
1319
+ signalFirings.push(...postResult.firings);
1320
+ // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1321
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1322
+ finalSession = { ...finalSession, pendingDirective: postResult.mergedDirective };
1323
+ }
1324
+ yield {
1325
+ ...chunk,
1326
+ session: finalSession,
1327
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
1328
+ };
1329
+ }
1330
+ else {
1331
+ yield chunk;
1332
+ }
1333
+ }
1128
1334
  }
1129
1335
  /**
1130
- * Process route streaming response with unified tool execution and data collection
1336
+ * Process flow streaming response with unified tool execution and data collection
1131
1337
  * @private
1132
1338
  */
1133
- async *processRouteStreamingResponse(params) {
1134
- const { selectedRoute, selectedStep, responseDirectives, history, context, historyEvents, signal } = params;
1339
+ async *processFlowStreamingResponse(params) {
1340
+ const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1135
1341
  let session = params.session;
1136
1342
  // Determine next step (same logic as non-streaming)
1137
1343
  let nextStep;
@@ -1139,21 +1345,44 @@ export class ResponseModal {
1139
1345
  nextStep = selectedStep;
1140
1346
  }
1141
1347
  else {
1142
- // Determine current step from session if we're already in this route
1143
- const currentStep = session.currentRoute?.id === selectedRoute.id && session.currentStep
1144
- ? selectedRoute.getStep(session.currentStep.id)
1348
+ // Determine current step from session if we're already in this flow
1349
+ const currentStep = session.currentFlow?.id === selectedFlow.id && session.currentStep
1350
+ ? selectedFlow.getStep(session.currentStep.id)
1145
1351
  : undefined;
1146
- // Get candidate steps based on current position in the route
1147
- const routingEngine = this.agent.getRoutingEngine();
1148
- const candidates = await routingEngine.getCandidateStepsWithConditions(selectedRoute, currentStep, // Pass current step instead of undefined to maintain progression
1149
- createTemplateContext({ data: session.data, session, context }));
1150
- if (candidates.length > 0) {
1151
- nextStep = candidates[0].step;
1152
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new route'}`);
1352
+ // STEP 1 (Algorithm 1): branches win over linear chain
1353
+ if (currentStep?.branches && currentStep.branches.length > 0) {
1354
+ const branchResult = await this.responsePipeline.evaluateStepBranches(currentStep, selectedFlow, session, context);
1355
+ if (branchResult) {
1356
+ // Branch resolved yield a final chunk with the updated session and return
1357
+ if (branchResult.nextStep) {
1358
+ session = branchResult.session;
1359
+ nextStep = branchResult.nextStep;
1360
+ }
1361
+ else {
1362
+ // Flow transition or completion — no step to render
1363
+ yield {
1364
+ delta: '',
1365
+ accumulated: '',
1366
+ done: true,
1367
+ session: branchResult.session,
1368
+ };
1369
+ return;
1370
+ }
1371
+ }
1153
1372
  }
1154
- else {
1155
- nextStep = selectedRoute.initialStep;
1156
- logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
1373
+ if (!nextStep) {
1374
+ // Get candidate steps based on current position in the flow
1375
+ const flowRouter = this.agent.getFlowRouter();
1376
+ const candidates = await flowRouter.getCandidateStepsWithConditions(selectedFlow, currentStep, // Pass current step instead of undefined to maintain progression
1377
+ createTemplateContext({ data: session.data, session, context }));
1378
+ if (candidates.length > 0) {
1379
+ nextStep = candidates[0].step;
1380
+ logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1381
+ }
1382
+ else {
1383
+ nextStep = selectedFlow.initialStep;
1384
+ logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1385
+ }
1157
1386
  }
1158
1387
  }
1159
1388
  // Update session with next step
@@ -1162,13 +1391,13 @@ export class ResponseModal {
1162
1391
  const sessionData = session.data || {};
1163
1392
  const missingRequires = nextStep.requires.filter(field => sessionData[String(field)] === undefined);
1164
1393
  if (missingRequires.length > 0) {
1165
- const warning = `[Agent] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1166
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step.`;
1394
+ const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1395
+ `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1167
1396
  logger.warn(warning);
1168
1397
  console.warn(warning);
1169
1398
  const currentStepId = session.currentStep?.id;
1170
1399
  if (currentStepId) {
1171
- const currentStepInstance = selectedRoute.getStep(currentStepId);
1400
+ const currentStepInstance = selectedFlow.getStep(currentStepId);
1172
1401
  if (currentStepInstance) {
1173
1402
  nextStep = currentStepInstance;
1174
1403
  logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
@@ -1185,142 +1414,192 @@ export class ResponseModal {
1185
1414
  logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1186
1415
  }
1187
1416
  // Build response schema and prompt (same as non-streaming)
1188
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
1189
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
1190
- route: selectedRoute,
1191
- currentStep: nextStep,
1192
- rules: selectedRoute.getRules(),
1193
- prohibitions: selectedRoute.getProhibitions(),
1194
- directives: responseDirectives,
1195
- history: historyEvents,
1196
- agentOptions: this.agent.getAgentOptions(),
1197
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
1198
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
1199
- context,
1200
- session,
1201
- agentSchema: this.agent.getSchema(),
1202
- });
1203
- // Collect available tools for AI
1204
- const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
1205
- // Generate message stream using AI provider
1206
- const agentOptions = this.agent.getAgentOptions();
1207
- const stream = agentOptions.provider.generateMessageStream({
1208
- prompt: responsePrompt,
1209
- history, // Use HistoryItem[] for AI provider
1210
- context,
1211
- tools: availableTools,
1212
- signal,
1213
- parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
1214
- });
1215
- // Stream chunks with unified tool handling
1216
- for await (const chunk of stream) {
1217
- let toolCalls = undefined;
1218
- // Extract tool calls from AI response on final chunk
1219
- if (chunk.done && chunk.structured?.toolCalls) {
1220
- toolCalls = chunk.structured.toolCalls;
1221
- const toolManager = this.getToolManager();
1222
- // Use concurrent execution for the initial batch of tool calls
1223
- if (toolManager && typeof toolManager.executeWithConcurrency === 'function') {
1224
- const toolCallRequests = toolCalls.map((tc, i) => ({
1225
- id: `${tc.toolName}-${i}-${Date.now()}`,
1226
- toolName: tc.toolName,
1227
- arguments: tc.arguments,
1228
- }));
1229
- const historyEvents = historyToEvents(history);
1230
- try {
1231
- for await (const update of toolManager.executeWithConcurrency({
1232
- toolCalls: toolCallRequests,
1233
- context,
1234
- data: session.data,
1235
- history: historyEvents,
1236
- signal,
1237
- route: selectedRoute,
1238
- step: nextStep,
1239
- })) {
1240
- // Apply context updates
1241
- if (update.contextUpdate) {
1242
- try {
1243
- await this.agent.updateContext(update.contextUpdate);
1244
- }
1245
- catch (error) {
1246
- logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
1417
+ const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
1418
+ // ── HALT SHORT-CIRCUIT (Requirement 2.5, 2.6, 2.7) ──────────────────────
1419
+ // After pre-LLM emissions are merged, if `halt: true` then skip the LLM
1420
+ // call entirely. Emit a single done chunk with the appropriate content.
1421
+ if (mergedPreDirective?.halt) {
1422
+ const reply = mergedPreDirective.reply || '';
1423
+ const reason = mergedPreDirective.reply ? 'reply' : 'halt';
1424
+ logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
1425
+ await this.finalizeSession(session, context);
1426
+ yield {
1427
+ delta: reply,
1428
+ accumulated: reply,
1429
+ done: true,
1430
+ session,
1431
+ stoppedReason: reason,
1432
+ executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
1433
+ };
1434
+ return;
1435
+ }
1436
+ // ── STEP.REPLY SHORT-CIRCUIT (Requirement 25.1–25.7, 17.9) ──────────────
1437
+ // A step with `reply` set emits a verbatim template response without LLM.
1438
+ // onEnter and prepare have already fired normally. If prepare returned
1439
+ // a PreDirective with `reply`, that overrides the step-declared reply.
1440
+ if (nextStep.reply != null) {
1441
+ const effectiveReply = mergedPreDirective?.reply ?? await render(nextStep.reply, createTemplateContext({ data: session.data || {}, context, session }));
1442
+ logger.debug(`[ResponseModal] Step.reply (streaming) skipping LLM call for step ${nextStep.id}`);
1443
+ await this.finalizeSession(session, context);
1444
+ yield {
1445
+ delta: effectiveReply,
1446
+ accumulated: effectiveReply,
1447
+ done: true,
1448
+ session,
1449
+ stoppedReason: 'reply',
1450
+ executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
1451
+ };
1452
+ return;
1453
+ }
1454
+ // Transient appendage: per-turn slot from PreDirective.appendPrompt.
1455
+ // Fresh each turn, never cached, never persisted.
1456
+ // Wrapped in try/finally to ensure cleanup even on abnormal termination.
1457
+ let turnTransientAppendage = transientAppendage;
1458
+ try {
1459
+ const { prompt: responsePrompt, appliedInstructions } = await this.responseEngine.buildResponsePrompt({
1460
+ flow: selectedFlow,
1461
+ currentStep: nextStep,
1462
+ rules: [],
1463
+ prohibitions: [],
1464
+ directives: responseDirectives,
1465
+ history: historyEvents,
1466
+ agentOptions: this.agent.getAgentOptions(),
1467
+ instructions: this.collectScopedInstructions(selectedFlow, nextStep),
1468
+ combinedTerms: this.agent.getTerms(),
1469
+ context,
1470
+ session,
1471
+ agentSchema: this.agent.schema,
1472
+ transientAppendage: turnTransientAppendage,
1473
+ });
1474
+ // Collect available tools for AI
1475
+ const availableTools = this.collectAvailableTools(selectedFlow, nextStep);
1476
+ // Generate message stream using AI provider
1477
+ const agentOptions = this.agent.getAgentOptions();
1478
+ const stream = agentOptions.provider.generateMessageStream({
1479
+ prompt: responsePrompt,
1480
+ history, // Use HistoryItem[] for AI provider
1481
+ context,
1482
+ tools: availableTools,
1483
+ signal,
1484
+ parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
1485
+ });
1486
+ // Stream chunks with unified tool handling
1487
+ for await (const chunk of stream) {
1488
+ let toolCalls = undefined;
1489
+ // Extract tool calls from AI response on final chunk
1490
+ if (chunk.done && chunk.structured?.toolCalls) {
1491
+ toolCalls = chunk.structured.toolCalls;
1492
+ const toolManager = this.getToolManager();
1493
+ // Use concurrent execution for the initial batch of tool calls
1494
+ if (toolManager && typeof toolManager.executeWithConcurrency === 'function') {
1495
+ const toolCallRequests = toolCalls.map((tc, i) => ({
1496
+ id: `${tc.toolName}-${i}-${Date.now()}`,
1497
+ toolName: tc.toolName,
1498
+ arguments: tc.arguments,
1499
+ }));
1500
+ const historyEvents = historyToEvents(history);
1501
+ try {
1502
+ for await (const update of toolManager.executeWithConcurrency({
1503
+ toolCalls: toolCallRequests,
1504
+ context,
1505
+ data: session.data,
1506
+ history: historyEvents,
1507
+ signal,
1508
+ flow: selectedFlow,
1509
+ step: nextStep,
1510
+ })) {
1511
+ // Apply context updates
1512
+ if (update.contextUpdate) {
1513
+ try {
1514
+ await this.agent.updateContext(update.contextUpdate);
1515
+ }
1516
+ catch (error) {
1517
+ logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
1518
+ }
1247
1519
  }
1248
- }
1249
- // Apply data updates
1250
- if (update.dataUpdate) {
1251
- try {
1252
- const updateDataMethod = this.agent.getUpdateDataMethod();
1253
- session = await updateDataMethod(session, update.dataUpdate);
1520
+ // Apply data updates
1521
+ if (update.dataUpdate) {
1522
+ try {
1523
+ const updateDataMethod = this.agent.getUpdateDataMethod();
1524
+ session = await updateDataMethod(session, update.dataUpdate);
1525
+ }
1526
+ catch (error) {
1527
+ logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
1528
+ }
1254
1529
  }
1255
- catch (error) {
1256
- logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
1530
+ // Yield progress updates immediately
1531
+ if (update.progress) {
1532
+ yield {
1533
+ delta: '',
1534
+ accumulated: chunk.accumulated,
1535
+ done: false,
1536
+ session,
1537
+ toolCalls: undefined,
1538
+ isFlowComplete: false,
1539
+ metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
1540
+ };
1257
1541
  }
1258
1542
  }
1259
- // Yield progress updates immediately
1260
- if (update.progress) {
1261
- yield {
1262
- delta: '',
1263
- accumulated: chunk.accumulated,
1264
- done: false,
1265
- session,
1266
- toolCalls: undefined,
1267
- isRouteComplete: false,
1268
- metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
1269
- };
1270
- }
1543
+ logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
1544
+ }
1545
+ catch (error) {
1546
+ logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
1547
+ // Fall back to the unified tool loop on failure
1548
+ const toolResult = await this.executeUnifiedToolLoop({
1549
+ toolCalls, context, session, history, selectedFlow,
1550
+ responsePrompt, availableTools, responseSchema, signal,
1551
+ });
1552
+ session = toolResult.session;
1553
+ toolCalls = toolResult.finalToolCalls;
1271
1554
  }
1272
- logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
1273
1555
  }
1274
- catch (error) {
1275
- logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
1276
- // Fall back to the unified tool loop on failure
1556
+ else {
1557
+ // Fallback: no ToolManager or no executeWithConcurrency, use unified tool loop
1277
1558
  const toolResult = await this.executeUnifiedToolLoop({
1278
- toolCalls, context, session, history, selectedRoute,
1559
+ toolCalls, context, session, history, selectedFlow,
1279
1560
  responsePrompt, availableTools, responseSchema, signal,
1280
1561
  });
1281
1562
  session = toolResult.session;
1282
1563
  toolCalls = toolResult.finalToolCalls;
1283
1564
  }
1284
1565
  }
1285
- else {
1286
- // Fallback: no ToolManager or no executeWithConcurrency, use unified tool loop
1287
- const toolResult = await this.executeUnifiedToolLoop({
1288
- toolCalls, context, session, history, selectedRoute,
1289
- responsePrompt, availableTools, responseSchema, signal,
1566
+ // Extract collected data on final chunk
1567
+ if (chunk.done && chunk.structured && nextStep.collect) {
1568
+ session = await this.collectDataFromResponse({
1569
+ result: { structured: chunk.structured },
1570
+ selectedFlow,
1571
+ nextStep,
1572
+ session,
1290
1573
  });
1291
- session = toolResult.session;
1292
- toolCalls = toolResult.finalToolCalls;
1293
1574
  }
1294
- }
1295
- // Extract collected data on final chunk
1296
- if (chunk.done && chunk.structured && nextStep.collect) {
1297
- session = await this.collectDataFromResponse({
1298
- result: { structured: chunk.structured },
1299
- selectedRoute,
1300
- nextStep,
1575
+ // Handle session finalization on final chunk
1576
+ if (chunk.done) {
1577
+ await this.finalizeSession(session, context);
1578
+ }
1579
+ // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1580
+ // - executedSteps: single step executed in this response
1581
+ // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
1582
+ // - session.currentStep: reflects the executed step
1583
+ yield {
1584
+ delta: chunk.delta,
1585
+ accumulated: chunk.accumulated,
1586
+ done: chunk.done,
1301
1587
  session,
1302
- });
1303
- }
1304
- // Handle session finalization on final chunk
1305
- if (chunk.done) {
1306
- await this.finalizeSession(session, context);
1588
+ toolCalls,
1589
+ isFlowComplete: false,
1590
+ executedSteps: chunk.done ? [{ id: nextStep.id, flowId: selectedFlow.id }] : undefined,
1591
+ stoppedReason: chunk.done ? 'needs_input' : undefined,
1592
+ metadata: chunk.metadata,
1593
+ structured: chunk.structured,
1594
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
1595
+ };
1307
1596
  }
1308
- // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1309
- // - executedSteps: single step executed in this response
1310
- // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
1311
- // - session.currentStep: reflects the executed step
1312
- yield {
1313
- delta: chunk.delta,
1314
- accumulated: chunk.accumulated,
1315
- done: chunk.done,
1316
- session,
1317
- toolCalls,
1318
- isRouteComplete: false,
1319
- executedSteps: chunk.done ? [{ id: nextStep.id, routeId: selectedRoute.id }] : undefined,
1320
- stoppedReason: chunk.done ? 'needs_input' : undefined,
1321
- metadata: chunk.metadata,
1322
- structured: chunk.structured,
1323
- };
1597
+ }
1598
+ finally {
1599
+ // Drain the transient appendage at end of turn.
1600
+ // This ensures PreDirective.appendPrompt does not leak to subsequent
1601
+ // turns even when the turn terminates abnormally (error, abort, reject).
1602
+ turnTransientAppendage = undefined;
1324
1603
  }
1325
1604
  }
1326
1605
  /**
@@ -1330,19 +1609,21 @@ export class ResponseModal {
1330
1609
  */
1331
1610
  async executeUnifiedToolLoop(params) {
1332
1611
  try {
1333
- const { context, history, selectedRoute, responsePrompt, availableTools, responseSchema, signal } = params;
1612
+ const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
1334
1613
  let { toolCalls, session } = params;
1335
1614
  // Convert HistoryItem[] to Event[] for internal processing
1336
1615
  const historyEvents = historyToEvents(history);
1337
1616
  // Map to store tool execution results for history
1338
1617
  const toolResultsMap = new Map();
1618
+ // Map to store tool call arguments for history reconstruction
1619
+ const toolArgsMap = new Map();
1339
1620
  // Execute initial dynamic tool calls
1340
1621
  if (toolCalls && toolCalls.length > 0) {
1341
1622
  logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
1342
1623
  for (const toolCall of toolCalls) {
1343
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1624
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1344
1625
  if (!tool) {
1345
- logger.warn(`[ResponseModal] Tool not found: ${toolCall.toolName}`);
1626
+ 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.`);
1346
1627
  continue;
1347
1628
  }
1348
1629
  try {
@@ -1366,6 +1647,7 @@ export class ResponseModal {
1366
1647
  }
1367
1648
  // Store the actual tool result data for history
1368
1649
  toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
1650
+ toolArgsMap.set(toolCall.toolName, toolCall.arguments);
1369
1651
  // Check if tool execution was successful
1370
1652
  if (!toolResult.success) {
1371
1653
  logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
@@ -1415,7 +1697,7 @@ export class ResponseModal {
1415
1697
  // Create tool result history items
1416
1698
  const toolResultHistoryItems = [];
1417
1699
  for (const toolCall of toolCalls || []) {
1418
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1700
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1419
1701
  if (tool) {
1420
1702
  // Create HistoryItem format for tool results
1421
1703
  // Add assistant message with tool_calls
@@ -1473,9 +1755,9 @@ export class ResponseModal {
1473
1755
  logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls.length} additional tool calls`);
1474
1756
  // Execute the follow-up tool calls
1475
1757
  for (const toolCall of followUpToolCalls) {
1476
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1758
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1477
1759
  if (!tool) {
1478
- logger.warn(`[ResponseModal] Tool not found in follow-up: ${toolCall.toolName}`);
1760
+ 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.`);
1479
1761
  continue;
1480
1762
  }
1481
1763
  try {
@@ -1523,6 +1805,7 @@ export class ResponseModal {
1523
1805
  }
1524
1806
  // Store the follow-up tool result for potential next loop iteration
1525
1807
  toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
1808
+ toolArgsMap.set(toolCall.toolName, toolCall.arguments);
1526
1809
  logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
1527
1810
  }
1528
1811
  catch (error) {
@@ -1543,7 +1826,7 @@ export class ResponseModal {
1543
1826
  }
1544
1827
  }
1545
1828
  if (toolLoopCount >= MAX_TOOL_LOOPS) {
1546
- logger.warn(`[ResponseModal] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
1829
+ 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.`);
1547
1830
  }
1548
1831
  // If tools were executed but no final text message was produced,
1549
1832
  // make one more LLM call to generate a proper text response from tool results.
@@ -1563,7 +1846,7 @@ export class ResponseModal {
1563
1846
  tool_calls: [{
1564
1847
  id: toolName,
1565
1848
  name: toolName,
1566
- arguments: {},
1849
+ arguments: toolArgsMap.get(toolName) || {},
1567
1850
  }],
1568
1851
  });
1569
1852
  finalToolResultHistoryItems.push({
@@ -1626,30 +1909,30 @@ export class ResponseModal {
1626
1909
  */
1627
1910
  async collectDataFromResponse(params) {
1628
1911
  try {
1629
- const { result, selectedRoute, nextStep, session } = params;
1912
+ const { result, selectedFlow, nextStep, session } = params;
1630
1913
  let updatedSession = session;
1631
- // Extract collected data from final response (only for route-based interactions)
1632
- if (selectedRoute && result.structured) {
1914
+ // Extract collected data from final response (only for flow-based interactions)
1915
+ if (selectedFlow && result.structured) {
1633
1916
  try {
1634
1917
  const collectedData = {};
1635
1918
  // AgentStructuredResponse extends Record<string, unknown>, so we can safely access properties
1636
1919
  const structuredData = result.structured;
1637
- // Collect ALL route fields (required + optional) from structured response
1638
- const allRouteFields = new Set();
1639
- // Add route required fields
1640
- if (selectedRoute.requiredFields) {
1641
- selectedRoute.requiredFields.forEach(field => allRouteFields.add(String(field)));
1920
+ // Collect ALL flow fields (required + optional) from structured response
1921
+ const allFlowFields = new Set();
1922
+ // Add flow required fields
1923
+ if (selectedFlow.requiredFields) {
1924
+ selectedFlow.requiredFields.forEach(field => allFlowFields.add(String(field)));
1642
1925
  }
1643
- // Add route optional fields
1644
- if (selectedRoute.optionalFields) {
1645
- selectedRoute.optionalFields.forEach(field => allRouteFields.add(String(field)));
1926
+ // Add flow optional fields
1927
+ if (selectedFlow.optionalFields) {
1928
+ selectedFlow.optionalFields.forEach(field => allFlowFields.add(String(field)));
1646
1929
  }
1647
- // Also include current step's collect fields (in case they're not in route fields)
1930
+ // Also include current step's collect fields (in case they're not in flow fields)
1648
1931
  if (nextStep?.collect) {
1649
- nextStep.collect.forEach(field => allRouteFields.add(String(field)));
1932
+ nextStep.collect.forEach(field => allFlowFields.add(String(field)));
1650
1933
  }
1651
1934
  // Extract all available fields from structured response
1652
- for (const field of allRouteFields) {
1935
+ for (const field of allFlowFields) {
1653
1936
  const fieldKey = String(field);
1654
1937
  if (fieldKey in structuredData && structuredData[fieldKey] !== undefined && structuredData[fieldKey] !== null) {
1655
1938
  collectedData[fieldKey] = structuredData[fieldKey];
@@ -1699,207 +1982,122 @@ export class ResponseModal {
1699
1982
  }
1700
1983
  }
1701
1984
  /**
1702
- * Handle route completion logic
1985
+ * Apply flow completion: release the session to idle state.
1986
+ *
1987
+ * This is a pure state transition. The framework emits **no message of
1988
+ * its own** at the completion boundary — every word delivered to the
1989
+ * user comes from a developer-defined step prompt. If the dev wants a
1990
+ * closing turn, they add a final interactive step with their own
1991
+ * `prompt`; the framework respects that step's natural LLM output.
1992
+ *
1993
+ * Behavior:
1994
+ * - Marks the active `flowHistory` entry as `completed: true` and
1995
+ * stamps `exitedAt`.
1996
+ * - Evaluates `flow.onComplete` for an explicit follow-up transition.
1997
+ * When set, populates `session.pendingDirective` (the next turn's
1998
+ * pipeline applies it). When absent, the session is fully idle.
1999
+ * - Clears `currentFlow` and `currentStep` to `undefined`.
2000
+ * - Clears owned fields when the flow is `reentrant` so subsequent
2001
+ * re-selections start from a clean state.
2002
+ *
2003
+ * Returns the updated session. Callers compose any reply text from
2004
+ * their own sources (an upstream LLM turn, a directive's `reply`, or
2005
+ * an empty string for silent completion).
2006
+ *
1703
2007
  * @private
1704
2008
  */
1705
- async handleRouteCompletion(params) {
1706
- const { selectedRoute, session, context, history, historyEvents, signal } = params;
1707
- // Get endStep spec from route
1708
- const endStepSpec = selectedRoute.endStepSpec;
1709
- // Create a temporary step for completion message generation using endStep configuration
1710
- const completionStep = new Step(selectedRoute.id, {
1711
- description: endStepSpec.description,
1712
- id: endStepSpec.id || END_ROUTE_ID,
1713
- collect: endStepSpec.collect,
1714
- requires: endStepSpec.requires,
1715
- prompt: endStepSpec.prompt || "Send a brief, natural farewell message thanking the user. Do NOT list or mention any collected data, field names, or internal information.",
2009
+ async applyFlowCompletion(params) {
2010
+ const { selectedFlow, session, context } = params;
2011
+ // 1) Evaluate onComplete first — needs the still-active session shape.
2012
+ const transitionConfig = await selectedFlow.evaluateOnComplete({ data: session.data }, context);
2013
+ // 2) Release to idle. If the flow is reentrant, scrub its owned
2014
+ // fields so re-selection on a future turn starts clean. When
2015
+ // onComplete fires we still go idle here — the next turn's
2016
+ // pipeline applies the pendingDirective before any routing.
2017
+ const ownedFields = selectedFlow.reentrant
2018
+ ? [
2019
+ ...(selectedFlow.requiredFields ?? []),
2020
+ ...(selectedFlow.optionalFields ?? []),
2021
+ ]
2022
+ : undefined;
2023
+ let nextSession = completeCurrentFlow(session, {
2024
+ clearOwnedFields: ownedFields,
1716
2025
  });
1717
- // Build response schema for completion (message only, no data collection)
1718
- const completionSchema = {
1719
- type: "object",
1720
- properties: {
1721
- message: {
1722
- type: "string",
1723
- description: "A natural, warm farewell message for the user. Must NOT contain task names, field names, collected data, or any internal/technical information.",
1724
- },
1725
- },
1726
- required: ["message"],
1727
- additionalProperties: false,
1728
- };
1729
- const templateContext = createTemplateContext({ context, session, history: historyEvents });
1730
- // Build completion response prompt using ResponseEngine
1731
- // Filter out conditional guidelines - only include always-active ones
1732
- const alwaysActiveGuidelines = [
1733
- ...this.agent.getGuidelines().filter(g => !g.condition),
1734
- ...selectedRoute.getGuidelines().filter(g => !g.condition),
1735
- ];
1736
- let completitionPrompt = "Send a brief, natural farewell message. Do NOT mention internal data or task details.";
1737
- if (endStepSpec.prompt) {
1738
- completitionPrompt = await render(endStepSpec.prompt, templateContext);
1739
- }
1740
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
1741
- route: selectedRoute,
1742
- currentStep: completionStep,
1743
- rules: selectedRoute.getRules(),
1744
- prohibitions: selectedRoute.getProhibitions(),
1745
- directives: [
1746
- "The conversation task has been completed successfully",
1747
- "Generate a natural, friendly farewell message for the user",
1748
- "Do NOT mention task names, route names, collected data, field names, or any internal/technical information",
1749
- "Do NOT list or summarize the data you collected - the user already knows what they told you",
1750
- "Do NOT use words like 'tarefa', 'dados coletados', 'prospecção', 'concluída' or similar internal terms",
1751
- "Keep it brief, warm, and conversational - as if ending a natural conversation",
1752
- "Do NOT ask for more information - the conversation is ending",
1753
- completitionPrompt,
1754
- ],
1755
- history: historyEvents,
1756
- agentOptions: this.agent.getAgentOptions(),
1757
- combinedGuidelines: alwaysActiveGuidelines, // Only non-conditional guidelines
1758
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
1759
- context,
1760
- session,
1761
- agentSchema: undefined, // No data collection schema for completion
1762
- });
1763
- // Generate completion message using AI provider
1764
- const agentOptions = this.agent.getAgentOptions();
1765
- logger.debug(`[ResponseModal] Calling AI provider for completion message...`);
1766
- const completionResult = await agentOptions.provider.generateMessage({
1767
- prompt: completionPrompt,
1768
- history, // Use HistoryItem[] for AI provider
1769
- context,
1770
- signal,
1771
- parameters: { jsonSchema: completionSchema, schemaName: "completion_message" },
1772
- });
1773
- logger.debug(`[ResponseModal] AI provider returned completion result`);
1774
- const message = completionResult.structured?.message || completionResult.message;
1775
- logger.debug(`[ResponseModal] Generated completion message for route: ${selectedRoute.title}`);
1776
- // Check for onComplete transition
1777
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
2026
+ // 3) Wire pendingDirective when onComplete returned a target.
1778
2027
  if (transitionConfig) {
1779
- // Find target route by ID or title
1780
- const targetRoute = this.agent.getRoutes().find((r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep);
1781
- if (targetRoute) {
1782
- const renderedCondition = await render(transitionConfig.condition, templateContext);
1783
- // Set pending transition in session
1784
- session.pendingTransition = {
1785
- targetRouteId: targetRoute.id,
1786
- condition: renderedCondition,
1787
- reason: "route_complete",
2028
+ const goToTarget = typeof transitionConfig.goTo === 'string'
2029
+ ? transitionConfig.goTo
2030
+ : transitionConfig.goTo?.flow;
2031
+ const targetFlow = goToTarget ? this.agent.getFlows().find((r) => r.id === goToTarget ||
2032
+ r.title === goToTarget) : undefined;
2033
+ if (targetFlow) {
2034
+ nextSession = {
2035
+ ...nextSession,
2036
+ pendingDirective: {
2037
+ goTo: targetFlow.id,
2038
+ },
1788
2039
  };
1789
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
2040
+ logger.debug(`[ResponseModal] Flow ${selectedFlow.title} completed with pending directive to: ${targetFlow.title}`);
1790
2041
  }
1791
- else {
1792
- logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
2042
+ else if (goToTarget) {
2043
+ logger.warn(`[FlowConfigurationError] onComplete target not found: flow "${selectedFlow.title}" completed but onComplete target "${goToTarget}" does not match any flow. ` +
2044
+ `Fix the onComplete value to reference an existing flow id/title, or remove onComplete to release the session to idle.`);
1793
2045
  }
1794
2046
  }
1795
- return message;
2047
+ else {
2048
+ logger.debug(`[ResponseModal] Flow ${selectedFlow.title} completed; session released to idle.`);
2049
+ }
2050
+ return nextSession;
1796
2051
  }
1797
2052
  /**
1798
- * Stream route completion response
2053
+ * Stream flow completion response
1799
2054
  * @private
1800
2055
  */
1801
- async *streamRouteCompletion(params) {
1802
- const { selectedRoute, context, history, historyEvents, signal } = params;
1803
- let session = params.session;
1804
- // Get endStep spec from route
1805
- const endStepSpec = selectedRoute.endStepSpec;
1806
- // Create a temporary step for completion message generation using endStep configuration
1807
- const completionStep = new Step(selectedRoute.id, {
1808
- description: endStepSpec.description,
1809
- id: endStepSpec.id || END_ROUTE_ID,
1810
- collect: endStepSpec.collect,
1811
- requires: endStepSpec.requires,
1812
- prompt: endStepSpec.prompt || "Send a brief, natural farewell message thanking the user. Do NOT list or mention any collected data, field names, or internal information.",
1813
- });
1814
- // Build response schema for completion
1815
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
1816
- const templateContext = createTemplateContext({ context, session, history: historyEvents }); // Use Event[] for template context
1817
- // Build completion response prompt
1818
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
1819
- route: selectedRoute,
1820
- currentStep: completionStep,
1821
- rules: selectedRoute.getRules(),
1822
- prohibitions: selectedRoute.getProhibitions(),
1823
- directives: undefined, // No directives for completion
1824
- history: historyEvents,
1825
- agentOptions: this.agent.getAgentOptions(),
1826
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
1827
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
1828
- context,
1829
- session,
1830
- agentSchema: this.agent.getSchema(),
1831
- });
1832
- // Stream completion message using AI provider
1833
- const agentOptions = this.agent.getAgentOptions();
1834
- const stream = agentOptions.provider.generateMessageStream({
1835
- prompt: completionPrompt,
1836
- history, // Use HistoryItem[] for AI provider
2056
+ /**
2057
+ * Stream a flow completion as a single terminal chunk.
2058
+ *
2059
+ * No LLM call is made. The framework no longer authors a farewell — the
2060
+ * completion path is a pure state transition. The chunk emits an empty
2061
+ * `delta` and a `done: true` flag with the idle session attached so
2062
+ * downstream consumers can finalize cleanly.
2063
+ *
2064
+ * If the developer wants closing copy in a streaming response, they
2065
+ * should add a final interactive step whose own LLM turn delivers it.
2066
+ *
2067
+ * @private
2068
+ */
2069
+ async *streamFlowCompletion(params) {
2070
+ const { selectedFlow, context, history } = params;
2071
+ const session = await this.applyFlowCompletion({
2072
+ selectedFlow,
2073
+ session: params.session,
1837
2074
  context,
1838
- signal,
1839
- parameters: { jsonSchema: responseSchema, schemaName: "completion_message_stream" },
2075
+ history,
1840
2076
  });
1841
- logger.debug(`[ResponseModal] Streaming completion message for route: ${selectedRoute.title}`);
1842
- // Check for onComplete transition
1843
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
1844
- if (transitionConfig) {
1845
- // Find target route by ID or title
1846
- const targetRoute = this.agent.getRoutes().find((r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep);
1847
- if (targetRoute) {
1848
- const renderedCondition = await render(transitionConfig.condition, templateContext);
1849
- // Set pending transition in session
1850
- session = {
1851
- ...session,
1852
- pendingTransition: {
1853
- targetRouteId: targetRoute.id,
1854
- condition: renderedCondition,
1855
- reason: "route_complete",
1856
- },
1857
- };
1858
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
1859
- }
1860
- else {
1861
- logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
1862
- }
1863
- }
1864
- // Set step to END_ROUTE marker
1865
- session = enterStep(session, END_ROUTE_ID, "Route completed");
1866
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
1867
- // Stream completion chunks
1868
- for await (const chunk of stream) {
1869
- // Update current session if we have one
1870
- if (chunk.done) {
1871
- await this.finalizeSession(session, context);
1872
- }
1873
- // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1874
- // - executedSteps: empty for route completion (no new steps executed)
1875
- // - stoppedReason: 'route_complete' for completed routes
1876
- // - session.currentStep: set to END_ROUTE
1877
- yield {
1878
- delta: chunk.delta,
1879
- accumulated: chunk.accumulated,
1880
- done: chunk.done,
1881
- session,
1882
- toolCalls: undefined,
1883
- isRouteComplete: true,
1884
- executedSteps: chunk.done ? [] : undefined,
1885
- stoppedReason: chunk.done ? 'route_complete' : undefined,
1886
- metadata: chunk.metadata,
1887
- structured: chunk.structured,
1888
- };
1889
- }
2077
+ await this.finalizeSession(session, context);
2078
+ yield {
2079
+ delta: '',
2080
+ accumulated: '',
2081
+ done: true,
2082
+ session,
2083
+ toolCalls: undefined,
2084
+ isFlowComplete: true,
2085
+ executedSteps: [],
2086
+ stoppedReason: params.stoppedReason ?? 'completed',
2087
+ };
1890
2088
  }
1891
2089
  /**
1892
- * Generate fallback response when no routes are available
2090
+ * Generate fallback response when no flows are available
1893
2091
  * @private
1894
2092
  */
1895
2093
  async generateFallbackResponse(params) {
1896
2094
  const { history, context, session, signal } = params;
1897
- logger.debug(`[ResponseModal] No route selected, generating basic response`);
1898
- // Build basic response prompt without route context
1899
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
2095
+ logger.debug(`[ResponseModal] No flow selected, generating basic response`);
2096
+ // Build basic response prompt without flow context
2097
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
1900
2098
  agentOptions: this.agent.getAgentOptions(),
1901
2099
  terms: this.agent.getTerms(),
1902
- guidelines: this.agent.getGuidelines(),
2100
+ instructions: this.collectScopedInstructions(),
1903
2101
  context,
1904
2102
  session,
1905
2103
  });
@@ -1919,18 +2117,18 @@ export class ResponseModal {
1919
2117
  schemaName: "fallback_response",
1920
2118
  },
1921
2119
  });
1922
- return result.structured?.message || result.message;
2120
+ return { message: result.structured?.message || result.message, appliedInstructions };
1923
2121
  }
1924
2122
  /**
1925
- * Stream fallback response when no routes are available
2123
+ * Stream fallback response when no flows are available
1926
2124
  * @private
1927
2125
  */
1928
2126
  async *streamFallbackResponse(params) {
1929
2127
  const { history, context, session, signal } = params;
1930
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
2128
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
1931
2129
  agentOptions: this.agent.getAgentOptions(),
1932
2130
  terms: this.agent.getTerms(),
1933
- guidelines: this.agent.getGuidelines(),
2131
+ instructions: this.collectScopedInstructions(),
1934
2132
  context,
1935
2133
  session,
1936
2134
  });
@@ -1956,8 +2154,8 @@ export class ResponseModal {
1956
2154
  await this.finalizeSession(session, context);
1957
2155
  }
1958
2156
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1959
- // - executedSteps: empty for fallback (no route/step execution)
1960
- // - stoppedReason: undefined for fallback (no route context)
2157
+ // - executedSteps: empty for fallback (no flow/step execution)
2158
+ // - stoppedReason: undefined for fallback (no flow context)
1961
2159
  // - session.currentStep: unchanged (no step progression)
1962
2160
  yield {
1963
2161
  delta: chunk.delta,
@@ -1965,11 +2163,12 @@ export class ResponseModal {
1965
2163
  done: chunk.done,
1966
2164
  session,
1967
2165
  toolCalls: undefined,
1968
- isRouteComplete: false,
2166
+ isFlowComplete: false,
1969
2167
  executedSteps: chunk.done ? [] : undefined,
1970
2168
  stoppedReason: undefined,
1971
2169
  metadata: chunk.metadata,
1972
2170
  structured: chunk.structured,
2171
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
1973
2172
  };
1974
2173
  }
1975
2174
  }
@@ -1990,52 +2189,52 @@ export class ResponseModal {
1990
2189
  // Execute finalize function
1991
2190
  await this.executeStepFinalize(session, context);
1992
2191
  // Update current session if we have one
1993
- const currentSession = this.agent.getCurrentSession();
2192
+ const currentSession = this.agent.currentSession;
1994
2193
  if (currentSession) {
1995
- this.agent.setCurrentSession(session);
2194
+ this.agent.currentSession = session;
1996
2195
  }
1997
2196
  }
1998
2197
  // ============================================================================
1999
2198
  // UTILITY METHODS - Helper methods for tool management and other utilities
2000
2199
  // ============================================================================
2001
2200
  /**
2002
- * Find an available tool by name for the given route using ToolManager
2201
+ * Find an available tool by name for the given flow using ToolManager
2003
2202
  * Delegates to ToolManager for unified tool resolution
2004
2203
  * @private
2005
2204
  */
2006
- findAvailableTool(toolName, route) {
2205
+ findAvailableTool(toolName, flow) {
2007
2206
  // Use ToolManager for unified tool resolution
2008
2207
  const toolManager = this.getToolManager();
2009
2208
  if (toolManager) {
2010
- return toolManager.find(toolName, undefined, undefined, route);
2209
+ return toolManager.find(toolName, undefined, undefined, flow);
2011
2210
  }
2012
2211
  // Fallback to legacy resolution if ToolManager not available
2013
2212
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for: ${toolName}`);
2014
- // Check route-level tools first (if route provided)
2015
- if (route) {
2016
- const routeTool = route
2213
+ // Check flow-level tools first (if flow provided)
2214
+ if (flow) {
2215
+ const flowTool = flow
2017
2216
  .getTools()
2018
- .find((tool) => tool.id === toolName || tool.name === toolName);
2019
- if (routeTool)
2020
- return routeTool;
2217
+ .find((tool) => tool.id === toolName || tool.id === toolName);
2218
+ if (flowTool)
2219
+ return flowTool;
2021
2220
  }
2022
2221
  // Fall back to agent-level tools
2023
2222
  const agentTools = this.agent.getTools();
2024
- return agentTools.find((tool) => tool.id === toolName || tool.name === toolName);
2223
+ return agentTools.find((tool) => tool.id === toolName || tool.id === toolName);
2025
2224
  }
2026
2225
  /**
2027
- * Collect all available tools for the given route and step context using ToolManager
2226
+ * Collect all available tools for the given flow and step context using ToolManager
2028
2227
  * Delegates to ToolManager for unified tool resolution and deduplication
2029
2228
  * @private
2030
2229
  */
2031
- collectAvailableTools(route, step) {
2230
+ collectAvailableTools(flow, step) {
2032
2231
  // Use ToolManager for unified tool collection if available
2033
2232
  const toolManager = this.getToolManager();
2034
2233
  if (toolManager) {
2035
- const availableTools = toolManager.getAvailable(undefined, step, route);
2234
+ const availableTools = toolManager.getAvailable(undefined, step, flow);
2036
2235
  return availableTools.map((tool) => ({
2037
2236
  id: tool.id,
2038
- name: tool.name || tool.id,
2237
+ name: tool.id || tool.id,
2039
2238
  description: tool.description,
2040
2239
  parameters: tool.parameters,
2041
2240
  }));
@@ -2047,9 +2246,9 @@ export class ResponseModal {
2047
2246
  this.agent.getTools().forEach((tool) => {
2048
2247
  availableTools.set(tool.id, tool);
2049
2248
  });
2050
- // Add route-level tools (these take precedence)
2051
- if (route) {
2052
- route.getTools().forEach((tool) => {
2249
+ // Add flow-level tools (these take precedence)
2250
+ if (flow) {
2251
+ flow.getTools().forEach((tool) => {
2053
2252
  availableTools.set(tool.id, tool);
2054
2253
  });
2055
2254
  }
@@ -2092,7 +2291,7 @@ export class ResponseModal {
2092
2291
  // Convert to the format expected by AI providers
2093
2292
  return Array.from(availableTools.values()).map((tool) => ({
2094
2293
  id: tool.id,
2095
- name: tool.name || tool.id,
2294
+ name: tool.id || tool.id,
2096
2295
  description: tool.description,
2097
2296
  parameters: tool.parameters,
2098
2297
  }));
@@ -2101,7 +2300,7 @@ export class ResponseModal {
2101
2300
  * Execute a prepare or finalize function/tool
2102
2301
  * @private
2103
2302
  */
2104
- async executePrepareFinalize(prepareOrFinalize, context, data, route, step) {
2303
+ async executePrepareFinalize(prepareOrFinalize, context, data, flow, step) {
2105
2304
  if (!prepareOrFinalize)
2106
2305
  return;
2107
2306
  if (typeof prepareOrFinalize === "function") {
@@ -2115,7 +2314,7 @@ export class ResponseModal {
2115
2314
  // Tool ID - use ToolManager for unified resolution
2116
2315
  const toolManager = this.getToolManager();
2117
2316
  if (toolManager) {
2118
- tool = toolManager.find(prepareOrFinalize, undefined, step, route);
2317
+ tool = toolManager.find(prepareOrFinalize, undefined, step, flow);
2119
2318
  }
2120
2319
  else {
2121
2320
  // Fallback to legacy resolution if ToolManager not available
@@ -2125,9 +2324,9 @@ export class ResponseModal {
2125
2324
  this.agent.getTools().forEach((t) => {
2126
2325
  availableTools.set(t.id, t);
2127
2326
  });
2128
- // Add route-level tools
2129
- if (route) {
2130
- route.getTools().forEach((t) => {
2327
+ // Add flow-level tools
2328
+ if (flow) {
2329
+ flow.getTools().forEach((t) => {
2131
2330
  availableTools.set(t.id, t);
2132
2331
  });
2133
2332
  }
@@ -2179,23 +2378,5 @@ export class ResponseModal {
2179
2378
  }
2180
2379
  }
2181
2380
  }
2182
- /**
2183
- * Merge terms with route-specific taking precedence on conflicts
2184
- * @private
2185
- */
2186
- mergeTerms(agentTerms, routeTerms) {
2187
- const merged = new Map();
2188
- // Add agent terms first
2189
- agentTerms.forEach((term) => {
2190
- const name = typeof term.name === "string" ? term.name : term.name.toString();
2191
- merged.set(name, term);
2192
- });
2193
- // Add route terms (these take precedence)
2194
- routeTerms.forEach((term) => {
2195
- const name = typeof term.name === "string" ? term.name : term.name.toString();
2196
- merged.set(name, term);
2197
- });
2198
- return Array.from(merged.values());
2199
- }
2200
2381
  }
2201
2382
  //# sourceMappingURL=ResponseModal.js.map