UNPKG

@cyqlelabs/mcp-dual-cycle-reasoner

Version:

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

510 lines (439 loc) 16.7 kB
import { CognitiveTrace, LoopDetectionResult, DiagnosisResult, FailureHypothesis, BeliefRevisionResult, RecoveryPlan, RecoveryPattern, Case, } from './types.js'; 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 { private caseBase: Case[] = []; /** * Strategy 4: Belief Revision for Strategy Invalidation * Implements AGM belief revision principles to maintain logical consistency */ async reviseBeliefs( currentBeliefs: string[], contradictingEvidence: string, _trace: CognitiveTrace ): Promise<BeliefRevisionResult> { const revisedBeliefs: string[] = []; const removedBeliefs: string[] = []; // 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 */ private async doesEvidenceContradictBelief(belief: string, evidence: string): Promise<boolean> { 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: LoopDetectionResult, trace: CognitiveTrace ): Promise<DiagnosisResult> { 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: DiagnosisResult, trace: CognitiveTrace, availablePatterns?: RecoveryPattern[] ): RecoveryPlan { // 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' as RecoveryPattern, 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_: Case): void { 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: string, maxResults: number = 5): Case[] { 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); } private generateFailureHypotheses( loopResult: LoopDetectionResult, _trace: CognitiveTrace ): FailureHypothesis[] { const hypotheses: FailureHypothesis[] = []; 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 } private async gatherEvidence( hypothesis: FailureHypothesis, trace: CognitiveTrace ): Promise<string[]> { const evidence: string[] = []; const recentActions = trace.recent_actions.slice(-5); // Define expected outcomes for each hypothesis type const expectedOutcomes: Record<FailureHypothesis, string> = { 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; } private async calculateHypothesisConfidence( hypothesis: FailureHypothesis, trace: CognitiveTrace ): Promise<number> { const evidence = await this.gatherEvidence(hypothesis, trace); const baseConfidence = 0.4; const evidenceBonus = evidence.length * 0.15; return Math.min(0.95, baseConfidence + evidenceBonus); } private generateDiagnosticActions( hypothesis: FailureHypothesis, _trace: CognitiveTrace ): string[] { 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', ]; } } private mapDiagnosisToLoopType(diagnosis: DiagnosisResult): any { // 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'; } } private extractContext(trace: CognitiveTrace): string { const recentActions = trace.recent_actions.slice(-3).join(' -> '); const currentContext = trace.current_context || 'unknown'; return `Actions: ${recentActions}, Context: ${currentContext}, Goal: ${trace.goal}`; } private generateNovelRecoveryPlan( diagnosis: DiagnosisResult, trace: CognitiveTrace, availablePatterns?: RecoveryPattern[] ): RecoveryPlan { 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', }; } private selectRecoveryPattern( diagnosis: DiagnosisResult, availablePatterns: RecoveryPattern[] ): RecoveryPattern { 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]; } } private generateActionsForPattern( pattern: RecoveryPattern, _diagnosis: DiagnosisResult, _trace: CognitiveTrace ): string[] { 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 */ private calculateCaseSimilarity(current: string, stored: string): number { // 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: string) => PorterStemmer.stem(term.toLowerCase())) .filter((term: string) => term.length > 2); const storedTerms = storedDoc .terms() .out('array') .map((term: string) => PorterStemmer.stem(term.toLowerCase())) .filter((term: string) => 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 */ private createTfIdfVector(terms: string[], allTerms: string[]): number[] { const termFreq = terms.reduce( (freq, term) => { freq[term] = (freq[term] || 0) + 1; return freq; }, {} as Record<string, number> ); 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 */ private calculateCosineSimilarity(vecA: number[], vecB: number[]): number { 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 */ private calculateJaccardDistance(set1: string[], set2: string[]): number { 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) } }