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.

535 lines (534 loc) 21.7 kB
import { getVibeTaskManagerConfig } from '../utils/config-loader.js'; import { createErrorContext } from '../utils/enhanced-errors.js'; import logger from '../../../logger.js'; export class AutoResearchDetector { static instance; config; evaluationCache = new Map(); performanceMetrics = { totalEvaluations: 0, cacheHits: 0, averageEvaluationTime: 0 }; constructor() { this.config = this.getDefaultConfig(); this.initializeConfig(); logger.debug('Auto-Research Detector initialized'); } static getInstance() { if (!AutoResearchDetector.instance) { AutoResearchDetector.instance = new AutoResearchDetector(); } return AutoResearchDetector.instance; } async evaluateResearchNeed(context) { const startTime = Date.now(); const evaluationId = this.generateEvaluationId(context); try { if (this.config.performance.enableCaching) { const cached = this.getCachedEvaluation(evaluationId); if (cached) { this.performanceMetrics.cacheHits++; logger.debug({ evaluationId }, 'Returning cached research evaluation'); return cached; } } logger.info({ taskId: context.task.id, projectId: context.projectContext.projectId, sessionId: context.sessionId }, 'Evaluating research need'); const conditions = await this.evaluateTriggerConditions(context); const decision = this.makeResearchDecision(conditions, context); const evaluation = { decision, context, timestamp: Date.now(), metadata: { detectorVersion: '1.0.0', configSnapshot: { enabled: this.config.enabled, thresholds: { ...this.config.thresholds } }, performance: { totalTime: Date.now() - startTime, conditionEvaluationTime: 0, decisionTime: 0, cacheOperationTime: 0 } } }; if (this.config.performance.enableCaching) { this.cacheEvaluation(evaluationId, evaluation); } this.updatePerformanceMetrics(evaluation.metadata.performance.totalTime); logger.info({ taskId: context.task.id, shouldTriggerResearch: decision.shouldTriggerResearch, primaryReason: decision.primaryReason, confidence: decision.confidence, evaluationTime: evaluation.metadata.performance.totalTime }, 'Research evaluation completed'); return evaluation; } catch (error) { const errorContext = createErrorContext('AutoResearchDetector', 'evaluateResearchNeed') .taskId(context.task.id) .metadata({ projectId: context.projectContext.projectId, sessionId: context.sessionId, evaluationId }) .build(); logger.error({ err: error, context: errorContext }, 'Research evaluation failed'); const fallbackDecision = { shouldTriggerResearch: false, confidence: 0.1, primaryReason: 'sufficient_context', reasoning: ['Evaluation failed, defaulting to no research'], recommendedScope: { depth: 'shallow', focus: 'technical', priority: 'low', estimatedQueries: 0 }, evaluatedConditions: this.getEmptyConditions(), metrics: { evaluationTime: Date.now() - startTime, conditionsChecked: 0, cacheHits: 0 } }; return { decision: fallbackDecision, context, timestamp: Date.now(), metadata: { detectorVersion: '1.0.0', configSnapshot: { enabled: false }, performance: { totalTime: Date.now() - startTime, conditionEvaluationTime: 0, decisionTime: 0, cacheOperationTime: 0 } } }; } } async evaluateTriggerConditions(context) { const startTime = Date.now(); const conditions = { projectType: await this.evaluateProjectType(context), taskComplexity: this.evaluateTaskComplexity(context), knowledgeGap: this.evaluateKnowledgeGap(context), domainSpecific: this.evaluateDomainSpecific(context) }; logger.debug({ taskId: context.task.id, evaluationTime: Date.now() - startTime, conditions: { projectType: conditions.projectType.isGreenfield, complexityScore: conditions.taskComplexity.complexityScore, hasInsufficientContext: conditions.knowledgeGap.hasInsufficientContext, specializedDomain: conditions.domainSpecific.specializedDomain } }, 'Trigger conditions evaluated'); return conditions; } async evaluateProjectType(context) { const { contextResult } = context; const hasCodebaseContext = contextResult && contextResult.summary.totalFiles > 0; const codebaseSize = contextResult?.summary.totalFiles || 0; const averageRelevance = contextResult?.summary.averageRelevance || 0; let codebaseMaturity = 'new'; let confidence = 0.5; if (codebaseSize === 0) { codebaseMaturity = 'new'; confidence = 0.9; } else if (codebaseSize < 10) { codebaseMaturity = 'developing'; confidence = 0.7; } else if (codebaseSize < 50) { codebaseMaturity = 'mature'; confidence = 0.8; } else { codebaseMaturity = 'legacy'; confidence = 0.6; } const isGreenfield = codebaseSize === 0 || (codebaseSize < 3 && averageRelevance < 0.5); return { isGreenfield, hasExistingCodebase: hasCodebaseContext || false, codebaseMaturity, confidence }; } evaluateTaskComplexity(context) { const { task } = context; const description = (task.description || task.title).toLowerCase(); const complexityIndicators = []; let complexityScore = 0; for (const indicator of this.config.complexityIndicators.highComplexity) { if (description.includes(indicator.toLowerCase())) { complexityIndicators.push(indicator); complexityScore += 0.3; } } for (const indicator of this.config.complexityIndicators.mediumComplexity) { if (description.includes(indicator.toLowerCase())) { complexityIndicators.push(indicator); complexityScore += 0.2; } } for (const indicator of this.config.complexityIndicators.architectural) { if (description.includes(indicator.toLowerCase())) { complexityIndicators.push(indicator); complexityScore += 0.25; } } for (const indicator of this.config.complexityIndicators.integration) { if (description.includes(indicator.toLowerCase())) { complexityIndicators.push(indicator); complexityScore += 0.2; } } complexityScore = Math.min(complexityScore, 1.0); const estimatedResearchValue = complexityScore * 0.8 + (complexityIndicators.length > 0 ? 0.2 : 0); const requiresSpecializedKnowledge = complexityScore > this.config.thresholds.minComplexityScore; return { complexityScore, complexityIndicators, estimatedResearchValue, requiresSpecializedKnowledge }; } evaluateKnowledgeGap(context) { const { contextResult } = context; if (!contextResult) { return { contextQuality: 0, relevanceScore: 0, filesFound: 0, averageRelevance: 0, hasInsufficientContext: true }; } const { summary } = contextResult; const contextQuality = this.calculateContextQuality(summary); const hasInsufficientContext = this.determineInsufficientContext(summary); return { contextQuality, relevanceScore: summary.averageRelevance, filesFound: summary.totalFiles, averageRelevance: summary.averageRelevance, hasInsufficientContext }; } evaluateDomainSpecific(context) { const { task } = context; const description = (task.description || task.title).toLowerCase(); const technologyStack = this.extractTechnologyStack(context); const unfamiliarTechnologies = this.identifyUnfamiliarTechnologies(technologyStack); const specializedDomain = this.isSpecializedDomain(description, technologyStack); const domainComplexity = this.calculateDomainComplexity(technologyStack, unfamiliarTechnologies); return { technologyStack, unfamiliarTechnologies, specializedDomain, domainComplexity }; } makeResearchDecision(conditions, _context) { const reasoning = []; let shouldTriggerResearch = false; let confidence = 0.5; let primaryReason = 'sufficient_context'; if (!this.config.enabled) { shouldTriggerResearch = false; primaryReason = 'sufficient_context'; confidence = 0.1; reasoning.push('Auto-research disabled in configuration'); return this.createDecision(shouldTriggerResearch, confidence, primaryReason, reasoning, conditions); } if (conditions.projectType.isGreenfield && conditions.projectType.confidence > 0.7) { shouldTriggerResearch = true; primaryReason = 'project_type'; confidence = conditions.projectType.confidence; reasoning.push('Greenfield project detected - research recommended for best practices'); } else if (conditions.taskComplexity.complexityScore > this.config.thresholds.minComplexityScore) { shouldTriggerResearch = true; primaryReason = 'task_complexity'; confidence = conditions.taskComplexity.complexityScore; reasoning.push(`High complexity task (score: ${conditions.taskComplexity.complexityScore.toFixed(2)}) - research recommended`); } else if (conditions.knowledgeGap.hasInsufficientContext) { shouldTriggerResearch = true; primaryReason = 'knowledge_gap'; confidence = 0.8; reasoning.push('Insufficient context found - research needed to fill knowledge gaps'); } else if (conditions.domainSpecific.specializedDomain) { shouldTriggerResearch = true; primaryReason = 'domain_specific'; confidence = conditions.domainSpecific.domainComplexity; reasoning.push('Specialized domain detected - research recommended for domain expertise'); } else { shouldTriggerResearch = false; primaryReason = 'sufficient_context'; confidence = Math.max(conditions.knowledgeGap.contextQuality, 0.6); reasoning.push('Sufficient context available - research not needed'); } return this.createDecision(shouldTriggerResearch, confidence, primaryReason, reasoning, conditions); } createDecision(shouldTriggerResearch, confidence, primaryReason, reasoning, conditions) { const recommendedScope = this.determineResearchScope(conditions, shouldTriggerResearch); return { shouldTriggerResearch, confidence, primaryReason, reasoning, recommendedScope, evaluatedConditions: conditions, metrics: { evaluationTime: 0, conditionsChecked: 4, cacheHits: 0 } }; } calculateContextQuality(summary) { if (summary.totalFiles === 0) return 0; const fileScore = Math.min(summary.totalFiles / this.config.thresholds.minFilesForSufficientContext, 1); const relevanceScore = summary.averageRelevance; return (fileScore * 0.4 + relevanceScore * 0.6); } determineInsufficientContext(summary) { return summary.totalFiles < this.config.thresholds.minFilesForSufficientContext || summary.averageRelevance < this.config.thresholds.minAverageRelevance; } extractTechnologyStack(context) { const { projectContext, task } = context; const technologies = []; if (projectContext.languages) { technologies.push(...projectContext.languages); } if (projectContext.frameworks) { technologies.push(...projectContext.frameworks); } if (projectContext.tools) { technologies.push(...projectContext.tools); } const description = (task.description || task.title).toLowerCase(); for (const tech of [...this.config.specializedTechnologies.emerging, ...this.config.specializedTechnologies.complexFrameworks]) { if (description.includes(tech.toLowerCase())) { technologies.push(tech); } } return [...new Set(technologies)]; } identifyUnfamiliarTechnologies(technologyStack) { const unfamiliar = []; for (const tech of technologyStack) { if (this.config.specializedTechnologies.emerging.includes(tech) || this.config.specializedTechnologies.complexFrameworks.includes(tech) || this.config.specializedTechnologies.enterprise.includes(tech)) { unfamiliar.push(tech); } } return unfamiliar; } isSpecializedDomain(description, technologyStack) { for (const domain of this.config.specializedTechnologies.domains) { if (description.includes(domain.toLowerCase())) { return true; } } const unfamiliarTechs = this.identifyUnfamiliarTechnologies(technologyStack); return unfamiliarTechs.length > 0; } calculateDomainComplexity(technologyStack, unfamiliarTechnologies) { const totalTechs = technologyStack.length; const unfamiliarRatio = totalTechs > 0 ? unfamiliarTechnologies.length / totalTechs : 0; return Math.min(unfamiliarRatio + (totalTechs > 5 ? 0.2 : 0), 1.0); } determineResearchScope(conditions, shouldTriggerResearch) { if (!shouldTriggerResearch) { return { depth: 'shallow', focus: 'technical', priority: 'low', estimatedQueries: 0 }; } let depth = 'medium'; let focus = 'technical'; let priority = 'medium'; let estimatedQueries = 2; if (conditions.taskComplexity.complexityScore > 0.7) { depth = 'deep'; priority = 'high'; estimatedQueries = 4; } else if (conditions.taskComplexity.complexityScore < 0.3) { depth = 'shallow'; priority = 'low'; estimatedQueries = 1; } if (conditions.domainSpecific.specializedDomain) { focus = 'comprehensive'; estimatedQueries += 1; } if (conditions.projectType.isGreenfield) { focus = 'comprehensive'; estimatedQueries += 1; } return { depth, focus, priority, estimatedQueries }; } generateEvaluationId(context) { const taskId = context.task.id; const projectId = context.projectContext.projectId; const taskHash = this.hashString(context.task.description || context.task.title); return `${projectId}-${taskId}-${taskHash}`; } getCachedEvaluation(evaluationId) { const cached = this.evaluationCache.get(evaluationId); if (!cached) return null; const now = Date.now(); const age = now - cached.timestamp; if (age > this.config.performance.cacheTTL) { this.evaluationCache.delete(evaluationId); return null; } return cached; } cacheEvaluation(evaluationId, evaluation) { this.evaluationCache.set(evaluationId, evaluation); if (this.evaluationCache.size > 100) { const oldestKey = this.evaluationCache.keys().next().value; if (oldestKey) { this.evaluationCache.delete(oldestKey); } } } updatePerformanceMetrics(evaluationTime) { this.performanceMetrics.totalEvaluations++; const total = this.performanceMetrics.totalEvaluations; const current = this.performanceMetrics.averageEvaluationTime; this.performanceMetrics.averageEvaluationTime = (current * (total - 1) + evaluationTime) / total; } hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } getEmptyConditions() { return { projectType: { isGreenfield: false, hasExistingCodebase: false, codebaseMaturity: 'new', confidence: 0 }, taskComplexity: { complexityScore: 0, complexityIndicators: [], estimatedResearchValue: 0, requiresSpecializedKnowledge: false }, knowledgeGap: { contextQuality: 0, relevanceScore: 0, filesFound: 0, averageRelevance: 0, hasInsufficientContext: true }, domainSpecific: { technologyStack: [], unfamiliarTechnologies: [], specializedDomain: false, domainComplexity: 0 } }; } async initializeConfig() { try { await getVibeTaskManagerConfig(); logger.debug('Auto-research detector configuration initialized'); } catch (error) { logger.warn({ err: error }, 'Failed to load config, using defaults'); } } getDefaultConfig() { return { enabled: true, thresholds: { minComplexityScore: 0.4, maxContextQuality: 0.8, minDecisionConfidence: 0.6, minFilesForSufficientContext: 3, minAverageRelevance: 0.5 }, complexityIndicators: { highComplexity: ['architecture', 'system', 'framework', 'migration', 'refactor'], mediumComplexity: ['integration', 'optimization', 'performance', 'security'], architectural: ['design', 'pattern', 'structure', 'component', 'module'], integration: ['api', 'service', 'database', 'external', 'third-party'] }, specializedTechnologies: { emerging: ['rust', 'deno', 'bun', 'astro', 'qwik', 'solid'], complexFrameworks: ['kubernetes', 'terraform', 'ansible', 'docker', 'microservices'], enterprise: ['sap', 'oracle', 'salesforce', 'sharepoint', 'dynamics'], domains: ['blockchain', 'machine-learning', 'ai', 'iot', 'embedded', 'gaming'] }, performance: { enableCaching: true, cacheTTL: 300000, maxEvaluationTime: 5000, enableParallelEvaluation: true } }; } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; logger.debug({ config: this.config }, 'Auto-research detector configuration updated'); } getConfig() { return { ...this.config }; } getPerformanceMetrics() { return { ...this.performanceMetrics, cacheSize: this.evaluationCache.size, cacheHitRate: this.performanceMetrics.totalEvaluations > 0 ? this.performanceMetrics.cacheHits / this.performanceMetrics.totalEvaluations : 0 }; } clearCache() { this.evaluationCache.clear(); logger.debug('Auto-research detector cache cleared'); } resetPerformanceMetrics() { this.performanceMetrics = { totalEvaluations: 0, cacheHits: 0, averageEvaluationTime: 0 }; logger.debug('Auto-research detector performance metrics reset'); } }