@falai/agent 1.2.8 → 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 (499) 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 +1191 -1014
  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 +509 -136
  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/types/agent.d.ts +183 -54
  126. package/dist/cjs/types/agent.d.ts.map +1 -1
  127. package/dist/cjs/types/agent.js +0 -6
  128. package/dist/cjs/types/agent.js.map +1 -1
  129. package/dist/cjs/types/ai.d.ts +3 -3
  130. package/dist/cjs/types/ai.d.ts.map +1 -1
  131. package/dist/cjs/types/errors.d.ts +15 -0
  132. package/dist/cjs/types/errors.d.ts.map +1 -0
  133. package/dist/cjs/types/errors.js +22 -0
  134. package/dist/cjs/types/errors.js.map +1 -0
  135. package/dist/cjs/types/flow.d.ts +513 -0
  136. package/dist/cjs/types/flow.d.ts.map +1 -0
  137. package/dist/cjs/types/{route.js → flow.js} +2 -2
  138. package/dist/cjs/types/flow.js.map +1 -0
  139. package/dist/cjs/types/index.d.ts +7 -6
  140. package/dist/cjs/types/index.d.ts.map +1 -1
  141. package/dist/cjs/types/index.js +6 -2
  142. package/dist/cjs/types/index.js.map +1 -1
  143. package/dist/cjs/types/persistence.d.ts +11 -7
  144. package/dist/cjs/types/persistence.d.ts.map +1 -1
  145. package/dist/cjs/types/routing.d.ts +1 -1
  146. package/dist/cjs/types/routing.d.ts.map +1 -1
  147. package/dist/cjs/types/session.d.ts +24 -23
  148. package/dist/cjs/types/session.d.ts.map +1 -1
  149. package/dist/cjs/types/signals.d.ts +248 -0
  150. package/dist/cjs/types/signals.d.ts.map +1 -0
  151. package/dist/cjs/types/signals.js +11 -0
  152. package/dist/cjs/types/signals.js.map +1 -0
  153. package/dist/cjs/types/template.d.ts +2 -8
  154. package/dist/cjs/types/template.d.ts.map +1 -1
  155. package/dist/cjs/types/tool.d.ts +36 -29
  156. package/dist/cjs/types/tool.d.ts.map +1 -1
  157. package/dist/cjs/types/tool.js +1 -1
  158. package/dist/cjs/types/tool.js.map +1 -1
  159. package/dist/cjs/utils/condition.d.ts +7 -1
  160. package/dist/cjs/utils/condition.d.ts.map +1 -1
  161. package/dist/cjs/utils/condition.js.map +1 -1
  162. package/dist/cjs/utils/id.d.ts +13 -5
  163. package/dist/cjs/utils/id.d.ts.map +1 -1
  164. package/dist/cjs/utils/id.js +24 -10
  165. package/dist/cjs/utils/id.js.map +1 -1
  166. package/dist/cjs/utils/index.d.ts +2 -2
  167. package/dist/cjs/utils/index.d.ts.map +1 -1
  168. package/dist/cjs/utils/index.js +7 -3
  169. package/dist/cjs/utils/index.js.map +1 -1
  170. package/dist/cjs/utils/session.d.ts +44 -5
  171. package/dist/cjs/utils/session.d.ts.map +1 -1
  172. package/dist/cjs/utils/session.js +197 -38
  173. package/dist/cjs/utils/session.js.map +1 -1
  174. package/dist/constants/index.d.ts +0 -9
  175. package/dist/constants/index.d.ts.map +1 -1
  176. package/dist/constants/index.js +3 -9
  177. package/dist/constants/index.js.map +1 -1
  178. package/dist/core/Agent.d.ts +119 -153
  179. package/dist/core/Agent.d.ts.map +1 -1
  180. package/dist/core/Agent.js +472 -325
  181. package/dist/core/Agent.js.map +1 -1
  182. package/dist/core/AutoChainExecutor.d.ts +107 -0
  183. package/dist/core/AutoChainExecutor.d.ts.map +1 -0
  184. package/dist/core/AutoChainExecutor.js +293 -0
  185. package/dist/core/AutoChainExecutor.js.map +1 -0
  186. package/dist/core/BranchEvaluator.d.ts +54 -0
  187. package/dist/core/BranchEvaluator.d.ts.map +1 -0
  188. package/dist/core/BranchEvaluator.js +126 -0
  189. package/dist/core/BranchEvaluator.js.map +1 -0
  190. package/dist/core/DirectiveBus.d.ts +88 -0
  191. package/dist/core/DirectiveBus.d.ts.map +1 -0
  192. package/dist/core/DirectiveBus.js +192 -0
  193. package/dist/core/DirectiveBus.js.map +1 -0
  194. package/dist/core/DirectiveChainTracker.d.ts +49 -0
  195. package/dist/core/DirectiveChainTracker.d.ts.map +1 -0
  196. package/dist/core/DirectiveChainTracker.js +117 -0
  197. package/dist/core/DirectiveChainTracker.js.map +1 -0
  198. package/dist/core/Flow.d.ts +186 -0
  199. package/dist/core/Flow.d.ts.map +1 -0
  200. package/dist/core/Flow.js +546 -0
  201. package/dist/core/Flow.js.map +1 -0
  202. package/dist/core/FlowRouter.d.ts +182 -0
  203. package/dist/core/FlowRouter.d.ts.map +1 -0
  204. package/dist/core/{RoutingEngine.js → FlowRouter.js} +322 -305
  205. package/dist/core/FlowRouter.js.map +1 -0
  206. package/dist/core/PersistenceManager.d.ts +2 -2
  207. package/dist/core/PersistenceManager.d.ts.map +1 -1
  208. package/dist/core/PersistenceManager.js +7 -7
  209. package/dist/core/PersistenceManager.js.map +1 -1
  210. package/dist/core/PromptComposer.d.ts +21 -8
  211. package/dist/core/PromptComposer.d.ts.map +1 -1
  212. package/dist/core/PromptComposer.js +183 -106
  213. package/dist/core/PromptComposer.js.map +1 -1
  214. package/dist/core/PromptSectionCache.d.ts +1 -1
  215. package/dist/core/PromptSectionCache.js +1 -1
  216. package/dist/core/ResponseEngine.d.ts +18 -8
  217. package/dist/core/ResponseEngine.d.ts.map +1 -1
  218. package/dist/core/ResponseEngine.js +38 -36
  219. package/dist/core/ResponseEngine.js.map +1 -1
  220. package/dist/core/ResponseModal.d.ts +73 -56
  221. package/dist/core/ResponseModal.d.ts.map +1 -1
  222. package/dist/core/ResponseModal.js +1193 -1016
  223. package/dist/core/ResponseModal.js.map +1 -1
  224. package/dist/core/ResponsePipeline.d.ts +124 -26
  225. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  226. package/dist/core/ResponsePipeline.js +509 -137
  227. package/dist/core/ResponsePipeline.js.map +1 -1
  228. package/dist/core/SignalEvaluator.d.ts +86 -0
  229. package/dist/core/SignalEvaluator.d.ts.map +1 -0
  230. package/dist/core/SignalEvaluator.js +326 -0
  231. package/dist/core/SignalEvaluator.js.map +1 -0
  232. package/dist/core/SignalProcessor.d.ts +152 -0
  233. package/dist/core/SignalProcessor.d.ts.map +1 -0
  234. package/dist/core/SignalProcessor.js +555 -0
  235. package/dist/core/SignalProcessor.js.map +1 -0
  236. package/dist/core/Step.d.ts +43 -32
  237. package/dist/core/Step.d.ts.map +1 -1
  238. package/dist/core/Step.js +220 -126
  239. package/dist/core/Step.js.map +1 -1
  240. package/dist/core/StreamingToolExecutor.d.ts +2 -2
  241. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  242. package/dist/core/StreamingToolExecutor.js.map +1 -1
  243. package/dist/core/ToolManager.d.ts +44 -13
  244. package/dist/core/ToolManager.d.ts.map +1 -1
  245. package/dist/core/ToolManager.js +174 -91
  246. package/dist/core/ToolManager.js.map +1 -1
  247. package/dist/core/createAgent.d.ts +35 -0
  248. package/dist/core/createAgent.d.ts.map +1 -0
  249. package/dist/core/createAgent.js +36 -0
  250. package/dist/core/createAgent.js.map +1 -0
  251. package/dist/core/flow-namespace.d.ts +49 -0
  252. package/dist/core/flow-namespace.d.ts.map +1 -0
  253. package/dist/core/flow-namespace.js +168 -0
  254. package/dist/core/flow-namespace.js.map +1 -0
  255. package/dist/index.d.ts +11 -14
  256. package/dist/index.d.ts.map +1 -1
  257. package/dist/index.js +9 -12
  258. package/dist/index.js.map +1 -1
  259. package/dist/types/agent.d.ts +183 -54
  260. package/dist/types/agent.d.ts.map +1 -1
  261. package/dist/types/agent.js +0 -6
  262. package/dist/types/agent.js.map +1 -1
  263. package/dist/types/ai.d.ts +3 -3
  264. package/dist/types/ai.d.ts.map +1 -1
  265. package/dist/types/errors.d.ts +15 -0
  266. package/dist/types/errors.d.ts.map +1 -0
  267. package/dist/types/errors.js +18 -0
  268. package/dist/types/errors.js.map +1 -0
  269. package/dist/types/flow.d.ts +513 -0
  270. package/dist/types/flow.d.ts.map +1 -0
  271. package/dist/types/flow.js +5 -0
  272. package/dist/types/flow.js.map +1 -0
  273. package/dist/types/index.d.ts +7 -6
  274. package/dist/types/index.d.ts.map +1 -1
  275. package/dist/types/index.js +4 -1
  276. package/dist/types/index.js.map +1 -1
  277. package/dist/types/persistence.d.ts +11 -7
  278. package/dist/types/persistence.d.ts.map +1 -1
  279. package/dist/types/routing.d.ts +1 -1
  280. package/dist/types/routing.d.ts.map +1 -1
  281. package/dist/types/session.d.ts +24 -23
  282. package/dist/types/session.d.ts.map +1 -1
  283. package/dist/types/signals.d.ts +248 -0
  284. package/dist/types/signals.d.ts.map +1 -0
  285. package/dist/types/signals.js +10 -0
  286. package/dist/types/signals.js.map +1 -0
  287. package/dist/types/template.d.ts +2 -8
  288. package/dist/types/template.d.ts.map +1 -1
  289. package/dist/types/tool.d.ts +36 -29
  290. package/dist/types/tool.d.ts.map +1 -1
  291. package/dist/types/tool.js +1 -1
  292. package/dist/types/tool.js.map +1 -1
  293. package/dist/utils/condition.d.ts +7 -1
  294. package/dist/utils/condition.d.ts.map +1 -1
  295. package/dist/utils/condition.js.map +1 -1
  296. package/dist/utils/id.d.ts +13 -5
  297. package/dist/utils/id.d.ts.map +1 -1
  298. package/dist/utils/id.js +22 -9
  299. package/dist/utils/id.js.map +1 -1
  300. package/dist/utils/index.d.ts +2 -2
  301. package/dist/utils/index.d.ts.map +1 -1
  302. package/dist/utils/index.js +2 -2
  303. package/dist/utils/index.js.map +1 -1
  304. package/dist/utils/session.d.ts +44 -5
  305. package/dist/utils/session.d.ts.map +1 -1
  306. package/dist/utils/session.js +193 -37
  307. package/dist/utils/session.js.map +1 -1
  308. package/docs/README.md +15 -202
  309. package/docs/concepts/architecture.md +281 -0
  310. package/docs/concepts/directives.md +400 -0
  311. package/docs/concepts/pipeline.md +399 -0
  312. package/docs/guides/branching.md +263 -0
  313. package/docs/guides/compaction.md +163 -0
  314. package/docs/guides/conditions.md +167 -0
  315. package/docs/guides/error-handling.md +176 -0
  316. package/docs/guides/flow-control.md +409 -0
  317. package/docs/guides/instructions.md +210 -0
  318. package/docs/guides/persistence.md +182 -0
  319. package/docs/guides/streaming.md +137 -0
  320. package/docs/migration/README.md +15 -0
  321. package/docs/migration/route-to-flow.md +560 -0
  322. package/docs/migration/v1-to-v2.md +909 -0
  323. package/docs/reference/adapters.md +481 -0
  324. package/docs/reference/branches.md +241 -0
  325. package/docs/reference/create-agent.md +186 -0
  326. package/docs/reference/directive.md +243 -0
  327. package/docs/reference/errors.md +122 -0
  328. package/docs/reference/flow.md +238 -0
  329. package/docs/reference/instruction.md +177 -0
  330. package/docs/reference/pre-directive.md +131 -0
  331. package/docs/reference/providers.md +227 -0
  332. package/docs/reference/signals.md +356 -0
  333. package/docs/reference/step.md +339 -0
  334. package/docs/reference/tool.md +269 -0
  335. package/docs/start/01-install.md +81 -0
  336. package/docs/start/02-first-agent.md +196 -0
  337. package/docs/start/03-collect-data.md +222 -0
  338. package/docs/start/04-add-tools.md +276 -0
  339. package/docs/start/05-go-to-production.md +216 -0
  340. package/examples/01-quickstart.ts +20 -0
  341. package/examples/02-data-extraction.ts +90 -0
  342. package/examples/03-tools.ts +136 -0
  343. package/examples/04-instructions.ts +100 -0
  344. package/examples/05-branching.ts +140 -0
  345. package/examples/06-flow-control.ts +103 -0
  346. package/examples/07-streaming.ts +69 -0
  347. package/examples/08-persistence.ts +98 -0
  348. package/examples/09-signals.ts +144 -0
  349. package/examples/tsconfig.json +30 -0
  350. package/package.json +2 -1
  351. package/src/adapters/MemoryAdapter.ts +3 -3
  352. package/src/adapters/MongoAdapter.ts +3 -3
  353. package/src/adapters/OpenSearchAdapter.ts +10 -8
  354. package/src/adapters/PostgreSQLAdapter.ts +26 -10
  355. package/src/adapters/PrismaAdapter.ts +6 -6
  356. package/src/adapters/RedisAdapter.ts +3 -3
  357. package/src/adapters/SQLiteAdapter.ts +31 -12
  358. package/src/constants/index.ts +2 -10
  359. package/src/core/Agent.ts +585 -374
  360. package/src/core/AutoChainExecutor.ts +440 -0
  361. package/src/core/BranchEvaluator.ts +167 -0
  362. package/src/core/DirectiveBus.ts +248 -0
  363. package/src/core/DirectiveChainTracker.ts +144 -0
  364. package/src/core/Flow.ts +666 -0
  365. package/src/core/{RoutingEngine.ts → FlowRouter.ts} +385 -365
  366. package/src/core/PersistenceManager.ts +8 -8
  367. package/src/core/PromptComposer.ts +209 -140
  368. package/src/core/PromptSectionCache.ts +1 -1
  369. package/src/core/ResponseEngine.ts +61 -46
  370. package/src/core/ResponseModal.ts +1453 -1240
  371. package/src/core/ResponsePipeline.ts +655 -175
  372. package/src/core/SignalEvaluator.ts +420 -0
  373. package/src/core/SignalProcessor.ts +723 -0
  374. package/src/core/Step.ts +279 -176
  375. package/src/core/StreamingToolExecutor.ts +4 -4
  376. package/src/core/ToolManager.ts +200 -97
  377. package/src/core/createAgent.ts +40 -0
  378. package/src/core/flow-namespace.ts +219 -0
  379. package/src/index.ts +42 -36
  380. package/src/types/agent.ts +182 -53
  381. package/src/types/ai.ts +3 -3
  382. package/src/types/errors.ts +18 -0
  383. package/src/types/flow.ts +590 -0
  384. package/src/types/index.ts +43 -16
  385. package/src/types/persistence.ts +12 -8
  386. package/src/types/routing.ts +1 -1
  387. package/src/types/session.ts +26 -23
  388. package/src/types/signals.ts +321 -0
  389. package/src/types/template.ts +3 -11
  390. package/src/types/tool.ts +50 -42
  391. package/src/utils/condition.ts +13 -4
  392. package/src/utils/id.ts +27 -9
  393. package/src/utils/index.ts +6 -2
  394. package/src/utils/session.ts +238 -42
  395. package/dist/cjs/core/BatchExecutor.d.ts +0 -359
  396. package/dist/cjs/core/BatchExecutor.d.ts.map +0 -1
  397. package/dist/cjs/core/BatchExecutor.js +0 -861
  398. package/dist/cjs/core/BatchExecutor.js.map +0 -1
  399. package/dist/cjs/core/BatchPromptBuilder.d.ts +0 -89
  400. package/dist/cjs/core/BatchPromptBuilder.d.ts.map +0 -1
  401. package/dist/cjs/core/BatchPromptBuilder.js +0 -223
  402. package/dist/cjs/core/BatchPromptBuilder.js.map +0 -1
  403. package/dist/cjs/core/Route.d.ts +0 -180
  404. package/dist/cjs/core/Route.d.ts.map +0 -1
  405. package/dist/cjs/core/Route.js +0 -542
  406. package/dist/cjs/core/Route.js.map +0 -1
  407. package/dist/cjs/core/RoutingEngine.d.ts +0 -185
  408. package/dist/cjs/core/RoutingEngine.d.ts.map +0 -1
  409. package/dist/cjs/core/RoutingEngine.js.map +0 -1
  410. package/dist/cjs/types/route.d.ts +0 -336
  411. package/dist/cjs/types/route.d.ts.map +0 -1
  412. package/dist/cjs/types/route.js.map +0 -1
  413. package/dist/core/BatchExecutor.d.ts +0 -359
  414. package/dist/core/BatchExecutor.d.ts.map +0 -1
  415. package/dist/core/BatchExecutor.js +0 -856
  416. package/dist/core/BatchExecutor.js.map +0 -1
  417. package/dist/core/BatchPromptBuilder.d.ts +0 -89
  418. package/dist/core/BatchPromptBuilder.d.ts.map +0 -1
  419. package/dist/core/BatchPromptBuilder.js +0 -219
  420. package/dist/core/BatchPromptBuilder.js.map +0 -1
  421. package/dist/core/Route.d.ts +0 -180
  422. package/dist/core/Route.d.ts.map +0 -1
  423. package/dist/core/Route.js +0 -538
  424. package/dist/core/Route.js.map +0 -1
  425. package/dist/core/RoutingEngine.d.ts +0 -185
  426. package/dist/core/RoutingEngine.d.ts.map +0 -1
  427. package/dist/core/RoutingEngine.js.map +0 -1
  428. package/dist/types/route.d.ts +0 -336
  429. package/dist/types/route.d.ts.map +0 -1
  430. package/dist/types/route.js +0 -5
  431. package/dist/types/route.js.map +0 -1
  432. package/docs/CONTRIBUTING.md +0 -521
  433. package/docs/api/README.md +0 -3299
  434. package/docs/api/overview.md +0 -1410
  435. package/docs/architecture/data-extraction-flow.md +0 -360
  436. package/docs/architecture/multi-step-execution.md +0 -277
  437. package/docs/core/agent/README.md +0 -938
  438. package/docs/core/agent/context-management.md +0 -796
  439. package/docs/core/agent/rules-and-prohibitions.md +0 -113
  440. package/docs/core/agent/session-management.md +0 -693
  441. package/docs/core/ai-integration/prompt-composition.md +0 -355
  442. package/docs/core/ai-integration/providers.md +0 -515
  443. package/docs/core/ai-integration/response-processing.md +0 -433
  444. package/docs/core/conversation-flows/data-collection.md +0 -772
  445. package/docs/core/conversation-flows/route-dsl.md +0 -509
  446. package/docs/core/conversation-flows/routes.md +0 -249
  447. package/docs/core/conversation-flows/step-transitions.md +0 -731
  448. package/docs/core/conversation-flows/steps.md +0 -268
  449. package/docs/core/error-handling.md +0 -830
  450. package/docs/core/persistence/adapters.md +0 -255
  451. package/docs/core/persistence/session-storage.md +0 -656
  452. package/docs/core/routing/intelligent-routing.md +0 -470
  453. package/docs/core/tools/enhanced-tool.md +0 -186
  454. package/docs/core/tools/streaming-execution.md +0 -161
  455. package/docs/core/tools/tool-definition.md +0 -970
  456. package/docs/core/tools/tool-scoping.md +0 -819
  457. package/docs/guides/advanced-patterns/publishing.md +0 -186
  458. package/docs/guides/context-compaction.md +0 -96
  459. package/docs/guides/error-handling-patterns.md +0 -578
  460. package/docs/guides/getting-started/README.md +0 -795
  461. package/docs/guides/migration/README.md +0 -101
  462. package/docs/guides/migration/flexible-routing-conditions.md +0 -375
  463. package/docs/guides/migration/multi-step-execution.md +0 -393
  464. package/docs/guides/migration/response-modal-refactor.md +0 -518
  465. package/docs/guides/prompt-optimization.md +0 -164
  466. package/examples/advanced-patterns/context-compaction.ts +0 -223
  467. package/examples/advanced-patterns/knowledge-based-agent.ts +0 -735
  468. package/examples/advanced-patterns/persistent-onboarding.ts +0 -728
  469. package/examples/advanced-patterns/route-lifecycle-hooks.ts +0 -556
  470. package/examples/advanced-patterns/streaming-responses.ts +0 -656
  471. package/examples/ai-providers/anthropic-integration.ts +0 -388
  472. package/examples/ai-providers/openai-integration.ts +0 -228
  473. package/examples/condition-patterns/function-only-conditions.ts +0 -365
  474. package/examples/condition-patterns/mixed-array-conditions.ts +0 -477
  475. package/examples/condition-patterns/route-skipif-patterns.ts +0 -468
  476. package/examples/condition-patterns/step-skipif-patterns.ts +0 -0
  477. package/examples/condition-patterns/string-only-conditions.ts +0 -296
  478. package/examples/conversation-flows/completion-transitions.ts +0 -318
  479. package/examples/core-concepts/basic-agent.ts +0 -503
  480. package/examples/core-concepts/modern-streaming-api.ts +0 -309
  481. package/examples/core-concepts/schema-driven-extraction.ts +0 -332
  482. package/examples/core-concepts/session-management.ts +0 -494
  483. package/examples/integrations/database-integration.ts +0 -631
  484. package/examples/integrations/healthcare-integration.ts +0 -595
  485. package/examples/integrations/search-integration.ts +0 -530
  486. package/examples/integrations/server-session-management.ts +0 -307
  487. package/examples/persistence/custom-adapter.ts +0 -526
  488. package/examples/persistence/database-persistence.ts +0 -583
  489. package/examples/persistence/memory-sessions.ts +0 -495
  490. package/examples/persistence/prisma-schema.example.prisma +0 -74
  491. package/examples/persistence/redis-persistence.ts +0 -488
  492. package/examples/tools/basic-tools.ts +0 -765
  493. package/examples/tools/data-enrichment-tools.ts +0 -593
  494. package/examples/tools/enhanced-tool-metadata.ts +0 -268
  495. package/examples/tools/streaming-tool-execution.ts +0 -283
  496. package/src/core/BatchExecutor.ts +0 -1187
  497. package/src/core/BatchPromptBuilder.ts +0 -299
  498. package/src/core/Route.ts +0 -678
  499. package/src/types/route.ts +0 -392
@@ -13,22 +13,23 @@ import type {
13
13
  Tool,
14
14
  Event,
15
15
  AgentStructuredResponse,
16
- Term,
17
16
  StoppedReason,
18
17
  ToolCallRequest,
18
+ ScopedInstructions,
19
+ AppliedInstruction,
20
+ PrepareResult,
21
+ PreDirective,
19
22
  } from "../types";
23
+ import type { SignalFiring } from "../types/signals";
20
24
  import type { Agent } from "./Agent";
21
- import type { Route } from "./Route";
25
+ import type { Flow } from "./Flow";
22
26
  import { Step } from "./Step";
23
27
  import { ResponseEngine } from "./ResponseEngine";
24
- import { ResponsePipeline } from "./ResponsePipeline";
25
- import { BatchExecutor, type HookFunction } from "./BatchExecutor";
26
- import { BatchPromptBuilder } from "./BatchPromptBuilder";
27
- import { cloneDeep, mergeCollected, enterStep, getLastMessageFromHistory, render, logger, historyToEvents, eventsToHistory, serializeToolResult } from "../utils";
28
+ import { ResponsePipeline, hasDirectivePositionField } from "./ResponsePipeline";
29
+ import { AutoChainExecutor, type AutoChainResult } from "./AutoChainExecutor";
30
+ import { cloneDeep, mergeCollected, enterStep, enterFlow, getLastMessageFromHistory, logger, historyToEvents, eventsToHistory, serializeToolResult, completeCurrentFlow, render } from "../utils";
28
31
  import { createTemplateContext } from "../utils/template";
29
32
  import type { ToolManager } from "./ToolManager";
30
- import { END_ROUTE_ID } from "../constants";
31
- import type { StepOptions } from "../types/route";
32
33
 
33
34
  /**
34
35
  * Configuration options for ResponseModal
@@ -109,7 +110,8 @@ export class ResponseGenerationError extends Error {
109
110
  ): ResponseGenerationError {
110
111
  const message = error instanceof Error ? error.message : String(error);
111
112
  return new ResponseGenerationError(
112
- `Response generation failed in ${phase}: ${message}`,
113
+ `[ResponseGenerationError] Response generation failed in ${phase}: ${message}. ` +
114
+ `Check provider configuration and the ${phase} phase handler.`,
113
115
  { originalError: error, params, phase, context }
114
116
  );
115
117
  }
@@ -129,18 +131,20 @@ interface ResponseContext<TContext = unknown, TData = unknown> {
129
131
  effectiveContext: TContext;
130
132
  session: SessionState<TData>;
131
133
  history: HistoryItem[]; // Keep as HistoryItem[] for external API compatibility
132
- selectedRoute?: Route<TContext, TData>;
134
+ selectedFlow?: Flow<TContext, TData>;
133
135
  selectedStep?: Step<TContext, TData>;
134
136
  responseDirectives?: string[];
135
- isRouteComplete: boolean;
136
- /** Batch of steps to execute (for multi-step execution) */
137
- batchSteps?: StepOptions<TContext, TData>[];
138
- /** Reason why batch determination stopped */
139
- batchStoppedReason?: StoppedReason;
140
- /** Step that caused batch to stop (if applicable) */
141
- batchStoppedAtStep?: StepOptions<TContext, TData>;
137
+ isFlowComplete: boolean;
142
138
  /** AbortSignal for cancellation propagation */
143
139
  signal?: AbortSignal;
140
+ /** Signal firings accumulated across both phases (pre + post) for the response surface. */
141
+ signalFirings?: SignalFiring<TContext, TData>[];
142
+ /** Pre-phase merged directive from signals (non-position fields like appendPrompt, injectTools). */
143
+ signalPreDirective?: PreDirective<TContext, TData>;
144
+ /** Whether the pre-signal phase emitted a halt directive. */
145
+ signalHalted?: boolean;
146
+ /** Reply from a halt directive. */
147
+ signalHaltReply?: string;
144
148
  }
145
149
 
146
150
  /**
@@ -150,8 +154,6 @@ interface ResponseContext<TContext = unknown, TData = unknown> {
150
154
  export class ResponseModal<TContext = unknown, TData = unknown> {
151
155
  private readonly responseEngine: ResponseEngine<TContext, TData>;
152
156
  private readonly responsePipeline: ResponsePipeline<TContext, TData>;
153
- private readonly batchExecutor: BatchExecutor<TContext, TData>;
154
- private readonly batchPromptBuilder: BatchPromptBuilder<TContext, TData>;
155
157
 
156
158
  constructor(
157
159
  private readonly agent: Agent<TContext, TData>,
@@ -163,20 +165,15 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
163
165
  // Initialize response pipeline with agent dependencies
164
166
  this.responsePipeline = new ResponsePipeline<TContext, TData>(
165
167
  this.agent.getAgentOptions(),
166
- () => this.agent.getRoutes(), // Pass a function to get routes dynamically
168
+ () => this.agent.getFlows(), // Pass a function to get flows dynamically
167
169
  this.agent.getTools(),
168
- this.agent.getRoutingEngine(),
170
+ this.agent.getFlowRouter(),
169
171
  this.agent.updateContext.bind(this.agent),
170
172
  this.agent.getUpdateDataMethod(),
171
173
  this.agent.updateCollectedData.bind(this.agent),
172
- this.getToolManager()
174
+ this.getToolManager(),
175
+ this.agent.signalProcessor
173
176
  );
174
-
175
- // Initialize batch executor for multi-step execution
176
- this.batchExecutor = new BatchExecutor<TContext, TData>();
177
-
178
- // Initialize batch prompt builder for combined prompts
179
- this.batchPromptBuilder = new BatchPromptBuilder<TContext, TData>(this.agent.promptSectionCache);
180
177
  }
181
178
 
182
179
  /**
@@ -196,7 +193,8 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
196
193
 
197
194
  } catch (error) {
198
195
  throw new ResponseGenerationError(
199
- `Failed to generate response: ${error instanceof Error ? error.message : String(error)}`,
196
+ `[ResponseGenerationError] Response generation failed: ${error instanceof Error ? error.message : String(error)}. ` +
197
+ `Check provider configuration and network connectivity.`,
200
198
  { originalError: error, params, phase: 'response_generation' }
201
199
  );
202
200
  }
@@ -379,6 +377,21 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
379
377
  return undefined;
380
378
  }
381
379
 
380
+ /**
381
+ * Collect scoped instructions from agent, flow, and step into a ScopedInstructions value.
382
+ * @private
383
+ */
384
+ private collectScopedInstructions(
385
+ flow?: Flow<TContext, TData>,
386
+ step?: Step<TContext, TData>,
387
+ ): ScopedInstructions<TContext, TData> {
388
+ return {
389
+ global: this.agent.instructions,
390
+ flow: flow ? { flowTitle: flow.title, items: flow.instructions } : undefined,
391
+ step: step ? { stepId: step.id, items: step.getInstructions() } : undefined,
392
+ };
393
+ }
394
+
382
395
  // UNIFIED RESPONSE LOGIC - Consolidates common logic between streaming and non-streaming
383
396
  // ============================================================================
384
397
 
@@ -393,7 +406,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
393
406
 
394
407
  // Validate input parameters
395
408
  if (!simpleHistory) {
396
- throw new ResponseGenerationError('History is required for response generation', { params, phase: 'validation' });
409
+ throw new ResponseGenerationError(
410
+ '[ResponseGenerationError] Missing history: history is required for response generation. ' +
411
+ 'Pass a valid history array to the respond/stream method.',
412
+ { params, phase: 'validation' }
413
+ );
397
414
  }
398
415
 
399
416
  // Convert HistoryItem[] to Event[] for internal processing
@@ -410,7 +427,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
410
427
  try {
411
428
  // Set current context and session in pipeline for consistency
412
429
  this.responsePipeline.setContext(await this.agent.getContext());
413
- this.responsePipeline.setCurrentSession(this.agent.getCurrentSession());
430
+ this.responsePipeline.setCurrentSession(this.agent.currentSession);
414
431
 
415
432
  responseContext = await this.responsePipeline.prepareResponseContext({
416
433
  contextOverride,
@@ -451,17 +468,18 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
451
468
  throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
452
469
  }
453
470
 
454
- // PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use
455
- // Also performs pre-extraction and batch determination
471
+ // PHASE 2: ROUTING + STEP SELECTION - Determine which flow and step to use
472
+ // Performs pre-extraction and step selection
456
473
  let routingResult: {
457
- selectedRoute?: Route<TContext, TData>;
474
+ selectedFlow?: Flow<TContext, TData>;
458
475
  selectedStep?: Step<TContext, TData>;
459
476
  responseDirectives?: string[];
460
477
  session: SessionState<TData>;
461
- isRouteComplete: boolean;
462
- batchSteps?: StepOptions<TContext, TData>[];
463
- batchStoppedReason?: StoppedReason;
464
- batchStoppedAtStep?: StepOptions<TContext, TData>;
478
+ isFlowComplete: boolean;
479
+ signalFirings?: SignalFiring<TContext, TData>[];
480
+ signalPreDirective?: PreDirective<TContext, TData>;
481
+ signalHalted?: boolean;
482
+ signalHaltReply?: string;
465
483
  };
466
484
  try {
467
485
  routingResult = await this.handleUnifiedRoutingAndStepSelection({
@@ -478,14 +496,15 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
478
496
  effectiveContext,
479
497
  session: routingResult.session,
480
498
  history,
481
- selectedRoute: routingResult.selectedRoute,
499
+ selectedFlow: routingResult.selectedFlow,
482
500
  selectedStep: routingResult.selectedStep,
483
501
  responseDirectives: routingResult.responseDirectives,
484
- isRouteComplete: routingResult.isRouteComplete,
485
- batchSteps: routingResult.batchSteps,
486
- batchStoppedReason: routingResult.batchStoppedReason,
487
- batchStoppedAtStep: routingResult.batchStoppedAtStep,
502
+ isFlowComplete: routingResult.isFlowComplete,
488
503
  signal,
504
+ signalFirings: routingResult.signalFirings,
505
+ signalPreDirective: routingResult.signalPreDirective,
506
+ signalHalted: routingResult.signalHalted,
507
+ signalHaltReply: routingResult.signalHaltReply,
489
508
  };
490
509
  } catch (error) {
491
510
  // Re-throw ResponseGenerationError as-is, wrap others
@@ -506,45 +525,183 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
506
525
  context: TContext;
507
526
  signal?: AbortSignal;
508
527
  }): Promise<{
509
- selectedRoute?: Route<TContext, TData>;
528
+ selectedFlow?: Flow<TContext, TData>;
510
529
  selectedStep?: Step<TContext, TData>;
511
530
  responseDirectives?: string[];
512
531
  session: SessionState<TData>;
513
- isRouteComplete: boolean;
514
- /** Batch of steps to execute (for multi-step execution) */
515
- batchSteps?: StepOptions<TContext, TData>[];
516
- /** Reason why batch determination stopped */
517
- batchStoppedReason?: StoppedReason;
518
- /** Step that caused batch to stop (if applicable) */
519
- batchStoppedAtStep?: StepOptions<TContext, TData>;
532
+ isFlowComplete: boolean;
533
+ /** Signal firings from the pre-phase (threaded through for response surface). */
534
+ signalFirings?: SignalFiring<TContext, TData>[];
535
+ /** Non-position signal directive for pre-LLM augmentation (appendPrompt, injectTools, etc). */
536
+ signalPreDirective?: PreDirective<TContext, TData>;
537
+ /** Pre-signal phase halted the turn. */
538
+ signalHalted?: boolean;
539
+ /** Reply text from the halt directive. */
540
+ signalHaltReply?: string;
520
541
  }> {
521
542
  try {
522
- // Use the ResponsePipeline for optimized routing and step selection
523
- // This avoids duplicate logic and leverages existing optimizations
524
- // ResponsePipeline expects Event[] for history
543
+ // Create a fresh chain tracker for this turn (Requirement 22.1)
544
+ this.responsePipeline.createChainTracker();
545
+
546
+ // ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
547
+ // When the current step has collect fields AND pre-extraction populates at least
548
+ // one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
549
+ const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
550
+ if (routingSkipResult) {
551
+ // Even when routing is skipped, run pre-signal phase if processor is present
552
+ if (this.agent.signalProcessor) {
553
+ const signalResult = await this.responsePipeline.runPreSignalPhase(
554
+ params.session, params.context, params.history,
555
+ );
556
+ // If signal halts, override the routing skip result
557
+ if (signalResult.mergedDirective?.halt) {
558
+ return {
559
+ ...routingSkipResult,
560
+ session: signalResult.updatedSession,
561
+ signalFirings: signalResult.firings,
562
+ signalHalted: true,
563
+ signalHaltReply: signalResult.mergedDirective.reply,
564
+ };
565
+ }
566
+ // If signal has position fields, override routing skip result
567
+ if (hasDirectivePositionField(signalResult.mergedDirective)) {
568
+ return this.applySignalPositionDirective(
569
+ signalResult, params,
570
+ );
571
+ }
572
+ // Non-position directive: propagate for pre-LLM augmentation
573
+ return {
574
+ ...routingSkipResult,
575
+ session: signalResult.updatedSession,
576
+ signalFirings: signalResult.firings,
577
+ signalPreDirective: signalResult.mergedDirective || undefined,
578
+ };
579
+ }
580
+ return routingSkipResult;
581
+ }
582
+
583
+ // ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
584
+ // When signalProcessor is present, run pre-signals in parallel with routing.
585
+ // When absent, call the router directly (zero overhead, preserve current behavior).
586
+ if (this.agent.signalProcessor) {
587
+ // Run pre-signal phase in parallel with routing (Requirement 8.1)
588
+ const [signalResult, routingResult] = await Promise.all([
589
+ this.responsePipeline.runPreSignalPhase(
590
+ params.session, params.context, params.history,
591
+ ),
592
+ this.responsePipeline.handleRoutingAndStepSelection({
593
+ session: params.session,
594
+ history: params.history,
595
+ context: params.context,
596
+ signal: params.signal,
597
+ }),
598
+ ]);
599
+
600
+ // ── Requirement 8.2: halt → discard routing, skip LLM ────────────
601
+ if (signalResult.mergedDirective?.halt) {
602
+ return {
603
+ selectedFlow: undefined,
604
+ selectedStep: undefined,
605
+ session: signalResult.updatedSession,
606
+ isFlowComplete: false,
607
+ signalFirings: signalResult.firings,
608
+ signalHalted: true,
609
+ signalHaltReply: signalResult.mergedDirective.reply,
610
+ };
611
+ }
612
+
613
+ // ── Requirement 8.3: position directive → discard routing, apply signal position ──
614
+ if (hasDirectivePositionField(signalResult.mergedDirective)) {
615
+ return this.applySignalPositionDirective(
616
+ signalResult, params,
617
+ );
618
+ }
619
+
620
+ // ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
621
+ // ── Requirement 8.5: no directive → use routing as-is ─────────────
622
+ let updatedSession = signalResult.updatedSession;
623
+
624
+ // Apply data/context updates from signal to the routed session
625
+ if (signalResult.mergedDirective?.dataUpdate) {
626
+ updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
627
+ }
628
+
629
+ // Use routing result for flow/step, but carry signal session state
630
+ // Merge routing session changes on top of signal session
631
+ const routingSession = routingResult.session;
632
+ updatedSession = {
633
+ ...updatedSession,
634
+ currentFlow: routingSession.currentFlow,
635
+ currentStep: routingSession.currentStep,
636
+ flowHistory: routingSession.flowHistory,
637
+ pendingDirective: routingSession.pendingDirective,
638
+ };
639
+
640
+ const isFlowComplete = routingResult.isFlowComplete;
641
+
642
+ // PRE-EXTRACTION: same logic as below — extract data from user message
643
+ if (routingResult.selectedFlow && !isFlowComplete) {
644
+ if (this.shouldPreExtractData(routingResult.selectedFlow)) {
645
+ logger.debug(
646
+ `[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
647
+ );
648
+ const extractedData = await this.preExtractFlowData({
649
+ route: routingResult.selectedFlow,
650
+ history: params.history,
651
+ context: params.context,
652
+ session: updatedSession,
653
+ signal: params.signal,
654
+ });
655
+ if (extractedData && Object.keys(extractedData).length > 0) {
656
+ logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
657
+ updatedSession = mergeCollected(updatedSession, extractedData);
658
+ await this.agent.updateCollectedData(extractedData);
659
+ }
660
+ }
661
+ }
662
+
663
+ // Determine next step
664
+ const stepResult = await this.responsePipeline.determineNextStep({
665
+ selectedFlow: routingResult.selectedFlow,
666
+ selectedStep: routingResult.selectedStep,
667
+ session: updatedSession,
668
+ isFlowComplete,
669
+ });
670
+
671
+ return {
672
+ selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
673
+ selectedStep: stepResult.nextStep,
674
+ responseDirectives: routingResult.responseDirectives,
675
+ session: stepResult.session,
676
+ isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
677
+ signalFirings: signalResult.firings,
678
+ signalPreDirective: signalResult.mergedDirective || undefined,
679
+ };
680
+ }
681
+
682
+ // ── No signal processor: existing behavior (zero overhead) ────────────
525
683
  const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
526
684
  session: params.session,
527
- history: params.history, // Already Event[]
685
+ history: params.history,
528
686
  context: params.context,
529
687
  signal: params.signal,
530
688
  });
531
689
 
532
690
  let updatedSession = routingResult.session;
533
- const isRouteComplete = routingResult.isRouteComplete;
691
+ const isFlowComplete = routingResult.isFlowComplete;
534
692
 
535
- // PRE-EXTRACTION: If entering a route that collects data, extract data from user message first
693
+ // PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
536
694
  // This allows us to skip steps whose data is already provided
537
- // Requirement 3.1: Perform Pre_Extraction before determining the Batch
538
- if (routingResult.selectedRoute && !isRouteComplete) {
539
- // Always pre-extract when route collects data (not just on new route entry)
540
- // This ensures batch determination has the most up-to-date data
541
- if (this.shouldPreExtractData(routingResult.selectedRoute)) {
695
+ if (routingResult.selectedFlow && !isFlowComplete) {
696
+ // Always pre-extract when flow collects data (not just on new flow entry)
697
+ // This ensures step selection has the most up-to-date data
698
+ if (this.shouldPreExtractData(routingResult.selectedFlow)) {
542
699
  logger.debug(
543
- `[ResponseModal] Pre-extracting data for route: ${routingResult.selectedRoute.title}`
700
+ `[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
544
701
  );
545
702
 
546
- const extractedData = await this.preExtractRouteData({
547
- route: routingResult.selectedRoute,
703
+ const extractedData = await this.preExtractFlowData({
704
+ route: routingResult.selectedFlow,
548
705
  history: params.history,
549
706
  context: params.context,
550
707
  session: updatedSession,
@@ -556,7 +713,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
556
713
  `[ResponseModal] Pre-extracted data:`,
557
714
  extractedData
558
715
  );
559
- // Requirement 3.3: Merge pre-extracted data into session before batch determination
716
+ // Merge pre-extracted data into session before step selection
560
717
  updatedSession = mergeCollected(updatedSession, extractedData);
561
718
  // Also update agent's collected data
562
719
  await this.agent.updateCollectedData(extractedData);
@@ -564,51 +721,21 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
564
721
  }
565
722
  }
566
723
 
567
- // BATCH DETERMINATION: Use BatchExecutor to determine which steps can execute together
568
- // Requirement 3.4: Pre-extraction results affect batch determination
569
- let batchSteps: StepOptions<TContext, TData>[] | undefined;
570
- let batchStoppedReason: StoppedReason | undefined;
571
- let batchStoppedAtStep: StepOptions<TContext, TData> | undefined;
572
-
573
- if (routingResult.selectedRoute && !isRouteComplete) {
574
- // Determine current step position for batch determination
575
- const currentStep = routingResult.selectedStep ||
576
- (updatedSession.currentStep ? routingResult.selectedRoute.getStep(updatedSession.currentStep.id) : undefined);
577
-
578
- logger.debug(`[ResponseModal] Determining batch starting from step: ${currentStep?.id || 'initial'}`);
579
-
580
- const batchResult = await this.batchExecutor.determineBatch({
581
- route: routingResult.selectedRoute,
582
- currentStep,
583
- sessionData: updatedSession.data || {},
584
- context: params.context,
585
- maxSteps: this.agent.getAgentOptions().maxStepsPerBatch,
586
- });
587
-
588
- batchSteps = batchResult.steps;
589
- batchStoppedReason = batchResult.stoppedReason;
590
- batchStoppedAtStep = batchResult.stoppedAtStep;
591
-
592
- logger.debug(`[ResponseModal] Batch determined: ${batchSteps.length} steps, stopped reason: ${batchStoppedReason}`);
593
- }
594
-
595
724
  // Determine next step using pipeline method for consistency
596
725
  const stepResult = await this.responsePipeline.determineNextStep({
597
- selectedRoute: routingResult.selectedRoute,
726
+ selectedFlow: routingResult.selectedFlow,
598
727
  selectedStep: routingResult.selectedStep,
599
728
  session: updatedSession, // Use updated session with pre-extracted data
600
- isRouteComplete, // Use updated completion status
729
+ isFlowComplete, // Use updated completion status
601
730
  });
602
731
 
603
732
  return {
604
- selectedRoute: routingResult.selectedRoute,
733
+ selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
605
734
  selectedStep: stepResult.nextStep, // Use the determined next step
606
735
  responseDirectives: routingResult.responseDirectives,
607
736
  session: stepResult.session,
608
- isRouteComplete, // Use updated completion status
609
- batchSteps,
610
- batchStoppedReason,
611
- batchStoppedAtStep,
737
+ // If a branch changed the flow, the original isFlowComplete no longer applies
738
+ isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
612
739
  };
613
740
  } catch (error) {
614
741
  throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
@@ -616,20 +743,255 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
616
743
  }
617
744
 
618
745
  /**
619
- * Check if a route should pre-extract data before determining the initial step
746
+ * Apply a signal's position directive (goTo, goToStep, complete, abort, reset).
747
+ * Discards routing result and uses the signal's position decision.
748
+ * @private
749
+ * @requirements 8.3
750
+ */
751
+ private applySignalPositionDirective(
752
+ signalResult: {
753
+ firings: SignalFiring<TContext, TData>[];
754
+ updatedSession: SessionState<TData>;
755
+ mergedDirective: PreDirective<TContext, TData> | undefined;
756
+ },
757
+ _params: { session: SessionState<TData>; history: Event[]; context: TContext },
758
+ ): {
759
+ selectedFlow?: Flow<TContext, TData>;
760
+ selectedStep?: Step<TContext, TData>;
761
+ responseDirectives?: string[];
762
+ session: SessionState<TData>;
763
+ isFlowComplete: boolean;
764
+ signalFirings?: SignalFiring<TContext, TData>[];
765
+ signalPreDirective?: PreDirective<TContext, TData>;
766
+ signalHalted?: boolean;
767
+ signalHaltReply?: string;
768
+ } {
769
+ const directive = signalResult.mergedDirective!;
770
+ let session = signalResult.updatedSession;
771
+ const flows = this.agent.getFlows();
772
+ let selectedFlow: Flow<TContext, TData> | undefined;
773
+ let selectedStep: Step<TContext, TData> | undefined;
774
+ let isFlowComplete = false;
775
+
776
+ // Apply data updates if present alongside position
777
+ if (directive.dataUpdate) {
778
+ session = mergeCollected(session, directive.dataUpdate);
779
+ }
780
+
781
+ if (directive.goTo) {
782
+ const flowTarget = typeof directive.goTo === 'string'
783
+ ? directive.goTo
784
+ : directive.goTo.flow ?? directive.goTo.step;
785
+
786
+ if (flowTarget) {
787
+ const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
788
+ if (targetFlow) {
789
+ session = enterFlow(session, targetFlow.id, targetFlow.title);
790
+ selectedFlow = targetFlow;
791
+
792
+ if (typeof directive.goTo === 'object' && directive.goTo.step) {
793
+ const targetStep = targetFlow.getStep(directive.goTo.step);
794
+ if (targetStep) {
795
+ session = enterStep(session, targetStep.id, targetStep.description);
796
+ selectedStep = targetStep;
797
+ }
798
+ }
799
+ } else {
800
+ logger.warn(`[Signals] Pre-phase goTo target not found: "${flowTarget}". Falling back to no flow.`);
801
+ }
802
+ }
803
+ } else if (directive.goToStep) {
804
+ const stepTarget = typeof directive.goToStep === 'string'
805
+ ? directive.goToStep
806
+ : directive.goToStep.step;
807
+ const flowTarget = typeof directive.goToStep === 'object'
808
+ ? directive.goToStep.flow
809
+ : undefined;
810
+
811
+ if (flowTarget) {
812
+ const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
813
+ if (targetFlow) {
814
+ session = enterFlow(session, targetFlow.id, targetFlow.title);
815
+ selectedFlow = targetFlow;
816
+ const targetStep = targetFlow.getStep(stepTarget);
817
+ if (targetStep) {
818
+ session = enterStep(session, targetStep.id, targetStep.description);
819
+ selectedStep = targetStep;
820
+ }
821
+ }
822
+ } else if (session.currentFlow) {
823
+ const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
824
+ if (currentFlow) {
825
+ selectedFlow = currentFlow;
826
+ const targetStep = currentFlow.getStep(stepTarget);
827
+ if (targetStep) {
828
+ session = enterStep(session, targetStep.id, targetStep.description);
829
+ selectedStep = targetStep;
830
+ }
831
+ }
832
+ }
833
+ } else if (directive.complete) {
834
+ isFlowComplete = true;
835
+ } else if (directive.abort) {
836
+ // Abort — no flow, session cleared or marked
837
+ isFlowComplete = true;
838
+ } else if (directive.reset) {
839
+ if (session.currentFlow) {
840
+ const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
841
+ if (currentFlow) {
842
+ selectedFlow = currentFlow;
843
+ const resetStep = typeof directive.reset === 'object' && directive.reset.step
844
+ ? directive.reset.step
845
+ : undefined;
846
+ if (resetStep) {
847
+ const targetStep = currentFlow.getStep(resetStep);
848
+ if (targetStep) {
849
+ session = enterStep(session, targetStep.id, targetStep.description);
850
+ selectedStep = targetStep;
851
+ }
852
+ } else {
853
+ const initialStep = currentFlow.initialStep;
854
+ session = enterStep(session, initialStep.id, initialStep.description);
855
+ selectedStep = initialStep;
856
+ }
857
+ }
858
+ }
859
+ }
860
+
861
+ return {
862
+ selectedFlow,
863
+ selectedStep,
864
+ session,
865
+ isFlowComplete,
866
+ signalFirings: signalResult.firings,
867
+ signalPreDirective: signalResult.mergedDirective || undefined,
868
+ };
869
+ }
870
+
871
+ /**
872
+ * Routing skip optimization (Requirements 20.1, 20.2, 20.3):
873
+ * When the current step declares `collect` fields AND pre-extraction populates
874
+ * at least one of those fields from the user's message, skip routing for this turn.
875
+ *
876
+ * Returns the routing result if the skip applies, or undefined to fall through
877
+ * to normal routing.
878
+ * @private
879
+ */
880
+ private async attemptRoutingSkipForCollect(params: {
881
+ session: SessionState<TData>;
882
+ history: Event[];
883
+ context: TContext;
884
+ signal?: AbortSignal;
885
+ }): Promise<{
886
+ selectedFlow?: Flow<TContext, TData>;
887
+ selectedStep?: Step<TContext, TData>;
888
+ responseDirectives?: string[];
889
+ session: SessionState<TData>;
890
+ isFlowComplete: boolean;
891
+ } | undefined> {
892
+ const { session } = params;
893
+
894
+ // Only applies when we already have a current flow and step
895
+ if (!session.currentFlow || !session.currentStep) {
896
+ return undefined;
897
+ }
898
+
899
+ // Also skip this optimization if there's a pending directive (it takes priority)
900
+ if (session.pendingDirective) {
901
+ return undefined;
902
+ }
903
+
904
+ // Look up the actual Flow and Step objects to access `collect`
905
+ const currentFlow = this.agent.getFlows().find(
906
+ (f) => f.id === session.currentFlow?.id
907
+ );
908
+ if (!currentFlow) {
909
+ return undefined;
910
+ }
911
+
912
+ const currentStep = currentFlow.getStep(session.currentStep.id);
913
+ if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
914
+ return undefined;
915
+ }
916
+
917
+ // We have a step with collect fields. Run pre-extraction to see if the
918
+ // user's message populates any of them.
919
+ const collectFields = currentStep.collect;
920
+
921
+ // Snapshot current data for comparison
922
+ const dataBefore = { ...session.data };
923
+
924
+ // Run pre-extraction against the current flow
925
+ const extractedData = await this.preExtractFlowData({
926
+ route: currentFlow,
927
+ history: params.history,
928
+ context: params.context,
929
+ session,
930
+ signal: params.signal,
931
+ });
932
+
933
+ if (!extractedData || Object.keys(extractedData).length === 0) {
934
+ return undefined;
935
+ }
936
+
937
+ // Determine which collect fields were newly populated by pre-extraction
938
+ const populatedCollectFields: string[] = [];
939
+ for (const field of collectFields) {
940
+ const key = field as string;
941
+ const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
942
+ const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
943
+ if (hasNewValue && !hadValue) {
944
+ populatedCollectFields.push(key);
945
+ }
946
+ }
947
+
948
+ if (populatedCollectFields.length === 0) {
949
+ // Pre-extraction didn't populate any declared collect field — no skip
950
+ return undefined;
951
+ }
952
+
953
+ // ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
954
+ logger.debug(
955
+ `[ResponseModal] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`
956
+ );
957
+
958
+ // Merge extracted data into session
959
+ const updatedSession = mergeCollected(session, extractedData);
960
+ await this.agent.updateCollectedData(extractedData);
961
+
962
+ // Determine next step using pipeline method for consistency
963
+ // Pass the current flow/step as the routing result (retained)
964
+ const stepResult = await this.responsePipeline.determineNextStep({
965
+ selectedFlow: currentFlow,
966
+ selectedStep: currentStep,
967
+ session: updatedSession,
968
+ isFlowComplete: false,
969
+ });
970
+
971
+ return {
972
+ selectedFlow: stepResult.flowChanged || currentFlow,
973
+ selectedStep: stepResult.nextStep,
974
+ responseDirectives: undefined,
975
+ session: stepResult.session,
976
+ isFlowComplete: stepResult.flowChanged ? false : false,
977
+ };
978
+ }
979
+
980
+ /**
981
+ * Check if a flow should pre-extract data before determining the initial step
620
982
  * @private
621
983
  */
622
- private shouldPreExtractData(route: Route<TContext, TData>): boolean {
623
- // Pre-extract if route has declared required or optional fields
624
- if (route.requiredFields && route.requiredFields.length > 0) {
984
+ private shouldPreExtractData(flow: Flow<TContext, TData>): boolean {
985
+ // Pre-extract if flow has declared required or optional fields
986
+ if (flow.requiredFields && flow.requiredFields.length > 0) {
625
987
  return true;
626
988
  }
627
- if (route.optionalFields && route.optionalFields.length > 0) {
989
+ if (flow.optionalFields && flow.optionalFields.length > 0) {
628
990
  return true;
629
991
  }
630
992
 
631
- // Pre-extract if any step in the route collects data
632
- const steps = route.getAllSteps();
993
+ // Pre-extract if any step in the flow collects data
994
+ const steps = flow.getAllSteps();
633
995
  const hasDataCollectionSteps = steps.some(
634
996
  step => step.collect && step.collect.length > 0
635
997
  );
@@ -638,21 +1000,21 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
638
1000
  }
639
1001
 
640
1002
  /**
641
- * Pre-extract data from user message when entering a route
1003
+ * Pre-extract data from user message when entering a flow
642
1004
  * This allows skipping steps whose data is already provided
643
1005
  * @private
644
1006
  */
645
- private async preExtractRouteData(params: {
646
- route: Route<TContext, TData>;
1007
+ private async preExtractFlowData(params: {
1008
+ route: Flow<TContext, TData>;
647
1009
  history: Event[];
648
1010
  context: TContext;
649
1011
  session: SessionState<TData>;
650
1012
  signal?: AbortSignal;
651
1013
  }): Promise<Partial<TData>> {
652
- const { route, history, signal } = params;
1014
+ const { route: flow, history, signal } = params;
653
1015
 
654
- // Build a schema for data extraction based on route's fields
655
- const extractionSchema = this.agent.getSchema();
1016
+ // Build a schema for data extraction based on flow's fields
1017
+ const extractionSchema = this.agent.schema;
656
1018
  if (!extractionSchema) {
657
1019
  logger.warn(`[ResponseModal] No schema available for pre-extraction`);
658
1020
  return {};
@@ -672,11 +1034,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
672
1034
  ];
673
1035
 
674
1036
  // Add field descriptions
675
- if (route.requiredFields) {
676
- extractionPrompt.push(`Required fields: ${route.requiredFields.join(', ')}`);
1037
+ if (flow.requiredFields) {
1038
+ extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
677
1039
  }
678
- if (route.optionalFields) {
679
- extractionPrompt.push(`Optional fields: ${route.optionalFields.join(', ')}`);
1040
+ if (flow.optionalFields) {
1041
+ extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
680
1042
  }
681
1043
 
682
1044
  extractionPrompt.push(
@@ -722,118 +1084,216 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
722
1084
  effectiveContext,
723
1085
  session: initialSession,
724
1086
  history,
725
- selectedRoute,
1087
+ selectedFlow,
726
1088
  selectedStep,
727
1089
  responseDirectives,
728
- isRouteComplete,
729
- batchSteps,
730
- batchStoppedReason,
1090
+ isFlowComplete,
731
1091
  signal,
1092
+ signalFirings: preSignalFirings,
1093
+ signalPreDirective,
1094
+ signalHalted,
1095
+ signalHaltReply,
732
1096
  } = responseContext;
733
1097
  let session = initialSession;
734
1098
 
735
- // Get last user message (needed for both route and completion handling)
1099
+ // Accumulator for signal firings across both phases (fire order)
1100
+ const signalFirings: SignalFiring<TContext, TData>[] = [...(preSignalFirings || [])];
1101
+
1102
+ // Get last user message (needed for both flow and completion handling)
736
1103
  // Convert HistoryItem[] to Event[] for internal processing
737
1104
  const historyEvents = historyToEvents(history);
738
1105
 
1106
+ // ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
1107
+ // Pre-signal phase emitted halt → skip LLM call entirely.
1108
+ if (signalHalted) {
1109
+ const haltMessage = signalHaltReply || '';
1110
+ // Run post-signal phase even on halt (post-phase sees complete turn context)
1111
+ const postResult = await this.responsePipeline.runPostSignalPhase(
1112
+ session, effectiveContext, historyEvents,
1113
+ );
1114
+ session = postResult.updatedSession;
1115
+ signalFirings.push(...postResult.firings);
1116
+
1117
+ // Apply post-phase position directive as pendingDirective (Requirement 9.3)
1118
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1119
+ session = { ...session, pendingDirective: postResult.mergedDirective };
1120
+ }
1121
+
1122
+ await this.finalizeSession(session, effectiveContext);
1123
+ return {
1124
+ message: haltMessage,
1125
+ session,
1126
+ toolCalls: undefined,
1127
+ isFlowComplete: false,
1128
+ executedSteps: [],
1129
+ stoppedReason: signalHaltReply ? 'reply' : 'halt',
1130
+ triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1131
+ };
1132
+ }
1133
+
739
1134
  let message: string;
740
1135
  let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
741
1136
  let executedSteps: StepRef[] | undefined;
742
1137
  let stoppedReason: StoppedReason | undefined;
1138
+ let appliedInstructions: AppliedInstruction[] | undefined;
743
1139
 
744
1140
 
745
1141
 
746
- if (selectedRoute && !isRouteComplete) {
747
- // Check if we have batch steps to execute
748
- if (batchSteps && batchSteps.length > 0) {
749
- // BATCH EXECUTION: Execute multiple steps in a single LLM call
750
- logger.debug(`[ResponseModal] Executing batch of ${batchSteps.length} steps`);
1142
+ if (selectedFlow && !isFlowComplete) {
1143
+ // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
1144
+ // If the current step is auto, the executor advances through it (and any
1145
+ // subsequent auto-steps) until an interactive step or terminal condition.
1146
+ let resolvedStep = selectedStep;
1147
+ const currentStepInstance = session.currentStep
1148
+ ? selectedFlow.getStep(session.currentStep.id)
1149
+ : selectedStep;
751
1150
 
752
- const batchResult = await this.executeBatchResponse({
753
- selectedRoute,
754
- batchSteps,
755
- responseDirectives,
1151
+ if (currentStepInstance?.auto) {
1152
+ const autoChainExecutor = new AutoChainExecutor<TContext, TData>({
1153
+ maxAutoStepsPerTurn: this.agent.maxAutoStepsPerTurn,
1154
+ });
1155
+ const autoResult: AutoChainResult<TContext, TData> = await autoChainExecutor.run({
756
1156
  session,
757
- history,
758
1157
  context: effectiveContext,
759
- historyEvents,
1158
+ flow: selectedFlow,
760
1159
  });
761
1160
 
762
- message = batchResult.message;
763
- toolCalls = batchResult.toolCalls;
764
- session = batchResult.session;
765
- executedSteps = batchResult.executedSteps;
766
- stoppedReason = batchStoppedReason;
1161
+ session = autoResult.session;
767
1162
 
768
- } else {
769
- // SINGLE STEP EXECUTION: Fall back to single-step processing
770
- // This happens when batch determination returns empty (first step needs input)
771
- const result = await this.processRouteResponse({
772
- selectedRoute,
773
- selectedStep,
774
- responseDirectives,
775
- session,
776
- history,
777
- context: effectiveContext,
778
- historyEvents,
779
- signal,
780
- });
1163
+ // Handle halt: emit verbatim reply, persist, return — no LLM call.
1164
+ if (autoResult.stoppedReason === 'halt') {
1165
+ message = autoResult.mergedDirective?.reply || '';
1166
+ stoppedReason = 'halt';
1167
+ executedSteps = [];
1168
+
1169
+ await this.finalizeSession(session, effectiveContext);
1170
+ return {
1171
+ message,
1172
+ session,
1173
+ toolCalls: undefined,
1174
+ isFlowComplete: false,
1175
+ executedSteps,
1176
+ stoppedReason,
1177
+ };
1178
+ }
781
1179
 
782
- message = result.message;
783
- toolCalls = result.toolCalls;
784
- session = result.session;
1180
+ // Handle flow completion or cross-flow redirect from auto-chain.
1181
+ // The auto-chain ended without resolving to an interactive step.
1182
+ // Possible reasons: last_step (no successor), completed (explicit
1183
+ // complete directive), or goto (cross-flow redirect).
1184
+ if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
1185
+ logger.debug(`[ResponseModal] Auto-chain ended with ${autoResult.stoppedReason}`);
1186
+ session = await this.applyFlowCompletion({
1187
+ selectedFlow,
1188
+ session,
1189
+ context: effectiveContext,
1190
+ history,
1191
+ });
785
1192
 
786
- // Track executed step for single-step execution
787
- if (selectedStep) {
788
- executedSteps = [{
789
- id: selectedStep.id,
790
- routeId: selectedRoute.id,
791
- }];
1193
+ await this.finalizeSession(session, effectiveContext);
1194
+ return {
1195
+ message: '',
1196
+ session,
1197
+ toolCalls: undefined,
1198
+ isFlowComplete: true,
1199
+ executedSteps: [],
1200
+ stoppedReason: autoResult.stoppedReason,
1201
+ };
792
1202
  }
793
- stoppedReason = batchStoppedReason || 'needs_input';
794
- }
795
1203
 
796
- } else if (isRouteComplete && selectedRoute) {
797
- // Handle route completion
798
- logger.debug(`[ResponseModal] Generating completion message for route: ${selectedRoute.title}`);
1204
+ // Normal case: auto-chain resolved to an interactive step.
1205
+ resolvedStep = autoResult.resolvedStep;
1206
+ }
799
1207
 
800
- try {
801
- message = await this.handleRouteCompletion({
802
- selectedRoute,
803
- session,
804
- context: effectiveContext,
805
- history,
806
- historyEvents,
807
- signal,
808
- });
1208
+ // SINGLE STEP EXECUTION: Process the resolved interactive step.
1209
+ // The auto-chain (if it ran) already walked auto-steps. Only the
1210
+ // interactive step remains for the LLM call.
1211
+ const result = await this.processFlowResponse({
1212
+ selectedFlow,
1213
+ selectedStep: resolvedStep,
1214
+ responseDirectives,
1215
+ session,
1216
+ history,
1217
+ context: effectiveContext,
1218
+ historyEvents,
1219
+ signal,
1220
+ // Propagate signal pre-directive's appendPrompt for this turn's LLM call (Requirement 8.4)
1221
+ transientAppendage: signalPreDirective?.appendPrompt,
1222
+ // Merge signal pre-directive (halt/reply/injectTools) into the pre-LLM bus
1223
+ mergedPreDirective: signalPreDirective,
1224
+ });
809
1225
 
810
- // Set step to END_ROUTE marker
811
- session = enterStep(session, END_ROUTE_ID, "Route completed");
812
- stoppedReason = 'route_complete';
813
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
814
- } catch (error) {
815
- logger.error(`[ResponseModal] Error generating completion message:`, error);
816
- // Fallback to simple completion message
817
- message = `Thank you! I've recorded all the information for your ${selectedRoute.title.toLowerCase()}.`;
818
- session = enterStep(session, END_ROUTE_ID, "Route completed");
819
- stoppedReason = 'route_complete';
1226
+ message = result.message;
1227
+ toolCalls = result.toolCalls;
1228
+ session = result.session;
1229
+ appliedInstructions = result.appliedInstructions;
1230
+
1231
+ // Track executed step for single-step execution
1232
+ if (resolvedStep) {
1233
+ executedSteps = [{
1234
+ id: resolvedStep.id,
1235
+ flowId: selectedFlow.id,
1236
+ }];
820
1237
  }
1238
+ // Use stoppedReason from processFlowResponse if set (halt/reply),
1239
+ // otherwise default to 'needs_input' for normal LLM responses.
1240
+ stoppedReason = result.stoppedReason || 'needs_input';
1241
+
1242
+ } else if (isFlowComplete && selectedFlow) {
1243
+ // Flow completion path: pure state transition, no LLM call.
1244
+ // The framework emits no message of its own.
1245
+ // stoppedReason is 'last_step' because this completion was detected by
1246
+ // implicit terminus (no successor or all successors skipped), not by an
1247
+ // explicit `complete` directive.
1248
+ logger.debug(`[ResponseModal] Releasing session to idle for completed flow: ${selectedFlow.title}`);
1249
+
1250
+ session = await this.applyFlowCompletion({
1251
+ selectedFlow,
1252
+ session,
1253
+ context: effectiveContext,
1254
+ history,
1255
+ });
1256
+ message = '';
1257
+ stoppedReason = 'last_step';
1258
+ executedSteps = [];
821
1259
 
822
1260
  } else {
823
- // Fallback: No routes defined, generate a simple response
1261
+ // Fallback: No flows defined, generate a simple response
824
1262
 
825
- message = await this.generateFallbackResponse({
1263
+ const fallbackResult = await this.generateFallbackResponse({
826
1264
  history,
827
1265
  context: effectiveContext,
828
1266
  session,
829
1267
  });
830
1268
 
1269
+ message = fallbackResult.message;
1270
+ appliedInstructions = fallbackResult.appliedInstructions;
1271
+
831
1272
  // For fallback responses, set empty executedSteps and no stoppedReason
832
- // since there's no route/step execution happening
1273
+ // since there's no flow/step execution happening
833
1274
  executedSteps = [];
834
1275
  stoppedReason = undefined;
835
1276
  }
836
1277
 
1278
+ // POST-SIGNAL PHASE (Requirement 9.1, 9.2, 9.3, 9.4)
1279
+ // Runs after finalize/onComplete and before session persistence.
1280
+ // Post-phase signals see the complete turn result: assistant message in
1281
+ // history, collected data, tool results.
1282
+ const postResult = await this.responsePipeline.runPostSignalPhase(
1283
+ session, effectiveContext, historyEvents,
1284
+ );
1285
+ session = postResult.updatedSession;
1286
+
1287
+ // Append post-phase firings to the accumulator (preserves fire order)
1288
+ signalFirings.push(...postResult.firings);
1289
+
1290
+ // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1291
+ // (no mid-turn re-entry per D6 decision). Pre-LLM-only fields are already
1292
+ // dropped inside runPostSignalPhase per Phase 4.5.
1293
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1294
+ session = { ...session, pendingDirective: postResult.mergedDirective };
1295
+ }
1296
+
837
1297
  // Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
838
1298
  // - executedSteps: array of steps executed (empty array if none)
839
1299
  // - stoppedReason: why execution stopped (undefined for fallback)
@@ -842,281 +1302,300 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
842
1302
  message,
843
1303
  session,
844
1304
  toolCalls,
845
- isRouteComplete,
1305
+ isFlowComplete: isFlowComplete,
846
1306
  executedSteps: executedSteps || [],
847
1307
  stoppedReason,
1308
+ appliedInstructions,
1309
+ triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
848
1310
  };
849
1311
  }
850
1312
 
851
1313
  /**
852
- * Execute a batch of steps with a single LLM call
853
- *
854
- * This method:
855
- * 1. Executes all prepare hooks for steps in the batch (in order)
856
- * 2. Builds a combined prompt using BatchPromptBuilder
857
- * 3. Makes a single LLM call
858
- * 4. Collects data from the response for all steps
859
- * 5. Executes all finalize hooks for steps in the batch (in order)
860
- *
1314
+ * Execute prepare function for current step if available
1315
+ * @private
1316
+ */
1317
+ private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
1318
+ if (session.currentFlow && session.currentStep) {
1319
+ const currentFlow = this.agent.getFlows().find(
1320
+ (r) => r.id === session.currentFlow?.id
1321
+ );
1322
+ if (currentFlow) {
1323
+ const currentStep = currentFlow.getStep(session.currentStep.id);
1324
+ // Skip auto-steps — their prepare is handled by AutoChainExecutor
1325
+ if (currentStep?.auto) {
1326
+ logger.debug(`[ResponseModal] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
1327
+ return;
1328
+ }
1329
+ if (currentStep?.prepare) {
1330
+ logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
1331
+ await this.executePrepareFinalize(
1332
+ currentStep.prepare,
1333
+ context,
1334
+ session.data,
1335
+ currentFlow,
1336
+ currentStep
1337
+ );
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+
1343
+ /**
1344
+ * Execute finalize function for current step if available
1345
+ * @private
1346
+ */
1347
+ private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
1348
+ if (session.currentFlow && session.currentStep) {
1349
+ const currentFlow = this.agent.getFlows().find(
1350
+ (r) => r.id === session.currentFlow?.id
1351
+ );
1352
+ if (currentFlow) {
1353
+ const currentStep = currentFlow.getStep(session.currentStep.id);
1354
+ if (currentStep?.finalize) {
1355
+ logger.debug(
1356
+ `[ResponseModal] Executing finalize for step: ${currentStep.id}`
1357
+ );
1358
+ await this.executePrepareFinalize(
1359
+ currentStep.finalize,
1360
+ context,
1361
+ session.data,
1362
+ currentFlow,
1363
+ currentStep
1364
+ );
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ /**
1371
+ * Process flow response with unified tool execution and data collection
861
1372
  * @private
862
- * **Validates: Requirements 1.1, 4.4, 5.1, 5.2**
863
1373
  */
864
- private async executeBatchResponse(params: {
865
- selectedRoute: Route<TContext, TData>;
866
- batchSteps: StepOptions<TContext, TData>[];
1374
+ private async processFlowResponse(params: {
1375
+ selectedFlow: Flow<TContext, TData>;
1376
+ selectedStep?: Step<TContext, TData>;
867
1377
  responseDirectives?: string[];
868
1378
  session: SessionState<TData>;
869
1379
  history: HistoryItem[];
870
1380
  context: TContext;
871
1381
  historyEvents: Event[];
872
1382
  signal?: AbortSignal;
1383
+ /**
1384
+ * Per-turn transient appendage from merged PreDirective.appendPrompt.
1385
+ * Fresh every turn, never cached, never persisted.
1386
+ */
1387
+ transientAppendage?: string[];
1388
+ /**
1389
+ * Merged PreDirective from the directive bus's pre-LLM phase drain.
1390
+ * When `halt: true`, the LLM call is skipped entirely.
1391
+ */
1392
+ mergedPreDirective?: PreDirective<TContext, TData>;
873
1393
  }): Promise<{
874
1394
  message: string;
875
1395
  toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
876
1396
  session: SessionState<TData>;
877
- executedSteps: StepRef[];
1397
+ appliedInstructions?: AppliedInstruction[];
1398
+ stoppedReason?: StoppedReason;
878
1399
  }> {
879
- const { selectedRoute, batchSteps, history, context, historyEvents, signal } = params;
1400
+ const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
880
1401
  let session = params.session;
881
1402
 
882
- logger.debug(`[ResponseModal] Starting batch execution for ${batchSteps.length} steps`);
883
-
884
- // Create hook executor function
885
- const executeHook = async (
886
- hook: HookFunction<TContext, TData>,
887
- hookContext: TContext,
888
- data?: Partial<TData>,
889
- step?: StepOptions<TContext, TData>
890
- ): Promise<void> => {
891
- // Find the route for this step
892
- const route = selectedRoute;
893
- // Convert StepOptions to Step if needed for executePrepareFinalize
894
- const stepInstance = step?.id ? route.getStep(step.id) : undefined;
895
- await this.executePrepareFinalize(hook, hookContext, data, route, stepInstance);
896
- };
1403
+ // Determine next step
1404
+ let nextStep: Step<TContext, TData>;
1405
+ if (selectedStep) {
1406
+ nextStep = selectedStep;
1407
+ } else {
1408
+ // Determine current step from session if we're already in this flow
1409
+ const isInSameFlow = session.currentFlow?.id === selectedFlow.id;
1410
+ const currentStep = isInSameFlow && session.currentStep
1411
+ ? selectedFlow.getStep(session.currentStep.id)
1412
+ : undefined;
897
1413
 
898
- // PHASE 1: Execute all prepare hooks (Requirement 5.1)
899
- logger.debug(`[ResponseModal] Executing prepare hooks for batch`);
900
- const prepareResult = await this.batchExecutor.executePrepareHooks({
901
- steps: batchSteps,
902
- context,
903
- data: session.data,
904
- executeHook,
905
- });
1414
+ logger.debug(`[ResponseModal] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
906
1415
 
907
- if (!prepareResult.success) {
908
- // Prepare hook failed - return error response
909
- logger.error(`[ResponseModal] Prepare hook failed:`, prepareResult.error);
910
- throw new ResponseGenerationError(
911
- `Prepare hook failed: ${prepareResult.error?.message}`,
912
- {
913
- phase: 'prepare_hooks',
914
- context: {
915
- stepId: prepareResult.error?.stepId,
916
- executedSteps: prepareResult.executedSteps,
1416
+ // STEP 1 (Algorithm 1): branches win over linear chain
1417
+ if (currentStep?.branches && currentStep.branches.length > 0) {
1418
+ const branchResult = await this.responsePipeline.evaluateStepBranches(
1419
+ currentStep, selectedFlow, session, context
1420
+ );
1421
+ if (branchResult) {
1422
+ if (branchResult.nextStep) {
1423
+ nextStep = branchResult.nextStep;
1424
+ session = branchResult.session;
1425
+ } else {
1426
+ // Flow transition or completion — no local step to render
1427
+ // Return empty message with updated session; caller handles flow transition
1428
+ return { message: '', session: branchResult.session };
917
1429
  }
918
1430
  }
919
- );
920
- }
921
-
922
- // PHASE 2: Build combined prompt using BatchPromptBuilder (Requirement 4.4)
923
- logger.debug(`[ResponseModal] Building batch prompt`);
924
- const batchPromptResult = await this.batchPromptBuilder.buildBatchPrompt({
925
- steps: batchSteps,
926
- route: selectedRoute,
927
- history: historyEvents,
928
- context,
929
- session,
930
- agentOptions: this.agent.getAgentOptions(),
931
- });
932
-
933
- logger.debug(`[ResponseModal] Batch prompt built with ${batchPromptResult.stepCount} steps, collecting: ${batchPromptResult.collectFields.join(', ')}`);
934
-
935
- // Build response schema for batch (includes all collect fields)
936
- const responseSchema = this.buildBatchResponseSchema(batchPromptResult.collectFields);
937
-
938
- // Collect available tools for AI (from all steps in batch)
939
- const availableTools = this.collectBatchAvailableTools(selectedRoute, batchSteps);
1431
+ }
940
1432
 
941
- // PHASE 3: Make single LLM call (Requirement 4.4)
942
- logger.debug(`[ResponseModal] Making LLM call for batch`);
943
- const agentOptions = this.agent.getAgentOptions();
944
- const result = await agentOptions.provider.generateMessage({
945
- prompt: batchPromptResult.prompt,
946
- history, // Use HistoryItem[] for AI provider
947
- context,
948
- tools: availableTools,
949
- signal,
950
- parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "batch_response" } : undefined,
951
- });
1433
+ if (!nextStep!) {
1434
+ // Get candidate steps based on current position in the flow
1435
+ const flowRouter = this.agent.getFlowRouter();
1436
+ const candidates = await flowRouter.getCandidateStepsWithConditions(
1437
+ selectedFlow,
1438
+ currentStep, // Pass current step instead of undefined to maintain progression
1439
+ createTemplateContext({ data: session.data, session, context })
1440
+ );
952
1441
 
953
- let message = result.structured?.message || result.message;
954
- let toolCalls = result.structured?.toolCalls;
1442
+ logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
955
1443
 
956
- logger.debug(`[ResponseModal] LLM response received for batch`);
1444
+ if (candidates.length > 0) {
1445
+ nextStep = candidates[0].step;
1446
+ logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1447
+ } else {
1448
+ // Fallback to initial step even if it should be skipped
1449
+ nextStep = selectedFlow.initialStep;
1450
+ logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1451
+ }
1452
+ }
1453
+ }
957
1454
 
958
- // Execute tools if any
959
- if (toolCalls && toolCalls.length > 0) {
960
- const toolResult = await this.executeUnifiedToolLoop({
961
- toolCalls,
962
- context,
963
- session,
964
- history,
965
- selectedRoute,
966
- responsePrompt: batchPromptResult.prompt,
967
- availableTools,
968
- responseSchema,
969
- signal,
970
- });
971
-
972
- session = toolResult.session;
973
- toolCalls = toolResult.finalToolCalls;
974
- if (toolResult.finalMessage) {
975
- message = toolResult.finalMessage;
1455
+ // Update session with next step
1456
+ // If the next step has requires fields that are missing, stay at the previous step
1457
+ if (nextStep.requires && nextStep.requires.length > 0) {
1458
+ const sessionData = session.data || {};
1459
+ const missingRequires = nextStep.requires.filter(
1460
+ field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1461
+ );
1462
+ if (missingRequires.length > 0) {
1463
+ const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1464
+ `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1465
+ logger.warn(warning);
1466
+ console.warn(warning);
1467
+ // Stay at the current step - don't enter the next one
1468
+ const currentStepId = session.currentStep?.id;
1469
+ if (currentStepId) {
1470
+ const currentStepInstance = selectedFlow.getStep(currentStepId);
1471
+ if (currentStepInstance) {
1472
+ nextStep = currentStepInstance;
1473
+ logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
1474
+ }
1475
+ }
1476
+ } else {
1477
+ session = enterStep(session, nextStep.id, nextStep.description);
1478
+ logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
976
1479
  }
1480
+ } else {
1481
+ session = enterStep(session, nextStep.id, nextStep.description);
1482
+ logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
977
1483
  }
978
1484
 
979
- // PHASE 4: Collect data from response for all steps (Requirement 6.1, 6.2, 6.3)
980
- logger.debug(`[ResponseModal] Collecting batch data`);
981
- const collectResult = this.batchExecutor.collectBatchData({
982
- steps: batchSteps,
983
- llmResponse: result.structured || {},
984
- session,
985
- schema: this.agent.getSchema(),
986
- });
987
-
988
- session = collectResult.session;
989
-
990
- if (collectResult.collectedData && Object.keys(collectResult.collectedData).length > 0) {
991
- // Update agent's collected data
992
- await this.agent.updateCollectedData(collectResult.collectedData);
993
- logger.debug(`[ResponseModal] Batch collected data:`, collectResult.collectedData);
994
- }
995
-
996
- if (collectResult.validationErrors && collectResult.validationErrors.length > 0) {
997
- logger.warn(`[ResponseModal] Batch data validation errors:`, collectResult.validationErrors);
1485
+ // Build response schema for this flow (with collect fields from step)
1486
+ const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
1487
+
1488
+ // ── HALT SHORT-CIRCUIT (Requirement 2.5, 2.6, 2.7) ──────────────────────
1489
+ // After pre-LLM emissions are merged, if `halt: true` then skip the LLM
1490
+ // call entirely. The behavior depends on whether `reply` is also set.
1491
+ if (mergedPreDirective?.halt) {
1492
+ if (mergedPreDirective.reply) {
1493
+ // halt + reply: emit the reply as the assistant message
1494
+ logger.debug(`[ResponseModal] Halt with reply — skipping LLM call for step ${nextStep.id}`);
1495
+ return { message: mergedPreDirective.reply, session, stoppedReason: 'reply' };
1496
+ } else {
1497
+ // halt without reply: emit empty assistant content
1498
+ logger.debug(`[ResponseModal] Halt without reply — skipping LLM call for step ${nextStep.id}`);
1499
+ return { message: '', session, stoppedReason: 'halt' };
1500
+ }
998
1501
  }
999
1502
 
1000
- // Update session to final step position
1001
- const lastStep = batchSteps[batchSteps.length - 1];
1002
- if (lastStep?.id) {
1003
- session = enterStep(session, lastStep.id, lastStep.description);
1004
- logger.debug(`[ResponseModal] Updated session to final batch step: ${lastStep.id}`);
1503
+ // ── STEP.REPLY SHORT-CIRCUIT (Requirement 25.1–25.7, 17.9) ──────────────
1504
+ // A step with `reply` set emits a verbatim template response without LLM.
1505
+ // onEnter and prepare have already fired normally at this point.
1506
+ // If prepare returned a PreDirective with `reply`, that overrides
1507
+ // the step-declared reply (last-emission-wins per Algorithm 4).
1508
+ if (nextStep.reply != null) {
1509
+ // Determine the effective reply: prepare-emitted reply wins over step-declared
1510
+ const effectiveReply = mergedPreDirective?.reply ?? await render(
1511
+ nextStep.reply,
1512
+ createTemplateContext({ data: session.data || {}, context, session })
1513
+ );
1514
+ logger.debug(`[ResponseModal] Step.reply — skipping LLM call for step ${nextStep.id}`);
1515
+ return { message: effectiveReply, session, stoppedReason: 'reply' };
1005
1516
  }
1006
1517
 
1007
- // PHASE 5: Execute all finalize hooks (Requirement 5.2)
1008
- logger.debug(`[ResponseModal] Executing finalize hooks for batch`);
1009
- const finalizeResult = await this.batchExecutor.executeFinalizeHooks({
1010
- steps: batchSteps,
1011
- context,
1012
- data: session.data,
1013
- executeHook,
1014
- });
1015
-
1016
- if (finalizeResult.errors && finalizeResult.errors.length > 0) {
1017
- // Log finalize errors but don't fail (Requirement 5.5)
1018
- logger.warn(`[ResponseModal] Some finalize hooks failed:`, finalizeResult.errors);
1019
- }
1518
+ // Transient appendage: per-turn slot from PreDirective.appendPrompt.
1519
+ // Fresh each turn, never cached, never persisted.
1520
+ // Wrapped in try/finally to ensure cleanup even on abnormal termination.
1521
+ let turnTransientAppendage: string[] | undefined = transientAppendage;
1522
+ try {
1523
+ // Build response prompt
1524
+ const { prompt: responsePrompt, appliedInstructions } = await this.responseEngine.buildResponsePrompt({
1525
+ flow: selectedFlow,
1526
+ currentStep: nextStep,
1527
+ rules: [],
1528
+ prohibitions: [],
1529
+ directives: responseDirectives,
1530
+ history: historyEvents,
1531
+ agentOptions: this.agent.getAgentOptions(),
1532
+ instructions: this.collectScopedInstructions(selectedFlow, nextStep),
1533
+ combinedTerms: this.agent.getTerms(),
1534
+ context,
1535
+ session,
1536
+ agentSchema: this.agent.schema,
1537
+ transientAppendage: turnTransientAppendage,
1538
+ });
1020
1539
 
1021
- // Build executed steps list
1022
- const executedSteps: StepRef[] = batchSteps
1023
- .filter(step => step.id)
1024
- .map(step => ({
1025
- id: step.id!,
1026
- routeId: selectedRoute.id,
1027
- }));
1540
+ // Collect available tools for AI
1541
+ const availableTools = this.collectAvailableTools(selectedFlow, nextStep);
1028
1542
 
1029
- logger.debug(`[ResponseModal] Batch execution complete. Executed ${executedSteps.length} steps`);
1543
+ // Generate message using AI provider
1544
+ const agentOptions = this.agent.getAgentOptions();
1545
+ const result = await agentOptions.provider.generateMessage({
1546
+ prompt: responsePrompt,
1547
+ history, // Use HistoryItem[] for AI provider
1548
+ context,
1549
+ tools: availableTools,
1550
+ signal,
1551
+ parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
1552
+ });
1030
1553
 
1031
- return {
1032
- message,
1033
- toolCalls,
1034
- session,
1035
- executedSteps,
1036
- };
1037
- }
1554
+ let message = result.structured?.message || result.message;
1555
+ let toolCalls = result.structured?.toolCalls;
1038
1556
 
1039
- /**
1040
- * Build response schema for batch execution
1041
- * @private
1042
- */
1043
- private buildBatchResponseSchema(collectFields: string[]): Record<string, unknown> {
1044
- const properties: Record<string, unknown> = {
1045
- message: {
1046
- type: "string",
1047
- description: "Natural, conversational response directed at the user. Must NOT contain field names, raw data, or internal information.",
1048
- },
1049
- };
1557
+ // Debug: Log initial AI response
1558
+ logger.debug(`[ResponseModal] Initial AI response:`, {
1559
+ hasMessage: !!message,
1560
+ messageLength: message?.length || 0,
1561
+ hasToolCalls: !!toolCalls,
1562
+ toolCallsCount: toolCalls?.length || 0,
1563
+ toolNames: toolCalls?.map(tc => tc.toolName) || [],
1564
+ });
1050
1565
 
1051
- const agentSchema = this.agent.getSchema();
1566
+ // Execute tools with unified loop handling
1567
+ const toolResult = await this.executeUnifiedToolLoop({
1568
+ toolCalls,
1569
+ context,
1570
+ session,
1571
+ history,
1572
+ selectedFlow,
1573
+ responsePrompt,
1574
+ availableTools,
1575
+ responseSchema,
1576
+ signal,
1577
+ });
1052
1578
 
1053
- // Add collect fields to schema, using agent schema definitions when available
1054
- for (const field of collectFields) {
1055
- if (agentSchema?.properties && agentSchema.properties[field]) {
1056
- properties[field] = agentSchema.properties[field];
1057
- } else {
1058
- // Dynamic fallback when no agent schema is defined
1059
- properties[field] = {
1060
- type: "string",
1061
- description: `Collected value for ${field}`,
1062
- };
1579
+ session = toolResult.session;
1580
+ toolCalls = toolResult.finalToolCalls;
1581
+ if (toolResult.finalMessage) {
1582
+ message = toolResult.finalMessage;
1063
1583
  }
1064
- }
1065
-
1066
- return {
1067
- type: "object",
1068
- properties,
1069
- required: ["message"],
1070
- additionalProperties: true,
1071
- };
1072
- }
1073
1584
 
1074
- /**
1075
- * Collect available tools from all steps in the batch
1076
- * @private
1077
- */
1078
- private collectBatchAvailableTools(
1079
- route: Route<TContext, TData>,
1080
- batchSteps: StepOptions<TContext, TData>[]
1081
- ): Array<{
1082
- id: string;
1083
- name: string;
1084
- description?: string;
1085
- parameters?: unknown;
1086
- }> {
1087
- const availableTools = new Map<string, Tool<TContext, TData>>();
1088
-
1089
- // Add agent-level tools
1090
- this.agent.getTools().forEach((tool) => {
1091
- availableTools.set(tool.id, tool);
1092
- });
1093
-
1094
- // Add route-level tools
1095
- route.getTools().forEach((tool: Tool<TContext, TData>) => {
1096
- availableTools.set(tool.id, tool);
1097
- });
1098
-
1099
- // Add step-level tools from all batch steps
1100
- for (const step of batchSteps) {
1101
- if (step.tools) {
1102
- for (const toolRef of step.tools) {
1103
- if (typeof toolRef === "string") {
1104
- // Reference to registered tool - already in availableTools
1105
- } else if (typeof toolRef === 'object' && 'id' in toolRef && toolRef.id) {
1106
- // Inline tool definition
1107
- availableTools.set(toolRef.id, toolRef);
1108
- }
1109
- }
1110
- }
1585
+ // Collect data from response
1586
+ // Use follow-up structured data from tool loop when available, fall back to original result
1587
+ const dataSource = toolResult.structured
1588
+ ? { structured: toolResult.structured }
1589
+ : result;
1590
+ session = await this.collectDataFromResponse({ result: dataSource, selectedFlow, nextStep, session });
1591
+
1592
+ return { message, toolCalls, session, appliedInstructions };
1593
+ } finally {
1594
+ // Drain the transient appendage at end of turn.
1595
+ // This ensures PreDirective.appendPrompt does not leak to subsequent
1596
+ // turns even when the turn terminates abnormally (error, abort, reject).
1597
+ turnTransientAppendage = undefined;
1111
1598
  }
1112
-
1113
- // Convert to the format expected by AI providers
1114
- return Array.from(availableTools.values()).map((tool) => ({
1115
- id: tool.id,
1116
- name: tool.name || tool.id,
1117
- description: tool.description,
1118
- parameters: tool.parameters,
1119
- }));
1120
1599
  }
1121
1600
 
1122
1601
  /**
@@ -1130,426 +1609,188 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1130
1609
  effectiveContext,
1131
1610
  session: initialSession,
1132
1611
  history,
1133
- selectedRoute,
1612
+ selectedFlow,
1134
1613
  selectedStep,
1135
1614
  responseDirectives,
1136
- isRouteComplete,
1137
- batchSteps,
1138
- batchStoppedReason,
1615
+ isFlowComplete,
1616
+ signal,
1617
+ signalFirings: preSignalFirings,
1618
+ signalPreDirective,
1619
+ signalHalted,
1620
+ signalHaltReply,
1139
1621
  } = responseContext;
1140
- const session = initialSession;
1622
+ let session = initialSession;
1623
+
1624
+ // Accumulator for signal firings across both phases (fire order)
1625
+ const signalFirings: SignalFiring<TContext, TData>[] = [...(preSignalFirings || [])];
1141
1626
 
1142
- // Get last user message (needed for both route and completion handling)
1143
1627
  // Convert HistoryItem[] to Event[] for internal processing
1144
1628
  const historyEvents = historyToEvents(history);
1145
1629
 
1146
- if (selectedRoute && !isRouteComplete) {
1147
- // Check if we have batch steps to execute
1148
- if (batchSteps && batchSteps.length > 0) {
1149
- // BATCH EXECUTION: Execute multiple steps with streaming
1150
- // Note: For streaming, we still use batch execution but stream the response
1151
- logger.debug(`[ResponseModal] Streaming batch execution for ${batchSteps.length} steps`);
1152
-
1153
- yield* this.streamBatchResponse({
1154
- selectedRoute,
1155
- batchSteps,
1156
- responseDirectives,
1157
- session,
1158
- history,
1159
- context: effectiveContext,
1160
- historyEvents,
1161
- batchStoppedReason,
1162
- });
1163
- } else {
1164
- // SINGLE STEP EXECUTION: Fall back to single-step streaming
1165
- yield* this.processRouteStreamingResponse({
1166
- selectedRoute,
1167
- selectedStep,
1168
- responseDirectives,
1169
- session,
1170
- history,
1171
- context: effectiveContext,
1172
- historyEvents,
1173
- });
1174
- }
1175
-
1176
- } else if (isRouteComplete && selectedRoute) {
1177
- // Handle route completion streaming
1178
- yield* this.streamRouteCompletion({
1179
- selectedRoute,
1180
- session,
1181
- context: effectiveContext,
1182
- history,
1183
- historyEvents,
1184
- });
1185
-
1186
- } else {
1187
- // Fallback: No routes defined, stream a simple response
1188
- yield* this.streamFallbackResponse({
1189
- history,
1190
- context: effectiveContext,
1191
- session,
1192
- });
1193
- }
1194
- }
1195
-
1196
- /**
1197
- * Stream a batch response with multiple steps
1198
- *
1199
- * Similar to executeBatchResponse but streams the LLM response.
1200
- *
1201
- * @private
1202
- */
1203
- private async *streamBatchResponse(params: {
1204
- selectedRoute: Route<TContext, TData>;
1205
- batchSteps: StepOptions<TContext, TData>[];
1206
- responseDirectives?: string[];
1207
- session: SessionState<TData>;
1208
- history: HistoryItem[];
1209
- context: TContext;
1210
- historyEvents: Event[];
1211
- batchStoppedReason?: StoppedReason;
1212
- signal?: AbortSignal;
1213
- }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
1214
- const { selectedRoute, batchSteps, history, context, historyEvents, batchStoppedReason, signal } = params;
1215
- let session = params.session;
1216
-
1217
- // Create hook executor function
1218
- const executeHook = async (
1219
- hook: HookFunction<TContext, TData>,
1220
- hookContext: TContext,
1221
- data?: Partial<TData>,
1222
- step?: StepOptions<TContext, TData>
1223
- ): Promise<void> => {
1224
- const route = selectedRoute;
1225
- const stepInstance = step?.id ? route.getStep(step.id) : undefined;
1226
- await this.executePrepareFinalize(hook, hookContext, data, route, stepInstance);
1227
- };
1228
-
1229
- // PHASE 1: Execute all prepare hooks
1230
- const prepareResult = await this.batchExecutor.executePrepareHooks({
1231
- steps: batchSteps,
1232
- context,
1233
- data: session.data,
1234
- executeHook,
1235
- });
1236
-
1237
- if (!prepareResult.success) {
1238
- // Yield error chunk
1239
- yield {
1240
- delta: "",
1241
- accumulated: "",
1242
- done: true,
1243
- session,
1244
- error: new ResponseGenerationError(
1245
- `Prepare hook failed: ${prepareResult.error?.message}`,
1246
- { phase: 'prepare_hooks' }
1247
- ),
1248
- };
1249
- return;
1250
- }
1251
-
1252
- // PHASE 2: Build combined prompt
1253
- const batchPromptResult = await this.batchPromptBuilder.buildBatchPrompt({
1254
- steps: batchSteps,
1255
- route: selectedRoute,
1256
- history: historyEvents,
1257
- context,
1258
- session,
1259
- agentOptions: this.agent.getAgentOptions(),
1260
- });
1261
-
1262
- const responseSchema = this.buildBatchResponseSchema(batchPromptResult.collectFields);
1263
- const availableTools = this.collectBatchAvailableTools(selectedRoute, batchSteps);
1264
-
1265
- // PHASE 3: Stream LLM response
1266
- const agentOptions = this.agent.getAgentOptions();
1267
- const stream = agentOptions.provider.generateMessageStream({
1268
- prompt: batchPromptResult.prompt,
1269
- history, // Use HistoryItem[] for AI provider
1270
- context,
1271
- tools: availableTools,
1272
- signal,
1273
- parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "batch_stream_response" } : undefined,
1274
- });
1275
-
1276
- // Build executed steps list
1277
- const executedSteps: StepRef[] = batchSteps
1278
- .filter(step => step.id)
1279
- .map(step => ({
1280
- id: step.id!,
1281
- routeId: selectedRoute.id,
1282
- }));
1283
-
1284
- // Stream chunks
1285
- for await (const chunk of stream) {
1286
- // On final chunk, collect data and execute finalize hooks
1287
- if (chunk.done) {
1288
- // Collect data from response
1289
- if (chunk.structured) {
1290
- const collectResult = this.batchExecutor.collectBatchData({
1291
- steps: batchSteps,
1292
- llmResponse: chunk.structured,
1293
- session,
1294
- schema: this.agent.getSchema(),
1295
- });
1296
-
1297
- session = collectResult.session;
1298
-
1299
- if (collectResult.collectedData && Object.keys(collectResult.collectedData).length > 0) {
1300
- await this.agent.updateCollectedData(collectResult.collectedData);
1301
- }
1302
- }
1303
-
1304
- // Update session to final step position
1305
- const lastStep = batchSteps[batchSteps.length - 1];
1306
- if (lastStep?.id) {
1307
- session = enterStep(session, lastStep.id, lastStep.description);
1308
- }
1309
-
1310
- // Execute finalize hooks
1311
- await this.batchExecutor.executeFinalizeHooks({
1312
- steps: batchSteps,
1313
- context,
1314
- data: session.data,
1315
- executeHook,
1316
- });
1317
-
1318
- // Finalize session
1319
- await this.finalizeSession(session, context);
1320
- }
1321
-
1322
- yield {
1323
- delta: chunk.delta,
1324
- accumulated: chunk.accumulated,
1325
- done: chunk.done,
1326
- session,
1327
- toolCalls: chunk.structured?.toolCalls,
1328
- isRouteComplete: false,
1329
- executedSteps: chunk.done ? executedSteps : undefined,
1330
- stoppedReason: chunk.done ? batchStoppedReason : undefined,
1331
- metadata: chunk.metadata,
1332
- structured: chunk.structured,
1333
- };
1334
- }
1335
- }
1336
-
1337
- /**
1338
- * Execute prepare function for current step if available
1339
- * @private
1340
- */
1341
- private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
1342
- if (session.currentRoute && session.currentStep) {
1343
- const currentRoute = this.agent.getRoutes().find(
1344
- (r) => r.id === session.currentRoute?.id
1345
- );
1346
- if (currentRoute) {
1347
- const currentStep = currentRoute.getStep(session.currentStep.id);
1348
- if (currentStep?.prepare) {
1349
- logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
1350
- await this.executePrepareFinalize(
1351
- currentStep.prepare,
1352
- context,
1353
- session.data,
1354
- currentRoute,
1355
- currentStep
1356
- );
1357
- }
1358
- }
1359
- }
1360
- }
1361
-
1362
- /**
1363
- * Execute finalize function for current step if available
1364
- * @private
1365
- */
1366
- private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
1367
- if (session.currentRoute && session.currentStep) {
1368
- const currentRoute = this.agent.getRoutes().find(
1369
- (r) => r.id === session.currentRoute?.id
1370
- );
1371
- if (currentRoute) {
1372
- const currentStep = currentRoute.getStep(session.currentStep.id);
1373
- if (currentStep?.finalize) {
1374
- logger.debug(
1375
- `[ResponseModal] Executing finalize for step: ${currentStep.id}`
1376
- );
1377
- await this.executePrepareFinalize(
1378
- currentStep.finalize,
1379
- context,
1380
- session.data,
1381
- currentRoute,
1382
- currentStep
1383
- );
1384
- }
1385
- }
1386
- }
1387
- }
1388
-
1389
- /**
1390
- * Process route response with unified tool execution and data collection
1391
- * @private
1392
- */
1393
- private async processRouteResponse(params: {
1394
- selectedRoute: Route<TContext, TData>;
1395
- selectedStep?: Step<TContext, TData>;
1396
- responseDirectives?: string[];
1397
- session: SessionState<TData>;
1398
- history: HistoryItem[];
1399
- context: TContext;
1400
- historyEvents: Event[];
1401
- signal?: AbortSignal;
1402
- }): Promise<{
1403
- message: string;
1404
- toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
1405
- session: SessionState<TData>;
1406
- }> {
1407
- const { selectedRoute, selectedStep, responseDirectives, history, context, historyEvents, signal } = params;
1408
- let session = params.session;
1409
-
1410
- // Determine next step
1411
- let nextStep: Step<TContext, TData>;
1412
- if (selectedStep) {
1413
- nextStep = selectedStep;
1414
- } else {
1415
- // Determine current step from session if we're already in this route
1416
- const isInSameRoute = session.currentRoute?.id === selectedRoute.id;
1417
- const currentStep = isInSameRoute && session.currentStep
1418
- ? selectedRoute.getStep(session.currentStep.id)
1419
- : undefined;
1420
-
1421
- logger.debug(`[ResponseModal] Step determination: route match=${isInSameRoute}, currentRoute=${session.currentRoute?.id}, selectedRoute=${selectedRoute.id}, currentStep=${currentStep?.id || 'none'}`);
1422
-
1423
- // Get candidate steps based on current position in the route
1424
- const routingEngine = this.agent.getRoutingEngine();
1425
- const candidates = await routingEngine.getCandidateStepsWithConditions(
1426
- selectedRoute,
1427
- currentStep, // Pass current step instead of undefined to maintain progression
1428
- createTemplateContext({ data: session.data, session, context })
1429
- );
1430
-
1431
- logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new route entry)'}`);
1432
-
1433
- if (candidates.length > 0) {
1434
- nextStep = candidates[0].step;
1435
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new route'}`);
1436
- } else {
1437
- // Fallback to initial step even if it should be skipped
1438
- nextStep = selectedRoute.initialStep;
1439
- logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
1440
- }
1441
- }
1442
-
1443
- // Update session with next step
1444
- // If the next step has requires fields that are missing, stay at the previous step
1445
- if (nextStep.requires && nextStep.requires.length > 0) {
1446
- const sessionData = session.data || {};
1447
- const missingRequires = nextStep.requires.filter(
1448
- field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1630
+ // ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
1631
+ if (signalHalted) {
1632
+ const haltMessage = signalHaltReply || '';
1633
+ // Run post-signal phase even on halt
1634
+ const postResult = await this.responsePipeline.runPostSignalPhase(
1635
+ session, effectiveContext, historyEvents,
1449
1636
  );
1450
- if (missingRequires.length > 0) {
1451
- const warning = `[Agent] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1452
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step.`;
1453
- logger.warn(warning);
1454
- console.warn(warning);
1455
- // Stay at the current step - don't enter the next one
1456
- const currentStepId = session.currentStep?.id;
1457
- if (currentStepId) {
1458
- const currentStepInstance = selectedRoute.getStep(currentStepId);
1459
- if (currentStepInstance) {
1460
- nextStep = currentStepInstance;
1461
- logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
1462
- }
1463
- }
1464
- } else {
1465
- session = enterStep(session, nextStep.id, nextStep.description);
1466
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1637
+ session = postResult.updatedSession;
1638
+ signalFirings.push(...postResult.firings);
1639
+
1640
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1641
+ session = { ...session, pendingDirective: postResult.mergedDirective };
1467
1642
  }
1468
- } else {
1469
- session = enterStep(session, nextStep.id, nextStep.description);
1470
- logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
1643
+
1644
+ await this.finalizeSession(session, effectiveContext);
1645
+ yield {
1646
+ delta: haltMessage,
1647
+ accumulated: haltMessage,
1648
+ done: true,
1649
+ session,
1650
+ stoppedReason: haltMessage ? 'reply' : 'halt',
1651
+ executedSteps: [],
1652
+ triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1653
+ } as AgentResponseStreamChunk<TData>;
1654
+ return;
1471
1655
  }
1472
1656
 
1473
- // Build response schema for this route (with collect fields from step)
1474
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
1475
-
1476
- // Build response prompt
1477
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
1478
- route: selectedRoute,
1479
- currentStep: nextStep,
1480
- rules: selectedRoute.getRules(),
1481
- prohibitions: selectedRoute.getProhibitions(),
1482
- directives: responseDirectives,
1483
- history: historyEvents,
1484
- agentOptions: this.agent.getAgentOptions(),
1485
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
1486
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
1487
- context,
1488
- session,
1489
- agentSchema: this.agent.getSchema(),
1490
- });
1657
+ // ── Determine the inner stream generator based on flow state ────────
1658
+ let innerStream: AsyncGenerator<AgentResponseStreamChunk<TData>>;
1491
1659
 
1492
- // Collect available tools for AI
1493
- const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
1660
+ if (selectedFlow && !isFlowComplete) {
1661
+ // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work (streaming path).
1662
+ let resolvedStep = selectedStep;
1663
+ const currentStepInstance = session.currentStep
1664
+ ? selectedFlow.getStep(session.currentStep.id)
1665
+ : selectedStep;
1494
1666
 
1495
- // Generate message using AI provider
1496
- const agentOptions = this.agent.getAgentOptions();
1497
- const result = await agentOptions.provider.generateMessage({
1498
- prompt: responsePrompt,
1499
- history, // Use HistoryItem[] for AI provider
1500
- context,
1501
- tools: availableTools,
1502
- signal,
1503
- parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
1504
- });
1667
+ if (currentStepInstance?.auto) {
1668
+ const autoChainExecutor = new AutoChainExecutor<TContext, TData>({
1669
+ maxAutoStepsPerTurn: this.agent.maxAutoStepsPerTurn,
1670
+ });
1671
+ const autoResult: AutoChainResult<TContext, TData> = await autoChainExecutor.run({
1672
+ session,
1673
+ context: effectiveContext,
1674
+ flow: selectedFlow,
1675
+ });
1505
1676
 
1506
- let message = result.structured?.message || result.message;
1507
- let toolCalls = result.structured?.toolCalls;
1677
+ session = autoResult.session;
1508
1678
 
1509
- // Debug: Log initial AI response
1510
- logger.debug(`[ResponseModal] Initial AI response:`, {
1511
- hasMessage: !!message,
1512
- messageLength: message?.length || 0,
1513
- hasToolCalls: !!toolCalls,
1514
- toolCallsCount: toolCalls?.length || 0,
1515
- toolNames: toolCalls?.map(tc => tc.toolName) || [],
1516
- });
1679
+ // Handle halt: emit verbatim reply as a single chunk, done.
1680
+ if (autoResult.stoppedReason === 'halt') {
1681
+ const reply = autoResult.mergedDirective?.reply || '';
1682
+ await this.finalizeSession(session, effectiveContext);
1683
+ yield {
1684
+ delta: reply,
1685
+ accumulated: reply,
1686
+ done: true,
1687
+ session,
1688
+ stoppedReason: 'halt',
1689
+ executedSteps: [],
1690
+ triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1691
+ } as AgentResponseStreamChunk<TData>;
1692
+ return;
1693
+ }
1517
1694
 
1518
- // Execute tools with unified loop handling
1519
- const toolResult = await this.executeUnifiedToolLoop({
1520
- toolCalls,
1521
- context,
1522
- session,
1523
- history,
1524
- selectedRoute,
1525
- responsePrompt,
1526
- availableTools,
1527
- responseSchema,
1528
- signal,
1529
- });
1695
+ // Handle flow completion or cross-flow redirect from auto-chain.
1696
+ if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
1697
+ innerStream = this.streamFlowCompletion({
1698
+ selectedFlow,
1699
+ session,
1700
+ context: effectiveContext,
1701
+ history,
1702
+ historyEvents,
1703
+ stoppedReason: autoResult.stoppedReason,
1704
+ });
1705
+ } else {
1706
+ // Normal case: resolved to an interactive step.
1707
+ resolvedStep = autoResult.resolvedStep;
1708
+ innerStream = this.processFlowStreamingResponse({
1709
+ selectedFlow,
1710
+ selectedStep: resolvedStep,
1711
+ responseDirectives,
1712
+ session,
1713
+ history,
1714
+ context: effectiveContext,
1715
+ historyEvents,
1716
+ signal,
1717
+ transientAppendage: signalPreDirective?.appendPrompt,
1718
+ mergedPreDirective: signalPreDirective,
1719
+ });
1720
+ }
1721
+ } else {
1722
+ // No auto-step: directly stream the interactive step.
1723
+ innerStream = this.processFlowStreamingResponse({
1724
+ selectedFlow,
1725
+ selectedStep: resolvedStep,
1726
+ responseDirectives,
1727
+ session,
1728
+ history,
1729
+ context: effectiveContext,
1730
+ historyEvents,
1731
+ signal,
1732
+ // Propagate signal pre-directive's appendPrompt for this turn's LLM call
1733
+ transientAppendage: signalPreDirective?.appendPrompt,
1734
+ mergedPreDirective: signalPreDirective,
1735
+ });
1736
+ }
1737
+
1738
+ } else if (isFlowComplete && selectedFlow) {
1739
+ // Handle flow completion streaming — implicit terminus (no successor
1740
+ // or all successors skipped), so the reason is 'last_step'.
1741
+ innerStream = this.streamFlowCompletion({
1742
+ selectedFlow,
1743
+ session,
1744
+ context: effectiveContext,
1745
+ history,
1746
+ historyEvents,
1747
+ stoppedReason: 'last_step',
1748
+ });
1530
1749
 
1531
- session = toolResult.session;
1532
- toolCalls = toolResult.finalToolCalls;
1533
- if (toolResult.finalMessage) {
1534
- message = toolResult.finalMessage;
1750
+ } else {
1751
+ // Fallback: No flows defined, stream a simple response
1752
+ innerStream = this.streamFallbackResponse({
1753
+ history,
1754
+ context: effectiveContext,
1755
+ session,
1756
+ });
1535
1757
  }
1536
1758
 
1537
- // Collect data from response
1538
- // Use follow-up structured data from tool loop when available, fall back to original result
1539
- const dataSource = toolResult.structured
1540
- ? { structured: toolResult.structured }
1541
- : result;
1542
- session = await this.collectDataFromResponse({ result: dataSource, selectedRoute, nextStep, session });
1759
+ // ── Intercept the inner stream to run post-signal phase on the final chunk ──
1760
+ // This mirrors the non-streaming path: post-phase runs after finalize/onComplete
1761
+ // and before session persistence, attaching triggeredSignals to the final chunk
1762
+ // (Requirement 11.2).
1763
+ for await (const chunk of innerStream!) {
1764
+ if (chunk.done) {
1765
+ // Run post-signal phase on final chunk (Requirement 9.1, 9.2)
1766
+ const postResult = await this.responsePipeline.runPostSignalPhase(
1767
+ chunk.session || session, effectiveContext, historyEvents,
1768
+ );
1769
+ let finalSession = postResult.updatedSession;
1770
+ signalFirings.push(...postResult.firings);
1771
+
1772
+ // Requirement 9.3: Post-phase position directive sets session.pendingDirective
1773
+ if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
1774
+ finalSession = { ...finalSession, pendingDirective: postResult.mergedDirective };
1775
+ }
1543
1776
 
1544
- return { message, toolCalls, session };
1777
+ yield {
1778
+ ...chunk,
1779
+ session: finalSession,
1780
+ triggeredSignals: signalFirings.length > 0 ? signalFirings as unknown as SignalFiring<unknown, TData>[] : undefined,
1781
+ } as AgentResponseStreamChunk<TData>;
1782
+ } else {
1783
+ yield chunk;
1784
+ }
1785
+ }
1545
1786
  }
1546
1787
 
1547
1788
  /**
1548
- * Process route streaming response with unified tool execution and data collection
1789
+ * Process flow streaming response with unified tool execution and data collection
1549
1790
  * @private
1550
1791
  */
1551
- private async *processRouteStreamingResponse(params: {
1552
- selectedRoute: Route<TContext, TData>;
1792
+ private async *processFlowStreamingResponse(params: {
1793
+ selectedFlow: Flow<TContext, TData>;
1553
1794
  selectedStep?: Step<TContext, TData>;
1554
1795
  responseDirectives?: string[];
1555
1796
  session: SessionState<TData>;
@@ -1557,8 +1798,18 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1557
1798
  context: TContext;
1558
1799
  historyEvents: Event[];
1559
1800
  signal?: AbortSignal;
1801
+ /**
1802
+ * Per-turn transient appendage from merged PreDirective.appendPrompt.
1803
+ * Fresh every turn, never cached, never persisted.
1804
+ */
1805
+ transientAppendage?: string[];
1806
+ /**
1807
+ * Merged PreDirective from the directive bus's pre-LLM phase drain.
1808
+ * When `halt: true`, the LLM call is skipped entirely.
1809
+ */
1810
+ mergedPreDirective?: PreDirective<TContext, TData>;
1560
1811
  }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
1561
- const { selectedRoute, selectedStep, responseDirectives, history, context, historyEvents, signal } = params;
1812
+ const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
1562
1813
  let session = params.session;
1563
1814
 
1564
1815
  // Determine next step (same logic as non-streaming)
@@ -1566,25 +1817,50 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1566
1817
  if (selectedStep) {
1567
1818
  nextStep = selectedStep;
1568
1819
  } else {
1569
- // Determine current step from session if we're already in this route
1570
- const currentStep = session.currentRoute?.id === selectedRoute.id && session.currentStep
1571
- ? selectedRoute.getStep(session.currentStep.id)
1820
+ // Determine current step from session if we're already in this flow
1821
+ const currentStep = session.currentFlow?.id === selectedFlow.id && session.currentStep
1822
+ ? selectedFlow.getStep(session.currentStep.id)
1572
1823
  : undefined;
1573
1824
 
1574
- // Get candidate steps based on current position in the route
1575
- const routingEngine = this.agent.getRoutingEngine();
1576
- const candidates = await routingEngine.getCandidateStepsWithConditions(
1577
- selectedRoute,
1578
- currentStep, // Pass current step instead of undefined to maintain progression
1579
- createTemplateContext({ data: session.data, session, context })
1580
- );
1825
+ // STEP 1 (Algorithm 1): branches win over linear chain
1826
+ if (currentStep?.branches && currentStep.branches.length > 0) {
1827
+ const branchResult = await this.responsePipeline.evaluateStepBranches(
1828
+ currentStep, selectedFlow, session, context
1829
+ );
1830
+ if (branchResult) {
1831
+ // Branch resolved — yield a final chunk with the updated session and return
1832
+ if (branchResult.nextStep) {
1833
+ session = branchResult.session;
1834
+ nextStep = branchResult.nextStep;
1835
+ } else {
1836
+ // Flow transition or completion — no step to render
1837
+ yield {
1838
+ delta: '',
1839
+ accumulated: '',
1840
+ done: true,
1841
+ session: branchResult.session,
1842
+ } as AgentResponseStreamChunk<TData>;
1843
+ return;
1844
+ }
1845
+ }
1846
+ }
1581
1847
 
1582
- if (candidates.length > 0) {
1583
- nextStep = candidates[0].step;
1584
- logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new route'}`);
1585
- } else {
1586
- nextStep = selectedRoute.initialStep;
1587
- logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
1848
+ if (!nextStep!) {
1849
+ // Get candidate steps based on current position in the flow
1850
+ const flowRouter = this.agent.getFlowRouter();
1851
+ const candidates = await flowRouter.getCandidateStepsWithConditions(
1852
+ selectedFlow,
1853
+ currentStep, // Pass current step instead of undefined to maintain progression
1854
+ createTemplateContext({ data: session.data, session, context })
1855
+ );
1856
+
1857
+ if (candidates.length > 0) {
1858
+ nextStep = candidates[0].step;
1859
+ logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
1860
+ } else {
1861
+ nextStep = selectedFlow.initialStep;
1862
+ logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
1863
+ }
1588
1864
  }
1589
1865
  }
1590
1866
 
@@ -1596,13 +1872,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1596
1872
  field => (sessionData as Record<string, unknown>)[String(field)] === undefined
1597
1873
  );
1598
1874
  if (missingRequires.length > 0) {
1599
- const warning = `[Agent] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1600
- `missing required fields [${missingRequires.join(', ')}]. Staying at current step.`;
1875
+ const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
1876
+ `missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
1601
1877
  logger.warn(warning);
1602
1878
  console.warn(warning);
1603
1879
  const currentStepId = session.currentStep?.id;
1604
1880
  if (currentStepId) {
1605
- const currentStepInstance = selectedRoute.getStep(currentStepId);
1881
+ const currentStepInstance = selectedFlow.getStep(currentStepId);
1606
1882
  if (currentStepInstance) {
1607
1883
  nextStep = currentStepInstance;
1608
1884
  logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
@@ -1618,152 +1894,207 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1618
1894
  }
1619
1895
 
1620
1896
  // Build response schema and prompt (same as non-streaming)
1621
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
1622
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
1623
- route: selectedRoute,
1624
- currentStep: nextStep,
1625
- rules: selectedRoute.getRules(),
1626
- prohibitions: selectedRoute.getProhibitions(),
1627
- directives: responseDirectives,
1628
- history: historyEvents,
1629
- agentOptions: this.agent.getAgentOptions(),
1630
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
1631
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
1632
- context,
1633
- session,
1634
- agentSchema: this.agent.getSchema(),
1635
- });
1897
+ const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
1898
+
1899
+ // ── HALT SHORT-CIRCUIT (Requirement 2.5, 2.6, 2.7) ──────────────────────
1900
+ // After pre-LLM emissions are merged, if `halt: true` then skip the LLM
1901
+ // call entirely. Emit a single done chunk with the appropriate content.
1902
+ if (mergedPreDirective?.halt) {
1903
+ const reply = mergedPreDirective.reply || '';
1904
+ const reason: StoppedReason = mergedPreDirective.reply ? 'reply' : 'halt';
1905
+ logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
1906
+ await this.finalizeSession(session, context);
1907
+ yield {
1908
+ delta: reply,
1909
+ accumulated: reply,
1910
+ done: true,
1911
+ session,
1912
+ stoppedReason: reason,
1913
+ executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
1914
+ } as AgentResponseStreamChunk<TData>;
1915
+ return;
1916
+ }
1636
1917
 
1637
- // Collect available tools for AI
1638
- const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
1918
+ // ── STEP.REPLY SHORT-CIRCUIT (Requirement 25.1–25.7, 17.9) ──────────────
1919
+ // A step with `reply` set emits a verbatim template response without LLM.
1920
+ // onEnter and prepare have already fired normally. If prepare returned
1921
+ // a PreDirective with `reply`, that overrides the step-declared reply.
1922
+ if (nextStep.reply != null) {
1923
+ const effectiveReply = mergedPreDirective?.reply ?? await render(
1924
+ nextStep.reply,
1925
+ createTemplateContext({ data: session.data || {}, context, session })
1926
+ );
1927
+ logger.debug(`[ResponseModal] Step.reply (streaming) — skipping LLM call for step ${nextStep.id}`);
1928
+ await this.finalizeSession(session, context);
1929
+ yield {
1930
+ delta: effectiveReply,
1931
+ accumulated: effectiveReply,
1932
+ done: true,
1933
+ session,
1934
+ stoppedReason: 'reply' as StoppedReason,
1935
+ executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
1936
+ } as AgentResponseStreamChunk<TData>;
1937
+ return;
1938
+ }
1639
1939
 
1640
- // Generate message stream using AI provider
1641
- const agentOptions = this.agent.getAgentOptions();
1642
- const stream = agentOptions.provider.generateMessageStream({
1643
- prompt: responsePrompt,
1644
- history, // Use HistoryItem[] for AI provider
1645
- context,
1646
- tools: availableTools,
1647
- signal,
1648
- parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
1649
- });
1940
+ // Transient appendage: per-turn slot from PreDirective.appendPrompt.
1941
+ // Fresh each turn, never cached, never persisted.
1942
+ // Wrapped in try/finally to ensure cleanup even on abnormal termination.
1943
+ let turnTransientAppendage: string[] | undefined = transientAppendage;
1944
+ try {
1945
+ const { prompt: responsePrompt, appliedInstructions } = await this.responseEngine.buildResponsePrompt({
1946
+ flow: selectedFlow,
1947
+ currentStep: nextStep,
1948
+ rules: [],
1949
+ prohibitions: [],
1950
+ directives: responseDirectives,
1951
+ history: historyEvents,
1952
+ agentOptions: this.agent.getAgentOptions(),
1953
+ instructions: this.collectScopedInstructions(selectedFlow, nextStep),
1954
+ combinedTerms: this.agent.getTerms(),
1955
+ context,
1956
+ session,
1957
+ agentSchema: this.agent.schema,
1958
+ transientAppendage: turnTransientAppendage,
1959
+ });
1650
1960
 
1651
- // Stream chunks with unified tool handling
1652
- for await (const chunk of stream) {
1653
- let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
1961
+ // Collect available tools for AI
1962
+ const availableTools = this.collectAvailableTools(selectedFlow, nextStep);
1654
1963
 
1655
- // Extract tool calls from AI response on final chunk
1656
- if (chunk.done && chunk.structured?.toolCalls) {
1657
- toolCalls = chunk.structured.toolCalls;
1964
+ // Generate message stream using AI provider
1965
+ const agentOptions = this.agent.getAgentOptions();
1966
+ const stream = agentOptions.provider.generateMessageStream({
1967
+ prompt: responsePrompt,
1968
+ history, // Use HistoryItem[] for AI provider
1969
+ context,
1970
+ tools: availableTools,
1971
+ signal,
1972
+ parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
1973
+ });
1658
1974
 
1659
- const toolManager = this.getToolManager();
1975
+ // Stream chunks with unified tool handling
1976
+ for await (const chunk of stream) {
1977
+ let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
1660
1978
 
1661
- // Use concurrent execution for the initial batch of tool calls
1662
- if (toolManager && typeof toolManager.executeWithConcurrency === 'function') {
1663
- const toolCallRequests: ToolCallRequest[] = toolCalls.map((tc, i) => ({
1664
- id: `${tc.toolName}-${i}-${Date.now()}`,
1665
- toolName: tc.toolName,
1666
- arguments: tc.arguments,
1667
- }));
1979
+ // Extract tool calls from AI response on final chunk
1980
+ if (chunk.done && chunk.structured?.toolCalls) {
1981
+ toolCalls = chunk.structured.toolCalls;
1668
1982
 
1669
- const historyEvents = historyToEvents(history);
1983
+ const toolManager = this.getToolManager();
1670
1984
 
1671
- try {
1672
- for await (const update of toolManager.executeWithConcurrency({
1673
- toolCalls: toolCallRequests,
1674
- context,
1675
- data: session.data,
1676
- history: historyEvents,
1677
- signal,
1678
- route: selectedRoute,
1679
- step: nextStep,
1680
- })) {
1681
- // Apply context updates
1682
- if (update.contextUpdate) {
1683
- try {
1684
- await this.agent.updateContext(update.contextUpdate as Partial<TContext>);
1685
- } catch (error) {
1686
- logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
1985
+ // Use concurrent execution for the initial batch of tool calls
1986
+ if (toolManager && typeof toolManager.executeWithConcurrency === 'function') {
1987
+ const toolCallRequests: ToolCallRequest[] = toolCalls.map((tc, i) => ({
1988
+ id: `${tc.toolName}-${i}-${Date.now()}`,
1989
+ toolName: tc.toolName,
1990
+ arguments: tc.arguments,
1991
+ }));
1992
+
1993
+ const historyEvents = historyToEvents(history);
1994
+
1995
+ try {
1996
+ for await (const update of toolManager.executeWithConcurrency({
1997
+ toolCalls: toolCallRequests,
1998
+ context,
1999
+ data: session.data,
2000
+ history: historyEvents,
2001
+ signal,
2002
+ flow: selectedFlow,
2003
+ step: nextStep,
2004
+ })) {
2005
+ // Apply context updates
2006
+ if (update.contextUpdate) {
2007
+ try {
2008
+ await this.agent.updateContext(update.contextUpdate as Partial<TContext>);
2009
+ } catch (error) {
2010
+ logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
2011
+ }
1687
2012
  }
1688
- }
1689
2013
 
1690
- // Apply data updates
1691
- if (update.dataUpdate) {
1692
- try {
1693
- const updateDataMethod = this.agent.getUpdateDataMethod();
1694
- session = await updateDataMethod(session, update.dataUpdate);
1695
- } catch (error) {
1696
- logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
2014
+ // Apply data updates
2015
+ if (update.dataUpdate) {
2016
+ try {
2017
+ const updateDataMethod = this.agent.getUpdateDataMethod();
2018
+ session = await updateDataMethod(session, update.dataUpdate);
2019
+ } catch (error) {
2020
+ logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
2021
+ }
1697
2022
  }
1698
- }
1699
2023
 
1700
- // Yield progress updates immediately
1701
- if (update.progress) {
1702
- yield {
1703
- delta: '',
1704
- accumulated: chunk.accumulated,
1705
- done: false,
1706
- session,
1707
- toolCalls: undefined,
1708
- isRouteComplete: false,
1709
- metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
1710
- };
2024
+ // Yield progress updates immediately
2025
+ if (update.progress) {
2026
+ yield {
2027
+ delta: '',
2028
+ accumulated: chunk.accumulated,
2029
+ done: false,
2030
+ session,
2031
+ toolCalls: undefined,
2032
+ isFlowComplete: false,
2033
+ metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
2034
+ };
2035
+ }
1711
2036
  }
1712
- }
1713
2037
 
1714
- logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
1715
- } catch (error) {
1716
- logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
1717
- // Fall back to the unified tool loop on failure
2038
+ logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
2039
+ } catch (error) {
2040
+ logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
2041
+ // Fall back to the unified tool loop on failure
2042
+ const toolResult = await this.executeUnifiedToolLoop({
2043
+ toolCalls, context, session, history, selectedFlow,
2044
+ responsePrompt, availableTools, responseSchema, signal,
2045
+ });
2046
+ session = toolResult.session;
2047
+ toolCalls = toolResult.finalToolCalls;
2048
+ }
2049
+ } else {
2050
+ // Fallback: no ToolManager or no executeWithConcurrency, use unified tool loop
1718
2051
  const toolResult = await this.executeUnifiedToolLoop({
1719
- toolCalls, context, session, history, selectedRoute,
2052
+ toolCalls, context, session, history, selectedFlow,
1720
2053
  responsePrompt, availableTools, responseSchema, signal,
1721
2054
  });
1722
2055
  session = toolResult.session;
1723
2056
  toolCalls = toolResult.finalToolCalls;
1724
2057
  }
1725
- } else {
1726
- // Fallback: no ToolManager or no executeWithConcurrency, use unified tool loop
1727
- const toolResult = await this.executeUnifiedToolLoop({
1728
- toolCalls, context, session, history, selectedRoute,
1729
- responsePrompt, availableTools, responseSchema, signal,
2058
+ }
2059
+
2060
+ // Extract collected data on final chunk
2061
+ if (chunk.done && chunk.structured && nextStep.collect) {
2062
+ session = await this.collectDataFromResponse({
2063
+ result: { structured: chunk.structured },
2064
+ selectedFlow,
2065
+ nextStep,
2066
+ session,
1730
2067
  });
1731
- session = toolResult.session;
1732
- toolCalls = toolResult.finalToolCalls;
1733
2068
  }
1734
- }
1735
2069
 
1736
- // Extract collected data on final chunk
1737
- if (chunk.done && chunk.structured && nextStep.collect) {
1738
- session = await this.collectDataFromResponse({
1739
- result: { structured: chunk.structured },
1740
- selectedRoute,
1741
- nextStep,
1742
- session,
1743
- });
1744
- }
2070
+ // Handle session finalization on final chunk
2071
+ if (chunk.done) {
2072
+ await this.finalizeSession(session, context);
2073
+ }
1745
2074
 
1746
- // Handle session finalization on final chunk
1747
- if (chunk.done) {
1748
- await this.finalizeSession(session, context);
2075
+ // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2076
+ // - executedSteps: single step executed in this response
2077
+ // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
2078
+ // - session.currentStep: reflects the executed step
2079
+ yield {
2080
+ delta: chunk.delta,
2081
+ accumulated: chunk.accumulated,
2082
+ done: chunk.done,
2083
+ session,
2084
+ toolCalls,
2085
+ isFlowComplete: false,
2086
+ executedSteps: chunk.done ? [{ id: nextStep.id, flowId: selectedFlow.id }] : undefined,
2087
+ stoppedReason: chunk.done ? 'needs_input' : undefined,
2088
+ metadata: chunk.metadata,
2089
+ structured: chunk.structured,
2090
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
2091
+ };
1749
2092
  }
1750
-
1751
- // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1752
- // - executedSteps: single step executed in this response
1753
- // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
1754
- // - session.currentStep: reflects the executed step
1755
- yield {
1756
- delta: chunk.delta,
1757
- accumulated: chunk.accumulated,
1758
- done: chunk.done,
1759
- session,
1760
- toolCalls,
1761
- isRouteComplete: false,
1762
- executedSteps: chunk.done ? [{ id: nextStep.id, routeId: selectedRoute.id }] : undefined,
1763
- stoppedReason: chunk.done ? 'needs_input' : undefined,
1764
- metadata: chunk.metadata,
1765
- structured: chunk.structured,
1766
- };
2093
+ } finally {
2094
+ // Drain the transient appendage at end of turn.
2095
+ // This ensures PreDirective.appendPrompt does not leak to subsequent
2096
+ // turns even when the turn terminates abnormally (error, abort, reject).
2097
+ turnTransientAppendage = undefined;
1767
2098
  }
1768
2099
  }
1769
2100
 
@@ -1777,7 +2108,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1777
2108
  context: TContext;
1778
2109
  session: SessionState<TData>;
1779
2110
  history: HistoryItem[];
1780
- selectedRoute?: Route<TContext, TData>;
2111
+ selectedFlow?: Flow<TContext, TData>;
1781
2112
  responsePrompt: string;
1782
2113
  availableTools: Array<{
1783
2114
  id: string;
@@ -1794,7 +2125,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1794
2125
  structured?: AgentStructuredResponse;
1795
2126
  }> {
1796
2127
  try {
1797
- const { context, history, selectedRoute, responsePrompt, availableTools, responseSchema, signal } = params;
2128
+ const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
1798
2129
  let { toolCalls, session } = params;
1799
2130
 
1800
2131
  // Convert HistoryItem[] to Event[] for internal processing
@@ -1810,9 +2141,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1810
2141
  logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
1811
2142
 
1812
2143
  for (const toolCall of toolCalls) {
1813
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
2144
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1814
2145
  if (!tool) {
1815
- logger.warn(`[ResponseModal] Tool not found: ${toolCall.toolName}`);
2146
+ 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.`);
1816
2147
  continue;
1817
2148
  }
1818
2149
 
@@ -1892,7 +2223,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1892
2223
  // Create tool result history items
1893
2224
  const toolResultHistoryItems: HistoryItem[] = [];
1894
2225
  for (const toolCall of toolCalls || []) {
1895
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
2226
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1896
2227
  if (tool) {
1897
2228
  // Create HistoryItem format for tool results
1898
2229
  // Add assistant message with tool_calls
@@ -1958,9 +2289,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1958
2289
 
1959
2290
  // Execute the follow-up tool calls
1960
2291
  for (const toolCall of followUpToolCalls!) {
1961
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
2292
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1962
2293
  if (!tool) {
1963
- logger.warn(`[ResponseModal] Tool not found in follow-up: ${toolCall.toolName}`);
2294
+ 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.`);
1964
2295
  continue;
1965
2296
  }
1966
2297
 
@@ -2033,7 +2364,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2033
2364
  }
2034
2365
 
2035
2366
  if (toolLoopCount >= MAX_TOOL_LOOPS) {
2036
- logger.warn(`[ResponseModal] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
2367
+ 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.`);
2037
2368
  }
2038
2369
 
2039
2370
  // If tools were executed but no final text message was produced,
@@ -2122,41 +2453,41 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2122
2453
  */
2123
2454
  private async collectDataFromResponse(params: {
2124
2455
  result: { structured?: AgentStructuredResponse };
2125
- selectedRoute?: Route<TContext, TData>;
2456
+ selectedFlow?: Flow<TContext, TData>;
2126
2457
  nextStep?: Step<TContext, TData>;
2127
2458
  session: SessionState<TData>;
2128
2459
  }): Promise<SessionState<TData>> {
2129
2460
  try {
2130
- const { result, selectedRoute, nextStep, session } = params;
2461
+ const { result, selectedFlow, nextStep, session } = params;
2131
2462
  let updatedSession = session;
2132
2463
 
2133
- // Extract collected data from final response (only for route-based interactions)
2134
- if (selectedRoute && result.structured) {
2464
+ // Extract collected data from final response (only for flow-based interactions)
2465
+ if (selectedFlow && result.structured) {
2135
2466
  try {
2136
2467
  const collectedData: Record<string, unknown> = {};
2137
2468
  // AgentStructuredResponse extends Record<string, unknown>, so we can safely access properties
2138
2469
  const structuredData = result.structured;
2139
2470
 
2140
- // Collect ALL route fields (required + optional) from structured response
2141
- const allRouteFields = new Set<string>();
2471
+ // Collect ALL flow fields (required + optional) from structured response
2472
+ const allFlowFields = new Set<string>();
2142
2473
 
2143
- // Add route required fields
2144
- if (selectedRoute.requiredFields) {
2145
- selectedRoute.requiredFields.forEach(field => allRouteFields.add(String(field)));
2474
+ // Add flow required fields
2475
+ if (selectedFlow.requiredFields) {
2476
+ selectedFlow.requiredFields.forEach(field => allFlowFields.add(String(field)));
2146
2477
  }
2147
2478
 
2148
- // Add route optional fields
2149
- if (selectedRoute.optionalFields) {
2150
- selectedRoute.optionalFields.forEach(field => allRouteFields.add(String(field)));
2479
+ // Add flow optional fields
2480
+ if (selectedFlow.optionalFields) {
2481
+ selectedFlow.optionalFields.forEach(field => allFlowFields.add(String(field)));
2151
2482
  }
2152
2483
 
2153
- // Also include current step's collect fields (in case they're not in route fields)
2484
+ // Also include current step's collect fields (in case they're not in flow fields)
2154
2485
  if (nextStep?.collect) {
2155
- nextStep.collect.forEach(field => allRouteFields.add(String(field)));
2486
+ nextStep.collect.forEach(field => allFlowFields.add(String(field)));
2156
2487
  }
2157
2488
 
2158
2489
  // Extract all available fields from structured response
2159
- for (const field of allRouteFields) {
2490
+ for (const field of allFlowFields) {
2160
2491
  const fieldKey = String(field);
2161
2492
  if (fieldKey in structuredData && structuredData[fieldKey] !== undefined && structuredData[fieldKey] !== null) {
2162
2493
  collectedData[fieldKey] = structuredData[fieldKey];
@@ -2207,240 +2538,147 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2207
2538
  }
2208
2539
 
2209
2540
  /**
2210
- * Handle route completion logic
2541
+ * Apply flow completion: release the session to idle state.
2542
+ *
2543
+ * This is a pure state transition. The framework emits **no message of
2544
+ * its own** at the completion boundary — every word delivered to the
2545
+ * user comes from a developer-defined step prompt. If the dev wants a
2546
+ * closing turn, they add a final interactive step with their own
2547
+ * `prompt`; the framework respects that step's natural LLM output.
2548
+ *
2549
+ * Behavior:
2550
+ * - Marks the active `flowHistory` entry as `completed: true` and
2551
+ * stamps `exitedAt`.
2552
+ * - Evaluates `flow.onComplete` for an explicit follow-up transition.
2553
+ * When set, populates `session.pendingDirective` (the next turn's
2554
+ * pipeline applies it). When absent, the session is fully idle.
2555
+ * - Clears `currentFlow` and `currentStep` to `undefined`.
2556
+ * - Clears owned fields when the flow is `reentrant` so subsequent
2557
+ * re-selections start from a clean state.
2558
+ *
2559
+ * Returns the updated session. Callers compose any reply text from
2560
+ * their own sources (an upstream LLM turn, a directive's `reply`, or
2561
+ * an empty string for silent completion).
2562
+ *
2211
2563
  * @private
2212
2564
  */
2213
- private async handleRouteCompletion(params: {
2214
- selectedRoute: Route<TContext, TData>;
2565
+ private async applyFlowCompletion(params: {
2566
+ selectedFlow: Flow<TContext, TData>;
2215
2567
  session: SessionState<TData>;
2216
2568
  context: TContext;
2217
2569
  history: HistoryItem[];
2218
- historyEvents: Event[];
2219
- signal?: AbortSignal;
2220
- }): Promise<string> {
2221
- const { selectedRoute, session, context, history, historyEvents, signal } = params;
2222
-
2223
- // Get endStep spec from route
2224
- const endStepSpec = selectedRoute.endStepSpec;
2225
-
2226
- // Create a temporary step for completion message generation using endStep configuration
2227
- const completionStep = new Step<TContext, TData>(selectedRoute.id, {
2228
- description: endStepSpec.description,
2229
- id: endStepSpec.id || END_ROUTE_ID,
2230
- collect: endStepSpec.collect,
2231
- requires: endStepSpec.requires,
2232
- 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.",
2233
- });
2234
-
2235
- // Build response schema for completion (message only, no data collection)
2236
- const completionSchema = {
2237
- type: "object",
2238
- properties: {
2239
- message: {
2240
- type: "string",
2241
- description: "A natural, warm farewell message for the user. Must NOT contain task names, field names, collected data, or any internal/technical information.",
2242
- },
2243
- },
2244
- required: ["message"],
2245
- additionalProperties: false,
2246
- };
2247
-
2248
- const templateContext = createTemplateContext({ context, session, history: historyEvents });
2249
-
2250
- // Build completion response prompt using ResponseEngine
2251
- // Filter out conditional guidelines - only include always-active ones
2252
- const alwaysActiveGuidelines = [
2253
- ...this.agent.getGuidelines().filter(g => !g.condition),
2254
- ...selectedRoute.getGuidelines().filter(g => !g.condition),
2255
- ];
2256
- let completitionPrompt = "Send a brief, natural farewell message. Do NOT mention internal data or task details."
2257
- if (endStepSpec.prompt) {
2258
- completitionPrompt = await render(endStepSpec.prompt, templateContext)
2259
- }
2570
+ }): Promise<SessionState<TData>> {
2571
+ const { selectedFlow, session, context } = params;
2260
2572
 
2261
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
2262
- route: selectedRoute,
2263
- currentStep: completionStep,
2264
- rules: selectedRoute.getRules(),
2265
- prohibitions: selectedRoute.getProhibitions(),
2266
- directives: [
2267
- "The conversation task has been completed successfully",
2268
- "Generate a natural, friendly farewell message for the user",
2269
- "Do NOT mention task names, route names, collected data, field names, or any internal/technical information",
2270
- "Do NOT list or summarize the data you collected - the user already knows what they told you",
2271
- "Do NOT use words like 'tarefa', 'dados coletados', 'prospecção', 'concluída' or similar internal terms",
2272
- "Keep it brief, warm, and conversational - as if ending a natural conversation",
2273
- "Do NOT ask for more information - the conversation is ending",
2274
- completitionPrompt,
2275
- ],
2276
- history: historyEvents,
2277
- agentOptions: this.agent.getAgentOptions(),
2278
- combinedGuidelines: alwaysActiveGuidelines, // Only non-conditional guidelines
2279
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
2573
+ // 1) Evaluate onComplete first — needs the still-active session shape.
2574
+ const transitionConfig = await selectedFlow.evaluateOnComplete(
2575
+ { data: session.data },
2280
2576
  context,
2281
- session,
2282
- agentSchema: undefined, // No data collection schema for completion
2283
- });
2284
-
2285
- // Generate completion message using AI provider
2286
- const agentOptions = this.agent.getAgentOptions();
2287
- logger.debug(`[ResponseModal] Calling AI provider for completion message...`);
2577
+ );
2288
2578
 
2289
- const completionResult = await agentOptions.provider.generateMessage({
2290
- prompt: completionPrompt,
2291
- history, // Use HistoryItem[] for AI provider
2292
- context,
2293
- signal,
2294
- parameters: { jsonSchema: completionSchema, schemaName: "completion_message" },
2579
+ // 2) Release to idle. If the flow is reentrant, scrub its owned
2580
+ // fields so re-selection on a future turn starts clean. When
2581
+ // onComplete fires we still go idle here — the next turn's
2582
+ // pipeline applies the pendingDirective before any routing.
2583
+ const ownedFields = selectedFlow.reentrant
2584
+ ? [
2585
+ ...(selectedFlow.requiredFields ?? []),
2586
+ ...(selectedFlow.optionalFields ?? []),
2587
+ ]
2588
+ : undefined;
2589
+
2590
+ let nextSession = completeCurrentFlow(session, {
2591
+ clearOwnedFields: ownedFields,
2295
2592
  });
2296
2593
 
2297
- logger.debug(`[ResponseModal] AI provider returned completion result`);
2298
- const message = completionResult.structured?.message || completionResult.message;
2299
- logger.debug(`[ResponseModal] Generated completion message for route: ${selectedRoute.title}`);
2300
-
2301
- // Check for onComplete transition
2302
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
2303
-
2594
+ // 3) Wire pendingDirective when onComplete returned a target.
2304
2595
  if (transitionConfig) {
2305
- // Find target route by ID or title
2306
- const targetRoute = this.agent.getRoutes().find(
2307
- (r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep
2308
- );
2309
-
2310
- if (targetRoute) {
2311
- const renderedCondition = await render(transitionConfig.condition, templateContext);
2312
- // Set pending transition in session
2313
- session.pendingTransition = {
2314
- targetRouteId: targetRoute.id,
2315
- condition: renderedCondition,
2316
- reason: "route_complete",
2596
+ const goToTarget = typeof transitionConfig.goTo === 'string'
2597
+ ? transitionConfig.goTo
2598
+ : transitionConfig.goTo?.flow;
2599
+
2600
+ const targetFlow = goToTarget ? this.agent.getFlows().find(
2601
+ (r) =>
2602
+ r.id === goToTarget ||
2603
+ r.title === goToTarget,
2604
+ ) : undefined;
2605
+
2606
+ if (targetFlow) {
2607
+ nextSession = {
2608
+ ...nextSession,
2609
+ pendingDirective: {
2610
+ goTo: targetFlow.id,
2611
+ },
2317
2612
  };
2318
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
2319
- } else {
2320
- logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
2613
+ logger.debug(
2614
+ `[ResponseModal] Flow ${selectedFlow.title} completed with pending directive to: ${targetFlow.title}`,
2615
+ );
2616
+ } else if (goToTarget) {
2617
+ logger.warn(
2618
+ `[FlowConfigurationError] onComplete target not found: flow "${selectedFlow.title}" completed but onComplete target "${goToTarget}" does not match any flow. ` +
2619
+ `Fix the onComplete value to reference an existing flow id/title, or remove onComplete to release the session to idle.`,
2620
+ );
2321
2621
  }
2622
+ } else {
2623
+ logger.debug(
2624
+ `[ResponseModal] Flow ${selectedFlow.title} completed; session released to idle.`,
2625
+ );
2322
2626
  }
2323
2627
 
2324
- return message;
2628
+ return nextSession;
2325
2629
  }
2326
2630
 
2327
2631
  /**
2328
- * Stream route completion response
2632
+ * Stream flow completion response
2633
+ * @private
2634
+ */
2635
+ /**
2636
+ * Stream a flow completion as a single terminal chunk.
2637
+ *
2638
+ * No LLM call is made. The framework no longer authors a farewell — the
2639
+ * completion path is a pure state transition. The chunk emits an empty
2640
+ * `delta` and a `done: true` flag with the idle session attached so
2641
+ * downstream consumers can finalize cleanly.
2642
+ *
2643
+ * If the developer wants closing copy in a streaming response, they
2644
+ * should add a final interactive step whose own LLM turn delivers it.
2645
+ *
2329
2646
  * @private
2330
2647
  */
2331
- private async *streamRouteCompletion(params: {
2332
- selectedRoute: Route<TContext, TData>;
2648
+ private async *streamFlowCompletion(params: {
2649
+ selectedFlow: Flow<TContext, TData>;
2333
2650
  session: SessionState<TData>;
2334
2651
  context: TContext;
2335
2652
  history: HistoryItem[];
2336
2653
  historyEvents: Event[];
2654
+ stoppedReason?: StoppedReason;
2337
2655
  signal?: AbortSignal;
2338
2656
  }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
2339
- const { selectedRoute, context, history, historyEvents, signal } = params;
2340
- let session = params.session;
2341
-
2342
- // Get endStep spec from route
2343
- const endStepSpec = selectedRoute.endStepSpec;
2344
-
2345
- // Create a temporary step for completion message generation using endStep configuration
2346
- const completionStep = new Step<TContext, TData>(selectedRoute.id, {
2347
- description: endStepSpec.description,
2348
- id: endStepSpec.id || END_ROUTE_ID,
2349
- collect: endStepSpec.collect,
2350
- requires: endStepSpec.requires,
2351
- 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.",
2352
- });
2657
+ const { selectedFlow, context, history } = params;
2353
2658
 
2354
- // Build response schema for completion
2355
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
2356
- const templateContext = createTemplateContext({ context, session, history: historyEvents }); // Use Event[] for template context
2357
-
2358
- // Build completion response prompt
2359
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
2360
- route: selectedRoute,
2361
- currentStep: completionStep,
2362
- rules: selectedRoute.getRules(),
2363
- prohibitions: selectedRoute.getProhibitions(),
2364
- directives: undefined, // No directives for completion
2365
- history: historyEvents,
2366
- agentOptions: this.agent.getAgentOptions(),
2367
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
2368
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
2369
- context,
2370
- session,
2371
- agentSchema: this.agent.getSchema(),
2372
- });
2373
-
2374
- // Stream completion message using AI provider
2375
- const agentOptions = this.agent.getAgentOptions();
2376
- const stream = agentOptions.provider.generateMessageStream({
2377
- prompt: completionPrompt,
2378
- history, // Use HistoryItem[] for AI provider
2659
+ const session = await this.applyFlowCompletion({
2660
+ selectedFlow,
2661
+ session: params.session,
2379
2662
  context,
2380
- signal,
2381
- parameters: { jsonSchema: responseSchema, schemaName: "completion_message_stream" },
2663
+ history,
2382
2664
  });
2383
2665
 
2384
- logger.debug(`[ResponseModal] Streaming completion message for route: ${selectedRoute.title}`);
2385
-
2386
- // Check for onComplete transition
2387
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
2388
-
2389
- if (transitionConfig) {
2390
- // Find target route by ID or title
2391
- const targetRoute = this.agent.getRoutes().find(
2392
- (r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep
2393
- );
2394
-
2395
- if (targetRoute) {
2396
- const renderedCondition = await render(transitionConfig.condition, templateContext);
2397
- // Set pending transition in session
2398
- session = {
2399
- ...session,
2400
- pendingTransition: {
2401
- targetRouteId: targetRoute.id,
2402
- condition: renderedCondition,
2403
- reason: "route_complete",
2404
- },
2405
- };
2406
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
2407
- } else {
2408
- logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
2409
- }
2410
- }
2411
-
2412
- // Set step to END_ROUTE marker
2413
- session = enterStep(session, END_ROUTE_ID, "Route completed");
2414
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
2415
-
2416
- // Stream completion chunks
2417
- for await (const chunk of stream) {
2418
- // Update current session if we have one
2419
- if (chunk.done) {
2420
- await this.finalizeSession(session, context);
2421
- }
2666
+ await this.finalizeSession(session, context);
2422
2667
 
2423
- // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2424
- // - executedSteps: empty for route completion (no new steps executed)
2425
- // - stoppedReason: 'route_complete' for completed routes
2426
- // - session.currentStep: set to END_ROUTE
2427
- yield {
2428
- delta: chunk.delta,
2429
- accumulated: chunk.accumulated,
2430
- done: chunk.done,
2431
- session,
2432
- toolCalls: undefined,
2433
- isRouteComplete: true,
2434
- executedSteps: chunk.done ? [] : undefined,
2435
- stoppedReason: chunk.done ? 'route_complete' : undefined,
2436
- metadata: chunk.metadata,
2437
- structured: chunk.structured,
2438
- };
2439
- }
2668
+ yield {
2669
+ delta: '',
2670
+ accumulated: '',
2671
+ done: true,
2672
+ session,
2673
+ toolCalls: undefined,
2674
+ isFlowComplete: true,
2675
+ executedSteps: [],
2676
+ stoppedReason: params.stoppedReason ?? 'completed',
2677
+ };
2440
2678
  }
2441
2679
 
2442
2680
  /**
2443
- * Generate fallback response when no routes are available
2681
+ * Generate fallback response when no flows are available
2444
2682
  * @private
2445
2683
  */
2446
2684
  private async generateFallbackResponse(params: {
@@ -2448,16 +2686,16 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2448
2686
  context: TContext;
2449
2687
  session: SessionState<TData>;
2450
2688
  signal?: AbortSignal;
2451
- }): Promise<string> {
2689
+ }): Promise<{ message: string; appliedInstructions?: AppliedInstruction[] }> {
2452
2690
  const { history, context, session, signal } = params;
2453
2691
 
2454
- logger.debug(`[ResponseModal] No route selected, generating basic response`);
2692
+ logger.debug(`[ResponseModal] No flow selected, generating basic response`);
2455
2693
 
2456
- // Build basic response prompt without route context
2457
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
2694
+ // Build basic response prompt without flow context
2695
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
2458
2696
  agentOptions: this.agent.getAgentOptions(),
2459
2697
  terms: this.agent.getTerms(),
2460
- guidelines: this.agent.getGuidelines(),
2698
+ instructions: this.collectScopedInstructions(),
2461
2699
  context,
2462
2700
  session,
2463
2701
  });
@@ -2479,11 +2717,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2479
2717
  },
2480
2718
  });
2481
2719
 
2482
- return result.structured?.message || result.message;
2720
+ return { message: result.structured?.message || result.message, appliedInstructions };
2483
2721
  }
2484
2722
 
2485
2723
  /**
2486
- * Stream fallback response when no routes are available
2724
+ * Stream fallback response when no flows are available
2487
2725
  * @private
2488
2726
  */
2489
2727
  private async *streamFallbackResponse(params: {
@@ -2494,10 +2732,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2494
2732
  }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
2495
2733
  const { history, context, session, signal } = params;
2496
2734
 
2497
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
2735
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
2498
2736
  agentOptions: this.agent.getAgentOptions(),
2499
2737
  terms: this.agent.getTerms(),
2500
- guidelines: this.agent.getGuidelines(),
2738
+ instructions: this.collectScopedInstructions(),
2501
2739
  context,
2502
2740
  session,
2503
2741
  });
@@ -2526,8 +2764,8 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2526
2764
  }
2527
2765
 
2528
2766
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2529
- // - executedSteps: empty for fallback (no route/step execution)
2530
- // - stoppedReason: undefined for fallback (no route context)
2767
+ // - executedSteps: empty for fallback (no flow/step execution)
2768
+ // - stoppedReason: undefined for fallback (no flow context)
2531
2769
  // - session.currentStep: unchanged (no step progression)
2532
2770
  yield {
2533
2771
  delta: chunk.delta,
@@ -2535,11 +2773,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2535
2773
  done: chunk.done,
2536
2774
  session,
2537
2775
  toolCalls: undefined,
2538
- isRouteComplete: false,
2776
+ isFlowComplete: false,
2539
2777
  executedSteps: chunk.done ? [] : undefined,
2540
2778
  stoppedReason: undefined,
2541
2779
  metadata: chunk.metadata,
2542
2780
  structured: chunk.structured,
2781
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
2543
2782
  };
2544
2783
  }
2545
2784
  }
@@ -2565,9 +2804,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2565
2804
  await this.executeStepFinalize(session, context);
2566
2805
 
2567
2806
  // Update current session if we have one
2568
- const currentSession = this.agent.getCurrentSession();
2807
+ const currentSession = this.agent.currentSession;
2569
2808
  if (currentSession) {
2570
- this.agent.setCurrentSession(session);
2809
+ this.agent.currentSession = session;
2571
2810
  }
2572
2811
  }
2573
2812
  // ============================================================================
@@ -2576,45 +2815,45 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2576
2815
 
2577
2816
 
2578
2817
  /**
2579
- * Find an available tool by name for the given route using ToolManager
2818
+ * Find an available tool by name for the given flow using ToolManager
2580
2819
  * Delegates to ToolManager for unified tool resolution
2581
2820
  * @private
2582
2821
  */
2583
2822
  private findAvailableTool(
2584
2823
  toolName: string,
2585
- route?: Route<TContext, TData>
2824
+ flow?: Flow<TContext, TData>
2586
2825
  ): Tool<TContext, TData> | undefined {
2587
2826
  // Use ToolManager for unified tool resolution
2588
2827
  const toolManager = this.getToolManager();
2589
2828
  if (toolManager) {
2590
- return toolManager.find(toolName, undefined, undefined, route);
2829
+ return toolManager.find(toolName, undefined, undefined, flow);
2591
2830
  }
2592
2831
 
2593
2832
  // Fallback to legacy resolution if ToolManager not available
2594
2833
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for: ${toolName}`);
2595
2834
 
2596
- // Check route-level tools first (if route provided)
2597
- if (route) {
2598
- const routeTool = route
2835
+ // Check flow-level tools first (if flow provided)
2836
+ if (flow) {
2837
+ const flowTool = flow
2599
2838
  .getTools()
2600
- .find((tool: Tool<TContext, TData>) => tool.id === toolName || tool.name === toolName);
2601
- if (routeTool) return routeTool;
2839
+ .find((tool: Tool<TContext, TData>) => tool.id === toolName || tool.id === toolName);
2840
+ if (flowTool) return flowTool;
2602
2841
  }
2603
2842
 
2604
2843
  // Fall back to agent-level tools
2605
2844
  const agentTools = this.agent.getTools();
2606
2845
  return agentTools.find(
2607
- (tool) => tool.id === toolName || tool.name === toolName
2846
+ (tool) => tool.id === toolName || tool.id === toolName
2608
2847
  );
2609
2848
  }
2610
2849
 
2611
2850
  /**
2612
- * Collect all available tools for the given route and step context using ToolManager
2851
+ * Collect all available tools for the given flow and step context using ToolManager
2613
2852
  * Delegates to ToolManager for unified tool resolution and deduplication
2614
2853
  * @private
2615
2854
  */
2616
2855
  private collectAvailableTools(
2617
- route?: Route<TContext, TData>,
2856
+ flow?: Flow<TContext, TData>,
2618
2857
  step?: Step<TContext, TData>
2619
2858
  ): Array<{
2620
2859
  id: string;
@@ -2625,10 +2864,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2625
2864
  // Use ToolManager for unified tool collection if available
2626
2865
  const toolManager = this.getToolManager();
2627
2866
  if (toolManager) {
2628
- const availableTools = toolManager.getAvailable(undefined, step, route);
2867
+ const availableTools = toolManager.getAvailable(undefined, step, flow);
2629
2868
  return availableTools.map((tool) => ({
2630
2869
  id: tool.id,
2631
- name: tool.name || tool.id,
2870
+ name: tool.id || tool.id,
2632
2871
  description: tool.description,
2633
2872
  parameters: tool.parameters,
2634
2873
  }));
@@ -2644,9 +2883,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2644
2883
  availableTools.set(tool.id, tool);
2645
2884
  });
2646
2885
 
2647
- // Add route-level tools (these take precedence)
2648
- if (route) {
2649
- route.getTools().forEach((tool: Tool<TContext, TData>) => {
2886
+ // Add flow-level tools (these take precedence)
2887
+ if (flow) {
2888
+ flow.getTools().forEach((tool: Tool<TContext, TData>) => {
2650
2889
  availableTools.set(tool.id, tool);
2651
2890
  });
2652
2891
  }
@@ -2692,7 +2931,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2692
2931
  // Convert to the format expected by AI providers
2693
2932
  return Array.from(availableTools.values()).map((tool) => ({
2694
2933
  id: tool.id,
2695
- name: tool.name || tool.id,
2934
+ name: tool.id || tool.id,
2696
2935
  description: tool.description,
2697
2936
  parameters: tool.parameters,
2698
2937
  }));
@@ -2706,11 +2945,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2706
2945
  prepareOrFinalize:
2707
2946
  | string
2708
2947
  | Tool<TContext, TData>
2709
- | ((context: TContext, data?: Partial<TData>) => void | Promise<void>)
2948
+ | ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult>)
2710
2949
  | undefined,
2711
2950
  context: TContext,
2712
2951
  data?: Partial<TData>,
2713
- route?: Route<TContext, TData>,
2952
+ flow?: Flow<TContext, TData>,
2714
2953
  step?: Step<TContext, TData>
2715
2954
  ): Promise<void> {
2716
2955
  if (!prepareOrFinalize) return;
@@ -2726,7 +2965,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2726
2965
  // Tool ID - use ToolManager for unified resolution
2727
2966
  const toolManager = this.getToolManager();
2728
2967
  if (toolManager) {
2729
- tool = toolManager.find(prepareOrFinalize, undefined, step, route);
2968
+ tool = toolManager.find(prepareOrFinalize, undefined, step, flow);
2730
2969
  } else {
2731
2970
  // Fallback to legacy resolution if ToolManager not available
2732
2971
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for prepare/finalize: ${prepareOrFinalize}`);
@@ -2738,9 +2977,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2738
2977
  availableTools.set(t.id, t);
2739
2978
  });
2740
2979
 
2741
- // Add route-level tools
2742
- if (route) {
2743
- route.getTools().forEach((t: Tool<TContext, TData>) => {
2980
+ // Add flow-level tools
2981
+ if (flow) {
2982
+ flow.getTools().forEach((t: Tool<TContext, TData>) => {
2744
2983
  availableTools.set(t.id, t);
2745
2984
  });
2746
2985
  }
@@ -2799,30 +3038,4 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2799
3038
  }
2800
3039
  }
2801
3040
 
2802
- /**
2803
- * Merge terms with route-specific taking precedence on conflicts
2804
- * @private
2805
- */
2806
- private mergeTerms(
2807
- agentTerms: Term<TContext, TData>[],
2808
- routeTerms: Term<TContext, TData>[]
2809
- ): Term<TContext, TData>[] {
2810
- const merged = new Map<string, Term<TContext, TData>>();
2811
-
2812
- // Add agent terms first
2813
- agentTerms.forEach((term) => {
2814
- const name =
2815
- typeof term.name === "string" ? term.name : term.name.toString();
2816
- merged.set(name, term);
2817
- });
2818
-
2819
- // Add route terms (these take precedence)
2820
- routeTerms.forEach((term) => {
2821
- const name =
2822
- typeof term.name === "string" ? term.name : term.name.toString();
2823
- merged.set(name, term);
2824
- });
2825
-
2826
- return Array.from(merged.values());
2827
- }
2828
3041
  }