@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
631 lines âĸ 26.1 kB
JavaScript
/**
* 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