@ariaflowagents/core 0.6.3 → 0.7.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 (161) hide show
  1. package/README.md +58 -5
  2. package/dist/agents/Agent.d.ts +1 -0
  3. package/dist/agents/Agent.d.ts.map +1 -1
  4. package/dist/agents/Agent.js +10 -1
  5. package/dist/agents/Agent.js.map +1 -1
  6. package/dist/agents/CompositeAgent.d.ts +8 -12
  7. package/dist/agents/CompositeAgent.d.ts.map +1 -1
  8. package/dist/agents/CompositeAgent.js +30 -16
  9. package/dist/agents/CompositeAgent.js.map +1 -1
  10. package/dist/agents/FlowAgent.js +2 -2
  11. package/dist/agents/FlowAgent.js.map +1 -1
  12. package/dist/agents/LLMAgent.d.ts.map +1 -1
  13. package/dist/agents/LLMAgent.js +4 -25
  14. package/dist/agents/LLMAgent.js.map +1 -1
  15. package/dist/agents/TriageAgent.d.ts.map +1 -1
  16. package/dist/agents/TriageAgent.js +8 -29
  17. package/dist/agents/TriageAgent.js.map +1 -1
  18. package/dist/callbacks/httpCallback.d.ts +1 -0
  19. package/dist/callbacks/httpCallback.d.ts.map +1 -1
  20. package/dist/callbacks/httpCallback.js +20 -6
  21. package/dist/callbacks/httpCallback.js.map +1 -1
  22. package/dist/callbacks/streamCallback.d.ts +26 -0
  23. package/dist/callbacks/streamCallback.d.ts.map +1 -0
  24. package/dist/callbacks/streamCallback.js +281 -0
  25. package/dist/callbacks/streamCallback.js.map +1 -0
  26. package/dist/flows/FlowManager.d.ts +19 -4
  27. package/dist/flows/FlowManager.d.ts.map +1 -1
  28. package/dist/flows/FlowManager.js +360 -132
  29. package/dist/flows/FlowManager.js.map +1 -1
  30. package/dist/flows/extraction.d.ts +17 -0
  31. package/dist/flows/extraction.d.ts.map +1 -0
  32. package/dist/flows/extraction.js +56 -0
  33. package/dist/flows/extraction.js.map +1 -0
  34. package/dist/flows/index.d.ts +1 -2
  35. package/dist/flows/index.d.ts.map +1 -1
  36. package/dist/flows/index.js +1 -1
  37. package/dist/flows/index.js.map +1 -1
  38. package/dist/flows/template.d.ts +2 -2
  39. package/dist/flows/template.d.ts.map +1 -1
  40. package/dist/flows/template.js +13 -0
  41. package/dist/flows/template.js.map +1 -1
  42. package/dist/flows/validation.d.ts +7 -0
  43. package/dist/flows/validation.d.ts.map +1 -0
  44. package/dist/flows/validation.js +42 -0
  45. package/dist/flows/validation.js.map +1 -0
  46. package/dist/hooks/builtin/metrics.d.ts +4 -34
  47. package/dist/hooks/builtin/metrics.d.ts.map +1 -1
  48. package/dist/hooks/builtin/metrics.js +3 -65
  49. package/dist/hooks/builtin/metrics.js.map +1 -1
  50. package/dist/hooks/helpers.d.ts +8 -47
  51. package/dist/hooks/helpers.d.ts.map +1 -1
  52. package/dist/hooks/helpers.js +38 -104
  53. package/dist/hooks/helpers.js.map +1 -1
  54. package/dist/hooks/index.d.ts +4 -1
  55. package/dist/hooks/index.d.ts.map +1 -1
  56. package/dist/hooks/index.js +2 -0
  57. package/dist/hooks/index.js.map +1 -1
  58. package/dist/index.d.ts +4 -4
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/processors/ProcessorRunner.d.ts.map +1 -1
  63. package/dist/processors/ProcessorRunner.js +13 -0
  64. package/dist/processors/ProcessorRunner.js.map +1 -1
  65. package/dist/prompts/PromptBuilder.d.ts.map +1 -1
  66. package/dist/prompts/PromptBuilder.js +1 -0
  67. package/dist/prompts/PromptBuilder.js.map +1 -1
  68. package/dist/prompts/types.d.ts +2 -1
  69. package/dist/prompts/types.d.ts.map +1 -1
  70. package/dist/prompts/types.js +29 -7
  71. package/dist/prompts/types.js.map +1 -1
  72. package/dist/runtime/ContextManager.d.ts +1 -1
  73. package/dist/runtime/ContextManager.d.ts.map +1 -1
  74. package/dist/runtime/ContextManager.js +141 -20
  75. package/dist/runtime/ContextManager.js.map +1 -1
  76. package/dist/runtime/ExtractionEngine.d.ts +34 -0
  77. package/dist/runtime/ExtractionEngine.d.ts.map +1 -0
  78. package/dist/runtime/ExtractionEngine.js +155 -0
  79. package/dist/runtime/ExtractionEngine.js.map +1 -0
  80. package/dist/runtime/FlowExecutor.d.ts +51 -0
  81. package/dist/runtime/FlowExecutor.d.ts.map +1 -0
  82. package/dist/runtime/FlowExecutor.js +523 -0
  83. package/dist/runtime/FlowExecutor.js.map +1 -0
  84. package/dist/runtime/InjectionQueue.d.ts +8 -1
  85. package/dist/runtime/InjectionQueue.d.ts.map +1 -1
  86. package/dist/runtime/InjectionQueue.js +33 -0
  87. package/dist/runtime/InjectionQueue.js.map +1 -1
  88. package/dist/runtime/Runtime.d.ts +32 -2
  89. package/dist/runtime/Runtime.d.ts.map +1 -1
  90. package/dist/runtime/Runtime.js +513 -612
  91. package/dist/runtime/Runtime.js.map +1 -1
  92. package/dist/runtime/SessionEventManager.d.ts +17 -0
  93. package/dist/runtime/SessionEventManager.d.ts.map +1 -0
  94. package/dist/runtime/SessionEventManager.js +149 -0
  95. package/dist/runtime/SessionEventManager.js.map +1 -0
  96. package/dist/runtime/SuggestionManager.d.ts +7 -0
  97. package/dist/runtime/SuggestionManager.d.ts.map +1 -0
  98. package/dist/runtime/SuggestionManager.js +50 -0
  99. package/dist/runtime/SuggestionManager.js.map +1 -0
  100. package/dist/runtime/index.d.ts +1 -1
  101. package/dist/runtime/index.d.ts.map +1 -1
  102. package/dist/runtime/index.js +1 -1
  103. package/dist/runtime/index.js.map +1 -1
  104. package/dist/services/MetricsService.d.ts +55 -0
  105. package/dist/services/MetricsService.d.ts.map +1 -0
  106. package/dist/services/MetricsService.js +86 -0
  107. package/dist/services/MetricsService.js.map +1 -0
  108. package/dist/services/TracingService.d.ts +13 -0
  109. package/dist/services/TracingService.d.ts.map +1 -0
  110. package/dist/services/TracingService.js +62 -0
  111. package/dist/services/TracingService.js.map +1 -0
  112. package/dist/session/stores/MemoryStore.js +1 -1
  113. package/dist/session/stores/MemoryStore.js.map +1 -1
  114. package/dist/tools/Tool.d.ts +25 -3
  115. package/dist/tools/Tool.d.ts.map +1 -1
  116. package/dist/tools/Tool.js.map +1 -1
  117. package/dist/tools/errorHandling.d.ts +1 -1
  118. package/dist/tools/errorHandling.d.ts.map +1 -1
  119. package/dist/tools/errorHandling.js +27 -20
  120. package/dist/tools/errorHandling.js.map +1 -1
  121. package/dist/tools/http.d.ts.map +1 -1
  122. package/dist/tools/http.js +53 -17
  123. package/dist/tools/http.js.map +1 -1
  124. package/dist/types/index.d.ts +179 -3
  125. package/dist/types/index.d.ts.map +1 -1
  126. package/dist/types/index.js +1 -0
  127. package/dist/types/index.js.map +1 -1
  128. package/dist/types/telemetry.d.ts +52 -0
  129. package/dist/types/telemetry.d.ts.map +1 -0
  130. package/dist/types/telemetry.js +2 -0
  131. package/dist/types/telemetry.js.map +1 -0
  132. package/dist/utils/aiStream.d.ts +7 -0
  133. package/dist/utils/aiStream.d.ts.map +1 -0
  134. package/dist/utils/aiStream.js +41 -0
  135. package/dist/utils/aiStream.js.map +1 -0
  136. package/dist/utils/chrono.d.ts +3 -46
  137. package/dist/utils/chrono.d.ts.map +1 -1
  138. package/dist/utils/chrono.js.map +1 -1
  139. package/dist/utils/isRecord.d.ts +2 -0
  140. package/dist/utils/isRecord.d.ts.map +1 -0
  141. package/dist/utils/isRecord.js +4 -0
  142. package/dist/utils/isRecord.js.map +1 -0
  143. package/dist/utils/messageNormalization.d.ts +3 -0
  144. package/dist/utils/messageNormalization.d.ts.map +1 -0
  145. package/dist/utils/messageNormalization.js +121 -0
  146. package/dist/utils/messageNormalization.js.map +1 -0
  147. package/dist/utils/streamChunk.d.ts +5 -0
  148. package/dist/utils/streamChunk.d.ts.map +1 -0
  149. package/dist/utils/streamChunk.js +50 -0
  150. package/dist/utils/streamChunk.js.map +1 -0
  151. package/guides/EXAMPLE_VERIFICATION.md +53 -0
  152. package/guides/FLOWS.md +29 -0
  153. package/guides/GETTING_STARTED.md +14 -1
  154. package/guides/README.md +3 -1
  155. package/guides/RUNTIME.md +75 -0
  156. package/guides/TOOLS.md +6 -0
  157. package/package.json +2 -2
  158. package/dist/flows/AgentFlowManager.d.ts +0 -161
  159. package/dist/flows/AgentFlowManager.d.ts.map +0 -1
  160. package/dist/flows/AgentFlowManager.js +0 -448
  161. package/dist/flows/AgentFlowManager.js.map +0 -1
@@ -1,8 +1,16 @@
1
- import { streamText, generateText } from 'ai';
2
- import { isFlowTransition, isFlowUpdate } from './transitions.js';
1
+ import { streamText, generateText, tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { createFlowTransition, isFlowTransition, isFlowUpdate } from './transitions.js';
4
+ import { validateFlowConfig } from './validation.js';
3
5
  import { isHandoffResult } from '../tools/handoff.js';
4
6
  import { isFinalResult } from '../tools/final.js';
5
7
  import { compileSanitizePattern, renderNodePrompt } from './template.js';
8
+ import { normalizeModelMessage } from '../utils/messageNormalization.js';
9
+ import { processAIStream } from '../utils/aiStream.js';
10
+ const implicitTransitionToolInputSchema = z.object({
11
+ data: z.record(z.unknown()).optional(),
12
+ message: z.string().optional(),
13
+ });
6
14
  export class FlowManager {
7
15
  config;
8
16
  nodes = new Map();
@@ -11,10 +19,12 @@ export class FlowManager {
11
19
  currentNodeConfig = null;
12
20
  initialized = false;
13
21
  pendingEvents = [];
22
+ deferredActions = [];
14
23
  flowEnded = false;
15
24
  sessionMessages;
16
25
  constructor(config) {
17
26
  this.config = config;
27
+ validateFlowConfig(config.flow, config.initialNode);
18
28
  for (const node of config.flow.nodes) {
19
29
  this.nodes.set(node.id, node);
20
30
  }
@@ -37,7 +47,6 @@ export class FlowManager {
37
47
  messages: config.contextMessages ?? [],
38
48
  };
39
49
  }
40
- this.sessionMessages = config.sessionMessages;
41
50
  if (this.initialized) {
42
51
  this.currentNodeConfig = this.nodes.get(this.context.currentNode) ?? null;
43
52
  }
@@ -48,11 +57,12 @@ export class FlowManager {
48
57
  }
49
58
  yield* this.transitionToGenerator(this.config.initialNode);
50
59
  this.initialized = true;
51
- if (this.currentNodeConfig?.autoRespond !== false) {
52
- yield* this.runInference();
60
+ if (!this.flowEnded && this.currentNodeConfig?.autoRespond !== false) {
61
+ yield* this.runInference(false);
53
62
  }
63
+ yield* this.flushDeferredActions();
54
64
  }
55
- async *process(userInput, options = {}) {
65
+ async *process(userInput) {
56
66
  if (!this.initialized) {
57
67
  throw new Error('Flow not initialized. Call initialize() first.');
58
68
  }
@@ -64,15 +74,15 @@ export class FlowManager {
64
74
  yield { type: 'error', error: 'No current node' };
65
75
  return;
66
76
  }
67
- this.appendMessage({ role: 'user', content: userInput }, options.appendUserToSession !== false);
77
+ this.appendMessage({ role: 'user', content: userInput });
68
78
  const nodeId = this.currentNodeConfig.id;
69
79
  this.context.nodeTurnCounts[nodeId] = (this.context.nodeTurnCounts[nodeId] ?? 0) + 1;
70
80
  if (this.currentNodeConfig.maxTurns && this.context.nodeTurnCounts[nodeId] > this.currentNodeConfig.maxTurns) {
71
81
  yield { type: 'error', error: `Max turns exceeded for node "${nodeId}"` };
72
82
  return;
73
83
  }
74
- yield* this.runInference();
75
- yield { type: 'turn-end' };
84
+ yield* this.runInference(true);
85
+ yield* this.flushDeferredActions();
76
86
  }
77
87
  async transitionTo(nodeId, data) {
78
88
  for await (const _part of this.transitionToGenerator(nodeId, data)) {
@@ -95,6 +105,9 @@ export class FlowManager {
95
105
  const previousNode = this.currentNodeConfig;
96
106
  if (previousNode) {
97
107
  yield* this.runActions(previousNode.postActions ?? []);
108
+ if (this.flowEnded) {
109
+ return;
110
+ }
98
111
  yield { type: 'node-exit', nodeName: previousNode.name ?? previousNode.id };
99
112
  }
100
113
  this.currentNodeConfig = nextNode;
@@ -106,6 +119,9 @@ export class FlowManager {
106
119
  await this.applyContextStrategy(nextNode);
107
120
  yield { type: 'node-enter', nodeName: nextNode.name ?? nextNode.id };
108
121
  yield* this.runActions(nextNode.preActions ?? []);
122
+ if (this.flowEnded) {
123
+ return;
124
+ }
109
125
  if (previousNode) {
110
126
  yield { type: 'flow-transition', from: previousNode.id, to: nextNode.id };
111
127
  }
@@ -126,63 +142,56 @@ export class FlowManager {
126
142
  flowEnded: this.flowEnded,
127
143
  };
128
144
  }
129
- async *runInference() {
130
- if (!this.currentNodeConfig) {
145
+ async *runInference(triggeredByUserTurn) {
146
+ if (this.flowEnded || !this.currentNodeConfig) {
131
147
  return;
132
148
  }
133
149
  const node = this.currentNodeConfig;
134
150
  const systemPrompt = this.buildSystemPrompt(node);
135
151
  const tools = this.resolveTools(node);
136
152
  const maxSteps = node.maxSteps ?? this.config.maxSteps ?? this.config.flow.maxSteps ?? 25;
137
- const streamConfig = {
153
+ const streamOptions = {
138
154
  model: this.config.model,
139
155
  system: systemPrompt,
140
156
  tools,
141
157
  maxSteps,
158
+ abortSignal: this.config.abortSignal,
142
159
  experimental_telemetry: this.config.telemetry,
143
160
  };
144
161
  // On first turn of a node (after context reset), use the node's prompt as the user message
145
162
  // This respects the flow design without auto-generating greetings
146
163
  if (this.context.messages.length === 0) {
147
- streamConfig.prompt = renderNodePrompt(node.prompt, this.context);
164
+ streamOptions.prompt = renderNodePrompt(node.prompt, this.context);
148
165
  }
149
166
  else {
150
- streamConfig.messages = this.context.messages;
167
+ streamOptions.messages = this.context.messages;
151
168
  }
152
- const result = streamText(streamConfig);
169
+ const result = streamText(streamOptions);
153
170
  const outputMode = node.output?.mode ?? 'stream';
154
171
  const shouldBuffer = outputMode === 'buffer';
155
172
  let responseText = '';
156
173
  let bufferedOutputEmitted = false;
157
- let pendingTransition = null;
158
- let pendingHandoff = null;
174
+ const pendingTransitions = [];
175
+ const pendingHandoffs = [];
159
176
  let finalResult = null;
160
177
  let finalEmitted = false;
161
178
  let hadToolResult = false;
162
179
  let pendingToolMessage = null;
163
- for await (const chunk of result.fullStream) {
164
- if (chunk.type === 'text-delta') {
180
+ for await (const part of processAIStream(result.fullStream)) {
181
+ // In buffered mode, hold text-delta tokens until we decide whether to emit.
182
+ // This allows transition/handoff turns to stay silent in routing nodes.
183
+ if (!(shouldBuffer && part.type === 'text-delta')) {
184
+ yield part;
185
+ }
186
+ if (part.type === 'text-delta') {
165
187
  if (finalResult) {
166
188
  continue;
167
189
  }
168
- responseText += chunk.text;
169
- if (!shouldBuffer) {
170
- yield { type: 'text-delta', text: chunk.text };
171
- }
172
- }
173
- if (chunk.type === 'tool-call') {
174
- const args = 'args' in chunk ? chunk.args : chunk.input;
175
- yield { type: 'tool-call', toolName: chunk.toolName, args, toolCallId: chunk.toolCallId };
190
+ responseText += part.text;
176
191
  }
177
- if (chunk.type === 'tool-result') {
178
- const toolResult = 'result' in chunk ? chunk.result : chunk.output;
192
+ if (part.type === 'tool-result') {
193
+ const toolResult = part.result;
179
194
  hadToolResult = true;
180
- yield {
181
- type: 'tool-result',
182
- toolName: chunk.toolName,
183
- result: toolResult,
184
- toolCallId: chunk.toolCallId,
185
- };
186
195
  if (isFinalResult(toolResult)) {
187
196
  finalResult = toolResult;
188
197
  if (!finalEmitted) {
@@ -208,18 +217,22 @@ export class FlowManager {
208
217
  continue;
209
218
  }
210
219
  if (isFlowTransition(toolResult)) {
211
- pendingTransition = {
220
+ pendingTransitions.push({
212
221
  targetNode: toolResult.targetNode,
213
222
  data: toolResult.data,
214
223
  node: toolResult.node,
215
- };
224
+ toolName: part.toolName,
225
+ toolCallId: part.toolCallId,
226
+ });
216
227
  }
217
228
  if (isHandoffResult(toolResult)) {
218
229
  const targetAgent = toolResult.targetAgent ?? toolResult.targetAgentId;
219
- pendingHandoff = {
230
+ pendingHandoffs.push({
220
231
  targetAgent,
221
232
  reason: toolResult.reason,
222
- };
233
+ toolName: part.toolName,
234
+ toolCallId: part.toolCallId,
235
+ });
223
236
  }
224
237
  if (typeof toolResult === 'object' &&
225
238
  toolResult !== null &&
@@ -229,17 +242,6 @@ export class FlowManager {
229
242
  pendingToolMessage = toolResult.message;
230
243
  }
231
244
  }
232
- if (chunk.type === 'tool-error') {
233
- const errText = typeof chunk.error === 'string'
234
- ? chunk.error
235
- : chunk.error?.message ?? 'Tool execution error';
236
- yield {
237
- type: 'tool-error',
238
- toolName: chunk.toolName,
239
- error: errText,
240
- toolCallId: chunk.toolCallId,
241
- };
242
- }
243
245
  }
244
246
  const response = await result.response;
245
247
  let safeText = responseText;
@@ -249,28 +251,22 @@ export class FlowManager {
249
251
  safeText = node.output.sanitize.message;
250
252
  }
251
253
  }
252
- if (finalResult) {
253
- this.appendMessage({ role: 'assistant', content: finalResult.text });
254
- }
255
- else if (shouldBuffer) {
256
- let appendedAssistant = false;
257
- for (const message of response.messages) {
258
- if (message.role === 'assistant' && typeof message.content === 'string') {
259
- if (!appendedAssistant) {
260
- this.appendMessage({ role: 'assistant', content: safeText });
261
- appendedAssistant = true;
262
- }
263
- continue;
264
- }
265
- this.appendMessage(message);
266
- }
267
- if (!appendedAssistant) {
268
- this.appendMessage({ role: 'assistant', content: safeText });
269
- }
254
+ for (const message of response.messages) {
255
+ this.appendMessage(message);
270
256
  }
271
- else {
272
- for (const message of response.messages) {
273
- this.appendMessage(message);
257
+ yield { type: 'turn-end', metadata: { response } };
258
+ const signalResolution = this.resolvePendingSignals(pendingTransitions, pendingHandoffs);
259
+ if (signalResolution.conflict) {
260
+ yield { type: 'error', error: signalResolution.conflict };
261
+ return;
262
+ }
263
+ let pendingTransition = signalResolution.transition ?? null;
264
+ const pendingHandoff = signalResolution.handoff ?? null;
265
+ if (pendingTransition) {
266
+ const policy = this.checkTransitionPolicy(node.id, pendingTransition.targetNode, 'tool', triggeredByUserTurn);
267
+ if (policy) {
268
+ pendingTransition = null;
269
+ yield { type: 'error', error: policy };
274
270
  }
275
271
  }
276
272
  if (!finalResult && !pendingTransition && !pendingHandoff && responseText.trim().length === 0 && pendingToolMessage) {
@@ -299,6 +295,7 @@ export class FlowManager {
299
295
  model: this.config.model,
300
296
  system: `${systemPrompt}\n\nRespond to the user now using the tool results above.`,
301
297
  messages: this.context.messages,
298
+ abortSignal: this.config.abortSignal,
302
299
  experimental_telemetry: this.config.telemetry,
303
300
  });
304
301
  responseText = followup.text;
@@ -308,7 +305,7 @@ export class FlowManager {
308
305
  bufferedOutputEmitted = true;
309
306
  }
310
307
  }
311
- if (!finalResult && shouldBuffer && responseText.trim().length > 0 && !bufferedOutputEmitted) {
308
+ if (!finalResult && !pendingTransition && !pendingHandoff && shouldBuffer && responseText.trim().length > 0 && !bufferedOutputEmitted) {
312
309
  yield { type: 'text-delta', text: safeText };
313
310
  }
314
311
  if (finalResult) {
@@ -326,11 +323,16 @@ export class FlowManager {
326
323
  yield { type: 'flow-end', reason: 'handoff' };
327
324
  return;
328
325
  }
329
- const nextTransition = pendingTransition ?? (await this.evaluateTransitions());
326
+ const evaluated = await this.evaluateTransitions(triggeredByUserTurn);
327
+ if (evaluated.blockedError) {
328
+ yield { type: 'error', error: evaluated.blockedError };
329
+ return;
330
+ }
331
+ const nextTransition = pendingTransition ?? evaluated.transition;
330
332
  if (nextTransition) {
331
333
  yield* this.transitionToGenerator(nextTransition.targetNode, nextTransition.data, nextTransition.node);
332
- if (this.currentNodeConfig?.autoRespond !== false) {
333
- yield* this.runInference();
334
+ if (!this.flowEnded && this.currentNodeConfig?.autoRespond !== false) {
335
+ yield* this.runInference(false);
334
336
  }
335
337
  }
336
338
  if (node.postActions?.some(action => action.type === 'end')) {
@@ -339,7 +341,10 @@ export class FlowManager {
339
341
  }
340
342
  }
341
343
  buildSystemPrompt(node) {
342
- const rolePrompt = this.config.defaultRolePrompt ?? this.config.flow.defaultRolePrompt ?? '';
344
+ const includeGlobalPrompt = node.addGlobalPrompt !== false;
345
+ const rolePrompt = includeGlobalPrompt
346
+ ? (this.config.defaultRolePrompt ?? this.config.flow.defaultRolePrompt ?? '')
347
+ : '';
343
348
  const renderedPrompt = renderNodePrompt(node.prompt, this.context);
344
349
  const dataKeys = Object.keys(this.context.collectedData);
345
350
  const dataContext = dataKeys.length > 0
@@ -348,15 +353,19 @@ export class FlowManager {
348
353
  const historyContext = this.context.nodeHistory.length > 1
349
354
  ? `\n\n## Progress\n${this.context.nodeHistory.slice(0, -1).join(' -> ')} -> **${node.name ?? node.id}**`
350
355
  : '';
351
- return `${rolePrompt}
352
-
353
- ## Current Task
354
- ${renderedPrompt}${dataContext}${historyContext}
355
-
356
- ## Instructions
356
+ const currentTaskSection = `## Current Task
357
+ ${renderedPrompt}${dataContext}${historyContext}`;
358
+ const instructionsSection = `## Instructions
357
359
  - Focus only on the current task
358
360
  - Use the available tools to progress the conversation
359
361
  - Do not attempt tasks outside your current scope`;
362
+ const sections = [];
363
+ if (rolePrompt.trim().length > 0) {
364
+ sections.push(rolePrompt.trim());
365
+ }
366
+ sections.push(currentTaskSection);
367
+ sections.push(instructionsSection);
368
+ return sections.join('\n\n');
360
369
  }
361
370
  async applyContextStrategy(node) {
362
371
  const strategy = node.contextStrategy ?? this.config.flow.contextStrategy ?? 'append';
@@ -367,9 +376,11 @@ ${renderedPrompt}${dataContext}${historyContext}
367
376
  case 'reset_with_summary':
368
377
  if (this.context.messages.length > 0) {
369
378
  const summary = await this.generateSummary(node);
370
- this.context.messages = [
371
- { role: 'system', content: `Previous conversation summary: ${summary}` },
372
- ];
379
+ if (summary && summary.trim().length > 0) {
380
+ this.context.messages = [
381
+ { role: 'system', content: `Previous conversation summary: ${summary}` },
382
+ ];
383
+ }
373
384
  }
374
385
  break;
375
386
  case 'append':
@@ -382,26 +393,166 @@ ${renderedPrompt}${dataContext}${historyContext}
382
393
  ?? this.config.flow.summaryPrompt
383
394
  ?? 'Summarize the key points from this conversation in 2-3 sentences.';
384
395
  const maxTokens = node.summaryMaxTokens ?? this.config.flow.summaryMaxTokens;
385
- const result = await generateText({
386
- model: this.config.model,
387
- system: prompt,
388
- messages: this.context.messages,
389
- ...(typeof maxTokens === 'number' ? { maxTokens } : {}),
390
- experimental_telemetry: this.config.telemetry,
391
- });
392
- return result.text;
396
+ const timeoutMs = node.summaryTimeoutMs ?? this.config.flow.summaryTimeoutMs ?? 5000;
397
+ try {
398
+ const generate = async () => {
399
+ const result = await generateText({
400
+ model: this.config.model,
401
+ system: prompt,
402
+ messages: this.context.messages,
403
+ ...(typeof maxTokens === 'number' ? { maxTokens } : {}),
404
+ abortSignal: this.config.abortSignal,
405
+ experimental_telemetry: this.config.telemetry,
406
+ });
407
+ return result.text;
408
+ };
409
+ if (timeoutMs <= 0) {
410
+ return await generate();
411
+ }
412
+ let timeoutHandle;
413
+ const timeout = new Promise(resolve => {
414
+ timeoutHandle = setTimeout(() => resolve(null), timeoutMs);
415
+ });
416
+ const summary = await Promise.race([generate().catch(() => null), timeout]);
417
+ if (timeoutHandle) {
418
+ clearTimeout(timeoutHandle);
419
+ }
420
+ return summary;
421
+ }
422
+ catch {
423
+ return null;
424
+ }
425
+ }
426
+ normalizeForComparison(value) {
427
+ if (value === null || value === undefined) {
428
+ return value;
429
+ }
430
+ if (value instanceof Date) {
431
+ return value.toISOString();
432
+ }
433
+ if (Array.isArray(value)) {
434
+ return value.map(item => this.normalizeForComparison(item));
435
+ }
436
+ if (typeof value === 'function') {
437
+ return '[function]';
438
+ }
439
+ if (typeof value === 'object') {
440
+ const input = value;
441
+ const sortedKeys = Object.keys(input).sort();
442
+ const output = {};
443
+ for (const key of sortedKeys) {
444
+ output[key] = this.normalizeForComparison(input[key]);
445
+ }
446
+ return output;
447
+ }
448
+ return value;
449
+ }
450
+ isEquivalentPayload(a, b) {
451
+ return JSON.stringify(this.normalizeForComparison(a)) === JSON.stringify(this.normalizeForComparison(b));
452
+ }
453
+ resolvePendingSignals(transitions, handoffs) {
454
+ if (transitions.length === 0 && handoffs.length === 0) {
455
+ return {};
456
+ }
457
+ if (transitions.length > 0 && handoffs.length > 0) {
458
+ const transitionTool = transitions[0];
459
+ const handoffTool = handoffs[0];
460
+ return {
461
+ conflict: `Conflicting flow signals: both transition (${transitionTool.targetNode}) and handoff (${handoffTool.targetAgent}) requested in the same turn.`,
462
+ };
463
+ }
464
+ if (handoffs.length > 0) {
465
+ const first = handoffs[0];
466
+ for (let i = 1; i < handoffs.length; i++) {
467
+ const next = handoffs[i];
468
+ if (next.targetAgent !== first.targetAgent) {
469
+ return {
470
+ conflict: `Conflicting handoffs requested in the same turn: "${first.targetAgent}" and "${next.targetAgent}".`,
471
+ };
472
+ }
473
+ }
474
+ return {
475
+ handoff: {
476
+ targetAgent: first.targetAgent,
477
+ reason: first.reason,
478
+ },
479
+ };
480
+ }
481
+ const firstTransition = transitions[0];
482
+ for (let i = 1; i < transitions.length; i++) {
483
+ const next = transitions[i];
484
+ if (next.targetNode !== firstTransition.targetNode) {
485
+ return {
486
+ conflict: `Conflicting transitions requested in the same turn: "${firstTransition.targetNode}" and "${next.targetNode}".`,
487
+ };
488
+ }
489
+ if (!this.isEquivalentPayload(next.data, firstTransition.data)) {
490
+ return {
491
+ conflict: `Conflicting transition payloads for node "${firstTransition.targetNode}" in the same turn.`,
492
+ };
493
+ }
494
+ if ((next.node?.id ?? null) !== (firstTransition.node?.id ?? null)) {
495
+ return {
496
+ conflict: `Conflicting dynamic node definitions for transition target "${firstTransition.targetNode}" in the same turn.`,
497
+ };
498
+ }
499
+ }
500
+ return {
501
+ transition: {
502
+ targetNode: firstTransition.targetNode,
503
+ data: firstTransition.data,
504
+ node: firstTransition.node,
505
+ },
506
+ };
393
507
  }
394
508
  resolveTools(node) {
395
- if (!node.tools)
509
+ const implicitTools = this.buildImplicitTransitionTools(node);
510
+ let explicitTools = {};
511
+ if (node.tools) {
512
+ explicitTools =
513
+ typeof node.tools === 'function'
514
+ ? (node.tools(this.context) ?? {})
515
+ : node.tools;
516
+ }
517
+ // Explicit node tools win on name collision to preserve backwards compatibility.
518
+ return this.wrapTools({
519
+ ...implicitTools,
520
+ ...explicitTools,
521
+ });
522
+ }
523
+ buildImplicitTransitionTools(node) {
524
+ const transitions = this.transitions.get(node.id) ?? [];
525
+ if (transitions.length === 0) {
396
526
  return {};
397
- if (typeof node.tools === 'function') {
398
- return this.wrapTools(node.tools(this.context) ?? {});
399
527
  }
400
- return this.wrapTools(node.tools);
528
+ const implicitTools = {};
529
+ const seenNames = new Set();
530
+ for (const transition of transitions) {
531
+ const eventName = typeof transition.on === 'string' ? transition.on.trim() : '';
532
+ if (!eventName || seenNames.has(eventName)) {
533
+ continue;
534
+ }
535
+ seenNames.add(eventName);
536
+ const label = transition.contract?.label?.trim();
537
+ const conditionText = transition.contract?.conditionText?.trim();
538
+ const descriptionParts = [
539
+ `Transition from "${node.id}" to "${transition.to}".`,
540
+ label ? `Label: ${label}.` : '',
541
+ conditionText ? `Condition: ${conditionText}.` : '',
542
+ 'Call only when transition criteria are satisfied.',
543
+ ].filter(Boolean);
544
+ implicitTools[eventName] = tool({
545
+ description: descriptionParts.join(' '),
546
+ inputSchema: implicitTransitionToolInputSchema,
547
+ execute: async (input) => createFlowTransition(transition.to, input.data, input.message),
548
+ });
549
+ }
550
+ return implicitTools;
401
551
  }
402
552
  wrapTools(tools) {
403
553
  const guard = this.config.toolCallGuard;
404
- if (!guard)
554
+ const optionsFactory = this.config.toolExecutionOptionsFactory;
555
+ if (!guard && !optionsFactory)
405
556
  return tools;
406
557
  const wrapped = {};
407
558
  for (const [toolName, toolDef] of Object.entries(tools ?? {})) {
@@ -412,51 +563,72 @@ ${renderedPrompt}${dataContext}${historyContext}
412
563
  }
413
564
  wrapped[toolName] = {
414
565
  ...toolDef,
415
- execute: async (args) => {
416
- const decision = await guard({ toolName, args });
417
- if (!decision.allowed) {
418
- throw new Error(decision.reason || 'Tool call blocked');
566
+ execute: async (args, options) => {
567
+ const toolCallId = typeof options?.toolCallId === 'string' ? options.toolCallId : undefined;
568
+ if (guard) {
569
+ const decision = await guard({ toolName, args, toolCallId });
570
+ if (!decision.allowed) {
571
+ throw new Error(decision.reason || 'Tool call blocked');
572
+ }
419
573
  }
420
- return exec(args);
574
+ const extraContext = optionsFactory?.({ toolName, args, toolCallId });
575
+ const mergedOptions = mergeToolExecutionOptions(options, extraContext);
576
+ return exec(args, mergedOptions);
421
577
  },
422
578
  };
423
579
  }
424
580
  return wrapped;
425
581
  }
426
- appendMessage(message, persistToSession = true) {
582
+ appendMessage(message) {
427
583
  const normalized = this.normalizeMessage(message);
428
584
  if (!normalized) {
429
585
  return;
430
586
  }
431
587
  this.context.messages.push(normalized);
432
- if (persistToSession && this.sessionMessages && this.sessionMessages !== this.context.messages) {
433
- this.sessionMessages.push(normalized);
434
- }
435
588
  }
436
589
  normalizeMessage(message) {
437
- // Keep only simple string messages in history to avoid provider-specific structured parts
438
- // causing failures on subsequent turns. Tool results are already reflected in collectedData.
439
- const role = message.role;
440
- if (role === 'tool') {
441
- return null;
442
- }
443
- const content = message.content;
444
- if (typeof content === 'string') {
445
- return message;
446
- }
447
- if (Array.isArray(content)) {
448
- const text = content
449
- .map((p) => (p && typeof p === 'object' && p.type === 'text' ? String(p.text ?? '') : ''))
450
- .join('')
451
- .trim();
452
- if (text.length === 0) {
453
- return null;
590
+ return normalizeModelMessage(message);
591
+ }
592
+ async *runActions(actions) {
593
+ for (const action of actions) {
594
+ if (action.deferUntil === 'turn-end') {
595
+ this.deferredActions.push(action);
596
+ continue;
597
+ }
598
+ switch (action.type) {
599
+ case 'say':
600
+ yield { type: 'text-delta', text: action.text };
601
+ break;
602
+ case 'end':
603
+ this.flowEnded = true;
604
+ yield { type: 'flow-end', reason: action.reason ?? 'action' };
605
+ break;
606
+ case 'function':
607
+ await action.handler(this.context);
608
+ break;
609
+ case 'emit':
610
+ this.pendingEvents.push({ name: action.event, data: action.data });
611
+ yield {
612
+ type: 'custom',
613
+ name: action.event,
614
+ data: action.data,
615
+ timestamp: new Date(),
616
+ };
617
+ break;
618
+ default:
619
+ break;
454
620
  }
455
- return { role, content: text };
456
621
  }
457
- return null;
458
622
  }
459
- async *runActions(actions) {
623
+ async *flushDeferredActions() {
624
+ if (this.deferredActions.length === 0) {
625
+ return;
626
+ }
627
+ const actions = this.deferredActions;
628
+ this.deferredActions = [];
629
+ if (this.flowEnded) {
630
+ return;
631
+ }
460
632
  for (const action of actions) {
461
633
  switch (action.type) {
462
634
  case 'say':
@@ -471,24 +643,57 @@ ${renderedPrompt}${dataContext}${historyContext}
471
643
  break;
472
644
  case 'emit':
473
645
  this.pendingEvents.push({ name: action.event, data: action.data });
646
+ yield {
647
+ type: 'custom',
648
+ name: action.event,
649
+ data: action.data,
650
+ timestamp: new Date(),
651
+ };
474
652
  break;
475
653
  default:
476
654
  break;
477
655
  }
656
+ if (this.flowEnded) {
657
+ return;
658
+ }
478
659
  }
479
660
  }
480
- async evaluateTransitions() {
661
+ checkTransitionPolicy(currentNodeId, targetNodeId, source, triggeredByUserTurn) {
662
+ const candidates = (this.transitions.get(currentNodeId) ?? []).filter(transition => transition.to === targetNodeId);
663
+ const candidate = candidates.find(transition => transition.contract) ?? candidates[0];
664
+ const contract = candidate?.contract;
665
+ if (!contract) {
666
+ return null;
667
+ }
668
+ if (contract.toolOnly && source === 'condition') {
669
+ return `Transition "${currentNodeId}" -> "${targetNodeId}" is blocked: contract.toolOnly=true requires a tool/event signal.`;
670
+ }
671
+ if (contract.requiresUserTurn && !triggeredByUserTurn) {
672
+ return `Transition "${currentNodeId}" -> "${targetNodeId}" is blocked: contract.requiresUserTurn=true requires a user turn before transition.`;
673
+ }
674
+ return null;
675
+ }
676
+ async evaluateTransitions(triggeredByUserTurn) {
481
677
  const currentNode = this.currentNodeConfig?.id;
482
678
  if (!currentNode) {
483
- return null;
679
+ return {};
484
680
  }
485
681
  const transitions = this.transitions.get(currentNode) ?? [];
486
682
  if (this.pendingEvents.length > 0) {
487
683
  for (const [index, event] of this.pendingEvents.entries()) {
488
684
  const match = transitions.find(transition => transition.on === event.name);
489
685
  if (match) {
686
+ const policy = this.checkTransitionPolicy(currentNode, match.to, 'event', triggeredByUserTurn);
490
687
  this.pendingEvents.splice(index, 1);
491
- return { targetNode: match.to, data: event.data };
688
+ if (policy) {
689
+ return { blockedError: policy };
690
+ }
691
+ return {
692
+ transition: {
693
+ targetNode: match.to,
694
+ data: event.data,
695
+ },
696
+ };
492
697
  }
493
698
  }
494
699
  }
@@ -498,10 +703,33 @@ ${renderedPrompt}${dataContext}${historyContext}
498
703
  }
499
704
  const shouldTransition = await transition.condition(this.context);
500
705
  if (shouldTransition) {
501
- return { targetNode: transition.to };
706
+ const policy = this.checkTransitionPolicy(currentNode, transition.to, 'condition', triggeredByUserTurn);
707
+ if (policy) {
708
+ return { blockedError: policy };
709
+ }
710
+ return { transition: { targetNode: transition.to } };
502
711
  }
503
712
  }
504
- return null;
713
+ return {};
714
+ }
715
+ }
716
+ function isRecord(value) {
717
+ return typeof value === 'object' && value !== null;
718
+ }
719
+ function mergeToolExecutionOptions(options, extraContext) {
720
+ if (!extraContext || Object.keys(extraContext).length === 0) {
721
+ return options;
505
722
  }
723
+ const baseOptions = isRecord(options) ? options : {};
724
+ const existingContext = isRecord(baseOptions.experimental_context)
725
+ ? baseOptions.experimental_context
726
+ : {};
727
+ return {
728
+ ...baseOptions,
729
+ experimental_context: {
730
+ ...existingContext,
731
+ ...extraContext,
732
+ },
733
+ };
506
734
  }
507
735
  //# sourceMappingURL=FlowManager.js.map