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
JavaScript
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');
}
}