@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,10 +1,3 @@
1
- import crypto from 'node:crypto';
2
- import { streamText, generateText, Output, stepCountIs } from 'ai';
3
- import { z } from 'zod';
4
- import { createHandoffTool, isHandoffResult } from '../tools/handoff.js';
5
- import { isFinalResult } from '../tools/final.js';
6
- import { InjectionQueue, getPolicyProfileInjections } from './InjectionQueue.js';
7
- import { checkStopConditions, defaultStopConditions } from '../guards/StopConditions.js';
8
1
  import { HookRunner } from '../hooks/HookRunner.js';
9
2
  import { ToolEnforcer } from '../guards/ToolEnforcer.js';
10
3
  import { defaultEnforcementRules } from '../guards/rules.js';
@@ -12,169 +5,224 @@ import { MemoryStore } from '../session/stores/MemoryStore.js';
12
5
  import { createHttpCallback } from '../callbacks/httpCallback.js';
13
6
  import { createStreamCallbackAdapter } from '../callbacks/streamCallback.js';
14
7
  import { compileSanitizePattern } from '../flows/template.js';
15
- import { runInputProcessors, runOutputProcessors } from '../processors/ProcessorRunner.js';
16
- import { getChunkArgs, getChunkErrorMessage, getChunkResult, getChunkToolCallId } from '../utils/streamChunk.js';
17
- import { normalizeModelMessage } from '../utils/messageNormalization.js';
18
- import { isRecord } from '../utils/isRecord.js';
8
+ import { defaultStopConditions } from '../guards/StopConditions.js';
19
9
  import { ExtractionEngine } from './ExtractionEngine.js';
20
- import { SessionEventManager } from './SessionEventManager.js';
21
10
  import { SuggestionManager } from './SuggestionManager.js';
22
11
  import { FlowExecutor } from './FlowExecutor.js';
23
- import { SessionWorkingMemory } from './WorkingMemory.js';
12
+ import { DefaultToolExecutor } from '../foundation/DefaultToolExecutor.js';
13
+ import { DefaultConversationState } from '../foundation/DefaultConversationState.js';
14
+ import { DefaultConversationEventLog } from '../foundation/DefaultConversationEventLog.js';
15
+ import { DefaultAgentStateController } from '../foundation/DefaultAgentStateController.js';
16
+ import { SessionCache } from './SessionCache.js';
17
+ import { StreamEmitter } from './StreamEmitter.js';
18
+ import { SessionMutex } from './SessionMutex.js';
19
+ import { runIntakeStage } from './pipeline/IntakeStage.js';
20
+ import { runAgentExecuteStage } from './pipeline/AgentExecuteStage.js';
21
+ import { runPostStreamStage } from './pipeline/PostStreamStage.js';
22
+ import { DefaultOrchestrationAuthority } from '../orchestration/DefaultOrchestrationAuthority.js';
24
23
  export class Runtime {
25
24
  config;
26
- agents = new Map();
27
- defaultAgentId;
28
- defaultModel;
29
- maxSteps;
30
- maxHandoffs;
31
- stopConditions;
32
- _sessionStore;
33
- hookRunner;
34
- enforcer;
35
- contextManager;
36
- turnCount = 0;
37
25
  abortControllers = new Map();
38
- alwaysRouteThroughTriage;
39
- triageAgentId;
40
- inputProcessors;
41
- outputProcessors;
42
- outputProcessorMode;
43
- outputRedactions;
44
- redactCarryKey = '__ariaRedactCarry';
45
- redactLookbehind = 64;
46
- runtimeEventLogKey = 'runtimeEventLog';
47
- runtimeSessionTurnKey = '__ariaSessionTurn';
48
- runtimeEventLogMaxEntries = 2000;
49
- checkpointEventTypes = new Set([
50
- 'tool-result',
51
- 'tool-error',
52
- 'flow-transition',
53
- ]);
54
- extractionEngine;
55
- sessionEventManager;
56
- suggestionManager;
57
- flowExecutor;
58
- wrapToolsWithEnforcement(context, tools) {
59
- console.log('[Runtime] Wrapping tools:', Object.keys(tools));
60
- const wrapped = {};
61
- for (const [toolName, toolDef] of Object.entries(tools ?? {})) {
62
- const exec = toolDef?.execute;
63
- if (typeof exec !== 'function') {
64
- wrapped[toolName] = toolDef;
65
- continue;
66
- }
67
- wrapped[toolName] = {
68
- ...toolDef,
69
- execute: async (args, options) => {
70
- const toolCallId = typeof options?.toolCallId === 'string'
71
- ? options.toolCallId
72
- : crypto.randomUUID();
73
- const idempotencyKey = this.buildToolIdempotencyKey(context, toolName, toolCallId);
74
- const callRecord = {
75
- toolCallId,
76
- toolName,
77
- args,
78
- idempotencyKey,
79
- success: true,
80
- timestamp: Date.now(),
81
- };
82
- const enforcement = await this.enforcer.check(callRecord, {
83
- previousCalls: context.toolCallHistory,
84
- currentStep: context.stepCount,
85
- sessionState: context.session.state ?? {},
86
- });
87
- if (!enforcement.allowed) {
88
- const reason = enforcement.reason ?? 'Tool call blocked by enforcement';
89
- callRecord.success = false;
90
- callRecord.error = new Error(reason);
91
- context.toolCallHistory.push(callRecord);
92
- await this.hookRunner.onToolError(context, callRecord, callRecord.error);
93
- throw callRecord.error;
94
- }
95
- // Preserve AI SDK tool execution context (toolCallId, messages, experimental_context, etc.).
96
- const enrichedOptions = this.withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey);
97
- try {
98
- console.log(`[Runtime] Executing tool ${toolName}...`);
99
- return await exec(args, enrichedOptions);
100
- }
101
- catch (error) {
102
- console.error(`[Runtime] Tool execution failed for ${toolName}:`, error);
103
- throw error;
104
- }
105
- },
106
- };
107
- }
108
- return wrapped;
109
- }
26
+ _userIdWarned = new Set();
27
+ sessionMutex = new SessionMutex();
28
+ turnServices;
29
+ _authority;
30
+ // Keep references for public API getters
31
+ _sessionStore;
32
+ _memoryService;
33
+ _toolExecutor;
34
+ _conversationState;
35
+ _eventLog;
36
+ _agentStateController;
110
37
  constructor(config) {
111
38
  this.config = config;
39
+ // Build agent map
40
+ const agents = new Map();
112
41
  for (const agent of config.agents) {
113
- this.agents.set(agent.id, agent);
42
+ agents.set(agent.id, agent);
43
+ }
44
+ // Config scalars
45
+ const defaultAgentId = config.defaultAgentId;
46
+ const defaultModel = config.defaultModel;
47
+ const maxSteps = config.maxSteps ?? 20;
48
+ const maxHandoffs = config.maxHandoffs ?? 10;
49
+ const stopConditions = config.stopConditions ?? defaultStopConditions;
50
+ const sessionStore = config.sessionStore ?? new MemoryStore();
51
+ const inputProcessors = config.inputProcessors ?? [];
52
+ const outputProcessors = config.outputProcessors ?? [];
53
+ const outputProcessorMode = config.outputProcessorMode ?? 'stream';
54
+ const alwaysRouteThroughTriage = config.alwaysRouteThroughTriage ?? false;
55
+ const triageAgentId = config.triageAgentId;
56
+ // Output redactions
57
+ let outputRedactions;
58
+ if (config.outputRedaction && config.outputRedaction.length > 0) {
59
+ outputRedactions = config.outputRedaction.map(r => {
60
+ const base = compileSanitizePattern(r.pattern);
61
+ const flags = base.flags.includes('g') ? base.flags : `${base.flags}g`;
62
+ const re = new RegExp(base.source, flags);
63
+ return { re, replacement: r.replacement };
64
+ });
114
65
  }
115
- this.defaultAgentId = config.defaultAgentId;
116
- this.defaultModel = config.defaultModel;
117
- this.maxSteps = config.maxSteps ?? 20;
118
- this.maxHandoffs = config.maxHandoffs ?? 10;
119
- this.stopConditions = config.stopConditions ?? defaultStopConditions;
120
- this._sessionStore = config.sessionStore ?? new MemoryStore();
66
+ // Hooks setup
121
67
  const hooks = { ...config.hooks };
122
68
  const appendOnStreamPart = (fn) => {
123
69
  const originalHook = hooks.onStreamPart;
124
70
  hooks.onStreamPart = async (context, part) => {
125
- if (originalHook) {
71
+ if (originalHook)
126
72
  await originalHook(context, part);
127
- }
128
73
  await fn(context, part);
129
74
  };
130
75
  };
131
76
  if (config.callback) {
132
- const httpCallback = createHttpCallback(config.callback);
133
- appendOnStreamPart(httpCallback);
77
+ appendOnStreamPart(createHttpCallback(config.callback));
134
78
  }
135
79
  if (config.streamCallback) {
136
80
  const adapter = createStreamCallbackAdapter(config.streamCallback);
137
81
  appendOnStreamPart(adapter.onStreamPart);
138
82
  const originalOnEnd = hooks.onEnd;
139
83
  hooks.onEnd = async (context, result) => {
140
- if (originalOnEnd) {
84
+ if (originalOnEnd)
141
85
  await originalOnEnd(context, result);
142
- }
143
86
  if (config.streamCallback?.flushOnEnd) {
144
87
  await adapter.flush(config.streamCallback.flushTimeoutMs ?? 2000);
145
88
  }
146
89
  };
147
90
  }
148
- this.hookRunner = new HookRunner(hooks);
149
- this.enforcer = new ToolEnforcer(config.enforcementRules ?? defaultEnforcementRules);
150
- this.contextManager = config.contextManager;
151
- this.alwaysRouteThroughTriage = config.alwaysRouteThroughTriage ?? false;
152
- this.triageAgentId = config.triageAgentId;
153
- this.inputProcessors = config.inputProcessors ?? [];
154
- this.outputProcessors = config.outputProcessors ?? [];
155
- this.outputProcessorMode = config.outputProcessorMode ?? 'stream';
156
- if (config.outputRedaction && config.outputRedaction.length > 0) {
157
- this.outputRedactions = config.outputRedaction.map(r => {
158
- const base = compileSanitizePattern(r.pattern);
159
- const flags = base.flags.includes('g') ? base.flags : `${base.flags}g`;
160
- const re = new RegExp(base.source, flags);
161
- return { re, replacement: r.replacement };
162
- });
163
- }
164
- this.extractionEngine = new ExtractionEngine({
165
- defaultModel: this.defaultModel,
166
- telemetry: config.telemetry,
91
+ const hookRunner = new HookRunner(hooks);
92
+ const enforcer = new ToolEnforcer(config.enforcementRules ?? defaultEnforcementRules);
93
+ const contextManager = config.contextManager;
94
+ // Service instances
95
+ const extractionEngine = new ExtractionEngine({ defaultModel, telemetry: config.telemetry });
96
+ const suggestionManager = new SuggestionManager(config);
97
+ const flowExecutor = new FlowExecutor(config);
98
+ const memoryService = config.memoryService;
99
+ const conversationState = new DefaultConversationState({
100
+ sessionStore,
101
+ defaultAgentId,
167
102
  });
168
- this.sessionEventManager = new SessionEventManager({
169
- touchSession: session => this.touchSession(session),
170
- getSessionTurn: session => this.getSessionTurn(session),
103
+ const eventLog = new DefaultConversationEventLog({ sessionStore });
104
+ const agentStateController = new DefaultAgentStateController();
105
+ const toolExecutor = new DefaultToolExecutor({
106
+ enforcer,
107
+ hookRunner,
108
+ memoryService,
109
+ });
110
+ let sessionCache;
111
+ if (config.sessionCache) {
112
+ sessionCache = new SessionCache(config.sessionCache);
113
+ }
114
+ const streamEmitter = new StreamEmitter({
115
+ hookRunner,
116
+ eventLog,
117
+ hasStreamPartHook: Boolean(hooks.onStreamPart),
118
+ outputRedactions,
119
+ redactCarryKey: '__ariaRedactCarry',
120
+ redactLookbehind: 64,
121
+ saveSessionCheckpoint: async (session) => {
122
+ await eventLog.checkpoint(session);
123
+ if (sessionCache)
124
+ sessionCache.put(session);
125
+ },
126
+ });
127
+ // Store references for public API
128
+ this._sessionStore = sessionStore;
129
+ this._memoryService = memoryService;
130
+ this._toolExecutor = toolExecutor;
131
+ this._conversationState = conversationState;
132
+ this._eventLog = eventLog;
133
+ this._agentStateController = agentStateController;
134
+ // Build TurnServices — one plain struct, no closures
135
+ this.turnServices = {
136
+ conversationState,
137
+ agentStateController,
138
+ eventLog,
139
+ toolExecutor,
140
+ streamEmitter,
141
+ hookRunner,
142
+ enforcer,
143
+ extractionEngine,
144
+ suggestionManager,
145
+ flowExecutor,
146
+ contextManager,
147
+ memoryService,
148
+ sessionCache,
149
+ sessionStore,
150
+ agents,
151
+ config,
152
+ defaultAgentId,
153
+ defaultModel,
154
+ maxSteps,
155
+ maxHandoffs,
156
+ stopConditions,
157
+ inputProcessors,
158
+ outputProcessors,
159
+ outputProcessorMode,
160
+ outputRedactions,
161
+ alwaysRouteThroughTriage,
162
+ triageAgentId,
163
+ };
164
+ // Build OrchestrationAuthority that shares the same backing services.
165
+ // This establishes the architectural connection: both text (this Runtime)
166
+ // and realtime (RealtimeRuntime) can compose the same authority, sharing
167
+ // session store, hooks, tool executor, and all foundation services.
168
+ this._authority = new DefaultOrchestrationAuthority({
169
+ agents: config.agents,
170
+ defaultAgentId,
171
+ hooks,
172
+ sessionStore,
173
+ memoryService,
174
+ memoryIngestion: config.memoryIngestion,
175
+ defaultModel,
176
+ extractionModel: config.extractionModel,
177
+ enforcementRules: config.enforcementRules,
178
+ telemetry: config.telemetry,
179
+ injectedServices: {
180
+ toolExecutor,
181
+ conversationState,
182
+ eventLog,
183
+ agentState: agentStateController,
184
+ hookRunner,
185
+ },
171
186
  });
172
- this.suggestionManager = new SuggestionManager(config);
173
- this.flowExecutor = new FlowExecutor(config);
174
187
  }
188
+ // --- Public API ---
175
189
  get sessionStore() {
176
190
  return this._sessionStore;
177
191
  }
192
+ get memoryService() {
193
+ return this._memoryService;
194
+ }
195
+ /** Foundation services — shared primitives reusable by VoiceEngine etc. */
196
+ get foundation() {
197
+ return {
198
+ toolExecutor: this._toolExecutor,
199
+ conversationState: this._conversationState,
200
+ eventLog: this._eventLog,
201
+ agentState: this._agentStateController,
202
+ };
203
+ }
204
+ /**
205
+ * The core orchestration authority shared by this Runtime.
206
+ *
207
+ * This authority uses the same backing services (session store, hook runner,
208
+ * tool executor, etc.) as the text pipeline. Use it to construct a
209
+ * RealtimeRuntime that shares state with this Runtime:
210
+ *
211
+ * ```typescript
212
+ * const runtime = createRuntime(config);
213
+ * const realtimeRuntime = new RealtimeRuntime({
214
+ * agents: config.agents,
215
+ * defaultAgentId: config.defaultAgentId,
216
+ * authority: runtime.authority,
217
+ * });
218
+ * ```
219
+ *
220
+ * This ensures text and realtime sessions share the same session store,
221
+ * the same hooks fire for both paths, and agent state is consistent.
222
+ */
223
+ get authority() {
224
+ return this._authority;
225
+ }
178
226
  getActiveAbortController(sessionId) {
179
227
  return this.abortControllers.get(sessionId);
180
228
  }
@@ -185,144 +233,84 @@ export class Runtime {
185
233
  this.abortControllers.delete(sessionId);
186
234
  }
187
235
  }
236
+ async getSession(id) {
237
+ return this._sessionStore.get(id);
238
+ }
239
+ async deleteSession(id) {
240
+ await this._sessionStore.delete(id);
241
+ }
242
+ getAgent(id) {
243
+ return this.turnServices.agents.get(id);
244
+ }
245
+ getAllAgents() {
246
+ return Array.from(this.turnServices.agents.values());
247
+ }
248
+ async *chat(sessionId, input, userId) {
249
+ yield* this.stream({ input, sessionId, userId });
250
+ }
251
+ // --- Main orchestration ---
188
252
  async *stream(options) {
189
- const { userId, abortSignal } = options;
190
- const input = options.input;
191
- if (typeof input !== 'string') {
192
- throw new Error('Runtime.stream: "input" is required and must be a string.');
193
- }
194
- const effectiveSessionId = options.sessionId ?? crypto.randomUUID();
195
- let session;
253
+ const svc = this.turnServices;
254
+ // Warn when memoryService is configured but userId is missing
255
+ if (svc.memoryService && !options.userId) {
256
+ const sessionId = options.sessionId ?? 'new';
257
+ if (!this._userIdWarned.has(sessionId)) {
258
+ this._userIdWarned.add(sessionId);
259
+ console.warn('[AriaFlow] memoryService is configured but session has no userId. ' +
260
+ 'Memory operations will be skipped. Pass userId via stream({ input, sessionId, userId }).');
261
+ }
262
+ }
263
+ // Acquire per-session mutex to prevent concurrent read-modify-write races.
264
+ // Only lock when a sessionId is provided (existing session). New sessions
265
+ // get a random UUID and cannot have concurrent callers.
266
+ let releaseMutex;
196
267
  if (options.sessionId) {
197
- const existing = await this._sessionStore.get(options.sessionId);
198
- session = existing ?? this.createSession(effectiveSessionId, userId);
268
+ releaseMutex = await this.sessionMutex.acquire(options.sessionId);
199
269
  }
200
- else {
201
- session = this.createSession(effectiveSessionId, userId);
202
- }
203
- if (this.alwaysRouteThroughTriage) {
204
- // If a flow agent is currently active and has an initialized, non-ended flow state,
205
- // keep routing to it so multi-turn flows can complete without triage interference.
206
- const currentAgentId = session.activeAgentId ?? session.currentAgent;
207
- const currentAgent = currentAgentId ? this.agents.get(currentAgentId) : undefined;
208
- const hasActiveFlow = Boolean(currentAgent) &&
209
- this.isFlowAgent(currentAgent) &&
210
- Boolean(this.getFlowState(session, currentAgent.id)?.initialized) &&
211
- !Boolean(this.getFlowState(session, currentAgent.id)?.flowEnded);
212
- if (!hasActiveFlow) {
213
- const triageId = this.triageAgentId ?? this.defaultAgentId;
214
- const triageAgent = this.agents.get(triageId);
215
- if (triageAgent && this.isTriageAgent(triageAgent)) {
216
- session.activeAgentId = triageId;
217
- session.currentAgent = triageId;
218
- }
219
- }
270
+ try {
271
+ yield* this._streamInner(options, svc);
220
272
  }
221
- const activeAgentId = session.activeAgentId ?? session.currentAgent ?? this.defaultAgentId;
222
- session.activeAgentId = activeAgentId;
223
- session.currentAgent = activeAgentId;
224
- this.turnCount++;
225
- const context = {
226
- session,
227
- agentId: activeAgentId,
228
- stepCount: 0,
229
- totalTokens: 0,
230
- handoffStack: [],
231
- startTime: Date.now(),
232
- consecutiveErrors: 0,
233
- toolCallHistory: [],
234
- };
235
- this.bumpSessionTurn(session);
236
- const injectionQueue = this.buildPolicyInjectionQueue();
237
- // Yield input through emit so hooks see it (once).
238
- yield* this.emit(context, { type: 'input', text: input, userId: session.userId || undefined });
239
- const controller = new AbortController();
273
+ finally {
274
+ releaseMutex?.();
275
+ }
276
+ }
277
+ /**
278
+ * Inner stream implementation — separated so the mutex can wrap the
279
+ * entire async generator lifecycle from IntakeStage through PostStreamStage.
280
+ */
281
+ async *_streamInner(options, svc) {
282
+ // Stage 1: Intake
283
+ const intakeGen = runIntakeStage(options, svc);
284
+ let intakeNext = await intakeGen.next();
285
+ while (!intakeNext.done) {
286
+ yield intakeNext.value;
287
+ intakeNext = await intakeGen.next();
288
+ }
289
+ const intake = intakeNext.value;
290
+ if (!intake)
291
+ return;
292
+ const { session, context, processedInput, injectionQueue, controller } = intake;
240
293
  this.abortControllers.set(session.id, controller);
241
- const abortHandler = () => {
242
- this.abortControllers.delete(session.id);
243
- };
244
- controller.signal.addEventListener('abort', abortHandler);
245
- let externalAbortHandler;
246
- if (abortSignal) {
247
- externalAbortHandler = () => {
248
- controller.abort(abortSignal.reason ?? 'External abort');
249
- };
250
- if (abortSignal.aborted) {
251
- controller.abort(abortSignal.reason ?? 'External abort');
252
- }
253
- else {
254
- abortSignal.addEventListener('abort', externalAbortHandler);
255
- }
256
- }
257
- await this.hookRunner.onStart(context);
258
- // Run input processors BEFORE persisting the user message.
259
- let processedInput = input;
294
+ const cleanup = this.setupAbortHandlers(session.id, controller, options.abortSignal);
260
295
  try {
261
- const turnInputProcessors = [...this.inputProcessors];
262
- if (turnInputProcessors.length > 0) {
263
- const candidateMessages = [
264
- ...session.messages,
265
- { role: 'user', content: processedInput },
266
- ];
267
- const procCtx = {
268
- session,
269
- agentId: activeAgentId,
270
- toolCallHistory: context.toolCallHistory,
271
- abortSignal: controller.signal,
272
- };
273
- const outcome = await runInputProcessors({
274
- processors: turnInputProcessors,
275
- input: processedInput,
276
- messages: candidateMessages,
277
- context: procCtx,
278
- });
279
- if (outcome.blocked) {
280
- yield* this.emit(context, {
281
- type: 'tripwire',
282
- phase: 'input',
283
- processorId: outcome.processorId,
284
- reason: outcome.reason,
285
- message: outcome.message,
286
- });
287
- yield* this.emit(context, { type: 'text-delta', text: outcome.message });
288
- yield* this.emit(context, { type: 'turn-end' });
289
- await this.hookRunner.onEnd(context, { success: true });
290
- return;
291
- }
292
- processedInput = outcome.input;
293
- }
294
- // Persist the (possibly modified) user message after input processors.
295
- this.appendSessionMessage(session, { role: 'user', content: processedInput });
296
- if (this.contextManager) {
297
- const messagesBefore = session.messages.length;
298
- const totalTokens = session.messages.reduce((sum, m) => {
299
- const text = typeof m.content === 'string' ? m.content : '';
300
- return sum + Math.ceil(text.length / 4);
301
- }, 0);
302
- const compacted = await this.contextManager.beforeTurn(session.messages, {
303
- turnCount: this.turnCount,
304
- totalTokens,
305
- sessionId: session.id,
306
- });
307
- session.messages = this.normalizeSessionHistory(compacted);
308
- const messagesAfter = session.messages.length;
309
- if (messagesBefore !== messagesAfter) {
310
- yield* this.emit(context, {
311
- type: 'context-compacted',
312
- messagesBefore,
313
- messagesAfter,
314
- });
315
- }
296
+ // Stages 2-4: Agent execute (includes gather, assemble, handoff loop)
297
+ yield* runAgentExecuteStage({
298
+ context,
299
+ input: processedInput,
300
+ injectionQueue,
301
+ abortController: controller,
302
+ }, svc);
303
+ if (svc.config.suggestionModel) {
304
+ yield* svc.suggestionManager.generateSuggestions(context);
316
305
  }
317
- yield* this.runLoop(context, injectionQueue, processedInput, controller);
318
- if (this.config.suggestionModel) {
319
- yield* this.generateSuggestions(context);
306
+ if (session.metadata) {
307
+ session.metadata.lastTurnHadToolCalls = context.toolCallHistory.length > 0;
320
308
  }
321
- await this.hookRunner.onEnd(context, { success: true });
309
+ await svc.hookRunner.onEnd(context, { success: true });
322
310
  }
323
311
  catch (error) {
324
312
  if (controller.signal.aborted) {
325
- yield* this.emit(context, {
313
+ yield* svc.streamEmitter.emit(context, {
326
314
  type: 'interrupted',
327
315
  sessionId: context.session.id,
328
316
  reason: controller.signal.reason ?? 'Operation cancelled',
@@ -330,1167 +318,45 @@ export class Runtime {
330
318
  lastAgentId: context.agentId,
331
319
  lastStep: context.stepCount,
332
320
  });
333
- await this.hookRunner.onEnd(context, { success: false });
321
+ await svc.hookRunner.onEnd(context, { success: false });
334
322
  }
335
323
  else {
336
- await this.hookRunner.onError(context, error);
337
- await this.hookRunner.onEnd(context, { success: false, error: error });
338
- yield* this.emit(context, { type: 'error', error: error.message });
324
+ await svc.hookRunner.onError(context, error);
325
+ await svc.hookRunner.onEnd(context, { success: false, error: error });
326
+ yield* svc.streamEmitter.emit(context, { type: 'error', error: error.message });
339
327
  }
340
328
  }
341
329
  finally {
342
- controller.signal.removeEventListener('abort', abortHandler);
343
- if (abortSignal && externalAbortHandler) {
344
- abortSignal.removeEventListener('abort', externalAbortHandler);
345
- }
330
+ cleanup();
346
331
  this.abortControllers.delete(session.id);
347
- await this.saveSessionCheckpoint(session);
348
- yield* this.emit(context, { type: 'done', sessionId: session.id });
349
- await this.saveSessionCheckpoint(session);
350
- }
351
- }
352
- async *generateSuggestions(context) {
353
- yield* this.suggestionManager.generateSuggestions(context);
354
- }
355
- async *chat(sessionId, input, userId) {
356
- yield* this.stream({ input, sessionId, userId });
357
- }
358
- async *runLoop(context, injectionQueue, input, abortController) {
359
- let currentInput = input;
360
- const abortSignal = abortController?.signal;
361
- let interruptionEmitted = false;
362
- let circularRecoveryActive = false;
363
- let circularRecoveryAttempts = 0;
364
- while (context.handoffStack.length < this.maxHandoffs) {
365
- if (abortSignal?.aborted) {
366
- if (!interruptionEmitted) {
367
- interruptionEmitted = true;
368
- yield* this.emit(context, {
369
- type: 'interrupted',
370
- sessionId: context.session.id,
371
- reason: abortSignal.reason ?? 'Operation cancelled',
372
- timestamp: new Date(),
373
- lastAgentId: context.agentId,
374
- lastStep: context.stepCount,
375
- });
376
- }
377
- return;
378
- }
379
- // Allow a single "bounce back" (A -> B -> A) for detours and multi-intent turns.
380
- // Stop only when an agent would be visited a third time in the same user turn.
381
- const priorVisits = context.handoffStack.filter(id => id === context.agentId).length;
382
- if (priorVisits >= 2 && !circularRecoveryActive) {
383
- const err = `Circular handoff detected: ${context.handoffStack.join(' -> ')} -> ${context.agentId}`;
384
- yield* this.emit(context, { type: 'error', error: err });
385
- circularRecoveryAttempts += 1;
386
- if (circularRecoveryAttempts > 1) {
387
- yield* this.emitCircularHandoffFallback(context);
388
- return;
389
- }
390
- circularRecoveryActive = true;
391
- // Reset path tracking for this turn and force a single in-agent recovery pass.
392
- context.handoffStack = [];
393
- }
394
- context.handoffStack.push(context.agentId);
395
- const agent = this.agents.get(context.agentId);
396
- if (!agent) {
397
- yield* this.emit(context, { type: 'error', error: `Agent "${context.agentId}" not found` });
398
- return;
399
- }
400
- // Agent-specific input processors. These run after the user message is in history and
401
- // before the agent gets a chance to call the model/tools.
402
- if (agent.inputProcessors && agent.inputProcessors.length > 0) {
403
- const procCtx = {
404
- session: context.session,
405
- agentId: agent.id,
406
- toolCallHistory: context.toolCallHistory,
407
- abortSignal,
408
- };
409
- const outcome = await runInputProcessors({
410
- processors: agent.inputProcessors,
411
- input: currentInput,
412
- messages: context.session.messages,
413
- context: procCtx,
414
- });
415
- if (outcome.blocked) {
416
- yield* this.emit(context, {
417
- type: 'tripwire',
418
- phase: 'input',
419
- processorId: outcome.processorId,
420
- reason: outcome.reason,
421
- message: outcome.message,
422
- });
423
- yield* this.emit(context, { type: 'text-delta', text: outcome.message });
424
- yield* this.emit(context, { type: 'turn-end' });
425
- return;
426
- }
427
- if (outcome.input !== currentInput) {
428
- currentInput = outcome.input;
429
- const last = context.session.messages[context.session.messages.length - 1];
430
- if (last && last.role === 'user' && typeof last.content === 'string') {
431
- last.content = currentInput;
432
- this.touchSession(context.session);
433
- }
434
- }
435
- }
436
- yield* this.emit(context, { type: 'agent-start', agentId: agent.id });
437
- await this.hookRunner.onAgentStart(context, agent.id);
438
- let autoContext;
439
- if (agent.autoRetrieve) {
440
- const toolName = agent.autoRetrieve.toolName ?? 'auto_retrieve';
441
- const toolCallId = crypto.randomUUID();
442
- const idempotencyKey = this.buildToolIdempotencyKey(context, toolName, toolCallId);
443
- const callRecord = {
444
- toolCallId,
445
- toolName,
446
- args: { input: currentInput },
447
- idempotencyKey,
448
- success: true,
449
- timestamp: Date.now(),
450
- };
451
- await this.hookRunner.onToolCall(context, callRecord);
452
- yield* this.emit(context, {
453
- type: 'tool-start',
454
- toolCallId,
455
- toolName,
456
- message: agent.autoRetrieve.message ?? 'Retrieving context...',
457
- });
458
- yield* this.emit(context, {
459
- type: 'tool-call',
460
- toolCallId,
461
- toolName,
462
- args: callRecord.args,
463
- });
464
- let result = null;
465
- try {
466
- result = await agent.autoRetrieve.run({ input: currentInput, context, abortSignal });
467
- callRecord.result = result;
468
- }
469
- catch (error) {
470
- callRecord.success = false;
471
- callRecord.error = error;
472
- await this.hookRunner.onToolError(context, callRecord, callRecord.error);
473
- yield* this.emit(context, {
474
- type: 'tool-error',
475
- toolCallId,
476
- toolName,
477
- error: callRecord.error.message,
478
- });
479
- }
480
- finally {
481
- callRecord.durationMs = Date.now() - callRecord.timestamp;
482
- }
483
- if (callRecord.result) {
484
- await this.hookRunner.onToolResult(context, callRecord);
485
- yield* this.emit(context, {
486
- type: 'tool-result',
487
- toolCallId,
488
- toolName,
489
- result: callRecord.result,
490
- });
491
- }
492
- yield* this.emit(context, {
493
- type: 'tool-done',
494
- toolCallId,
495
- toolName,
496
- durationMs: callRecord.durationMs ?? 0,
497
- });
498
- if (result?.text?.trim()) {
499
- autoContext = {
500
- label: agent.autoRetrieve.label ?? 'Relevant Context',
501
- text: result.text,
502
- };
503
- }
504
- }
505
- const extractionSnapshot = await this.runTurnExtraction(agent, context, currentInput, abortSignal);
506
- let activeRoutes;
507
- if (this.isTriageAgent(agent)) {
508
- const agentContext = this.buildAgentContext(currentInput, context, abortSignal);
509
- const results = await Promise.all(agent.routes.map(async (route) => {
510
- if (!route.condition)
511
- return { route, active: true };
512
- try {
513
- const active = await route.condition(currentInput, agentContext);
514
- return { route, active };
515
- }
516
- catch (error) {
517
- console.error(`Error evaluating condition for route ${route.agentId}:`, error);
518
- return { route, active: false }; // Fail-closed on error
519
- }
520
- }));
521
- activeRoutes = results.filter(r => r.active).map(r => r.route);
522
- }
523
- let system = this.buildSystemPrompt(agent, injectionQueue, autoContext, context, activeRoutes);
524
- const turnToolErrors = [];
525
- if (extractionSnapshot?.includeInSystemPrompt) {
526
- system += this.extractionEngine.buildExtractionSystemBlock(extractionSnapshot);
527
- }
528
- if (circularRecoveryActive) {
529
- system = `${system}\n\n## Routing Recovery
530
- A circular handoff was detected in this turn.
531
- Stay with the current agent and provide the best direct answer.
532
- Do not call or suggest handoff/transfer tools in this response.`;
533
- }
534
- const handoffCandidates = circularRecoveryActive ? [] : this.getHandoffCandidates(agent, activeRoutes);
535
- const handoffTool = handoffCandidates.length > 0
536
- ? createHandoffTool(handoffCandidates, context.agentId)
537
- : null;
538
- let handoffTo = null;
539
- let handoffReason = 'No reason provided';
540
- if (this.isFlowAgent(agent)) {
541
- const stopResult = checkStopConditions(context, this.stopConditions);
542
- if (stopResult.shouldStop) {
543
- yield* this.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
544
- return;
545
- }
546
- const agentMaxSteps = agent.maxSteps ?? agent.maxTurns ?? this.maxSteps;
547
- if (agentMaxSteps <= 0) {
548
- yield* this.emit(context, { type: 'error', error: `Max steps exceeded for agent "${agent.id}"` });
549
- return;
550
- }
551
- context.stepCount += 1;
552
- yield* this.emit(context, { type: 'step-start', step: context.stepCount, agentId: agent.id });
553
- await this.hookRunner.onStepStart(context, context.stepCount);
554
- const toolCalls = [];
555
- try {
556
- for await (const part of this.runFlowAgent(agent, context, currentInput, system, handoffTool ?? undefined, (target, reason) => {
557
- handoffTo = target;
558
- handoffReason = reason ?? 'No reason provided';
559
- }, toolCalls, abortSignal)) {
560
- yield part;
561
- }
562
- context.consecutiveErrors = 0;
563
- if (context.session.metadata) {
564
- context.session.metadata.totalSteps += 1;
565
- }
566
- yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
567
- await this.hookRunner.onStepEnd(context, context.stepCount, {
568
- toolCalls,
569
- finishReason: 'flow',
570
- tokensUsed: 0,
571
- handoffTo: handoffTo ?? undefined,
572
- });
573
- }
574
- catch (error) {
575
- if (abortSignal?.aborted) {
576
- if (!interruptionEmitted) {
577
- interruptionEmitted = true;
578
- yield* this.emit(context, {
579
- type: 'interrupted',
580
- sessionId: context.session.id,
581
- reason: abortSignal.reason ?? 'Operation cancelled',
582
- timestamp: new Date(),
583
- lastAgentId: context.agentId,
584
- lastStep: context.stepCount,
585
- });
586
- }
587
- return;
588
- }
589
- context.consecutiveErrors += 1;
590
- await this.hookRunner.onError(context, error);
591
- yield* this.emit(context, { type: 'error', error: error.message });
592
- if (context.consecutiveErrors >= 3) {
593
- return;
594
- }
595
- }
596
- }
597
- else {
598
- const tools = this.wrapToolsWithEnforcement(context, handoffTool ? { ...agent.tools, handoff: handoffTool } : { ...agent.tools });
599
- let agentSteps = 0;
600
- const agentMaxSteps = agent.maxSteps ?? agent.maxTurns ?? this.maxSteps;
601
- while (agentSteps < agentMaxSteps) {
602
- const stopResult = checkStopConditions(context, this.stopConditions);
603
- if (stopResult.shouldStop) {
604
- yield* this.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
605
- return;
606
- }
607
- agentSteps += 1;
608
- context.stepCount += 1;
609
- yield* this.emit(context, { type: 'step-start', step: context.stepCount, agentId: agent.id });
610
- await this.hookRunner.onStepStart(context, context.stepCount);
611
- try {
612
- const model = agent.model ?? this.defaultModel;
613
- if (!model) {
614
- throw new Error(`Agent "${agent.id}" is missing a model`);
615
- }
616
- if (this.isTriageAgent(agent) && agent.triageMode === 'structured') {
617
- const schema = z.object({
618
- agentId: z.string().describe('Target agent ID for this request.'),
619
- reason: z.string().describe('Short, concrete reason grounded in the user request.'),
620
- confidence: z.number().min(0).max(1).describe('Routing confidence from 0 to 1.'),
621
- stayWithCurrent: z.boolean().describe('True only if the current agent is best fit.'),
622
- });
623
- try {
624
- // AI SDK v6+ recommended structured output approach.
625
- const { output: decision } = await generateText({
626
- model: model,
627
- output: Output.object({ schema }),
628
- system: this.buildStructuredTriagePrompt(agent, activeRoutes),
629
- messages: context.session.messages,
630
- abortSignal,
631
- experimental_telemetry: agent.telemetry ?? this.config.telemetry,
632
- });
633
- const allowed = new Set(agent.routes.map(route => route.agentId));
634
- let target = decision.agentId;
635
- if (!allowed.has(target)) {
636
- target = agent.defaultAgent ?? agent.routes[0]?.agentId ?? agent.id;
637
- }
638
- handoffTo = target;
639
- handoffReason = decision.reason ?? 'Routed by triage';
640
- }
641
- catch (err) {
642
- throw err;
643
- }
644
- yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
645
- await this.hookRunner.onStepEnd(context, context.stepCount, {
646
- toolCalls: [],
647
- finishReason: 'handoff',
648
- tokensUsed: 0,
649
- handoffTo,
650
- });
651
- break;
652
- }
653
- const result = streamText({
654
- model: model,
655
- system,
656
- messages: context.session.messages,
657
- tools,
658
- abortSignal,
659
- // Let the AI SDK handle tool-calling steps internally.
660
- // Default is stepCountIs(1), which ends right after tool-calls.
661
- stopWhen: stepCountIs(agent.toolMaxSteps ?? 5),
662
- // Tool execution can read this via options.experimental_context.
663
- experimental_context: {
664
- session: context.session,
665
- agentId: agent.id,
666
- },
667
- experimental_telemetry: agent.telemetry ?? this.config.telemetry,
668
- });
669
- const toolCalls = [];
670
- let finalResult = null;
671
- let finalEmitted = false;
672
- const outputProcessors = this.getAgentOutputProcessors(agent);
673
- const bufferOutput = this.outputProcessorMode === 'buffer' && outputProcessors.length > 0;
674
- let bufferedText = '';
675
- let stoppedByGuard = false;
676
- for await (const chunk of result.fullStream) {
677
- if (chunk.type === 'text-delta') {
678
- if (finalResult) {
679
- continue;
680
- }
681
- // Fail-closed: Suppress text delta if critical tool errors have occurred in this turn.
682
- if (turnToolErrors.length > 0) {
683
- continue;
684
- }
685
- if (bufferOutput) {
686
- bufferedText += chunk.text;
687
- }
688
- else {
689
- yield* this.emit(context, { type: 'text-delta', text: chunk.text });
690
- }
691
- }
692
- if (chunk.type === 'tool-call') {
693
- const args = getChunkArgs(chunk);
694
- const callRecord = {
695
- toolCallId: chunk.toolCallId,
696
- toolName: chunk.toolName,
697
- args,
698
- idempotencyKey: this.buildToolIdempotencyKey(context, chunk.toolName, chunk.toolCallId),
699
- success: true,
700
- timestamp: Date.now(),
701
- };
702
- toolCalls.push(callRecord);
703
- await this.hookRunner.onToolCall(context, callRecord);
704
- // Emit tool-start with optional filler from tool config
705
- const toolConfig = this.agents.get(context.agentId)?.tools;
706
- const toolDef = toolConfig?.[chunk.toolName];
707
- const filler = toolDef?.filler ?? `Let me check ${chunk.toolName}...`;
708
- yield* this.emit(context, {
709
- type: 'tool-start',
710
- toolCallId: chunk.toolCallId,
711
- toolName: chunk.toolName,
712
- message: filler,
713
- });
714
- yield* this.emit(context, {
715
- type: 'tool-call',
716
- toolCallId: chunk.toolCallId,
717
- toolName: chunk.toolName,
718
- args,
719
- });
720
- }
721
- if (chunk.type === 'tool-error') {
722
- const errText = getChunkErrorMessage(chunk);
723
- const args = getChunkArgs(chunk);
724
- const toolCallId = getChunkToolCallId(chunk);
725
- const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
726
- if (callRecord) {
727
- callRecord.success = false;
728
- callRecord.error = new Error(errText);
729
- if (args !== undefined)
730
- callRecord.args = args;
731
- context.toolCallHistory.push(callRecord);
732
- await this.hookRunner.onToolError(context, callRecord, callRecord.error);
733
- // Fail-closed: Track critical tool errors to block turn finalization.
734
- const toolPolicy = agent.toolPolicies?.[callRecord.toolName];
735
- const toolDef = agent.tools?.[callRecord.toolName];
736
- const isCritical = toolPolicy?.critical ?? toolDef?.critical ?? true;
737
- if (isCritical) {
738
- turnToolErrors.push(callRecord);
739
- }
740
- }
741
- yield* this.emit(context, {
742
- type: 'tool-error',
743
- toolCallId: toolCallId ?? chunk.toolCallId,
744
- toolName: chunk.toolName,
745
- error: errText,
746
- });
747
- }
748
- if (chunk.type === 'tool-result') {
749
- const startTime = toolCalls.find(call => call.toolCallId === chunk.toolCallId)?.timestamp ?? Date.now();
750
- const toolResult = getChunkResult(chunk);
751
- const callRecord = toolCalls.find(call => call.toolCallId === chunk.toolCallId);
752
- if (callRecord) {
753
- callRecord.result = toolResult;
754
- callRecord.durationMs = Date.now() - callRecord.timestamp;
755
- context.toolCallHistory.push(callRecord);
756
- await this.hookRunner.onToolResult(context, callRecord);
757
- }
758
- // Emit tool-done with duration
759
- const durationMs = Date.now() - startTime;
760
- yield* this.emit(context, {
761
- type: 'tool-done',
762
- toolCallId: chunk.toolCallId,
763
- toolName: chunk.toolName,
764
- durationMs,
765
- });
766
- if (callRecord) {
767
- const enforcement = await this.enforcer.checkResult(callRecord, {
768
- previousCalls: context.toolCallHistory,
769
- currentStep: context.stepCount,
770
- sessionState: context.session.state ?? {},
771
- });
772
- if (!enforcement.allowed) {
773
- const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
774
- callRecord.success = false;
775
- callRecord.error = new Error(reason);
776
- await this.hookRunner.onToolError(context, callRecord, callRecord.error);
777
- // Fail-closed: Track enforcement failures as critical errors.
778
- turnToolErrors.push(callRecord);
779
- yield* this.emit(context, {
780
- type: 'tool-error',
781
- toolCallId: chunk.toolCallId,
782
- toolName: chunk.toolName,
783
- error: reason,
784
- });
785
- finalResult = { type: 'final', text: reason };
786
- if (!finalEmitted) {
787
- finalEmitted = true;
788
- if (bufferOutput) {
789
- bufferedText += reason;
790
- }
791
- else {
792
- yield* this.emit(context, { type: 'text-delta', text: reason });
793
- }
794
- }
795
- continue;
796
- }
797
- }
798
- yield* this.emit(context, {
799
- type: 'tool-result',
800
- toolCallId: chunk.toolCallId,
801
- toolName: chunk.toolName,
802
- result: toolResult,
803
- });
804
- // Stop as soon as a stop condition triggers, even mid-stream.
805
- const stopResult = checkStopConditions(context, this.stopConditions);
806
- if (stopResult.shouldStop) {
807
- yield* this.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
808
- stoppedByGuard = true;
809
- break;
810
- }
811
- if (isFinalResult(toolResult)) {
812
- finalResult = toolResult;
813
- if (!finalEmitted) {
814
- finalEmitted = true;
815
- if (bufferOutput) {
816
- bufferedText += toolResult.text;
817
- }
818
- else {
819
- yield* this.emit(context, { type: 'text-delta', text: toolResult.text });
820
- }
821
- }
822
- continue;
823
- }
824
- if (isHandoffResult(toolResult)) {
825
- const targetAgent = toolResult.targetAgent ?? toolResult.targetAgentId;
826
- handoffTo = targetAgent;
827
- handoffReason = toolResult.reason ?? 'No reason provided';
828
- }
829
- }
830
- }
831
- if (stoppedByGuard) {
832
- return;
833
- }
834
- const response = await result.response;
835
- const finishReason = finalResult ? 'final' : await result.finishReason;
836
- // Only finalize buffered output when the model has completed a non-tool finish.
837
- // If the model ended on tool-calls, we must persist the tool messages and
838
- // continue the loop (or let AI SDK continue in maxSteps) instead of emitting
839
- // synthetic assistant text, which can duplicate responses.
840
- if (finalResult || (bufferOutput && finishReason !== 'tool-calls') || turnToolErrors.length > 0) {
841
- let rawText = finalResult ? finalResult.text : bufferedText;
842
- // Fail-closed: Suppress success message if critical tool failed.
843
- if (turnToolErrors.length > 0) {
844
- const failedTools = turnToolErrors.map(e => `\`${e.toolName}\``).join(', ');
845
- rawText = `I encountered an error while using the following tools: ${failedTools}. I cannot proceed with the requested action at this time.`;
846
- yield* this.emit(context, {
847
- type: 'error',
848
- error: `Turn blocked by critical tool failures: ${failedTools}`,
849
- });
850
- }
851
- const processed = await this.runOutputProcessing(agent, context, rawText);
852
- if (processed.tripwire) {
853
- yield* this.emit(context, {
854
- type: 'tripwire',
855
- phase: 'output',
856
- processorId: processed.tripwire.processorId,
857
- reason: processed.tripwire.reason,
858
- message: processed.tripwire.message,
859
- });
860
- }
861
- if (bufferOutput || turnToolErrors.length > 0) {
862
- yield* this.emit(context, { type: 'text-delta', text: processed.text });
863
- }
864
- this.appendSessionMessage(context.session, { role: 'assistant', content: processed.text });
865
- }
866
- else {
867
- const beforeLen = context.session.messages.length;
868
- this.appendSessionMessages(context.session, response.messages);
869
- const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, beforeLen);
870
- for (const t of tripwires) {
871
- yield* this.emit(context, {
872
- type: 'tripwire',
873
- phase: 'output',
874
- processorId: t.processorId,
875
- reason: t.reason,
876
- message: t.message,
877
- });
878
- }
879
- }
880
- const usage = await result.usage;
881
- const totalTokens = usage.totalTokens ?? 0;
882
- context.totalTokens += totalTokens;
883
- if (context.session.metadata) {
884
- context.session.metadata.totalTokens += totalTokens;
885
- context.session.metadata.totalSteps += 1;
886
- }
887
- context.consecutiveErrors = 0;
888
- yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
889
- await this.hookRunner.onStepEnd(context, context.stepCount, {
890
- toolCalls,
891
- finishReason,
892
- tokensUsed: totalTokens,
893
- handoffTo: handoffTo ?? undefined,
894
- });
895
- if (finalResult || finishReason !== 'tool-calls' || handoffTo) {
896
- break;
897
- }
898
- }
899
- catch (error) {
900
- console.error('RUNTIME_LOOP_ERROR:', error);
901
- if (abortSignal?.aborted) {
902
- if (!interruptionEmitted) {
903
- interruptionEmitted = true;
904
- yield* this.emit(context, {
905
- type: 'interrupted',
906
- sessionId: context.session.id,
907
- reason: abortSignal.reason ?? 'Operation cancelled',
908
- timestamp: new Date(),
909
- lastAgentId: context.agentId,
910
- lastStep: context.stepCount,
911
- });
912
- }
913
- return;
914
- }
915
- context.consecutiveErrors += 1;
916
- await this.hookRunner.onError(context, error);
917
- yield* this.emit(context, { type: 'error', error: error.message });
918
- if (context.consecutiveErrors >= 3) {
919
- return;
920
- }
921
- }
922
- }
923
- }
924
- yield* this.emit(context, { type: 'agent-end', agentId: agent.id });
925
- await this.hookRunner.onAgentEnd(context, agent.id);
926
- if (circularRecoveryActive && handoffTo) {
927
- yield* this.emit(context, {
928
- type: 'error',
929
- error: `Circular handoff recovery failed: attempted handoff ${context.agentId} -> ${handoffTo}`,
930
- });
931
- yield* this.emitCircularHandoffFallback(context);
932
- return;
933
- }
934
- if (circularRecoveryActive) {
935
- circularRecoveryActive = false;
936
- }
937
- if (!handoffTo) {
938
- break;
939
- }
940
- await this.hookRunner.onHandoff(context, context.agentId, handoffTo, handoffReason);
941
- yield* this.emit(context, {
942
- type: 'handoff',
943
- from: context.agentId,
944
- to: handoffTo,
945
- reason: handoffReason,
946
- });
947
- context.session.handoffHistory.push({
948
- from: context.agentId,
949
- to: handoffTo,
950
- reason: handoffReason,
951
- timestamp: new Date(),
952
- });
953
- if (context.session.metadata) {
954
- context.session.metadata.handoffHistory.push({
955
- from: context.agentId,
956
- to: handoffTo,
957
- reason: handoffReason,
958
- timestamp: new Date(),
959
- });
960
- }
961
- context.session.activeAgentId = handoffTo;
962
- context.session.currentAgent = handoffTo;
963
- await this.saveSessionCheckpoint(context.session);
964
- context.agentId = handoffTo;
965
- }
966
- if (context.handoffStack.length >= this.maxHandoffs) {
967
- yield* this.emit(context, { type: 'error', error: `Maximum handoffs (${this.maxHandoffs}) exceeded` });
968
- }
969
- }
970
- getCircularHandoffFallbackMessage() {
971
- const configured = this.config.circularHandoffFallbackMessage?.trim();
972
- if (configured) {
973
- return configured;
332
+ yield* runPostStreamStage(context, session, svc);
974
333
  }
975
- return 'I hit a routing issue between agents. I will continue here. Tell me the exact outcome you need in one sentence.';
976
334
  }
977
- async *emitCircularHandoffFallback(context) {
978
- const fallback = this.getCircularHandoffFallbackMessage();
979
- this.appendSessionMessage(context.session, { role: 'assistant', content: fallback });
980
- yield* this.emit(context, { type: 'text-delta', text: fallback });
981
- }
982
- buildPolicyInjectionQueue() {
983
- const queue = new InjectionQueue();
984
- const profile = this.config.policyProfile ?? 'minimal';
985
- queue.addBatch(getPolicyProfileInjections(profile));
986
- if (this.config.policyInjections && this.config.policyInjections.length > 0) {
987
- queue.addBatch(this.config.policyInjections);
988
- }
989
- return queue;
990
- }
991
- createSession(id, userId) {
992
- const now = new Date();
993
- return {
994
- id: id ?? crypto.randomUUID(),
995
- userId,
996
- messages: [],
997
- createdAt: now,
998
- updatedAt: now,
999
- workingMemory: {},
1000
- currentAgent: this.defaultAgentId,
1001
- activeAgentId: this.defaultAgentId,
1002
- state: {},
1003
- metadata: {
1004
- createdAt: now,
1005
- lastActiveAt: now,
1006
- totalTokens: 0,
1007
- totalSteps: 0,
1008
- handoffHistory: [],
1009
- },
1010
- agentStates: {},
1011
- handoffHistory: [],
1012
- };
1013
- }
1014
- isFlowAgent(agent) {
1015
- return agent.type === 'flow';
1016
- }
1017
- isTriageAgent(agent) {
1018
- return agent.type === 'triage';
1019
- }
1020
- buildPromptMemoryView(session, agent) {
1021
- const memory = session.workingMemory ?? {};
1022
- const allowlist = agent.promptMemoryAllowlist ?? this.config.promptMemoryAllowlist;
1023
- const internalKeys = [
1024
- this.runtimeEventLogKey,
1025
- this.runtimeSessionTurnKey,
1026
- this.redactCarryKey,
1027
- ];
1028
- const filtered = {};
1029
- for (const [key, value] of Object.entries(memory)) {
1030
- if (internalKeys.includes(key)) {
1031
- continue;
1032
- }
1033
- if (allowlist && !allowlist.includes(key)) {
1034
- continue;
1035
- }
1036
- filtered[key] = value;
1037
- }
1038
- return filtered;
1039
- }
1040
- buildSystemPrompt(agent, injectionQueue, autoContext, context, activeRoutes) {
1041
- const basePrompt = this.isTriageAgent(agent)
1042
- ? this.buildTriagePrompt(agent, activeRoutes)
1043
- : agent.systemPrompt;
1044
- const autoBlock = autoContext?.text?.trim()
1045
- ? `\n\n## ${autoContext.label}\n${autoContext.text}`
1046
- : '';
1047
- const systemInjections = injectionQueue.getFor('system');
1048
- const memory = context?.session ? this.buildPromptMemoryView(context.session, agent) : {};
1049
- const memoryBlock = Object.keys(memory).length > 0
1050
- ? `\n\n## Known Information\n${JSON.stringify(memory, null, 2)}`
1051
- : '';
1052
- const merged = `${basePrompt}${autoBlock}${memoryBlock}`;
1053
- return systemInjections ? `${merged}\n\n${systemInjections}` : merged;
1054
- }
1055
- buildStructuredTriagePrompt(agent, activeRoutes) {
1056
- const routes = activeRoutes ?? agent.routes;
1057
- const routeDescriptions = routes
1058
- .map(route => `- ${route.agentId}: ${route.description}`)
1059
- .join('\n');
1060
- const allowed = routes.map(route => route.agentId);
1061
- const defaultNote = agent.defaultAgent ? `Default: ${agent.defaultAgent}` : 'Default: none';
1062
- return `${agent.systemPrompt}
1063
-
1064
- ## Available Specialists
1065
- ${routeDescriptions}
1066
-
1067
- ## Output (JSON)
1068
- Return a JSON object with:
1069
- - agentId: one of [${allowed.join(', ')}]
1070
- - reason: short, concrete reason grounded in the user request
1071
- - confidence: number 0-1 (0 = weak match, 1 = perfect match)
1072
- - stayWithCurrent: boolean (true only if current agent is best fit)
1073
- ${defaultNote}`;
1074
- }
1075
- buildTriagePrompt(agent, activeRoutes) {
1076
- const routes = activeRoutes ?? agent.routes;
1077
- const routeDescriptions = routes
1078
- .map(route => `- **${route.agentId}**: ${route.description}`)
1079
- .join('\n');
1080
- const defaultNote = agent.defaultAgent
1081
- ? `\n- Use "${agent.defaultAgent}" when no specialist applies.`
1082
- : '';
1083
- return `${agent.systemPrompt}
1084
-
1085
- ## Available Specialists
1086
- ${routeDescriptions}
1087
-
1088
- ## Instructions
1089
- - For general questions, answer directly
1090
- - When the customer needs specialized help, use the handoff tool
1091
- - Always provide a brief reason for the handoff${defaultNote}`;
1092
- }
1093
- getHandoffCandidates(agent, activeRoutes) {
1094
- if (this.isTriageAgent(agent)) {
1095
- const routes = activeRoutes ?? agent.routes;
1096
- return routes
1097
- .map(route => this.agents.get(route.agentId))
1098
- .filter((candidate) => Boolean(candidate));
1099
- }
1100
- // Production default: only triage routes by default. Other agents can handoff only if explicitly configured.
1101
- const targets = agent.canHandoffTo;
1102
- if (!targets || targets.length === 0) {
1103
- return [];
1104
- }
1105
- return targets
1106
- .map(id => this.agents.get(id))
1107
- .filter((candidate) => Boolean(candidate));
1108
- }
1109
- getFlowState(session, agentId) {
1110
- const stored = session.agentStates?.[agentId]?.state;
1111
- if (!stored) {
1112
- return undefined;
1113
- }
1114
- return stored;
1115
- }
1116
- buildAgentContext(input, context, abortSignal) {
1117
- return {
1118
- session: context.session,
1119
- messages: context.session.messages,
1120
- workingMemory: new SessionWorkingMemory(context.session),
1121
- currentAgent: context.agentId,
1122
- turnCount: this.turnCount,
1123
- metadata: context.session.metadata,
1124
- abortSignal,
1125
- };
1126
- }
1127
- setFlowState(session, agentId, state) {
1128
- session.agentStates[agentId] = {
1129
- agentId,
1130
- state: state,
1131
- lastActive: new Date(),
1132
- };
1133
- this.updateFlowStateSnapshot(session, agentId, state);
1134
- this.touchSession(session);
1135
- }
1136
- clearFlowState(session, agentId) {
1137
- delete session.agentStates[agentId];
1138
- this.touchSession(session);
1139
- }
1140
- updateFlowStateSnapshot(session, agentId, state) {
1141
- const existing = session.workingMemory.flowStateByAgent;
1142
- const byAgent = typeof existing === 'object' && existing !== null
1143
- ? { ...existing }
1144
- : {};
1145
- byAgent[agentId] = {
1146
- currentNode: state.context.currentNode,
1147
- collectedData: state.context.collectedData,
1148
- nodeHistory: state.context.nodeHistory,
1149
- initialized: state.initialized,
1150
- flowEnded: state.flowEnded,
1151
- updatedAt: new Date().toISOString(),
1152
- };
1153
- session.workingMemory.flowStateByAgent = byAgent;
1154
- this.touchSession(session);
1155
- }
1156
- buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond) {
1157
- // If we don't need to inject a handoff tool and we don't need to suppress the initial autoRespond,
1158
- // we can return the flow as-is. Otherwise we must clone nodes to apply changes.
1159
- const needsInitialSuppression = suppressAutoRespond &&
1160
- agent.flow.nodes.some(n => n.id === agent.initialNode && n.autoRespond === undefined);
1161
- if (!handoffTool && !needsInitialSuppression)
1162
- return agent.flow;
1163
- return {
1164
- ...agent.flow,
1165
- nodes: agent.flow.nodes.map(node => {
1166
- const shouldSuppress = suppressAutoRespond
1167
- && node.id === agent.initialNode
1168
- && node.autoRespond === undefined;
1169
- const existingTools = node.tools;
1170
- // Support tool factories (tools: (ctx) => ToolSet) as well as static ToolSets.
1171
- if (typeof existingTools === 'function') {
1172
- return {
1173
- ...node,
1174
- tools: (ctx) => {
1175
- const resolved = existingTools(ctx) ?? {};
1176
- if (!handoffTool)
1177
- return resolved;
1178
- return resolved.handoff ? resolved : { ...resolved, handoff: handoffTool };
1179
- },
1180
- ...(shouldSuppress ? { autoRespond: false } : {}),
1181
- };
1182
- }
1183
- const toolSet = existingTools ?? {};
1184
- if (!handoffTool || toolSet.handoff) {
1185
- return shouldSuppress ? { ...node, autoRespond: false } : node;
1186
- }
1187
- return {
1188
- ...node,
1189
- tools: {
1190
- ...toolSet,
1191
- handoff: handoffTool,
1192
- },
1193
- ...(shouldSuppress ? { autoRespond: false } : {}),
1194
- };
1195
- }),
1196
- };
1197
- }
1198
- getFlowNode(agent, nodeId) {
1199
- if (!nodeId) {
1200
- return undefined;
1201
- }
1202
- return agent.flow.nodes.find(node => node.id === nodeId);
1203
- }
1204
- getExtractionConfig(agent, session) {
1205
- if (this.isFlowAgent(agent)) {
1206
- const flowState = this.getFlowState(session, agent.id);
1207
- const nodeId = flowState?.context.currentNode ?? agent.initialNode;
1208
- const node = this.getFlowNode(agent, nodeId);
1209
- const config = node?.extraction ?? agent.extraction;
1210
- if (!config) {
1211
- return null;
1212
- }
1213
- return { config, nodeId };
1214
- }
1215
- if (!agent.extraction) {
1216
- return null;
1217
- }
1218
- return { config: agent.extraction };
1219
- }
1220
- async runTurnExtraction(agent, context, input, abortSignal) {
1221
- return this.extractionEngine.runTurnExtraction(agent, context, input, {
1222
- getFlowState: (s, id) => this.getFlowState(s, id),
1223
- setFlowState: (s, id, st) => this.setFlowState(s, id, st),
1224
- getFlowNode: (a, nid) => this.getFlowNode(a, nid),
1225
- isFlowAgent: (a) => this.isFlowAgent(a),
1226
- touchSession: (s) => this.touchSession(s),
1227
- }, abortSignal);
1228
- }
1229
- async shouldHandleFlowInput(agent, input, flowState, abortSignal) {
1230
- return this.flowExecutor.shouldHandleFlowInput(agent, input, flowState, this.getFlowExecutorHelpers(), abortSignal);
1231
- }
1232
- async *runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff, abortSignal) {
1233
- yield* this.flowExecutor.runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff, this.getFlowExecutorHelpers(), abortSignal);
1234
- }
1235
- async *runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls, abortSignal) {
1236
- yield* this.flowExecutor.runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls, this.getFlowExecutorHelpers(), abortSignal);
1237
- }
1238
- getFlowExecutorHelpers() {
1239
- return {
1240
- emit: (ctx, part) => this.emit(ctx, part),
1241
- runOutputProcessing: (agent, ctx, text) => this.runOutputProcessing(agent, ctx, text),
1242
- appendSessionMessage: (s, m) => this.appendSessionMessage(s, m),
1243
- appendSessionMessages: (s, ms) => this.appendSessionMessages(s, ms),
1244
- postProcessPersistedAssistantMessages: (agent, ctx, start) => this.postProcessPersistedAssistantMessages(agent, ctx, start),
1245
- getAgentOutputProcessors: (agent) => this.getAgentOutputProcessors(agent),
1246
- getSessionTurn: (s) => this.getSessionTurn(s),
1247
- buildToolIdempotencyKey: (ctx, tn, tcid) => this.buildToolIdempotencyKey(ctx, tn, tcid),
1248
- setFlowState: (s, id, st) => this.setFlowState(s, id, st),
1249
- getFlowState: (s, id) => this.getFlowState(s, id),
1250
- updateFlowStateSnapshot: (s, id, st) => this.updateFlowStateSnapshot(s, id, st),
1251
- clearFlowState: (s, id) => this.clearFlowState(s, id),
1252
- buildFlowWithHandoff: (a, ht, s) => this.buildFlowWithHandoff(a, ht, s),
1253
- getFlowNode: (a, ni) => this.getFlowNode(a, ni),
1254
- touchSession: (s) => this.touchSession(s),
1255
- matchesDetourRule: (i, p) => this.matchesDetourRuleHelper(i, p),
1256
- enforcecheck: (call, ctx) => this.enforcer.check(call, {
1257
- previousCalls: ctx.toolCallHistory,
1258
- currentStep: ctx.stepCount,
1259
- sessionState: ctx.session.state ?? {},
1260
- }),
1261
- enforcecheckResult: (call, ctx) => this.enforcer.checkResult(call, {
1262
- previousCalls: ctx.toolCallHistory,
1263
- currentStep: ctx.stepCount,
1264
- sessionState: ctx.session.state ?? {},
1265
- }),
1266
- onToolCallHook: (ctx, call) => this.hookRunner.onToolCall(ctx, call),
1267
- onToolResultHook: (ctx, call) => this.hookRunner.onToolResult(ctx, call),
1268
- onToolErrorHook: (ctx, call, error) => this.hookRunner.onToolError(ctx, call, error),
335
+ // --- Private helpers ---
336
+ setupAbortHandlers(sessionId, controller, externalSignal) {
337
+ const abortHandler = () => {
338
+ this.abortControllers.delete(sessionId);
1269
339
  };
1270
- }
1271
- matchesDetourRuleHelper(input, patterns) {
1272
- if (!patterns || patterns.length === 0) {
1273
- return false;
1274
- }
1275
- for (const pattern of patterns) {
1276
- try {
1277
- const regex = new RegExp(pattern, 'i');
1278
- if (regex.test(input)) {
1279
- return true;
1280
- }
340
+ controller.signal.addEventListener('abort', abortHandler);
341
+ let externalAbortHandler;
342
+ if (externalSignal) {
343
+ externalAbortHandler = () => {
344
+ controller.abort(externalSignal.reason ?? 'External abort');
345
+ };
346
+ if (externalSignal.aborted) {
347
+ controller.abort(externalSignal.reason ?? 'External abort');
1281
348
  }
1282
- catch {
1283
- if (input.includes(pattern.toLowerCase())) {
1284
- return true;
1285
- }
349
+ else {
350
+ externalSignal.addEventListener('abort', externalAbortHandler);
1286
351
  }
1287
352
  }
1288
- return false;
1289
- }
1290
- async getSession(id) {
1291
- return this.sessionStore.get(id);
1292
- }
1293
- async deleteSession(id) {
1294
- await this.sessionStore.delete(id);
1295
- }
1296
- getAgent(id) {
1297
- return this.agents.get(id);
1298
- }
1299
- getAllAgents() {
1300
- return Array.from(this.agents.values());
1301
- }
1302
- normalizeSessionMessage(message) {
1303
- return normalizeModelMessage(message);
1304
- }
1305
- normalizeSessionHistory(messages) {
1306
- const normalized = [];
1307
- for (const message of messages) {
1308
- const next = this.normalizeSessionMessage(message);
1309
- if (next) {
1310
- normalized.push(next);
353
+ return () => {
354
+ controller.signal.removeEventListener('abort', abortHandler);
355
+ if (externalSignal && externalAbortHandler) {
356
+ externalSignal.removeEventListener('abort', externalAbortHandler);
1311
357
  }
1312
- }
1313
- return normalized;
1314
- }
1315
- appendSessionMessage(session, message) {
1316
- const normalized = this.normalizeSessionMessage(message);
1317
- if (!normalized) {
1318
- return;
1319
- }
1320
- session.messages.push(normalized);
1321
- this.touchSession(session);
1322
- }
1323
- appendSessionMessages(session, messages) {
1324
- for (const message of messages) {
1325
- this.appendSessionMessage(session, message);
1326
- }
1327
- }
1328
- touchSession(session) {
1329
- const now = new Date();
1330
- session.updatedAt = now;
1331
- if (session.metadata) {
1332
- session.metadata.lastActiveAt = now;
1333
- }
1334
- }
1335
- getSessionTurn(session) {
1336
- const value = session.workingMemory[this.runtimeSessionTurnKey];
1337
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
1338
- }
1339
- bumpSessionTurn(session) {
1340
- const next = this.getSessionTurn(session) + 1;
1341
- session.workingMemory[this.runtimeSessionTurnKey] = next;
1342
- this.touchSession(session);
1343
- return next;
1344
- }
1345
- async saveSessionCheckpoint(session) {
1346
- this.touchSession(session);
1347
- await this._sessionStore.save(session);
1348
- }
1349
- shouldCheckpointAfterPart(part) {
1350
- return this.checkpointEventTypes.has(part.type);
1351
- }
1352
- recordRuntimeEvent(context, part) {
1353
- this.sessionEventManager.recordRuntimeEvent(context, part);
1354
- }
1355
- buildToolIdempotencyKey(context, toolName, toolCallId) {
1356
- return `${context.session.id}:${context.agentId}:${context.stepCount}:${toolName}:${toolCallId}`;
1357
- }
1358
- withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey) {
1359
- const baseOptions = isRecord(options) ? options : {};
1360
- const existingContext = isRecord(baseOptions.experimental_context)
1361
- ? baseOptions.experimental_context
1362
- : {};
1363
- return {
1364
- ...baseOptions,
1365
- toolCallId,
1366
- experimental_context: {
1367
- ...existingContext,
1368
- session: context.session,
1369
- sessionId: context.session.id,
1370
- agentId: context.agentId,
1371
- step: context.stepCount,
1372
- turn: this.getSessionTurn(context.session),
1373
- toolName,
1374
- toolCallId,
1375
- idempotencyKey,
1376
- },
1377
358
  };
1378
359
  }
1379
- buildProcessorContext(context, abortSignal) {
1380
- return {
1381
- session: context.session,
1382
- agentId: context.agentId,
1383
- toolCallHistory: context.toolCallHistory,
1384
- abortSignal,
1385
- };
1386
- }
1387
- getAgentOutputProcessors(agent) {
1388
- return [
1389
- ...this.outputProcessors,
1390
- ...(agent.outputProcessors ?? []),
1391
- ];
1392
- }
1393
- applyRedactionsToText(text) {
1394
- if (!this.outputRedactions || this.outputRedactions.length === 0)
1395
- return text;
1396
- let out = text;
1397
- for (const r of this.outputRedactions) {
1398
- out = out.replace(r.re, r.replacement);
1399
- }
1400
- return out;
1401
- }
1402
- async runOutputProcessing(agent, context, text) {
1403
- const processors = this.getAgentOutputProcessors(agent);
1404
- let cur = text;
1405
- if (processors.length > 0) {
1406
- const abortSignal = this.getActiveAbortController(context.session.id)?.signal;
1407
- const outcome = await runOutputProcessors({
1408
- processors,
1409
- text: cur,
1410
- messages: context.session.messages,
1411
- context: this.buildProcessorContext(context, abortSignal),
1412
- });
1413
- if (outcome.blocked) {
1414
- const msg = this.applyRedactionsToText(outcome.message);
1415
- return {
1416
- text: msg,
1417
- tripwire: { processorId: outcome.processorId, reason: outcome.reason, message: msg },
1418
- };
1419
- }
1420
- cur = outcome.text;
1421
- }
1422
- cur = this.applyRedactionsToText(cur);
1423
- return { text: cur };
1424
- }
1425
- async postProcessPersistedAssistantMessages(agent, context, startIndex) {
1426
- const tripwires = [];
1427
- const msgs = context.session.messages;
1428
- for (let i = startIndex; i < msgs.length; i++) {
1429
- const m = msgs[i];
1430
- if (!m || m.role !== 'assistant' || typeof m.content !== 'string')
1431
- continue;
1432
- const res = await this.runOutputProcessing(agent, context, m.content);
1433
- msgs[i] = { ...m, content: res.text };
1434
- if (res.tripwire) {
1435
- tripwires.push(res.tripwire);
1436
- }
1437
- }
1438
- return tripwires;
1439
- }
1440
- async *emit(context, part) {
1441
- // Defense-in-depth redaction of streamed assistant output.
1442
- if (this.outputRedactions && this.outputRedactions.length > 0) {
1443
- const sessionId = context.session.id;
1444
- if (part.type === 'text-delta') {
1445
- const next = this.applyOutputRedactions(context.session, part.text, false);
1446
- if (next) {
1447
- const redacted = { ...part, text: next };
1448
- yield* this.emitWithHooks(context, redacted);
1449
- }
1450
- return;
1451
- }
1452
- if (part.type === 'turn-end' || part.type === 'done') {
1453
- const flushed = this.applyOutputRedactions(context.session, '', true);
1454
- if (flushed) {
1455
- const carryPart = { type: 'text-delta', text: flushed };
1456
- yield* this.emitWithHooks(context, carryPart);
1457
- }
1458
- delete context.session.workingMemory[this.redactCarryKey];
1459
- }
1460
- }
1461
- yield* this.emitWithHooks(context, part);
1462
- }
1463
- async *emitWithHooks(context, part) {
1464
- this.recordRuntimeEvent(context, part);
1465
- await this.hookRunner.onStreamPart(context, part);
1466
- if (this.shouldCheckpointAfterPart(part)) {
1467
- await this.saveSessionCheckpoint(context.session);
1468
- }
1469
- yield part;
1470
- }
1471
- applyOutputRedactions(session, text, flush) {
1472
- if (!this.outputRedactions || this.outputRedactions.length === 0)
1473
- return text;
1474
- const carry = typeof session.workingMemory[this.redactCarryKey] === 'string'
1475
- ? session.workingMemory[this.redactCarryKey]
1476
- : '';
1477
- let combined = `${carry}${text}`;
1478
- for (const r of this.outputRedactions) {
1479
- combined = combined.replace(r.re, r.replacement);
1480
- }
1481
- const keep = flush ? 0 : this.redactLookbehind;
1482
- if (keep === 0) {
1483
- session.workingMemory[this.redactCarryKey] = '';
1484
- return combined;
1485
- }
1486
- if (combined.length <= keep) {
1487
- session.workingMemory[this.redactCarryKey] = combined;
1488
- return '';
1489
- }
1490
- const out = combined.slice(0, combined.length - keep);
1491
- session.workingMemory[this.redactCarryKey] = combined.slice(-keep);
1492
- return out;
1493
- }
1494
360
  }
1495
361
  export function createRuntime(config) {
1496
362
  return new Runtime(config);