UNPKG

@cyqlelabs/mcp-dual-cycle-reasoner

Version:

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

747 lines (746 loc) 32.5 kB
import { createHash } from 'crypto'; import * as ss from 'simple-statistics'; export class Sentinel { stateHistory = []; maxHistorySize = 20; config; constructor(config = {}) { this.config = { progress_indicators: config.progress_indicators || [], min_actions_for_detection: config.min_actions_for_detection || 5, alternating_threshold: config.alternating_threshold || 0.5, repetition_threshold: config.repetition_threshold || 0.4, progress_threshold_adjustment: config.progress_threshold_adjustment || 0.2, }; } /** * Enhanced statistical anomaly detection using entropy and advanced metrics */ detectStatisticalAnomalies(actions) { if (actions.length < 3) return 0; const actionFrequencies = this.calculateActionFrequencies(actions); const frequencies = Object.values(actionFrequencies); // Calculate entropy-based anomaly score const entropy = this.calculateEntropy(frequencies); const maxEntropy = Math.log2(Object.keys(actionFrequencies).length); const normalizedEntropy = maxEntropy > 0 ? entropy / maxEntropy : 0; // Calculate standard deviation of action intervals const actionHashes = actions.map((a) => this.hashAction(a)); const intervalVariance = actionHashes.length > 1 ? ss.variance(actionHashes.map((_, i) => i)) : 0; // Combine entropy and variance for anomaly score const entropyScore = 1 - normalizedEntropy; // Lower entropy = higher anomaly const varianceScore = intervalVariance < 0.1 ? 0.8 : 0.2; // Low variance = repetitive return entropyScore * 0.7 + varianceScore * 0.3; } /** * Time series analysis for detecting temporal patterns */ detectTemporalPatterns(actions) { if (actions.length < 5) return 0; const actionSequence = actions.map((a) => this.hashAction(a)); // Calculate autocorrelation to detect periodic patterns const autocorr = this.calculateAutocorrelation(actionSequence, 1); const periodicityScore = Math.abs(autocorr); // Moving average to detect trend changes const movingAvg = this.calculateMovingAverage(actionSequence, 3); const trendVariance = movingAvg.length > 1 ? ss.variance(movingAvg) : 0; // High periodicity + low trend variance = stuck pattern return periodicityScore > 0.7 && trendVariance < 0.1 ? 0.8 : 0.2; } /** * Strategy 1: Domain-Agnostic Action Pattern Analysis * Detects loops using semantic action similarity and behavioral patterns */ detectActionAnomalies(trace, windowSize = 10) { if (!trace.recent_actions || trace.recent_actions.length === 0) { return { detected: false, confidence: 0, details: 'No action history available' }; } // Use configurable minimum actions threshold to avoid false positives on legitimate exploration const minActionsForDetection = Math.max(this.config.min_actions_for_detection, Math.min(windowSize, 8)); if (trace.recent_actions.length < minActionsForDetection) { return { detected: false, confidence: 0, details: `Insufficient action history: ${trace.recent_actions.length}/${minActionsForDetection} required`, }; } const recentActions = trace.recent_actions.slice(-windowSize); // Domain-agnostic semantic similarity analysis const semanticClusters = this.clusterSemanticallySimilarActions(recentActions); const semanticRepetitionRatio = this.calculateSemanticRepetition(semanticClusters, recentActions.length); // Extract action parameters for deeper analysis const actionParams = recentActions.map((action) => this.extractActionParameters(action)); const parameterRepetition = this.detectParameterPatterns(actionParams); // Check for exact repetition patterns (fallback for simple cases) const uniqueActions = new Set(recentActions); const exactRepetitionRatio = 1 - uniqueActions.size / recentActions.length; // Check for cyclical patterns (A-B-A-B or A-B-C-A-B-C) const cyclicalScore = this.detectCyclicalPatterns(recentActions); // Check for oscillating patterns (A-B-A-B specifically) const oscillationScore = this.detectOscillationPatterns(recentActions); // Enhanced pattern detection for alternating actions using semantic similarity const alternatingScore = this.detectAlternatingPatterns(semanticClusters, recentActions); // Check for configurable progress indicators that suggest positive task advancement const hasProgressAction = this.config.progress_indicators.length > 0 && recentActions.some((action) => this.config.progress_indicators.some((indicator) => this.semanticSimilarity(action, indicator) > 0.7)); // Calculate combined anomaly score using multiple detection methods const anomalyScores = { semantic_repetition: semanticRepetitionRatio, parameter_repetition: parameterRepetition, exact_repetition: exactRepetitionRatio, cyclical_pattern: cyclicalScore, oscillation_pattern: oscillationScore, alternating_pattern: alternatingScore, statistical_anomaly: this.detectStatisticalAnomalies(recentActions), temporal_pattern: this.detectTemporalPatterns(recentActions), }; // Weight different detection methods based on their reliability const weights = { semantic_repetition: 0.25, parameter_repetition: 0.2, exact_repetition: 0.15, cyclical_pattern: 0.15, oscillation_pattern: 0.1, alternating_pattern: 0.1, statistical_anomaly: 0.03, temporal_pattern: 0.02, }; const combinedAnomalyScore = Object.entries(anomalyScores).reduce((sum, [method, score]) => { return sum + score * weights[method]; }, 0); // Adjust threshold based on whether we have progress indicators const baseThreshold = 0.35; // More sensitive than before const anomalyThreshold = hasProgressAction ? baseThreshold + this.config.progress_threshold_adjustment : baseThreshold; if (combinedAnomalyScore > anomalyThreshold) { // Find the most significant detection method const dominantMethod = Object.entries(anomalyScores).reduce((max, [method, score]) => (score > max.score ? { method, score } : max), { method: '', score: 0 }); return { detected: true, type: 'action_repetition', confidence: Math.min(0.95, combinedAnomalyScore + 0.1), details: `Loop detected via ${dominantMethod.method}: ${(combinedAnomalyScore * 100).toFixed(1)}% anomaly score. Semantic: ${(anomalyScores.semantic_repetition * 100).toFixed(1)}%, Parameter: ${(anomalyScores.parameter_repetition * 100).toFixed(1)}%, Exact: ${(anomalyScores.exact_repetition * 100).toFixed(1)}%, Cyclical: ${(anomalyScores.cyclical_pattern * 100).toFixed(1)}%`, actions_involved: Array.from(uniqueActions), statistical_metrics: { entropy_score: anomalyScores.statistical_anomaly, variance_score: anomalyScores.parameter_repetition, trend_score: anomalyScores.temporal_pattern, cyclicity_score: anomalyScores.cyclical_pattern, }, }; } return { detected: false, confidence: 1 - combinedAnomalyScore, details: `Action diversity acceptable: ${(combinedAnomalyScore * 100).toFixed(1)}% combined anomaly score`, }; } /** * Strategy 2: Domain-Agnostic State Invariance Tracking * Detects when the agent returns to functionally equivalent states */ detectStateInvariance(trace, threshold = 2) { if (!trace.current_context) { return { detected: false, confidence: 0, details: 'No state context available' }; } const currentContext = trace.current_context || 'unknown'; // Extract structured state information from context const stateFeatures = this.extractStateFeatures(currentContext); const currentStateHash = this.hashStateFeatures(stateFeatures); // Also consider recent actions as part of context for better detection const actionContext = trace.recent_actions ? trace.recent_actions.slice(-3).join('->') : ''; const combinedContext = `${currentContext}|${actionContext}`; const combinedStateHash = createHash('md5').update(combinedContext).digest('hex'); // Add both hashes to state history this.stateHistory.push(currentStateHash); this.stateHistory.push(combinedStateHash); if (this.stateHistory.length > this.maxHistorySize) { this.stateHistory.shift(); } // Count occurrences of current state in recent history const currentOccurrences = this.stateHistory.filter((hash) => hash === currentStateHash).length; const combinedOccurrences = this.stateHistory.filter((hash) => hash === combinedStateHash).length; const exactOccurrences = Math.max(currentOccurrences, combinedOccurrences); // Check for semantic state similarity (not just exact matches) const semanticSimilarStates = this.stateHistory.filter((hash) => this.calculateSemanticStateSimilarity(hash, currentStateHash, stateFeatures) > 0.8).length; const totalSimilarStates = Math.max(exactOccurrences, semanticSimilarStates); if (totalSimilarStates >= threshold) { const confidence = Math.min(0.95, 0.7 + (totalSimilarStates - threshold) * 0.1); return { detected: true, type: 'state_invariance', confidence, details: `State revisitation detected: ${totalSimilarStates} similar states found (${exactOccurrences} exact, ${semanticSimilarStates} semantic). Features: ${stateFeatures.slice(0, 3).join(', ')}`, }; } // Check for gradual state convergence (states becoming more similar over time) const convergenceScore = this.detectStateConvergence(stateFeatures); if (convergenceScore > 0.7) { return { detected: true, type: 'state_invariance', confidence: 0.8, details: `State convergence detected: ${(convergenceScore * 100).toFixed(1)}% convergence score indicating minimal progress`, }; } return { detected: false, confidence: 0.8, details: `State appears novel, ${totalSimilarStates} similar states found`, }; } /** * Strategy 3: Enhanced Progress Heuristic Evaluation * Uses advanced time series analysis for stagnation detection */ detectProgressStagnation(trace, windowSize = 6) { if (!trace.step_count || trace.step_count < 3) { return { detected: false, confidence: 0, details: 'Insufficient step history' }; } const actionCount = trace.recent_actions ? trace.recent_actions.length : 0; // Calculate action diversity in recent window if (actionCount > 0) { const recentWindow = trace.recent_actions.slice(-windowSize); const uniqueActionsInWindow = new Set(recentWindow).size; const diversityRatio = uniqueActionsInWindow / recentWindow.length; // Low diversity suggests repetitive behavior if (diversityRatio < 0.4 && recentWindow.length >= 4) { return { detected: true, type: 'progress_stagnation', confidence: 0.8, details: `Low action diversity detected: ${(diversityRatio * 100).toFixed(1)}% unique actions in recent window`, }; } } // Enhanced progress analysis using time series const timeSeriesAnalysis = this.analyzeActionTimeSeries(trace); const progressRate = actionCount / trace.step_count; // Combine multiple stagnation indicators const stagnationScore = Math.max(timeSeriesAnalysis.stagnationScore, timeSeriesAnalysis.cyclicityScore, progressRate < 0.3 ? 0.8 : 0.2); const stagnationThreshold = 0.6; if (stagnationScore > stagnationThreshold && trace.step_count > 5) { const confidence = Math.min(0.95, 0.6 + stagnationScore * 0.3); const details = `Advanced stagnation detected: Stagnation=${(stagnationScore * 100).toFixed(1)}%, Trend=${(timeSeriesAnalysis.trendScore * 100).toFixed(1)}%, Cyclicity=${(timeSeriesAnalysis.cyclicityScore * 100).toFixed(1)}%, Progress rate=${progressRate.toFixed(3)}`; return { detected: true, type: 'progress_stagnation', confidence, details, }; } return { detected: false, confidence: 0.9, details: `Progress trends healthy: Rate=${progressRate.toFixed(3)}, diversity acceptable`, }; } /** * Hybrid loop detection combining all three strategies */ detectLoop(trace, method = 'hybrid') { switch (method) { case 'statistical': return this.detectActionAnomalies(trace); case 'pattern': return this.detectStateInvariance(trace); case 'hybrid': default: const actionResult = this.detectActionAnomalies(trace); const stateResult = this.detectStateInvariance(trace); const progressResult = this.detectProgressStagnation(trace); // Combine results - if any method detects a loop with high confidence, flag it const results = [actionResult, stateResult, progressResult]; const positiveResults = results.filter((r) => r.detected); if (positiveResults.length === 0) { const avgConfidence = results.reduce((sum, r) => sum + r.confidence, 0) / results.length; return { detected: false, confidence: avgConfidence, details: `No loops detected by any method. ${results.map((r) => r.details).join('; ')}`, }; } // Return the highest confidence positive result const bestResult = positiveResults.reduce((best, current) => current.confidence > best.confidence ? current : best); return { ...bestResult, details: `${bestResult.details} (${positiveResults.length}/${results.length} methods agreed)`, }; } } // Helper methods calculateHashSimilarity(hash1, hash2) { if (hash1 === hash2) return 1.0; if (hash1.length !== hash2.length) return 0.0; let matches = 0; for (let i = 0; i < hash1.length; i++) { if (hash1[i] === hash2[i]) matches++; } return matches / hash1.length; } /** * Update configuration for progress indicators and thresholds */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Helper method to calculate action frequencies */ calculateActionFrequencies(actions) { const frequencies = {}; actions.forEach((action) => { frequencies[action] = (frequencies[action] || 0) + 1; }); return frequencies; } /** * Helper method to hash actions for numerical analysis */ hashAction(action) { let hash = 0; for (let i = 0; i < action.length; i++) { const char = action.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash) % 1000; // Normalize to 0-999 range } /** * Advanced time series analysis for detecting complex temporal patterns */ analyzeActionTimeSeries(trace) { const actions = trace.recent_actions; if (actions.length < 4) { return { trendScore: 0, cyclicityScore: 0, stagnationScore: 0 }; } // Convert actions to numerical sequence for analysis const actionSequence = actions.map((a) => this.hashAction(a)); // Calculate trend using linear regression const xValues = actionSequence.map((_, i) => i); const yValues = actionSequence; const n = actionSequence.length; const sumX = xValues.reduce((sum, x) => sum + x, 0); const sumY = yValues.reduce((sum, y) => sum + y, 0); const sumXY = xValues.reduce((sum, x, i) => sum + x * yValues[i], 0); const sumXX = xValues.reduce((sum, x) => sum + x * x, 0); const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); const trendScore = Math.abs(slope) < 0.1 ? 0.8 : 0.2; // Low slope = stagnation // Detect cyclicity using frequency analysis const fft = this.simpleFFT(actionSequence); const dominantFrequency = this.findDominantFrequency(fft); const cyclicityScore = dominantFrequency > 0.3 ? 0.9 : 0.1; // Calculate stagnation using variance const variance = ss.variance(actionSequence); const stagnationScore = variance < 10 ? 0.8 : 0.2; return { trendScore, cyclicityScore, stagnationScore }; } /** * Simple FFT implementation for frequency analysis */ simpleFFT(sequence) { const n = sequence.length; if (n <= 1) return sequence; // Simplified DFT for detecting dominant frequencies const frequencies = []; for (let k = 0; k < n / 2; k++) { let real = 0; let imag = 0; for (let t = 0; t < n; t++) { const angle = (2 * Math.PI * k * t) / n; real += sequence[t] * Math.cos(angle); imag += sequence[t] * Math.sin(angle); } frequencies.push(Math.sqrt(real * real + imag * imag)); } return frequencies; } /** * Find dominant frequency in FFT output */ findDominantFrequency(fft) { if (fft.length === 0) return 0; const max = Math.max(...fft); const total = fft.reduce((sum, val) => sum + val, 0); return total > 0 ? max / total : 0; } /** * Calculate entropy manually since simple-statistics doesn't have it */ calculateEntropy(frequencies) { const total = frequencies.reduce((sum, freq) => sum + freq, 0); if (total === 0) return 0; return frequencies.reduce((entropy, freq) => { if (freq === 0) return entropy; const probability = freq / total; return entropy - probability * Math.log2(probability); }, 0); } /** * Calculate autocorrelation manually */ calculateAutocorrelation(sequence, lag) { if (sequence.length <= lag) return 0; const mean = ss.mean(sequence); const variance = ss.variance(sequence); if (variance === 0) return 0; let correlation = 0; const n = sequence.length - lag; for (let i = 0; i < n; i++) { correlation += (sequence[i] - mean) * (sequence[i + lag] - mean); } return correlation / (n * variance); } /** * Calculate moving average manually */ calculateMovingAverage(sequence, windowSize) { if (sequence.length < windowSize) return []; const result = []; for (let i = 0; i <= sequence.length - windowSize; i++) { const window = sequence.slice(i, i + windowSize); result.push(ss.mean(window)); } return result; } /** * Reset internal state (useful for testing or starting new sessions) */ reset() { this.stateHistory = []; } // Domain-Agnostic Helper Methods /** * Cluster actions by semantic similarity to detect repeated intentions */ clusterSemanticallySimilarActions(actions) { const clusters = []; const processed = new Set(); for (let i = 0; i < actions.length; i++) { if (processed.has(i)) continue; const cluster = [actions[i]]; processed.add(i); for (let j = i + 1; j < actions.length; j++) { if (processed.has(j)) continue; if (this.semanticSimilarity(actions[i], actions[j]) > 0.7) { cluster.push(actions[j]); processed.add(j); } } clusters.push(cluster); } return clusters; } /** * Calculate semantic similarity between two action strings */ semanticSimilarity(action1, action2) { // Extract action name and parameters const parsed1 = this.extractActionParameters(action1); const parsed2 = this.extractActionParameters(action2); // If action names are identical, they're semantically similar if (parsed1.name === parsed2.name) { return 0.8 + this.parameterSimilarity(parsed1.params, parsed2.params) * 0.2; } // Check for semantically similar action names const nameSimilarity = this.tokenSimilarity(parsed1.name, parsed2.name); return nameSimilarity * 0.6 + this.parameterSimilarity(parsed1.params, parsed2.params) * 0.4; } /** * Extract action name and parameters from action string */ extractActionParameters(action) { // Handle various action formats: // "scroll_down(500)" -> name: "scroll_down", params: ["500"] // "click_element button" -> name: "click_element", params: ["button"] // "navigate_to_page" -> name: "navigate_to_page", params: [] const match = action.match(/^([^(]+)(?:\(([^)]*)\))?(.*)$/); if (!match) return { name: action, params: [] }; const name = match[1].trim(); const parenParams = match[2] ? match[2].split(',').map((p) => p.trim()) : []; const spaceParams = match[3] ? match[3] .trim() .split(/\s+/) .filter((p) => p) : []; return { name, params: [...parenParams, ...spaceParams] }; } /** * Calculate similarity between parameter sets */ parameterSimilarity(params1, params2) { if (params1.length === 0 && params2.length === 0) return 1.0; if (params1.length === 0 || params2.length === 0) return 0.0; const intersection = params1.filter((p) => params2.includes(p)); const union = [...new Set([...params1, ...params2])]; return intersection.length / union.length; // Jaccard similarity } /** * Calculate token-level similarity between strings */ tokenSimilarity(str1, str2) { const tokens1 = str1.toLowerCase().split(/[_\s]+/); const tokens2 = str2.toLowerCase().split(/[_\s]+/); const intersection = tokens1.filter((t) => tokens2.includes(t)); const union = [...new Set([...tokens1, ...tokens2])]; return intersection.length / union.length; } /** * Calculate semantic repetition ratio from clustered actions */ calculateSemanticRepetition(clusters, totalActions) { if (totalActions === 0) return 0; // Count actions in clusters with more than one member const repeatedActions = clusters.reduce((count, cluster) => { return cluster.length > 1 ? count + cluster.length : count; }, 0); return repeatedActions / totalActions; } /** * Detect patterns in action parameters */ detectParameterPatterns(actionParams) { if (actionParams.length < 3) return 0; // Group by action name const actionGroups = new Map(); actionParams.forEach(({ name, params }) => { if (!actionGroups.has(name)) actionGroups.set(name, []); actionGroups.get(name).push(params); }); let totalPatterns = 0; let totalComparisons = 0; // Look for parameter patterns within each action group for (const [, paramsList] of actionGroups) { if (paramsList.length < 2) continue; for (let i = 0; i < paramsList.length - 1; i++) { for (let j = i + 1; j < paramsList.length; j++) { totalComparisons++; // Check if parameters are identical or follow a pattern const similarity = this.parameterSimilarity(paramsList[i], paramsList[j]); if (similarity > 0.7) { totalPatterns++; } } } } return totalComparisons > 0 ? totalPatterns / totalComparisons : 0; } /** * Detect cyclical patterns in action sequences */ detectCyclicalPatterns(actions) { if (actions.length < 4) return 0; let maxCyclicity = 0; // Check for cycles of length 2 to actions.length/2 for (let cycleLen = 2; cycleLen <= Math.floor(actions.length / 2); cycleLen++) { let matches = 0; let comparisons = 0; for (let i = 0; i < actions.length - cycleLen; i++) { if (i + cycleLen < actions.length) { comparisons++; if (this.semanticSimilarity(actions[i], actions[i + cycleLen]) > 0.7) { matches++; } } } if (comparisons > 0) { const cyclicity = matches / comparisons; maxCyclicity = Math.max(maxCyclicity, cyclicity); } } return maxCyclicity; } /** * Detect oscillation patterns (A-B-A-B) */ detectOscillationPatterns(actions) { if (actions.length < 4) return 0; let oscillations = 0; let checks = 0; for (let i = 0; i < actions.length - 3; i++) { checks++; if (this.semanticSimilarity(actions[i], actions[i + 2]) > 0.7 && this.semanticSimilarity(actions[i + 1], actions[i + 3]) > 0.7 && this.semanticSimilarity(actions[i], actions[i + 1]) < 0.7) { oscillations++; } } return checks > 0 ? oscillations / checks : 0; } /** * Detect alternating patterns using semantic clusters */ detectAlternatingPatterns(clusters, actions) { if (actions.length < 4) return 0; // Create a mapping from action to cluster ID const actionToCluster = new Map(); clusters.forEach((cluster, clusterId) => { cluster.forEach((action) => actionToCluster.set(action, clusterId)); }); // Convert actions to cluster sequence const clusterSequence = actions.map((action) => actionToCluster.get(action) ?? -1); let alternations = 0; let checks = 0; for (let i = 0; i < clusterSequence.length - 3; i++) { checks++; if (clusterSequence[i] === clusterSequence[i + 2] && clusterSequence[i + 1] === clusterSequence[i + 3] && clusterSequence[i] !== clusterSequence[i + 1]) { alternations++; } } return checks > 0 ? alternations / checks : 0; } /** * Extract domain-agnostic state features from context string */ extractStateFeatures(context) { // Extract various types of state information that might be present const features = []; // Extract numbers (positions, counts, IDs, etc.) const numbers = context.match(/\d+/g) || []; features.push(...numbers.map((n) => `num:${n}`)); // Extract quoted strings (element text, URLs, etc.) const quotedStrings = context.match(/"([^"]+)"/g) || []; features.push(...quotedStrings.map((s) => `text:${s.replace(/"/g, '')}`)); // Extract URLs or paths const urlPattern = /https?:\/\/[^\s]+|\/[^\s]*/g; const urls = context.match(urlPattern) || []; features.push(...urls.map((u) => `url:${u}`)); // Extract key-value pairs (JSON-like or structured data) const keyValuePattern = /(\w+):\s*([^,\s}]+)/g; let match; while ((match = keyValuePattern.exec(context)) !== null) { features.push(`kv:${match[1]}=${match[2]}`); } // Extract common state indicators const stateIndicators = [ 'visible', 'hidden', 'enabled', 'disabled', 'active', 'inactive', 'loading', 'loaded', 'error', 'success', ]; stateIndicators.forEach((indicator) => { if (context.toLowerCase().includes(indicator)) { features.push(`state:${indicator}`); } }); // Extract words that might represent important entities const words = context.toLowerCase().match(/\b\w{3,}\b/g) || []; const commonWords = new Set([ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'how', 'man', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she', 'too', 'use', ]); const importantWords = words.filter((word) => !commonWords.has(word)); features.push(...importantWords.slice(0, 10).map((w) => `word:${w}`)); return features; } /** * Create a hash from state features for comparison */ hashStateFeatures(features) { const sortedFeatures = features.sort().join('|'); return createHash('md5').update(sortedFeatures).digest('hex'); } /** * Calculate semantic similarity between state hashes using their features */ calculateSemanticStateSimilarity(hash1, hash2, currentFeatures) { // For now, we'll use a simple approach - in a real implementation, // you might want to store features alongside hashes if (hash1 === hash2) return 1.0; // Calculate character-level similarity as a proxy for semantic similarity return this.calculateHashSimilarity(hash1, hash2); } /** * Detect if states are converging over time (becoming more similar) */ detectStateConvergence(currentFeatures) { if (this.stateHistory.length < 4) return 0; // Take the last few states and compare their similarity to current state const recentStates = this.stateHistory.slice(-4); let totalSimilarity = 0; let comparisons = 0; for (const stateHash of recentStates) { // This is a simplified approach - in a full implementation, // you'd want to store features alongside hashes totalSimilarity += this.calculateHashSimilarity(stateHash, this.hashStateFeatures(currentFeatures)); comparisons++; } return comparisons > 0 ? totalSimilarity / comparisons : 0; } }