@exaflow/core
Version:
Core package for exaflow flow execution framework
486 lines (471 loc) • 15.1 kB
TypeScript
import { JSONSchema7 } from 'json-schema';
import { EventEmitter } from 'eventemitter3';
/**
* Supported expression languages. Currently 'cel' is the default and recommended.
*/
type ExpressionLanguage = 'cel';
interface XFFlowMetadata<TState extends object = Record<string, unknown>> {
id: string;
title: string;
description?: string;
/**
* Expression language used for all conditions and rules in this flow.
* Defaults to 'cel'.
*/
expressionLanguage?: ExpressionLanguage;
globalState?: TState;
/**
* JSON Schema for validating the globalState structure.
* When provided, all state changes will be validated against this schema.
*/
stateSchema?: JSONSchema7;
stateRules?: StateRule[];
autoAdvance?: AutoAdvanceMode;
metadata?: Record<string, unknown>;
}
interface XFFlow<TState extends object = Record<string, unknown>> extends XFFlowMetadata<TState> {
nodes: XFNode[];
startNodeId: string;
}
type AutoAdvanceMode = 'always' | 'default' | 'never';
type NodeType = 'start' | 'action' | 'decision' | 'end' | 'isolated';
interface XFNode {
id: string;
title: string;
content?: string;
/**
* Operations to perform when this node is entered
*/
actions?: StateAction[];
outlets?: XFOutlet[];
isAutoAdvance?: boolean;
metadata?: Record<string, unknown>;
}
interface XFOutlet {
id: string;
to: string;
label?: string;
condition?: string;
/**
* Operations to perform when this outlet is traversed
*/
actions?: StateAction[];
metadata?: Record<string, unknown>;
}
interface StateAction {
type: 'set';
target: string;
value?: unknown;
expression?: string;
}
interface StateCondition {
expression: string;
targetEdge?: string;
}
interface StateRule {
condition: string;
action: 'forceTransition' | 'setState' | 'triggerEvent';
target?: string;
value?: unknown;
}
interface ValidationResult {
isValid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
}
interface ValidationError {
type: 'missing_node' | 'invalid_edge' | 'circular_dependency' | 'syntax_error' | 'invalid_path_structure';
message: string;
nodeId?: string;
edgeId?: string;
}
interface ValidationWarning {
type: 'unreachable_node' | 'missing_end_node' | 'complex_condition' | 'circular_dependency' | 'suboptimal_path_order' | 'unreachable_path' | 'missing_default_path';
message: string;
nodeId?: string;
edgeId?: string;
}
interface ExecutionResult<TState extends object = Record<string, unknown>> {
node: AnnotatedNode;
choices: Choice[];
isComplete: boolean;
canGoBack: boolean;
autoAdvanced?: boolean;
state: TState;
}
interface Choice {
id: string;
label: string;
description?: string;
outletId: string;
disabled?: boolean;
disabledReason?: string;
metadata?: Record<string, unknown>;
}
interface AnnotatedNode {
node: XFNode;
type: NodeType;
}
interface ExecutionStep<TState extends object = Record<string, unknown>> {
node: AnnotatedNode;
choice?: string;
timestamp: Date;
state: TState;
}
interface EngineOptions<TState extends object = Record<string, unknown>> {
initialState?: Partial<TState>;
enableHistory?: boolean;
maxHistorySize?: number;
autoSave?: boolean;
autoAdvance?: AutoAdvanceMode;
showDisabledChoices?: boolean;
enableLogging?: boolean;
stateManager?: IStateManager<TState>;
}
type EngineEvent = 'nodeEnter' | 'nodeExit' | 'stateChange' | 'autoAdvance' | 'complete' | 'error';
interface EngineEventData<TState extends object = Record<string, unknown>> {
nodeEnter: {
node: XFNode;
state: TState;
};
nodeExit: {
node: XFNode;
choice?: string;
state: TState;
};
stateChange: {
oldState: TState;
newState: TState;
};
autoAdvance: {
from: XFNode;
to: XFNode;
condition: string;
};
complete: {
history: ExecutionStep<TState>[];
finalState: TState;
};
error: {
error: Error;
context: unknown;
};
}
/**
* Interface for state management with dependency injection support.
* Generic type T represents the state shape (must be object-like).
*/
interface StateManagerOptions {
expressionLanguage?: ExpressionLanguage;
stateSchema?: JSONSchema7;
validateOnChange?: boolean;
}
interface IStateManager<TState extends object = Record<string, unknown>> {
/**
* Get the current state
*/
getState(): TState;
/**
* Update the state with new values
*/
setState(newState: Partial<TState>): void;
/**
* Execute state actions
*/
executeActions(actions: StateAction[]): void;
/**
* Evaluate a condition expression against the current state
*/
evaluateCondition(expression: string): boolean;
/**
* Reset the state to a new initial state
*/
reset(newState?: Partial<TState>): void;
/**
* Event emitter methods for state change notifications
*/
on(event: 'stateChange', listener: (data: {
oldState: TState;
newState: TState;
}) => void): this;
on(event: 'error', listener: (data: {
error: Error;
context?: {
type: string;
rule?: StateRule;
};
}) => void): this;
off(event: string, listener: (...args: unknown[]) => void): this;
emit(event: string, ...args: unknown[]): boolean;
}
declare class FlowEngine<TState extends object = Record<string, unknown>> extends EventEmitter<EngineEventData<TState>> {
private flow;
private nodeTypes;
private stateManager;
private currentNode;
private history;
private options;
private contentInterpolator;
constructor(flow: XFFlow, options?: EngineOptions<TState>);
start(): Promise<ExecutionResult<TState>>;
next(choiceId?: string): Promise<ExecutionResult<TState>>;
getCurrentNode(): AnnotatedNode | null;
getHistory(): ExecutionStep<TState>[];
getAvailableChoices(): Choice[];
isComplete(): boolean;
canGoBack(): boolean;
goBack(): Promise<ExecutionResult<TState>>;
reset(): void;
getState(): TState;
getStateManager(): IStateManager<TState>;
private transitionToNode;
private evaluateAutomaticDecision;
private evaluateOutletCondition;
/**
* Selects the appropriate outlet for auto-advance using if-else style logic.
* Evaluates outlets in order, returning the first outlet whose condition is true.
* If no conditional outlets match, returns the default outlet (one without condition).
*/
private selectAutoAdvanceOutlet;
/**
* Get a node with interpolated content and title
*/
private getInterpolatedNode;
private createExecutionResult;
private handleForcedTransition;
private shouldAutoAdvance;
private findNodeById;
private getOutlets;
private findOutletById;
}
declare class StateManager<TState extends object = Record<string, unknown>> extends EventEmitter implements IStateManager<TState> {
private state;
private rules;
private expressionLanguage;
private evaluators;
private stateSchema;
private schemaValidator;
private validateOnChange;
private actionExecutor;
constructor(initialState?: TState, rules?: StateRule[], options?: StateManagerOptions);
getState(): TState;
setState(newState: Partial<TState>): void;
executeActions(actions: StateAction[]): void;
evaluateCondition(expression: string): boolean;
private evaluateRules;
private executeRule;
reset(newState?: Partial<TState>): void;
/**
* Validate state against the JSON schema if one is provided
*/
private validateState;
}
interface ExpressionEvaluator {
/**
* Compile an expression into a function for evaluation.
* @param expression The expression to compile.
* @param options Optional options for the compiler.
* @returns A compiled function that can be called to evaluate the expression.
*/
compile(expression: string, options?: ExpressionCompilationOptions): ExpressionCompilationResult;
/**
* Evaluate an expression.
* @param expression The expression to evaluate.
* @param context The context to evaluate the expression in.
* @param options Optional options for the evaluator.
* @returns The result of the evaluation.
*/
evaluate(expression: string, context: unknown, options?: ExpressionEvaluationOptions): unknown;
/**
* Clear the cache of compiled expressions.
*/
clearCache?(): void;
}
interface ExpressionEvaluationOptions {
/**
* Callback function to handle evaluation errors.
*
* If it is not provided, the error will be thrown as an exception.
*/
onError?: (error: Error) => void;
}
interface ExpressionCompilationOptions {
/**
* Whether to cache the compiled expression.
*/
cache?: boolean;
}
interface ExpressionCompilationResult {
success: boolean;
error?: unknown;
}
/**
* CelEvaluator compiles and evaluates CEL expressions against a state context.
* It caches compiled runtimes per expression for performance.
*/
declare class CelEvaluator implements ExpressionEvaluator {
private cache;
compile(expression: string, options?: ExpressionCompilationOptions): ExpressionCompilationResult;
evaluate(expression: string, context: unknown, { onError }?: ExpressionEvaluationOptions): unknown;
clearCache(): void;
}
declare class FlowValidator {
private evaluators;
constructor(evaluators?: Record<string, ExpressionEvaluator>);
validate(flow: XFFlow): ValidationResult;
/**
* Validates that auto-advance nodes follow proper if-else logic patterns.
* Auto-advance nodes should have their outlets structured like if-else statements:
* - Conditional outlets (if/else-if clauses) should be evaluated in order
* - At most one default outlet (else clause) without conditions
* - No unreachable outlets after a default outlet
*/
private validateAutoAdvanceNodes;
/**
* Validates that a node's outlets follow proper if-else structure:
* - Conditional outlets come first (if/else-if)
* - At most one default outlet (else)
* - Default outlet should be last
*/
private validateIfElsePathStructure;
/**
* Checks if a condition expression is always true (basic heuristics)
*/
private isAlwaysTrueCondition;
private isValidExpression;
}
interface PathTestResult {
isValid: boolean;
totalPaths: number;
completedPaths: number;
errors: PathTestPointOfInterest[];
warnings: PathTestPointOfInterest[];
pathSummary: PathSummary[];
coverage: CoverageReport;
}
interface PathTestPointOfInterest {
type: 'missing_node' | 'unreachable_node' | 'infinite_loop' | 'missing_end' | 'invalid_transition' | 'state_error' | 'long_path' | 'unused_node' | 'complex_condition' | 'state_inconsistency';
message: string;
path?: string[];
nodeId?: string;
pathId?: string;
}
interface PathSummary {
id: string;
path: string[];
endType: 'completed' | 'error' | 'infinite_loop';
steps: number;
finalState: Record<string, unknown>;
choices: string[];
}
interface CoverageReport {
nodesCovered: number;
totalNodes: number;
pathsCovered: number;
totalPaths: number;
uncoveredNodes: string[];
uncoveredPaths: string[];
}
declare class PathTester {
private flow;
private nodeTypes;
private maxPaths;
private maxSteps;
private evaluators;
private defaultLanguage;
private actionExecutor;
constructor(flow: XFFlow, maxPaths?: number, maxSteps?: number);
/**
* Test all possible paths through the flow
*/
testAllPaths(): Promise<PathTestResult>;
/**
* Test a specific path through the flow
*/
testPath(choices: string[]): Promise<PathSummary>;
/**
* Generate a simple test report
*/
generateReport(result: PathTestResult): string;
private findNodeById;
private getAvailablePaths;
private evaluateCondition;
private generateCoverageReport;
}
interface AnalysisInsight {
type: 'info' | 'warning' | 'error' | 'suggestion';
category: 'structure' | 'paths' | 'content' | 'performance' | 'usability';
title: string;
description: string;
nodeId?: string;
pathExample?: string[];
actionable?: boolean;
suggestion?: string;
}
interface FlowAnalysis {
isValid: boolean;
score: number;
insights: AnalysisInsight[];
structure: {
totalNodes: number;
decisionNodes: number;
endNodes: number;
startNodes: number;
averageChoicesPerDecision: number;
maxDepth: number;
branchingFactor: number;
};
paths: {
totalPaths: number;
completedPaths: number;
averagePathLength: number;
shortestPath: number;
longestPath: number;
unreachableNodes: number;
loops: number;
};
content: {
averageNodeTextLength: number;
emptyNodes: number;
duplicateContent: number;
clarityScore: number;
};
userExperience: {
replayability: number;
engagement: number;
complexity: 'simple' | 'moderate' | 'complex';
estimatedPlayTime: string;
};
}
declare class FlowAnalyzer {
private validator;
private pathTester?;
constructor();
analyze(flow: XFFlow): Promise<FlowAnalysis>;
private analyzeNodesInDetail;
private generateSampleContent;
private expandContent;
private analyzeStructure;
private analyzePaths;
private analyzeContent;
private analyzeUserExperience;
private addValidationInsights;
private calculateScore;
private calculateBranchingFactor;
private calculateClarityScore;
private getInsightPriority;
}
/**
* Simple test runner utility for XSYS flows
*/
declare function runPathTests(flow: XFFlow, options?: {
maxSteps?: number;
maxPaths?: number;
verbose?: boolean;
}): Promise<PathTestResult>;
/**
* Quick validation function for a single flow
*/
declare function validateFlow(flow: XFFlow): ValidationResult;
declare function inferNodeTypes(nodes: XFNode[]): Record<string, NodeType>;
export { type AnalysisInsight, type AnnotatedNode, type AutoAdvanceMode, CelEvaluator, type Choice, type EngineEvent, type EngineEventData, type EngineOptions, type ExecutionResult, type ExecutionStep, type FlowAnalysis, FlowAnalyzer, FlowEngine, FlowValidator, type IStateManager, type NodeType, PathTester, type StateAction, type StateCondition, StateManager, type StateManagerOptions, type StateRule, type ValidationError, type ValidationResult, type ValidationWarning, type XFFlow, type XFFlowMetadata, type XFNode, type XFOutlet, inferNodeTypes, runPathTests, validateFlow };