@ariaflowagents/core 0.6.3 → 0.7.0

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