import { type LanguageModel, type ModelMessage, type ToolSet, type TelemetrySettings } from 'ai'; import { type AgentStreamPart, type FlowConfig, type FlowContext, type FlowManagerState, type FlowNodeConfig } from '../types/index.js'; import { TokenAccumulator } from '../runtime/TokenAccumulator.js'; import type { TurnUsage } from '../types/telemetry.js'; export interface FlowManagerConfig { flow: FlowConfig; initialNode: string; model: LanguageModel; defaultRolePrompt?: string; contextMessages?: ModelMessage[]; state?: FlowManagerState; telemetry?: TelemetrySettings; abortSignal?: AbortSignal; /** * Agent-level tools merged after node.tools for tool-node resolution and lookups. * Node tools win on name collision. */ agentTools?: ToolSet | ((ctx: FlowContext) => ToolSet); /** Default maxSteps for streamText tool loop. */ maxSteps?: number; /** * Optional tool-call guard executed before a tool's execute() runs. * If it returns {allowed:false}, the tool execution is blocked by throwing, * which surfaces as a `tool-error` in the AI SDK stream. */ toolCallGuard?: (call: { toolName: string; args: unknown; toolCallId?: string; }) => Promise<{ allowed: true; } | { allowed: false; reason: string; }>; /** * Optional hook to enrich tool execute options (for example to inject idempotency metadata). * Returned fields are shallow-merged into `options.experimental_context`. */ toolExecutionOptionsFactory?: (call: { toolName: string; args: unknown; toolCallId?: string; }) => Record | undefined; /** Optional callback for emitting flow-level metrics (timing, counters). Wired by the runtime to custom stream events. */ metricsEmitter?: (name: string, data: Record) => void; /** Shared per-session accumulator; created internally if omitted. */ tokenAccumulator?: TokenAccumulator; /** Model context window size for utilization (tokens). */ modelContextWindow?: number; /** Called after provider usage resolves for each LLM call (async, off the stream path). */ onTokensUpdate?: (turn: TurnUsage) => void; } export declare class FlowManager { private config; private nodes; private transitions; private context; private currentNodeConfig; private initialized; private pendingEvents; private deferredActions; private flowEnded; private sessionMessages?; /** * Headless FlowCapability used to share state-management code with * CapabilityCallWorker. FlowManager rebuilds this lazily from its own * authoritative state (context/initialized/flowEnded) whenever a getter * or resolveTools() needs it — keeping streaming logic untouched. */ private flowCapability; /** * Tracks transition edges (from->to) within a single process() call. * Reset at the start of each user turn. Used to detect oscillation * (e.g., triage->services->triage->services) and break infinite loops. */ private turnTransitionCounts; /** Maximum times the same from->to edge can fire in one turn before being blocked. */ private maxOscillations; private pendingMetrics; private readonly tokenAccumulator; /** Prevents the SAME tool node from re-entering (infinite loop). */ private executingToolNode; private _toolNodeId; private emitMetric; /** Drain queued metrics as custom stream events. Call from any generator. */ private drainMetrics; constructor(config: FlowManagerConfig); /** * Fire-and-forget token capture after the stream or generateText completes. * Must not be awaited on response paths. */ private scheduleStreamTextUsage; private scheduleGenerateTextUsage; private applyUsageToAccumulator; initialize(): AsyncGenerator; process(userInput: string): AsyncGenerator; transitionTo(nodeId: string, data?: Record): Promise; transitionToNode(node: FlowNodeConfig, data?: Record): Promise; transitionToGenerator(nodeId: string, data?: Record, dynamicNode?: FlowNodeConfig): AsyncGenerator; get collectedData(): Record; get currentNode(): string | undefined; get hasEnded(): boolean; getState(): FlowManagerState; /** * Build a FlowCapability snapshot from FlowManager's current authoritative state. * FlowManager owns transitions/streaming; FlowCapability owns state-query logic. * Rebuilding on each call is cheap (pure in-memory graph traversal, no I/O). */ private rebuildCapability; private resolveNodeModel; /** * Expression transitions: first matching synchronous predicate wins (array order). */ evaluateExpressionTransitions(nodeId: string): string | null; private resolveMergedExplicitTools; /** * After a tool-node run, pick the next node: expression edges first per transition row, then `on` event. */ private pickToolNodeTransition; private runToolNodeTurn; private runInference; /** * Builds the system prompt as an array of SystemModelMessage objects. * Layer 1 (role prompt) is marked with Anthropic cache_control for prompt caching. * Uses AI SDK SystemModelMessage format: { role: 'system', content: string, providerOptions? }. */ private buildSystemPrompt; private applyContextStrategy; private generateSummary; private normalizeForComparison; private isEquivalentPayload; private resolvePendingSignals; private resolveTools; private buildImplicitTransitionTools; private wrapTools; private appendMessage; private normalizeMessage; private runActions; private flushDeferredActions; private checkTransitionPolicy; private evaluateTransitions; /** * Extraction node inference: loops until a Zod schema is fully satisfied. * Replaces both the ExtractionEngine's generateObject call and the standard * streamText inference -- single LLM call per turn. * * Three key correctness properties: * 1. safeParse uses nullified collectedData (undefined -> null) so nullable * schema fields don't fail on absent keys. * 2. The follow-up prompt only mentions REQUIRED missing fields, not optional ones. * 3. On auto-transition, if the next node is also an extraction node, the last * user message is re-extracted against the new schema (cross-node carry-forward). */ private runExtractionNodeInference; /** * Check if extraction is complete by running safeParse with nullified data. * Fills undefined schema keys with null so .nullable() fields don't fail * on absent keys (undefined !== null in Zod). */ private isExtractionComplete; /** * Extract structured fields from the last user message against a node's schema. * Returns the number of NEW fields that were extracted (0 if nothing new). * Used both for normal extraction turns and cross-node carry-forward. */ private extractFromLastUserMessage; /** * Cross-node extraction carry-forward: when transitioning from one extraction * node to another, re-extract from the last user message against the new node's * schema. This handles the case where a user provides data for multiple schemas * in a single message (e.g., incident details AND vehicle details in one turn). * * Stops the cascade when zero new fields are extracted -- the user message has * no data for this schema, so further hops would waste LLM calls. */ private runExtractionCarryForward; }