@ariaflowagents/core 0.7.1 → 0.9.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 (338) hide show
  1. package/README.md +90 -1
  2. package/dist/agents/Agent.d.ts +188 -9
  3. package/dist/agents/Agent.d.ts.map +1 -1
  4. package/dist/agents/Agent.js +246 -24
  5. package/dist/agents/Agent.js.map +1 -1
  6. package/dist/agents/CompositeAgent.d.ts +4 -3
  7. package/dist/agents/CompositeAgent.d.ts.map +1 -1
  8. package/dist/agents/CompositeAgent.js +19 -9
  9. package/dist/agents/CompositeAgent.js.map +1 -1
  10. package/dist/agents/FlowAgent.d.ts +3 -2
  11. package/dist/agents/FlowAgent.d.ts.map +1 -1
  12. package/dist/agents/FlowAgent.js +16 -6
  13. package/dist/agents/FlowAgent.js.map +1 -1
  14. package/dist/agents/TriageAgent.d.ts +8 -2
  15. package/dist/agents/TriageAgent.d.ts.map +1 -1
  16. package/dist/agents/TriageAgent.js +39 -6
  17. package/dist/agents/TriageAgent.js.map +1 -1
  18. package/dist/agents/index.d.ts +1 -1
  19. package/dist/agents/index.d.ts.map +1 -1
  20. package/dist/agents/index.js +0 -1
  21. package/dist/agents/index.js.map +1 -1
  22. package/dist/capabilities/AutoRetrieveCapability.d.ts +30 -0
  23. package/dist/capabilities/AutoRetrieveCapability.d.ts.map +1 -0
  24. package/dist/capabilities/AutoRetrieveCapability.js +36 -0
  25. package/dist/capabilities/AutoRetrieveCapability.js.map +1 -0
  26. package/dist/capabilities/ExtractionCapability.d.ts +25 -0
  27. package/dist/capabilities/ExtractionCapability.d.ts.map +1 -0
  28. package/dist/capabilities/ExtractionCapability.js +74 -0
  29. package/dist/capabilities/ExtractionCapability.js.map +1 -0
  30. package/dist/capabilities/FlowCapability.d.ts +81 -0
  31. package/dist/capabilities/FlowCapability.d.ts.map +1 -0
  32. package/dist/capabilities/FlowCapability.js +482 -0
  33. package/dist/capabilities/FlowCapability.js.map +1 -0
  34. package/dist/capabilities/GuardrailCapability.d.ts +30 -0
  35. package/dist/capabilities/GuardrailCapability.d.ts.map +1 -0
  36. package/dist/capabilities/GuardrailCapability.js +38 -0
  37. package/dist/capabilities/GuardrailCapability.js.map +1 -0
  38. package/dist/capabilities/HandoffCapability.d.ts +19 -0
  39. package/dist/capabilities/HandoffCapability.d.ts.map +1 -0
  40. package/dist/capabilities/HandoffCapability.js +58 -0
  41. package/dist/capabilities/HandoffCapability.js.map +1 -0
  42. package/dist/capabilities/LivePromptAssembler.d.ts +108 -0
  43. package/dist/capabilities/LivePromptAssembler.d.ts.map +1 -0
  44. package/dist/capabilities/LivePromptAssembler.js +157 -0
  45. package/dist/capabilities/LivePromptAssembler.js.map +1 -0
  46. package/dist/capabilities/TriageCapability.d.ts +16 -0
  47. package/dist/capabilities/TriageCapability.d.ts.map +1 -0
  48. package/dist/capabilities/TriageCapability.js +61 -0
  49. package/dist/capabilities/TriageCapability.js.map +1 -0
  50. package/dist/capabilities/adapters/ai-sdk.d.ts +14 -0
  51. package/dist/capabilities/adapters/ai-sdk.d.ts.map +1 -0
  52. package/dist/capabilities/adapters/ai-sdk.js +29 -0
  53. package/dist/capabilities/adapters/ai-sdk.js.map +1 -0
  54. package/dist/capabilities/adapters/gemini.d.ts +15 -0
  55. package/dist/capabilities/adapters/gemini.d.ts.map +1 -0
  56. package/dist/capabilities/adapters/gemini.js +40 -0
  57. package/dist/capabilities/adapters/gemini.js.map +1 -0
  58. package/dist/capabilities/index.d.ts +154 -0
  59. package/dist/capabilities/index.d.ts.map +1 -0
  60. package/dist/capabilities/index.js +128 -0
  61. package/dist/capabilities/index.js.map +1 -0
  62. package/dist/eval/EvalRunner.d.ts +12 -0
  63. package/dist/eval/EvalRunner.d.ts.map +1 -0
  64. package/dist/eval/EvalRunner.js +64 -0
  65. package/dist/eval/EvalRunner.js.map +1 -0
  66. package/dist/eval/scoring.d.ts +15 -0
  67. package/dist/eval/scoring.d.ts.map +1 -0
  68. package/dist/eval/scoring.js +152 -0
  69. package/dist/eval/scoring.js.map +1 -0
  70. package/dist/eval/types.d.ts +59 -0
  71. package/dist/eval/types.d.ts.map +1 -0
  72. package/dist/eval/types.js +2 -0
  73. package/dist/eval/types.js.map +1 -0
  74. package/dist/flows/FlowGraph.d.ts +3 -1
  75. package/dist/flows/FlowGraph.d.ts.map +1 -1
  76. package/dist/flows/FlowGraph.js +5 -0
  77. package/dist/flows/FlowGraph.js.map +1 -1
  78. package/dist/flows/FlowManager.d.ts +68 -1
  79. package/dist/flows/FlowManager.d.ts.map +1 -1
  80. package/dist/flows/FlowManager.js +499 -36
  81. package/dist/flows/FlowManager.js.map +1 -1
  82. package/dist/flows/extraction.d.ts +16 -1
  83. package/dist/flows/extraction.d.ts.map +1 -1
  84. package/dist/flows/extraction.js +34 -0
  85. package/dist/flows/extraction.js.map +1 -1
  86. package/dist/flows/index.d.ts +2 -0
  87. package/dist/flows/index.d.ts.map +1 -1
  88. package/dist/flows/index.js +1 -0
  89. package/dist/flows/index.js.map +1 -1
  90. package/dist/flows/validation.d.ts +1 -1
  91. package/dist/flows/validation.d.ts.map +1 -1
  92. package/dist/flows/validation.js +13 -1
  93. package/dist/flows/validation.js.map +1 -1
  94. package/dist/foundation/AgentDefinition.d.ts +18 -0
  95. package/dist/foundation/AgentDefinition.d.ts.map +1 -0
  96. package/dist/foundation/AgentDefinition.js +2 -0
  97. package/dist/foundation/AgentDefinition.js.map +1 -0
  98. package/dist/foundation/AgentStateController.d.ts +26 -0
  99. package/dist/foundation/AgentStateController.d.ts.map +1 -0
  100. package/dist/foundation/AgentStateController.js +2 -0
  101. package/dist/foundation/AgentStateController.js.map +1 -0
  102. package/dist/foundation/ConversationEventLog.d.ts +72 -0
  103. package/dist/foundation/ConversationEventLog.d.ts.map +1 -0
  104. package/dist/foundation/ConversationEventLog.js +2 -0
  105. package/dist/foundation/ConversationEventLog.js.map +1 -0
  106. package/dist/foundation/ConversationState.d.ts +31 -0
  107. package/dist/foundation/ConversationState.d.ts.map +1 -0
  108. package/dist/foundation/ConversationState.js +2 -0
  109. package/dist/foundation/ConversationState.js.map +1 -0
  110. package/dist/foundation/DefaultAgentStateController.d.ts +24 -0
  111. package/dist/foundation/DefaultAgentStateController.d.ts.map +1 -0
  112. package/dist/foundation/DefaultAgentStateController.js +49 -0
  113. package/dist/foundation/DefaultAgentStateController.js.map +1 -0
  114. package/dist/foundation/DefaultConversationEventLog.d.ts +28 -0
  115. package/dist/foundation/DefaultConversationEventLog.d.ts.map +1 -0
  116. package/dist/foundation/DefaultConversationEventLog.js +195 -0
  117. package/dist/foundation/DefaultConversationEventLog.js.map +1 -0
  118. package/dist/foundation/DefaultConversationState.d.ts +34 -0
  119. package/dist/foundation/DefaultConversationState.d.ts.map +1 -0
  120. package/dist/foundation/DefaultConversationState.js +100 -0
  121. package/dist/foundation/DefaultConversationState.js.map +1 -0
  122. package/dist/foundation/DefaultToolExecutor.d.ts +58 -0
  123. package/dist/foundation/DefaultToolExecutor.d.ts.map +1 -0
  124. package/dist/foundation/DefaultToolExecutor.js +128 -0
  125. package/dist/foundation/DefaultToolExecutor.js.map +1 -0
  126. package/dist/foundation/ToolExecutor.d.ts +44 -0
  127. package/dist/foundation/ToolExecutor.d.ts.map +1 -0
  128. package/dist/foundation/ToolExecutor.js +2 -0
  129. package/dist/foundation/ToolExecutor.js.map +1 -0
  130. package/dist/foundation/createFoundation.d.ts +33 -0
  131. package/dist/foundation/createFoundation.d.ts.map +1 -0
  132. package/dist/foundation/createFoundation.js +34 -0
  133. package/dist/foundation/createFoundation.js.map +1 -0
  134. package/dist/foundation/index.d.ts +15 -0
  135. package/dist/foundation/index.d.ts.map +1 -0
  136. package/dist/foundation/index.js +8 -0
  137. package/dist/foundation/index.js.map +1 -0
  138. package/dist/hooks/HookRunner.d.ts +5 -1
  139. package/dist/hooks/HookRunner.d.ts.map +1 -1
  140. package/dist/hooks/HookRunner.js +7 -0
  141. package/dist/hooks/HookRunner.js.map +1 -1
  142. package/dist/hooks/builtin/metrics.d.ts.map +1 -1
  143. package/dist/hooks/builtin/metrics.js +12 -0
  144. package/dist/hooks/builtin/metrics.js.map +1 -1
  145. package/dist/hooks/builtin/observability.d.ts +21 -0
  146. package/dist/hooks/builtin/observability.d.ts.map +1 -0
  147. package/dist/hooks/builtin/observability.js +535 -0
  148. package/dist/hooks/builtin/observability.js.map +1 -0
  149. package/dist/index.d.ts +24 -3
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +16 -2
  152. package/dist/index.js.map +1 -1
  153. package/dist/memory/MemoryService.d.ts +40 -0
  154. package/dist/memory/MemoryService.d.ts.map +1 -0
  155. package/dist/memory/MemoryService.js +2 -0
  156. package/dist/memory/MemoryService.js.map +1 -0
  157. package/dist/memory/index.d.ts +5 -0
  158. package/dist/memory/index.d.ts.map +1 -0
  159. package/dist/memory/index.js +3 -0
  160. package/dist/memory/index.js.map +1 -0
  161. package/dist/memory/preloadMemory.d.ts +17 -0
  162. package/dist/memory/preloadMemory.d.ts.map +1 -0
  163. package/dist/memory/preloadMemory.js +62 -0
  164. package/dist/memory/preloadMemory.js.map +1 -0
  165. package/dist/memory/stores/InMemoryMemoryService.d.ts +20 -0
  166. package/dist/memory/stores/InMemoryMemoryService.d.ts.map +1 -0
  167. package/dist/memory/stores/InMemoryMemoryService.js +92 -0
  168. package/dist/memory/stores/InMemoryMemoryService.js.map +1 -0
  169. package/dist/memory/types.d.ts +49 -0
  170. package/dist/memory/types.d.ts.map +1 -0
  171. package/dist/memory/types.js +8 -0
  172. package/dist/memory/types.js.map +1 -0
  173. package/dist/orchestration/DefaultOrchestrationAuthority.d.ts +91 -0
  174. package/dist/orchestration/DefaultOrchestrationAuthority.d.ts.map +1 -0
  175. package/dist/orchestration/DefaultOrchestrationAuthority.js +786 -0
  176. package/dist/orchestration/DefaultOrchestrationAuthority.js.map +1 -0
  177. package/dist/orchestration/OrchestrationAuthority.d.ts +119 -0
  178. package/dist/orchestration/OrchestrationAuthority.d.ts.map +1 -0
  179. package/dist/orchestration/OrchestrationAuthority.js +2 -0
  180. package/dist/orchestration/OrchestrationAuthority.js.map +1 -0
  181. package/dist/orchestration/RealtimeExtractionRunner.d.ts +25 -0
  182. package/dist/orchestration/RealtimeExtractionRunner.d.ts.map +1 -0
  183. package/dist/orchestration/RealtimeExtractionRunner.js +62 -0
  184. package/dist/orchestration/RealtimeExtractionRunner.js.map +1 -0
  185. package/dist/orchestration/index.d.ts +5 -0
  186. package/dist/orchestration/index.d.ts.map +1 -0
  187. package/dist/orchestration/index.js +4 -0
  188. package/dist/orchestration/index.js.map +1 -0
  189. package/dist/orchestration/types.d.ts +134 -0
  190. package/dist/orchestration/types.d.ts.map +1 -0
  191. package/dist/orchestration/types.js +2 -0
  192. package/dist/orchestration/types.js.map +1 -0
  193. package/dist/prompts/AgentPrompt.d.ts +110 -0
  194. package/dist/prompts/AgentPrompt.d.ts.map +1 -0
  195. package/dist/prompts/AgentPrompt.js +373 -0
  196. package/dist/prompts/AgentPrompt.js.map +1 -0
  197. package/dist/prompts/PromptAssembly.d.ts +119 -0
  198. package/dist/prompts/PromptAssembly.d.ts.map +1 -0
  199. package/dist/prompts/PromptAssembly.js +150 -0
  200. package/dist/prompts/PromptAssembly.js.map +1 -0
  201. package/dist/prompts/PromptBuilder.d.ts +22 -3
  202. package/dist/prompts/PromptBuilder.d.ts.map +1 -1
  203. package/dist/prompts/PromptBuilder.js +242 -13
  204. package/dist/prompts/PromptBuilder.js.map +1 -1
  205. package/dist/prompts/PromptRenderer.d.ts +43 -0
  206. package/dist/prompts/PromptRenderer.d.ts.map +1 -0
  207. package/dist/prompts/PromptRenderer.js +114 -0
  208. package/dist/prompts/PromptRenderer.js.map +1 -0
  209. package/dist/prompts/brandVoice.d.ts +10 -0
  210. package/dist/prompts/brandVoice.d.ts.map +1 -0
  211. package/dist/prompts/brandVoice.js +87 -0
  212. package/dist/prompts/brandVoice.js.map +1 -0
  213. package/dist/prompts/index.d.ts +11 -4
  214. package/dist/prompts/index.d.ts.map +1 -1
  215. package/dist/prompts/index.js +7 -2
  216. package/dist/prompts/index.js.map +1 -1
  217. package/dist/prompts/security.d.ts +5 -0
  218. package/dist/prompts/security.d.ts.map +1 -0
  219. package/dist/prompts/security.js +52 -0
  220. package/dist/prompts/security.js.map +1 -0
  221. package/dist/prompts/types.d.ts +65 -1
  222. package/dist/prompts/types.d.ts.map +1 -1
  223. package/dist/prompts/types.js +26 -0
  224. package/dist/prompts/types.js.map +1 -1
  225. package/dist/realtime/RealtimeAudioClient.d.ts +105 -0
  226. package/dist/realtime/RealtimeAudioClient.d.ts.map +1 -0
  227. package/dist/realtime/RealtimeAudioClient.js +15 -0
  228. package/dist/realtime/RealtimeAudioClient.js.map +1 -0
  229. package/dist/realtime/RealtimeRuntime.d.ts +136 -0
  230. package/dist/realtime/RealtimeRuntime.d.ts.map +1 -0
  231. package/dist/realtime/RealtimeRuntime.js +270 -0
  232. package/dist/realtime/RealtimeRuntime.js.map +1 -0
  233. package/dist/realtime/index.d.ts +4 -0
  234. package/dist/realtime/index.d.ts.map +1 -0
  235. package/dist/realtime/index.js +2 -0
  236. package/dist/realtime/index.js.map +1 -0
  237. package/dist/runtime/ContextBudget.d.ts +57 -0
  238. package/dist/runtime/ContextBudget.d.ts.map +1 -0
  239. package/dist/runtime/ContextBudget.js +103 -0
  240. package/dist/runtime/ContextBudget.js.map +1 -0
  241. package/dist/runtime/ContextManager.d.ts +8 -5
  242. package/dist/runtime/ContextManager.d.ts.map +1 -1
  243. package/dist/runtime/ContextManager.js +47 -14
  244. package/dist/runtime/ContextManager.js.map +1 -1
  245. package/dist/runtime/ExtractionEngine.d.ts +2 -1
  246. package/dist/runtime/ExtractionEngine.d.ts.map +1 -1
  247. package/dist/runtime/ExtractionEngine.js +11 -0
  248. package/dist/runtime/ExtractionEngine.js.map +1 -1
  249. package/dist/runtime/FlowExecutor.d.ts +22 -15
  250. package/dist/runtime/FlowExecutor.d.ts.map +1 -1
  251. package/dist/runtime/FlowExecutor.js +102 -149
  252. package/dist/runtime/FlowExecutor.js.map +1 -1
  253. package/dist/runtime/Runtime.d.ts +53 -78
  254. package/dist/runtime/Runtime.d.ts.map +1 -1
  255. package/dist/runtime/Runtime.js +272 -1406
  256. package/dist/runtime/Runtime.js.map +1 -1
  257. package/dist/runtime/SessionCache.d.ts +16 -0
  258. package/dist/runtime/SessionCache.d.ts.map +1 -0
  259. package/dist/runtime/SessionCache.js +49 -0
  260. package/dist/runtime/SessionCache.js.map +1 -0
  261. package/dist/runtime/SessionMutex.d.ts +37 -0
  262. package/dist/runtime/SessionMutex.d.ts.map +1 -0
  263. package/dist/runtime/SessionMutex.js +59 -0
  264. package/dist/runtime/SessionMutex.js.map +1 -0
  265. package/dist/runtime/StreamEmitter.d.ts +34 -0
  266. package/dist/runtime/StreamEmitter.d.ts.map +1 -0
  267. package/dist/runtime/StreamEmitter.js +91 -0
  268. package/dist/runtime/StreamEmitter.js.map +1 -0
  269. package/dist/runtime/handoffFilters.d.ts +60 -0
  270. package/dist/runtime/handoffFilters.d.ts.map +1 -0
  271. package/dist/runtime/handoffFilters.js +95 -0
  272. package/dist/runtime/handoffFilters.js.map +1 -0
  273. package/dist/runtime/pipeline/AgentExecuteStage.d.ts +22 -0
  274. package/dist/runtime/pipeline/AgentExecuteStage.d.ts.map +1 -0
  275. package/dist/runtime/pipeline/AgentExecuteStage.js +958 -0
  276. package/dist/runtime/pipeline/AgentExecuteStage.js.map +1 -0
  277. package/dist/runtime/pipeline/ContextAssembleStage.d.ts +26 -0
  278. package/dist/runtime/pipeline/ContextAssembleStage.d.ts.map +1 -0
  279. package/dist/runtime/pipeline/ContextAssembleStage.js +253 -0
  280. package/dist/runtime/pipeline/ContextAssembleStage.js.map +1 -0
  281. package/dist/runtime/pipeline/ContextGatherStage.d.ts +21 -0
  282. package/dist/runtime/pipeline/ContextGatherStage.d.ts.map +1 -0
  283. package/dist/runtime/pipeline/ContextGatherStage.js +161 -0
  284. package/dist/runtime/pipeline/ContextGatherStage.js.map +1 -0
  285. package/dist/runtime/pipeline/IntakeStage.d.ts +25 -0
  286. package/dist/runtime/pipeline/IntakeStage.d.ts.map +1 -0
  287. package/dist/runtime/pipeline/IntakeStage.js +126 -0
  288. package/dist/runtime/pipeline/IntakeStage.js.map +1 -0
  289. package/dist/runtime/pipeline/PostStreamStage.d.ts +26 -0
  290. package/dist/runtime/pipeline/PostStreamStage.d.ts.map +1 -0
  291. package/dist/runtime/pipeline/PostStreamStage.js +129 -0
  292. package/dist/runtime/pipeline/PostStreamStage.js.map +1 -0
  293. package/dist/runtime/pipeline/TurnPipeline.d.ts +54 -0
  294. package/dist/runtime/pipeline/TurnPipeline.d.ts.map +1 -0
  295. package/dist/runtime/pipeline/TurnPipeline.js +15 -0
  296. package/dist/runtime/pipeline/TurnPipeline.js.map +1 -0
  297. package/dist/runtime/pipeline/TurnServices.d.ts +48 -0
  298. package/dist/runtime/pipeline/TurnServices.d.ts.map +1 -0
  299. package/dist/runtime/pipeline/TurnServices.js +2 -0
  300. package/dist/runtime/pipeline/TurnServices.js.map +1 -0
  301. package/dist/runtime/pipeline/agentTypeGuards.d.ts +4 -0
  302. package/dist/runtime/pipeline/agentTypeGuards.d.ts.map +1 -0
  303. package/dist/runtime/pipeline/agentTypeGuards.js +7 -0
  304. package/dist/runtime/pipeline/agentTypeGuards.js.map +1 -0
  305. package/dist/runtime/pipeline/index.d.ts +11 -0
  306. package/dist/runtime/pipeline/index.d.ts.map +1 -0
  307. package/dist/runtime/pipeline/index.js +13 -0
  308. package/dist/runtime/pipeline/index.js.map +1 -0
  309. package/dist/runtime/pipeline/outputProcessing.d.ts +23 -0
  310. package/dist/runtime/pipeline/outputProcessing.d.ts.map +1 -0
  311. package/dist/runtime/pipeline/outputProcessing.js +63 -0
  312. package/dist/runtime/pipeline/outputProcessing.js.map +1 -0
  313. package/dist/runtime/pipeline/sessionUtils.d.ts +12 -0
  314. package/dist/runtime/pipeline/sessionUtils.d.ts.map +1 -0
  315. package/dist/runtime/pipeline/sessionUtils.js +73 -0
  316. package/dist/runtime/pipeline/sessionUtils.js.map +1 -0
  317. package/dist/tools/Tool.d.ts +7 -0
  318. package/dist/tools/Tool.d.ts.map +1 -1
  319. package/dist/tools/Tool.js +12 -3
  320. package/dist/tools/Tool.js.map +1 -1
  321. package/dist/tools/memory.d.ts +26 -0
  322. package/dist/tools/memory.d.ts.map +1 -0
  323. package/dist/tools/memory.js +51 -0
  324. package/dist/tools/memory.js.map +1 -0
  325. package/dist/types/index.d.ts +238 -9
  326. package/dist/types/index.d.ts.map +1 -1
  327. package/dist/types/index.js +4 -0
  328. package/dist/types/index.js.map +1 -1
  329. package/dist/types/telemetry.d.ts +107 -0
  330. package/dist/types/telemetry.d.ts.map +1 -1
  331. package/guides/AGENTS.md +173 -0
  332. package/guides/README.md +12 -0
  333. package/guides/TOOLS.md +93 -27
  334. package/package.json +25 -4
  335. package/dist/agents/LLMAgent.d.ts +0 -11
  336. package/dist/agents/LLMAgent.d.ts.map +0 -1
  337. package/dist/agents/LLMAgent.js +0 -31
  338. package/dist/agents/LLMAgent.js.map +0 -1
@@ -1,16 +1,21 @@
1
1
  import { streamText, generateText, tool } from 'ai';
2
2
  import { z } from 'zod';
3
+ import { isExtractionNode, } from '../types/index.js';
3
4
  import { createFlowTransition, isFlowTransition, isFlowUpdate } from './transitions.js';
5
+ import { extractStructuredFields, mergeExtractionData, computeMissingFields, toNullableSchema } from './extraction.js';
4
6
  import { validateFlowConfig } from './validation.js';
5
7
  import { isHandoffResult } from '../tools/handoff.js';
6
8
  import { isFinalResult } from '../tools/final.js';
7
9
  import { compileSanitizePattern, renderNodePrompt } from './template.js';
8
10
  import { normalizeModelMessage } from '../utils/messageNormalization.js';
9
11
  import { processAIStream } from '../utils/aiStream.js';
12
+ import { FlowCapability } from '../capabilities/FlowCapability.js';
10
13
  const implicitTransitionToolInputSchema = z.object({
11
14
  data: z.record(z.unknown()).optional(),
12
15
  message: z.string().optional(),
13
16
  });
17
+ /** Default max number of times the same A->B transition can repeat in a single turn. */
18
+ const DEFAULT_MAX_OSCILLATIONS = 2;
14
19
  export class FlowManager {
15
20
  config;
16
21
  nodes = new Map();
@@ -22,9 +27,37 @@ export class FlowManager {
22
27
  deferredActions = [];
23
28
  flowEnded = false;
24
29
  sessionMessages;
30
+ /**
31
+ * Headless FlowCapability used to share state-management code with
32
+ * CapabilityCallWorker. FlowManager rebuilds this lazily from its own
33
+ * authoritative state (context/initialized/flowEnded) whenever a getter
34
+ * or resolveTools() needs it — keeping streaming logic untouched.
35
+ */
36
+ flowCapability;
37
+ /**
38
+ * Tracks transition edges (from->to) within a single process() call.
39
+ * Reset at the start of each user turn. Used to detect oscillation
40
+ * (e.g., triage->services->triage->services) and break infinite loops.
41
+ */
42
+ turnTransitionCounts = new Map();
43
+ /** Maximum times the same from->to edge can fire in one turn before being blocked. */
44
+ maxOscillations;
45
+ pendingMetrics = [];
46
+ emitMetric(name, data) {
47
+ this.pendingMetrics.push({ name, data });
48
+ this.config.metricsEmitter?.(name, data);
49
+ }
50
+ /** Drain queued metrics as custom stream events. Call from any generator. */
51
+ *drainMetrics() {
52
+ while (this.pendingMetrics.length > 0) {
53
+ const metric = this.pendingMetrics.shift();
54
+ yield { type: 'custom', name: metric.name, data: metric.data, timestamp: new Date() };
55
+ }
56
+ }
25
57
  constructor(config) {
26
58
  this.config = config;
27
59
  validateFlowConfig(config.flow, config.initialNode);
60
+ this.maxOscillations = config.flow.maxOscillations ?? DEFAULT_MAX_OSCILLATIONS;
28
61
  for (const node of config.flow.nodes) {
29
62
  this.nodes.set(node.id, node);
30
63
  }
@@ -61,6 +94,7 @@ export class FlowManager {
61
94
  yield* this.runInference(false);
62
95
  }
63
96
  yield* this.flushDeferredActions();
97
+ yield* this.drainMetrics();
64
98
  }
65
99
  async *process(userInput) {
66
100
  if (!this.initialized) {
@@ -74,6 +108,8 @@ export class FlowManager {
74
108
  yield { type: 'error', error: 'No current node' };
75
109
  return;
76
110
  }
111
+ // Reset oscillation tracker at the start of each user turn
112
+ this.turnTransitionCounts.clear();
77
113
  this.appendMessage({ role: 'user', content: userInput });
78
114
  const nodeId = this.currentNodeConfig.id;
79
115
  this.context.nodeTurnCounts[nodeId] = (this.context.nodeTurnCounts[nodeId] ?? 0) + 1;
@@ -83,6 +119,7 @@ export class FlowManager {
83
119
  }
84
120
  yield* this.runInference(true);
85
121
  yield* this.flushDeferredActions();
122
+ yield* this.drainMetrics();
86
123
  }
87
124
  async transitionTo(nodeId, data) {
88
125
  for await (const _part of this.transitionToGenerator(nodeId, data)) {
@@ -95,6 +132,7 @@ export class FlowManager {
95
132
  }
96
133
  }
97
134
  async *transitionToGenerator(nodeId, data, dynamicNode) {
135
+ const transitionStart = Date.now();
98
136
  if (!this.nodes.has(nodeId) && dynamicNode) {
99
137
  this.nodes.set(nodeId, dynamicNode);
100
138
  }
@@ -103,6 +141,33 @@ export class FlowManager {
103
141
  throw new Error(`Node "${nodeId}" not found`);
104
142
  }
105
143
  const previousNode = this.currentNodeConfig;
144
+ // Oscillation detection: if the same from->to edge fires too many times
145
+ // in a single user turn, break the loop to prevent infinite ping-pong.
146
+ if (previousNode) {
147
+ const edgeKey = `${previousNode.id}->${nodeId}`;
148
+ const count = (this.turnTransitionCounts.get(edgeKey) ?? 0) + 1;
149
+ this.turnTransitionCounts.set(edgeKey, count);
150
+ if (count > this.maxOscillations) {
151
+ yield {
152
+ type: 'error',
153
+ error: `Flow oscillation detected: "${previousNode.id}" and "${nodeId}" have been ` +
154
+ `transitioning back and forth ${count} times in this turn. Breaking the loop. ` +
155
+ `The agent will respond from the current node "${previousNode.id}" without transitioning.`,
156
+ };
157
+ // Stay on the current node — do NOT transition
158
+ return;
159
+ }
160
+ }
161
+ // Output contract validation: validate exiting node's outputSchema
162
+ if (previousNode?.outputSchema) {
163
+ const result = previousNode.outputSchema.safeParse(this.context.collectedData);
164
+ if (!result.success) {
165
+ this.emitMetric('flow.contract.validation_fail', { nodeId: previousNode.id, direction: 'output' });
166
+ yield { type: 'error', error: `Output contract violation on node "${previousNode.id}": ${result.error.message}` };
167
+ return;
168
+ }
169
+ this.emitMetric('flow.contract.validation_pass', { nodeId: previousNode.id, direction: 'output' });
170
+ }
106
171
  if (previousNode) {
107
172
  yield* this.runActions(previousNode.postActions ?? []);
108
173
  if (this.flowEnded) {
@@ -117,6 +182,16 @@ export class FlowManager {
117
182
  Object.assign(this.context.collectedData, data);
118
183
  }
119
184
  await this.applyContextStrategy(nextNode);
185
+ // Input contract validation: validate entering node's inputSchema
186
+ if (nextNode.inputSchema) {
187
+ const result = nextNode.inputSchema.safeParse(this.context.collectedData);
188
+ if (!result.success) {
189
+ this.emitMetric('flow.contract.validation_fail', { nodeId: nextNode.id, direction: 'input' });
190
+ yield { type: 'error', error: `Input contract violation on node "${nextNode.id}": ${result.error.message}` };
191
+ return;
192
+ }
193
+ this.emitMetric('flow.contract.validation_pass', { nodeId: nextNode.id, direction: 'input' });
194
+ }
120
195
  yield { type: 'node-enter', nodeName: nextNode.name ?? nextNode.id };
121
196
  yield* this.runActions(nextNode.preActions ?? []);
122
197
  if (this.flowEnded) {
@@ -125,28 +200,59 @@ export class FlowManager {
125
200
  if (previousNode) {
126
201
  yield { type: 'flow-transition', from: previousNode.id, to: nextNode.id };
127
202
  }
203
+ this.emitMetric('flow.transition.duration', {
204
+ durationMs: Date.now() - transitionStart,
205
+ from: previousNode?.id ?? '__init__',
206
+ to: nodeId,
207
+ });
128
208
  }
129
209
  get collectedData() {
130
- return this.context.collectedData;
210
+ return this.rebuildCapability().collectedData;
131
211
  }
132
212
  get currentNode() {
133
- return this.currentNodeConfig?.id;
213
+ return this.rebuildCapability().currentNode;
134
214
  }
135
215
  get hasEnded() {
136
- return this.flowEnded;
216
+ return this.rebuildCapability().hasEnded;
137
217
  }
138
218
  getState() {
219
+ const capState = this.rebuildCapability().getState();
139
220
  return {
221
+ context: capState.context,
222
+ initialized: capState.initialized,
223
+ flowEnded: capState.flowEnded,
224
+ };
225
+ }
226
+ /**
227
+ * Build a FlowCapability snapshot from FlowManager's current authoritative state.
228
+ * FlowManager owns transitions/streaming; FlowCapability owns state-query logic.
229
+ * Rebuilding on each call is cheap (pure in-memory graph traversal, no I/O).
230
+ */
231
+ rebuildCapability() {
232
+ const state = {
140
233
  context: this.context,
141
234
  initialized: this.initialized,
142
235
  flowEnded: this.flowEnded,
143
236
  };
237
+ this.flowCapability = new FlowCapability({
238
+ flow: this.config.flow,
239
+ initialNode: this.config.initialNode,
240
+ defaultRolePrompt: this.config.defaultRolePrompt,
241
+ state,
242
+ });
243
+ return this.flowCapability;
144
244
  }
145
245
  async *runInference(triggeredByUserTurn) {
146
246
  if (this.flowEnded || !this.currentNodeConfig) {
147
247
  return;
148
248
  }
249
+ const inferenceStart = Date.now();
149
250
  const node = this.currentNodeConfig;
251
+ // Delegate to extraction node handler if applicable
252
+ if (isExtractionNode(node)) {
253
+ yield* this.runExtractionNodeInference(node, triggeredByUserTurn);
254
+ return;
255
+ }
150
256
  const systemPrompt = this.buildSystemPrompt(node);
151
257
  const tools = this.resolveTools(node);
152
258
  const maxSteps = node.maxSteps ?? this.config.maxSteps ?? this.config.flow.maxSteps ?? 25;
@@ -177,12 +283,21 @@ export class FlowManager {
177
283
  let finalEmitted = false;
178
284
  let hadToolResult = false;
179
285
  let pendingToolMessage = null;
286
+ let ttftEmitted = false;
180
287
  for await (const part of processAIStream(result.fullStream)) {
181
288
  // In buffered mode, hold text-delta tokens until we decide whether to emit.
182
289
  // This allows transition/handoff turns to stay silent in routing nodes.
183
290
  if (!(shouldBuffer && part.type === 'text-delta')) {
184
291
  yield part;
185
292
  }
293
+ // Emit TTFT metric on first text-delta
294
+ if (part.type === 'text-delta' && !ttftEmitted) {
295
+ ttftEmitted = true;
296
+ this.emitMetric('flow.inference.ttft', {
297
+ durationMs: Date.now() - inferenceStart,
298
+ nodeId: node.id,
299
+ });
300
+ }
186
301
  if (part.type === 'text-delta') {
187
302
  if (finalResult) {
188
303
  continue;
@@ -244,6 +359,20 @@ export class FlowManager {
244
359
  }
245
360
  }
246
361
  const response = await result.response;
362
+ // Emit inference duration and cache metrics
363
+ this.emitMetric('flow.inference.duration', {
364
+ durationMs: Date.now() - inferenceStart,
365
+ nodeId: node.id,
366
+ });
367
+ const anthropicMeta = response.providerMetadata?.anthropic;
368
+ if (anthropicMeta) {
369
+ if (typeof anthropicMeta.cacheReadInputTokens === 'number' && anthropicMeta.cacheReadInputTokens > 0) {
370
+ this.emitMetric('flow.cache.hit_tokens', { tokens: anthropicMeta.cacheReadInputTokens, nodeId: node.id });
371
+ }
372
+ if (typeof anthropicMeta.cacheCreationInputTokens === 'number' && anthropicMeta.cacheCreationInputTokens > 0) {
373
+ this.emitMetric('flow.cache.miss_tokens', { tokens: anthropicMeta.cacheCreationInputTokens, nodeId: node.id });
374
+ }
375
+ }
247
376
  let safeText = responseText;
248
377
  if (shouldBuffer && node.output?.sanitize?.pattern && node.output?.sanitize?.message) {
249
378
  const re = compileSanitizePattern(node.output.sanitize.pattern);
@@ -340,15 +469,25 @@ export class FlowManager {
340
469
  yield { type: 'flow-end', reason: 'post-action' };
341
470
  }
342
471
  }
472
+ /**
473
+ * Builds the system prompt as an array of SystemModelMessage objects.
474
+ * Layer 1 (role prompt) is marked with Anthropic cache_control for prompt caching.
475
+ * Uses AI SDK SystemModelMessage format: { role: 'system', content: string, providerOptions? }.
476
+ */
343
477
  buildSystemPrompt(node) {
344
478
  const includeGlobalPrompt = node.addGlobalPrompt !== false;
345
479
  const rolePrompt = includeGlobalPrompt
346
480
  ? (this.config.defaultRolePrompt ?? this.config.flow.defaultRolePrompt ?? '')
347
481
  : '';
348
482
  const renderedPrompt = renderNodePrompt(node.prompt, this.context);
349
- const dataKeys = Object.keys(this.context.collectedData);
483
+ // Filter collectedData to relevantFields if specified (Phase 3: selective data inclusion)
484
+ const allData = this.context.collectedData;
485
+ const filteredData = node.relevantFields
486
+ ? Object.fromEntries(Object.entries(allData).filter(([k]) => node.relevantFields.includes(k)))
487
+ : allData;
488
+ const dataKeys = Object.keys(filteredData);
350
489
  const dataContext = dataKeys.length > 0
351
- ? `\n\n## Collected Information\n${JSON.stringify(this.context.collectedData, null, 2)}`
490
+ ? `\n\n## Collected Information\n${JSON.stringify(filteredData, null, 2)}`
352
491
  : '';
353
492
  const historyContext = this.context.nodeHistory.length > 1
354
493
  ? `\n\n## Progress\n${this.context.nodeHistory.slice(0, -1).join(' -> ')} -> **${node.name ?? node.id}**`
@@ -359,13 +498,22 @@ ${renderedPrompt}${dataContext}${historyContext}`;
359
498
  - Focus only on the current task
360
499
  - Use the available tools to progress the conversation
361
500
  - Do not attempt tasks outside your current scope`;
362
- const sections = [];
501
+ const blocks = [];
502
+ // Layer 1: Role prompt (cacheable -- stable across all turns and nodes)
363
503
  if (rolePrompt.trim().length > 0) {
364
- sections.push(rolePrompt.trim());
504
+ blocks.push({
505
+ role: 'system',
506
+ content: rolePrompt.trim(),
507
+ providerOptions: {
508
+ anthropic: { cacheControl: { type: 'ephemeral' } },
509
+ },
510
+ });
365
511
  }
366
- sections.push(currentTaskSection);
367
- sections.push(instructionsSection);
368
- return sections.join('\n\n');
512
+ // Layer 2: Node-specific task context (changes per node)
513
+ blocks.push({ role: 'system', content: currentTaskSection });
514
+ // Layer 3: Fixed instructions
515
+ blocks.push({ role: 'system', content: instructionsSection });
516
+ return blocks;
369
517
  }
370
518
  async applyContextStrategy(node) {
371
519
  const strategy = node.contextStrategy ?? this.config.flow.contextStrategy ?? 'append';
@@ -389,6 +537,7 @@ ${renderedPrompt}${dataContext}${historyContext}`;
389
537
  }
390
538
  }
391
539
  async generateSummary(node) {
540
+ const summaryStart = Date.now();
392
541
  const prompt = node.summaryPrompt
393
542
  ?? this.config.flow.summaryPrompt
394
543
  ?? 'Summarize the key points from this conversation in 2-3 sentences.';
@@ -406,20 +555,33 @@ ${renderedPrompt}${dataContext}${historyContext}`;
406
555
  });
407
556
  return result.text;
408
557
  };
558
+ let summary;
409
559
  if (timeoutMs <= 0) {
410
- return await generate();
560
+ summary = await generate();
411
561
  }
412
- let timeoutHandle;
413
- const timeout = new Promise(resolve => {
414
- timeoutHandle = setTimeout(() => resolve(null), timeoutMs);
415
- });
416
- const summary = await Promise.race([generate().catch(() => null), timeout]);
417
- if (timeoutHandle) {
418
- clearTimeout(timeoutHandle);
562
+ else {
563
+ let timeoutHandle;
564
+ const timeout = new Promise(resolve => {
565
+ timeoutHandle = setTimeout(() => resolve(null), timeoutMs);
566
+ });
567
+ summary = await Promise.race([generate().catch(() => null), timeout]);
568
+ if (timeoutHandle) {
569
+ clearTimeout(timeoutHandle);
570
+ }
419
571
  }
572
+ this.emitMetric('flow.summary.duration', {
573
+ durationMs: Date.now() - summaryStart,
574
+ nodeId: node.id,
575
+ timedOut: summary === null,
576
+ });
420
577
  return summary;
421
578
  }
422
579
  catch {
580
+ this.emitMetric('flow.summary.duration', {
581
+ durationMs: Date.now() - summaryStart,
582
+ nodeId: node.id,
583
+ error: true,
584
+ });
423
585
  return null;
424
586
  }
425
587
  }
@@ -478,23 +640,26 @@ ${renderedPrompt}${dataContext}${historyContext}`;
478
640
  },
479
641
  };
480
642
  }
643
+ // When multiple transitions are requested in the same turn, take the first one.
644
+ // The LLM calls tools in order of intent -- the first tool call is the primary action.
645
+ // Conflicting targets or payloads are warned, not errored, to avoid leaving the user
646
+ // with no response. The first transition is the safest bet because it corresponds
647
+ // to the LLM's first (and usually correct) tool call.
481
648
  const firstTransition = transitions[0];
482
649
  for (let i = 1; i < transitions.length; i++) {
483
650
  const next = transitions[i];
484
651
  if (next.targetNode !== firstTransition.targetNode) {
485
- return {
486
- conflict: `Conflicting transitions requested in the same turn: "${firstTransition.targetNode}" and "${next.targetNode}".`,
487
- };
652
+ console.warn(`[AriaFlow] Conflicting transitions in the same turn: "${firstTransition.targetNode}" (tool: ${firstTransition.toolName}) ` +
653
+ `vs "${next.targetNode}" (tool: ${next.toolName}). Taking the first transition "${firstTransition.targetNode}".`);
654
+ break;
488
655
  }
489
656
  if (!this.isEquivalentPayload(next.data, firstTransition.data)) {
490
- return {
491
- conflict: `Conflicting transition payloads for node "${firstTransition.targetNode}" in the same turn.`,
492
- };
657
+ console.warn(`[AriaFlow] Conflicting transition payloads for node "${firstTransition.targetNode}" in the same turn. Using the first payload.`);
658
+ break;
493
659
  }
494
660
  if ((next.node?.id ?? null) !== (firstTransition.node?.id ?? null)) {
495
- return {
496
- conflict: `Conflicting dynamic node definitions for transition target "${firstTransition.targetNode}" in the same turn.`,
497
- };
661
+ console.warn(`[AriaFlow] Conflicting dynamic node definitions for transition target "${firstTransition.targetNode}" in the same turn. Using the first definition.`);
662
+ break;
498
663
  }
499
664
  }
500
665
  return {
@@ -506,13 +671,25 @@ ${renderedPrompt}${dataContext}${historyContext}`;
506
671
  };
507
672
  }
508
673
  resolveTools(node) {
509
- const implicitTools = this.buildImplicitTransitionTools(node);
510
- let explicitTools = {};
511
- if (node.tools) {
512
- explicitTools =
513
- typeof node.tools === 'function'
514
- ? (node.tools(this.context) ?? {})
515
- : node.tools;
674
+ // Resolve explicit node tools first so we know which names to exclude.
675
+ const explicitTools = node.tools
676
+ ? (typeof node.tools === 'function' ? (node.tools(this.context) ?? {}) : node.tools)
677
+ : {};
678
+ const explicitNames = new Set(Object.keys(explicitTools));
679
+ // Use FlowCapability to generate implicit transition tool declarations.
680
+ // This shares tool-building logic with CapabilityCallWorker; FlowManager
681
+ // wraps each ToolDeclaration in the AI SDK tool() format for streamText.
682
+ const cap = this.rebuildCapability();
683
+ const implicitTools = {};
684
+ for (const decl of cap.getTools()) {
685
+ // Skip explicit node tools — they will be merged in below with higher priority.
686
+ if (explicitNames.has(decl.name))
687
+ continue;
688
+ implicitTools[decl.name] = tool({
689
+ description: decl.description,
690
+ inputSchema: decl.parameters,
691
+ execute: decl.execute,
692
+ });
516
693
  }
517
694
  // Explicit node tools win on name collision to preserve backwards compatibility.
518
695
  return this.wrapTools({
@@ -556,11 +733,11 @@ ${renderedPrompt}${dataContext}${historyContext}`;
556
733
  return tools;
557
734
  const wrapped = {};
558
735
  for (const [toolName, toolDef] of Object.entries(tools ?? {})) {
559
- const exec = toolDef?.execute;
560
- if (typeof exec !== 'function') {
736
+ if (!('execute' in toolDef) || typeof toolDef.execute !== 'function') {
561
737
  wrapped[toolName] = toolDef;
562
738
  continue;
563
739
  }
740
+ const exec = toolDef.execute;
564
741
  wrapped[toolName] = {
565
742
  ...toolDef,
566
743
  execute: async (args, options) => {
@@ -712,6 +889,292 @@ ${renderedPrompt}${dataContext}${historyContext}`;
712
889
  }
713
890
  return {};
714
891
  }
892
+ /**
893
+ * Extraction node inference: loops until a Zod schema is fully satisfied.
894
+ * Replaces both the ExtractionEngine's generateObject call and the standard
895
+ * streamText inference -- single LLM call per turn.
896
+ *
897
+ * Three key correctness properties:
898
+ * 1. safeParse uses nullified collectedData (undefined -> null) so nullable
899
+ * schema fields don't fail on absent keys.
900
+ * 2. The follow-up prompt only mentions REQUIRED missing fields, not optional ones.
901
+ * 3. On auto-transition, if the next node is also an extraction node, the last
902
+ * user message is re-extracted against the new schema (cross-node carry-forward).
903
+ */
904
+ async *runExtractionNodeInference(node, triggeredByUserTurn) {
905
+ const extractionStart = Date.now();
906
+ const turnCount = this.context.nodeTurnCounts[node.id] ?? 0;
907
+ // Check if extraction is already complete from prior turns.
908
+ // Fill undefined schema keys with null so .nullable() fields pass safeParse.
909
+ if (this.isExtractionComplete(node)) {
910
+ this.emitMetric('flow.extraction.complete', { nodeId: node.id, turns: turnCount });
911
+ if (node.extractionCompleteTransition) {
912
+ yield* this.transitionToGenerator(node.extractionCompleteTransition, this.context.collectedData);
913
+ if (!this.flowEnded && this.currentNodeConfig) {
914
+ // Cross-node carry-forward: if the next node is also an extraction node,
915
+ // re-extract from the last user message against the new schema.
916
+ if (isExtractionNode(this.currentNodeConfig)) {
917
+ yield* this.runExtractionCarryForward(this.currentNodeConfig);
918
+ }
919
+ else if (this.currentNodeConfig.autoRespond !== false) {
920
+ yield* this.runInference(false);
921
+ }
922
+ }
923
+ }
924
+ return;
925
+ }
926
+ // Enforce max extraction turns
927
+ const maxTurns = node.extractionMaxTurns ?? 10;
928
+ if (turnCount > maxTurns) {
929
+ this.emitMetric('flow.extraction.max_turns_exceeded', { nodeId: node.id, turnCount });
930
+ yield { type: 'error', error: `Extraction node "${node.id}" exceeded max turns (${maxTurns})` };
931
+ if (node.extractionCompleteTransition) {
932
+ yield* this.transitionToGenerator(node.extractionCompleteTransition, this.context.collectedData);
933
+ if (!this.flowEnded && this.currentNodeConfig?.autoRespond !== false) {
934
+ yield* this.runInference(false);
935
+ }
936
+ }
937
+ return;
938
+ }
939
+ // Extract structured fields from the latest user message.
940
+ // Use a nullable version of the schema so the LLM can return null for
941
+ // fields not mentioned, instead of hallucinating values.
942
+ //
943
+ // Run extraction if:
944
+ // - This was triggered by a user turn (normal extraction loop), OR
945
+ // - This is the first entry to the extraction node (turnCount === 0)
946
+ // and there is a user message available (e.g., the user said something
947
+ // that triggered a transition from a non-extraction node).
948
+ const shouldExtract = (triggeredByUserTurn || turnCount === 0) && this.context.messages.length > 0;
949
+ if (shouldExtract) {
950
+ await this.extractFromLastUserMessage(node);
951
+ }
952
+ // Re-check after extraction
953
+ if (this.isExtractionComplete(node)) {
954
+ const reqFields = node.extractionRequiredFields ?? Object.keys(node.extractionSchema?.shape ?? {});
955
+ const doneCollected = {};
956
+ for (const key of reqFields) {
957
+ const v = this.context.collectedData[key];
958
+ if (v !== undefined && v !== null) {
959
+ doneCollected[key] = v;
960
+ }
961
+ }
962
+ this.emitMetric('flow.extraction.update', {
963
+ nodeId: node.id,
964
+ collected: doneCollected,
965
+ missing: [],
966
+ });
967
+ this.emitMetric('flow.extraction.complete', { nodeId: node.id, turns: turnCount });
968
+ this.emitMetric('flow.extraction.duration', { durationMs: Date.now() - extractionStart, nodeId: node.id });
969
+ yield* this.drainMetrics();
970
+ if (node.extractionCompleteTransition) {
971
+ yield* this.transitionToGenerator(node.extractionCompleteTransition, this.context.collectedData);
972
+ if (!this.flowEnded && this.currentNodeConfig) {
973
+ if (isExtractionNode(this.currentNodeConfig)) {
974
+ yield* this.runExtractionCarryForward(this.currentNodeConfig);
975
+ }
976
+ else if (this.currentNodeConfig.autoRespond !== false) {
977
+ yield* this.runInference(false);
978
+ }
979
+ }
980
+ }
981
+ return;
982
+ }
983
+ // Compute missing REQUIRED fields only -- do not ask for optional fields.
984
+ const requiredFields = node.extractionRequiredFields
985
+ ?? Object.keys(node.extractionSchema?.shape ?? {});
986
+ const missingFields = computeMissingFields(this.context.collectedData, requiredFields);
987
+ const collectedSnapshot = {};
988
+ for (const key of requiredFields) {
989
+ const v = this.context.collectedData[key];
990
+ if (v !== undefined && v !== null) {
991
+ collectedSnapshot[key] = v;
992
+ }
993
+ }
994
+ this.emitMetric('flow.extraction.update', {
995
+ nodeId: node.id,
996
+ collected: collectedSnapshot,
997
+ missing: missingFields,
998
+ });
999
+ this.emitMetric('flow.extraction.fields.collected', {
1000
+ nodeId: node.id,
1001
+ total: requiredFields.length,
1002
+ collected: requiredFields.length - missingFields.length,
1003
+ missing: missingFields.length,
1004
+ });
1005
+ yield* this.drainMetrics();
1006
+ // If all REQUIRED fields are present but safeParse still fails (e.g., validation
1007
+ // errors like min length), check which fields have validation issues.
1008
+ if (missingFields.length === 0) {
1009
+ // All required fields exist but schema validation failed.
1010
+ // This means a field has an invalid value (wrong format, too short, etc.).
1011
+ // Auto-transition with what we have rather than looping forever.
1012
+ this.emitMetric('flow.extraction.complete', { nodeId: node.id, turns: turnCount });
1013
+ this.emitMetric('flow.extraction.duration', { durationMs: Date.now() - extractionStart, nodeId: node.id });
1014
+ if (node.extractionCompleteTransition) {
1015
+ yield* this.transitionToGenerator(node.extractionCompleteTransition, this.context.collectedData);
1016
+ if (!this.flowEnded && this.currentNodeConfig?.autoRespond !== false) {
1017
+ yield* this.runInference(false);
1018
+ }
1019
+ }
1020
+ return;
1021
+ }
1022
+ // Generate follow-up prompt asking ONLY for required missing fields.
1023
+ const promptMode = node.extractionPromptMode ?? 'llm';
1024
+ if (promptMode === 'template') {
1025
+ const prompts = missingFields.map(field => node.extractionFieldPrompts?.[field] ?? `Could you please provide your ${field}?`);
1026
+ const followUpText = prompts.join(' ');
1027
+ yield { type: 'text-delta', text: followUpText };
1028
+ this.appendMessage({ role: 'assistant', content: followUpText });
1029
+ }
1030
+ else {
1031
+ const systemPrompt = this.buildSystemPrompt(node);
1032
+ const collectedSummary = Object.entries(this.context.collectedData)
1033
+ .filter(([, v]) => v !== null && v !== undefined)
1034
+ .map(([k, v]) => ` ${k}: ${v}`)
1035
+ .join('\n');
1036
+ const extractionContext = `\n\nYou are collecting required information. ` +
1037
+ `Already collected:\n${collectedSummary || ' (nothing yet)'}\n` +
1038
+ `Still needed: ${missingFields.join(', ')}.\n` +
1039
+ `Ask ONLY for the missing fields listed above. Do not ask for anything else. Be concise.`;
1040
+ const blocks = [...systemPrompt];
1041
+ if (blocks.length > 0) {
1042
+ blocks[blocks.length - 1] = {
1043
+ ...blocks[blocks.length - 1],
1044
+ content: blocks[blocks.length - 1].content + extractionContext,
1045
+ };
1046
+ }
1047
+ const streamOptions = {
1048
+ model: this.config.model,
1049
+ system: blocks,
1050
+ abortSignal: this.config.abortSignal,
1051
+ experimental_telemetry: this.config.telemetry,
1052
+ };
1053
+ if (this.context.messages.length === 0) {
1054
+ streamOptions.prompt = renderNodePrompt(node.prompt, this.context);
1055
+ }
1056
+ else {
1057
+ streamOptions.messages = this.context.messages;
1058
+ }
1059
+ const result = streamText(streamOptions);
1060
+ let responseText = '';
1061
+ for await (const part of processAIStream(result.fullStream)) {
1062
+ yield part;
1063
+ if (part.type === 'text-delta') {
1064
+ responseText += part.text;
1065
+ }
1066
+ }
1067
+ const response = await result.response;
1068
+ for (const message of response.messages) {
1069
+ this.appendMessage(message);
1070
+ }
1071
+ yield { type: 'turn-end', metadata: { response } };
1072
+ }
1073
+ this.emitMetric('flow.extraction.duration', {
1074
+ durationMs: Date.now() - extractionStart,
1075
+ nodeId: node.id,
1076
+ });
1077
+ }
1078
+ /**
1079
+ * Check if extraction is complete by running safeParse with nullified data.
1080
+ * Fills undefined schema keys with null so .nullable() fields don't fail
1081
+ * on absent keys (undefined !== null in Zod).
1082
+ */
1083
+ isExtractionComplete(node) {
1084
+ const dataForParse = { ...this.context.collectedData };
1085
+ const schemaShape = node.extractionSchema?.shape;
1086
+ if (schemaShape) {
1087
+ for (const key of Object.keys(schemaShape)) {
1088
+ if (!(key in dataForParse)) {
1089
+ dataForParse[key] = null;
1090
+ }
1091
+ }
1092
+ }
1093
+ return node.extractionSchema.safeParse(dataForParse).success;
1094
+ }
1095
+ /**
1096
+ * Extract structured fields from the last user message against a node's schema.
1097
+ * Returns the number of NEW fields that were extracted (0 if nothing new).
1098
+ * Used both for normal extraction turns and cross-node carry-forward.
1099
+ */
1100
+ async extractFromLastUserMessage(node) {
1101
+ try {
1102
+ const lastUserMsg = this.context.messages
1103
+ .slice()
1104
+ .reverse()
1105
+ .find((m) => m.role === 'user');
1106
+ const userInput = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
1107
+ if (userInput.length > 0) {
1108
+ const nullableSchema = toNullableSchema(node.extractionSchema);
1109
+ const before = { ...this.context.collectedData };
1110
+ const extracted = await extractStructuredFields({
1111
+ model: this.config.model,
1112
+ schema: nullableSchema,
1113
+ userMessage: userInput,
1114
+ systemPrompt: node.extraction?.systemPrompt
1115
+ ?? 'Extract only facts explicitly stated in the latest user message. Return null for any field not clearly provided. Do not infer or guess.',
1116
+ abortSignal: this.config.abortSignal,
1117
+ telemetry: this.config.telemetry,
1118
+ });
1119
+ const merged = mergeExtractionData(this.context.collectedData, extracted);
1120
+ // Count how many fields are genuinely new (not already in collectedData)
1121
+ let newFields = 0;
1122
+ for (const [key, value] of Object.entries(merged)) {
1123
+ if (value !== null && value !== undefined && before[key] !== value) {
1124
+ newFields++;
1125
+ }
1126
+ }
1127
+ Object.assign(this.context.collectedData, merged);
1128
+ return newFields;
1129
+ }
1130
+ }
1131
+ catch {
1132
+ // Extraction failed; continue with what we have
1133
+ }
1134
+ return 0;
1135
+ }
1136
+ /**
1137
+ * Cross-node extraction carry-forward: when transitioning from one extraction
1138
+ * node to another, re-extract from the last user message against the new node's
1139
+ * schema. This handles the case where a user provides data for multiple schemas
1140
+ * in a single message (e.g., incident details AND vehicle details in one turn).
1141
+ *
1142
+ * Stops the cascade when zero new fields are extracted -- the user message has
1143
+ * no data for this schema, so further hops would waste LLM calls.
1144
+ */
1145
+ async *runExtractionCarryForward(nextNode) {
1146
+ // Try to extract from the last user message against the new schema.
1147
+ const newFieldCount = await this.extractFromLastUserMessage(nextNode);
1148
+ // If we extracted nothing new, stop cascading. The user message has no data
1149
+ // for this schema. Ask the user for the missing information instead of
1150
+ // burning LLM calls on downstream nodes.
1151
+ if (newFieldCount === 0) {
1152
+ if (nextNode.autoRespond !== false) {
1153
+ yield* this.runInference(false);
1154
+ }
1155
+ return;
1156
+ }
1157
+ // If carry-forward satisfied the schema, continue the cascade.
1158
+ if (this.isExtractionComplete(nextNode)) {
1159
+ this.emitMetric('flow.extraction.complete', { nodeId: nextNode.id, turns: 0 });
1160
+ if (nextNode.extractionCompleteTransition) {
1161
+ yield* this.transitionToGenerator(nextNode.extractionCompleteTransition, this.context.collectedData);
1162
+ if (!this.flowEnded && this.currentNodeConfig) {
1163
+ if (isExtractionNode(this.currentNodeConfig)) {
1164
+ yield* this.runExtractionCarryForward(this.currentNodeConfig);
1165
+ }
1166
+ else if (this.currentNodeConfig.autoRespond !== false) {
1167
+ yield* this.runInference(false);
1168
+ }
1169
+ }
1170
+ }
1171
+ return;
1172
+ }
1173
+ // Extracted some fields but not enough -- ask for the rest.
1174
+ if (nextNode.autoRespond !== false) {
1175
+ yield* this.runInference(false);
1176
+ }
1177
+ }
715
1178
  }
716
1179
  function isRecord(value) {
717
1180
  return typeof value === 'object' && value !== null;