@cyqlelabs/mcp-dual-cycle-reasoner
Version:
MCP server implementing dual-cycle metacognitive reasoning framework for autonomous agents
393 lines (392 loc) • 17.2 kB
JavaScript
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)
}
}