UNPKG

dfa-mcp-server

Version:

DFA-based workflow MCP server for guiding LLM task completion

576 lines 25.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkflowEngine = void 0; const judge_engine_js_1 = require("./judge-engine.js"); const error_formatter_js_1 = require("./error-formatter.js"); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); class WorkflowEngine { definitions = new Map(); instances = new Map(); checkpoints = new Map(); workflowDir; checkpointDir; judgeEngine; constructor(workflowDir = '.workflows') { this.workflowDir = workflowDir; this.checkpointDir = path.join(workflowDir, 'checkpoints'); this.judgeEngine = new judge_engine_js_1.JudgeEngine(workflowDir); } async initialize() { // Ensure workflow directory exists await fs.mkdir(this.workflowDir, { recursive: true }); await fs.mkdir(this.checkpointDir, { recursive: true }); // Initialize judge engine await this.judgeEngine.initialize(); // Load saved workflow definitions await this.loadDefinitions(); // Load existing workflow instances await this.loadInstances(); // Load existing checkpoints await this.loadCheckpoints(); } async registerWorkflow(definition) { // Validate workflow definition if (!definition.name || !definition.states || !definition.initialState) { throw new Error('Invalid workflow definition: missing required fields'); } // Validate initial state exists if (!definition.states[definition.initialState]) { throw new Error(`Invalid initial state: ${definition.initialState}`); } // Validate all transitions point to valid states for (const [stateName, state] of Object.entries(definition.states)) { if (state.transitions) { for (const [action, rules] of Object.entries(state.transitions)) { for (const rule of rules) { if (!definition.states[rule.target]) { throw new Error(`Invalid transition: ${stateName} -> ${action} -> ${rule.target} (state not found)`); } } } } } // Register the workflow this.definitions.set(definition.name, definition); // Persist the definition await this.saveDefinition(definition); } listWorkflows() { return Array.from(this.definitions.values()).map(def => ({ name: def.name, description: def.description })); } async createWorkflow(type, initialContext = {}) { const definition = this.definitions.get(type); if (!definition) { throw new Error(`Unknown workflow type: ${type}`); } // Use crypto.randomUUID if available, otherwise fallback to timestamp-based ID const id = crypto.randomUUID ? `wf-${crypto.randomUUID()}` : `wf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const instance = { id, definitionName: type, currentState: definition.initialState, context: initialContext, createdAt: new Date(), updatedAt: new Date() }; this.instances.set(id, instance); await this.saveInstance(instance); await this.logTransition(id, '', 'start', definition.initialState); return instance; } async transition(workflowId, action, data, expectedTargetState) { const instance = this.instances.get(workflowId); if (!instance) { throw new Error(`Workflow not found: ${workflowId}`); } const definition = this.definitions.get(instance.definitionName); if (!definition) { throw new Error(`Definition not found: ${instance.definitionName}`); } const currentStatedef = definition.states[instance.currentState]; if (!currentStatedef) { throw new Error(`Invalid state: ${instance.currentState}`); } if (currentStatedef.final) { throw new Error(`Cannot transition from final state: ${instance.currentState}`); } const transitionRules = currentStatedef.transitions?.[action]; if (!transitionRules || transitionRules.length === 0) { const validActions = Object.keys(currentStatedef.transitions || {}); const errorMessage = error_formatter_js_1.ErrorFormatter.formatInvalidActionError(instance.currentState, action, validActions, definition.name); throw new Error(errorMessage); } // Evaluate conditions to find the target state const conditionResult = await this.judgeEngine.evaluateTransitionConditions(transitionRules, instance.context, { workflowId, fromState: instance.currentState, action, toState: '', // Will be determined by condition evaluation data, context: instance.context, definition }); if (!conditionResult.matchedRule) { throw new Error(`No conditions matched for action '${action}' from state '${instance.currentState}'\n${conditionResult.overallReasoning}`); } const nextState = conditionResult.matchedRule.target; // Check if user provided an expected target state and it differs from the evaluated one let targetMismatchWarning; if (expectedTargetState && expectedTargetState !== nextState) { targetMismatchWarning = `Based on condition "${conditionResult.matchedRule.condition}", workflow engine has determined '${nextState}' as target state and changed the state to '${nextState}' instead of '${expectedTargetState}'.`; console.warn(`[Workflow ${workflowId}] Target state mismatch: ${targetMismatchWarning}`); } // Create transition attempt for judge validation const attempt = { workflowId, fromState: instance.currentState, action, toState: nextState, data, context: instance.context, definition }; // Validate transition with judge const judgeDecision = await this.judgeEngine.validateTransition(attempt); if (!judgeDecision.approved) { const error = new Error(`Transition rejected by judge: ${judgeDecision.reasoning}`); error.judgeDecision = judgeDecision; throw error; } // Update context based on action const newContext = this.updateContext(instance.context, action, data, definition); // Log the transition with judge decision await this.logTransition(workflowId, instance.currentState, action, nextState, data, judgeDecision); // Update instance instance.currentState = nextState; instance.context = newContext; instance.updatedAt = new Date(); await this.saveInstance(instance); // Return result with next actions const nextStatedef = definition.states[nextState]; const nextActions = nextStatedef.transitions ? Object.keys(nextStatedef.transitions) : []; // Check context size and potentially truncate const { context: safeContext, warning } = this.getSafeContext(newContext); const result = { state: nextState, context: safeContext, nextActions, progress: this.calculateProgress(instance, definition), complete: nextStatedef.final }; // Add warning if context was truncated if (warning) { result.contextWarning = warning; } // Add target mismatch warning if applicable if (targetMismatchWarning) { result.warning = targetMismatchWarning; result.conditionMatched = conditionResult.matchedRule.condition; result.conditionDescription = conditionResult.matchedRule.description; } return result; } async getStatus(workflowId) { const instance = this.instances.get(workflowId); if (!instance) { throw new Error(`Workflow not found: ${workflowId}`); } const definition = this.definitions.get(instance.definitionName); if (!definition) { throw new Error(`Definition not found: ${instance.definitionName}`); } const statedef = definition.states[instance.currentState]; const nextActions = statedef.transitions ? Object.keys(statedef.transitions) : []; // Check context size and potentially truncate const { context: safeContext, warning } = this.getSafeContext(instance.context); const result = { state: instance.currentState, context: safeContext, nextActions, progress: this.calculateProgress(instance, definition), complete: statedef.final }; // Add warning if context was truncated if (warning) { result.contextWarning = warning; } return result; } updateContext(context, action, data, definition) { let newContext; // If workflow has custom context updater, use it if (definition?.contextUpdater) { newContext = definition.contextUpdater(context, action, data); } else { // Otherwise, generic update: merge data into context if (data && typeof data === 'object') { newContext = { ...context, ...data, lastAction: action }; } else { newContext = { ...context, lastAction: action }; } } // Check context size and warn if it's getting large const contextSize = JSON.stringify(newContext).length; if (contextSize > 100000) { // 100KB warning threshold console.warn(`[Workflow ${definition?.name || 'unknown'}] Large context detected: ${(contextSize / 1024).toFixed(2)}KB`); if (contextSize > 500000) { // 500KB error threshold console.error(`[Workflow ${definition?.name || 'unknown'}] Context is very large (${(contextSize / 1024).toFixed(2)}KB). Consider reducing data size.`); } } return newContext; } calculateProgress(instance, definition) { // If workflow has custom progress calculator, use it if (definition.progressCalculator) { return definition.progressCalculator(instance, definition); } // Otherwise, generic progress if (definition.states[instance.currentState].final) { return 'Workflow completed'; } return `Current state: ${instance.currentState}`; } /** * Check context size and return truncated version if too large */ getSafeContext(context) { const contextStr = JSON.stringify(context); const contextSize = contextStr.length; if (contextSize > 100000) { // 100KB threshold const warningMessage = `Context size (${(contextSize / 1024).toFixed(2)}KB) exceeds safe limit`; // Return a truncated context with metadata return { context: { _truncated: true, _originalSize: contextSize, _message: 'Context too large to return in full', // Include some basic info if available lastAction: context.lastAction, _summary: 'Use workflow.status with full=false to get summary only' }, warning: warningMessage }; } return { context }; } async saveInstance(instance) { const filePath = path.join(this.workflowDir, `${instance.id}.json`); await fs.writeFile(filePath, JSON.stringify(instance, null, 2)); } async loadInstances() { try { const files = await fs.readdir(this.workflowDir); for (const file of files) { if (file.endsWith('.json') && !file.endsWith('.log')) { const filePath = path.join(this.workflowDir, file); const content = await fs.readFile(filePath, 'utf-8'); const instance = JSON.parse(content); this.instances.set(instance.id, instance); } } } catch (error) { // Directory might not exist yet if (error.code !== 'ENOENT') { throw error; } } } async logTransition(workflowId, fromState, action, toState, data, judgeDecision) { const log = { timestamp: new Date(), fromState, action, toState, data, judgeDecision }; const logPath = path.join(this.workflowDir, `${workflowId}.log`); const logLine = JSON.stringify(log) + '\n'; await fs.appendFile(logPath, logLine); } // Checkpoint functionality async createCheckpoint(workflowId, description, metadata) { const instance = this.instances.get(workflowId); if (!instance) { throw new Error(`Workflow not found: ${workflowId}`); } // Use crypto.randomUUID if available, otherwise fallback to timestamp-based ID const checkpointId = crypto.randomUUID ? `cp-${crypto.randomUUID()}` : `cp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const checkpoint = { id: checkpointId, workflowId, timestamp: new Date(), state: instance.currentState, context: { ...instance.context }, // Deep copy context description, metadata }; // Store in memory const workflowCheckpoints = this.checkpoints.get(workflowId) || []; workflowCheckpoints.push(checkpoint); this.checkpoints.set(workflowId, workflowCheckpoints); // Persist to disk await this.saveCheckpoint(checkpoint); // Log the checkpoint creation await this.logTransition(workflowId, instance.currentState, 'CHECKPOINT', instance.currentState, { checkpointId, description }); return checkpoint; } async rollbackToCheckpoint(workflowId, checkpointId) { const instance = this.instances.get(workflowId); if (!instance) { throw new Error(`Workflow not found: ${workflowId}`); } const workflowCheckpoints = this.checkpoints.get(workflowId) || []; const checkpoint = workflowCheckpoints.find(cp => cp.id === checkpointId); if (!checkpoint) { throw new Error(`Checkpoint not found: ${checkpointId}`); } // Store current state before rollback for logging const previousState = instance.currentState; const previousContext = { ...instance.context }; // Restore state and context instance.currentState = checkpoint.state; instance.context = { ...checkpoint.context }; // Deep copy instance.updatedAt = new Date(); // Save the rolled back instance await this.saveInstance(instance); // Log the rollback await this.logTransition(workflowId, previousState, 'ROLLBACK', checkpoint.state, { checkpointId, fromContext: previousContext, toContext: checkpoint.context }); // Get the state definition for next actions const definition = this.definitions.get(instance.definitionName); if (!definition) { throw new Error(`Definition not found: ${instance.definitionName}`); } const statedef = definition.states[instance.currentState]; const nextActions = statedef.transitions ? Object.keys(statedef.transitions) : []; // Check context size and potentially truncate const { context: safeContext, warning } = this.getSafeContext(instance.context); const result = { state: instance.currentState, context: safeContext, nextActions, progress: this.calculateProgress(instance, definition), complete: statedef.final }; // Add warning if context was truncated if (warning) { result.contextWarning = warning; } return result; } async listCheckpoints(workflowId) { const checkpoints = this.checkpoints.get(workflowId) || []; return checkpoints.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } async saveCheckpoint(checkpoint) { const filePath = path.join(this.checkpointDir, `${checkpoint.workflowId}-${checkpoint.id}.json`); await fs.writeFile(filePath, JSON.stringify(checkpoint, null, 2)); } async loadCheckpoints() { try { const files = await fs.readdir(this.checkpointDir); for (const file of files) { if (file.endsWith('.json')) { const filePath = path.join(this.checkpointDir, file); const content = await fs.readFile(filePath, 'utf-8'); const checkpoint = JSON.parse(content); // Convert string dates back to Date objects checkpoint.timestamp = new Date(checkpoint.timestamp); const workflowCheckpoints = this.checkpoints.get(checkpoint.workflowId) || []; workflowCheckpoints.push(checkpoint); this.checkpoints.set(checkpoint.workflowId, workflowCheckpoints); } } } catch (error) { // Directory might not exist yet if (error.code !== 'ENOENT') { throw error; } } } async saveDefinition(definition) { const defsDir = path.join(this.workflowDir, 'definitions'); await fs.mkdir(defsDir, { recursive: true }); const filePath = path.join(defsDir, `${definition.name}.json`); // Don't save functions - they can't be serialized const { contextUpdater, progressCalculator, ...serializableDefinition } = definition; await fs.writeFile(filePath, JSON.stringify(serializableDefinition, null, 2)); } async loadDefinitions() { try { const defsDir = path.join(this.workflowDir, 'definitions'); const files = await fs.readdir(defsDir); for (const file of files) { if (file.endsWith('.json')) { const filePath = path.join(defsDir, file); const content = await fs.readFile(filePath, 'utf-8'); const definition = JSON.parse(content); this.definitions.set(definition.name, definition); } } } catch (error) { // Directory might not exist yet if (error.code !== 'ENOENT') { throw error; } } } // Judge-specific methods /** * Validate a transition without executing it */ async validateTransition(workflowId, action, data, expectedTargetState) { const instance = this.instances.get(workflowId); if (!instance) { throw new Error(`Workflow not found: ${workflowId}`); } const definition = this.definitions.get(instance.definitionName); if (!definition) { throw new Error(`Definition not found: ${instance.definitionName}`); } const currentStatedef = definition.states[instance.currentState]; if (!currentStatedef) { throw new Error(`Invalid state: ${instance.currentState}`); } const transitionRules = currentStatedef.transitions?.[action]; if (!transitionRules || transitionRules.length === 0) { const validActions = Object.keys(currentStatedef.transitions || {}); const errorMessage = error_formatter_js_1.ErrorFormatter.formatInvalidActionError(instance.currentState, action, validActions, definition.name); return { approved: false, confidence: 0, reasoning: errorMessage, violations: [`Action '${action}' not available in current state`], suggestions: validActions.length > 0 ? [`Valid actions: ${validActions.join(', ')}`, `Example: workflow.advance({ id: "${workflowId}", action: "${validActions[0]}", data: { ... } })`] : [`State '${instance.currentState}' has no available transitions`] }; } // Evaluate conditions to find the target state const conditionResult = await this.judgeEngine.evaluateTransitionConditions(transitionRules, instance.context, { workflowId, fromState: instance.currentState, action, toState: '', // Will be determined by condition evaluation data, context: instance.context, definition }); if (!conditionResult.matchedRule) { return { approved: false, confidence: 0, reasoning: `No conditions matched for action '${action}' from state '${instance.currentState}'`, violations: [`None of the ${transitionRules.length} condition(s) evaluated to true`], suggestions: [ `Conditions evaluated:`, ...conditionResult.evaluations.map((e, i) => `${i + 1}. "${e.condition}" -> ${e.result ? 'TRUE' : 'FALSE'} (${(e.confidence * 100).toFixed(0)}% confidence)`), '', conditionResult.overallReasoning ], metadata: { conditionEvaluations: conditionResult.evaluations } }; } const nextState = conditionResult.matchedRule.target; // Check if user provided an expected target state and it differs from the evaluated one let targetMismatchInfo = undefined; if (expectedTargetState && expectedTargetState !== nextState) { targetMismatchInfo = { warning: `Based on condition "${conditionResult.matchedRule.condition}", workflow engine has determined '${nextState}' as target state instead of '${expectedTargetState}'.`, expectedState: expectedTargetState, actualState: nextState, conditionMatched: conditionResult.matchedRule.condition, conditionDescription: conditionResult.matchedRule.description }; } const attempt = { workflowId, fromState: instance.currentState, action, toState: nextState, data, context: instance.context, definition }; const decision = await this.judgeEngine.validateTransition(attempt); // Record the decision to history even for validation-only calls await this.judgeEngine.recordDecision(workflowId, decision); // Add target mismatch info to decision metadata if applicable if (targetMismatchInfo && decision.approved) { decision.metadata = { ...decision.metadata, targetMismatch: targetMismatchInfo }; } return decision; } /** * Get judge decision history for a workflow */ async getJudgeHistory(workflowId, limit = 20, offset = 0) { return this.judgeEngine.getDecisionHistory(workflowId, limit, offset); } /** * Update judge configuration for a workflow */ async updateJudgeConfig(workflowName, judgeConfig) { const definition = this.definitions.get(workflowName); if (!definition) { throw new Error(`Workflow definition not found: ${workflowName}`); } definition.judgeConfig = judgeConfig; await this.saveDefinition(definition); } } exports.WorkflowEngine = WorkflowEngine; //# sourceMappingURL=workflow-engine.js.map