UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

503 lines (502 loc) 20.8 kB
import { jobManager, JobStatus } from '../../../services/job-manager/index.js'; import { getTimeoutManager } from '../utils/timeout-manager.js'; import logger from '../../../logger.js'; import { EventEmitter } from 'events'; export class JobManagerIntegrationService extends EventEmitter { static instance; taskJobs = new Map(); jobMetrics = new Map(); jobQueue = []; runningJobs = new Set(); jobSubscriptions = new Map(); progressSubscriptions = new Map(); config = null; userConfig; processingInterval; initialized = false; constructor(config) { super(); this.userConfig = config; logger.info('Job Manager Integration Service initialized (job processor deferred)'); } getConfig() { if (!this.config) { const timeoutManager = getTimeoutManager(); this.config = { maxConcurrentJobs: 5, priorityWeights: { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 }, retryPolicy: { maxRetries: 3, backoffMultiplier: 2, initialDelayMs: 1000 }, timeoutPolicy: { defaultTimeoutMs: timeoutManager.getTimeout('taskExecution'), operationTimeouts: { 'decomposition': timeoutManager.getTimeout('taskDecomposition'), 'execution': timeoutManager.getTimeout('taskExecution'), 'validation': timeoutManager.getTimeout('databaseOperations'), 'analysis': timeoutManager.getTimeout('taskRefinement'), 'codemap': timeoutManager.getTimeout('fileOperations'), 'context_enrichment': timeoutManager.getTimeout('taskRefinement') } }, resourceLimits: { maxMemoryMB: 2048, maxCpuWeight: 8, maxDiskSpaceMB: 1024 }, ...this.userConfig }; logger.debug('Job Manager configuration initialized with timeout values'); this.ensureInitialized(); } return this.config; } ensureInitialized() { if (!this.initialized) { this.initialized = true; this.startJobProcessor(); logger.debug('Job Manager Integration Service job processor started'); } } static getInstance(config) { if (!JobManagerIntegrationService.instance) { JobManagerIntegrationService.instance = new JobManagerIntegrationService(config); } return JobManagerIntegrationService.instance; } async createTaskJob(toolName, params, options) { try { const jobId = jobManager.createJob(toolName, params); const taskJob = { ...jobManager.getJob(jobId), taskId: options.taskId, projectId: options.projectId, operationType: options.operationType, priority: options.priority || 'medium', estimatedDuration: options.estimatedDuration, resourceRequirements: options.resourceRequirements || { memoryMB: 256, cpuWeight: 1 }, dependencies: options.dependencies || [], metadata: { retryCount: 0, maxRetries: this.getConfig().retryPolicy.maxRetries, ...options.metadata } }; this.taskJobs.set(jobId, taskJob); this.jobMetrics.set(jobId, { jobId, startTime: Date.now(), resourceUsage: { peakMemoryMB: 0, averageCpuUsage: 0 }, performanceScore: 0, errorCount: 0, retryCount: 0 }); if (taskJob.dependencies && taskJob.dependencies.length > 0) { this.jobQueue.push(taskJob); logger.info({ jobId, dependencies: taskJob.dependencies }, 'Job queued due to dependencies'); } else { this.jobQueue.push(taskJob); logger.info({ jobId, operationType: taskJob.operationType }, 'Job queued for execution'); } this.emit('job_created', taskJob); return jobId; } catch (error) { logger.error({ err: error, toolName, options }, 'Failed to create task job'); throw new Error(`Failed to create task job: ${error instanceof Error ? error.message : String(error)}`); } } getTaskJob(jobId) { return this.taskJobs.get(jobId) || null; } getJobMetrics(jobId) { return this.jobMetrics.get(jobId) || null; } async updateJobProgress(jobId, progress, message, resourceUsage) { try { const updated = jobManager.updateJobStatus(jobId, JobStatus.RUNNING, message, progress); if (!updated) { return false; } const metrics = this.jobMetrics.get(jobId); if (metrics && resourceUsage) { metrics.resourceUsage = { ...metrics.resourceUsage, ...resourceUsage }; } const progressCallbacks = this.progressSubscriptions.get(jobId) || []; progressCallbacks.forEach(callback => { try { callback(jobId, progress, message); } catch (error) { logger.error({ err: error, jobId }, 'Error in progress callback'); } }); this.emit('job_progress', jobId, progress, message); return true; } catch (error) { logger.error({ err: error, jobId }, 'Failed to update job progress'); return false; } } async completeJob(jobId, result, finalMetrics) { try { const success = jobManager.setJobResult(jobId, { isError: false, content: [{ type: 'text', text: JSON.stringify(result) }] }); if (!success) { return false; } const metrics = this.jobMetrics.get(jobId); if (metrics) { metrics.endTime = Date.now(); metrics.duration = metrics.endTime - metrics.startTime; if (finalMetrics) { Object.assign(metrics, finalMetrics); } metrics.performanceScore = this.calculatePerformanceScore(metrics); } this.runningJobs.delete(jobId); const taskJob = this.taskJobs.get(jobId); if (taskJob) { const callbacks = this.jobSubscriptions.get(jobId) || []; callbacks.forEach(callback => { try { callback(taskJob, metrics); } catch (error) { logger.error({ err: error, jobId }, 'Error in job completion callback'); } }); } this.emit('job_completed', jobId, result, metrics); logger.info({ jobId, duration: metrics?.duration }, 'Job completed successfully'); return true; } catch (error) { logger.error({ err: error, jobId }, 'Failed to complete job'); return false; } } async failJob(jobId, error, shouldRetry = true) { try { const taskJob = this.taskJobs.get(jobId); const metrics = this.jobMetrics.get(jobId); if (metrics) { metrics.errorCount++; metrics.endTime = Date.now(); metrics.duration = metrics.endTime - metrics.startTime; } if (shouldRetry && taskJob && metrics) { const maxRetries = taskJob.metadata?.maxRetries || this.getConfig().retryPolicy.maxRetries; const currentRetries = metrics.retryCount; if (currentRetries < maxRetries) { metrics.retryCount++; const delay = this.getConfig().retryPolicy.initialDelayMs * Math.pow(this.getConfig().retryPolicy.backoffMultiplier, currentRetries); logger.info({ jobId, retryCount: metrics.retryCount, delay }, 'Scheduling job retry'); setTimeout(() => { this.jobQueue.unshift(taskJob); }, delay); return true; } } const success = jobManager.setJobResult(jobId, { isError: true, content: [{ type: 'text', text: error.message }] }); this.runningJobs.delete(jobId); this.emit('job_failed', jobId, error, metrics); logger.error({ err: error, jobId, retryCount: metrics?.retryCount }, 'Job failed permanently'); return success; } catch (err) { logger.error({ err, jobId }, 'Failed to handle job failure'); return false; } } async cancelJob(jobId, reason) { try { const taskJob = this.taskJobs.get(jobId); if (!taskJob) { logger.warn({ jobId }, 'Attempted to cancel non-existent task job'); return false; } const success = jobManager.setJobResult(jobId, { isError: true, content: [{ type: 'text', text: reason || 'Job cancelled by user' }] }); if (success) { this.runningJobs.delete(jobId); const queueIndex = this.jobQueue.findIndex(job => job.id === jobId); if (queueIndex !== -1) { this.jobQueue.splice(queueIndex, 1); } const metrics = this.jobMetrics.get(jobId); if (metrics) { metrics.endTime = Date.now(); metrics.duration = metrics.endTime - metrics.startTime; } this.emit('job_cancelled', jobId, reason, metrics); logger.info({ jobId, reason }, 'Job cancelled successfully'); } return success; } catch (error) { logger.error({ err: error, jobId }, 'Failed to cancel job'); return false; } } getQueueStatus() { const jobsByPriority = {}; const jobsByOperation = {}; this.jobQueue.forEach(job => { jobsByPriority[job.priority] = (jobsByPriority[job.priority] || 0) + 1; jobsByOperation[job.operationType] = (jobsByOperation[job.operationType] || 0) + 1; }); const now = Date.now(); const waitTimes = this.jobQueue.map(job => now - job.createdAt); const averageWaitTime = waitTimes.length > 0 ? waitTimes.reduce((sum, time) => sum + time, 0) / waitTimes.length : 0; return { queueLength: this.jobQueue.length, runningJobs: this.runningJobs.size, totalJobs: this.taskJobs.size, jobsByPriority, jobsByOperation, averageWaitTime }; } getJobStatistics() { const allJobs = Array.from(this.taskJobs.values()); const allMetrics = Array.from(this.jobMetrics.values()); const completedJobs = allJobs.filter(job => job.status === JobStatus.COMPLETED).length; const failedJobs = allJobs.filter(job => job.status === JobStatus.FAILED).length; const runningJobs = allJobs.filter(job => job.status === JobStatus.RUNNING).length; const queuedJobs = allJobs.filter(job => job.status === JobStatus.PENDING).length; const completedMetrics = allMetrics.filter(m => m.duration !== undefined); const averageExecutionTime = completedMetrics.length > 0 ? completedMetrics.reduce((sum, m) => sum + (m.duration || 0), 0) / completedMetrics.length : 0; const averagePerformanceScore = allMetrics.length > 0 ? allMetrics.reduce((sum, m) => sum + m.performanceScore, 0) / allMetrics.length : 0; const averageMemoryMB = allMetrics.length > 0 ? allMetrics.reduce((sum, m) => sum + m.resourceUsage.peakMemoryMB, 0) / allMetrics.length : 0; const averageCpuUsage = allMetrics.length > 0 ? allMetrics.reduce((sum, m) => sum + m.resourceUsage.averageCpuUsage, 0) / allMetrics.length : 0; const peakMemoryMB = Math.max(...allMetrics.map(m => m.resourceUsage.peakMemoryMB), 0); const operationStats = {}; allJobs.forEach(job => { const opType = job.operationType; if (!operationStats[opType]) { operationStats[opType] = { count: 0, averageTime: 0, successRate: 0 }; } operationStats[opType].count++; }); Object.keys(operationStats).forEach(opType => { const jobsOfType = allJobs.filter(job => job.operationType === opType); const metricsOfType = jobsOfType .map(job => this.jobMetrics.get(job.id)) .filter(m => m && m.duration !== undefined); const completedOfType = jobsOfType.filter(job => job.status === JobStatus.COMPLETED).length; const totalOfType = jobsOfType.length; operationStats[opType].averageTime = metricsOfType.length > 0 ? metricsOfType.reduce((sum, m) => sum + (m.duration || 0), 0) / metricsOfType.length : 0; operationStats[opType].successRate = totalOfType > 0 ? completedOfType / totalOfType : 0; }); return { totalJobs: allJobs.length, completedJobs, failedJobs, runningJobs, queuedJobs, averageExecutionTime, averagePerformanceScore, resourceUtilization: { averageMemoryMB, averageCpuUsage, peakMemoryMB }, operationStats }; } subscribeToJob(jobId, callback) { if (!this.jobSubscriptions.has(jobId)) { this.jobSubscriptions.set(jobId, []); } this.jobSubscriptions.get(jobId).push(callback); return () => { const callbacks = this.jobSubscriptions.get(jobId); if (callbacks) { const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } } }; } subscribeToJobProgress(jobId, callback) { if (!this.progressSubscriptions.has(jobId)) { this.progressSubscriptions.set(jobId, []); } this.progressSubscriptions.get(jobId).push(callback); return () => { const callbacks = this.progressSubscriptions.get(jobId); if (callbacks) { const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } } }; } updateConfig(newConfig) { const currentConfig = this.getConfig(); this.config = { ...currentConfig, ...newConfig }; logger.info({ config: this.config }, 'Job manager configuration updated'); this.emit('config_updated', this.config); } cleanupOldJobs(maxAgeMs = 24 * 60 * 60 * 1000) { const now = Date.now(); let cleanedCount = 0; for (const [jobId, job] of this.taskJobs.entries()) { if ((job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) && (now - job.updatedAt) > maxAgeMs) { this.taskJobs.delete(jobId); this.jobMetrics.delete(jobId); this.jobSubscriptions.delete(jobId); this.progressSubscriptions.delete(jobId); cleanedCount++; } } if (cleanedCount > 0) { logger.info({ cleanedCount, maxAgeMs }, 'Cleaned up old jobs'); this.emit('jobs_cleaned', cleanedCount); } return cleanedCount; } dispose() { if (this.processingInterval) { clearInterval(this.processingInterval); this.processingInterval = undefined; } this.taskJobs.clear(); this.jobMetrics.clear(); this.jobQueue.length = 0; this.runningJobs.clear(); this.jobSubscriptions.clear(); this.progressSubscriptions.clear(); this.removeAllListeners(); logger.info('Job Manager Integration Service disposed'); } startJobProcessor() { this.processingInterval = setInterval(() => { this.processJobQueue().catch(error => { logger.error({ err: error }, 'Error in job processing loop'); }); }, 1000); logger.debug('Job processor started'); } async processJobQueue() { if (this.jobQueue.length === 0 || this.runningJobs.size >= this.getConfig().maxConcurrentJobs) { return; } this.jobQueue.sort((a, b) => { const priorityDiff = this.getConfig().priorityWeights[b.priority] - this.getConfig().priorityWeights[a.priority]; if (priorityDiff !== 0) return priorityDiff; return a.createdAt - b.createdAt; }); const readyJobs = this.jobQueue.filter(job => this.areDependenciesSatisfied(job)); const jobsToStart = readyJobs.slice(0, this.getConfig().maxConcurrentJobs - this.runningJobs.size); for (const job of jobsToStart) { await this.startJob(job); } } areDependenciesSatisfied(job) { if (!job.dependencies || job.dependencies.length === 0) { return true; } return job.dependencies.every(depJobId => { const depJob = this.taskJobs.get(depJobId); return depJob && depJob.status === JobStatus.COMPLETED; }); } async startJob(job) { try { const queueIndex = this.jobQueue.findIndex(queuedJob => queuedJob.id === job.id); if (queueIndex !== -1) { this.jobQueue.splice(queueIndex, 1); } this.runningJobs.add(job.id); jobManager.updateJobStatus(job.id, JobStatus.RUNNING, 'Job started'); const metrics = this.jobMetrics.get(job.id); if (metrics) { metrics.startTime = Date.now(); } const timeoutMs = this.getConfig().timeoutPolicy.operationTimeouts[job.operationType] || this.getConfig().timeoutPolicy.defaultTimeoutMs; setTimeout(() => { if (this.runningJobs.has(job.id)) { this.handleJobTimeout(job.id); } }, timeoutMs); this.emit('job_started', job); logger.info({ jobId: job.id, operationType: job.operationType }, 'Job started'); } catch (error) { logger.error({ err: error, jobId: job.id }, 'Failed to start job'); await this.failJob(job.id, error instanceof Error ? error : new Error(String(error)), false); } } async handleJobTimeout(jobId) { const job = this.taskJobs.get(jobId); if (!job) return; logger.warn({ jobId, operationType: job.operationType }, 'Job timed out'); const metrics = this.jobMetrics.get(jobId); if (metrics) { metrics.errorCount++; } await this.failJob(jobId, new Error('Job execution timed out'), true); } calculatePerformanceScore(metrics) { let score = 100; score -= metrics.errorCount * 10; score -= metrics.retryCount * 5; if (metrics.resourceUsage.peakMemoryMB < 512) { score += 5; } if (metrics.resourceUsage.averageCpuUsage < 50) { score += 5; } return Math.max(0, Math.min(100, score)); } } export const jobManagerIntegration = JobManagerIntegrationService.getInstance(); export function getJobManagerIntegration(config) { return JobManagerIntegrationService.getInstance(config); }