@falai/agent 1.2.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +40 -886
  2. package/dist/adapters/MemoryAdapter.js +2 -2
  3. package/dist/adapters/MemoryAdapter.js.map +1 -1
  4. package/dist/adapters/MongoAdapter.js +2 -2
  5. package/dist/adapters/MongoAdapter.js.map +1 -1
  6. package/dist/adapters/OpenSearchAdapter.d.ts.map +1 -1
  7. package/dist/adapters/OpenSearchAdapter.js +9 -7
  8. package/dist/adapters/OpenSearchAdapter.js.map +1 -1
  9. package/dist/adapters/PostgreSQLAdapter.d.ts +14 -0
  10. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  11. package/dist/adapters/PostgreSQLAdapter.js +25 -9
  12. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  13. package/dist/adapters/PrismaAdapter.js +5 -5
  14. package/dist/adapters/PrismaAdapter.js.map +1 -1
  15. package/dist/adapters/RedisAdapter.js +2 -2
  16. package/dist/adapters/RedisAdapter.js.map +1 -1
  17. package/dist/adapters/SQLiteAdapter.d.ts +17 -0
  18. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  19. package/dist/adapters/SQLiteAdapter.js +30 -11
  20. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  21. package/dist/cjs/adapters/MemoryAdapter.js +2 -2
  22. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/MongoAdapter.js +2 -2
  24. package/dist/cjs/adapters/MongoAdapter.js.map +1 -1
  25. package/dist/cjs/adapters/OpenSearchAdapter.d.ts.map +1 -1
  26. package/dist/cjs/adapters/OpenSearchAdapter.js +9 -7
  27. package/dist/cjs/adapters/OpenSearchAdapter.js.map +1 -1
  28. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts +14 -0
  29. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  30. package/dist/cjs/adapters/PostgreSQLAdapter.js +25 -9
  31. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  32. package/dist/cjs/adapters/PrismaAdapter.js +5 -5
  33. package/dist/cjs/adapters/PrismaAdapter.js.map +1 -1
  34. package/dist/cjs/adapters/RedisAdapter.js +2 -2
  35. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  36. package/dist/cjs/adapters/SQLiteAdapter.d.ts +17 -0
  37. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  38. package/dist/cjs/adapters/SQLiteAdapter.js +30 -11
  39. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  40. package/dist/cjs/constants/index.d.ts +0 -9
  41. package/dist/cjs/constants/index.d.ts.map +1 -1
  42. package/dist/cjs/constants/index.js +2 -11
  43. package/dist/cjs/constants/index.js.map +1 -1
  44. package/dist/cjs/core/Agent.d.ts +119 -153
  45. package/dist/cjs/core/Agent.d.ts.map +1 -1
  46. package/dist/cjs/core/Agent.js +471 -324
  47. package/dist/cjs/core/Agent.js.map +1 -1
  48. package/dist/cjs/core/AutoChainExecutor.d.ts +107 -0
  49. package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -0
  50. package/dist/cjs/core/AutoChainExecutor.js +297 -0
  51. package/dist/cjs/core/AutoChainExecutor.js.map +1 -0
  52. package/dist/cjs/core/BranchEvaluator.d.ts +54 -0
  53. package/dist/cjs/core/BranchEvaluator.d.ts.map +1 -0
  54. package/dist/cjs/core/BranchEvaluator.js +130 -0
  55. package/dist/cjs/core/BranchEvaluator.js.map +1 -0
  56. package/dist/cjs/core/DirectiveBus.d.ts +88 -0
  57. package/dist/cjs/core/DirectiveBus.d.ts.map +1 -0
  58. package/dist/cjs/core/DirectiveBus.js +196 -0
  59. package/dist/cjs/core/DirectiveBus.js.map +1 -0
  60. package/dist/cjs/core/DirectiveChainTracker.d.ts +49 -0
  61. package/dist/cjs/core/DirectiveChainTracker.d.ts.map +1 -0
  62. package/dist/cjs/core/DirectiveChainTracker.js +121 -0
  63. package/dist/cjs/core/DirectiveChainTracker.js.map +1 -0
  64. package/dist/cjs/core/Flow.d.ts +186 -0
  65. package/dist/cjs/core/Flow.d.ts.map +1 -0
  66. package/dist/cjs/core/Flow.js +550 -0
  67. package/dist/cjs/core/Flow.js.map +1 -0
  68. package/dist/cjs/core/FlowRouter.d.ts +182 -0
  69. package/dist/cjs/core/FlowRouter.d.ts.map +1 -0
  70. package/dist/cjs/core/{RoutingEngine.js → FlowRouter.js} +323 -306
  71. package/dist/cjs/core/FlowRouter.js.map +1 -0
  72. package/dist/cjs/core/PersistenceManager.d.ts +2 -2
  73. package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
  74. package/dist/cjs/core/PersistenceManager.js +7 -7
  75. package/dist/cjs/core/PersistenceManager.js.map +1 -1
  76. package/dist/cjs/core/PromptComposer.d.ts +21 -8
  77. package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
  78. package/dist/cjs/core/PromptComposer.js +182 -105
  79. package/dist/cjs/core/PromptComposer.js.map +1 -1
  80. package/dist/cjs/core/PromptSectionCache.d.ts +1 -1
  81. package/dist/cjs/core/PromptSectionCache.js +1 -1
  82. package/dist/cjs/core/ResponseEngine.d.ts +18 -8
  83. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  84. package/dist/cjs/core/ResponseEngine.js +38 -36
  85. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  86. package/dist/cjs/core/ResponseModal.d.ts +73 -56
  87. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  88. package/dist/cjs/core/ResponseModal.js +1196 -1015
  89. package/dist/cjs/core/ResponseModal.js.map +1 -1
  90. package/dist/cjs/core/ResponsePipeline.d.ts +124 -26
  91. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  92. package/dist/cjs/core/ResponsePipeline.js +524 -134
  93. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  94. package/dist/cjs/core/SignalEvaluator.d.ts +86 -0
  95. package/dist/cjs/core/SignalEvaluator.d.ts.map +1 -0
  96. package/dist/cjs/core/SignalEvaluator.js +333 -0
  97. package/dist/cjs/core/SignalEvaluator.js.map +1 -0
  98. package/dist/cjs/core/SignalProcessor.d.ts +152 -0
  99. package/dist/cjs/core/SignalProcessor.d.ts.map +1 -0
  100. package/dist/cjs/core/SignalProcessor.js +562 -0
  101. package/dist/cjs/core/SignalProcessor.js.map +1 -0
  102. package/dist/cjs/core/Step.d.ts +43 -32
  103. package/dist/cjs/core/Step.d.ts.map +1 -1
  104. package/dist/cjs/core/Step.js +221 -126
  105. package/dist/cjs/core/Step.js.map +1 -1
  106. package/dist/cjs/core/StreamingToolExecutor.d.ts +2 -2
  107. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
  108. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
  109. package/dist/cjs/core/ToolManager.d.ts +44 -13
  110. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  111. package/dist/cjs/core/ToolManager.js +174 -91
  112. package/dist/cjs/core/ToolManager.js.map +1 -1
  113. package/dist/cjs/core/createAgent.d.ts +35 -0
  114. package/dist/cjs/core/createAgent.d.ts.map +1 -0
  115. package/dist/cjs/core/createAgent.js +39 -0
  116. package/dist/cjs/core/createAgent.js.map +1 -0
  117. package/dist/cjs/core/flow-namespace.d.ts +49 -0
  118. package/dist/cjs/core/flow-namespace.d.ts.map +1 -0
  119. package/dist/cjs/core/flow-namespace.js +171 -0
  120. package/dist/cjs/core/flow-namespace.js.map +1 -0
  121. package/dist/cjs/index.d.ts +11 -14
  122. package/dist/cjs/index.d.ts.map +1 -1
  123. package/dist/cjs/index.js +18 -22
  124. package/dist/cjs/index.js.map +1 -1
  125. package/dist/cjs/providers/GeminiProvider.d.ts +3 -3
  126. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  127. package/dist/cjs/providers/GeminiProvider.js +16 -14
  128. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  129. package/dist/cjs/types/agent.d.ts +183 -54
  130. package/dist/cjs/types/agent.d.ts.map +1 -1
  131. package/dist/cjs/types/agent.js +0 -6
  132. package/dist/cjs/types/agent.js.map +1 -1
  133. package/dist/cjs/types/ai.d.ts +3 -3
  134. package/dist/cjs/types/ai.d.ts.map +1 -1
  135. package/dist/cjs/types/errors.d.ts +15 -0
  136. package/dist/cjs/types/errors.d.ts.map +1 -0
  137. package/dist/cjs/types/errors.js +22 -0
  138. package/dist/cjs/types/errors.js.map +1 -0
  139. package/dist/cjs/types/flow.d.ts +513 -0
  140. package/dist/cjs/types/flow.d.ts.map +1 -0
  141. package/dist/cjs/types/{route.js → flow.js} +2 -2
  142. package/dist/cjs/types/flow.js.map +1 -0
  143. package/dist/cjs/types/index.d.ts +7 -6
  144. package/dist/cjs/types/index.d.ts.map +1 -1
  145. package/dist/cjs/types/index.js +6 -2
  146. package/dist/cjs/types/index.js.map +1 -1
  147. package/dist/cjs/types/persistence.d.ts +11 -7
  148. package/dist/cjs/types/persistence.d.ts.map +1 -1
  149. package/dist/cjs/types/routing.d.ts +1 -1
  150. package/dist/cjs/types/routing.d.ts.map +1 -1
  151. package/dist/cjs/types/session.d.ts +24 -23
  152. package/dist/cjs/types/session.d.ts.map +1 -1
  153. package/dist/cjs/types/signals.d.ts +248 -0
  154. package/dist/cjs/types/signals.d.ts.map +1 -0
  155. package/dist/cjs/types/signals.js +11 -0
  156. package/dist/cjs/types/signals.js.map +1 -0
  157. package/dist/cjs/types/template.d.ts +2 -8
  158. package/dist/cjs/types/template.d.ts.map +1 -1
  159. package/dist/cjs/types/tool.d.ts +36 -29
  160. package/dist/cjs/types/tool.d.ts.map +1 -1
  161. package/dist/cjs/types/tool.js +1 -1
  162. package/dist/cjs/types/tool.js.map +1 -1
  163. package/dist/cjs/utils/condition.d.ts +7 -1
  164. package/dist/cjs/utils/condition.d.ts.map +1 -1
  165. package/dist/cjs/utils/condition.js.map +1 -1
  166. package/dist/cjs/utils/id.d.ts +13 -5
  167. package/dist/cjs/utils/id.d.ts.map +1 -1
  168. package/dist/cjs/utils/id.js +24 -10
  169. package/dist/cjs/utils/id.js.map +1 -1
  170. package/dist/cjs/utils/index.d.ts +2 -2
  171. package/dist/cjs/utils/index.d.ts.map +1 -1
  172. package/dist/cjs/utils/index.js +7 -3
  173. package/dist/cjs/utils/index.js.map +1 -1
  174. package/dist/cjs/utils/session.d.ts +44 -5
  175. package/dist/cjs/utils/session.d.ts.map +1 -1
  176. package/dist/cjs/utils/session.js +197 -38
  177. package/dist/cjs/utils/session.js.map +1 -1
  178. package/dist/constants/index.d.ts +0 -9
  179. package/dist/constants/index.d.ts.map +1 -1
  180. package/dist/constants/index.js +3 -9
  181. package/dist/constants/index.js.map +1 -1
  182. package/dist/core/Agent.d.ts +119 -153
  183. package/dist/core/Agent.d.ts.map +1 -1
  184. package/dist/core/Agent.js +472 -325
  185. package/dist/core/Agent.js.map +1 -1
  186. package/dist/core/AutoChainExecutor.d.ts +107 -0
  187. package/dist/core/AutoChainExecutor.d.ts.map +1 -0
  188. package/dist/core/AutoChainExecutor.js +293 -0
  189. package/dist/core/AutoChainExecutor.js.map +1 -0
  190. package/dist/core/BranchEvaluator.d.ts +54 -0
  191. package/dist/core/BranchEvaluator.d.ts.map +1 -0
  192. package/dist/core/BranchEvaluator.js +126 -0
  193. package/dist/core/BranchEvaluator.js.map +1 -0
  194. package/dist/core/DirectiveBus.d.ts +88 -0
  195. package/dist/core/DirectiveBus.d.ts.map +1 -0
  196. package/dist/core/DirectiveBus.js +192 -0
  197. package/dist/core/DirectiveBus.js.map +1 -0
  198. package/dist/core/DirectiveChainTracker.d.ts +49 -0
  199. package/dist/core/DirectiveChainTracker.d.ts.map +1 -0
  200. package/dist/core/DirectiveChainTracker.js +117 -0
  201. package/dist/core/DirectiveChainTracker.js.map +1 -0
  202. package/dist/core/Flow.d.ts +186 -0
  203. package/dist/core/Flow.d.ts.map +1 -0
  204. package/dist/core/Flow.js +546 -0
  205. package/dist/core/Flow.js.map +1 -0
  206. package/dist/core/FlowRouter.d.ts +182 -0
  207. package/dist/core/FlowRouter.d.ts.map +1 -0
  208. package/dist/core/{RoutingEngine.js → FlowRouter.js} +322 -305
  209. package/dist/core/FlowRouter.js.map +1 -0
  210. package/dist/core/PersistenceManager.d.ts +2 -2
  211. package/dist/core/PersistenceManager.d.ts.map +1 -1
  212. package/dist/core/PersistenceManager.js +7 -7
  213. package/dist/core/PersistenceManager.js.map +1 -1
  214. package/dist/core/PromptComposer.d.ts +21 -8
  215. package/dist/core/PromptComposer.d.ts.map +1 -1
  216. package/dist/core/PromptComposer.js +183 -106
  217. package/dist/core/PromptComposer.js.map +1 -1
  218. package/dist/core/PromptSectionCache.d.ts +1 -1
  219. package/dist/core/PromptSectionCache.js +1 -1
  220. package/dist/core/ResponseEngine.d.ts +18 -8
  221. package/dist/core/ResponseEngine.d.ts.map +1 -1
  222. package/dist/core/ResponseEngine.js +38 -36
  223. package/dist/core/ResponseEngine.js.map +1 -1
  224. package/dist/core/ResponseModal.d.ts +73 -56
  225. package/dist/core/ResponseModal.d.ts.map +1 -1
  226. package/dist/core/ResponseModal.js +1198 -1017
  227. package/dist/core/ResponseModal.js.map +1 -1
  228. package/dist/core/ResponsePipeline.d.ts +124 -26
  229. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  230. package/dist/core/ResponsePipeline.js +524 -135
  231. package/dist/core/ResponsePipeline.js.map +1 -1
  232. package/dist/core/SignalEvaluator.d.ts +86 -0
  233. package/dist/core/SignalEvaluator.d.ts.map +1 -0
  234. package/dist/core/SignalEvaluator.js +326 -0
  235. package/dist/core/SignalEvaluator.js.map +1 -0
  236. package/dist/core/SignalProcessor.d.ts +152 -0
  237. package/dist/core/SignalProcessor.d.ts.map +1 -0
  238. package/dist/core/SignalProcessor.js +555 -0
  239. package/dist/core/SignalProcessor.js.map +1 -0
  240. package/dist/core/Step.d.ts +43 -32
  241. package/dist/core/Step.d.ts.map +1 -1
  242. package/dist/core/Step.js +220 -126
  243. package/dist/core/Step.js.map +1 -1
  244. package/dist/core/StreamingToolExecutor.d.ts +2 -2
  245. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  246. package/dist/core/StreamingToolExecutor.js.map +1 -1
  247. package/dist/core/ToolManager.d.ts +44 -13
  248. package/dist/core/ToolManager.d.ts.map +1 -1
  249. package/dist/core/ToolManager.js +174 -91
  250. package/dist/core/ToolManager.js.map +1 -1
  251. package/dist/core/createAgent.d.ts +35 -0
  252. package/dist/core/createAgent.d.ts.map +1 -0
  253. package/dist/core/createAgent.js +36 -0
  254. package/dist/core/createAgent.js.map +1 -0
  255. package/dist/core/flow-namespace.d.ts +49 -0
  256. package/dist/core/flow-namespace.d.ts.map +1 -0
  257. package/dist/core/flow-namespace.js +168 -0
  258. package/dist/core/flow-namespace.js.map +1 -0
  259. package/dist/index.d.ts +11 -14
  260. package/dist/index.d.ts.map +1 -1
  261. package/dist/index.js +9 -12
  262. package/dist/index.js.map +1 -1
  263. package/dist/providers/GeminiProvider.d.ts +3 -3
  264. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  265. package/dist/providers/GeminiProvider.js +16 -14
  266. package/dist/providers/GeminiProvider.js.map +1 -1
  267. package/dist/types/agent.d.ts +183 -54
  268. package/dist/types/agent.d.ts.map +1 -1
  269. package/dist/types/agent.js +0 -6
  270. package/dist/types/agent.js.map +1 -1
  271. package/dist/types/ai.d.ts +3 -3
  272. package/dist/types/ai.d.ts.map +1 -1
  273. package/dist/types/errors.d.ts +15 -0
  274. package/dist/types/errors.d.ts.map +1 -0
  275. package/dist/types/errors.js +18 -0
  276. package/dist/types/errors.js.map +1 -0
  277. package/dist/types/flow.d.ts +513 -0
  278. package/dist/types/flow.d.ts.map +1 -0
  279. package/dist/types/flow.js +5 -0
  280. package/dist/types/flow.js.map +1 -0
  281. package/dist/types/index.d.ts +7 -6
  282. package/dist/types/index.d.ts.map +1 -1
  283. package/dist/types/index.js +4 -1
  284. package/dist/types/index.js.map +1 -1
  285. package/dist/types/persistence.d.ts +11 -7
  286. package/dist/types/persistence.d.ts.map +1 -1
  287. package/dist/types/routing.d.ts +1 -1
  288. package/dist/types/routing.d.ts.map +1 -1
  289. package/dist/types/session.d.ts +24 -23
  290. package/dist/types/session.d.ts.map +1 -1
  291. package/dist/types/signals.d.ts +248 -0
  292. package/dist/types/signals.d.ts.map +1 -0
  293. package/dist/types/signals.js +10 -0
  294. package/dist/types/signals.js.map +1 -0
  295. package/dist/types/template.d.ts +2 -8
  296. package/dist/types/template.d.ts.map +1 -1
  297. package/dist/types/tool.d.ts +36 -29
  298. package/dist/types/tool.d.ts.map +1 -1
  299. package/dist/types/tool.js +1 -1
  300. package/dist/types/tool.js.map +1 -1
  301. package/dist/utils/condition.d.ts +7 -1
  302. package/dist/utils/condition.d.ts.map +1 -1
  303. package/dist/utils/condition.js.map +1 -1
  304. package/dist/utils/id.d.ts +13 -5
  305. package/dist/utils/id.d.ts.map +1 -1
  306. package/dist/utils/id.js +22 -9
  307. package/dist/utils/id.js.map +1 -1
  308. package/dist/utils/index.d.ts +2 -2
  309. package/dist/utils/index.d.ts.map +1 -1
  310. package/dist/utils/index.js +2 -2
  311. package/dist/utils/index.js.map +1 -1
  312. package/dist/utils/session.d.ts +44 -5
  313. package/dist/utils/session.d.ts.map +1 -1
  314. package/dist/utils/session.js +193 -37
  315. package/dist/utils/session.js.map +1 -1
  316. package/docs/README.md +15 -202
  317. package/docs/concepts/architecture.md +281 -0
  318. package/docs/concepts/directives.md +400 -0
  319. package/docs/concepts/pipeline.md +399 -0
  320. package/docs/guides/branching.md +263 -0
  321. package/docs/guides/compaction.md +163 -0
  322. package/docs/guides/conditions.md +167 -0
  323. package/docs/guides/error-handling.md +176 -0
  324. package/docs/guides/flow-control.md +409 -0
  325. package/docs/guides/instructions.md +210 -0
  326. package/docs/guides/persistence.md +182 -0
  327. package/docs/guides/streaming.md +137 -0
  328. package/docs/migration/README.md +15 -0
  329. package/docs/migration/route-to-flow.md +560 -0
  330. package/docs/migration/v1-to-v2.md +909 -0
  331. package/docs/reference/adapters.md +481 -0
  332. package/docs/reference/branches.md +241 -0
  333. package/docs/reference/create-agent.md +186 -0
  334. package/docs/reference/directive.md +243 -0
  335. package/docs/reference/errors.md +122 -0
  336. package/docs/reference/flow.md +238 -0
  337. package/docs/reference/instruction.md +177 -0
  338. package/docs/reference/pre-directive.md +131 -0
  339. package/docs/reference/providers.md +227 -0
  340. package/docs/reference/signals.md +356 -0
  341. package/docs/reference/step.md +339 -0
  342. package/docs/reference/tool.md +269 -0
  343. package/docs/start/01-install.md +81 -0
  344. package/docs/start/02-first-agent.md +196 -0
  345. package/docs/start/03-collect-data.md +222 -0
  346. package/docs/start/04-add-tools.md +276 -0
  347. package/docs/start/05-go-to-production.md +216 -0
  348. package/examples/01-quickstart.ts +20 -0
  349. package/examples/02-data-extraction.ts +90 -0
  350. package/examples/03-tools.ts +136 -0
  351. package/examples/04-instructions.ts +100 -0
  352. package/examples/05-branching.ts +140 -0
  353. package/examples/06-flow-control.ts +103 -0
  354. package/examples/07-streaming.ts +69 -0
  355. package/examples/08-persistence.ts +98 -0
  356. package/examples/09-signals.ts +144 -0
  357. package/examples/tsconfig.json +30 -0
  358. package/package.json +2 -1
  359. package/src/adapters/MemoryAdapter.ts +3 -3
  360. package/src/adapters/MongoAdapter.ts +3 -3
  361. package/src/adapters/OpenSearchAdapter.ts +10 -8
  362. package/src/adapters/PostgreSQLAdapter.ts +26 -10
  363. package/src/adapters/PrismaAdapter.ts +6 -6
  364. package/src/adapters/RedisAdapter.ts +3 -3
  365. package/src/adapters/SQLiteAdapter.ts +31 -12
  366. package/src/constants/index.ts +2 -10
  367. package/src/core/Agent.ts +585 -374
  368. package/src/core/AutoChainExecutor.ts +440 -0
  369. package/src/core/BranchEvaluator.ts +167 -0
  370. package/src/core/DirectiveBus.ts +248 -0
  371. package/src/core/DirectiveChainTracker.ts +144 -0
  372. package/src/core/Flow.ts +666 -0
  373. package/src/core/{RoutingEngine.ts → FlowRouter.ts} +385 -365
  374. package/src/core/PersistenceManager.ts +8 -8
  375. package/src/core/PromptComposer.ts +209 -140
  376. package/src/core/PromptSectionCache.ts +1 -1
  377. package/src/core/ResponseEngine.ts +61 -46
  378. package/src/core/ResponseModal.ts +1458 -1241
  379. package/src/core/ResponsePipeline.ts +675 -173
  380. package/src/core/SignalEvaluator.ts +420 -0
  381. package/src/core/SignalProcessor.ts +723 -0
  382. package/src/core/Step.ts +279 -176
  383. package/src/core/StreamingToolExecutor.ts +4 -4
  384. package/src/core/ToolManager.ts +200 -97
  385. package/src/core/createAgent.ts +40 -0
  386. package/src/core/flow-namespace.ts +219 -0
  387. package/src/index.ts +42 -36
  388. package/src/providers/GeminiProvider.ts +17 -15
  389. package/src/types/agent.ts +182 -53
  390. package/src/types/ai.ts +3 -3
  391. package/src/types/errors.ts +18 -0
  392. package/src/types/flow.ts +590 -0
  393. package/src/types/index.ts +43 -16
  394. package/src/types/persistence.ts +12 -8
  395. package/src/types/routing.ts +1 -1
  396. package/src/types/session.ts +26 -23
  397. package/src/types/signals.ts +321 -0
  398. package/src/types/template.ts +3 -11
  399. package/src/types/tool.ts +50 -42
  400. package/src/utils/condition.ts +13 -4
  401. package/src/utils/id.ts +27 -9
  402. package/src/utils/index.ts +6 -2
  403. package/src/utils/session.ts +238 -42
  404. package/dist/cjs/core/BatchExecutor.d.ts +0 -359
  405. package/dist/cjs/core/BatchExecutor.d.ts.map +0 -1
  406. package/dist/cjs/core/BatchExecutor.js +0 -861
  407. package/dist/cjs/core/BatchExecutor.js.map +0 -1
  408. package/dist/cjs/core/BatchPromptBuilder.d.ts +0 -89
  409. package/dist/cjs/core/BatchPromptBuilder.d.ts.map +0 -1
  410. package/dist/cjs/core/BatchPromptBuilder.js +0 -223
  411. package/dist/cjs/core/BatchPromptBuilder.js.map +0 -1
  412. package/dist/cjs/core/Route.d.ts +0 -180
  413. package/dist/cjs/core/Route.d.ts.map +0 -1
  414. package/dist/cjs/core/Route.js +0 -542
  415. package/dist/cjs/core/Route.js.map +0 -1
  416. package/dist/cjs/core/RoutingEngine.d.ts +0 -185
  417. package/dist/cjs/core/RoutingEngine.d.ts.map +0 -1
  418. package/dist/cjs/core/RoutingEngine.js.map +0 -1
  419. package/dist/cjs/types/route.d.ts +0 -336
  420. package/dist/cjs/types/route.d.ts.map +0 -1
  421. package/dist/cjs/types/route.js.map +0 -1
  422. package/dist/core/BatchExecutor.d.ts +0 -359
  423. package/dist/core/BatchExecutor.d.ts.map +0 -1
  424. package/dist/core/BatchExecutor.js +0 -856
  425. package/dist/core/BatchExecutor.js.map +0 -1
  426. package/dist/core/BatchPromptBuilder.d.ts +0 -89
  427. package/dist/core/BatchPromptBuilder.d.ts.map +0 -1
  428. package/dist/core/BatchPromptBuilder.js +0 -219
  429. package/dist/core/BatchPromptBuilder.js.map +0 -1
  430. package/dist/core/Route.d.ts +0 -180
  431. package/dist/core/Route.d.ts.map +0 -1
  432. package/dist/core/Route.js +0 -538
  433. package/dist/core/Route.js.map +0 -1
  434. package/dist/core/RoutingEngine.d.ts +0 -185
  435. package/dist/core/RoutingEngine.d.ts.map +0 -1
  436. package/dist/core/RoutingEngine.js.map +0 -1
  437. package/dist/types/route.d.ts +0 -336
  438. package/dist/types/route.d.ts.map +0 -1
  439. package/dist/types/route.js +0 -5
  440. package/dist/types/route.js.map +0 -1
  441. package/docs/CONTRIBUTING.md +0 -521
  442. package/docs/api/README.md +0 -3299
  443. package/docs/api/overview.md +0 -1410
  444. package/docs/architecture/data-extraction-flow.md +0 -360
  445. package/docs/architecture/multi-step-execution.md +0 -277
  446. package/docs/core/agent/README.md +0 -938
  447. package/docs/core/agent/context-management.md +0 -796
  448. package/docs/core/agent/rules-and-prohibitions.md +0 -113
  449. package/docs/core/agent/session-management.md +0 -693
  450. package/docs/core/ai-integration/prompt-composition.md +0 -355
  451. package/docs/core/ai-integration/providers.md +0 -515
  452. package/docs/core/ai-integration/response-processing.md +0 -433
  453. package/docs/core/conversation-flows/data-collection.md +0 -772
  454. package/docs/core/conversation-flows/route-dsl.md +0 -509
  455. package/docs/core/conversation-flows/routes.md +0 -249
  456. package/docs/core/conversation-flows/step-transitions.md +0 -731
  457. package/docs/core/conversation-flows/steps.md +0 -268
  458. package/docs/core/error-handling.md +0 -830
  459. package/docs/core/persistence/adapters.md +0 -255
  460. package/docs/core/persistence/session-storage.md +0 -656
  461. package/docs/core/routing/intelligent-routing.md +0 -470
  462. package/docs/core/tools/enhanced-tool.md +0 -186
  463. package/docs/core/tools/streaming-execution.md +0 -161
  464. package/docs/core/tools/tool-definition.md +0 -970
  465. package/docs/core/tools/tool-scoping.md +0 -819
  466. package/docs/guides/advanced-patterns/publishing.md +0 -186
  467. package/docs/guides/context-compaction.md +0 -96
  468. package/docs/guides/error-handling-patterns.md +0 -578
  469. package/docs/guides/getting-started/README.md +0 -795
  470. package/docs/guides/migration/README.md +0 -101
  471. package/docs/guides/migration/flexible-routing-conditions.md +0 -375
  472. package/docs/guides/migration/multi-step-execution.md +0 -393
  473. package/docs/guides/migration/response-modal-refactor.md +0 -518
  474. package/docs/guides/prompt-optimization.md +0 -164
  475. package/examples/advanced-patterns/context-compaction.ts +0 -223
  476. package/examples/advanced-patterns/knowledge-based-agent.ts +0 -735
  477. package/examples/advanced-patterns/persistent-onboarding.ts +0 -728
  478. package/examples/advanced-patterns/route-lifecycle-hooks.ts +0 -556
  479. package/examples/advanced-patterns/streaming-responses.ts +0 -656
  480. package/examples/ai-providers/anthropic-integration.ts +0 -388
  481. package/examples/ai-providers/openai-integration.ts +0 -228
  482. package/examples/condition-patterns/function-only-conditions.ts +0 -365
  483. package/examples/condition-patterns/mixed-array-conditions.ts +0 -477
  484. package/examples/condition-patterns/route-skipif-patterns.ts +0 -468
  485. package/examples/condition-patterns/step-skipif-patterns.ts +0 -0
  486. package/examples/condition-patterns/string-only-conditions.ts +0 -296
  487. package/examples/conversation-flows/completion-transitions.ts +0 -318
  488. package/examples/core-concepts/basic-agent.ts +0 -503
  489. package/examples/core-concepts/modern-streaming-api.ts +0 -309
  490. package/examples/core-concepts/schema-driven-extraction.ts +0 -332
  491. package/examples/core-concepts/session-management.ts +0 -494
  492. package/examples/integrations/database-integration.ts +0 -631
  493. package/examples/integrations/healthcare-integration.ts +0 -595
  494. package/examples/integrations/search-integration.ts +0 -530
  495. package/examples/integrations/server-session-management.ts +0 -307
  496. package/examples/persistence/custom-adapter.ts +0 -526
  497. package/examples/persistence/database-persistence.ts +0 -583
  498. package/examples/persistence/memory-sessions.ts +0 -495
  499. package/examples/persistence/prisma-schema.example.prisma +0 -74
  500. package/examples/persistence/redis-persistence.ts +0 -488
  501. package/examples/tools/basic-tools.ts +0 -765
  502. package/examples/tools/data-enrichment-tools.ts +0 -593
  503. package/examples/tools/enhanced-tool-metadata.ts +0 -268
  504. package/examples/tools/streaming-tool-execution.ts +0 -283
  505. package/src/core/BatchExecutor.ts +0 -1187
  506. package/src/core/BatchPromptBuilder.ts +0 -299
  507. package/src/core/Route.ts +0 -678
  508. package/src/types/route.ts +0 -392
@@ -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
@@ -1802,15 +2133,17 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1802
2133
 
1803
2134
  // Map to store tool execution results for history
1804
2135
  const toolResultsMap = new Map<string, string>();
2136
+ // Map to store tool call arguments for history reconstruction
2137
+ const toolArgsMap = new Map<string, Record<string, unknown>>();
1805
2138
 
1806
2139
  // Execute initial dynamic tool calls
1807
2140
  if (toolCalls && toolCalls.length > 0) {
1808
2141
  logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
1809
2142
 
1810
2143
  for (const toolCall of toolCalls) {
1811
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
2144
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1812
2145
  if (!tool) {
1813
- 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.`);
1814
2147
  continue;
1815
2148
  }
1816
2149
 
@@ -1836,6 +2169,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1836
2169
 
1837
2170
  // Store the actual tool result data for history
1838
2171
  toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
2172
+ toolArgsMap.set(toolCall.toolName, toolCall.arguments);
1839
2173
 
1840
2174
  // Check if tool execution was successful
1841
2175
  if (!toolResult.success) {
@@ -1889,7 +2223,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1889
2223
  // Create tool result history items
1890
2224
  const toolResultHistoryItems: HistoryItem[] = [];
1891
2225
  for (const toolCall of toolCalls || []) {
1892
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
2226
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1893
2227
  if (tool) {
1894
2228
  // Create HistoryItem format for tool results
1895
2229
  // Add assistant message with tool_calls
@@ -1955,9 +2289,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1955
2289
 
1956
2290
  // Execute the follow-up tool calls
1957
2291
  for (const toolCall of followUpToolCalls!) {
1958
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
2292
+ const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
1959
2293
  if (!tool) {
1960
- 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.`);
1961
2295
  continue;
1962
2296
  }
1963
2297
 
@@ -2008,6 +2342,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2008
2342
 
2009
2343
  // Store the follow-up tool result for potential next loop iteration
2010
2344
  toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
2345
+ toolArgsMap.set(toolCall.toolName, toolCall.arguments);
2011
2346
 
2012
2347
  logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
2013
2348
  } catch (error) {
@@ -2029,7 +2364,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2029
2364
  }
2030
2365
 
2031
2366
  if (toolLoopCount >= MAX_TOOL_LOOPS) {
2032
- 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.`);
2033
2368
  }
2034
2369
 
2035
2370
  // If tools were executed but no final text message was produced,
@@ -2051,7 +2386,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2051
2386
  tool_calls: [{
2052
2387
  id: toolName,
2053
2388
  name: toolName,
2054
- arguments: {},
2389
+ arguments: toolArgsMap.get(toolName) || {},
2055
2390
  }],
2056
2391
  });
2057
2392
  finalToolResultHistoryItems.push({
@@ -2118,41 +2453,41 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2118
2453
  */
2119
2454
  private async collectDataFromResponse(params: {
2120
2455
  result: { structured?: AgentStructuredResponse };
2121
- selectedRoute?: Route<TContext, TData>;
2456
+ selectedFlow?: Flow<TContext, TData>;
2122
2457
  nextStep?: Step<TContext, TData>;
2123
2458
  session: SessionState<TData>;
2124
2459
  }): Promise<SessionState<TData>> {
2125
2460
  try {
2126
- const { result, selectedRoute, nextStep, session } = params;
2461
+ const { result, selectedFlow, nextStep, session } = params;
2127
2462
  let updatedSession = session;
2128
2463
 
2129
- // Extract collected data from final response (only for route-based interactions)
2130
- if (selectedRoute && result.structured) {
2464
+ // Extract collected data from final response (only for flow-based interactions)
2465
+ if (selectedFlow && result.structured) {
2131
2466
  try {
2132
2467
  const collectedData: Record<string, unknown> = {};
2133
2468
  // AgentStructuredResponse extends Record<string, unknown>, so we can safely access properties
2134
2469
  const structuredData = result.structured;
2135
2470
 
2136
- // Collect ALL route fields (required + optional) from structured response
2137
- const allRouteFields = new Set<string>();
2471
+ // Collect ALL flow fields (required + optional) from structured response
2472
+ const allFlowFields = new Set<string>();
2138
2473
 
2139
- // Add route required fields
2140
- if (selectedRoute.requiredFields) {
2141
- 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)));
2142
2477
  }
2143
2478
 
2144
- // Add route optional fields
2145
- if (selectedRoute.optionalFields) {
2146
- 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)));
2147
2482
  }
2148
2483
 
2149
- // 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)
2150
2485
  if (nextStep?.collect) {
2151
- nextStep.collect.forEach(field => allRouteFields.add(String(field)));
2486
+ nextStep.collect.forEach(field => allFlowFields.add(String(field)));
2152
2487
  }
2153
2488
 
2154
2489
  // Extract all available fields from structured response
2155
- for (const field of allRouteFields) {
2490
+ for (const field of allFlowFields) {
2156
2491
  const fieldKey = String(field);
2157
2492
  if (fieldKey in structuredData && structuredData[fieldKey] !== undefined && structuredData[fieldKey] !== null) {
2158
2493
  collectedData[fieldKey] = structuredData[fieldKey];
@@ -2203,240 +2538,147 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2203
2538
  }
2204
2539
 
2205
2540
  /**
2206
- * 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
+ *
2207
2563
  * @private
2208
2564
  */
2209
- private async handleRouteCompletion(params: {
2210
- selectedRoute: Route<TContext, TData>;
2565
+ private async applyFlowCompletion(params: {
2566
+ selectedFlow: Flow<TContext, TData>;
2211
2567
  session: SessionState<TData>;
2212
2568
  context: TContext;
2213
2569
  history: HistoryItem[];
2214
- historyEvents: Event[];
2215
- signal?: AbortSignal;
2216
- }): Promise<string> {
2217
- const { selectedRoute, session, context, history, historyEvents, signal } = params;
2218
-
2219
- // Get endStep spec from route
2220
- const endStepSpec = selectedRoute.endStepSpec;
2221
-
2222
- // Create a temporary step for completion message generation using endStep configuration
2223
- const completionStep = new Step<TContext, TData>(selectedRoute.id, {
2224
- description: endStepSpec.description,
2225
- id: endStepSpec.id || END_ROUTE_ID,
2226
- collect: endStepSpec.collect,
2227
- requires: endStepSpec.requires,
2228
- 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.",
2229
- });
2230
-
2231
- // Build response schema for completion (message only, no data collection)
2232
- const completionSchema = {
2233
- type: "object",
2234
- properties: {
2235
- message: {
2236
- type: "string",
2237
- description: "A natural, warm farewell message for the user. Must NOT contain task names, field names, collected data, or any internal/technical information.",
2238
- },
2239
- },
2240
- required: ["message"],
2241
- additionalProperties: false,
2242
- };
2243
-
2244
- const templateContext = createTemplateContext({ context, session, history: historyEvents });
2245
-
2246
- // Build completion response prompt using ResponseEngine
2247
- // Filter out conditional guidelines - only include always-active ones
2248
- const alwaysActiveGuidelines = [
2249
- ...this.agent.getGuidelines().filter(g => !g.condition),
2250
- ...selectedRoute.getGuidelines().filter(g => !g.condition),
2251
- ];
2252
- let completitionPrompt = "Send a brief, natural farewell message. Do NOT mention internal data or task details."
2253
- if (endStepSpec.prompt) {
2254
- completitionPrompt = await render(endStepSpec.prompt, templateContext)
2255
- }
2570
+ }): Promise<SessionState<TData>> {
2571
+ const { selectedFlow, session, context } = params;
2256
2572
 
2257
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
2258
- route: selectedRoute,
2259
- currentStep: completionStep,
2260
- rules: selectedRoute.getRules(),
2261
- prohibitions: selectedRoute.getProhibitions(),
2262
- directives: [
2263
- "The conversation task has been completed successfully",
2264
- "Generate a natural, friendly farewell message for the user",
2265
- "Do NOT mention task names, route names, collected data, field names, or any internal/technical information",
2266
- "Do NOT list or summarize the data you collected - the user already knows what they told you",
2267
- "Do NOT use words like 'tarefa', 'dados coletados', 'prospecção', 'concluída' or similar internal terms",
2268
- "Keep it brief, warm, and conversational - as if ending a natural conversation",
2269
- "Do NOT ask for more information - the conversation is ending",
2270
- completitionPrompt,
2271
- ],
2272
- history: historyEvents,
2273
- agentOptions: this.agent.getAgentOptions(),
2274
- combinedGuidelines: alwaysActiveGuidelines, // Only non-conditional guidelines
2275
- 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 },
2276
2576
  context,
2277
- session,
2278
- agentSchema: undefined, // No data collection schema for completion
2279
- });
2280
-
2281
- // Generate completion message using AI provider
2282
- const agentOptions = this.agent.getAgentOptions();
2283
- logger.debug(`[ResponseModal] Calling AI provider for completion message...`);
2577
+ );
2284
2578
 
2285
- const completionResult = await agentOptions.provider.generateMessage({
2286
- prompt: completionPrompt,
2287
- history, // Use HistoryItem[] for AI provider
2288
- context,
2289
- signal,
2290
- 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,
2291
2592
  });
2292
2593
 
2293
- logger.debug(`[ResponseModal] AI provider returned completion result`);
2294
- const message = completionResult.structured?.message || completionResult.message;
2295
- logger.debug(`[ResponseModal] Generated completion message for route: ${selectedRoute.title}`);
2296
-
2297
- // Check for onComplete transition
2298
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
2299
-
2594
+ // 3) Wire pendingDirective when onComplete returned a target.
2300
2595
  if (transitionConfig) {
2301
- // Find target route by ID or title
2302
- const targetRoute = this.agent.getRoutes().find(
2303
- (r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep
2304
- );
2305
-
2306
- if (targetRoute) {
2307
- const renderedCondition = await render(transitionConfig.condition, templateContext);
2308
- // Set pending transition in session
2309
- session.pendingTransition = {
2310
- targetRouteId: targetRoute.id,
2311
- condition: renderedCondition,
2312
- 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
+ },
2313
2612
  };
2314
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
2315
- } else {
2316
- 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
+ );
2317
2621
  }
2622
+ } else {
2623
+ logger.debug(
2624
+ `[ResponseModal] Flow ${selectedFlow.title} completed; session released to idle.`,
2625
+ );
2318
2626
  }
2319
2627
 
2320
- return message;
2628
+ return nextSession;
2321
2629
  }
2322
2630
 
2323
2631
  /**
2324
- * 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
+ *
2325
2646
  * @private
2326
2647
  */
2327
- private async *streamRouteCompletion(params: {
2328
- selectedRoute: Route<TContext, TData>;
2648
+ private async *streamFlowCompletion(params: {
2649
+ selectedFlow: Flow<TContext, TData>;
2329
2650
  session: SessionState<TData>;
2330
2651
  context: TContext;
2331
2652
  history: HistoryItem[];
2332
2653
  historyEvents: Event[];
2654
+ stoppedReason?: StoppedReason;
2333
2655
  signal?: AbortSignal;
2334
2656
  }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
2335
- const { selectedRoute, context, history, historyEvents, signal } = params;
2336
- let session = params.session;
2337
-
2338
- // Get endStep spec from route
2339
- const endStepSpec = selectedRoute.endStepSpec;
2340
-
2341
- // Create a temporary step for completion message generation using endStep configuration
2342
- const completionStep = new Step<TContext, TData>(selectedRoute.id, {
2343
- description: endStepSpec.description,
2344
- id: endStepSpec.id || END_ROUTE_ID,
2345
- collect: endStepSpec.collect,
2346
- requires: endStepSpec.requires,
2347
- 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.",
2348
- });
2657
+ const { selectedFlow, context, history } = params;
2349
2658
 
2350
- // Build response schema for completion
2351
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
2352
- const templateContext = createTemplateContext({ context, session, history: historyEvents }); // Use Event[] for template context
2353
-
2354
- // Build completion response prompt
2355
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
2356
- route: selectedRoute,
2357
- currentStep: completionStep,
2358
- rules: selectedRoute.getRules(),
2359
- prohibitions: selectedRoute.getProhibitions(),
2360
- directives: undefined, // No directives for completion
2361
- history: historyEvents,
2362
- agentOptions: this.agent.getAgentOptions(),
2363
- combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
2364
- combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
2365
- context,
2366
- session,
2367
- agentSchema: this.agent.getSchema(),
2368
- });
2369
-
2370
- // Stream completion message using AI provider
2371
- const agentOptions = this.agent.getAgentOptions();
2372
- const stream = agentOptions.provider.generateMessageStream({
2373
- prompt: completionPrompt,
2374
- history, // Use HistoryItem[] for AI provider
2659
+ const session = await this.applyFlowCompletion({
2660
+ selectedFlow,
2661
+ session: params.session,
2375
2662
  context,
2376
- signal,
2377
- parameters: { jsonSchema: responseSchema, schemaName: "completion_message_stream" },
2663
+ history,
2378
2664
  });
2379
2665
 
2380
- logger.debug(`[ResponseModal] Streaming completion message for route: ${selectedRoute.title}`);
2381
-
2382
- // Check for onComplete transition
2383
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
2384
-
2385
- if (transitionConfig) {
2386
- // Find target route by ID or title
2387
- const targetRoute = this.agent.getRoutes().find(
2388
- (r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep
2389
- );
2390
-
2391
- if (targetRoute) {
2392
- const renderedCondition = await render(transitionConfig.condition, templateContext);
2393
- // Set pending transition in session
2394
- session = {
2395
- ...session,
2396
- pendingTransition: {
2397
- targetRouteId: targetRoute.id,
2398
- condition: renderedCondition,
2399
- reason: "route_complete",
2400
- },
2401
- };
2402
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
2403
- } else {
2404
- logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
2405
- }
2406
- }
2407
-
2408
- // Set step to END_ROUTE marker
2409
- session = enterStep(session, END_ROUTE_ID, "Route completed");
2410
- logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
2411
-
2412
- // Stream completion chunks
2413
- for await (const chunk of stream) {
2414
- // Update current session if we have one
2415
- if (chunk.done) {
2416
- await this.finalizeSession(session, context);
2417
- }
2666
+ await this.finalizeSession(session, context);
2418
2667
 
2419
- // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2420
- // - executedSteps: empty for route completion (no new steps executed)
2421
- // - stoppedReason: 'route_complete' for completed routes
2422
- // - session.currentStep: set to END_ROUTE
2423
- yield {
2424
- delta: chunk.delta,
2425
- accumulated: chunk.accumulated,
2426
- done: chunk.done,
2427
- session,
2428
- toolCalls: undefined,
2429
- isRouteComplete: true,
2430
- executedSteps: chunk.done ? [] : undefined,
2431
- stoppedReason: chunk.done ? 'route_complete' : undefined,
2432
- metadata: chunk.metadata,
2433
- structured: chunk.structured,
2434
- };
2435
- }
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
+ };
2436
2678
  }
2437
2679
 
2438
2680
  /**
2439
- * Generate fallback response when no routes are available
2681
+ * Generate fallback response when no flows are available
2440
2682
  * @private
2441
2683
  */
2442
2684
  private async generateFallbackResponse(params: {
@@ -2444,16 +2686,16 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2444
2686
  context: TContext;
2445
2687
  session: SessionState<TData>;
2446
2688
  signal?: AbortSignal;
2447
- }): Promise<string> {
2689
+ }): Promise<{ message: string; appliedInstructions?: AppliedInstruction[] }> {
2448
2690
  const { history, context, session, signal } = params;
2449
2691
 
2450
- logger.debug(`[ResponseModal] No route selected, generating basic response`);
2692
+ logger.debug(`[ResponseModal] No flow selected, generating basic response`);
2451
2693
 
2452
- // Build basic response prompt without route context
2453
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
2694
+ // Build basic response prompt without flow context
2695
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
2454
2696
  agentOptions: this.agent.getAgentOptions(),
2455
2697
  terms: this.agent.getTerms(),
2456
- guidelines: this.agent.getGuidelines(),
2698
+ instructions: this.collectScopedInstructions(),
2457
2699
  context,
2458
2700
  session,
2459
2701
  });
@@ -2475,11 +2717,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2475
2717
  },
2476
2718
  });
2477
2719
 
2478
- return result.structured?.message || result.message;
2720
+ return { message: result.structured?.message || result.message, appliedInstructions };
2479
2721
  }
2480
2722
 
2481
2723
  /**
2482
- * Stream fallback response when no routes are available
2724
+ * Stream fallback response when no flows are available
2483
2725
  * @private
2484
2726
  */
2485
2727
  private async *streamFallbackResponse(params: {
@@ -2490,10 +2732,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2490
2732
  }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
2491
2733
  const { history, context, session, signal } = params;
2492
2734
 
2493
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
2735
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
2494
2736
  agentOptions: this.agent.getAgentOptions(),
2495
2737
  terms: this.agent.getTerms(),
2496
- guidelines: this.agent.getGuidelines(),
2738
+ instructions: this.collectScopedInstructions(),
2497
2739
  context,
2498
2740
  session,
2499
2741
  });
@@ -2522,8 +2764,8 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2522
2764
  }
2523
2765
 
2524
2766
  // Response structure completeness (Requirement 8.1, 8.2, 8.3)
2525
- // - executedSteps: empty for fallback (no route/step execution)
2526
- // - stoppedReason: undefined for fallback (no route context)
2767
+ // - executedSteps: empty for fallback (no flow/step execution)
2768
+ // - stoppedReason: undefined for fallback (no flow context)
2527
2769
  // - session.currentStep: unchanged (no step progression)
2528
2770
  yield {
2529
2771
  delta: chunk.delta,
@@ -2531,11 +2773,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2531
2773
  done: chunk.done,
2532
2774
  session,
2533
2775
  toolCalls: undefined,
2534
- isRouteComplete: false,
2776
+ isFlowComplete: false,
2535
2777
  executedSteps: chunk.done ? [] : undefined,
2536
2778
  stoppedReason: undefined,
2537
2779
  metadata: chunk.metadata,
2538
2780
  structured: chunk.structured,
2781
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
2539
2782
  };
2540
2783
  }
2541
2784
  }
@@ -2561,9 +2804,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2561
2804
  await this.executeStepFinalize(session, context);
2562
2805
 
2563
2806
  // Update current session if we have one
2564
- const currentSession = this.agent.getCurrentSession();
2807
+ const currentSession = this.agent.currentSession;
2565
2808
  if (currentSession) {
2566
- this.agent.setCurrentSession(session);
2809
+ this.agent.currentSession = session;
2567
2810
  }
2568
2811
  }
2569
2812
  // ============================================================================
@@ -2572,45 +2815,45 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2572
2815
 
2573
2816
 
2574
2817
  /**
2575
- * 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
2576
2819
  * Delegates to ToolManager for unified tool resolution
2577
2820
  * @private
2578
2821
  */
2579
2822
  private findAvailableTool(
2580
2823
  toolName: string,
2581
- route?: Route<TContext, TData>
2824
+ flow?: Flow<TContext, TData>
2582
2825
  ): Tool<TContext, TData> | undefined {
2583
2826
  // Use ToolManager for unified tool resolution
2584
2827
  const toolManager = this.getToolManager();
2585
2828
  if (toolManager) {
2586
- return toolManager.find(toolName, undefined, undefined, route);
2829
+ return toolManager.find(toolName, undefined, undefined, flow);
2587
2830
  }
2588
2831
 
2589
2832
  // Fallback to legacy resolution if ToolManager not available
2590
2833
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for: ${toolName}`);
2591
2834
 
2592
- // Check route-level tools first (if route provided)
2593
- if (route) {
2594
- const routeTool = route
2835
+ // Check flow-level tools first (if flow provided)
2836
+ if (flow) {
2837
+ const flowTool = flow
2595
2838
  .getTools()
2596
- .find((tool: Tool<TContext, TData>) => tool.id === toolName || tool.name === toolName);
2597
- if (routeTool) return routeTool;
2839
+ .find((tool: Tool<TContext, TData>) => tool.id === toolName || tool.id === toolName);
2840
+ if (flowTool) return flowTool;
2598
2841
  }
2599
2842
 
2600
2843
  // Fall back to agent-level tools
2601
2844
  const agentTools = this.agent.getTools();
2602
2845
  return agentTools.find(
2603
- (tool) => tool.id === toolName || tool.name === toolName
2846
+ (tool) => tool.id === toolName || tool.id === toolName
2604
2847
  );
2605
2848
  }
2606
2849
 
2607
2850
  /**
2608
- * 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
2609
2852
  * Delegates to ToolManager for unified tool resolution and deduplication
2610
2853
  * @private
2611
2854
  */
2612
2855
  private collectAvailableTools(
2613
- route?: Route<TContext, TData>,
2856
+ flow?: Flow<TContext, TData>,
2614
2857
  step?: Step<TContext, TData>
2615
2858
  ): Array<{
2616
2859
  id: string;
@@ -2621,10 +2864,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2621
2864
  // Use ToolManager for unified tool collection if available
2622
2865
  const toolManager = this.getToolManager();
2623
2866
  if (toolManager) {
2624
- const availableTools = toolManager.getAvailable(undefined, step, route);
2867
+ const availableTools = toolManager.getAvailable(undefined, step, flow);
2625
2868
  return availableTools.map((tool) => ({
2626
2869
  id: tool.id,
2627
- name: tool.name || tool.id,
2870
+ name: tool.id || tool.id,
2628
2871
  description: tool.description,
2629
2872
  parameters: tool.parameters,
2630
2873
  }));
@@ -2640,9 +2883,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2640
2883
  availableTools.set(tool.id, tool);
2641
2884
  });
2642
2885
 
2643
- // Add route-level tools (these take precedence)
2644
- if (route) {
2645
- 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>) => {
2646
2889
  availableTools.set(tool.id, tool);
2647
2890
  });
2648
2891
  }
@@ -2688,7 +2931,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2688
2931
  // Convert to the format expected by AI providers
2689
2932
  return Array.from(availableTools.values()).map((tool) => ({
2690
2933
  id: tool.id,
2691
- name: tool.name || tool.id,
2934
+ name: tool.id || tool.id,
2692
2935
  description: tool.description,
2693
2936
  parameters: tool.parameters,
2694
2937
  }));
@@ -2702,11 +2945,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2702
2945
  prepareOrFinalize:
2703
2946
  | string
2704
2947
  | Tool<TContext, TData>
2705
- | ((context: TContext, data?: Partial<TData>) => void | Promise<void>)
2948
+ | ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult>)
2706
2949
  | undefined,
2707
2950
  context: TContext,
2708
2951
  data?: Partial<TData>,
2709
- route?: Route<TContext, TData>,
2952
+ flow?: Flow<TContext, TData>,
2710
2953
  step?: Step<TContext, TData>
2711
2954
  ): Promise<void> {
2712
2955
  if (!prepareOrFinalize) return;
@@ -2722,7 +2965,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2722
2965
  // Tool ID - use ToolManager for unified resolution
2723
2966
  const toolManager = this.getToolManager();
2724
2967
  if (toolManager) {
2725
- tool = toolManager.find(prepareOrFinalize, undefined, step, route);
2968
+ tool = toolManager.find(prepareOrFinalize, undefined, step, flow);
2726
2969
  } else {
2727
2970
  // Fallback to legacy resolution if ToolManager not available
2728
2971
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for prepare/finalize: ${prepareOrFinalize}`);
@@ -2734,9 +2977,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2734
2977
  availableTools.set(t.id, t);
2735
2978
  });
2736
2979
 
2737
- // Add route-level tools
2738
- if (route) {
2739
- route.getTools().forEach((t: Tool<TContext, TData>) => {
2980
+ // Add flow-level tools
2981
+ if (flow) {
2982
+ flow.getTools().forEach((t: Tool<TContext, TData>) => {
2740
2983
  availableTools.set(t.id, t);
2741
2984
  });
2742
2985
  }
@@ -2795,30 +3038,4 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2795
3038
  }
2796
3039
  }
2797
3040
 
2798
- /**
2799
- * Merge terms with route-specific taking precedence on conflicts
2800
- * @private
2801
- */
2802
- private mergeTerms(
2803
- agentTerms: Term<TContext, TData>[],
2804
- routeTerms: Term<TContext, TData>[]
2805
- ): Term<TContext, TData>[] {
2806
- const merged = new Map<string, Term<TContext, TData>>();
2807
-
2808
- // Add agent terms first
2809
- agentTerms.forEach((term) => {
2810
- const name =
2811
- typeof term.name === "string" ? term.name : term.name.toString();
2812
- merged.set(name, term);
2813
- });
2814
-
2815
- // Add route terms (these take precedence)
2816
- routeTerms.forEach((term) => {
2817
- const name =
2818
- typeof term.name === "string" ? term.name : term.name.toString();
2819
- merged.set(name, term);
2820
- });
2821
-
2822
- return Array.from(merged.values());
2823
- }
2824
3041
  }