@exaflow/core
Version:
Core package for exaflow flow execution framework
1,639 lines (1,624 loc) • 86.2 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CelEvaluator: () => CelEvaluator,
FlowAnalyzer: () => FlowAnalyzer,
FlowEngine: () => FlowEngine,
FlowValidator: () => FlowValidator,
PathTester: () => PathTester,
StateManager: () => StateManager,
inferNodeTypes: () => inferNodeTypes,
runPathTests: () => runPathTests,
validateFlow: () => validateFlow
});
module.exports = __toCommonJS(index_exports);
// src/core/flow-engine.ts
var import_eventemitter32 = require("eventemitter3");
// src/core/state-manager.ts
var import_eventemitter3 = require("eventemitter3");
// src/core/cel-evaluator.ts
var import_cel_js = require("cel-js");
var CelEvaluator = class {
constructor() {
this.cache = /* @__PURE__ */ new Map();
}
compile(expression, options) {
const parsed = (0, import_cel_js.parse)(expression);
if (parsed.isSuccess && options?.cache) {
this.cache.set(expression, parsed.cst);
}
return {
success: parsed.isSuccess,
error: !parsed.isSuccess ? parsed.errors.join(", ") : void 0
};
}
evaluate(expression, context, { onError } = { onError: void 0 }) {
if (!expression || typeof expression !== "string") return void 0;
try {
let cst = this.cache.get(expression);
if (!cst) {
const parsed = (0, import_cel_js.parse)(expression);
if (parsed.isSuccess) {
cst = parsed.cst;
this.cache.set(expression, cst);
} else {
onError?.(new Error(parsed.errors.join(", ")));
return void 0;
}
}
const result = (0, import_cel_js.evaluate)(
cst,
context && typeof context === "object" ? { ...context } : {}
);
return result;
} catch (err) {
onError?.(err instanceof Error ? err : new Error(err));
return void 0;
}
}
clearCache() {
this.cache.clear();
}
};
// src/utils/evaluator-factory.ts
var EvaluatorFactory = class {
/**
* Get the default set of evaluators (singleton pattern for performance)
*/
static getDefaultEvaluators() {
if (!this.defaultEvaluators) {
this.defaultEvaluators = {
cel: new CelEvaluator()
};
}
return this.defaultEvaluators;
}
/**
* Create evaluators with optional custom ones
*/
static createEvaluators(customEvaluators) {
if (customEvaluators) {
return { ...this.getDefaultEvaluators(), ...customEvaluators };
}
return this.getDefaultEvaluators();
}
/**
* Get a specific evaluator by language
*/
static getEvaluator(language, evaluators) {
const allEvaluators = evaluators ?? this.getDefaultEvaluators();
const evaluator = allEvaluators[language];
if (!evaluator) {
throw new Error(`No evaluator found for language: ${language}`);
}
return evaluator;
}
};
EvaluatorFactory.defaultEvaluators = null;
// src/utils/error-handler.ts
var XSYSErrorHandler = class {
constructor(enableLogging = false) {
this.enableLogging = enableLogging;
}
/**
* Create a standardized XSYS error
*/
createError(code, message, context, cause) {
return {
code,
message,
context,
cause
};
}
/**
* Handle and log errors consistently
*/
handleError(error, context) {
let xsysError;
if (this.isXSYSError(error)) {
xsysError = error;
} else if (error instanceof Error) {
xsysError = this.createError(
"UNKNOWN_ERROR",
error.message,
context,
error
);
} else {
xsysError = this.createError("UNKNOWN_ERROR", String(error), context);
}
if (this.enableLogging) {
console.error("XSYS Error:", xsysError);
}
return xsysError;
}
/**
* Safe execution wrapper with error handling
*/
async safeExecute(operation, fallback, context) {
try {
const result = await operation();
return { success: true, result };
} catch (error) {
const xsysError = this.handleError(error, context);
return { success: false, result: fallback, error: xsysError };
}
}
/**
* Safe synchronous execution wrapper
*/
safeExecuteSync(operation, fallback, context) {
try {
const result = operation();
return { success: true, result };
} catch (error) {
const xsysError = this.handleError(error, context);
return { success: false, result: fallback, error: xsysError };
}
}
/**
* Validate if an object is an XSYS error
*/
isXSYSError(obj) {
return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj;
}
};
var ErrorCodes = {
// Flow execution errors
NODE_NOT_FOUND: "NODE_NOT_FOUND",
PATH_NOT_FOUND: "PATH_NOT_FOUND",
INVALID_TRANSITION: "INVALID_TRANSITION",
CONDITION_EVALUATION_FAILED: "CONDITION_EVALUATION_FAILED",
// State management errors
STATE_ACTION_FAILED: "STATE_ACTION_FAILED",
INVALID_STATE_VALUE: "INVALID_STATE_VALUE",
// Validation errors
VALIDATION_FAILED: "VALIDATION_FAILED",
INVALID_FLOW: "INVALID_FLOW",
// Expression evaluation errors
EXPRESSION_COMPILATION_FAILED: "EXPRESSION_COMPILATION_FAILED",
EXPRESSION_EVALUATION_FAILED: "EXPRESSION_EVALUATION_FAILED",
UNSUPPORTED_EXPRESSION_LANGUAGE: "UNSUPPORTED_EXPRESSION_LANGUAGE",
// General errors
UNKNOWN_ERROR: "UNKNOWN_ERROR",
OPERATION_TIMEOUT: "OPERATION_TIMEOUT"
};
var globalErrorHandler = new XSYSErrorHandler();
function createConditionEvaluationError(condition, cause) {
return globalErrorHandler.createError(
ErrorCodes.CONDITION_EVALUATION_FAILED,
`Failed to evaluate condition: ${condition}`,
{ condition },
cause
);
}
function createStateActionError(action, cause) {
return globalErrorHandler.createError(
ErrorCodes.STATE_ACTION_FAILED,
`Failed to execute state action: ${action}`,
{ action },
cause
);
}
// src/utils/state-executor.ts
var StateActionExecutor = class {
constructor(options = {}) {
this.expressionLanguage = options.expressionLanguage ?? "cel";
this.enableLogging = options.enableLogging ?? false;
this.evaluators = options.evaluators ?? EvaluatorFactory.getDefaultEvaluators();
}
/**
* Execute a single state action on the provided state
*/
executeAction(action, state) {
const newState = { ...state };
const errors = [];
try {
const { type, target, value, expression } = action;
switch (type) {
case "set":
if (expression) {
const evaluatedValue = this.evaluateExpression(
expression,
newState
);
this.setNestedValue(newState, target, evaluatedValue);
if (this.enableLogging) {
console.log(`Setting ${target} to ${evaluatedValue}.`);
}
} else {
this.setNestedValue(newState, target, value);
}
break;
default:
throw new Error(`Unknown action type: ${type}`);
}
return {
success: true,
newState,
errors
};
} catch (error) {
const xsysError = createStateActionError(
`${action.type}:${action.target}`,
error instanceof Error ? error : void 0
);
errors.push(xsysError.message);
return {
success: false,
newState: state,
// Return original state on error
errors
};
}
}
/**
* Execute multiple state actions in sequence
*/
executeActions(actions, initialState) {
let currentState = { ...initialState };
const allErrors = [];
for (const action of actions) {
const result = this.executeAction(action, currentState);
if (result.success) {
currentState = result.newState;
} else {
allErrors.push(...result.errors);
}
}
return {
success: allErrors.length === 0,
newState: currentState,
errors: allErrors
};
}
/**
* Evaluate a condition expression
*/
evaluateCondition(expression, state) {
try {
const expr = (expression || "").trim();
if (!expr) return true;
const evaluator = this.evaluators[this.expressionLanguage];
if (!evaluator) {
throw new Error(
`Unsupported expression language: ${this.expressionLanguage}`
);
}
const result = evaluator.evaluate(expr, state);
if (typeof result === "boolean") {
return result;
}
if (this.enableLogging) {
console.warn(
`Condition ${expr} evaluated to ${result}`,
"Expected boolean result"
);
}
return false;
} catch (error) {
const xsysError = createConditionEvaluationError(
expression,
error instanceof Error ? error : void 0
);
if (this.enableLogging) {
console.warn("XSYS Condition Evaluation Error:", xsysError);
}
return false;
}
}
/**
* Evaluate an expression and return the result
*/
evaluateExpression(expression, state) {
let evalError;
const evaluator = this.evaluators[this.expressionLanguage];
if (!evaluator) {
throw new Error(
`Unsupported expression language: ${this.expressionLanguage}`
);
}
const value = evaluator.evaluate(expression, state, {
onError: (err) => {
evalError = err;
}
});
if (evalError) {
throw evalError;
}
return value;
}
/**
* Set nested value in state using dot notation
*/
setNestedValue(state, path, value) {
if (!(typeof state === "object" && state !== null)) {
throw new Error("State must be an object");
}
const keys = path.split(".");
const lastKey = keys.pop();
if (!lastKey) {
throw new Error(`Invalid path: ${path}`);
}
const target = keys.reduce(
(obj, key) => {
if (!(key in obj)) obj[key] = {};
const prop = obj[key];
if (!prop) {
throw new Error(`Invalid path: ${path}`);
}
if (typeof prop !== "object") {
throw new Error(`Invalid path: ${path}`);
}
return prop;
},
state
);
target[lastKey] = value;
}
};
// src/utils/schema-validator.ts
var import_ajv = __toESM(require("ajv"));
var import_ajv_formats = __toESM(require("ajv-formats"));
var SchemaValidator = class {
constructor() {
this.compiledSchemas = /* @__PURE__ */ new Map();
this.ajv = new import_ajv.default({
allErrors: true,
// Collect all validation errors
removeAdditional: false,
// Don't remove additional properties
useDefaults: true,
// Apply default values from schema
coerceTypes: false
// Don't coerce types automatically
});
(0, import_ajv_formats.default)(this.ajv);
}
/**
* Compile and cache a JSON schema for validation
*/
compileSchema(schema, schemaId) {
const id = schemaId || JSON.stringify(schema);
let validator = this.compiledSchemas.get(id);
if (!validator) {
try {
validator = this.ajv.compile(schema);
this.compiledSchemas.set(id, validator);
} catch (error) {
throw new Error(
`Failed to compile schema: ${error instanceof Error ? error.message : String(error)}`
);
}
}
return validator;
}
/**
* Validate data against a JSON schema
*/
validate(data, schema, schemaId) {
try {
const validator = this.compileSchema(schema, schemaId);
const isValid = validator(data);
if (isValid) {
return {
isValid: true,
errors: [],
data
};
}
const errors = this.formatValidationErrors(validator.errors || []);
return {
isValid: false,
errors,
data
};
} catch (error) {
return {
isValid: false,
errors: [
`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`
],
data
};
}
}
/**
* Validate data and throw an error if validation fails
*/
validateOrThrow(data, schema, schemaId) {
const result = this.validate(data, schema, schemaId);
if (!result.isValid) {
const error = new Error(
`Schema validation failed: ${result.errors.join(", ")}`
);
error.validationErrors = result.errors;
error.data = data;
throw error;
}
}
/**
* Format Ajv validation errors into human-readable messages
*/
formatValidationErrors(errors) {
return errors.map((error) => {
const path = error.instancePath || "root";
const message = error.message || "validation failed";
switch (error.keyword) {
case "required":
return `Missing required property: ${error.params?.missingProperty} at ${path}`;
case "type":
return `Invalid type at ${path}: expected ${error.params?.type}, got ${typeof error.data}`;
case "minimum":
return `Value at ${path} (${error.data}) is below minimum ${error.params?.limit}`;
case "maximum":
return `Value at ${path} (${error.data}) is above maximum ${error.params?.limit}`;
case "minLength":
return `String at ${path} is too short (minimum length: ${error.params?.limit})`;
case "maxLength":
return `String at ${path} is too long (maximum length: ${error.params?.limit})`;
case "pattern":
return `String at ${path} does not match required pattern`;
case "format":
return `Invalid format at ${path}: expected ${error.params?.format}`;
case "additionalProperties":
return `Unexpected property '${error.params?.additionalProperty}' at ${path}`;
default:
return `Validation error at ${path}: ${message}`;
}
});
}
/**
* Clear compiled schema cache
*/
clearCache() {
this.compiledSchemas.clear();
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
size: this.compiledSchemas.size,
schemas: Array.from(this.compiledSchemas.keys())
};
}
};
var globalSchemaValidator = new SchemaValidator();
// src/core/state-manager.ts
var StateManager = class extends import_eventemitter3.EventEmitter {
constructor(initialState = {}, rules = [], options = {}) {
super();
this.rules = rules;
this.expressionLanguage = options.expressionLanguage ?? "cel";
this.evaluators = EvaluatorFactory.getDefaultEvaluators();
this.stateSchema = options.stateSchema;
this.schemaValidator = globalSchemaValidator;
this.validateOnChange = options.validateOnChange ?? true;
this.validateState(initialState, "Initial state validation failed");
this.state = { ...initialState };
this.actionExecutor = new StateActionExecutor({
expressionLanguage: this.expressionLanguage,
evaluators: this.evaluators,
enableLogging: true
});
}
getState() {
return JSON.parse(JSON.stringify(this.state));
}
setState(newState) {
const oldState = JSON.parse(JSON.stringify(this.state));
const updatedState = { ...this.state, ...newState };
this.validateState(updatedState, "State update validation failed");
this.state = updatedState;
this.emit("stateChange", { oldState, newState: this.getState() });
this.evaluateRules();
}
executeActions(actions) {
const oldState = JSON.parse(JSON.stringify(this.state));
const result = this.actionExecutor.executeActions(actions, this.state);
const newState = result.newState;
this.validateState(newState, "Action execution validation failed");
this.state = newState;
this.emit("stateChange", { oldState, newState: this.getState() });
this.evaluateRules();
}
evaluateCondition(expression) {
return this.actionExecutor.evaluateCondition(expression, this.state);
}
evaluateRules() {
for (const rule of this.rules) {
if (this.evaluateCondition(rule.condition)) {
this.executeRule(rule);
}
}
}
executeRule(rule) {
switch (rule.action) {
case "forceTransition":
this.emit("error", {
error: new Error(`Force transition to ${rule.target}`),
context: { rule, type: "forceTransition" }
});
break;
case "setState":
if (rule.target && rule.value !== void 0) {
const result = this.actionExecutor.executeAction(
{ type: "set", target: rule.target, value: rule.value },
this.state
);
if (result.success) {
this.state = result.newState;
}
}
break;
case "triggerEvent":
break;
}
}
reset(newState = {}) {
const oldState = JSON.parse(JSON.stringify(this.state));
const resetState = JSON.parse(JSON.stringify(newState));
this.validateState(resetState, "State reset validation failed");
this.state = resetState;
this.emit("stateChange", { oldState, newState: this.getState() });
this.evaluateRules();
}
/**
* Validate state against the JSON schema if one is provided
*/
validateState(state, errorPrefix) {
if (!this.stateSchema || !this.validateOnChange) {
return;
}
try {
const result = this.schemaValidator.validate(state, this.stateSchema);
if (!result.isValid) {
const error = new Error(`${errorPrefix}: ${result.errors.join(", ")}`);
this.emit("error", {
error,
context: { type: "schemaValidation", errors: result.errors }
});
throw error;
}
} catch (error) {
if (error instanceof Error) {
throw error;
}
const validationError = new Error(
`${errorPrefix}: Unexpected validation error`
);
this.emit("error", {
error: validationError,
context: { type: "schemaValidation", originalError: error }
});
throw validationError;
}
}
};
// src/utils/content-interpolator.ts
var ContentInterpolator = class {
constructor(options = {}) {
this.expressionLanguage = options.expressionLanguage ?? "cel";
this.enableLogging = options.enableLogging ?? false;
this.evaluators = EvaluatorFactory.getDefaultEvaluators();
}
/**
* Interpolate expressions in content string
*/
interpolate(content, state) {
if (!content || typeof content !== "string") {
return {
content: content || "",
hasInterpolations: false,
errors: []
};
}
const errors = [];
let hasInterpolations = false;
const escapedPlaceholder = "___XSYS_ESCAPED_INTERPOLATION___";
let escapedInterpolations = [];
let processedContent = content.replace(
/\\\$\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g,
(_match, expression) => {
escapedInterpolations.push(`\${${expression}}`);
return `${escapedPlaceholder}${escapedInterpolations.length - 1}`;
}
);
const interpolationRegex = /\$\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
const interpolatedContent = processedContent.replace(
interpolationRegex,
(_match, expression) => {
hasInterpolations = true;
try {
const result = this.evaluateExpression(expression.trim(), state);
if (result === void 0 || result === null) {
const errorMsg = `Variable "${expression}" is undefined`;
errors.push(errorMsg);
if (this.enableLogging) {
console.warn("XSYS Content Interpolation Warning:", errorMsg);
}
return "";
}
return this.formatValue(result);
} catch (error) {
const errorMsg = `Failed to interpolate "${expression}": ${error instanceof Error ? error.message : String(error)}`;
errors.push(errorMsg);
if (this.enableLogging) {
console.warn("XSYS Content Interpolation Error:", errorMsg);
}
return "";
}
}
);
const finalContent = interpolatedContent.replace(
new RegExp(`${escapedPlaceholder}(\\d+)`, "g"),
(match, ...args) => {
return escapedInterpolations[parseInt(args[0])] || match;
}
);
return {
content: finalContent,
hasInterpolations,
errors
};
}
/**
* Check if content contains interpolation expressions
*/
hasInterpolations(content) {
if (!content || typeof content !== "string") return false;
return /\$\{[^}]+\}/.test(content);
}
/**
* Extract all interpolation expressions from content
*/
extractExpressions(content) {
if (!content || typeof content !== "string") return [];
const expressions = [];
const interpolationRegex = /\$\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
let match;
while ((match = interpolationRegex.exec(content)) !== null) {
if (!match || !match[1]) {
continue;
}
expressions.push(match[1].trim());
}
return expressions;
}
/**
* Validate all expressions in content
*/
validateExpressions(content) {
const expressions = this.extractExpressions(content);
const errors = [];
for (const expression of expressions) {
try {
const evaluator = this.evaluators[this.expressionLanguage];
if (!evaluator) {
errors.push(
`Unsupported expression language: ${this.expressionLanguage}`
);
continue;
}
if (evaluator.compile) {
const result = evaluator.compile(expression, { cache: false });
if (!result.success && result.error) {
errors.push(`Invalid expression "${expression}": ${result.error}`);
}
}
} catch (error) {
errors.push(
`Expression validation failed for "${expression}": ${error instanceof Error ? error.message : String(error)}`
);
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Evaluate a single expression
*/
evaluateExpression(expression, state) {
const evaluator = this.evaluators[this.expressionLanguage];
if (!evaluator) {
throw new Error(
`Unsupported expression language: ${this.expressionLanguage}`
);
}
let evaluationError;
const result = evaluator.evaluate(expression, state, {
onError: (err) => {
evaluationError = err;
}
});
if (evaluationError) {
throw evaluationError;
}
return result;
}
/**
* Format a value for display in content
*/
formatValue(value) {
if (value === null || value === void 0) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number") {
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (Array.isArray(value)) {
return value.map((v) => this.formatValue(v)).join(", ");
}
if (typeof value === "object") {
try {
return JSON.stringify(value);
} catch {
return "[Object]";
}
}
return String(value);
}
};
// src/utils/infer-node-types.ts
function inferNodeTypes(nodes) {
const incoming = /* @__PURE__ */ new Map();
const outgoing = /* @__PURE__ */ new Map();
for (const n of nodes) {
outgoing.set(n.id, n.outlets?.length ?? 0);
for (const p of n.outlets ?? []) {
incoming.set(p.to, (incoming.get(p.to) ?? 0) + 1);
}
}
const typeMap = {};
for (const n of nodes) {
const out = outgoing.get(n.id) ?? 0;
const inc = incoming.get(n.id) ?? 0;
if (out === 0 && inc === 0) {
typeMap[n.id] = "isolated";
} else if (!inc && out > 0) {
typeMap[n.id] = "start";
} else if (out === 0 && inc > 0) {
typeMap[n.id] = "end";
} else if (out > 1) {
typeMap[n.id] = "decision";
} else {
typeMap[n.id] = "action";
}
}
return typeMap;
}
// src/core/flow-engine.ts
var FlowEngine = class extends import_eventemitter32.EventEmitter {
constructor(flow, options = {}) {
super();
this.currentNode = null;
this.history = [];
this.flow = flow;
this.nodeTypes = inferNodeTypes(flow.nodes);
this.options = {
enableHistory: true,
maxHistorySize: 100,
autoSave: false,
...options
};
const initialState = {
...JSON.parse(JSON.stringify(flow.globalState || {})),
...JSON.parse(JSON.stringify(options.initialState || {}))
};
if (options.stateManager) {
this.stateManager = options.stateManager;
} else {
this.stateManager = new StateManager(
initialState,
flow.stateRules || [],
{
expressionLanguage: flow.expressionLanguage ?? "cel",
stateSchema: flow.stateSchema,
validateOnChange: true
}
);
}
this.contentInterpolator = new ContentInterpolator({
expressionLanguage: flow.expressionLanguage ?? "cel",
enableLogging: options.enableLogging ?? false
});
this.stateManager.on(
"stateChange",
(data) => this.emit("stateChange", data)
);
this.stateManager.on(
"error",
(data) => {
if (data.context?.type === "forceTransition" && data.context.rule?.target) {
this.handleForcedTransition(data.context.rule.target);
} else {
this.emit("error", data);
}
}
);
}
async start() {
const startNode = this.findNodeById(this.flow.startNodeId);
if (!startNode) {
throw new Error(
`Start node with id "${this.flow.startNodeId}" not found`
);
}
return this.transitionToNode(startNode);
}
async next(choiceId) {
if (!this.currentNode) {
throw new Error("No current node. Call start() first.");
}
let targetNode = null;
const currentNodeType = this.nodeTypes[this.currentNode.id];
if (choiceId) {
const outletResult = this.findOutletById(choiceId);
if (!outletResult || outletResult.fromNodeId !== this.currentNode.id) {
throw new Error(`Invalid choice: ${choiceId}`);
}
targetNode = this.findNodeById(outletResult.outlet.to);
} else {
if (currentNodeType === "decision") {
targetNode = this.evaluateAutomaticDecision(this.currentNode);
} else {
if (currentNodeType === "action") {
const outlets = this.getOutlets(this.currentNode.id);
if (outlets.length === 1 && outlets[0]) {
targetNode = this.findNodeById(outlets[0].to);
}
}
}
}
if (!targetNode && currentNodeType !== "decision") {
throw new Error("No valid transition found");
}
if (currentNodeType === "decision" && !choiceId) {
return this.createExecutionResult();
}
if (!targetNode) {
throw new Error("No valid transition found");
}
return this.transitionToNode(targetNode, choiceId);
}
getCurrentNode() {
if (!this.currentNode) {
return null;
}
const nodeType = this.nodeTypes[this.currentNode.id];
if (!nodeType) {
throw new Error(`Node type not found for node: ${this.currentNode.id}`);
}
return {
node: this.getInterpolatedNode(this.currentNode),
type: nodeType
};
}
getHistory() {
return [...this.history];
}
getAvailableChoices() {
if (!this.currentNode) {
return [];
}
const currentNodeType = this.nodeTypes[this.currentNode.id];
if (currentNodeType === "end") {
return [];
}
const allOutlets = this.getOutlets(this.currentNode.id);
const showDisabled = this.options.showDisabledChoices || false;
const currentState = this.stateManager.getState();
let outletsToShow;
let choices;
const isSingleChoice = allOutlets.length === 1;
const singleChoiceDefaultLabel = "Continue";
if (showDisabled) {
outletsToShow = allOutlets;
choices = outletsToShow.map((outlet, index) => {
const isEnabled = this.evaluateOutletCondition(outlet);
const description = outlet.metadata && "description" in outlet.metadata && typeof outlet.metadata.description === "string" ? outlet.metadata.description : void 0;
let label = this.contentInterpolator.interpolate(
outlet.label ? outlet.label : isSingleChoice ? singleChoiceDefaultLabel : "Choice " + (index + 1),
// Ensure outlets have labels
currentState
).content;
return {
id: outlet.id,
label,
description,
outletId: outlet.id,
disabled: !isEnabled,
disabledReason: !isEnabled && outlet.condition ? "Condition not met" : void 0
};
});
} else {
outletsToShow = allOutlets.filter(
(outlet) => this.evaluateOutletCondition(outlet)
);
choices = outletsToShow.map((outlet, index) => {
let label = this.contentInterpolator.interpolate(
outlet.label ? outlet.label : isSingleChoice ? singleChoiceDefaultLabel : "Choice " + (index + 1),
// Ensure outlets have labels
currentState
).content;
return {
id: outlet.id,
label,
description: outlet.metadata && "description" in outlet.metadata && typeof outlet.metadata.description === "string" ? outlet.metadata.description : void 0,
outletId: outlet.id
};
});
}
const enabledChoices = showDisabled ? choices.filter((choice) => !choice.disabled) : choices;
if (enabledChoices.length === 1 && enabledChoices[0] && !enabledChoices[0].label?.trim()) {
const continueChoice = enabledChoices[0];
if (!continueChoice.id || !continueChoice.outletId) {
throw new Error("Invalid choice structure: missing required fields");
}
const targetOutlet = outletsToShow.find(
(p) => p.id === continueChoice.id
);
const targetNode = targetOutlet ? this.findNodeById(targetOutlet.to) : null;
return [
{
...continueChoice,
id: continueChoice.id,
outletId: continueChoice.outletId,
label: singleChoiceDefaultLabel,
description: `Continue to ${targetNode?.title || "next step"}`
},
...choices.filter((choice) => choice.disabled)
// Add any disabled choices if showing them
];
}
return choices;
}
isComplete() {
if (!this.currentNode) {
return false;
}
const currentNodeType = this.nodeTypes[this.currentNode.id];
return currentNodeType === "end";
}
canGoBack() {
return Boolean(this.options.enableHistory) && this.history.length > 1;
}
goBack() {
if (!this.canGoBack()) {
throw new Error("Cannot go back");
}
this.history.pop();
const previousStep = this.history[this.history.length - 1];
if (!previousStep) {
throw new Error("No previous step available");
}
this.stateManager.reset(previousStep.state);
this.currentNode = previousStep.node.node;
return Promise.resolve(this.createExecutionResult());
}
reset() {
this.currentNode = null;
this.history = [];
this.stateManager.reset(
JSON.parse(JSON.stringify(this.flow.globalState || {}))
);
}
getState() {
return this.stateManager.getState();
}
getStateManager() {
return this.stateManager;
}
async transitionToNode(node, choiceId) {
if (this.currentNode) {
this.emit("nodeExit", {
node: this.currentNode,
choice: choiceId,
state: this.stateManager.getState()
});
}
if (choiceId && this.currentNode) {
const outletResult = this.findOutletById(choiceId);
if (outletResult?.outlet.actions) {
this.stateManager.executeActions(outletResult.outlet.actions);
}
}
this.currentNode = node;
if (node.actions) {
this.stateManager.executeActions(node.actions);
}
if (this.options.enableHistory) {
const nodeType = this.nodeTypes[node.id];
if (!nodeType) {
throw new Error(`Node type not found for node: ${node.id}`);
}
const step = {
node: { node, type: nodeType },
choice: choiceId,
timestamp: /* @__PURE__ */ new Date(),
state: this.stateManager.getState()
};
this.history.push(step);
const maxSize = this.options.maxHistorySize || 100;
if (this.history.length > maxSize) {
this.history.shift();
}
}
this.emit("nodeEnter", {
node,
state: this.stateManager.getState()
});
const availableOutlets = this.getOutlets(node.id);
if (this.shouldAutoAdvance(node, availableOutlets)) {
const selectedOutlet = this.selectAutoAdvanceOutlet(
node,
availableOutlets
);
if (!selectedOutlet) {
this.emit("error", {
error: new Error(
`No valid outlet found for auto-advance from node "${node.id}"`
),
context: {
node,
availableOutlets
}
});
return this.createExecutionResult();
}
const nodeTo = this.flow.nodes.find((n) => n.id === selectedOutlet.to);
if (!nodeTo) {
this.emit("error", {
error: new Error(`Node with id "${selectedOutlet.to}" not found`),
context: {
node,
outlet: selectedOutlet
}
});
return this.createExecutionResult();
}
this.emit("autoAdvance", {
from: node,
to: nodeTo,
condition: selectedOutlet.condition
});
return this.transitionToNode(nodeTo, selectedOutlet.id);
}
return this.createExecutionResult();
}
evaluateAutomaticDecision(node) {
const availableOutlets = this.getOutlets(node.id);
const sortedOutlets = [...availableOutlets];
for (const outlet of sortedOutlets) {
if (this.evaluateOutletCondition(outlet)) {
return this.findNodeById(outlet.to);
}
}
return null;
}
evaluateOutletCondition(outlet) {
if (!outlet.condition) return true;
try {
return this.stateManager.evaluateCondition(outlet.condition);
} catch (error) {
console.warn(
`Failed to evaluate outlet condition: ${outlet.condition}`,
error
);
return false;
}
}
/**
* 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).
*/
selectAutoAdvanceOutlet(_node, availableOutlets) {
const conditionalOutlets = availableOutlets.filter(
(outlet) => outlet.condition
);
const defaultOutlets = availableOutlets.filter(
(outlet) => !outlet.condition
);
for (const outlet of conditionalOutlets) {
if (this.evaluateOutletCondition(outlet)) {
return outlet;
}
}
if (defaultOutlets.length > 0 && defaultOutlets[0]) {
return defaultOutlets[0];
}
return null;
}
/**
* Get a node with interpolated content and title
*/
getInterpolatedNode(node) {
const currentState = this.stateManager.getState();
let interpolatedNode = { ...node };
let hasAnyInterpolations = false;
const allErrors = [];
if (node.title && this.contentInterpolator.hasInterpolations(node.title)) {
const titleResult = this.contentInterpolator.interpolate(
node.title,
currentState
);
interpolatedNode.title = titleResult.content;
hasAnyInterpolations = true;
allErrors.push(...titleResult.errors);
}
if (node.content && this.contentInterpolator.hasInterpolations(node.content)) {
const contentResult = this.contentInterpolator.interpolate(
node.content,
currentState
);
interpolatedNode.content = contentResult.content;
hasAnyInterpolations = true;
allErrors.push(...contentResult.errors);
}
if (allErrors.length > 0 && this.options.enableLogging) {
console.warn(`Interpolation errors for node "${node.id}":`, allErrors);
}
if (!hasAnyInterpolations) {
return node;
}
return interpolatedNode;
}
createExecutionResult() {
if (!this.currentNode) {
throw new Error("No current node. Call start() first.");
}
const nodeType = this.nodeTypes[this.currentNode.id];
if (!nodeType) {
throw new Error(`Node type not found for node: ${this.currentNode.id}`);
}
return {
node: {
node: this.getInterpolatedNode(this.currentNode),
type: nodeType
},
choices: this.getAvailableChoices(),
isComplete: this.isComplete(),
canGoBack: this.canGoBack(),
state: this.stateManager.getState()
};
}
handleForcedTransition(targetNodeId) {
const targetNode = this.findNodeById(targetNodeId);
if (!targetNode) {
this.emit("error", {
error: new Error(
`Target node "${targetNodeId}" not found for forced transition`
),
context: { type: "forcedTransition", targetNodeId }
});
return;
}
this.transitionToNode(targetNode).catch((error) => {
this.emit("error", {
error,
context: { type: "forcedTransition", targetNodeId }
});
});
}
shouldAutoAdvance(node, availableOutlets) {
if (this.flow.autoAdvance === "never" || this.options.autoAdvance === "never") {
return false;
}
if (node.isAutoAdvance) {
return this.selectAutoAdvanceOutlet(node, availableOutlets) !== null;
} else if (this.flow.autoAdvance === "always" || this.options.autoAdvance === "always") {
return this.selectAutoAdvanceOutlet(node, availableOutlets) !== null;
} else {
return false;
}
}
findNodeById(id) {
return this.flow.nodes.find((node) => node.id === id) || null;
}
getOutlets(nodeId) {
const node = this.findNodeById(nodeId);
return node?.outlets || [];
}
findOutletById(outletId) {
for (const node of this.flow.nodes) {
if (node.outlets) {
const outlet = node.outlets.find((p) => p.id === outletId);
if (outlet) {
return { outlet, fromNodeId: node.id };
}
}
}
return null;
}
};
// src/utils/performance-cache.ts
var LRUCache = class {
constructor(options = {}) {
this.cache = /* @__PURE__ */ new Map();
this.maxSize = options.maxSize ?? 100;
this.ttl = options.ttl ?? 3e5;
}
get(key) {
const entry = this.cache.get(key);
if (!entry) return void 0;
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return void 0;
}
this.cache.delete(key);
this.cache.set(key, entry);
return entry.value;
}
set(key, value) {
this.cache.delete(key);
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== void 0) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, { value, timestamp: Date.now() });
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
};
// src/utils/graph-utils.ts
var FlowGraphUtils = class {
constructor(flow) {
this.flow = flow;
this.nodeMap = /* @__PURE__ */ new Map();
this.pathMap = /* @__PURE__ */ new Map();
this.reachabilityCache = new LRUCache({ maxSize: 50, ttl: 3e5 });
this.depthCache = new LRUCache({ maxSize: 50, ttl: 3e5 });
for (const node of flow.nodes) {
this.nodeMap.set(node.id, node);
}
for (const node of flow.nodes) {
if (node.outlets) {
for (const path of node.outlets) {
this.pathMap.set(path.id, { path, fromNodeId: node.id });
}
}
}
}
/**
* Find a node by ID
*/
findNode(nodeId) {
return this.nodeMap.get(nodeId) || null;
}
/**
* Find a path by ID with its source node
*/
findPath(pathId) {
return this.pathMap.get(pathId) || null;
}
/**
* Get all outgoing paths from a node
*/
getOutgoingPaths(nodeId) {
const node = this.nodeMap.get(nodeId);
return node?.outlets || [];
}
/**
* Get all incoming paths to a node
*/
getIncomingPaths(nodeId) {
const incoming = [];
for (const node of this.flow.nodes) {
if (node.outlets) {
for (const path of node.outlets) {
if (path.to === nodeId) {
incoming.push({ path, fromNodeId: node.id });
}
}
}
}
return incoming;
}
/**
* Find all nodes reachable from a given start node (cached)
*/
findReachableNodes(startNodeId) {
const start = startNodeId || this.flow.startNodeId;
const cacheKey = `reachable:${start}`;
const cached = this.reachabilityCache.get(cacheKey);
if (cached) {
return new Set(cached);
}
const reachable = /* @__PURE__ */ new Set();
const queue = [start];
while (queue.length > 0) {
const nodeId = queue.shift();
if (!nodeId || reachable.has(nodeId)) continue;
reachable.add(nodeId);
const outgoingPaths = this.getOutgoingPaths(nodeId);
for (const path of outgoingPaths) {
if (!reachable.has(path.to)) {
queue.push(path.to);
}
}
}
this.reachabilityCache.set(cacheKey, reachable);
return reachable;
}
/**
* Find unreachable nodes
*/
findUnreachableNodes(startNodeId) {
const reachable = this.findReachableNodes(startNodeId);
const unreachable = /* @__PURE__ */ new Set();
for (const node of this.flow.nodes) {
if (!reachable.has(node.id)) {
unreachable.add(node.id);
}
}
return unreachable;
}
/**
* Check if a specific node is reachable from start
*/
isNodeReachable(targetNodeId, startNodeId) {
const start = startNodeId || this.flow.startNodeId;
const visited = /* @__PURE__ */ new Set();
const queue = [start];
while (queue.length > 0) {
const nodeId = queue.shift();
if (!nodeId || visited.has(nodeId)) continue;
visited.add(nodeId);
if (nodeId === targetNodeId) return true;
const outgoingPaths = this.getOutgoingPaths(nodeId);
for (const path of outgoingPaths) {
if (!visited.has(path.to)) {
queue.push(path.to);
}
}
}
return false;
}
/**
* Calculate the maximum depth from start node
*/
calculateMaxDepth(startNodeId) {
const start = startNodeId || this.flow.startNodeId;
const visited = /* @__PURE__ */ new Set();
const dfs = (nodeId, depth) => {
if (visited.has(nodeId)) return depth;
visited.add(nodeId);
const node = this.findNode(nodeId);
if (!node) return depth;
let maxDepth = depth;
const outgoingPaths = this.getOutgoingPaths(nodeId);
for (const path of outgoingPaths) {
maxDepth = Math.max(maxDepth, dfs(path.to, depth + 1));
}
return maxDepth;
};
return dfs(start, 1);
}
/**
* Calculate node depths from start (cached)
*/
calculateNodeDepths(startNodeId) {
const start = startNodeId || this.flow.startNodeId;
const cacheKey = `depths:${start}`;
const cached = this.depthCache.get(cacheKey);
if (cached) {
return new Map(cached);
}
const depths = /* @__PURE__ */ new Map();
const queue = [{ nodeId: start, depth: 0 }];
while (queue.length > 0) {
const current = queue.shift();
if (!current) break;
const { nodeId, depth } = current;
if (depths.has(nodeId)) continue;
depths.set(nodeId, depth);
const outgoingPaths = this.getOutgoingPaths(nodeId);
for (const path of outgoingPaths) {
if (!depths.has(path.to)) {
queue.push({ nodeId: path.to, depth: depth + 1 });
}
}
}
this.depthCache.set(cacheKey, depths);
return depths;
}
/**
* Detect cycles in the flow graph
*/
hasCycles(startNodeId) {
const start = startNodeId || this.flow.startNodeId;
const visiting = /* @__PURE__ */ new Set();
const visited = /* @__PURE__ */ new Set();
const dfs = (nodeId) => {
if (visiting.has(nodeId)) return true;
if (visited.has(nodeId)) return false;
visiting.add(nodeId);
const outgoingPaths = this.getOutgoingPaths(nodeId);
for (const path of outgoingPaths) {
if (dfs(path.to)) return true;
}
visiting.delete(nodeId);
visited.add(nodeId);
return false;
};
return dfs(start);
}
/**
* Comprehensive graph traversal with detailed results
*/
performTraversal(startNodeId) {
const start = startNodeId || this.flow.startNodeId;
const reachableNodes = this.findReachableNodes(start);
const nodeDepths = this.calculateNodeDepths(start);
const visitedPaths = /* @__PURE__ */ new Set();
const pathsFromNode = /* @__PURE__ */ new Map();
for (const nodeId of reachableNodes) {
const outgoingPaths = this.getOutgoingPaths(nodeId);
pathsFromNode.set(nodeId, outgoingPaths);
for (const path of outgoingPaths) {
if (reachableNodes.has(path.to)) {
visitedPaths.add(path.id);
}
}
}
return {
reachableNodes,
visitedPaths,
nodeDepths,
pathsFromNode
};
}
/**
* Explore all possible paths through the flow with optional state tracking
*/
exploreAllPaths(options = {}) {
const {
maxDepth = 100,
maxPaths = 1e3,
includeState = false,
conditionEvaluator
} = options;
const allPaths = [];
const queue = [
{
nodeId: this.flow.startNodeId,
path: [this.flow.startNodeId],
pathIds: [],
depth: 1,
state: includeState ? { ...this.flow.globalState } : void 0
}
];
const reachableNodes = /* @__PURE__ */ new Set();
let currentMaxDepth = 0;
let hasCycles = false;
while (queue.length > 0 && allPaths.length < maxPaths) {
const current = queue.shift();
if (!current) break;
reachableNodes.add(current.nodeId);
currentMaxDepth = Math.max(currentMaxDepth, current.depth);
const nodeCount = current.path.filter(
(id) => id === current.nodeId
).length;
if (nodeCount > 1) {
hasCycles = true;
}
const node = this.findNode(current.nodeId);
if (!node) continue;
const outgoingPaths = this.getOutgoingPaths(current.nodeId);
if (outgoingPaths.length === 0 || current.depth >= maxDepth) {
allPaths.push({
nodeIds: current.path,
pathIds: current.pathIds,
depth: current.depth,
state: current.state
});
continue;
}
for (const path of outgoingPaths) {
if (conditionEvaluator && path.condition && current.state) {
try {
if (!conditionEvaluator(path.condition, current.state)) {
continue;
}
} catch {
continue;
}
}
const newPath = [...current.path, path.to];
const newPathIds = [...current.pathIds, path.id];
queue.push({
nodeId: path.to,
path: newPath,
pathIds: newPathIds,
depth: current.depth + 1,
state: current.state ? { ...current.state } : void 0
});
}
}
const allNodeIds = new Set(this.flow.nodes.map((n) => n.id));
const unreachableNodes = /* @__PURE__ */ new Set();
for (const nodeId of allNodeIds) {
if (!reachableNodes.has(nodeId)) {
unreachableNodes.add(nodeId);
}
}
return {
allPaths,
reachableNodes,
unreachableNodes,
maxDepth: currentMaxDepth,
hasCycles
};
}
/**
* Get flow statistics
*/
getFlowStats() {
const traversal = this.performTraversal();
const pathExploration = this.exploreAllPaths({ maxPaths: 100 });
return {
totalNodes: this.flow.nodes.length,
reachableNodes: traversal.reachableNodes.size,
unreachableNodes: this.flow.nodes.length - traversal.reachableNodes.size,
totalPaths: traversal.visitedPaths.size,
maxDepth: this.calculateMaxDepth(),
hasCycles: this.hasCycles(),
possiblePaths: pathExploration.allPaths.length
};
}
};
// src/analysis/flow-validator.ts
var FlowValidator = class {
constructor(evaluators) {
this.evaluators = evaluators ?? EvaluatorFactory.getDefaultEvaluators();
}
validate(flow) {
const errors = [];
const warnings = [];
const nodeTypes = inferNodeTypes(flow.nodes);
const graphUtils = new Flo