@falai/agent 2.6.1 → 2.7.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 (320) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/MemoryAdapter.js +29 -33
  3. package/dist/adapters/MemoryAdapter.js.map +1 -1
  4. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  5. package/dist/adapters/PostgreSQLAdapter.js +16 -11
  6. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  7. package/dist/adapters/RedisAdapter.d.ts +1 -0
  8. package/dist/adapters/RedisAdapter.d.ts.map +1 -1
  9. package/dist/adapters/RedisAdapter.js +75 -25
  10. package/dist/adapters/RedisAdapter.js.map +1 -1
  11. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  12. package/dist/adapters/SQLiteAdapter.js +7 -29
  13. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  14. package/dist/adapters/sessionRow.d.ts +22 -0
  15. package/dist/adapters/sessionRow.d.ts.map +1 -0
  16. package/dist/adapters/sessionRow.js +48 -0
  17. package/dist/adapters/sessionRow.js.map +1 -0
  18. package/dist/cjs/adapters/MemoryAdapter.js +29 -33
  19. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  20. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  21. package/dist/cjs/adapters/PostgreSQLAdapter.js +16 -11
  22. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/RedisAdapter.d.ts +1 -0
  24. package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
  25. package/dist/cjs/adapters/RedisAdapter.js +75 -25
  26. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  27. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  28. package/dist/cjs/adapters/SQLiteAdapter.js +7 -29
  29. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  30. package/dist/cjs/adapters/sessionRow.d.ts +22 -0
  31. package/dist/cjs/adapters/sessionRow.d.ts.map +1 -0
  32. package/dist/cjs/adapters/sessionRow.js +52 -0
  33. package/dist/cjs/adapters/sessionRow.js.map +1 -0
  34. package/dist/cjs/core/Agent.d.ts +8 -0
  35. package/dist/cjs/core/Agent.d.ts.map +1 -1
  36. package/dist/cjs/core/Agent.js +40 -2
  37. package/dist/cjs/core/Agent.js.map +1 -1
  38. package/dist/cjs/core/AutoChainExecutor.d.ts +8 -18
  39. package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
  40. package/dist/cjs/core/AutoChainExecutor.js +23 -26
  41. package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
  42. package/dist/cjs/core/CompactionEngine.d.ts +14 -1
  43. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
  44. package/dist/cjs/core/CompactionEngine.js +30 -6
  45. package/dist/cjs/core/CompactionEngine.js.map +1 -1
  46. package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
  47. package/dist/cjs/core/FlowRouter.js +32 -10
  48. package/dist/cjs/core/FlowRouter.js.map +1 -1
  49. package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -1
  50. package/dist/cjs/core/ResponseGenerationError.js +3 -5
  51. package/dist/cjs/core/ResponseGenerationError.js.map +1 -1
  52. package/dist/cjs/core/ResponseModal.d.ts +29 -0
  53. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  54. package/dist/cjs/core/ResponseModal.js +141 -18
  55. package/dist/cjs/core/ResponseModal.js.map +1 -1
  56. package/dist/cjs/core/ResponsePipeline.d.ts +36 -6
  57. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  58. package/dist/cjs/core/ResponsePipeline.js +208 -73
  59. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  60. package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -1
  61. package/dist/cjs/core/SessionFinalizer.js +30 -3
  62. package/dist/cjs/core/SessionFinalizer.js.map +1 -1
  63. package/dist/cjs/core/SessionManager.d.ts +10 -1
  64. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  65. package/dist/cjs/core/SessionManager.js +43 -16
  66. package/dist/cjs/core/SessionManager.js.map +1 -1
  67. package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
  68. package/dist/cjs/core/SignalProcessor.js +5 -77
  69. package/dist/cjs/core/SignalProcessor.js.map +1 -1
  70. package/dist/cjs/core/Step.d.ts.map +1 -1
  71. package/dist/cjs/core/Step.js +50 -2
  72. package/dist/cjs/core/Step.js.map +1 -1
  73. package/dist/cjs/core/StepLifecycle.d.ts +16 -6
  74. package/dist/cjs/core/StepLifecycle.d.ts.map +1 -1
  75. package/dist/cjs/core/StepLifecycle.js +97 -14
  76. package/dist/cjs/core/StepLifecycle.js.map +1 -1
  77. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
  78. package/dist/cjs/core/StreamingToolExecutor.js +28 -4
  79. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
  80. package/dist/cjs/core/ToolLoopExecutor.d.ts +5 -1
  81. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
  82. package/dist/cjs/core/ToolLoopExecutor.js +155 -63
  83. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
  84. package/dist/cjs/core/ToolManager.d.ts +1 -1
  85. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  86. package/dist/cjs/core/ToolManager.js +40 -17
  87. package/dist/cjs/core/ToolManager.js.map +1 -1
  88. package/dist/cjs/core/flow-namespace.d.ts +15 -0
  89. package/dist/cjs/core/flow-namespace.d.ts.map +1 -1
  90. package/dist/cjs/core/flow-namespace.js +22 -0
  91. package/dist/cjs/core/flow-namespace.js.map +1 -1
  92. package/dist/cjs/index.d.ts +4 -1
  93. package/dist/cjs/index.d.ts.map +1 -1
  94. package/dist/cjs/index.js +5 -2
  95. package/dist/cjs/index.js.map +1 -1
  96. package/dist/cjs/providers/AnthropicProvider.d.ts +10 -3
  97. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  98. package/dist/cjs/providers/AnthropicProvider.js +54 -73
  99. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  100. package/dist/cjs/providers/GeminiProvider.d.ts +9 -3
  101. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  102. package/dist/cjs/providers/GeminiProvider.js +24 -68
  103. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  104. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +9 -0
  105. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  106. package/dist/cjs/providers/OpenAICompatibleProvider.js +101 -74
  107. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
  108. package/dist/cjs/providers/OpenAIProvider.d.ts +1 -1
  109. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  110. package/dist/cjs/providers/OpenAIProvider.js +1 -1
  111. package/dist/cjs/providers/errorClassification.d.ts +7 -0
  112. package/dist/cjs/providers/errorClassification.d.ts.map +1 -1
  113. package/dist/cjs/providers/errorClassification.js +27 -0
  114. package/dist/cjs/providers/errorClassification.js.map +1 -1
  115. package/dist/cjs/types/agent.d.ts +26 -0
  116. package/dist/cjs/types/agent.d.ts.map +1 -1
  117. package/dist/cjs/types/flow.d.ts +49 -32
  118. package/dist/cjs/types/flow.d.ts.map +1 -1
  119. package/dist/cjs/types/index.d.ts +1 -1
  120. package/dist/cjs/types/index.d.ts.map +1 -1
  121. package/dist/cjs/types/index.js.map +1 -1
  122. package/dist/cjs/types/tool.d.ts +2 -0
  123. package/dist/cjs/types/tool.d.ts.map +1 -1
  124. package/dist/cjs/types/tool.js.map +1 -1
  125. package/dist/cjs/utils/index.d.ts +4 -4
  126. package/dist/cjs/utils/index.d.ts.map +1 -1
  127. package/dist/cjs/utils/index.js +10 -1
  128. package/dist/cjs/utils/index.js.map +1 -1
  129. package/dist/cjs/utils/retry.d.ts +99 -1
  130. package/dist/cjs/utils/retry.d.ts.map +1 -1
  131. package/dist/cjs/utils/retry.js +140 -3
  132. package/dist/cjs/utils/retry.js.map +1 -1
  133. package/dist/cjs/utils/serialize.d.ts +17 -0
  134. package/dist/cjs/utils/serialize.d.ts.map +1 -1
  135. package/dist/cjs/utils/serialize.js +33 -0
  136. package/dist/cjs/utils/serialize.js.map +1 -1
  137. package/dist/cjs/utils/session.d.ts +21 -1
  138. package/dist/cjs/utils/session.d.ts.map +1 -1
  139. package/dist/cjs/utils/session.js +36 -4
  140. package/dist/cjs/utils/session.js.map +1 -1
  141. package/dist/core/Agent.d.ts +8 -0
  142. package/dist/core/Agent.d.ts.map +1 -1
  143. package/dist/core/Agent.js +40 -2
  144. package/dist/core/Agent.js.map +1 -1
  145. package/dist/core/AutoChainExecutor.d.ts +8 -18
  146. package/dist/core/AutoChainExecutor.d.ts.map +1 -1
  147. package/dist/core/AutoChainExecutor.js +23 -26
  148. package/dist/core/AutoChainExecutor.js.map +1 -1
  149. package/dist/core/CompactionEngine.d.ts +14 -1
  150. package/dist/core/CompactionEngine.d.ts.map +1 -1
  151. package/dist/core/CompactionEngine.js +30 -6
  152. package/dist/core/CompactionEngine.js.map +1 -1
  153. package/dist/core/FlowRouter.d.ts.map +1 -1
  154. package/dist/core/FlowRouter.js +32 -10
  155. package/dist/core/FlowRouter.js.map +1 -1
  156. package/dist/core/ResponseGenerationError.d.ts.map +1 -1
  157. package/dist/core/ResponseGenerationError.js +3 -5
  158. package/dist/core/ResponseGenerationError.js.map +1 -1
  159. package/dist/core/ResponseModal.d.ts +29 -0
  160. package/dist/core/ResponseModal.d.ts.map +1 -1
  161. package/dist/core/ResponseModal.js +142 -19
  162. package/dist/core/ResponseModal.js.map +1 -1
  163. package/dist/core/ResponsePipeline.d.ts +36 -6
  164. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  165. package/dist/core/ResponsePipeline.js +208 -73
  166. package/dist/core/ResponsePipeline.js.map +1 -1
  167. package/dist/core/SessionFinalizer.d.ts.map +1 -1
  168. package/dist/core/SessionFinalizer.js +31 -4
  169. package/dist/core/SessionFinalizer.js.map +1 -1
  170. package/dist/core/SessionManager.d.ts +10 -1
  171. package/dist/core/SessionManager.d.ts.map +1 -1
  172. package/dist/core/SessionManager.js +44 -17
  173. package/dist/core/SessionManager.js.map +1 -1
  174. package/dist/core/SignalProcessor.d.ts.map +1 -1
  175. package/dist/core/SignalProcessor.js +5 -77
  176. package/dist/core/SignalProcessor.js.map +1 -1
  177. package/dist/core/Step.d.ts.map +1 -1
  178. package/dist/core/Step.js +50 -2
  179. package/dist/core/Step.js.map +1 -1
  180. package/dist/core/StepLifecycle.d.ts +16 -6
  181. package/dist/core/StepLifecycle.d.ts.map +1 -1
  182. package/dist/core/StepLifecycle.js +97 -14
  183. package/dist/core/StepLifecycle.js.map +1 -1
  184. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  185. package/dist/core/StreamingToolExecutor.js +28 -4
  186. package/dist/core/StreamingToolExecutor.js.map +1 -1
  187. package/dist/core/ToolLoopExecutor.d.ts +5 -1
  188. package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
  189. package/dist/core/ToolLoopExecutor.js +155 -63
  190. package/dist/core/ToolLoopExecutor.js.map +1 -1
  191. package/dist/core/ToolManager.d.ts +1 -1
  192. package/dist/core/ToolManager.d.ts.map +1 -1
  193. package/dist/core/ToolManager.js +41 -18
  194. package/dist/core/ToolManager.js.map +1 -1
  195. package/dist/core/flow-namespace.d.ts +15 -0
  196. package/dist/core/flow-namespace.d.ts.map +1 -1
  197. package/dist/core/flow-namespace.js +22 -0
  198. package/dist/core/flow-namespace.js.map +1 -1
  199. package/dist/index.d.ts +4 -1
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +2 -1
  202. package/dist/index.js.map +1 -1
  203. package/dist/providers/AnthropicProvider.d.ts +10 -3
  204. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  205. package/dist/providers/AnthropicProvider.js +56 -75
  206. package/dist/providers/AnthropicProvider.js.map +1 -1
  207. package/dist/providers/GeminiProvider.d.ts +9 -3
  208. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  209. package/dist/providers/GeminiProvider.js +26 -70
  210. package/dist/providers/GeminiProvider.js.map +1 -1
  211. package/dist/providers/OpenAICompatibleProvider.d.ts +9 -0
  212. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  213. package/dist/providers/OpenAICompatibleProvider.js +103 -76
  214. package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
  215. package/dist/providers/OpenAIProvider.d.ts +1 -1
  216. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  217. package/dist/providers/OpenAIProvider.js +1 -1
  218. package/dist/providers/errorClassification.d.ts +7 -0
  219. package/dist/providers/errorClassification.d.ts.map +1 -1
  220. package/dist/providers/errorClassification.js +26 -0
  221. package/dist/providers/errorClassification.js.map +1 -1
  222. package/dist/types/agent.d.ts +26 -0
  223. package/dist/types/agent.d.ts.map +1 -1
  224. package/dist/types/flow.d.ts +49 -32
  225. package/dist/types/flow.d.ts.map +1 -1
  226. package/dist/types/index.d.ts +1 -1
  227. package/dist/types/index.d.ts.map +1 -1
  228. package/dist/types/index.js.map +1 -1
  229. package/dist/types/tool.d.ts +2 -0
  230. package/dist/types/tool.d.ts.map +1 -1
  231. package/dist/types/tool.js.map +1 -1
  232. package/dist/utils/index.d.ts +4 -4
  233. package/dist/utils/index.d.ts.map +1 -1
  234. package/dist/utils/index.js +3 -3
  235. package/dist/utils/index.js.map +1 -1
  236. package/dist/utils/retry.d.ts +99 -1
  237. package/dist/utils/retry.d.ts.map +1 -1
  238. package/dist/utils/retry.js +137 -3
  239. package/dist/utils/retry.js.map +1 -1
  240. package/dist/utils/serialize.d.ts +17 -0
  241. package/dist/utils/serialize.d.ts.map +1 -1
  242. package/dist/utils/serialize.js +31 -0
  243. package/dist/utils/serialize.js.map +1 -1
  244. package/dist/utils/session.d.ts +21 -1
  245. package/dist/utils/session.d.ts.map +1 -1
  246. package/dist/utils/session.js +33 -4
  247. package/dist/utils/session.js.map +1 -1
  248. package/docs/concepts/architecture.md +3 -3
  249. package/docs/concepts/directives.md +1 -1
  250. package/docs/guides/error-handling.md +46 -45
  251. package/docs/guides/flow-control.md +8 -1
  252. package/docs/guides/instructions.md +15 -6
  253. package/docs/guides/persistence.md +12 -5
  254. package/docs/guides/streaming.md +10 -0
  255. package/docs/migration/README.md +4 -0
  256. package/docs/migration/v2-3-to-v2-4.md +4 -0
  257. package/docs/migration/v2-6-to-v2-7.md +246 -0
  258. package/docs/reference/adapters.md +15 -1
  259. package/docs/reference/create-agent.md +28 -0
  260. package/docs/reference/directive.md +1 -1
  261. package/docs/reference/errors.md +29 -31
  262. package/docs/reference/providers.md +23 -19
  263. package/docs/reference/step.md +28 -21
  264. package/docs/reference/tool.md +14 -5
  265. package/docs/start/02-first-agent.md +8 -4
  266. package/docs/start/03-collect-data.md +19 -10
  267. package/examples/01-quickstart.ts +1 -1
  268. package/examples/02-data-extraction.ts +1 -1
  269. package/examples/03-tools.ts +1 -1
  270. package/examples/04-instructions.ts +1 -1
  271. package/examples/05-branching.ts +1 -1
  272. package/examples/06-flow-control.ts +3 -3
  273. package/examples/07-streaming.ts +1 -1
  274. package/examples/08-persistence.ts +1 -1
  275. package/examples/09-signals.ts +1 -1
  276. package/package.json +2 -2
  277. package/src/adapters/MemoryAdapter.ts +29 -33
  278. package/src/adapters/PostgreSQLAdapter.ts +23 -18
  279. package/src/adapters/RedisAdapter.ts +81 -34
  280. package/src/adapters/SQLiteAdapter.ts +10 -31
  281. package/src/adapters/sessionRow.ts +57 -0
  282. package/src/core/Agent.ts +48 -2
  283. package/src/core/AutoChainExecutor.ts +37 -50
  284. package/src/core/CompactionEngine.ts +40 -6
  285. package/src/core/FlowRouter.ts +38 -12
  286. package/src/core/ResponseGenerationError.ts +3 -6
  287. package/src/core/ResponseModal.ts +185 -19
  288. package/src/core/ResponsePipeline.ts +259 -85
  289. package/src/core/SessionFinalizer.ts +35 -5
  290. package/src/core/SessionManager.ts +55 -21
  291. package/src/core/SignalProcessor.ts +5 -85
  292. package/src/core/Step.ts +79 -2
  293. package/src/core/StepLifecycle.ts +125 -22
  294. package/src/core/StreamingToolExecutor.ts +31 -6
  295. package/src/core/ToolLoopExecutor.ts +200 -74
  296. package/src/core/ToolManager.ts +45 -18
  297. package/src/core/flow-namespace.ts +29 -0
  298. package/src/index.ts +4 -1
  299. package/src/providers/AnthropicProvider.ts +83 -131
  300. package/src/providers/GeminiProvider.ts +42 -116
  301. package/src/providers/OpenAICompatibleProvider.ts +132 -133
  302. package/src/providers/OpenAIProvider.ts +2 -2
  303. package/src/providers/errorClassification.ts +34 -0
  304. package/src/types/agent.ts +25 -0
  305. package/src/types/flow.ts +37 -39
  306. package/src/types/index.ts +1 -0
  307. package/src/types/tool.ts +2 -0
  308. package/src/utils/index.ts +22 -3
  309. package/src/utils/retry.ts +251 -2
  310. package/src/utils/serialize.ts +38 -0
  311. package/src/utils/session.ts +41 -4
  312. package/dist/cjs/core/DirectiveBus.d.ts +0 -88
  313. package/dist/cjs/core/DirectiveBus.d.ts.map +0 -1
  314. package/dist/cjs/core/DirectiveBus.js +0 -196
  315. package/dist/cjs/core/DirectiveBus.js.map +0 -1
  316. package/dist/core/DirectiveBus.d.ts +0 -88
  317. package/dist/core/DirectiveBus.d.ts.map +0 -1
  318. package/dist/core/DirectiveBus.js +0 -192
  319. package/dist/core/DirectiveBus.js.map +0 -1
  320. package/src/core/DirectiveBus.ts +0 -248
package/src/core/Agent.ts CHANGED
@@ -205,8 +205,14 @@ export class Agent<TContext = unknown, TData = unknown> implements ResponseModal
205
205
  this.signalProcessor = undefined;
206
206
  }
207
207
 
208
- // Set log level based on debug option
208
+ // Set log level based on debug option. NOTE: loglevel's default logger is
209
+ // process-global — one agent enabling debug turns on DEBUG for every agent
210
+ // in the process. Warned so multi-tenant embedders aren't surprised.
209
211
  if (options.debug) {
212
+ logger.warn(
213
+ `[Agent] "${options.name}" enabled debug logging via the PROCESS-GLOBAL loglevel level. ` +
214
+ `Every agent in this process now logs at DEBUG. Scope logging in your host if needed.`
215
+ );
210
216
  logger.setLevel(LoggerLevel.DEBUG);
211
217
  }
212
218
 
@@ -868,6 +874,26 @@ export class Agent<TContext = unknown, TData = unknown> implements ResponseModal
868
874
  }
869
875
  }
870
876
 
877
+ // Overlap detection: warn (don't throw) when the incoming flow's
878
+ // requiredFields intersect another registered flow's — the schema is
879
+ // agent-level, so both flows complete together and one is silently
880
+ // excluded from routing.
881
+ if (options.requiredFields && options.requiredFields.length > 0) {
882
+ const incoming = new Set(options.requiredFields.map(String));
883
+ for (const existing of this._flows) {
884
+ const shared = (existing.requiredFields ?? [])
885
+ .map(String)
886
+ .filter((f) => incoming.has(f));
887
+ if (shared.length > 0) {
888
+ logger.warn(
889
+ `[FlowConfigurationError] Overlapping requiredFields: flows "${existing.title}" and "${options.title}" share [${shared.join(', ')}]. ` +
890
+ `The schema is agent-level, so data collected for one flow marks the other complete and excludes it from routing. ` +
891
+ `Give each flow distinct requiredFields, or set \`reentrant: true\` on flows that legitimately share fields.`
892
+ );
893
+ }
894
+ }
895
+ }
896
+
871
897
  const flow = new Flow<TContext, TData>(options, this);
872
898
 
873
899
  // Validate that step collect fields reference valid schema keys
@@ -1151,12 +1177,20 @@ export class Agent<TContext = unknown, TData = unknown> implements ResponseModal
1151
1177
  *
1152
1178
  * String form desugars to `{ goTo: target }`.
1153
1179
  *
1180
+ * Durability: with a persistence adapter and autoSave configured (the
1181
+ * defaults), the queued directive is persisted immediately — safe for
1182
+ * out-of-process callers like webhooks or cron. Without an adapter it is
1183
+ * memory-only, as is `persistence.autoSave: false` (then persisting before
1184
+ * the next turn is the caller's job).
1185
+ *
1154
1186
  * @param target - Flow ID/title string (desugars to `{ goTo: target }`) or a full Directive
1155
1187
  * @param session - Session to update (uses current session if not provided)
1156
1188
  * @returns Updated session with `pendingDirective` set
1157
1189
  *
1158
1190
  * @throws FlowConfigurationError if the string target doesn't match any flow
1159
1191
  * @throws FlowConfigurationError if the directive fails validation
1192
+ * @throws SessionConflictError when persistence is enabled and another writer
1193
+ * moved the stored session since this copy was loaded
1160
1194
  *
1161
1195
  * @example
1162
1196
  * // String shorthand — desugars to { goTo: 'Feedback' }
@@ -1166,7 +1200,6 @@ export class Agent<TContext = unknown, TData = unknown> implements ResponseModal
1166
1200
  * // Full directive
1167
1201
  * const updated = await agent.dispatch({ goTo: 'Billing', reply: 'Transferring you now.' }, session);
1168
1202
  */
1169
- // eslint-disable-next-line @typescript-eslint/require-await
1170
1203
  async dispatch(
1171
1204
  target: string | Directive<TContext, TData>,
1172
1205
  session?: SessionState<TData>
@@ -1225,6 +1258,19 @@ export class Agent<TContext = unknown, TData = unknown> implements ResponseModal
1225
1258
  },
1226
1259
  };
1227
1260
 
1261
+ // Durability: with an adapter + autoSave configured, dispatch persists
1262
+ // immediately — webhooks/cron run out-of-process from the responder, and a
1263
+ // memory-only queue would evaporate with this process. The save stamps the
1264
+ // new version back onto updatedSession, so the next turn's auto-save CAS
1265
+ // stays clean. Persisting BEFORE the in-memory sync keeps the existing
1266
+ // invariant: a throwing dispatch leaves the session untouched.
1267
+ if (this._persistenceManager && this.options.persistence?.autoSave !== false) {
1268
+ await this._persistenceManager.saveSessionState(updatedSession.id, updatedSession);
1269
+ logger.debug(
1270
+ `[Agent] Dispatched directive persisted to adapter for session ${updatedSession.id}`
1271
+ );
1272
+ }
1273
+
1228
1274
  // Update current session in place if no explicit session was passed
1229
1275
  if (!session && this.session.current) {
1230
1276
  this.session.syncSession(updatedSession);
@@ -15,10 +15,11 @@
15
15
  * Implements Algorithm 1 from `.kiro/specs/auto-steps/design.md`.
16
16
  */
17
17
 
18
- import type { SessionState } from "../types";
18
+ import type { Directive, SessionState } from "../types";
19
19
  import type { Event } from "../types/history";
20
20
  import { FlowConfigurationError, Step } from "./Step";
21
21
  import { Flow } from "./Flow";
22
+ import { flow } from "./flow-namespace";
22
23
  import { enterStep, mergeCollected, logger } from "../utils";
23
24
  import { createTemplateContext } from "../utils/template";
24
25
  import type {
@@ -31,20 +32,13 @@ import { evaluateIfPredicates } from "../utils/condition";
31
32
 
32
33
  /**
33
34
  * The directive-like object that `prepare` may return on auto-steps.
34
- * This is a structural subset of Directive (pre-LLM fields).
35
+ * A structural subset of Directive (pre-LLM + position fields), so results
36
+ * merge through the canonical Algorithm 4 (`flow.merge`).
35
37
  */
36
- export interface AutoStepPrepareResult {
37
- dataUpdate?: Record<string, unknown>;
38
- contextUpdate?: Record<string, unknown>;
39
- halt?: boolean;
40
- reply?: string;
41
- /** Position-changing: jump to a step within the current flow. */
42
- goToStep?: string;
43
- /** Position-changing: jump to another flow. */
44
- goTo?: string;
45
- /** Position-changing: mark the flow as complete. */
46
- complete?: boolean;
47
- }
38
+ export type AutoStepPrepareResult<TContext = unknown, TData = unknown> = Pick<
39
+ Directive<TContext, TData>,
40
+ "dataUpdate" | "contextUpdate" | "halt" | "reply" | "goToStep" | "goTo" | "complete"
41
+ >;
48
42
 
49
43
  /**
50
44
  * Result of running the auto-chain.
@@ -53,7 +47,7 @@ export interface AutoChainResult<TContext = unknown, TData = unknown> {
53
47
  /** The interactive step to hand off to the LLM path (undefined if chain ended without one). */
54
48
  resolvedStep?: Step<TContext, TData>;
55
49
  /** The merged directive from the halting auto-step's prepare (only set when stoppedReason = 'halt'). */
56
- mergedDirective?: AutoStepPrepareResult;
50
+ mergedDirective?: AutoStepPrepareResult<TContext, TData>;
57
51
  /** Why the chain stopped, if it didn't reach an interactive step normally. */
58
52
  stoppedReason?: StoppedReason;
59
53
  /** Updated session after all auto-step state writes. */
@@ -192,10 +186,24 @@ export class AutoChainExecutor<TContext = unknown, TData = unknown> {
192
186
 
193
187
  // STEP 5: position-changing directive (goToStep, goTo, complete)
194
188
  if (merged.goToStep) {
195
- const targetStep = flow.getStep(merged.goToStep);
189
+ // String form targets the current flow; object form carrying
190
+ // a different `flow` defers to the pipeline for cross-flow
191
+ // resolution (same as goTo).
192
+ const goTarget = typeof merged.goToStep === "string"
193
+ ? { step: merged.goToStep, flowId: undefined }
194
+ : { step: merged.goToStep.step, flowId: merged.goToStep.flow };
195
+ if (goTarget.flowId && goTarget.flowId !== flow.id) {
196
+ return {
197
+ resolvedStep: step,
198
+ mergedDirective: merged,
199
+ stoppedReason: 'goto',
200
+ session,
201
+ };
202
+ }
203
+ const targetStep = flow.getStep(goTarget.step);
196
204
  if (!targetStep) {
197
205
  throw new FlowConfigurationError(
198
- `[FlowConfigurationError] Auto-step "${step.id}" goToStep targets unknown step: "${merged.goToStep}" does not exist in the current flow. ` +
206
+ `[FlowConfigurationError] Auto-step "${step.id}" goToStep targets unknown step: "${goTarget.step}" does not exist in the current flow. ` +
199
207
  `Check the step id or use goTo to target a different flow.`
200
208
  );
201
209
  }
@@ -263,26 +271,15 @@ export class AutoChainExecutor<TContext = unknown, TData = unknown> {
263
271
  }
264
272
 
265
273
  /**
266
- * Run onEnter and prepare hooks for an auto-step, collecting any
274
+ * Run the prepare hook for an auto-step, collecting any
267
275
  * Directive return values (pre-LLM fields honored).
268
276
  */
269
277
  private async runStepHooks(
270
278
  step: Step<TContext, TData>,
271
279
  session: SessionState<TData>,
272
280
  context: TContext
273
- ): Promise<AutoStepPrepareResult | undefined> {
274
- let merged: AutoStepPrepareResult | undefined;
275
-
276
- // onEnter (future hook — not yet in StepOptions, but handle if present)
277
- const stepWithHooks = step as Step<TContext, TData> & {
278
- onEnter?: (context: TContext, data: Partial<TData> | undefined) => Promise<unknown>;
279
- };
280
- if (typeof stepWithHooks.onEnter === 'function') {
281
- const onEnterResult = await stepWithHooks.onEnter(context, session.data);
282
- if (onEnterResult && typeof onEnterResult === 'object') {
283
- merged = this.mergeDirectives(merged, onEnterResult as AutoStepPrepareResult);
284
- }
285
- }
281
+ ): Promise<AutoStepPrepareResult<TContext, TData> | undefined> {
282
+ let merged: AutoStepPrepareResult<TContext, TData> | undefined;
286
283
 
287
284
  // prepare hook — for auto-steps, prepare may return a Directive with pre-LLM fields
288
285
  if (step.prepare) {
@@ -296,7 +293,7 @@ export class AutoChainExecutor<TContext = unknown, TData = unknown> {
296
293
  );
297
294
  // prepare may return void or a directive-like object
298
295
  if (prepareResult && typeof prepareResult === 'object') {
299
- merged = this.mergeDirectives(merged, prepareResult as AutoStepPrepareResult);
296
+ merged = this.mergeDirectives(merged, prepareResult as AutoStepPrepareResult<TContext, TData>);
300
297
  }
301
298
  } else {
302
299
  // Tool reference (string or Tool object) — for auto-steps, tool-based
@@ -312,27 +309,17 @@ export class AutoChainExecutor<TContext = unknown, TData = unknown> {
312
309
  }
313
310
 
314
311
  /**
315
- * Merge two directive-like objects. Later values override earlier ones
316
- * for scalar fields; object fields (dataUpdate, contextUpdate) are deep-merged.
312
+ * Merge two directive-like objects via the canonical Algorithm 4 merge —
313
+ * one algorithm shared with tools, hooks and signals.
317
314
  */
318
315
  private mergeDirectives(
319
- base: AutoStepPrepareResult | undefined,
320
- incoming: AutoStepPrepareResult
321
- ): AutoStepPrepareResult {
316
+ base: AutoStepPrepareResult<TContext, TData> | undefined,
317
+ incoming: AutoStepPrepareResult<TContext, TData>
318
+ ): AutoStepPrepareResult<TContext, TData> {
322
319
  if (!base) return { ...incoming };
323
- return {
324
- dataUpdate: incoming.dataUpdate
325
- ? { ...(base.dataUpdate || {}), ...incoming.dataUpdate }
326
- : base.dataUpdate,
327
- contextUpdate: incoming.contextUpdate
328
- ? { ...(base.contextUpdate || {}), ...incoming.contextUpdate }
329
- : base.contextUpdate,
330
- halt: incoming.halt ?? base.halt,
331
- reply: incoming.reply ?? base.reply,
332
- goTo: incoming.goTo ?? base.goTo,
333
- goToStep: incoming.goToStep ?? base.goToStep,
334
- complete: incoming.complete ?? base.complete,
335
- };
320
+ // AutoStepPrepareResult is a structural subset of Directive, so both
321
+ // sides flow through the canonical merge directly — no casts needed.
322
+ return flow.merge<TContext, TData>(base, incoming);
336
323
  }
337
324
 
338
325
  /**
@@ -180,6 +180,27 @@ export class CompactionEngine {
180
180
  }
181
181
  }
182
182
 
183
+ /**
184
+ * Adjust the left edge of a preserved window so it never begins with an
185
+ * orphaned tool result — a role:'tool' message whose assistant tool_calls
186
+ * parent lies outside the window. Providers reject such histories at the
187
+ * next request.
188
+ *
189
+ * The preserve count is a target, not a hard guarantee: when the naive
190
+ * message-count boundary would split an assistant/tool pair, the window
191
+ * grows left just enough to keep the pair together.
192
+ */
193
+ private static alignPreserveStart(
194
+ history: HistoryItem[],
195
+ start: number
196
+ ): number {
197
+ let aligned = Math.max(0, Math.min(start, history.length));
198
+ while (aligned > 0 && history[aligned].role === "tool") {
199
+ aligned--;
200
+ }
201
+ return aligned;
202
+ }
203
+
183
204
  /**
184
205
  * Aggressive truncation fallback: remove oldest messages (no LLM needed).
185
206
  * Keeps only the most recent messages that fit within the token budget.
@@ -191,11 +212,16 @@ export class CompactionEngine {
191
212
  const threshold = options.maxTokens * options.compactionThreshold;
192
213
  const preserveCount = options.preserveRecentCount;
193
214
 
194
- // Always preserve the last preserveRecentCount messages
195
- const preserved = history.slice(-preserveCount);
215
+ // Always preserve the last ~preserveRecentCount messages (aligned so
216
+ // the window never opens on an orphaned tool result)
217
+ const preserveStart = CompactionEngine.alignPreserveStart(
218
+ history,
219
+ Math.max(0, history.length - preserveCount)
220
+ );
221
+ const preserved = history.slice(preserveStart);
196
222
 
197
223
  // Try to keep as many older messages as fit within budget
198
- const older = history.slice(0, -preserveCount);
224
+ const older = history.slice(0, preserveStart);
199
225
  const result: HistoryItem[] = [];
200
226
 
201
227
  // Add older messages from most recent backwards until we'd exceed budget
@@ -219,7 +245,9 @@ export class CompactionEngine {
219
245
  * Layer 3 (micro_compact): Compress verbose tool outputs inline
220
246
  * Layer 4 (auto_compact): Summarize old messages via LLM provider
221
247
  *
222
- * The last `preserveRecentCount` messages are NEVER modified or removed.
248
+ * The last `preserveRecentCount` messages are NEVER modified or removed
249
+ * (the preserved window may extend slightly further left so it never
250
+ * splits an assistant/tool pair at its boundary).
223
251
  */
224
252
  static async checkAndCompact(
225
253
  history: HistoryItem[],
@@ -277,8 +305,14 @@ export class CompactionEngine {
277
305
  }
278
306
 
279
307
  // Layer 4: Auto-compact (summarize old messages via LLM)
280
- const oldMessages = microCompacted.slice(0, -preserveCount);
281
- const recentMessages = microCompacted.slice(-preserveCount);
308
+ // preserveRecentCount is a target, not a hard guarantee: the window's
309
+ // left edge is aligned so it never opens on an orphaned tool result.
310
+ const preserveStart = CompactionEngine.alignPreserveStart(
311
+ microCompacted,
312
+ Math.max(0, microCompacted.length - preserveCount)
313
+ );
314
+ const oldMessages = microCompacted.slice(0, preserveStart);
315
+ const recentMessages = microCompacted.slice(preserveStart);
282
316
 
283
317
  const summary = await CompactionEngine.summarizeMessages(
284
318
  oldMessages,
@@ -919,6 +919,18 @@ export class FlowRouter<TContext = unknown, TData = unknown> {
919
919
  logger.debug(`[FlowRouter] Selected route: ${selectedFlow.title}`);
920
920
  updatedSession = this.enterFlowIfNeeded(updatedSession, selectedFlow);
921
921
  }
922
+ } else {
923
+ // Routing LLM output could not be parsed into the expected structured
924
+ // shape (no `flows` payload). The turn degrades to a fallback response
925
+ // with the flow position untouched — surface that loudly instead of
926
+ // failing silently.
927
+ const rawSnippet = (routingResult.message ?? "").trim().slice(0, 120);
928
+ logger.warn(
929
+ `[FlowRouter] Routing output unparseable: routing LLM returned no structured "flows" payload` +
930
+ `${rawSnippet ? `; raw output: "${rawSnippet}"` : ""}. ` +
931
+ `This turn degrades to a generic fallback response and the flow position is left unchanged. ` +
932
+ `Check the provider's structured-output/JSON support or inspect the routing prompt for schema violations.`
933
+ );
922
934
  }
923
935
 
924
936
  return {
@@ -1041,25 +1053,39 @@ export class FlowRouter<TContext = unknown, TData = unknown> {
1041
1053
  }
1042
1054
 
1043
1055
  // Apply sticky routing: if there's a current route, only switch if the
1044
- // best alternative exceeds the current flow's score by the configured margin
1056
+ // best alternative exceeds the current flow's score by the configured margin.
1057
+ // The margin applies symmetrically: even when the current flow has no
1058
+ // weighted entry (the AI scored it 0 or omitted it), switching away
1059
+ // requires clearing the margin over a zero baseline — shared agent-level
1060
+ // data can otherwise zero out a mid-conversation flow.
1045
1061
  if (currentRouteId) {
1046
1062
  const currentEntry = weightedScores.find(e => e.route.id === currentRouteId);
1047
1063
  const bestEntry = weightedScores[0];
1048
1064
 
1049
- if (currentEntry && bestEntry.route.id !== currentRouteId) {
1050
- if (bestEntry.score < currentEntry.score + switchMargin) {
1065
+ if (bestEntry && bestEntry.route.id !== currentRouteId) {
1066
+ const currentScore = currentEntry ? currentEntry.score : 0;
1067
+
1068
+ if (bestEntry.score < currentScore + switchMargin) {
1069
+ const currentRoute =
1070
+ currentEntry?.route ?? routes.find(r => r.id === currentRouteId);
1071
+ if (currentRoute) {
1072
+ logger.debug(
1073
+ `[FlowRouter] Staying on current flow: ${currentRoute.title} ` +
1074
+ `(current: ${currentScore}, best alternative: ${bestEntry.score}, ` +
1075
+ `margin required: ${switchMargin})`
1076
+ );
1077
+ return currentRoute;
1078
+ }
1051
1079
  logger.debug(
1052
- `[FlowRouter] Staying on current flow: ${currentEntry.route.title} ` +
1053
- `(current: ${currentEntry.score}, best alternative: ${bestEntry.score}, ` +
1054
- `margin required: ${switchMargin})`
1080
+ `[FlowRouter] Current flow ${currentRouteId} is no longer routable — selecting best alternative: ${bestEntry.route.title}`
1081
+ );
1082
+ } else {
1083
+ logger.debug(
1084
+ `[FlowRouter] Switching flow: ${currentEntry?.route.title ?? currentRouteId} → ${bestEntry.route.title} ` +
1085
+ `(current: ${currentScore}, alternative: ${bestEntry.score}, ` +
1086
+ `margin: ${switchMargin})`
1055
1087
  );
1056
- return currentEntry.route;
1057
1088
  }
1058
- logger.debug(
1059
- `[FlowRouter] Switching flow: ${currentEntry.route.title} → ${bestEntry.route.title} ` +
1060
- `(current: ${currentEntry.score}, alternative: ${bestEntry.score}, ` +
1061
- `margin: ${switchMargin})`
1062
- );
1063
1089
  }
1064
1090
  }
1065
1091
 
@@ -21,13 +21,10 @@ export class ResponseGenerationError extends Error {
21
21
  message: string,
22
22
  public readonly details?: ResponseGenerationErrorDetails
23
23
  ) {
24
- super(message);
24
+ // Native `cause` chain: `err.cause` is the original error, so
25
+ // consumers can reach ProviderError.code etc. without string matching.
26
+ super(message, { cause: details?.originalError });
25
27
  this.name = 'ResponseGenerationError';
26
-
27
- // Preserve stack trace from original error if available
28
- if (details?.originalError instanceof Error && details.originalError.stack) {
29
- this.stack = `${this.stack}\nCaused by: ${details.originalError.stack}`;
30
- }
31
28
  }
32
29
 
33
30
  /**