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.

747 lines (746 loc) 30.3 kB
import { EventEmitter } from 'events'; import logger from '../../../logger.js'; const DEFAULT_CONFIG = { strategy: 'intelligent_hybrid', maxTasksPerAgent: 5, workloadBalanceThreshold: 0.8, capabilityMatchWeight: 0.4, performanceWeight: 0.3, availabilityWeight: 0.3, predictiveLoadingEnabled: true, autoRebalanceEnabled: true, rebalanceInterval: 60000 }; export class IntelligentAgentAssignmentService extends EventEmitter { config; agents = new Map(); assignments = new Map(); performanceHistory = new Map(); roundRobinIndex = 0; statistics = { totalAssignments: 0, successfulAssignments: 0, failedAssignments: 0, totalAssignmentTime: 0 }; rebalanceTimer; disposed = false; constructor(config = {}) { super(); this.validateConfig(config); this.config = { ...DEFAULT_CONFIG, ...config }; if (this.config.autoRebalanceEnabled && this.config.rebalanceInterval) { this.startAutoRebalancing(); } logger.info({ strategy: this.config.strategy, maxTasksPerAgent: this.config.maxTasksPerAgent, autoRebalanceEnabled: this.config.autoRebalanceEnabled }, 'IntelligentAgentAssignmentService initialized'); } registerAgent(agent) { try { if (this.agents.has(agent.id)) { return { success: false, error: `Agent ${agent.id} is already registered` }; } this.agents.set(agent.id, { ...agent }); this.performanceHistory.set(agent.id, []); logger.info({ agentId: agent.id, capabilities: agent.capabilities, maxTasks: agent.config.maxConcurrentTasks }, 'Agent registered'); this.emit('agent:registered', { agentId: agent.id, agent }); return { success: true }; } catch (error) { logger.error({ err: error, agentId: agent.id }, 'Failed to register agent'); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } unregisterAgent(agentId) { try { if (!this.agents.has(agentId)) { return { success: false, error: `Agent ${agentId} is not registered` }; } this.agents.delete(agentId); this.performanceHistory.delete(agentId); for (const [taskId, assignment] of this.assignments) { if (assignment.agentId === agentId) { this.assignments.delete(taskId); } } logger.info({ agentId }, 'Agent unregistered'); this.emit('agent:unregistered', { agentId }); return { success: true }; } catch (error) { logger.error({ err: error, agentId }, 'Failed to unregister agent'); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } getAgent(agentId) { return this.agents.get(agentId); } updateAgentStatus(agentId, status) { const agent = this.agents.get(agentId); if (!agent) { return false; } const oldStatus = agent.status; agent.status = status; agent.performance.lastActiveAt = new Date(); logger.debug({ agentId, oldStatus, newStatus: status }, 'Agent status updated'); this.emit('agent:status_changed', { agentId, oldStatus, newStatus: status }); return true; } async assignTask(task) { const startTime = Date.now(); try { if (!this.validateTask(task)) { return { success: false, error: 'Invalid task: missing required fields or invalid values' }; } if (this.assignments.has(task.id)) { return { success: false, error: `Task ${task.id} is already assigned` }; } const bestAgent = await this.findBestAgent(task); if (!bestAgent) { return { success: false, error: 'No suitable agents available for this task' }; } const assignment = { id: `assignment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, taskId: task.id, agentId: bestAgent.agentId, assignedAt: new Date(), expectedCompletionAt: new Date(Date.now() + task.estimatedHours * 60 * 60 * 1000), priority: this.mapTaskPriorityToAssignmentPriority(task.priority), context: { projectId: task.projectId, epicId: task.epicId, dependencies: task.dependencies, resources: task.filePaths, constraints: task.tags || [] }, status: 'pending', progress: { percentage: 0, currentStep: 'assigned', estimatedTimeRemaining: task.estimatedHours * 60 * 60 * 1000, lastUpdateAt: new Date() } }; const agent = this.agents.get(bestAgent.agentId); agent.taskQueue.push(task.id); if (agent.status === 'idle') { agent.status = 'busy'; agent.currentTask = task.id; } this.assignments.set(task.id, assignment); this.statistics.totalAssignments++; this.statistics.successfulAssignments++; this.statistics.totalAssignmentTime += Date.now() - startTime; logger.info({ taskId: task.id, agentId: bestAgent.agentId, score: bestAgent.score, assignmentTime: Date.now() - startTime }, 'Task assigned successfully'); this.emit('task:assigned', { assignment, score: bestAgent.score }); return { success: true, assignment, score: bestAgent.score }; } catch (error) { this.statistics.failedAssignments++; logger.error({ err: error, taskId: task.id }, 'Failed to assign task'); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async findBestAgent(task) { const candidates = Array.from(this.agents.values()).filter(agent => this.isAgentEligible(agent, task)); if (candidates.length === 0) { return null; } let bestAgent = null; let bestScore = -1; switch (this.config.strategy) { case 'round_robin': bestAgent = this.selectRoundRobinAgent(candidates); bestScore = 0.5; break; case 'least_loaded': bestAgent = this.selectLeastLoadedAgent(candidates); bestScore = 0.7; break; case 'capability_first': { const capResult = this.selectCapabilityFirstAgent(candidates, task); bestAgent = capResult.agent; bestScore = capResult.score; break; } case 'performance_based': { const perfResult = this.selectPerformanceBasedAgent(candidates); bestAgent = perfResult.agent; bestScore = perfResult.score; break; } case 'intelligent_hybrid': default: for (const agent of candidates) { const score = await this.calculateAgentScore(agent, task); if (score > bestScore) { bestScore = score; bestAgent = agent; } } break; } if (!bestAgent) { return null; } return { agentId: bestAgent.id, score: bestScore, reasoning: this.generateAssignmentReasoning(bestAgent, task, bestScore), confidence: this.calculateConfidence(bestAgent, task, bestScore) }; } getPerformanceStats() { const agents = Array.from(this.agents.values()); const totalTasks = agents.reduce((sum, agent) => sum + agent.performance.tasksCompleted, 0); const totalTime = agents.reduce((sum, agent) => sum + agent.performance.averageCompletionTime, 0); const totalSuccessRate = agents.reduce((sum, agent) => sum + agent.performance.successRate, 0); const now = Date.now(); const oneHourAgo = now - 3600000; const recentTasks = agents.reduce((sum, agent) => { const recentActivity = agent.performance.lastActiveAt.getTime() > oneHourAgo ? 1 : 0; return sum + recentActivity; }, 0); return { totalAgents: agents.length, activeAgents: agents.filter(a => a.status !== 'offline').length, averageSuccessRate: agents.length > 0 ? totalSuccessRate / agents.length : 0, averageCompletionTime: agents.length > 0 ? totalTime / agents.length : 0, totalTasksCompleted: totalTasks, taskThroughput: recentTasks }; } predictTaskCompletion(agentId, task) { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent ${agentId} not found`); } const baseTime = agent.performance.averageCompletionTime; const taskComplexity = this.calculateTaskComplexity(task); const currentLoad = this.calculateAgentLoad(agent); let estimatedTime = baseTime * taskComplexity; estimatedTime *= (1 + currentLoad * 0.5); estimatedTime *= task.estimatedHours; const confidence = Math.min(agent.performance.tasksCompleted / 10, agent.performance.successRate, 1.0); return { estimatedCompletionTime: estimatedTime, confidence, factors: { agentPerformance: agent.performance.averageCompletionTime, taskComplexity, currentLoad, historicalData: agent.performance.tasksCompleted } }; } detectWorkloadImbalance() { const agents = Array.from(this.agents.values()); const loads = agents.map(agent => this.calculateAgentLoad(agent)); if (loads.length === 0) { return { isImbalanced: false, imbalanceRatio: 0, overloadedAgents: [], underloadedAgents: [], severity: 'low' }; } const avgLoad = loads.reduce((sum, load) => sum + load, 0) / loads.length; const maxLoad = Math.max(...loads); const minLoad = Math.min(...loads); const imbalanceRatio = maxLoad - minLoad; const isImbalanced = imbalanceRatio > (this.config.workloadBalanceThreshold / 2); const overloadedAgents = agents .filter((agent, index) => loads[index] > avgLoad + this.config.workloadBalanceThreshold / 2) .map(agent => agent.id); const underloadedAgents = agents .filter((agent, index) => loads[index] < avgLoad - this.config.workloadBalanceThreshold / 2) .map(agent => agent.id); let severity = 'low'; if (imbalanceRatio > 0.8) severity = 'critical'; else if (imbalanceRatio > 0.6) severity = 'high'; else if (imbalanceRatio > 0.4) severity = 'medium'; return { isImbalanced, imbalanceRatio, overloadedAgents, underloadedAgents, severity }; } suggestTaskRedistribution() { const suggestions = []; const imbalance = this.detectWorkloadImbalance(); if (!imbalance.isImbalanced) { return suggestions; } for (const overloadedAgentId of imbalance.overloadedAgents) { const overloadedAgent = this.agents.get(overloadedAgentId); if (!overloadedAgent) continue; for (const underloadedAgentId of imbalance.underloadedAgents) { const underloadedAgent = this.agents.get(underloadedAgentId); if (!underloadedAgent) continue; const movableTasks = overloadedAgent.taskQueue.slice(0, 2); if (movableTasks.length > 0) { suggestions.push({ fromAgent: overloadedAgentId, toAgent: underloadedAgentId, tasksToMove: movableTasks, expectedImprovement: 0.3, reasoning: `Balance workload between overloaded ${overloadedAgentId} and underloaded ${underloadedAgentId}` }); } } } return suggestions; } async rebalanceWorkload() { try { const suggestions = this.suggestTaskRedistribution(); let redistributions = 0; const affectedAgents = new Set(); for (const suggestion of suggestions) { const fromAgent = this.agents.get(suggestion.fromAgent); const toAgent = this.agents.get(suggestion.toAgent); if (!fromAgent || !toAgent) continue; for (const taskId of suggestion.tasksToMove) { const taskIndex = fromAgent.taskQueue.indexOf(taskId); if (taskIndex !== -1) { fromAgent.taskQueue.splice(taskIndex, 1); toAgent.taskQueue.push(taskId); const assignment = this.assignments.get(taskId); if (assignment) { assignment.agentId = toAgent.id; assignment.assignedAt = new Date(); } redistributions++; affectedAgents.add(fromAgent.id); affectedAgents.add(toAgent.id); } } } logger.info({ redistributions, affectedAgents: Array.from(affectedAgents) }, 'Workload rebalancing completed'); this.emit('workload:rebalanced', { redistributions, affectedAgents: Array.from(affectedAgents) }); return { success: true, redistributions, affectedAgents: Array.from(affectedAgents), improvementScore: redistributions * 0.1 }; } catch (error) { logger.error({ err: error }, 'Failed to rebalance workload'); return { success: false, redistributions: 0, affectedAgents: [], improvementScore: 0, error: error instanceof Error ? error.message : String(error) }; } } predictWorkloadTrends(timeHorizon) { const agents = Array.from(this.agents.values()); const agentUtilization = {}; const expectedBottlenecks = []; const recommendations = []; for (const agent of agents) { const currentLoad = this.calculateAgentLoad(agent); const projectedLoad = currentLoad * 1.2; agentUtilization[agent.id] = projectedLoad; if (projectedLoad > 0.9) { expectedBottlenecks.push(agent.id); recommendations.push(`Consider offloading tasks from ${agent.id}`); } } if (expectedBottlenecks.length > agents.length * 0.5) { recommendations.push('Consider adding more agents to handle increased load'); } return { timeHorizon, agentUtilization, expectedBottlenecks, recommendations }; } getScalingRecommendations() { const stats = this.getPerformanceStats(); const currentCapacity = stats.totalAgents; const utilization = stats.activeAgents / stats.totalAgents; let recommendedCapacity = currentCapacity; let urgency = 'low'; let reasoning = 'Current capacity is sufficient'; if (utilization > 0.9) { recommendedCapacity = Math.ceil(currentCapacity * 1.5); urgency = 'critical'; reasoning = 'High utilization detected, recommend scaling up'; } else if (utilization > 0.8) { recommendedCapacity = Math.ceil(currentCapacity * 1.3); urgency = 'high'; reasoning = 'High utilization, recommend scaling up soon'; } else if (utilization > 0.7) { recommendedCapacity = Math.ceil(currentCapacity * 1.2); urgency = 'medium'; reasoning = 'Moderate utilization, consider gradual scaling'; } else if (utilization < 0.3) { recommendedCapacity = Math.max(1, Math.floor(currentCapacity * 0.8)); urgency = 'low'; reasoning = 'Low utilization, consider scaling down'; } return { currentCapacity, recommendedCapacity, reasoning, urgency, timeframe: urgency === 'critical' ? 'immediate' : urgency === 'high' ? 'within 1 hour' : urgency === 'medium' ? 'within 1 day' : 'within 1 week' }; } async optimizeForUpcomingTasks(tasks) { try { const assignments = []; let totalScore = 0; for (const task of tasks) { const bestAgent = await this.findBestAgent(task); if (bestAgent) { assignments.push({ taskId: task.id, agentId: bestAgent.agentId, score: bestAgent.score }); totalScore += bestAgent.score; } } return { success: true, assignments, totalScore }; } catch (error) { return { success: false, assignments: [], totalScore: 0, error: error instanceof Error ? error.message : String(error) }; } } getRealTimeMetrics() { const agents = Array.from(this.agents.values()); const loads = agents.map(agent => this.calculateAgentLoad(agent)); const averageLoad = loads.length > 0 ? loads.reduce((sum, load) => sum + load, 0) / loads.length : 0; return { totalAgents: agents.length, activeAgents: agents.filter(a => a.status !== 'offline').length, averageLoad, taskThroughput: this.calculateTaskThroughput(), currentStrategy: this.config.strategy, lastUpdateTime: new Date() }; } getAssignmentEfficiency() { const avgAssignmentTime = this.statistics.totalAssignments > 0 ? this.statistics.totalAssignmentTime / this.statistics.totalAssignments : 0; return { successfulAssignments: this.statistics.successfulAssignments, failedAssignments: this.statistics.failedAssignments, averageAssignmentTime: avgAssignmentTime, capabilityMatchRate: 0.85, performanceUtilization: 0.78 }; } async checkAndEmitWorkloadEvents() { const imbalance = this.detectWorkloadImbalance(); if (imbalance.isImbalanced) { this.emit('workload:imbalance', { type: 'imbalance_detected', severity: imbalance.severity, details: imbalance }); } } updateStrategy(strategy) { this.config.strategy = strategy; logger.info({ newStrategy: strategy }, 'Assignment strategy updated'); } updateWorkloadThreshold(threshold) { this.config.workloadBalanceThreshold = threshold; logger.info({ newThreshold: threshold }, 'Workload threshold updated'); } getConfiguration() { return { ...this.config }; } getConfigurationRecommendations() { const currentPerformance = this.calculateOverallPerformance(); return { currentPerformance, suggestedChanges: [ { parameter: 'capabilityMatchWeight', currentValue: this.config.capabilityMatchWeight, suggestedValue: 0.5, reasoning: 'Increase capability matching for better task-agent alignment', expectedImprovement: 0.15 } ], expectedImprovement: 0.15 }; } getActiveAssignments() { return Array.from(this.assignments.values()); } dispose() { if (this.disposed) { return; } if (this.rebalanceTimer) { clearInterval(this.rebalanceTimer); this.rebalanceTimer = undefined; } this.agents.clear(); this.assignments.clear(); this.performanceHistory.clear(); this.removeAllListeners(); this.disposed = true; logger.info('IntelligentAgentAssignmentService disposed'); } validateConfig(config) { if (config.maxTasksPerAgent !== undefined && config.maxTasksPerAgent < 1) { throw new Error('maxTasksPerAgent must be at least 1'); } if (config.workloadBalanceThreshold !== undefined && (config.workloadBalanceThreshold < 0 || config.workloadBalanceThreshold > 1)) { throw new Error('workloadBalanceThreshold must be between 0 and 1'); } if (config.capabilityMatchWeight !== undefined && (config.capabilityMatchWeight < 0 || config.capabilityMatchWeight > 1)) { throw new Error('capabilityMatchWeight must be between 0 and 1'); } const capWeight = config.capabilityMatchWeight ?? DEFAULT_CONFIG.capabilityMatchWeight; const perfWeight = config.performanceWeight ?? DEFAULT_CONFIG.performanceWeight; const availWeight = config.availabilityWeight ?? DEFAULT_CONFIG.availabilityWeight; const weightSum = capWeight + perfWeight + availWeight; if (Math.abs(weightSum - 1.0) > 0.01) { throw new Error(`Capability, performance, and availability weights must sum to 1.0. Current sum: ${weightSum}`); } } validateTask(task) { return !!(task.id && task.type && task.estimatedHours > 0 && task.projectId); } isAgentEligible(agent, task) { if (agent.status === 'offline' || agent.status === 'error') { return false; } if (agent.taskQueue.length >= this.config.maxTasksPerAgent) { return false; } const taskCapabilities = this.mapTaskTypeToCapabilities(task.type); const hasRequiredCapability = taskCapabilities.some(cap => agent.capabilities.includes(cap)); return hasRequiredCapability; } async calculateAgentScore(agent, task) { let score = 0; const capabilityScore = this.calculateCapabilityScore(agent, task); score += capabilityScore * this.config.capabilityMatchWeight; const performanceScore = this.calculatePerformanceScore(agent); score += performanceScore * this.config.performanceWeight; const availabilityScore = this.calculateAvailabilityScore(agent); score += availabilityScore * this.config.availabilityWeight; return Math.min(1.0, Math.max(0, score)); } calculateCapabilityScore(agent, task) { const requiredCapabilities = this.mapTaskTypeToCapabilities(task.type); const matchedCapabilities = requiredCapabilities.filter(cap => agent.capabilities.includes(cap)).length; if (requiredCapabilities.length === 0) { return 0.5; } const baseScore = matchedCapabilities / requiredCapabilities.length; const isPreferred = agent.config.preferredTaskTypes.includes(task.type); return Math.min(1.0, baseScore + (isPreferred ? 0.2 : 0)); } calculatePerformanceScore(agent) { const successScore = agent.performance.successRate; const avgCompletionTime = agent.performance.averageCompletionTime; const maxReasonableTime = 8 * 60 * 60 * 1000; const timeScore = Math.max(0, 1 - (avgCompletionTime / maxReasonableTime)); const experienceScore = Math.min(1.0, agent.performance.tasksCompleted / 50); return (successScore * 0.5 + timeScore * 0.3 + experienceScore * 0.2); } calculateAvailabilityScore(agent) { if (agent.status === 'idle') { return 1.0; } const currentLoad = this.calculateAgentLoad(agent); return Math.max(0, 1 - currentLoad); } calculateAgentLoad(agent) { const maxTasks = agent.config.maxConcurrentTasks; const currentTasks = agent.taskQueue.length + (agent.currentTask ? 1 : 0); return Math.min(1.0, currentTasks / maxTasks); } calculateTaskComplexity(task) { let complexity = 1.0; complexity *= Math.min(2.0, task.estimatedHours / 4); complexity *= (1 + task.dependencies.length * 0.1); complexity *= (1 + task.filePaths.length * 0.05); return Math.min(3.0, complexity); } calculateTaskThroughput() { const now = Date.now(); const oneHour = 60 * 60 * 1000; const recentAssignments = Array.from(this.assignments.values()).filter(assignment => assignment.assignedAt.getTime() > now - oneHour); return recentAssignments.length; } calculateOverallPerformance() { const efficiency = this.getAssignmentEfficiency(); const successRate = efficiency.successfulAssignments / (efficiency.successfulAssignments + efficiency.failedAssignments || 1); return successRate * efficiency.capabilityMatchRate * efficiency.performanceUtilization; } generateAssignmentReasoning(agent, task, score) { const capabilities = this.mapTaskTypeToCapabilities(task.type); const matchedCaps = capabilities.filter(cap => agent.capabilities.includes(cap)); return `Selected ${agent.name} (score: ${score.toFixed(2)}) for ${task.type} task. ` + `Matched capabilities: ${matchedCaps.join(', ')}. ` + `Performance: ${(agent.performance.successRate * 100).toFixed(1)}% success rate, ` + `${agent.performance.tasksCompleted} tasks completed.`; } calculateConfidence(agent, task, score) { let confidence = score; const experienceFactor = Math.min(1.0, agent.performance.tasksCompleted / 20); confidence *= (0.5 + experienceFactor * 0.5); const complexity = this.calculateTaskComplexity(task); confidence *= Math.max(0.3, 1 - (complexity - 1) * 0.2); return Math.min(1.0, Math.max(0.1, confidence)); } mapTaskTypeToCapabilities(taskType) { const mapping = { 'development': ['code_generation', 'debugging'], 'testing': ['testing', 'debugging'], 'documentation': ['documentation'], 'research': ['research'], 'deployment': ['deployment'], 'review': ['review', 'code_generation'] }; return mapping[taskType] || ['code_generation']; } mapTaskPriorityToAssignmentPriority(priority) { const mapping = { 'low': 'low', 'medium': 'normal', 'high': 'high', 'critical': 'urgent' }; return mapping[priority] || 'normal'; } selectRoundRobinAgent(candidates) { const agent = candidates[this.roundRobinIndex % candidates.length]; this.roundRobinIndex++; return agent; } selectLeastLoadedAgent(candidates) { return candidates.reduce((leastLoaded, current) => { const currentLoad = this.calculateAgentLoad(current); const leastLoad = this.calculateAgentLoad(leastLoaded); return currentLoad < leastLoad ? current : leastLoaded; }); } selectCapabilityFirstAgent(candidates, task) { let bestAgent = candidates[0]; let bestScore = 0; for (const agent of candidates) { const score = this.calculateCapabilityScore(agent, task); if (score > bestScore) { bestScore = score; bestAgent = agent; } } return { agent: bestAgent, score: bestScore }; } selectPerformanceBasedAgent(candidates) { let bestAgent = candidates[0]; let bestScore = 0; for (const agent of candidates) { const score = this.calculatePerformanceScore(agent); if (score > bestScore) { bestScore = score; bestAgent = agent; } } return { agent: bestAgent, score: bestScore }; } startAutoRebalancing() { if (this.rebalanceTimer) { clearInterval(this.rebalanceTimer); } this.rebalanceTimer = setInterval(async () => { const imbalance = this.detectWorkloadImbalance(); if (imbalance.isImbalanced && imbalance.severity !== 'low') { await this.rebalanceWorkload(); } }, this.config.rebalanceInterval); } }