vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
753 lines (752 loc) • 31.8 kB
JavaScript
import path from 'path';
import { EventEmitter } from 'events';
import * as fs from 'fs-extra';
import logger from '../../../logger.js';
import { FileUtils } from '../utils/file-utils.js';
import { createErrorContext } from '../utils/enhanced-errors.js';
import { getVibeTaskManagerOutputDir } from '../utils/config-loader.js';
export function createWorkflowId(id) {
if (!id || id.trim().length === 0) {
throw new Error('Workflow ID cannot be empty');
}
return id.trim();
}
export function createSessionId(id) {
if (!id || id.trim().length === 0) {
throw new Error('Session ID cannot be empty');
}
return id.trim();
}
export function createTaskId(id) {
if (!id || id.trim().length === 0) {
throw new Error('Task ID cannot be empty');
}
return id.trim();
}
export function createProjectId(id) {
if (!id || id.trim().length === 0) {
throw new Error('Project ID cannot be empty');
}
return id.trim();
}
export function createSuccess(data) {
return { success: true, data };
}
export function createFailure(error) {
return { success: false, error };
}
export function resolveWorkflowId(data) {
try {
const metadata = data.metadata;
const resolvedId = (metadata?.jobId ||
metadata?.sessionId ||
data.taskId ||
null);
if (!resolvedId || resolvedId.trim().length === 0) {
return createFailure('No valid ID found in progress event data');
}
const workflowId = createWorkflowId(resolvedId.trim());
return createSuccess(workflowId);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error during ID resolution';
return createFailure(`Failed to resolve workflow ID: ${errorMessage}`);
}
}
export function mapSubtaskToParentWorkflowId(taskId) {
const rddSubtaskPattern = /^(task-\d+)-(?:atomic|plan|impl)-\d+$/;
const match = taskId.match(rddSubtaskPattern);
if (match) {
const parentId = match[1];
logger.debug({
originalTaskId: taskId,
parentTaskId: parentId,
pattern: 'rdd_subtask',
matchType: 'RDD engine subtask'
}, 'Mapped RDD subtask to parent task ID');
return parentId;
}
const genericSubtaskPattern = /^(.+)-[a-zA-Z]+-\d+$/;
const genericMatch = taskId.match(genericSubtaskPattern);
if (genericMatch) {
const parentId = genericMatch[1];
logger.debug({
originalTaskId: taskId,
parentTaskId: parentId,
pattern: 'generic_subtask',
matchType: 'Generic subtask pattern'
}, 'Mapped generic subtask to parent task ID');
return parentId;
}
logger.debug({
taskId: taskId,
pattern: 'no_mapping',
matchType: 'Direct task ID (no mapping needed)'
}, 'No subtask pattern detected, using original task ID');
return taskId;
}
export function resolveWorkflowIdWithMapping(data) {
try {
logger.debug({
dataKeys: Object.keys(data),
taskId: data.taskId,
metadataJobId: data.metadata?.jobId,
metadataSessionId: data.metadata?.sessionId
}, 'Starting enhanced workflow ID resolution');
const standardResult = resolveWorkflowId(data);
if (standardResult.success) {
const workflowId = standardResult.data;
const mappedId = mapSubtaskToParentWorkflowId(workflowId);
if (mappedId !== workflowId) {
logger.debug({
originalWorkflowId: workflowId,
mappedWorkflowId: mappedId,
resolution: 'subtask_mapped_to_parent'
}, 'Successfully mapped subtask ID to parent workflow ID');
return createSuccess(createWorkflowId(mappedId));
}
logger.debug({
workflowId: workflowId,
resolution: 'direct_workflow_id'
}, 'Successfully resolved direct workflow ID');
return standardResult;
}
logger.debug({
error: standardResult.error,
resolution: 'failed'
}, 'Enhanced workflow ID resolution failed');
return standardResult;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error during enhanced ID resolution';
logger.error({
err: error,
dataKeys: Object.keys(data),
taskId: data.taskId
}, 'Exception during enhanced workflow ID resolution');
return createFailure(`Failed to resolve workflow ID with mapping: ${errorMessage}`);
}
}
export var WorkflowPhase;
(function (WorkflowPhase) {
WorkflowPhase["INITIALIZATION"] = "initialization";
WorkflowPhase["DECOMPOSITION"] = "decomposition";
WorkflowPhase["ORCHESTRATION"] = "orchestration";
WorkflowPhase["EXECUTION"] = "execution";
WorkflowPhase["COMPLETED"] = "completed";
WorkflowPhase["FAILED"] = "failed";
WorkflowPhase["CANCELLED"] = "cancelled";
})(WorkflowPhase || (WorkflowPhase = {}));
export var WorkflowState;
(function (WorkflowState) {
WorkflowState["PENDING"] = "pending";
WorkflowState["IN_PROGRESS"] = "in_progress";
WorkflowState["COMPLETED"] = "completed";
WorkflowState["FAILED"] = "failed";
WorkflowState["CANCELLED"] = "cancelled";
WorkflowState["BLOCKED"] = "blocked";
WorkflowState["RETRYING"] = "retrying";
})(WorkflowState || (WorkflowState = {}));
export const SUB_PHASES = {
[WorkflowPhase.INITIALIZATION]: [
{ name: 'setup', weight: 0.4, order: 1 },
{ name: 'validation', weight: 0.3, order: 2 },
{ name: 'preparation', weight: 0.3, order: 3 }
],
[WorkflowPhase.DECOMPOSITION]: [
{ name: 'research', weight: 0.2, order: 1 },
{ name: 'context_gathering', weight: 0.25, order: 2 },
{ name: 'decomposition', weight: 0.3, order: 3 },
{ name: 'validation', weight: 0.15, order: 4 },
{ name: 'dependency_detection', weight: 0.1, order: 5 }
],
[WorkflowPhase.ORCHESTRATION]: [
{ name: 'task_preparation', weight: 0.4, order: 1 },
{ name: 'dependency_resolution', weight: 0.3, order: 2 },
{ name: 'agent_assignment', weight: 0.3, order: 3 }
],
[WorkflowPhase.EXECUTION]: [
{ name: 'task_execution', weight: 0.7, order: 1 },
{ name: 'monitoring', weight: 0.2, order: 2 },
{ name: 'completion_verification', weight: 0.1, order: 3 }
],
[WorkflowPhase.COMPLETED]: [],
[WorkflowPhase.FAILED]: [],
[WorkflowPhase.CANCELLED]: []
};
const VALID_TRANSITIONS = new Map([
[`${WorkflowPhase.INITIALIZATION}:${WorkflowState.PENDING}`, new Set([
`${WorkflowPhase.INITIALIZATION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.INITIALIZATION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.INITIALIZATION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.INITIALIZATION}:${WorkflowState.IN_PROGRESS}`, new Set([
`${WorkflowPhase.INITIALIZATION}:${WorkflowState.COMPLETED}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.PENDING}`,
`${WorkflowPhase.INITIALIZATION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.INITIALIZATION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.INITIALIZATION}:${WorkflowState.COMPLETED}`, new Set([
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.PENDING}`
])],
[`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.PENDING}`, new Set([
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.IN_PROGRESS}`, new Set([
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.COMPLETED}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.CANCELLED}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.RETRYING}`
])],
[`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.COMPLETED}`, new Set([
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.PENDING}`
])],
[`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.RETRYING}`, new Set([
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.DECOMPOSITION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.PENDING}`, new Set([
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.IN_PROGRESS}`, new Set([
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.COMPLETED}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.CANCELLED}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.RETRYING}`
])],
[`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.COMPLETED}`, new Set([
`${WorkflowPhase.EXECUTION}:${WorkflowState.PENDING}`
])],
[`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.RETRYING}`, new Set([
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.ORCHESTRATION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.EXECUTION}:${WorkflowState.PENDING}`, new Set([
`${WorkflowPhase.EXECUTION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.CANCELLED}`
])],
[`${WorkflowPhase.EXECUTION}:${WorkflowState.IN_PROGRESS}`, new Set([
`${WorkflowPhase.EXECUTION}:${WorkflowState.COMPLETED}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.CANCELLED}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.RETRYING}`
])],
[`${WorkflowPhase.EXECUTION}:${WorkflowState.COMPLETED}`, new Set([
`${WorkflowPhase.COMPLETED}:${WorkflowState.COMPLETED}`
])],
[`${WorkflowPhase.EXECUTION}:${WorkflowState.RETRYING}`, new Set([
`${WorkflowPhase.EXECUTION}:${WorkflowState.IN_PROGRESS}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.FAILED}`,
`${WorkflowPhase.EXECUTION}:${WorkflowState.CANCELLED}`
])]
]);
export class WorkflowStateManager extends EventEmitter {
static instance = null;
workflows = new Map();
persistenceEnabled = true;
persistenceDirectory;
version = '1.0.0';
constructor(persistenceDirectory) {
super();
this.persistenceDirectory = persistenceDirectory ||
path.join(getVibeTaskManagerOutputDir(), 'workflow-states');
}
static getInstance(persistenceDirectory) {
if (!WorkflowStateManager.instance) {
WorkflowStateManager.instance = new WorkflowStateManager(persistenceDirectory);
}
return WorkflowStateManager.instance;
}
async initializeWorkflow(workflowId, sessionId, projectId, metadata = {}) {
const context = createErrorContext('WorkflowStateManager', 'initializeWorkflow')
.sessionId(sessionId)
.projectId(projectId)
.metadata({ workflowId })
.build();
try {
const now = new Date();
const initialPhase = {
phase: WorkflowPhase.INITIALIZATION,
state: WorkflowState.PENDING,
startTime: now,
progress: 0,
metadata: {},
retryCount: 0,
maxRetries: 3
};
const workflow = {
workflowId,
sessionId,
projectId,
currentPhase: WorkflowPhase.INITIALIZATION,
currentState: WorkflowState.PENDING,
overallProgress: 0,
startTime: now,
phases: new Map([[WorkflowPhase.INITIALIZATION, initialPhase]]),
transitions: [],
metadata,
persistedAt: now,
version: this.version
};
this.workflows.set(workflowId, workflow);
if (this.persistenceEnabled) {
await this.persistWorkflow(workflow);
}
logger.info({
workflowId,
sessionId,
projectId,
phase: WorkflowPhase.INITIALIZATION,
state: WorkflowState.PENDING
}, 'Workflow initialized');
this.emit('workflow:initialized', { workflowId, sessionId, projectId, snapshot: workflow });
return workflow;
}
catch (error) {
logger.error({ err: error, ...context }, 'Failed to initialize workflow');
throw error;
}
}
async transitionWorkflow(workflowId, toPhase, toState, options = {}) {
const context = createErrorContext('WorkflowStateManager', 'transitionWorkflow')
.metadata({ workflowId, toPhase, toState, ...options })
.build();
try {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found`);
}
const fromPhase = workflow.currentPhase;
const fromState = workflow.currentState;
const isValidTransition = this.validateTransition(fromPhase, fromState, toPhase, toState);
if (!isValidTransition) {
throw new Error(`Invalid transition from ${fromPhase}:${fromState} to ${toPhase}:${toState}`);
}
const now = new Date();
const transition = {
fromPhase,
fromState,
toPhase,
toState,
timestamp: now,
reason: options.reason,
metadata: options.metadata,
triggeredBy: options.triggeredBy
};
if (workflow.phases.has(fromPhase)) {
const currentPhaseExecution = workflow.phases.get(fromPhase);
if (toState === WorkflowState.COMPLETED || toState === WorkflowState.FAILED) {
currentPhaseExecution.endTime = now;
currentPhaseExecution.duration = now.getTime() - currentPhaseExecution.startTime.getTime();
currentPhaseExecution.state = toState;
if (options.progress !== undefined) {
currentPhaseExecution.progress = options.progress;
}
}
}
if (toPhase !== fromPhase) {
const newPhaseExecution = {
phase: toPhase,
state: toState,
startTime: now,
progress: options.progress || 0,
metadata: options.metadata || {},
retryCount: 0,
maxRetries: 3
};
workflow.phases.set(toPhase, newPhaseExecution);
}
else {
const phaseExecution = workflow.phases.get(toPhase);
phaseExecution.state = toState;
if (options.progress !== undefined) {
phaseExecution.progress = options.progress;
}
if (options.metadata) {
phaseExecution.metadata = { ...phaseExecution.metadata, ...options.metadata };
}
}
workflow.currentPhase = toPhase;
workflow.currentState = toState;
workflow.transitions.push(transition);
workflow.persistedAt = now;
workflow.overallProgress = this.calculateOverallProgress(workflow);
if (toPhase === WorkflowPhase.COMPLETED || toPhase === WorkflowPhase.FAILED) {
workflow.endTime = now;
workflow.totalDuration = now.getTime() - workflow.startTime.getTime();
}
if (this.persistenceEnabled) {
await this.persistWorkflow(workflow);
}
logger.info({
workflowId,
fromPhase,
fromState,
toPhase,
toState,
progress: workflow.overallProgress,
reason: options.reason
}, 'Workflow transitioned');
const stateChangeEvent = {
workflowId,
sessionId: workflow.sessionId,
projectId: workflow.projectId,
transition,
snapshot: workflow
};
this.emit('workflow:state-changed', stateChangeEvent);
this.emit(`workflow:${toPhase}:${toState}`, stateChangeEvent);
return workflow;
}
catch (error) {
logger.error({ err: error, ...context }, 'Failed to transition workflow');
throw error;
}
}
async updatePhaseProgress(workflowId, phase, progress, metadata) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found`);
}
const phaseExecution = workflow.phases.get(phase);
if (!phaseExecution) {
throw new Error(`Phase ${phase} not found in workflow ${workflowId}`);
}
phaseExecution.progress = Math.max(0, Math.min(100, progress));
if (metadata) {
phaseExecution.metadata = { ...phaseExecution.metadata, ...metadata };
}
workflow.overallProgress = this.calculateOverallProgress(workflow);
workflow.persistedAt = new Date();
if (this.persistenceEnabled) {
await this.persistWorkflow(workflow);
}
logger.debug({
workflowId,
phase,
progress,
overallProgress: workflow.overallProgress
}, 'Phase progress updated');
this.emit('workflow:progress-updated', {
workflowId,
sessionId: workflow.sessionId,
projectId: workflow.projectId,
phase,
progress,
overallProgress: workflow.overallProgress
});
}
initializeSubPhases(workflowId, phase) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found`);
}
const phaseExecution = workflow.phases.get(phase);
if (!phaseExecution) {
throw new Error(`Phase ${phase} not found in workflow ${workflowId}`);
}
if (!phaseExecution.subPhases) {
phaseExecution.subPhases = new Map();
}
const subPhaseDefinitions = SUB_PHASES[phase];
for (const subPhaseDef of subPhaseDefinitions) {
if (!phaseExecution.subPhases.has(subPhaseDef.name)) {
phaseExecution.subPhases.set(subPhaseDef.name, {
subPhase: subPhaseDef.name,
parentPhase: phase,
state: WorkflowState.PENDING,
startTime: new Date(),
progress: 0,
weight: subPhaseDef.weight,
order: subPhaseDef.order,
metadata: {}
});
}
}
logger.debug({
workflowId,
phase,
subPhaseCount: subPhaseDefinitions.length
}, 'Sub-phases initialized for phase');
}
async updateSubPhaseProgress(workflowId, phase, subPhase, progress, state, metadata) {
try {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
return createFailure(`Workflow ${workflowId} not found`);
}
const phaseExecution = workflow.phases.get(phase);
if (!phaseExecution) {
return createFailure(`Phase ${phase} not found in workflow ${workflowId}`);
}
this.initializeSubPhases(workflowId, phase);
const subPhaseExecution = phaseExecution.subPhases.get(subPhase);
if (!subPhaseExecution) {
return createFailure(`Sub-phase ${subPhase} not found in phase ${phase} for workflow ${workflowId}`);
}
subPhaseExecution.progress = Math.max(0, Math.min(100, progress));
if (state) {
subPhaseExecution.state = state;
}
if (metadata) {
subPhaseExecution.metadata = { ...subPhaseExecution.metadata, ...metadata };
}
if (progress >= 100 && subPhaseExecution.state !== WorkflowState.COMPLETED) {
subPhaseExecution.state = WorkflowState.COMPLETED;
subPhaseExecution.endTime = new Date();
subPhaseExecution.duration = subPhaseExecution.endTime.getTime() - subPhaseExecution.startTime.getTime();
}
const phaseProgress = this.calculatePhaseProgressFromSubPhases(phaseExecution);
phaseExecution.progress = phaseProgress;
workflow.overallProgress = this.calculateOverallProgress(workflow);
workflow.persistedAt = new Date();
if (this.persistenceEnabled) {
await this.persistWorkflow(workflow);
}
logger.debug({
workflowId,
phase,
subPhase,
subPhaseProgress: progress,
phaseProgress,
overallProgress: workflow.overallProgress
}, 'Sub-phase progress updated');
this.emit('workflow:subphase-updated', {
workflowId,
sessionId: workflow.sessionId,
projectId: workflow.projectId,
phase,
subPhase,
progress,
phaseProgress,
overallProgress: workflow.overallProgress
});
return createSuccess(undefined);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error during sub-phase progress update';
logger.error({
err: error,
workflowId,
phase,
subPhase,
progress
}, 'Failed to update sub-phase progress');
return createFailure(`Failed to update sub-phase progress: ${errorMessage}`);
}
}
calculatePhaseProgressFromSubPhases(phaseExecution) {
if (!phaseExecution.subPhases || phaseExecution.subPhases.size === 0) {
return phaseExecution.progress;
}
let weightedProgress = 0;
let totalWeight = 0;
for (const subPhase of phaseExecution.subPhases.values()) {
weightedProgress += subPhase.progress * subPhase.weight;
totalWeight += subPhase.weight;
}
return totalWeight > 0 ? Math.round(weightedProgress / totalWeight) : 0;
}
getSubPhaseStatus(workflowId, phase) {
const workflow = this.workflows.get(workflowId);
if (!workflow) {
return null;
}
const phaseExecution = workflow.phases.get(phase);
if (!phaseExecution || !phaseExecution.subPhases) {
return null;
}
return new Map(phaseExecution.subPhases);
}
async startSubPhase(workflowId, phase, subPhase, metadata) {
await this.updateSubPhaseProgress(workflowId, phase, subPhase, 0, WorkflowState.IN_PROGRESS, metadata);
logger.info({
workflowId,
phase,
subPhase
}, 'Sub-phase started');
}
async completeSubPhase(workflowId, phase, subPhase, metadata) {
await this.updateSubPhaseProgress(workflowId, phase, subPhase, 100, WorkflowState.COMPLETED, metadata);
logger.info({
workflowId,
phase,
subPhase
}, 'Sub-phase completed');
}
getWorkflow(workflowId) {
return this.workflows.get(workflowId);
}
getProjectWorkflows(projectId) {
return Array.from(this.workflows.values()).filter(w => w.projectId === projectId);
}
getSessionWorkflows(sessionId) {
return Array.from(this.workflows.values()).filter(w => w.sessionId === sessionId);
}
hasWorkflow(workflowId) {
return this.workflows.has(workflowId);
}
hasPhase(workflowId, phase) {
const workflow = this.workflows.get(workflowId);
return workflow ? workflow.phases.has(phase) : false;
}
validateTransition(fromPhase, fromState, toPhase, toState) {
const fromKey = `${fromPhase}:${fromState}`;
const toKey = `${toPhase}:${toState}`;
const validTransitions = VALID_TRANSITIONS.get(fromKey);
return validTransitions ? validTransitions.has(toKey) : false;
}
calculateOverallProgress(workflow) {
const phaseWeights = {
[WorkflowPhase.INITIALIZATION]: 5,
[WorkflowPhase.DECOMPOSITION]: 30,
[WorkflowPhase.ORCHESTRATION]: 15,
[WorkflowPhase.EXECUTION]: 45,
[WorkflowPhase.COMPLETED]: 5,
[WorkflowPhase.FAILED]: 0,
[WorkflowPhase.CANCELLED]: 0
};
let totalWeight = 0;
let completedWeight = 0;
for (const [phase, execution] of workflow.phases) {
const weight = phaseWeights[phase] || 0;
totalWeight += weight;
if (execution.state === WorkflowState.COMPLETED) {
completedWeight += weight;
}
else if (execution.state === WorkflowState.IN_PROGRESS) {
completedWeight += (weight * execution.progress) / 100;
}
}
return totalWeight > 0 ? Math.round((completedWeight / totalWeight) * 100) : 0;
}
async persistWorkflow(workflow) {
try {
await fs.ensureDir(this.persistenceDirectory);
const workflowToSave = {
...workflow,
phases: Object.fromEntries(workflow.phases),
persistedAt: new Date()
};
const filePath = `${this.persistenceDirectory}/${workflow.workflowId}.json`;
const saveResult = await FileUtils.writeJsonFile(filePath, workflowToSave);
if (!saveResult.success) {
logger.warn({
workflowId: workflow.workflowId,
error: saveResult.error
}, 'Failed to persist workflow state');
}
}
catch (error) {
logger.error({
err: error,
workflowId: workflow.workflowId
}, 'Error persisting workflow state');
}
}
async loadWorkflow(workflowId) {
try {
const filePath = `${this.persistenceDirectory}/${workflowId}.json`;
const loadResult = await FileUtils.readJsonFile(filePath);
if (!loadResult.success) {
return null;
}
const workflowData = loadResult.data;
if (!workflowData || typeof workflowData !== 'object') {
logger.warn({ workflowId }, 'Invalid workflow data structure');
return null;
}
const phases = workflowData.phases && typeof workflowData.phases === 'object'
? new Map(Object.entries(workflowData.phases))
: new Map();
const startTime = typeof workflowData.startTime === 'string' || typeof workflowData.startTime === 'number'
? new Date(workflowData.startTime)
: new Date();
const endTime = workflowData.endTime && (typeof workflowData.endTime === 'string' || typeof workflowData.endTime === 'number')
? new Date(workflowData.endTime)
: undefined;
const persistedAt = typeof workflowData.persistedAt === 'string' || typeof workflowData.persistedAt === 'number'
? new Date(workflowData.persistedAt)
: new Date();
const transitions = Array.isArray(workflowData.transitions)
? workflowData.transitions.map((t) => {
const transition = t;
return {
...transition,
timestamp: typeof transition.timestamp === 'string' || typeof transition.timestamp === 'number'
? new Date(transition.timestamp)
: new Date()
};
})
: [];
const workflow = {
...workflowData,
phases,
startTime,
endTime,
persistedAt,
transitions
};
this.workflows.set(workflowId, workflow);
return workflow;
}
catch (error) {
logger.error({ err: error, workflowId }, 'Failed to load workflow from persistence');
return null;
}
}
async cleanupOldWorkflows(olderThanDays = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
let cleanedCount = 0;
for (const [workflowId, workflow] of this.workflows) {
if (workflow.endTime && workflow.endTime < cutoffDate) {
this.workflows.delete(workflowId);
try {
const filePath = `${this.persistenceDirectory}/${workflowId}.json`;
await fs.remove(filePath);
cleanedCount++;
}
catch (error) {
logger.warn({ err: error, workflowId }, 'Failed to remove persisted workflow file');
}
}
}
logger.info({ cleanedCount, olderThanDays }, 'Workflow cleanup completed');
return cleanedCount;
}
getWorkflowStats() {
const workflows = Array.from(this.workflows.values());
const total = workflows.length;
const byPhase = {};
const byState = {};
let totalDuration = 0;
let completedCount = 0;
let durationCount = 0;
for (const workflow of workflows) {
byPhase[workflow.currentPhase] = (byPhase[workflow.currentPhase] || 0) + 1;
byState[workflow.currentState] = (byState[workflow.currentState] || 0) + 1;
if (workflow.totalDuration) {
totalDuration += workflow.totalDuration;
durationCount++;
}
if (workflow.currentPhase === WorkflowPhase.COMPLETED) {
completedCount++;
}
}
return {
total,
byPhase,
byState,
averageDuration: durationCount > 0 ? totalDuration / durationCount : 0,
completionRate: total > 0 ? (completedCount / total) * 100 : 0
};
}
}