UNPKG

@cyqlelabs/mcp-dual-cycle-reasoner

Version:

MCP server implementing dual-cycle metacognitive reasoning framework for autonomous agents

393 lines (392 loc) 17.2 kB
import natural from 'natural'; import nlp from 'compromise'; import { semanticAnalyzer } from './semantic-analyzer.js'; // Extract needed components from natural const { SentimentAnalyzer, PorterStemmer, WordTokenizer } = natural; export class Adjudicator { caseBase = []; /** * Strategy 4: Belief Revision for Strategy Invalidation * Implements AGM belief revision principles to maintain logical consistency */ async reviseBeliefs(currentBeliefs, contradictingEvidence, _trace) { const revisedBeliefs = []; const removedBeliefs = []; // For simplified approach, keep non-contradicted beliefs and add new insight for (const belief of currentBeliefs) { const contradicts = await this.doesEvidenceContradictBelief(belief, contradictingEvidence); if (!contradicts) { revisedBeliefs.push(belief); } else { removedBeliefs.push(belief); } } // Add new belief based on evidence const newBelief = `Current strategy ineffective: ${contradictingEvidence}`; revisedBeliefs.push(newBelief); const rationale = `Removed ${removedBeliefs.length} contradicted beliefs and added insight about strategy failure`; return { revised_beliefs: revisedBeliefs, removed_beliefs: removedBeliefs, rationale, }; } /** * Enhanced semantic belief contradiction detection using NLI transformers */ async doesEvidenceContradictBelief(belief, evidence) { const result = await semanticAnalyzer.assessBeliefContradiction(belief, evidence); return result.contradicts; } /** * Strategy 5: Abductive Reasoning for Failure Diagnosis * Generates and evaluates hypotheses to explain observed failures */ async diagnoseFailure(loopResult, trace) { const hypotheses = this.generateFailureHypotheses(loopResult, trace); const evaluatedHypotheses = await Promise.all(hypotheses.map(async (hypothesis) => ({ hypothesis, evidence: await this.gatherEvidence(hypothesis, trace), confidence: await this.calculateHypothesisConfidence(hypothesis, trace), }))); // Sort by confidence evaluatedHypotheses.sort((a, b) => b.confidence - a.confidence); const bestHypothesis = evaluatedHypotheses[0]; const suggestedActions = this.generateDiagnosticActions(bestHypothesis.hypothesis, trace); return { primary_hypothesis: bestHypothesis.hypothesis, confidence: bestHypothesis.confidence, evidence: bestHypothesis.evidence, suggested_actions: suggestedActions, }; } /** * Strategy 6: Case-Based Reasoning for Recovery * Retrieves and adapts solutions from similar past problems */ generateRecoveryPlan(diagnosis, trace, availablePatterns) { // First, try to retrieve similar cases const similarCases = this.retrieveSimilarCases(`${this.mapDiagnosisToLoopType(diagnosis)}: ${diagnosis.primary_hypothesis} in ${this.extractContext(trace)}`); if (similarCases.length > 0 && similarCases[0].outcome) { // Create a plan based on the successful case return { pattern: 'strategic_retreat', actions: [similarCases[0].solution], rationale: `Adapted from similar successful case`, expected_outcome: 'Recovery based on proven solution', }; } // If no successful cases found, generate new plan return this.generateNovelRecoveryPlan(diagnosis, trace, availablePatterns); } /** * Store experience for future case-based reasoning */ storeExperience(case_) { this.caseBase.push(case_); // Limit case base size to prevent memory issues if (this.caseBase.length > 1000) { // Remove oldest cases with low success rate this.caseBase = this.caseBase .sort((a, b) => { if (a.outcome !== b.outcome) { return b.outcome ? 1 : -1; } return (b.timestamp || 0) - (a.timestamp || 0); }) .slice(0, 800); } } /** * Retrieve similar cases for CBR */ retrieveSimilarCases(problemDescription, maxResults = 5) { const scoredCases = this.caseBase.map((case_) => ({ case: case_, similarity: this.calculateCaseSimilarity(problemDescription, case_.problem_description), })); return scoredCases .sort((a, b) => b.similarity - a.similarity) .slice(0, maxResults) .map((item) => item.case); } generateFailureHypotheses(loopResult, _trace) { const hypotheses = []; if (loopResult.type === 'action_repetition') { hypotheses.push('element_state_error', 'selector_error'); } if (loopResult.type === 'state_invariance') { hypotheses.push('page_state_error', 'network_error'); } if (loopResult.type === 'progress_stagnation') { hypotheses.push('task_model_error', 'element_state_error'); } // Always consider these as backup hypotheses hypotheses.push('unknown'); return [...new Set(hypotheses)]; // Remove duplicates } async gatherEvidence(hypothesis, trace) { const evidence = []; const recentActions = trace.recent_actions.slice(-5); // Define expected outcomes for each hypothesis type const expectedOutcomes = { element_state_error: 'Elements can be found and interacted with successfully', page_state_error: 'Page state changes appropriately after actions', selector_error: 'Selectors work correctly without errors', task_model_error: 'Task progresses efficiently toward completion', network_error: 'Network operations complete without timeout or connection issues', unknown: 'Actions execute successfully without errors', }; const expectedOutcome = expectedOutcomes[hypothesis]; // Use semantic analysis to assess each recent action for (const action of recentActions) { try { const assessment = await semanticAnalyzer.assessActionOutcome(action, expectedOutcome); if (assessment.category === 'failure' && assessment.confidence > 0.7) { evidence.push(`Action "${action}" contradicts expected outcome: ${assessment.reasoning}`); } else if (assessment.category === 'neutral' && assessment.confidence > 0.8) { evidence.push(`Action "${action}" shows unclear outcome: ${assessment.reasoning}`); } } catch (error) { console.error('Error assessing action outcome:', error); } } // Additional hypothesis-specific semantic checks switch (hypothesis) { case 'page_state_error': if (trace.recent_actions.length > 1) { const recentContext = trace.current_context || ''; if (recentContext === '') { evidence.push('Page state has not changed despite actions'); } } break; case 'task_model_error': if (trace.step_count > 5) { const hasProgress = trace.recent_actions.length > 0; if (!hasProgress) { evidence.push('No progress suggests incorrect task understanding'); } } break; } return evidence; } async calculateHypothesisConfidence(hypothesis, trace) { const evidence = await this.gatherEvidence(hypothesis, trace); const baseConfidence = 0.4; const evidenceBonus = evidence.length * 0.15; return Math.min(0.95, baseConfidence + evidenceBonus); } generateDiagnosticActions(hypothesis, _trace) { switch (hypothesis) { case 'element_state_error': return [ 'Check element visibility and enabled state', 'Verify element is not obscured by overlays', 'Wait for element to become interactive', ]; case 'page_state_error': return [ 'Refresh the page', 'Wait for pending network requests', 'Check for JavaScript errors', ]; case 'selector_error': return [ 'Try alternative selectors for the same element', 'Use xpath or CSS selector alternatives', 'Switch to visual element identification', ]; case 'task_model_error': return [ 'Re-examine the current goal and sub-goals', 'Gather more information about page structure', 'Consider alternative task decomposition', ]; case 'network_error': return ['Retry the last action', 'Check network connectivity', 'Increase timeout values']; default: return [ 'Gather more diagnostic information', 'Try a different approach', 'Consider human escalation', ]; } } mapDiagnosisToLoopType(diagnosis) { // Map diagnosis back to loop type for case retrieval switch (diagnosis.primary_hypothesis) { case 'element_state_error': case 'selector_error': return 'action_repetition'; case 'page_state_error': case 'network_error': return 'state_invariance'; case 'task_model_error': return 'progress_stagnation'; default: return 'action_repetition'; } } extractContext(trace) { const recentActions = trace.recent_actions.slice(-3).join(' -> '); const currentContext = trace.current_context || 'unknown'; return `Actions: ${recentActions}, Context: ${currentContext}, Goal: ${trace.goal}`; } generateNovelRecoveryPlan(diagnosis, trace, availablePatterns) { const patterns = availablePatterns || [ 'strategic_retreat', 'context_refresh', 'modality_switching', ]; const selectedPattern = this.selectRecoveryPattern(diagnosis, patterns); return { pattern: selectedPattern, actions: this.generateActionsForPattern(selectedPattern, diagnosis, trace), rationale: `Novel recovery plan for ${diagnosis.primary_hypothesis} with confidence ${diagnosis.confidence}`, expected_outcome: 'Break current loop and resume progress toward goal', }; } selectRecoveryPattern(diagnosis, availablePatterns) { switch (diagnosis.primary_hypothesis) { case 'page_state_error': case 'network_error': return availablePatterns.includes('context_refresh') ? 'context_refresh' : availablePatterns[0]; case 'selector_error': case 'element_state_error': return availablePatterns.includes('modality_switching') ? 'modality_switching' : availablePatterns[0]; case 'task_model_error': return availablePatterns.includes('information_foraging') ? 'information_foraging' : availablePatterns[0]; default: return availablePatterns.includes('strategic_retreat') ? 'strategic_retreat' : availablePatterns[0]; } } generateActionsForPattern(pattern, _diagnosis, _trace) { switch (pattern) { case 'strategic_retreat': return [ 'Undo last 2-3 actions', 'Return to known good state', 'Try alternative approach to current sub-goal', ]; case 'context_refresh': return [ 'Refresh page', 'Clear browser cache', 'Restart from current goal with fresh state', ]; case 'modality_switching': return [ 'Take screenshot of current page', 'Use visual element detection', 'Click element by coordinates instead of selector', ]; case 'information_foraging': return [ 'Explore page structure systematically', 'Document available interactive elements', 'Build updated mental model of page', ]; case 'human_escalation': return [ 'Pause autonomous execution', 'Request human guidance', 'Provide detailed context about failure', ]; default: return ['Try generic recovery approach']; } } /** * Enhanced semantic similarity calculation using multiple NLP techniques */ calculateCaseSimilarity(current, stored) { // Parse both texts with compromise const currentDoc = nlp(current); const storedDoc = nlp(stored); // Extract and stem key terms const currentTerms = currentDoc .terms() .out('array') .map((term) => PorterStemmer.stem(term.toLowerCase())) .filter((term) => term.length > 2); const storedTerms = storedDoc .terms() .out('array') .map((term) => PorterStemmer.stem(term.toLowerCase())) .filter((term) => term.length > 2); // Calculate Jaccard similarity for stemmed terms const jaccardSimilarity = this.calculateJaccardDistance(currentTerms, storedTerms); const jaccardScore = 1 - jaccardSimilarity; // Calculate sentiment similarity using natural library const tokenizer = new WordTokenizer(); const analyzer = new SentimentAnalyzer('English', PorterStemmer, 'afinn'); const currentTokens = tokenizer.tokenize(current) || []; const storedTokens = tokenizer.tokenize(stored) || []; const currentSentiment = analyzer.getSentiment(currentTokens); const storedSentiment = analyzer.getSentiment(storedTokens); const sentimentSimilarity = 1 - Math.abs(currentSentiment - storedSentiment); // Calculate TF-IDF based similarity for better semantic matching const allTerms = [...new Set([...currentTerms, ...storedTerms])]; const currentVector = this.createTfIdfVector(currentTerms, allTerms); const storedVector = this.createTfIdfVector(storedTerms, allTerms); const cosineSimilarity = this.calculateCosineSimilarity(currentVector, storedVector); // Combine multiple similarity measures const combinedSimilarity = jaccardScore * 0.4 + sentimentSimilarity * 0.3 + cosineSimilarity * 0.3; return Math.max(0, Math.min(1, combinedSimilarity)); } /** * Create TF-IDF vector for semantic similarity */ createTfIdfVector(terms, allTerms) { const termFreq = terms.reduce((freq, term) => { freq[term] = (freq[term] || 0) + 1; return freq; }, {}); return allTerms.map((term) => { const tf = (termFreq[term] || 0) / terms.length; // Simplified IDF calculation const idf = Math.log(1 + 1 / Math.max(1, termFreq[term] || 0)); return tf * idf; }); } /** * Calculate cosine similarity between two vectors */ calculateCosineSimilarity(vecA, vecB) { const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0)); const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0)); if (magnitudeA === 0 || magnitudeB === 0) return 0; return dotProduct / (magnitudeA * magnitudeB); } /** * Enhanced evidence gathering using semantic analysis */ /** * Calculate Jaccard distance between two string arrays */ calculateJaccardDistance(set1, set2) { const s1 = new Set(set1); const s2 = new Set(set2); const intersection = new Set([...s1].filter((x) => s2.has(x))); const union = new Set([...s1, ...s2]); if (union.size === 0) return 0; const jaccardSimilarity = intersection.size / union.size; return 1 - jaccardSimilarity; // Return distance (1 - similarity) } }