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
JavaScript
/**
* 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;