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