dfa-mcp-server
Version:
DFA-based workflow MCP server for guiding LLM task completion
576 lines • 25.7 kB
JavaScript
;
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