@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
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import log from "loglevel";
14
+ import type { Directive } from "../types";
14
15
  import type {
15
16
  ToolCallRequest,
16
17
  ToolExecutionUpdate,
@@ -20,6 +21,7 @@ import type {
20
21
  TrackedTool,
21
22
  } from "../types/tool";
22
23
  import { evaluateToolGates } from "./toolGates";
24
+ import { isToolResultLike, extractResultDirectives } from "../utils";
23
25
 
24
26
  /** Options for the StreamingToolExecutor */
25
27
  interface StreamingToolExecutorOptions {
@@ -257,8 +259,18 @@ export class StreamingToolExecutor<TContext = unknown, TData = unknown> {
257
259
  this.parentSignal.addEventListener("abort", parentAbortHandler, { once: true });
258
260
  }
259
261
 
262
+ // Per-call context so ctx.dispatch() emissions attribute to THIS
263
+ // tool even while siblings run concurrently on the shared context.
264
+ const dispatchedDirectives: Directive[] = [];
265
+ const callContext: ToolContext<TContext, TData> = {
266
+ ...this.toolContext,
267
+ dispatch: (directive) => {
268
+ dispatchedDirectives.push(directive);
269
+ },
270
+ };
271
+
260
272
  // Execute the tool handler
261
- const result = await tool.handler(this.toolContext, toolCall.arguments);
273
+ const result = await tool.handler(callContext, toolCall.arguments);
262
274
 
263
275
  // Clean up abort listeners
264
276
  if (batchAbortController) {
@@ -282,6 +294,15 @@ export class StreamingToolExecutor<TContext = unknown, TData = unknown> {
282
294
  // Normalize the result
283
295
  const executionResult = this.normalizeResult(result, tool);
284
296
 
297
+ // Merge ctx.dispatch() emissions with result-carried directives —
298
+ // the same collection the sequential ToolManager.executeTool does.
299
+ if (dispatchedDirectives.length > 0) {
300
+ executionResult.directives = [
301
+ ...(executionResult.directives ?? []),
302
+ ...dispatchedDirectives,
303
+ ];
304
+ }
305
+
285
306
  // Apply per-tool maxResultSizeChars truncation (Req 9.4)
286
307
  const truncatedResult = this.applyResultTruncation(executionResult, tool);
287
308
 
@@ -326,18 +347,22 @@ export class StreamingToolExecutor<TContext = unknown, TData = unknown> {
326
347
  result: unknown,
327
348
  tool: Tool<TContext, TData>
328
349
  ): ToolExecutionResult {
329
- if (
330
- result &&
331
- typeof result === "object" &&
332
- ("data" in result || "success" in result || "error" in result)
333
- ) {
350
+ // Only SEMANTIC markers identify a ToolResult — bare `{data}` / `{error}`
351
+ // shapes are indistinguishable from ordinary business payloads and are
352
+ // wrapped as raw results instead (shared guard with ToolManager).
353
+ if (isToolResultLike(result)) {
334
354
  const r = result as Record<string, unknown>;
355
+ // A handler may return `{ directive }` (singular shorthand) or
356
+ // `{ directives }` — normalized via the shared extractor so the
357
+ // concurrent path honors them identically to sequential execution.
358
+ const directives = extractResultDirectives(result);
335
359
  return {
336
360
  success: r.success !== false,
337
361
  data: r.data,
338
362
  error: r.error as string | undefined,
339
363
  contextUpdate: r.contextUpdate as Record<string, unknown> | undefined,
340
364
  dataUpdate: r.dataUpdate as Record<string, unknown> | undefined,
365
+ ...(directives.length > 0 ? { directives } : {}),
341
366
  metadata: {
342
367
  toolId: tool.id,
343
368
  ...(r.meta as Record<string, unknown> | undefined),
@@ -18,6 +18,7 @@ import type {
18
18
  AgentOptions,
19
19
  AgentResponseStreamChunk,
20
20
  AgentStructuredResponse,
21
+ Directive,
21
22
  HistoryItem,
22
23
  SessionState,
23
24
  ToolCallRequest,
@@ -26,8 +27,33 @@ import type { Flow } from "./Flow";
26
27
  import type { Step } from "./Step";
27
28
  import type { ToolManager } from "./ToolManager";
28
29
  import { ResponseGenerationError } from "./ResponseGenerationError";
30
+ import { flow } from "./flow-namespace";
29
31
  import { historyToEvents, logger, serializeToolResult, assistantMessage, toolMessage } from "../utils";
30
32
 
33
+ /**
34
+ * One attempted tool execution, keyed by CALL (not tool name): parallel or
35
+ * repeated calls to the same tool in one turn keep their own results instead
36
+ * of overwriting each other.
37
+ */
38
+ interface ToolExecutionRecord {
39
+ requestId: string;
40
+ toolName: string;
41
+ arguments: Record<string, unknown>;
42
+ /** Which loop round produced this call (0 = the initial streamed batch). */
43
+ round: number;
44
+ /** Serialized result fed back to the model; set EVEN on failure. */
45
+ result?: string;
46
+ }
47
+
48
+ const executionFailure = (toolName: string, error: unknown): string =>
49
+ JSON.stringify({
50
+ success: false,
51
+ error: `${toolName}: ${error instanceof Error ? error.message : String(error)}`,
52
+ });
53
+
54
+ const toolNotFoundFailure = (toolName: string): string =>
55
+ JSON.stringify({ success: false, error: `Tool "${toolName}" is not registered in any scope.` });
56
+
31
57
  export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
32
58
  constructor(
33
59
  private readonly deps: {
@@ -69,6 +95,8 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
69
95
  finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
70
96
  finalMessage?: string;
71
97
  structured?: AgentStructuredResponse;
98
+ /** Directives emitted by tools this turn (ctx.dispatch / `{directive}` returns), merged. */
99
+ directives?: Directive<TContext, TData>;
72
100
  }> {
73
101
  try {
74
102
  const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
@@ -77,22 +105,32 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
77
105
  const { toolCalls } = params;
78
106
  let { session } = params;
79
107
 
108
+ // Directives emitted by tools this turn — consumed by the caller
109
+ const collectedDirectives: Directive<TContext, TData>[] = [];
110
+
80
111
  // Convert HistoryItem[] to Event[] for internal processing
81
112
  const historyEvents = historyToEvents(history);
82
113
 
83
- // Map to store tool execution results for history
84
- const toolResultsMap = new Map<string, string>();
85
- // Map to store tool call arguments for history reconstruction
86
- const toolArgsMap = new Map<string, Record<string, unknown>>();
114
+ // Execution records keyed by call shared with the follow-up loop
115
+ const records: ToolExecutionRecord[] = [];
87
116
 
88
117
  // Execute initial dynamic tool calls
89
118
  if (toolCalls && toolCalls.length > 0) {
90
119
  logger.debug(`[ToolLoopExecutor] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
91
120
 
92
- for (const toolCall of toolCalls) {
121
+ for (const [callIndex, toolCall] of toolCalls.entries()) {
122
+ const record: ToolExecutionRecord = {
123
+ requestId: `init-${callIndex}-${toolCall.toolName}`,
124
+ toolName: toolCall.toolName,
125
+ arguments: toolCall.arguments,
126
+ round: 0,
127
+ };
128
+ records.push(record);
129
+
93
130
  const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
94
131
  if (!tool) {
95
- 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.`);
132
+ logger.warn(`[ToolExecutionError] Tool not found: "${toolCall.toolName}" is not registered in any scope. Register the tool or check the tool name.`);
133
+ record.result = toolNotFoundFailure(toolCall.toolName);
96
134
  continue;
97
135
  }
98
136
 
@@ -109,8 +147,12 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
109
147
  });
110
148
 
111
149
  // Store the actual tool result data for history
112
- toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
113
- toolArgsMap.set(toolCall.toolName, toolCall.arguments);
150
+ record.result = serializeToolResult(toolResult);
151
+
152
+ // Collect tool-emitted directives (ctx.dispatch / {directive})
153
+ if (toolResult.directives?.length) {
154
+ collectedDirectives.push(...toolResult.directives as Directive<TContext, TData>[]);
155
+ }
114
156
 
115
157
  // Check if tool execution was successful
116
158
  if (!toolResult.success) {
@@ -143,20 +185,37 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
143
185
  logger.debug(`[ToolLoopExecutor] Executed dynamic tool: ${toolCall.toolName} (success: ${toolResult.success})`);
144
186
  } catch (error) {
145
187
  logger.error(`[ToolLoopExecutor] Tool execution error for ${toolCall.toolName}:`, error);
146
- // Continue with other tools rather than failing the entire response
188
+ // A thrown handler must be visible to the model as a FAILED
189
+ // tool call — reporting success makes it confirm actions
190
+ // that never happened.
191
+ record.result = executionFailure(toolCall.toolName, error);
147
192
  continue;
148
193
  }
149
194
  }
150
195
  }
151
196
 
197
+ // A tool spoke verbatim this turn: its reply IS the final message —
198
+ // skip the follow-up LLM call (documented dispatch semantics).
199
+ const preLoopDirectives = flow.mergeAll(collectedDirectives);
200
+ if (preLoopDirectives?.reply) {
201
+ logger.debug("[ToolLoopExecutor] Tool directive reply short-circuits follow-up LLM call");
202
+ return {
203
+ session,
204
+ finalToolCalls: toolCalls,
205
+ finalMessage: preLoopDirectives.reply,
206
+ structured: { message: preLoopDirectives.reply },
207
+ directives: preLoopDirectives,
208
+ };
209
+ }
210
+
152
211
  // Hand off to the multi-round follow-up loop shared with the
153
212
  // streaming path. The initial batch above already executed
154
213
  // sequentially and populated the result maps.
155
214
  return await this.runFollowUpLoop({
156
215
  toolCalls,
157
216
  session,
158
- toolResultsMap,
159
- toolArgsMap,
217
+ records,
218
+ collectedDirectives,
160
219
  context,
161
220
  history,
162
221
  selectedFlow,
@@ -186,8 +245,10 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
186
245
  private async runFollowUpLoop(params: {
187
246
  toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
188
247
  session: SessionState<TData>;
189
- toolResultsMap: Map<string, string>;
190
- toolArgsMap: Map<string, Record<string, unknown>>;
248
+ /** Execution records from prior rounds (initial batch = round 0). */
249
+ records: ToolExecutionRecord[];
250
+ /** Shared directive collector — initial-batch emissions land here too. */
251
+ collectedDirectives?: Directive<TContext, TData>[];
191
252
  context: TContext;
192
253
  history: HistoryItem[];
193
254
  selectedFlow?: Flow<TContext, TData>;
@@ -205,8 +266,9 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
205
266
  finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
206
267
  finalMessage?: string;
207
268
  structured?: AgentStructuredResponse;
269
+ directives?: Directive<TContext, TData>;
208
270
  }> {
209
- const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal, toolResultsMap, toolArgsMap } = params;
271
+ const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal, records, collectedDirectives = [] } = params;
210
272
  let { toolCalls, session } = params;
211
273
  try {
212
274
  // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
@@ -222,45 +284,43 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
222
284
  toolLoopCount++;
223
285
  logger.debug(`[ToolLoopExecutor] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS} with ${toolCalls?.length || 0} tool calls`);
224
286
 
225
- // Create tool result history items
287
+ // Create tool result history items for the PREVIOUS round's
288
+ // calls. Failures are reported as failures — never as success.
289
+ const previousRound = toolLoopCount - 1;
290
+ const roundRecords = records.filter((r) => r.round === previousRound);
226
291
  const toolResultHistoryItems: HistoryItem[] = [];
227
- for (const toolCall of toolCalls || []) {
228
- const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
229
- if (tool) {
230
- // assistant tool_call + tool result pair, via the shared
231
- // history factories (same as forceFinalTextFromTools).
232
- toolResultHistoryItems.push(
233
- assistantMessage(null, [
234
- { id: toolCall.toolName, name: toolCall.toolName, arguments: toolCall.arguments },
235
- ]),
236
- toolMessage(
237
- toolCall.toolName,
238
- toolCall.toolName,
239
- toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
240
- ),
241
- );
242
- }
292
+ for (const record of roundRecords) {
293
+ toolResultHistoryItems.push(
294
+ assistantMessage(null, [
295
+ { id: record.requestId, name: record.toolName, arguments: record.arguments },
296
+ ]),
297
+ toolMessage(
298
+ record.toolName,
299
+ record.requestId,
300
+ record.result ?? toolNotFoundFailure(record.toolName),
301
+ ),
302
+ );
243
303
  }
244
304
 
245
305
  // Create updated history with tool results
246
306
  const updatedHistory = [...history, ...toolResultHistoryItems];
247
307
 
248
- // Make follow-up AI call to see if more tools are needed
249
- // After first iteration, don't provide tools to force a text response
308
+ // Make follow-up AI call to see if more tools are needed.
309
+ // Tools are offered EVERY iteration the loop is already bounded
310
+ // by MAX_TOOL_LOOPS, so withholding them after round only made
311
+ // maxToolLoops values above 2 unreachable.
250
312
  const agentOptions = this.deps.getAgentOptions();
251
- const shouldProvideTools = toolLoopCount === 1;
252
313
 
253
314
  logger.debug(`[ToolLoopExecutor] Making follow-up AI call (loop ${toolLoopCount}):`, {
254
- providingTools: shouldProvideTools,
255
- toolsCount: shouldProvideTools ? availableTools.length : 0,
256
- addingTextInstruction: toolLoopCount > 1,
315
+ providingTools: true,
316
+ toolsCount: availableTools.length,
257
317
  });
258
318
 
259
319
  const followUpResult = await agentOptions.provider.generateMessage({
260
- prompt: responsePrompt + (toolLoopCount > 1 ? "\n\nProvide a text response to the user based on the tool results." : ""),
320
+ prompt: responsePrompt,
261
321
  history: updatedHistory, // Use HistoryItem[] for AI provider
262
322
  context,
263
- tools: shouldProvideTools ? availableTools : [], // Only provide tools on first iteration
323
+ tools: availableTools,
264
324
  parameters: responseSchema ? {
265
325
  jsonSchema: responseSchema,
266
326
  schemaName: "tool_followup",
@@ -284,10 +344,19 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
284
344
  logger.debug(`[ToolLoopExecutor] Follow-up call produced ${followUpToolCalls!.length} additional tool calls`);
285
345
 
286
346
  // Execute the follow-up tool calls
287
- for (const toolCall of followUpToolCalls!) {
347
+ for (const [callIndex, toolCall] of followUpToolCalls!.entries()) {
348
+ const record: ToolExecutionRecord = {
349
+ requestId: `fup-${toolLoopCount}-${callIndex}-${toolCall.toolName}`,
350
+ toolName: toolCall.toolName,
351
+ arguments: toolCall.arguments,
352
+ round: toolLoopCount,
353
+ };
354
+ records.push(record);
355
+
288
356
  const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
289
357
  if (!tool) {
290
- 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.`);
358
+ logger.warn(`[ToolExecutionError] Tool not found in follow-up: "${toolCall.toolName}" is not registered in any scope. Register the tool or check the tool name.`);
359
+ record.result = toolNotFoundFailure(toolCall.toolName);
291
360
  continue;
292
361
  }
293
362
 
@@ -328,16 +397,34 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
328
397
  }
329
398
 
330
399
  // Store the follow-up tool result for potential next loop iteration
331
- toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
332
- toolArgsMap.set(toolCall.toolName, toolCall.arguments);
400
+ record.result = serializeToolResult(toolResult);
401
+
402
+ // Collect tool-emitted directives (ctx.dispatch / {directive})
403
+ if (toolResult.directives?.length) {
404
+ collectedDirectives.push(...toolResult.directives as Directive<TContext, TData>[]);
405
+ }
333
406
 
334
407
  logger.debug(`[ToolLoopExecutor] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
335
408
  } catch (error) {
336
409
  logger.error(`[ToolLoopExecutor] Follow-up tool execution error for ${toolCall.toolName}:`, error);
410
+ // Visible failure beats a fabricated success.
411
+ record.result = executionFailure(toolCall.toolName, error);
337
412
  continue;
338
413
  }
339
414
  }
340
415
 
416
+ // A tool emitted a verbatim reply this round: it IS the final
417
+ // message — stop looping and skip any further LLM call.
418
+ const roundDirectives = flow.mergeAll(collectedDirectives);
419
+ if (roundDirectives?.reply) {
420
+ logger.debug("[ToolLoopExecutor] Tool directive reply short-circuits remaining tool loop");
421
+ finalMessage = roundDirectives.reply;
422
+ followUpStructured = { message: roundDirectives.reply };
423
+ hasToolCalls = false;
424
+ toolCalls = undefined;
425
+ break;
426
+ }
427
+
341
428
  // Update toolCalls for next iteration or final response
342
429
  toolCalls = followUpToolCalls;
343
430
  } else {
@@ -363,8 +450,7 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
363
450
  logger.debug(`[ToolLoopExecutor] No final message after tool loop, making additional LLM call for text response`);
364
451
  const forced = await this.forceFinalTextFromTools({
365
452
  history,
366
- toolResultsMap,
367
- toolArgsMap,
453
+ records,
368
454
  responsePrompt,
369
455
  responseSchema,
370
456
  context,
@@ -390,6 +476,7 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
390
476
  finalToolCalls: toolCalls,
391
477
  finalMessage,
392
478
  structured: followUpStructured,
479
+ directives: flow.mergeAll(collectedDirectives),
393
480
  };
394
481
  } catch (error) {
395
482
  throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
@@ -408,24 +495,25 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
408
495
  */
409
496
  private async forceFinalTextFromTools(params: {
410
497
  history: HistoryItem[];
411
- toolResultsMap: Map<string, string>;
412
- toolArgsMap: Map<string, Record<string, unknown>>;
498
+ records: ToolExecutionRecord[];
413
499
  responsePrompt: string;
414
500
  responseSchema?: Record<string, unknown>;
415
501
  context: TContext;
416
502
  signal?: AbortSignal;
417
503
  }): Promise<{ finalMessage?: string; structured?: AgentStructuredResponse }> {
418
- const { history, toolResultsMap, toolArgsMap, responsePrompt, responseSchema, context, signal } = params;
504
+ const { history, records, responsePrompt, responseSchema, context, signal } = params;
419
505
 
420
506
  // Reconstruct assistant tool_call + tool result pairs so the follow-up
421
- // call can see what the tools returned.
507
+ // call can see what the tools returned — every executed call, keyed by
508
+ // its own request id.
422
509
  const finalToolResultHistoryItems: HistoryItem[] = [];
423
- for (const [toolName, toolResult] of toolResultsMap) {
510
+ for (const record of records) {
511
+ if (!record.result) continue;
424
512
  finalToolResultHistoryItems.push(
425
513
  assistantMessage(null, [
426
- { id: toolName, name: toolName, arguments: toolArgsMap.get(toolName) || {} },
514
+ { id: record.requestId, name: record.toolName, arguments: record.arguments },
427
515
  ]),
428
- toolMessage(toolName, toolName, toolResult),
516
+ toolMessage(record.toolName, record.requestId, record.result),
429
517
  );
430
518
  }
431
519
 
@@ -494,12 +582,17 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
494
582
  /** Closing message forced from the tool results, if one was generated. */
495
583
  finalMessage?: string;
496
584
  structured?: AgentStructuredResponse;
585
+ /** Directives emitted by tools this turn (ctx.dispatch / `{directive}` returns), merged. */
586
+ directives?: Directive<TContext, TData>;
497
587
  }
498
588
  > {
499
589
  const { context, history, selectedFlow, step, accumulated, responsePrompt, availableTools, responseSchema, signal } = params;
500
590
  let { session } = params;
501
591
  let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = params.toolCalls;
502
592
 
593
+ // Directives emitted by tools this turn — surfaced to the caller
594
+ const collectedDirectives: Directive<TContext, TData>[] = [];
595
+
503
596
  // Use concurrent execution for the initial batch of tool calls
504
597
  const toolCallRequests: ToolCallRequest[] = params.toolCalls.map((tc, i) => ({
505
598
  id: `${tc.toolName}-${i}-${Date.now()}`,
@@ -508,9 +601,15 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
508
601
  }));
509
602
  // Map request id -> request so completed results can be attributed back
510
603
  // to their tool name/arguments for the forced final-text call.
511
- const requestById = new Map(toolCallRequests.map((r) => [r.id, r]));
512
- const toolResultsMap = new Map<string, string>();
513
- const toolArgsMap = new Map<string, Record<string, unknown>>();
604
+ // Execution records keyed by call id — seeded at queue time so results
605
+ // (and failures) attach to the right call even for same-name calls.
606
+ const records: ToolExecutionRecord[] = toolCallRequests.map((req) => ({
607
+ requestId: req.id,
608
+ toolName: req.toolName,
609
+ arguments: req.arguments,
610
+ round: 0,
611
+ }));
612
+ const recordById = new Map(records.map((r) => [r.requestId, r]));
514
613
 
515
614
  const historyEvents = historyToEvents(history);
516
615
 
@@ -547,10 +646,13 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
547
646
 
548
647
  // Capture tool results for the forced final-text call
549
648
  if (update.result) {
550
- const req = requestById.get(update.toolCallId);
551
- if (req) {
552
- toolResultsMap.set(req.toolName, serializeToolResult(update.result));
553
- toolArgsMap.set(req.toolName, req.arguments);
649
+ const record = recordById.get(update.toolCallId);
650
+ if (record) {
651
+ record.result = serializeToolResult(update.result);
652
+ }
653
+ // Collect tool-emitted directives (ctx.dispatch / {directive})
654
+ if (update.result.directives?.length) {
655
+ collectedDirectives.push(...update.result.directives as Directive<TContext, TData>[]);
554
656
  }
555
657
  }
556
658
 
@@ -578,8 +680,8 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
578
680
  const followUp = await this.runFollowUpLoop({
579
681
  toolCalls,
580
682
  session,
581
- toolResultsMap,
582
- toolArgsMap,
683
+ records,
684
+ collectedDirectives,
583
685
  context,
584
686
  history,
585
687
  selectedFlow,
@@ -593,21 +695,45 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
593
695
  finalMessage = followUp.finalMessage;
594
696
  structured = followUp.structured;
595
697
  } catch (error) {
596
- logger.error(`[ToolLoopExecutor] Concurrent tool execution failed, falling back to sequential:`, error);
597
- // Fall back to the unified tool loop on failure. runLoop re-executes
598
- // the tools from scratch, so any partial results collected above are
599
- // intentionally discarded (it builds and forces its own).
600
- const toolResult = await this.runLoop({
601
- toolCalls, context, session, history, selectedFlow,
602
- responsePrompt, availableTools, responseSchema, signal,
603
- });
604
- session = toolResult.session;
605
- toolCalls = toolResult.finalToolCalls;
606
- finalMessage = toolResult.finalMessage;
607
- structured = toolResult.structured;
698
+ logger.error(`[ToolLoopExecutor] Concurrent batch failed:`, error);
699
+ const executedRecords = records.filter((r) => r.result !== undefined);
700
+ if (executedRecords.length > 0) {
701
+ // Some tools already executed re-running them would duplicate
702
+ // real-world side effects (sends, writes, charges). Close the
703
+ // turn from the results we have instead.
704
+ logger.warn(
705
+ `[ToolLoopExecutor] ${executedRecords.length} tool(s) already executed; closing the turn from their results rather than re-executing.`
706
+ );
707
+ const forced = await this.forceFinalTextFromTools({
708
+ history,
709
+ records,
710
+ responsePrompt,
711
+ responseSchema,
712
+ context,
713
+ signal,
714
+ });
715
+ if (forced.finalMessage) {
716
+ finalMessage = forced.finalMessage;
717
+ }
718
+ structured = structured ?? forced.structured;
719
+ } else {
720
+ // Nothing executed — safe to fall back to the unified loop.
721
+ const toolResult = await this.runLoop({
722
+ toolCalls, context, session, history, selectedFlow,
723
+ responsePrompt, availableTools, responseSchema, signal,
724
+ });
725
+ session = toolResult.session;
726
+ toolCalls = toolResult.finalToolCalls;
727
+ finalMessage = toolResult.finalMessage;
728
+ structured = toolResult.structured;
729
+ if (toolResult.directives) {
730
+ collectedDirectives.length = 0;
731
+ collectedDirectives.push(toolResult.directives);
732
+ }
733
+ }
608
734
  }
609
735
 
610
- return { session, toolCalls, finalMessage, structured };
736
+ return { session, toolCalls, finalMessage, structured, directives: flow.mergeAll(collectedDirectives) };
611
737
  }
612
738
 
613
739
  /**