UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

631 lines â€ĸ 26.1 kB
/** * Template Orchestration Engine * @description Core engine for executing orchestration templates * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from '../../logging/Logger.js'; import { DependencyResolver } from './DependencyResolver.js'; import { StepExecutor } from './StepExecutor.js'; import { EntityAdoptionService } from '../services/EntityAdoptionService.js'; import { OrchestrationStateManager } from './OrchestrationStateManager.js'; import { TemplateValidator } from '../utils/TemplateValidator.js'; import { TemplateStore } from '../storage/TemplateStore.js'; import { ExecutionMonitor } from '../utils/ExecutionMonitor.js'; import { ValidationMiddleware } from './ValidationMiddleware.js'; export class TemplateOrchestrationEngine { logger = getLogger(); dependencyResolver; stepExecutor; stateManager; templateValidator; templateStore; executionMonitor; validationMiddleware; constructor(context) { this.dependencyResolver = new DependencyResolver(); this.stepExecutor = new StepExecutor(context); this.stateManager = new OrchestrationStateManager(); this.templateValidator = new TemplateValidator(); this.templateStore = new TemplateStore(); this.executionMonitor = new ExecutionMonitor(); this.validationMiddleware = new ValidationMiddleware(); // Create and wire up EntityAdoptionService if we have required dependencies this.logger.debug(`Checking EntityAdoptionService dependencies: entityRouter=${!!context.entityRouter}, cacheManager=${!!context.cacheManager}, logger=${!!context.logger}`); if (context.entityRouter && context.cacheManager && context.logger) { const adoptionService = new EntityAdoptionService(context.entityRouter, context.cacheManager, context.logger); this.stepExecutor.setAdoptionService(adoptionService); this.logger.info('EntityAdoptionService initialized and wired to StepExecutor'); } else { this.logger.warn(`EntityAdoptionService not initialized - missing required dependencies in context: entityRouter=${!!context.entityRouter}, cacheManager=${!!context.cacheManager}, logger=${!!context.logger}`); } } /** * Execute an orchestration template from inline template (DEBUG MODE) * 🚀 NEW: Bypasses database lookup for rapid development/debugging */ async executeOrchestrationDirect(template, parameters, context) { const startTime = Date.now(); const executionId = this.generateExecutionId(); this.logger.info({ templateId: template.id || 'inline-template', executionId, parameters: Object.keys(parameters), mode: 'DIRECT' }, '🚀 Starting DIRECT orchestration execution (bypassing database)'); let state = null; try { // 1. Validate inline template await this.validateExecution(template, parameters); // 2. Initialize execution state state = await this.stateManager.initializeState(executionId, template.id || 'inline-template', parameters); // 3. Start execution monitoring this.executionMonitor.startMonitoring(executionId, template.config); // 4. Execute pre-execution hook if defined if (template.config?.pre_execution) { await this.executeHook('pre_execution', template.config.pre_execution, state); } // 5. Build execution plan const executionPlan = await this.dependencyResolver.buildExecutionPlan(template.steps, state); this.logger.info({ executionId, batches: executionPlan.batches.length, estimatedDuration: executionPlan.estimated_duration_ms, mode: 'DIRECT' }, 'DIRECT execution plan created'); // 6. Execute steps according to plan await this.executeSteps(executionPlan, state, template.config, context); // 7. Execute post-execution hook if defined if (template.config?.post_execution) { await this.executeHook('post_execution', template.config.post_execution, state); } // 8. Process outputs const outputs = await this.processOutputs(template, state); state.outputs = outputs; // 9. Build and return result const result = this.buildSuccessResult(executionId, template, state, startTime, executionPlan); // Add mode indicator result.mode = 'DIRECT'; // 10. Save execution record (for debugging/audit purposes) await this.saveExecutionRecord(executionId, template.id || 'inline-template', state, result); this.logger.info({ executionId, duration: Date.now() - startTime, success: true, mode: 'DIRECT' }, '🚀 DIRECT orchestration completed successfully'); return result; } catch (error) { this.logger.error({ executionId, error: error instanceof Error ? error.message : String(error), mode: 'DIRECT' }, '🚀 DIRECT orchestration execution failed'); // Handle rollback if configured if (state && template.config?.rollback_on_failure) { await this.performRollback(state); } // Build error result const result = this.buildErrorResult(executionId, template.id || 'inline-template', state, error instanceof Error ? error : new Error(String(error)), startTime); // Add mode indicator result.mode = 'DIRECT'; // Save failed execution record if (state) { await this.saveExecutionRecord(executionId, template.id || 'inline-template', state, result); } return result; } finally { // Clean up monitoring this.executionMonitor.stopMonitoring(executionId); } } /** * Execute an orchestration template */ async executeOrchestration(templateId, parameters, context) { const startTime = Date.now(); const executionId = this.generateExecutionId(); this.logger.info({ templateId, executionId, parameters: Object.keys(parameters) }, 'Starting orchestration execution'); let state = null; try { // 1. Load and validate template const template = await this.loadTemplate(templateId); await this.validateExecution(template, parameters); // 2. Initialize execution state state = await this.stateManager.initializeState(executionId, templateId, parameters); // 3. Start execution monitoring this.executionMonitor.startMonitoring(executionId, template.config); // 4. Execute pre-execution hook if defined if (template.config?.pre_execution) { await this.executeHook('pre_execution', template.config.pre_execution, state); } // 5. Build execution plan const executionPlan = await this.dependencyResolver.buildExecutionPlan(template.steps, state); this.logger.info({ executionId, batches: executionPlan.batches.length, estimatedDuration: executionPlan.estimated_duration_ms }, 'Execution plan created'); // 6. Execute steps according to plan await this.executeSteps(executionPlan, state, template.config, context); // 7. Execute post-execution hook if defined if (template.config?.post_execution) { await this.executeHook('post_execution', template.config.post_execution, state); } // 8. Process outputs const outputs = await this.processOutputs(template, state); state.outputs = outputs; // 9. Build and return result const result = this.buildSuccessResult(executionId, template, state, startTime, executionPlan); // 10. Save execution record await this.saveExecutionRecord(executionId, template.id, state, result); this.logger.info({ executionId, duration: Date.now() - startTime, success: true }, 'Orchestration completed successfully'); return result; } catch (error) { this.logger.error({ executionId, error: error instanceof Error ? error.message : String(error), templateId }, 'Orchestration execution failed'); // Handle rollback if configured if (state) { const template = await this.templateStore.getTemplate(state.template_id); if (template?.config?.rollback_on_failure) { await this.performRollback(state); } } // Build error result const result = this.buildErrorResult(executionId, templateId, state, error instanceof Error ? error : new Error(String(error)), startTime); // Save failed execution record if (state) { await this.saveExecutionRecord(executionId, templateId, state, result); } return result; } finally { // Clean up monitoring this.executionMonitor.stopMonitoring(executionId); } } /** * Load template from store */ async loadTemplate(templateId) { const template = await this.templateStore.getTemplate(templateId); if (!template) { throw new Error(`Template not found: ${templateId}`); } return template; } /** * Validate template and parameters */ async validateExecution(template, parameters) { // Use comprehensive validation middleware for pre-execution validation const validationReport = await this.validationMiddleware.validateBeforeExecution(template, parameters, { validateStructure: true, validateEntities: true, platform: template.platform }); if (!validationReport.valid) { const errorDetails = this.validationMiddleware.formatErrorsForDisplay(validationReport); const suggestedFixes = this.validationMiddleware.getSuggestedFixes(validationReport); this.logger.error({ templateId: template.id, templateName: template.name, validationErrors: validationReport.entityValidation?.errors, structureErrors: validationReport.structureValidation?.errors }, 'Template validation failed before execution'); throw new Error(`Template validation failed:\n\n${errorDetails}\n\nSuggested fixes:\n${suggestedFixes.map((fix, i) => `${i + 1}. ${fix}`).join('\n')}\n\nPlease fix these issues before executing the template.`); } this.logger.info({ templateId: template.id, validationSummary: validationReport.summary }, 'Template passed pre-execution validation'); } /** * Execute steps according to plan */ async executeSteps(plan, state, config, context) { for (const batch of plan.batches) { this.logger.info({ batchNumber: batch.batch_number, stepCount: batch.steps.length, canParallelize: batch.can_parallelize }, 'Executing batch'); if (batch.can_parallelize && config?.parallel_execution) { await this.executeParallelBatch(batch.steps, state, context); } else { await this.executeSequentialBatch(batch.steps, state, context); } // Check if we should continue if (!this.shouldContinue(state, config)) { break; } } } /** * Execute steps in parallel */ async executeParallelBatch(steps, state, context) { const promises = steps.map(step => this.executeSingleStep(step, state, context)); // Use Promise.allSettled to ensure all atomic completions are processed // even if some steps fail - this prevents race conditions const results = await Promise.allSettled(promises); // Check if any steps failed and throw the first error to maintain fail-fast behavior const firstFailure = results.find(result => result.status === 'rejected'); if (firstFailure && firstFailure.status === 'rejected') { throw firstFailure.reason; } } /** * Execute steps sequentially */ async executeSequentialBatch(steps, state, context) { for (const step of steps) { await this.executeSingleStep(step, state, context); } } /** * Execute a single step */ async executeSingleStep(step, state, context) { try { // Record step start state.recordStepStart(step.id); // Check skip condition if (step.skip_if && await this.evaluateCondition(step.skip_if, state)) { state.recordSkippedStep(step.id); return; } // Execute with retry logic let lastError = null; const maxAttempts = step.retry?.max_attempts || 1; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const result = await this.stepExecutor.executeStep(step, state, context); console.log(`🏁 Step ${step.id} execution returned, state managed by StepExecutor`); return; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < maxAttempts) { await this.handleRetry(step, attempt); } } } // All attempts failed throw lastError; } catch (error) { // Check if StepExecutor already recorded the error (avoid double-recording) const stepState = state.steps.get(step.id); if (!stepState?.error) { console.log(`🔒 Orchestration recording error for step ${step.id}`); state.recordStepError(step.id, error instanceof Error ? error : new Error(String(error))); } else { console.log(`â„šī¸ Step ${step.id} error already recorded by StepExecutor`); } // Handle error based on configuration if (step.on_error === 'continue') { this.logger.warn({ stepId: step.id, error: error instanceof Error ? error.message : String(error) }, 'Step failed but continuing execution'); } else if (step.error_handler) { await this.executeSingleStep(step.error_handler, state, context); } else { throw error; } } } /** * Handle retry delay */ async handleRetry(step, attempt) { if (!step.retry) return; let delay = step.retry.delay; if (step.retry.backoff === 'exponential') { const multiplier = step.retry.backoff_multiplier || 2; delay = delay * Math.pow(multiplier, attempt - 1); } await new Promise(resolve => setTimeout(resolve, delay)); } /** * Evaluate JSONPath condition */ async evaluateCondition(condition, state) { try { const result = state.get(condition); return !!result; } catch (error) { this.logger.warn({ condition, error: error instanceof Error ? error.message : String(error) }, 'Failed to evaluate condition'); return false; } } /** * Check if execution should continue */ shouldContinue(state, config) { if (state.status === 'cancelled') { return false; } if (!config?.continue_on_error && state.errors.length > 0) { return false; } return true; } /** * Execute hook code */ async executeHook(hookName, code, state) { this.logger.info({ hookName }, 'Executing hook'); try { await this.stepExecutor.executePluginCode(code, state); } catch (error) { this.logger.error({ hookName, error: error instanceof Error ? error.message : String(error) }, 'Hook execution failed'); throw error; } } /** * Process template outputs */ async processOutputs(template, state) { const outputs = {}; if (!template.outputs) { return outputs; } for (const [key, definition] of Object.entries(template.outputs)) { try { outputs[key] = state.get(definition.value); } catch (error) { this.logger.warn({ outputKey: key, path: definition.value, error: error instanceof Error ? error.message : String(error) }, 'Failed to extract output'); } } return outputs; } /** * Perform rollback operations */ async performRollback(state) { this.logger.info({ executionId: state.execution_id }, 'Performing rollback'); // TODO: Implement rollback logic // This would involve reversing successful operations } /** * Build success result */ buildSuccessResult(executionId, template, state, startTime, plan) { const createdEntities = this.extractCreatedEntities(state); const summary = this.buildSummary(template, state, createdEntities); return { execution_id: executionId, template_id: template.id, template_name: template.name, success: true, status: 'completed', summary, created_entities: createdEntities, outputs: state.outputs, execution_time_ms: Date.now() - startTime, steps_executed: this.countExecutedSteps(state), steps_skipped: this.countSkippedSteps(state), steps_failed: this.countFailedSteps(state), errors: [], warnings: [], debug: state.variables['debug'] ? { execution_plan: plan, step_results: state.steps } : undefined }; } /** * Build error result */ buildErrorResult(executionId, templateId, state, error, startTime) { return { execution_id: executionId, template_id: templateId, template_name: 'Unknown', success: false, status: 'failed', summary: { headline: 'Orchestration failed', operation_type: 'orchestration', total_operations: 0, successful_operations: 0, key_entities: [] }, created_entities: state ? this.extractCreatedEntities(state) : [], outputs: state ? state.outputs : {}, execution_time_ms: Date.now() - startTime, steps_executed: state ? this.countExecutedSteps(state) : 0, steps_skipped: state ? this.countSkippedSteps(state) : 0, steps_failed: state ? this.countFailedSteps(state) : 0, errors: [{ message: error.message, timestamp: new Date(), recoverable: false }], warnings: [] }; } /** * Extract created entities from state */ extractCreatedEntities(state) { console.log(`🔍 [extractCreatedEntities] Checking state for entities`); console.log(`🔍 state.outputs exists: ${!!state.outputs}`); console.log(`🔍 state.outputs.created_entities exists: ${!!state.outputs?.created_entities}`); console.log(`🔍 state.outputs.created_entities length: ${state.outputs?.created_entities?.length || 0}`); if (state.outputs?.created_entities) { console.log(`🔍 state.outputs.created_entities content:`, JSON.stringify(state.outputs.created_entities, null, 2)); } // FIXED: Check for entities stored in state.outputs.created_entities first (new approach) if (state.outputs?.created_entities && Array.isArray(state.outputs.created_entities)) { console.log(`✅ Found ${state.outputs.created_entities.length} entities in state.outputs`); this.logger.debug({ count: state.outputs.created_entities.length }, 'Found created entities in state.outputs'); return state.outputs.created_entities; } else { console.log(`❌ No entities found in state.outputs.created_entities`); } // Fallback: Check step results (legacy approach) const entities = []; state.steps.forEach((stepState, stepId) => { if (stepState.status === 'completed' && stepState.result) { // Check for entity data in various possible locations let entityData = null; // Check if result has success and data (manageEntityLifecycle format) if (stepState.result.success && stepState.result.data) { entityData = stepState.result.data; } else if (stepState.result.created_entity) { // Legacy format entityData = stepState.result.created_entity; } else if (stepState.result.entity) { // Alternative format entityData = stepState.result.entity; } if (entityData && entityData.id) { entities.push({ step_id: stepId, entity_type: entityData.entity_type || entityData.type, entity_id: entityData.id, entity_key: entityData.key, entity_name: entityData.name, data: entityData }); this.logger.debug({ stepId, entityId: entityData.id, entityType: entityData.entity_type || entityData.type }, 'Extracted entity from step result'); } } }); return entities; } /** * Build execution summary */ buildSummary(template, state, createdEntities) { return { headline: `Executed ${template.name} successfully`, operation_type: 'orchestration', total_operations: state.steps.size, successful_operations: this.countExecutedSteps(state), key_entities: createdEntities.map(e => ({ type: e.entity_type, id: e.entity_id, name: e.entity_name, operation: 'create' })) }; } /** * Count executed steps */ countExecutedSteps(state) { console.log(`🔍 [countExecutedSteps] Checking state.steps`); console.log(`🔍 state.steps.size: ${state.steps.size}`); let count = 0; state.steps.forEach((step, stepId) => { console.log(`🔍 Step ${stepId}: status = ${step.status}`); if (step.status === 'completed') { count++; console.log(`✅ Counting completed step: ${stepId}`); } }); console.log(`🔍 Total executed steps: ${count}`); return count; } /** * Count skipped steps */ countSkippedSteps(state) { let count = 0; state.steps.forEach(step => { if (step.status === 'skipped') count++; }); return count; } /** * Count failed steps */ countFailedSteps(state) { let count = 0; state.steps.forEach(step => { if (step.status === 'failed') count++; }); return count; } /** * Save execution record */ async saveExecutionRecord(executionId, templateId, state, result) { try { await this.templateStore.saveExecution({ id: executionId, template_id: templateId, started_at: state.started_at.toISOString(), completed_at: state.completed_at?.toISOString(), status: result.status, parameters: JSON.stringify(state.parameters), state: JSON.stringify(state), result: JSON.stringify(result), error: result.errors.length > 0 ? result.errors[0].message : undefined }); } catch (error) { this.logger.error({ executionId, error: error instanceof Error ? error.message : String(error) }, 'Failed to save execution record'); } } /** * Generate unique execution ID */ generateExecutionId() { return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } //# sourceMappingURL=TemplateOrchestrationEngine.js.map