UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

551 lines (466 loc) • 14.8 kB
/** * Multi-Phase Workflow Manager with Checkpoints * * Purpose: Enable complex workflows with phase transitions, checkpoints, * and rollback capabilities * * Key Features: * - Multi-phase workflow execution * - Checkpoint creation and restoration * - Phase-to-phase handoffs with validation * - Rollback to previous checkpoints * - Phase dependency management * - Progress tracking and reporting * * @module MultiPhaseWorkflow * @version 1.0.0 * @date 2025-11-25 */ const fs = require('fs-extra'); const path = require('path'); const yaml = require('js-yaml'); const crypto = require('crypto'); class MultiPhaseWorkflow { constructor(rootDir = process.cwd()) { this.rootDir = rootDir; this.checkpointsDir = path.join(rootDir, '.sf-agent', 'checkpoints'); this.workflowsDir = path.join(rootDir, '.sf-agent', 'workflows'); // Workflow state this.state = { workflowId: null, currentPhase: null, phases: [], checkpoints: [], artifacts: {}, status: 'not-started', }; // Phase lifecycle hooks this.hooks = { beforePhase: [], afterPhase: [], onCheckpoint: [], onRollback: [], onError: [], }; } /** * Initialize workflow manager */ async initialize() { await fs.ensureDir(this.checkpointsDir); await fs.ensureDir(this.workflowsDir); console.log('āœ“ Multi-phase workflow manager initialized'); return true; } /** * Define workflow with phases */ async defineWorkflow(definition) { const workflow = { id: definition.id || this.generateWorkflowId(), name: definition.name, description: definition.description, phases: definition.phases || [], metadata: { created: new Date().toISOString(), track: definition.track || 'balanced', complexity: definition.complexity || 50, }, config: { autoCheckpoint: definition.autoCheckpoint !== false, checkpointFrequency: definition.checkpointFrequency || 'per-phase', allowRollback: definition.allowRollback !== false, validateHandoffs: definition.validateHandoffs !== false, }, }; // Initialize state this.state.workflowId = workflow.id; this.state.phases = workflow.phases; this.state.status = 'defined'; // Save workflow definition await this.saveWorkflow(workflow); console.log(`\nšŸ“‹ Workflow defined: ${workflow.name}`); console.log(` Phases: ${workflow.phases.length}`); console.log(` Checkpointing: ${workflow.config.autoCheckpoint ? 'enabled' : 'disabled'}\n`); return workflow; } /** * Execute workflow */ async execute(workflowId, options = {}) { console.log(`\nšŸš€ Starting workflow execution: ${workflowId}\n`); try { // Load workflow const workflow = await this.loadWorkflow(workflowId); this.state.workflowId = workflow.id; this.state.phases = workflow.phases; this.state.status = 'running'; this.state.startTime = new Date().toISOString(); // Create initial checkpoint if (workflow.config.autoCheckpoint) { await this.createCheckpoint('workflow-start', { description: 'Initial workflow state', automatic: true, }); } // Execute phases sequentially for (let i = 0; i < workflow.phases.length; i++) { const phase = workflow.phases[i]; // Check dependencies if (!(await this.checkPhaseDependencies(phase, i))) { throw new Error(`Phase dependencies not met: ${phase.name}`); } // Execute phase const result = await this.executePhase(phase, workflow); // Create checkpoint after phase if (workflow.config.autoCheckpoint) { await this.createCheckpoint(`phase-${i}-complete`, { phase: phase.name, result, automatic: true, }); } // Validate handoff to next phase if (i < workflow.phases.length - 1 && workflow.config.validateHandoffs) { const nextPhase = workflow.phases[i + 1]; await this.validateHandoff(phase, nextPhase, result); } } // Workflow complete this.state.status = 'completed'; this.state.endTime = new Date().toISOString(); console.log('\nāœ… Workflow completed successfully!\n'); return { success: true, workflowId: workflow.id, phases: this.state.phases.length, checkpoints: this.state.checkpoints.length, artifacts: this.state.artifacts, }; } catch (error) { console.error(`\nāŒ Workflow execution failed: ${error.message}\n`); this.state.status = 'failed'; this.state.error = error.message; // Call error hooks await this.callHooks('onError', { error }); throw error; } } /** * Execute single phase */ async executePhase(phase, workflow) { console.log(`\nšŸ“ Phase: ${phase.name}`); console.log(` Description: ${phase.description || 'N/A'}`); this.state.currentPhase = phase.name; const phaseExecution = { name: phase.name, startTime: new Date().toISOString(), status: 'running', }; try { // Call before-phase hooks await this.callHooks('beforePhase', { phase, workflow }); // Execute phase steps const stepResults = []; for (const step of phase.steps || []) { console.log(` → Step: ${step.name}`); const stepResult = await this.executeStep(step, phase); stepResults.push(stepResult); // Store artifacts if (stepResult.artifacts) { this.state.artifacts[step.name] = stepResult.artifacts; } } // Phase complete phaseExecution.status = 'completed'; phaseExecution.endTime = new Date().toISOString(); phaseExecution.results = stepResults; // Call after-phase hooks await this.callHooks('afterPhase', { phase, workflow, results: stepResults }); console.log(` āœ“ Phase completed\n`); return phaseExecution; } catch (error) { phaseExecution.status = 'failed'; phaseExecution.error = error.message; throw error; } } /** * Execute single step */ async executeStep(step, phase) { // This would call actual agents/tasks // For now, simulate execution const duration = Math.random() * 1000 + 500; // 0.5-1.5 seconds await new Promise((resolve) => setTimeout(resolve, duration)); return { step: step.name, agent: step.agent, status: 'completed', duration: duration / 1000, artifacts: step.outputs || [], }; } /** * Create checkpoint */ async createCheckpoint(name, metadata = {}) { const checkpoint = { id: this.generateCheckpointId(), name, workflowId: this.state.workflowId, timestamp: new Date().toISOString(), metadata, state: { currentPhase: this.state.currentPhase, artifacts: { ...this.state.artifacts }, status: this.state.status, }, }; // Save checkpoint const checkpointPath = path.join( this.checkpointsDir, `${this.state.workflowId}_${checkpoint.id}.json` ); await fs.writeJson(checkpointPath, checkpoint, { spaces: 2 }); this.state.checkpoints.push(checkpoint); console.log(` šŸ’¾ Checkpoint created: ${name}`); // Call checkpoint hooks await this.callHooks('onCheckpoint', { checkpoint }); return checkpoint; } /** * Restore from checkpoint */ async restoreCheckpoint(checkpointId) { console.log(`\nā®ļø Restoring from checkpoint: ${checkpointId}\n`); // Load checkpoint const checkpointPath = path.join( this.checkpointsDir, `${this.state.workflowId}_${checkpointId}.json` ); if (!(await fs.pathExists(checkpointPath))) { throw new Error(`Checkpoint not found: ${checkpointId}`); } const checkpoint = await fs.readJson(checkpointPath); // Restore state this.state.currentPhase = checkpoint.state.currentPhase; this.state.artifacts = checkpoint.state.artifacts; this.state.status = checkpoint.state.status; console.log(` āœ“ State restored to: ${checkpoint.timestamp}`); console.log(` Current phase: ${this.state.currentPhase}\n`); // Call rollback hooks await this.callHooks('onRollback', { checkpoint }); return checkpoint; } /** * List checkpoints for workflow */ async listCheckpoints(workflowId) { const pattern = `${workflowId}_*.json`; const files = await fs.readdir(this.checkpointsDir); const checkpoints = []; for (const file of files) { if (file.startsWith(workflowId)) { const checkpoint = await fs.readJson(path.join(this.checkpointsDir, file)); checkpoints.push({ id: checkpoint.id, name: checkpoint.name, timestamp: checkpoint.timestamp, phase: checkpoint.state.currentPhase, automatic: checkpoint.metadata.automatic, }); } } return checkpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); } /** * Check phase dependencies */ async checkPhaseDependencies(phase, phaseIndex) { if (!phase.dependencies || phase.dependencies.length === 0) { return true; } // Check if dependent phases are complete for (const dependency of phase.dependencies) { const dependentPhase = this.state.phases.find((p) => p.name === dependency); if (!dependentPhase) { console.error(` āœ— Dependency not found: ${dependency}`); return false; } // Check if dependent phase has been executed const dependentIndex = this.state.phases.indexOf(dependentPhase); if (dependentIndex >= phaseIndex) { console.error(` āœ— Dependency not yet executed: ${dependency}`); return false; } } console.log(` āœ“ Dependencies met`); return true; } /** * Validate handoff between phases */ async validateHandoff(fromPhase, toPhase, result) { console.log(` šŸ”„ Validating handoff: ${fromPhase.name} → ${toPhase.name}`); // Check required artifacts if (toPhase.requiredArtifacts) { for (const artifact of toPhase.requiredArtifacts) { if (!this.state.artifacts[artifact]) { throw new Error(`Required artifact missing: ${artifact}`); } } } // Check exit criteria if (fromPhase.exitCriteria) { for (const criteria of fromPhase.exitCriteria) { if (!(await this.checkCriteria(criteria, result))) { throw new Error(`Exit criteria not met: ${criteria}`); } } } console.log(` āœ“ Handoff validated\n`); return true; } /** * Check criteria */ async checkCriteria(criteria, result) { // Simple criteria checking // In production, this would be more sophisticated return result.status === 'completed'; } /** * Register hook */ registerHook(event, callback) { if (this.hooks[event]) { this.hooks[event].push(callback); } } /** * Call hooks */ async callHooks(event, data) { if (this.hooks[event]) { for (const callback of this.hooks[event]) { try { await callback(data); } catch (error) { console.error(`Hook error (${event}):`, error.message); } } } } /** * Get workflow progress */ getProgress() { const totalPhases = this.state.phases.length; const completedPhases = this.state.checkpoints.filter( (c) => c.name.includes('phase-') && c.name.includes('-complete') ).length; const progress = totalPhases > 0 ? (completedPhases / totalPhases) * 100 : 0; return { totalPhases, completedPhases, currentPhase: this.state.currentPhase, progress: Math.round(progress), status: this.state.status, checkpoints: this.state.checkpoints.length, }; } /** * Get workflow status */ getStatus() { return { workflowId: this.state.workflowId, status: this.state.status, currentPhase: this.state.currentPhase, progress: this.getProgress(), startTime: this.state.startTime, endTime: this.state.endTime, artifacts: Object.keys(this.state.artifacts).length, checkpoints: this.state.checkpoints.length, }; } /** * Generate phase report */ async generateReport(workflowId) { const workflow = await this.loadWorkflow(workflowId); const checkpoints = await this.listCheckpoints(workflowId); const report = { workflow: { id: workflow.id, name: workflow.name, track: workflow.metadata.track, }, execution: { startTime: this.state.startTime, endTime: this.state.endTime, duration: this.state.endTime && this.state.startTime ? (new Date(this.state.endTime) - new Date(this.state.startTime)) / 1000 : null, status: this.state.status, }, phases: { total: this.state.phases.length, completed: checkpoints.filter((c) => c.name.includes('phase-')).length, details: this.state.phases.map((p) => ({ name: p.name, description: p.description, steps: (p.steps || []).length, })), }, checkpoints: { total: checkpoints.length, automatic: checkpoints.filter((c) => c.automatic).length, manual: checkpoints.filter((c) => !c.automatic).length, list: checkpoints, }, artifacts: { count: Object.keys(this.state.artifacts).length, list: Object.keys(this.state.artifacts), }, }; return report; } /** * Save workflow */ async saveWorkflow(workflow) { const workflowPath = path.join(this.workflowsDir, `${workflow.id}.json`); await fs.writeJson(workflowPath, workflow, { spaces: 2 }); } /** * Load workflow */ async loadWorkflow(workflowId) { const workflowPath = path.join(this.workflowsDir, `${workflowId}.json`); if (!(await fs.pathExists(workflowPath))) { throw new Error(`Workflow not found: ${workflowId}`); } return await fs.readJson(workflowPath); } /** * Generate workflow ID */ generateWorkflowId() { return `workflow_${Date.now()}_${Math.floor(Math.random() * 1000)}`; } /** * Generate checkpoint ID */ generateCheckpointId() { return crypto .createHash('md5') .update(`${Date.now()}_${Math.random()}`) .digest('hex') .slice(0, 8); } } module.exports = MultiPhaseWorkflow;