UNPKG

abyss-ai

Version:

Autonomous AI coding agent - enhanced OpenCode with autonomous capabilities

923 lines (755 loc) 31.3 kB
import { Log } from "../../util/log" import { IAgent, AgentTask, AgentResult, SharedContext, AgentType, ReasoningMode } from "../types/agent" import { QuestionGenerator, GeneratedQuestion } from "../question-generation/question-generator" export class AgentCoordinator { private log = Log.create({ service: "agent-coordinator" }) private agents: Map<string, IAgent> = new Map() private taskQueue: AgentTask[] = [] private activeTasks: Map<string, AgentTask> = new Map() private taskHistory: Map<string, AgentResult[]> = new Map() private questionGenerator: QuestionGenerator // Performance monitoring private performanceMetrics = { totalTasksProcessed: 0, averageProcessingTime: 0, successRate: 0, agentUtilization: new Map<string, number>() } constructor() { this.questionGenerator = new QuestionGenerator() this.log.info("AgentCoordinator initialized with dynamic question generation") } // Initialize specialized agents async initializeAgents(): Promise<void> { this.log.info("Initializing specialized agents") try { // We'll register agents as they're implemented // For now, just log that the coordinator is ready this.log.info("Agent coordinator ready for agent registration") } catch (error) { this.log.error("Failed to initialize agents", { error }) throw error } } // Register an agent with the coordinator registerAgent(agent: IAgent): void { this.log.info("Registering agent", { agentId: agent.id, agentType: agent.type, capabilities: agent.capabilities }) this.agents.set(agent.id, agent) this.performanceMetrics.agentUtilization.set(agent.id, 0) } // Unregister an agent async unregisterAgent(agentId: string): Promise<void> { const agent = this.agents.get(agentId) if (agent) { await agent.dispose() this.agents.delete(agentId) this.performanceMetrics.agentUtilization.delete(agentId) this.log.info("Agent unregistered", { agentId }) } } // Get list of registered agents getRegisteredAgents(): { id: string; type: AgentType; capabilities: string[] }[] { return Array.from(this.agents.values()).map(agent => ({ id: agent.id, type: agent.type, capabilities: agent.capabilities })) } // Main task processing method with enhanced question-driven approach async processTask(task: AgentTask): Promise<AgentResult[]> { this.log.info("Processing task with question-driven approach", { taskId: task.id, taskType: task.type, reasoningModes: task.reasoningModes }) const startTime = Date.now() try { // Add to active tasks this.activeTasks.set(task.id, task) // Generate specialized questions for this task (make-it-heavy approach) const questionResult = await this.questionGenerator.generateQuestions(task) this.log.debug("Generated specialized questions", { taskId: task.id, questionCount: questionResult.questions.length, perspectives: questionResult.questions.map(q => q.perspective), complexity: questionResult.context.complexity }) // Process using question-driven agent selection const results = await this.processWithQuestionDrivenApproach(task, questionResult.questions) // Perform collaborative analysis with question context await this.collaborativeAnalysisWithQuestions(results, task.context, questionResult.questions) // Store results in history this.taskHistory.set(task.id, results) // Update performance metrics this.updatePerformanceMetrics(results, Date.now() - startTime) this.log.info("Question-driven task completed successfully", { taskId: task.id, resultCount: results.length, questionCount: questionResult.questions.length, processingTime: Date.now() - startTime }) return results } catch (error) { this.log.error("Question-driven task processing failed", { taskId: task.id, error }) throw error } finally { // Remove from active tasks this.activeTasks.delete(task.id) } } // Legacy method for backward compatibility async processTaskLegacy(task: AgentTask): Promise<AgentResult[]> { this.log.info("Processing task (legacy mode)", { taskId: task.id, taskType: task.type, reasoningModes: task.reasoningModes }) const startTime = Date.now() try { // Add to active tasks this.activeTasks.set(task.id, task) // Select appropriate agents for the task const selectedAgents = this.selectAgentsForTask(task) if (selectedAgents.length === 0) { throw new Error(`No suitable agents found for task type: ${task.type}`) } this.log.debug("Selected agents for task", { taskId: task.id, agentIds: selectedAgents.map(a => a.id) }) // Process task with selected agents in parallel const results = await this.processWithAgents(selectedAgents, task) // Perform collaborative analysis await this.collaborativeAnalysis(selectedAgents, results, task.context) // Store results in history this.taskHistory.set(task.id, results) // Update performance metrics this.updatePerformanceMetrics(results, Date.now() - startTime) this.log.info("Task completed successfully", { taskId: task.id, resultCount: results.length, processingTime: Date.now() - startTime }) return results } catch (error) { this.log.error("Task processing failed", { taskId: task.id, error }) throw error } finally { // Remove from active tasks this.activeTasks.delete(task.id) } } // Question-driven processing method inspired by make-it-heavy private async processWithQuestionDrivenApproach( task: AgentTask, questions: GeneratedQuestion[] ): Promise<AgentResult[]> { this.log.debug("Processing with question-driven approach", { taskId: task.id, questionCount: questions.length }) const results: AgentResult[] = [] // Process questions in parallel (like make-it-heavy's 4 agents approach) const questionProcessingPromises = questions.map(async (question) => { try { // Find the best agent for this specific question const agent = this.selectAgentForQuestion(question) if (!agent) { this.log.warn("No suitable agent found for question", { questionId: question.id, perspective: question.perspective }) return null } this.log.debug("Processing question with agent", { questionId: question.id, agentId: agent.id, perspective: question.perspective }) // Create a specialized task for this question const questionTask: AgentTask = { ...task, id: `${task.id}-q-${question.id}`, type: `question-driven-${task.type}`, reasoningModes: [question.reasoningMode], context: { ...task.context, generatedQuestion: question.question, questionPerspective: question.perspective, originalTaskId: task.id } } // Process the question with the selected agent const result = await agent.process(questionTask) // Enhance result with question context const enhancedResult = { ...result, metadata: { ...result.metadata, questionId: question.id, questionPerspective: question.perspective, originalQuestion: question.question } } return enhancedResult } catch (error) { this.log.error("Question processing failed", { questionId: question.id, error }) return null } }) // Wait for all questions to be processed const questionResults = await Promise.all(questionProcessingPromises) // Filter out null results and add to main results results.push(...questionResults.filter(result => result !== null) as AgentResult[]) this.log.info("Question-driven processing completed", { taskId: task.id, questionsProcessed: questions.length, successfulResults: results.length }) return results } // Select the best agent for a specific generated question private selectAgentForQuestion(question: GeneratedQuestion): IAgent | null { // First, try to find an agent of the target type const targetTypeAgents = Array.from(this.agents.values()) .filter(agent => agent.type === question.targetAgentType) if (targetTypeAgents.length > 0) { // Find the best match based on capabilities and reasoning mode const bestMatch = targetTypeAgents.find(agent => agent.reasoningModes.includes(question.reasoningMode) && this.agentHasCapabilities(agent, question.expectedCapabilities) ) if (bestMatch) return bestMatch // Fallback to any agent of target type with compatible reasoning mode const reasoningMatch = targetTypeAgents.find(agent => agent.reasoningModes.includes(question.reasoningMode) ) if (reasoningMatch) return reasoningMatch } // Fallback: find any agent with compatible reasoning mode and capabilities const compatibleAgents = Array.from(this.agents.values()) .filter(agent => agent.reasoningModes.includes(question.reasoningMode) && this.agentHasCapabilities(agent, question.expectedCapabilities) ) if (compatibleAgents.length > 0) { // Return the agent with the highest capability match score return compatibleAgents.reduce((best, current) => { const bestScore = this.calculateCapabilityMatchScore(best, question.expectedCapabilities) const currentScore = this.calculateCapabilityMatchScore(current, question.expectedCapabilities) return currentScore > bestScore ? current : best }) } return null } // Check if agent has required capabilities private agentHasCapabilities(agent: IAgent, requiredCapabilities: string[]): boolean { return requiredCapabilities.some(cap => agent.capabilities.some(agentCap => agentCap.toLowerCase().includes(cap.toLowerCase()) ) ) } // Calculate capability match score for agent selection private calculateCapabilityMatchScore(agent: IAgent, requiredCapabilities: string[]): number { let matchCount = 0 for (const requiredCap of requiredCapabilities) { for (const agentCap of agent.capabilities) { if (agentCap.toLowerCase().includes(requiredCap.toLowerCase())) { matchCount++ break } } } return requiredCapabilities.length > 0 ? matchCount / requiredCapabilities.length : 0 } // Enhanced collaborative analysis with question context private async collaborativeAnalysisWithQuestions( results: AgentResult[], context: Record<string, any>, questions: GeneratedQuestion[] ): Promise<void> { this.log.debug("Performing collaborative analysis with question context", { resultCount: results.length, questionCount: questions.length }) try { // Create enhanced shared context with question information const sharedContext: SharedContext = { sessionId: context.sessionId || `session-${Date.now()}`, projectPath: context.projectPath, language: context.language, frameworks: context.frameworks, previousResults: results, // Add question context codeContext: context.codeContext } // Group results by question perspective for better synthesis const resultsByPerspective = this.groupResultsByPerspective(results) // Synthesize insights from each perspective const perspectiveInsights = await this.synthesizePerspectiveInsights( resultsByPerspective, questions, sharedContext ) this.log.info("Question-driven collaborative analysis completed", { perspectives: Object.keys(resultsByPerspective).length, insights: perspectiveInsights.length }) } catch (error) { this.log.error("Question-driven collaborative analysis failed", { error }) } } // Group results by their question perspective private groupResultsByPerspective(results: AgentResult[]): { [perspective: string]: AgentResult[] } { const grouped: { [perspective: string]: AgentResult[] } = {} results.forEach(result => { const perspective = result.metadata?.questionPerspective || 'general' if (!grouped[perspective]) { grouped[perspective] = [] } grouped[perspective].push(result) }) return grouped } // Synthesize insights from different perspectives private async synthesizePerspectiveInsights( resultsByPerspective: { [perspective: string]: AgentResult[] }, questions: GeneratedQuestion[], context: SharedContext ): Promise<any[]> { const insights = [] for (const [perspective, perspectiveResults] of Object.entries(resultsByPerspective)) { const relatedQuestion = questions.find(q => q.perspective === perspective) const insight = { perspective, originalQuestion: relatedQuestion?.question, resultCount: perspectiveResults.length, averageConfidence: perspectiveResults.reduce((sum, r) => sum + r.confidence, 0) / perspectiveResults.length, keyFindings: this.extractKeyFindings(perspectiveResults), recommendations: this.generatePerspectiveRecommendations(perspectiveResults) } insights.push(insight) } return insights } // Extract key findings from perspective results private extractKeyFindings(results: AgentResult[]): any[] { const findings = [] results.forEach(result => { if (result.result && typeof result.result === 'object') { // Extract key information from the result if (result.result.issues) { findings.push(...result.result.issues) } if (result.result.insights) { findings.push(...result.result.insights) } if (result.result.recommendations) { findings.push(...result.result.recommendations) } } }) return findings } // Generate recommendations based on perspective analysis private generatePerspectiveRecommendations(results: AgentResult[]): string[] { const recommendations = [] const highConfidenceResults = results.filter(r => r.confidence > 0.8) const hasWarnings = results.some(r => r.warnings && r.warnings.length > 0) const hasErrors = results.some(r => r.errors && r.errors.length > 0) if (highConfidenceResults.length > 0) { recommendations.push(`Found ${highConfidenceResults.length} high-confidence findings that should be prioritized`) } if (hasWarnings) { recommendations.push('Review warnings identified in this perspective') } if (hasErrors) { recommendations.push('Address errors found in this analysis perspective') } return recommendations } // Process multiple tasks in batch async processBatch(tasks: AgentTask[]): Promise<Map<string, AgentResult[]>> { this.log.info("Processing task batch", { taskCount: tasks.length }) const results = new Map<string, AgentResult[]>() // Process tasks in parallel with concurrency limit const concurrencyLimit = Math.min(5, this.agents.size) const chunks = this.chunkArray(tasks, concurrencyLimit) for (const chunk of chunks) { const chunkPromises = chunk.map(async (task) => { try { const taskResults = await this.processTask(task) results.set(task.id, taskResults) } catch (error) { this.log.error("Batch task failed", { taskId: task.id, error }) results.set(task.id, []) } }) await Promise.all(chunkPromises) } return results } // Select appropriate agents for a task private selectAgentsForTask(task: AgentTask): IAgent[] { const selectedAgents: IAgent[] = [] // Agent selection strategy based on task type and requirements for (const agent of this.agents.values()) { if (this.isAgentSuitableForTask(agent, task)) { selectedAgents.push(agent) } } // Sort agents by suitability score selectedAgents.sort((a, b) => { const scoreA = this.calculateAgentSuitabilityScore(a, task) const scoreB = this.calculateAgentSuitabilityScore(b, task) return scoreB - scoreA }) // Limit to top agents to avoid overwhelming return selectedAgents.slice(0, Math.min(3, selectedAgents.length)) } private isAgentSuitableForTask(agent: IAgent, task: AgentTask): boolean { // Check if agent supports any of the required reasoning modes const hasCompatibleReasoningMode = task.reasoningModes.some(mode => agent.reasoningModes.includes(mode) ) if (!hasCompatibleReasoningMode) return false // Check if agent has relevant capabilities const hasRelevantCapability = this.checkCapabilityMatch(agent, task) return hasRelevantCapability } private checkCapabilityMatch(agent: IAgent, task: AgentTask): boolean { // Simple capability matching - can be made more sophisticated const taskTypeToCapabilities: { [key: string]: string[] } = { 'code-analysis': ['analysis', 'static-analysis', 'code-review'], 'error-detection': ['error-detection', 'debugging', 'validation'], 'refactoring': ['refactoring', 'code-improvement', 'optimization'], 'documentation': ['documentation', 'code-explanation', 'comments'], 'performance': ['performance', 'optimization', 'profiling'], 'security': ['security', 'vulnerability-detection', 'audit'], 'file-processing': ['file-handling', 'large-files', 'chunking'] } const requiredCapabilities = taskTypeToCapabilities[task.type] || [task.type] return requiredCapabilities.some(cap => agent.capabilities.some(agentCap => agentCap.toLowerCase().includes(cap.toLowerCase()) ) ) } private calculateAgentSuitabilityScore(agent: IAgent, task: AgentTask): number { let score = 0 // Score based on reasoning mode compatibility const compatibleModes = task.reasoningModes.filter(mode => agent.reasoningModes.includes(mode) ) score += compatibleModes.length * 0.3 // Score based on capability match const capabilityScore = this.calculateCapabilityScore(agent, task) score += capabilityScore * 0.4 // Score based on agent performance history const utilizationScore = this.performanceMetrics.agentUtilization.get(agent.id) || 0 score += (1 - utilizationScore) * 0.2 // Prefer less utilized agents // Score based on agent type const typeScore = this.calculateTypeScore(agent, task) score += typeScore * 0.1 return score } private calculateCapabilityScore(agent: IAgent, task: AgentTask): number { const taskCapabilities = this.getTaskCapabilities(task) let matchCount = 0 for (const taskCap of taskCapabilities) { for (const agentCap of agent.capabilities) { if (agentCap.toLowerCase().includes(taskCap.toLowerCase())) { matchCount++ break } } } return taskCapabilities.length > 0 ? matchCount / taskCapabilities.length : 0 } private getTaskCapabilities(task: AgentTask): string[] { // Extract implied capabilities from task const capabilities = [] if (task.type.includes('analysis')) capabilities.push('analysis') if (task.type.includes('error')) capabilities.push('error-detection') if (task.type.includes('performance')) capabilities.push('performance') if (task.type.includes('security')) capabilities.push('security') if (task.type.includes('refactor')) capabilities.push('refactoring') return capabilities.length > 0 ? capabilities : [task.type] } private calculateTypeScore(agent: IAgent, task: AgentTask): number { // Simple type matching score const typeMatches: { [key: string]: AgentType[] } = { 'code-analysis': [AgentType.CODE_ANALYZER], 'error-detection': [AgentType.ERROR_DETECTOR], 'refactoring': [AgentType.REFACTORING_AGENT], 'documentation': [AgentType.DOCUMENTATION_AGENT], 'performance': [AgentType.PERFORMANCE_AGENT], 'security': [AgentType.SECURITY_AGENT], 'file-processing': [AgentType.FILE_PROCESSOR] } const suitableTypes = typeMatches[task.type] || [] return suitableTypes.includes(agent.type) ? 1 : 0.5 } // Process task with selected agents private async processWithAgents(agents: IAgent[], task: AgentTask): Promise<AgentResult[]> { const results: AgentResult[] = [] // Create shared context const sharedContext: SharedContext = { sessionId: task.context.sessionId || `session-${Date.now()}`, projectPath: task.context.projectPath, language: task.context.language, frameworks: task.context.frameworks, codeContext: task.data?.toString(), userPreferences: task.context.userPreferences, previousResults: this.taskHistory.get(task.id) || [] } // Process with each reasoning mode for (const reasoningMode of task.reasoningModes) { const compatibleAgents = agents.filter(agent => agent.reasoningModes.includes(reasoningMode) ) if (compatibleAgents.length === 0) { this.log.warn("No agents available for reasoning mode", { reasoningMode }) continue } // Use the most suitable agent for this reasoning mode const selectedAgent = compatibleAgents[0] try { this.log.debug("Processing with agent", { agentId: selectedAgent.id, reasoningMode, taskId: task.id }) const result = await selectedAgent.process(task) results.push(result) // Update agent utilization const currentUtilization = this.performanceMetrics.agentUtilization.get(selectedAgent.id) || 0 this.performanceMetrics.agentUtilization.set(selectedAgent.id, currentUtilization + 0.1) } catch (error) { this.log.error("Agent processing failed", { agentId: selectedAgent.id, reasoningMode, error }) // Continue with other agents/modes even if one fails continue } } return results } // Perform collaborative analysis between agents private async collaborativeAnalysis( agents: IAgent[], results: AgentResult[], context: Record<string, any> ): Promise<void> { this.log.debug("Performing collaborative analysis", { agentCount: agents.length, resultCount: results.length }) try { const sharedContext: SharedContext = { sessionId: context.sessionId || `session-${Date.now()}`, projectPath: context.projectPath, language: context.language, frameworks: context.frameworks, previousResults: results } // Allow agents to collaborate and update their understanding const collaborationPromises = agents.map(agent => agent.collaborate(agents, sharedContext).catch(error => { this.log.warn("Agent collaboration failed", { agentId: agent.id, error }) }) ) await Promise.all(collaborationPromises) // Synthesize insights from collaboration await this.synthesizeCollaborativeInsights(results, sharedContext) } catch (error) { this.log.error("Collaborative analysis failed", { error }) } } private async synthesizeCollaborativeInsights( results: AgentResult[], context: SharedContext ): Promise<void> { // Analyze patterns across results const insights = { commonFindings: this.findCommonFindings(results), conflictingResults: this.findConflictingResults(results), confidenceDistribution: this.analyzeConfidenceDistribution(results), recommendedActions: this.generateRecommendedActions(results) } this.log.info("Collaborative insights generated", { commonFindings: insights.commonFindings.length, conflicts: insights.conflictingResults.length, avgConfidence: insights.confidenceDistribution.average }) // Store insights for future reference if (context.sessionId) { // Could store in a session-based cache or database this.log.debug("Insights stored for session", { sessionId: context.sessionId }) } } private findCommonFindings(results: AgentResult[]): any[] { const findings: { [key: string]: number } = {} results.forEach(result => { if (result.result && typeof result.result === 'object') { // Extract key findings from result const keys = Object.keys(result.result) keys.forEach(key => { findings[key] = (findings[key] || 0) + 1 }) } }) // Return findings that appear in multiple results return Object.entries(findings) .filter(([_, count]) => count > 1) .map(([finding, count]) => ({ finding, occurrences: count })) } private findConflictingResults(results: AgentResult[]): any[] { const conflicts = [] // Simple conflict detection based on confidence scores const lowConfidenceResults = results.filter(r => r.confidence < 0.5) const highConfidenceResults = results.filter(r => r.confidence > 0.8) if (lowConfidenceResults.length > 0 && highConfidenceResults.length > 0) { conflicts.push({ type: 'confidence_conflict', description: 'Some agents have high confidence while others have low confidence', lowConfidenceAgents: lowConfidenceResults.map(r => r.agentId), highConfidenceAgents: highConfidenceResults.map(r => r.agentId) }) } return conflicts } private analyzeConfidenceDistribution(results: AgentResult[]): any { const confidences = results.map(r => r.confidence) return { average: confidences.reduce((sum, c) => sum + c, 0) / confidences.length, min: Math.min(...confidences), max: Math.max(...confidences), standardDeviation: this.calculateStandardDeviation(confidences) } } private calculateStandardDeviation(values: number[]): number { const avg = values.reduce((sum, val) => sum + val, 0) / values.length const squaredDiffs = values.map(val => Math.pow(val - avg, 2)) const avgSquaredDiff = squaredDiffs.reduce((sum, val) => sum + val, 0) / values.length return Math.sqrt(avgSquaredDiff) } private generateRecommendedActions(results: AgentResult[]): string[] { const actions = [] const avgConfidence = results.reduce((sum, r) => sum + r.confidence, 0) / results.length if (avgConfidence < 0.6) { actions.push('Consider running additional analysis with different reasoning modes') } const hasWarnings = results.some(r => r.warnings && r.warnings.length > 0) if (hasWarnings) { actions.push('Review and address warnings identified by agents') } const hasErrors = results.some(r => r.errors && r.errors.length > 0) if (hasErrors) { actions.push('Address errors identified during analysis') } return actions } // Utility methods private chunkArray<T>(array: T[], chunkSize: number): T[][] { const chunks = [] for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)) } return chunks } private updatePerformanceMetrics(results: AgentResult[], processingTime: number): void { this.performanceMetrics.totalTasksProcessed++ // Update average processing time const currentAvg = this.performanceMetrics.averageProcessingTime const taskCount = this.performanceMetrics.totalTasksProcessed this.performanceMetrics.averageProcessingTime = (currentAvg * (taskCount - 1) + processingTime) / taskCount // Update success rate const successfulResults = results.filter(r => !r.errors || r.errors.length === 0).length const currentSuccessRate = this.performanceMetrics.successRate this.performanceMetrics.successRate = (currentSuccessRate * (taskCount - 1) + (successfulResults / Math.max(1, results.length))) / taskCount } // Queue management addToQueue(task: AgentTask): void { this.taskQueue.push(task) this.log.debug("Task added to queue", { taskId: task.id, queueSize: this.taskQueue.length }) } async processQueue(): Promise<void> { if (this.taskQueue.length === 0) return this.log.info("Processing task queue", { queueSize: this.taskQueue.length }) while (this.taskQueue.length > 0) { const task = this.taskQueue.shift()! try { await this.processTask(task) } catch (error) { this.log.error("Queue task processing failed", { taskId: task.id, error }) } } } // Status and monitoring getStatus(): any { return { registeredAgents: this.agents.size, activeTasks: this.activeTasks.size, queuedTasks: this.taskQueue.length, totalTasksProcessed: this.performanceMetrics.totalTasksProcessed, averageProcessingTime: this.performanceMetrics.averageProcessingTime, successRate: this.performanceMetrics.successRate, agentUtilization: Object.fromEntries(this.performanceMetrics.agentUtilization) } } async healthCheck(): Promise<boolean> { try { // Check if all agents are healthy const healthChecks = Array.from(this.agents.values()).map(agent => agent.isHealthy().catch(() => false) ) const results = await Promise.all(healthChecks) const healthyCount = results.filter(Boolean).length this.log.debug("Health check completed", { totalAgents: this.agents.size, healthyAgents: healthyCount }) return healthyCount === this.agents.size } catch (error) { this.log.error("Health check failed", { error }) return false } } // Cleanup async dispose(): Promise<void> { this.log.info("Disposing AgentCoordinator") // Dispose all agents const disposePromises = Array.from(this.agents.values()).map(agent => agent.dispose().catch(error => this.log.warn("Agent disposal failed", { agentId: agent.id, error }) ) ) await Promise.all(disposePromises) // Clear collections this.agents.clear() this.taskQueue.length = 0 this.activeTasks.clear() this.taskHistory.clear() this.log.info("AgentCoordinator disposed") } }