@ariaflowagents/core 0.7.0 → 0.8.1

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 (228) 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/flows/FlowManager.d.ts +8 -0
  23. package/dist/flows/FlowManager.d.ts.map +1 -1
  24. package/dist/flows/FlowManager.js +38 -4
  25. package/dist/flows/FlowManager.js.map +1 -1
  26. package/dist/flows/template.d.ts +2 -2
  27. package/dist/flows/template.d.ts.map +1 -1
  28. package/dist/flows/template.js +13 -0
  29. package/dist/flows/template.js.map +1 -1
  30. package/dist/foundation/AgentDefinition.d.ts +18 -0
  31. package/dist/foundation/AgentDefinition.d.ts.map +1 -0
  32. package/dist/foundation/AgentDefinition.js +2 -0
  33. package/dist/foundation/AgentDefinition.js.map +1 -0
  34. package/dist/foundation/AgentStateController.d.ts +26 -0
  35. package/dist/foundation/AgentStateController.d.ts.map +1 -0
  36. package/dist/foundation/AgentStateController.js +2 -0
  37. package/dist/foundation/AgentStateController.js.map +1 -0
  38. package/dist/foundation/ConversationEventLog.d.ts +72 -0
  39. package/dist/foundation/ConversationEventLog.d.ts.map +1 -0
  40. package/dist/foundation/ConversationEventLog.js +2 -0
  41. package/dist/foundation/ConversationEventLog.js.map +1 -0
  42. package/dist/foundation/ConversationState.d.ts +31 -0
  43. package/dist/foundation/ConversationState.d.ts.map +1 -0
  44. package/dist/foundation/ConversationState.js +2 -0
  45. package/dist/foundation/ConversationState.js.map +1 -0
  46. package/dist/foundation/DefaultAgentStateController.d.ts +24 -0
  47. package/dist/foundation/DefaultAgentStateController.d.ts.map +1 -0
  48. package/dist/foundation/DefaultAgentStateController.js +49 -0
  49. package/dist/foundation/DefaultAgentStateController.js.map +1 -0
  50. package/dist/foundation/DefaultConversationEventLog.d.ts +28 -0
  51. package/dist/foundation/DefaultConversationEventLog.d.ts.map +1 -0
  52. package/dist/foundation/DefaultConversationEventLog.js +195 -0
  53. package/dist/foundation/DefaultConversationEventLog.js.map +1 -0
  54. package/dist/foundation/DefaultConversationState.d.ts +34 -0
  55. package/dist/foundation/DefaultConversationState.d.ts.map +1 -0
  56. package/dist/foundation/DefaultConversationState.js +100 -0
  57. package/dist/foundation/DefaultConversationState.js.map +1 -0
  58. package/dist/foundation/DefaultToolExecutor.d.ts +58 -0
  59. package/dist/foundation/DefaultToolExecutor.d.ts.map +1 -0
  60. package/dist/foundation/DefaultToolExecutor.js +128 -0
  61. package/dist/foundation/DefaultToolExecutor.js.map +1 -0
  62. package/dist/foundation/ToolExecutor.d.ts +44 -0
  63. package/dist/foundation/ToolExecutor.d.ts.map +1 -0
  64. package/dist/foundation/ToolExecutor.js +2 -0
  65. package/dist/foundation/ToolExecutor.js.map +1 -0
  66. package/dist/foundation/createFoundation.d.ts +33 -0
  67. package/dist/foundation/createFoundation.d.ts.map +1 -0
  68. package/dist/foundation/createFoundation.js +34 -0
  69. package/dist/foundation/createFoundation.js.map +1 -0
  70. package/dist/foundation/index.d.ts +15 -0
  71. package/dist/foundation/index.d.ts.map +1 -0
  72. package/dist/foundation/index.js +8 -0
  73. package/dist/foundation/index.js.map +1 -0
  74. package/dist/hooks/HookRunner.d.ts +2 -0
  75. package/dist/hooks/HookRunner.d.ts.map +1 -1
  76. package/dist/hooks/HookRunner.js +4 -0
  77. package/dist/hooks/HookRunner.js.map +1 -1
  78. package/dist/index.d.ts +13 -2
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +9 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/memory/MemoryService.d.ts +40 -0
  83. package/dist/memory/MemoryService.d.ts.map +1 -0
  84. package/dist/memory/MemoryService.js +2 -0
  85. package/dist/memory/MemoryService.js.map +1 -0
  86. package/dist/memory/index.d.ts +5 -0
  87. package/dist/memory/index.d.ts.map +1 -0
  88. package/dist/memory/index.js +3 -0
  89. package/dist/memory/index.js.map +1 -0
  90. package/dist/memory/preloadMemory.d.ts +17 -0
  91. package/dist/memory/preloadMemory.d.ts.map +1 -0
  92. package/dist/memory/preloadMemory.js +62 -0
  93. package/dist/memory/preloadMemory.js.map +1 -0
  94. package/dist/memory/stores/InMemoryMemoryService.d.ts +20 -0
  95. package/dist/memory/stores/InMemoryMemoryService.d.ts.map +1 -0
  96. package/dist/memory/stores/InMemoryMemoryService.js +92 -0
  97. package/dist/memory/stores/InMemoryMemoryService.js.map +1 -0
  98. package/dist/memory/types.d.ts +49 -0
  99. package/dist/memory/types.d.ts.map +1 -0
  100. package/dist/memory/types.js +8 -0
  101. package/dist/memory/types.js.map +1 -0
  102. package/dist/prompts/AgentPrompt.d.ts +110 -0
  103. package/dist/prompts/AgentPrompt.d.ts.map +1 -0
  104. package/dist/prompts/AgentPrompt.js +373 -0
  105. package/dist/prompts/AgentPrompt.js.map +1 -0
  106. package/dist/prompts/PromptAssembly.d.ts +119 -0
  107. package/dist/prompts/PromptAssembly.d.ts.map +1 -0
  108. package/dist/prompts/PromptAssembly.js +150 -0
  109. package/dist/prompts/PromptAssembly.js.map +1 -0
  110. package/dist/prompts/PromptBuilder.d.ts +22 -3
  111. package/dist/prompts/PromptBuilder.d.ts.map +1 -1
  112. package/dist/prompts/PromptBuilder.js +242 -13
  113. package/dist/prompts/PromptBuilder.js.map +1 -1
  114. package/dist/prompts/PromptRenderer.d.ts +43 -0
  115. package/dist/prompts/PromptRenderer.d.ts.map +1 -0
  116. package/dist/prompts/PromptRenderer.js +114 -0
  117. package/dist/prompts/PromptRenderer.js.map +1 -0
  118. package/dist/prompts/brandVoice.d.ts +10 -0
  119. package/dist/prompts/brandVoice.d.ts.map +1 -0
  120. package/dist/prompts/brandVoice.js +87 -0
  121. package/dist/prompts/brandVoice.js.map +1 -0
  122. package/dist/prompts/index.d.ts +11 -4
  123. package/dist/prompts/index.d.ts.map +1 -1
  124. package/dist/prompts/index.js +7 -2
  125. package/dist/prompts/index.js.map +1 -1
  126. package/dist/prompts/security.d.ts +5 -0
  127. package/dist/prompts/security.d.ts.map +1 -0
  128. package/dist/prompts/security.js +52 -0
  129. package/dist/prompts/security.js.map +1 -0
  130. package/dist/prompts/types.d.ts +65 -1
  131. package/dist/prompts/types.d.ts.map +1 -1
  132. package/dist/prompts/types.js +26 -0
  133. package/dist/prompts/types.js.map +1 -1
  134. package/dist/runtime/ContextBudget.d.ts +57 -0
  135. package/dist/runtime/ContextBudget.d.ts.map +1 -0
  136. package/dist/runtime/ContextBudget.js +103 -0
  137. package/dist/runtime/ContextBudget.js.map +1 -0
  138. package/dist/runtime/ContextManager.d.ts +8 -5
  139. package/dist/runtime/ContextManager.d.ts.map +1 -1
  140. package/dist/runtime/ContextManager.js +47 -14
  141. package/dist/runtime/ContextManager.js.map +1 -1
  142. package/dist/runtime/FlowExecutor.d.ts +16 -11
  143. package/dist/runtime/FlowExecutor.d.ts.map +1 -1
  144. package/dist/runtime/FlowExecutor.js +32 -138
  145. package/dist/runtime/FlowExecutor.js.map +1 -1
  146. package/dist/runtime/Runtime.d.ts +31 -78
  147. package/dist/runtime/Runtime.d.ts.map +1 -1
  148. package/dist/runtime/Runtime.js +225 -1406
  149. package/dist/runtime/Runtime.js.map +1 -1
  150. package/dist/runtime/SessionCache.d.ts +16 -0
  151. package/dist/runtime/SessionCache.d.ts.map +1 -0
  152. package/dist/runtime/SessionCache.js +49 -0
  153. package/dist/runtime/SessionCache.js.map +1 -0
  154. package/dist/runtime/SessionMutex.d.ts +37 -0
  155. package/dist/runtime/SessionMutex.d.ts.map +1 -0
  156. package/dist/runtime/SessionMutex.js +59 -0
  157. package/dist/runtime/SessionMutex.js.map +1 -0
  158. package/dist/runtime/StreamEmitter.d.ts +34 -0
  159. package/dist/runtime/StreamEmitter.d.ts.map +1 -0
  160. package/dist/runtime/StreamEmitter.js +91 -0
  161. package/dist/runtime/StreamEmitter.js.map +1 -0
  162. package/dist/runtime/handoffFilters.d.ts +60 -0
  163. package/dist/runtime/handoffFilters.d.ts.map +1 -0
  164. package/dist/runtime/handoffFilters.js +95 -0
  165. package/dist/runtime/handoffFilters.js.map +1 -0
  166. package/dist/runtime/pipeline/AgentExecuteStage.d.ts +22 -0
  167. package/dist/runtime/pipeline/AgentExecuteStage.d.ts.map +1 -0
  168. package/dist/runtime/pipeline/AgentExecuteStage.js +889 -0
  169. package/dist/runtime/pipeline/AgentExecuteStage.js.map +1 -0
  170. package/dist/runtime/pipeline/ContextAssembleStage.d.ts +26 -0
  171. package/dist/runtime/pipeline/ContextAssembleStage.d.ts.map +1 -0
  172. package/dist/runtime/pipeline/ContextAssembleStage.js +253 -0
  173. package/dist/runtime/pipeline/ContextAssembleStage.js.map +1 -0
  174. package/dist/runtime/pipeline/ContextGatherStage.d.ts +21 -0
  175. package/dist/runtime/pipeline/ContextGatherStage.d.ts.map +1 -0
  176. package/dist/runtime/pipeline/ContextGatherStage.js +161 -0
  177. package/dist/runtime/pipeline/ContextGatherStage.js.map +1 -0
  178. package/dist/runtime/pipeline/IntakeStage.d.ts +25 -0
  179. package/dist/runtime/pipeline/IntakeStage.d.ts.map +1 -0
  180. package/dist/runtime/pipeline/IntakeStage.js +126 -0
  181. package/dist/runtime/pipeline/IntakeStage.js.map +1 -0
  182. package/dist/runtime/pipeline/PostStreamStage.d.ts +26 -0
  183. package/dist/runtime/pipeline/PostStreamStage.d.ts.map +1 -0
  184. package/dist/runtime/pipeline/PostStreamStage.js +129 -0
  185. package/dist/runtime/pipeline/PostStreamStage.js.map +1 -0
  186. package/dist/runtime/pipeline/TurnPipeline.d.ts +54 -0
  187. package/dist/runtime/pipeline/TurnPipeline.d.ts.map +1 -0
  188. package/dist/runtime/pipeline/TurnPipeline.js +15 -0
  189. package/dist/runtime/pipeline/TurnPipeline.js.map +1 -0
  190. package/dist/runtime/pipeline/TurnServices.d.ts +48 -0
  191. package/dist/runtime/pipeline/TurnServices.d.ts.map +1 -0
  192. package/dist/runtime/pipeline/TurnServices.js +2 -0
  193. package/dist/runtime/pipeline/TurnServices.js.map +1 -0
  194. package/dist/runtime/pipeline/agentTypeGuards.d.ts +4 -0
  195. package/dist/runtime/pipeline/agentTypeGuards.d.ts.map +1 -0
  196. package/dist/runtime/pipeline/agentTypeGuards.js +7 -0
  197. package/dist/runtime/pipeline/agentTypeGuards.js.map +1 -0
  198. package/dist/runtime/pipeline/index.d.ts +11 -0
  199. package/dist/runtime/pipeline/index.d.ts.map +1 -0
  200. package/dist/runtime/pipeline/index.js +13 -0
  201. package/dist/runtime/pipeline/index.js.map +1 -0
  202. package/dist/runtime/pipeline/outputProcessing.d.ts +23 -0
  203. package/dist/runtime/pipeline/outputProcessing.d.ts.map +1 -0
  204. package/dist/runtime/pipeline/outputProcessing.js +63 -0
  205. package/dist/runtime/pipeline/outputProcessing.js.map +1 -0
  206. package/dist/runtime/pipeline/sessionUtils.d.ts +12 -0
  207. package/dist/runtime/pipeline/sessionUtils.d.ts.map +1 -0
  208. package/dist/runtime/pipeline/sessionUtils.js +73 -0
  209. package/dist/runtime/pipeline/sessionUtils.js.map +1 -0
  210. package/dist/tools/Tool.d.ts +7 -0
  211. package/dist/tools/Tool.d.ts.map +1 -1
  212. package/dist/tools/Tool.js +12 -3
  213. package/dist/tools/Tool.js.map +1 -1
  214. package/dist/tools/memory.d.ts +26 -0
  215. package/dist/tools/memory.d.ts.map +1 -0
  216. package/dist/tools/memory.js +51 -0
  217. package/dist/tools/memory.js.map +1 -0
  218. package/dist/types/index.d.ts +177 -6
  219. package/dist/types/index.d.ts.map +1 -1
  220. package/dist/types/index.js.map +1 -1
  221. package/guides/AGENTS.md +173 -0
  222. package/guides/README.md +12 -0
  223. package/guides/TOOLS.md +93 -27
  224. package/package.json +12 -4
  225. package/dist/agents/LLMAgent.d.ts +0 -11
  226. package/dist/agents/LLMAgent.d.ts.map +0 -1
  227. package/dist/agents/LLMAgent.js +0 -31
  228. 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,177 @@ 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';
24
22
  export class Runtime {
25
23
  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
24
  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
- }
25
+ _userIdWarned = new Set();
26
+ sessionMutex = new SessionMutex();
27
+ turnServices;
28
+ // Keep references for public API getters
29
+ _sessionStore;
30
+ _memoryService;
31
+ _toolExecutor;
32
+ _conversationState;
33
+ _eventLog;
34
+ _agentStateController;
110
35
  constructor(config) {
111
36
  this.config = config;
37
+ // Build agent map
38
+ const agents = new Map();
112
39
  for (const agent of config.agents) {
113
- this.agents.set(agent.id, agent);
40
+ agents.set(agent.id, agent);
41
+ }
42
+ // Config scalars
43
+ const defaultAgentId = config.defaultAgentId;
44
+ const defaultModel = config.defaultModel;
45
+ const maxSteps = config.maxSteps ?? 20;
46
+ const maxHandoffs = config.maxHandoffs ?? 10;
47
+ const stopConditions = config.stopConditions ?? defaultStopConditions;
48
+ const sessionStore = config.sessionStore ?? new MemoryStore();
49
+ const inputProcessors = config.inputProcessors ?? [];
50
+ const outputProcessors = config.outputProcessors ?? [];
51
+ const outputProcessorMode = config.outputProcessorMode ?? 'stream';
52
+ const alwaysRouteThroughTriage = config.alwaysRouteThroughTriage ?? false;
53
+ const triageAgentId = config.triageAgentId;
54
+ // Output redactions
55
+ let outputRedactions;
56
+ if (config.outputRedaction && config.outputRedaction.length > 0) {
57
+ outputRedactions = config.outputRedaction.map(r => {
58
+ const base = compileSanitizePattern(r.pattern);
59
+ const flags = base.flags.includes('g') ? base.flags : `${base.flags}g`;
60
+ const re = new RegExp(base.source, flags);
61
+ return { re, replacement: r.replacement };
62
+ });
114
63
  }
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();
64
+ // Hooks setup
121
65
  const hooks = { ...config.hooks };
122
66
  const appendOnStreamPart = (fn) => {
123
67
  const originalHook = hooks.onStreamPart;
124
68
  hooks.onStreamPart = async (context, part) => {
125
- if (originalHook) {
69
+ if (originalHook)
126
70
  await originalHook(context, part);
127
- }
128
71
  await fn(context, part);
129
72
  };
130
73
  };
131
74
  if (config.callback) {
132
- const httpCallback = createHttpCallback(config.callback);
133
- appendOnStreamPart(httpCallback);
75
+ appendOnStreamPart(createHttpCallback(config.callback));
134
76
  }
135
77
  if (config.streamCallback) {
136
78
  const adapter = createStreamCallbackAdapter(config.streamCallback);
137
79
  appendOnStreamPart(adapter.onStreamPart);
138
80
  const originalOnEnd = hooks.onEnd;
139
81
  hooks.onEnd = async (context, result) => {
140
- if (originalOnEnd) {
82
+ if (originalOnEnd)
141
83
  await originalOnEnd(context, result);
142
- }
143
84
  if (config.streamCallback?.flushOnEnd) {
144
85
  await adapter.flush(config.streamCallback.flushTimeoutMs ?? 2000);
145
86
  }
146
87
  };
147
88
  }
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,
89
+ const hookRunner = new HookRunner(hooks);
90
+ const enforcer = new ToolEnforcer(config.enforcementRules ?? defaultEnforcementRules);
91
+ const contextManager = config.contextManager;
92
+ // Service instances
93
+ const extractionEngine = new ExtractionEngine({ defaultModel, telemetry: config.telemetry });
94
+ const suggestionManager = new SuggestionManager(config);
95
+ const flowExecutor = new FlowExecutor(config);
96
+ const memoryService = config.memoryService;
97
+ const conversationState = new DefaultConversationState({
98
+ sessionStore,
99
+ defaultAgentId,
167
100
  });
168
- this.sessionEventManager = new SessionEventManager({
169
- touchSession: session => this.touchSession(session),
170
- getSessionTurn: session => this.getSessionTurn(session),
101
+ const eventLog = new DefaultConversationEventLog({ sessionStore });
102
+ const agentStateController = new DefaultAgentStateController();
103
+ const toolExecutor = new DefaultToolExecutor({
104
+ enforcer,
105
+ hookRunner,
106
+ memoryService,
171
107
  });
172
- this.suggestionManager = new SuggestionManager(config);
173
- this.flowExecutor = new FlowExecutor(config);
108
+ let sessionCache;
109
+ if (config.sessionCache) {
110
+ sessionCache = new SessionCache(config.sessionCache);
111
+ }
112
+ const streamEmitter = new StreamEmitter({
113
+ hookRunner,
114
+ eventLog,
115
+ hasStreamPartHook: Boolean(hooks.onStreamPart),
116
+ outputRedactions,
117
+ redactCarryKey: '__ariaRedactCarry',
118
+ redactLookbehind: 64,
119
+ saveSessionCheckpoint: async (session) => {
120
+ await eventLog.checkpoint(session);
121
+ if (sessionCache)
122
+ sessionCache.put(session);
123
+ },
124
+ });
125
+ // Store references for public API
126
+ this._sessionStore = sessionStore;
127
+ this._memoryService = memoryService;
128
+ this._toolExecutor = toolExecutor;
129
+ this._conversationState = conversationState;
130
+ this._eventLog = eventLog;
131
+ this._agentStateController = agentStateController;
132
+ // Build TurnServices — one plain struct, no closures
133
+ this.turnServices = {
134
+ conversationState,
135
+ agentStateController,
136
+ eventLog,
137
+ toolExecutor,
138
+ streamEmitter,
139
+ hookRunner,
140
+ enforcer,
141
+ extractionEngine,
142
+ suggestionManager,
143
+ flowExecutor,
144
+ contextManager,
145
+ memoryService,
146
+ sessionCache,
147
+ sessionStore,
148
+ agents,
149
+ config,
150
+ defaultAgentId,
151
+ defaultModel,
152
+ maxSteps,
153
+ maxHandoffs,
154
+ stopConditions,
155
+ inputProcessors,
156
+ outputProcessors,
157
+ outputProcessorMode,
158
+ outputRedactions,
159
+ alwaysRouteThroughTriage,
160
+ triageAgentId,
161
+ };
174
162
  }
163
+ // --- Public API ---
175
164
  get sessionStore() {
176
165
  return this._sessionStore;
177
166
  }
167
+ get memoryService() {
168
+ return this._memoryService;
169
+ }
170
+ /** Foundation services — shared primitives reusable by VoiceEngine etc. */
171
+ get foundation() {
172
+ return {
173
+ toolExecutor: this._toolExecutor,
174
+ conversationState: this._conversationState,
175
+ eventLog: this._eventLog,
176
+ agentState: this._agentStateController,
177
+ };
178
+ }
178
179
  getActiveAbortController(sessionId) {
179
180
  return this.abortControllers.get(sessionId);
180
181
  }
@@ -185,144 +186,84 @@ export class Runtime {
185
186
  this.abortControllers.delete(sessionId);
186
187
  }
187
188
  }
189
+ async getSession(id) {
190
+ return this._sessionStore.get(id);
191
+ }
192
+ async deleteSession(id) {
193
+ await this._sessionStore.delete(id);
194
+ }
195
+ getAgent(id) {
196
+ return this.turnServices.agents.get(id);
197
+ }
198
+ getAllAgents() {
199
+ return Array.from(this.turnServices.agents.values());
200
+ }
201
+ async *chat(sessionId, input, userId) {
202
+ yield* this.stream({ input, sessionId, userId });
203
+ }
204
+ // --- Main orchestration ---
188
205
  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;
206
+ const svc = this.turnServices;
207
+ // Warn when memoryService is configured but userId is missing
208
+ if (svc.memoryService && !options.userId) {
209
+ const sessionId = options.sessionId ?? 'new';
210
+ if (!this._userIdWarned.has(sessionId)) {
211
+ this._userIdWarned.add(sessionId);
212
+ console.warn('[AriaFlow] memoryService is configured but session has no userId. ' +
213
+ 'Memory operations will be skipped. Pass userId via stream({ input, sessionId, userId }).');
214
+ }
215
+ }
216
+ // Acquire per-session mutex to prevent concurrent read-modify-write races.
217
+ // Only lock when a sessionId is provided (existing session). New sessions
218
+ // get a random UUID and cannot have concurrent callers.
219
+ let releaseMutex;
196
220
  if (options.sessionId) {
197
- const existing = await this._sessionStore.get(options.sessionId);
198
- session = existing ?? this.createSession(effectiveSessionId, userId);
199
- }
200
- else {
201
- session = this.createSession(effectiveSessionId, userId);
221
+ releaseMutex = await this.sessionMutex.acquire(options.sessionId);
202
222
  }
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
- }
223
+ try {
224
+ yield* this._streamInner(options, svc);
220
225
  }
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();
226
+ finally {
227
+ releaseMutex?.();
228
+ }
229
+ }
230
+ /**
231
+ * Inner stream implementation — separated so the mutex can wrap the
232
+ * entire async generator lifecycle from IntakeStage through PostStreamStage.
233
+ */
234
+ async *_streamInner(options, svc) {
235
+ // Stage 1: Intake
236
+ const intakeGen = runIntakeStage(options, svc);
237
+ let intakeNext = await intakeGen.next();
238
+ while (!intakeNext.done) {
239
+ yield intakeNext.value;
240
+ intakeNext = await intakeGen.next();
241
+ }
242
+ const intake = intakeNext.value;
243
+ if (!intake)
244
+ return;
245
+ const { session, context, processedInput, injectionQueue, controller } = intake;
240
246
  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;
247
+ const cleanup = this.setupAbortHandlers(session.id, controller, options.abortSignal);
260
248
  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
- }
249
+ // Stages 2-4: Agent execute (includes gather, assemble, handoff loop)
250
+ yield* runAgentExecuteStage({
251
+ context,
252
+ input: processedInput,
253
+ injectionQueue,
254
+ abortController: controller,
255
+ }, svc);
256
+ if (svc.config.suggestionModel) {
257
+ yield* svc.suggestionManager.generateSuggestions(context);
316
258
  }
317
- yield* this.runLoop(context, injectionQueue, processedInput, controller);
318
- if (this.config.suggestionModel) {
319
- yield* this.generateSuggestions(context);
259
+ if (session.metadata) {
260
+ session.metadata.lastTurnHadToolCalls = context.toolCallHistory.length > 0;
320
261
  }
321
- await this.hookRunner.onEnd(context, { success: true });
262
+ await svc.hookRunner.onEnd(context, { success: true });
322
263
  }
323
264
  catch (error) {
324
265
  if (controller.signal.aborted) {
325
- yield* this.emit(context, {
266
+ yield* svc.streamEmitter.emit(context, {
326
267
  type: 'interrupted',
327
268
  sessionId: context.session.id,
328
269
  reason: controller.signal.reason ?? 'Operation cancelled',
@@ -330,1167 +271,45 @@ export class Runtime {
330
271
  lastAgentId: context.agentId,
331
272
  lastStep: context.stepCount,
332
273
  });
333
- await this.hookRunner.onEnd(context, { success: false });
274
+ await svc.hookRunner.onEnd(context, { success: false });
334
275
  }
335
276
  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 });
277
+ await svc.hookRunner.onError(context, error);
278
+ await svc.hookRunner.onEnd(context, { success: false, error: error });
279
+ yield* svc.streamEmitter.emit(context, { type: 'error', error: error.message });
339
280
  }
340
281
  }
341
282
  finally {
342
- controller.signal.removeEventListener('abort', abortHandler);
343
- if (abortSignal && externalAbortHandler) {
344
- abortSignal.removeEventListener('abort', externalAbortHandler);
345
- }
283
+ cleanup();
346
284
  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);
285
+ yield* runPostStreamStage(context, session, svc);
350
286
  }
351
287
  }
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;
974
- }
975
- return 'I hit a routing issue between agents. I will continue here. Tell me the exact outcome you need in one sentence.';
976
- }
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),
288
+ // --- Private helpers ---
289
+ setupAbortHandlers(sessionId, controller, externalSignal) {
290
+ const abortHandler = () => {
291
+ this.abortControllers.delete(sessionId);
1269
292
  };
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
- }
293
+ controller.signal.addEventListener('abort', abortHandler);
294
+ let externalAbortHandler;
295
+ if (externalSignal) {
296
+ externalAbortHandler = () => {
297
+ controller.abort(externalSignal.reason ?? 'External abort');
298
+ };
299
+ if (externalSignal.aborted) {
300
+ controller.abort(externalSignal.reason ?? 'External abort');
1281
301
  }
1282
- catch {
1283
- if (input.includes(pattern.toLowerCase())) {
1284
- return true;
1285
- }
302
+ else {
303
+ externalSignal.addEventListener('abort', externalAbortHandler);
1286
304
  }
1287
305
  }
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);
306
+ return () => {
307
+ controller.signal.removeEventListener('abort', abortHandler);
308
+ if (externalSignal && externalAbortHandler) {
309
+ externalSignal.removeEventListener('abort', externalAbortHandler);
1311
310
  }
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
311
  };
1378
312
  }
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
313
  }
1495
314
  export function createRuntime(config) {
1496
315
  return new Runtime(config);