UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

288 lines (249 loc) 8.88 kB
import { EventEmitter } from 'events'; import { ProcessDefinition, ProcessExecution, ExecutionStatus, Activity, ActivityResult, ProcessError, LogEntry, ActivityType } from './types.js'; import { ActivityExecutor } from './activity-executor.js'; import { VariableResolver } from './variable-resolver.js'; import { ProcessStore } from './process-store.js'; // import { AgentOrchestrator } from '../agent-orchestration/orchestrator.js'; export class ProcessEngine extends EventEmitter { private executions: Map<string, ProcessExecution> = new Map(); private activityExecutor: ActivityExecutor; private variableResolver: VariableResolver; private store: ProcessStore; // private agentOrchestrator: AgentOrchestrator; constructor(store: ProcessStore, agentOrchestrator?: any /*AgentOrchestrator*/) { super(); this.store = store; // this.agentOrchestrator = agentOrchestrator; this.activityExecutor = new ActivityExecutor(this, agentOrchestrator); this.variableResolver = new VariableResolver(); } async executeProcess( process: ProcessDefinition, triggerId: string = 'manual', initialVariables: Record<string, any> = {} ): Promise<ProcessExecution> { const execution: ProcessExecution = { id: this.generateExecutionId(), processId: process.id, processVersion: process.version, status: 'pending', triggeredBy: triggerId, startedAt: new Date().toISOString(), variables: { ...process.variables, ...initialVariables }, activityResults: [], logs: [] }; this.executions.set(execution.id, execution); await this.store.saveExecution(execution); this.log(execution.id, 'info', `Starting process: ${process.name}`); this.emit('execution:started', execution); try { execution.status = 'running'; await this.store.saveExecution(execution); // Execute activities sequentially for (const activity of process.activities) { if (execution.status !== 'running') { break; // Process was paused or cancelled } await this.executeActivity(execution, activity); } if (execution.status === 'running') { execution.status = 'completed'; execution.completedAt = new Date().toISOString(); execution.duration = Date.now() - new Date(execution.startedAt).getTime(); // Execute success handlers if any if (process.onSuccess) { for (const activity of process.onSuccess) { await this.executeActivity(execution, activity); } } } } catch (error) { execution.status = 'failed'; execution.error = this.createProcessError(error); execution.completedAt = new Date().toISOString(); execution.duration = Date.now() - new Date(execution.startedAt).getTime(); this.log(execution.id, 'error', `Process failed: ${execution.error.message}`); // Execute failure handlers if any if (process.onFailure) { for (const activity of process.onFailure) { try { await this.executeActivity(execution, activity); } catch (handlerError) { this.log(execution.id, 'error', `Failure handler error: ${handlerError}`); } } } } await this.store.saveExecution(execution); this.emit('execution:completed', execution); return execution; } private async executeActivity( execution: ProcessExecution, activity: Activity ): Promise<ActivityResult> { // Check condition if specified if (activity.condition) { const shouldRun = await this.variableResolver.evaluateCondition( activity.condition, execution.variables ); if (!shouldRun) { const result: ActivityResult = { activityId: activity.id, status: 'skipped', startedAt: new Date().toISOString(), completedAt: new Date().toISOString(), duration: 0 }; execution.activityResults.push(result); this.log(execution.id, 'info', `Skipped activity: ${activity.name} (condition not met)`); return result; } } this.log(execution.id, 'info', `Executing activity: ${activity.name}`, activity.id); this.emit('activity:started', { execution, activity }); const startTime = Date.now(); const result: ActivityResult = { activityId: activity.id, status: 'success', startedAt: new Date().toISOString(), completedAt: '', duration: 0 }; try { // Resolve input variables const resolvedInputs = await this.variableResolver.resolveVariables( activity.inputs || {}, execution.variables ); // Execute based on activity type const outputs = await this.activityExecutor.execute( activity, resolvedInputs, execution ); result.outputs = outputs; result.status = 'success'; // Store outputs in execution variables if (activity.outputs && outputs) { for (const outputVar of activity.outputs) { execution.variables[outputVar] = outputs[outputVar]; } } } catch (error) { result.status = 'failed'; result.error = error instanceof Error ? error.message : String(error); // Handle error based on error handler configuration if (activity.errorHandler) { await this.handleActivityError(execution, activity, error); } else { throw error; // Propagate to process level } } finally { result.completedAt = new Date().toISOString(); result.duration = Date.now() - startTime; execution.activityResults.push(result); this.emit('activity:completed', { execution, activity, result }); } return result; } private async handleActivityError( execution: ProcessExecution, activity: Activity, error: any ): Promise<void> { const handler = activity.errorHandler!; switch (handler.type) { case 'retry': // Implement retry logic if (handler.retryPolicy) { // TODO: Implement retry with backoff this.log(execution.id, 'warn', `Retrying activity: ${activity.name}`); } break; case 'skip': this.log(execution.id, 'warn', `Skipping failed activity: ${activity.name}`); break; case 'alternate': if (handler.alternateActivities) { for (const altActivity of handler.alternateActivities) { await this.executeActivity(execution, altActivity); } } break; case 'fail': default: throw error; } } async pauseExecution(executionId: string): Promise<void> { const execution = this.executions.get(executionId); if (execution && execution.status === 'running') { execution.status = 'paused'; await this.store.saveExecution(execution); this.emit('execution:paused', execution); } } async resumeExecution(executionId: string): Promise<void> { const execution = this.executions.get(executionId); if (execution && execution.status === 'paused') { execution.status = 'running'; await this.store.saveExecution(execution); this.emit('execution:resumed', execution); // TODO: Continue execution from where it left off } } async cancelExecution(executionId: string): Promise<void> { const execution = this.executions.get(executionId); if (execution && ['running', 'paused', 'waiting'].includes(execution.status)) { execution.status = 'cancelled'; execution.completedAt = new Date().toISOString(); execution.duration = Date.now() - new Date(execution.startedAt).getTime(); await this.store.saveExecution(execution); this.emit('execution:cancelled', execution); } } getExecution(executionId: string): ProcessExecution | undefined { return this.executions.get(executionId); } private log( executionId: string, level: LogEntry['level'], message: string, activityId?: string ): void { const execution = this.executions.get(executionId); if (execution) { const entry: LogEntry = { timestamp: new Date().toISOString(), level, message, activityId }; execution.logs.push(entry); this.emit('execution:log', { executionId, entry }); } } private createProcessError(error: any): ProcessError { return { message: error instanceof Error ? error.message : String(error), code: error.code, stack: error instanceof Error ? error.stack : undefined, retryable: false // TODO: Determine based on error type }; } private generateExecutionId(): string { return `exec-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } }