UNPKG

@cyqlelabs/mcp-dual-cycle-reasoner

Version:

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

911 lines (764 loc) 30.8 kB
import { CognitiveTrace, LoopDetectionResult, SentinelConfig } from './types.js'; import { createHash } from 'crypto'; import * as ss from 'simple-statistics'; export class Sentinel { private stateHistory: string[] = []; private readonly maxHistorySize: number = 20; private config: SentinelConfig; constructor(config: Partial<SentinelConfig> = {}) { 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 */ private detectStatisticalAnomalies(actions: string[]): number { 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 */ private detectTemporalPatterns(actions: string[]): number { 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: CognitiveTrace, windowSize: number = 10): LoopDetectionResult { 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 as keyof typeof weights]; }, 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: CognitiveTrace, threshold: number = 2): LoopDetectionResult { 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: CognitiveTrace, windowSize: number = 6): LoopDetectionResult { 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: CognitiveTrace, method: 'statistical' | 'pattern' | 'hybrid' = 'hybrid' ): LoopDetectionResult { 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 private calculateHashSimilarity(hash1: string, hash2: string): number { 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: Partial<SentinelConfig>): void { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration */ getConfig(): SentinelConfig { return { ...this.config }; } /** * Helper method to calculate action frequencies */ private calculateActionFrequencies(actions: string[]): Record<string, number> { const frequencies: Record<string, number> = {}; actions.forEach((action) => { frequencies[action] = (frequencies[action] || 0) + 1; }); return frequencies; } /** * Helper method to hash actions for numerical analysis */ private hashAction(action: string): number { 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 */ private analyzeActionTimeSeries(trace: CognitiveTrace): { trendScore: number; cyclicityScore: number; stagnationScore: number; } { 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 */ private simpleFFT(sequence: number[]): number[] { const n = sequence.length; if (n <= 1) return sequence; // Simplified DFT for detecting dominant frequencies const frequencies: number[] = []; 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 */ private findDominantFrequency(fft: number[]): number { 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 */ private calculateEntropy(frequencies: number[]): number { 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 */ private calculateAutocorrelation(sequence: number[], lag: number): number { 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 */ private calculateMovingAverage(sequence: number[], windowSize: number): number[] { if (sequence.length < windowSize) return []; const result: number[] = []; 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(): void { this.stateHistory = []; } // Domain-Agnostic Helper Methods /** * Cluster actions by semantic similarity to detect repeated intentions */ private clusterSemanticallySimilarActions(actions: string[]): string[][] { const clusters: string[][] = []; const processed = new Set<number>(); 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 */ private semanticSimilarity(action1: string, action2: string): number { // 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 */ private extractActionParameters(action: string): { name: string; params: string[] } { // 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 */ private parameterSimilarity(params1: string[], params2: string[]): number { 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 */ private tokenSimilarity(str1: string, str2: string): number { 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 */ private calculateSemanticRepetition(clusters: string[][], totalActions: number): number { 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 */ private detectParameterPatterns(actionParams: Array<{ name: string; params: string[] }>): number { if (actionParams.length < 3) return 0; // Group by action name const actionGroups = new Map<string, string[][]>(); 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 */ private detectCyclicalPatterns(actions: string[]): number { 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) */ private detectOscillationPatterns(actions: string[]): number { 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 */ private detectAlternatingPatterns(clusters: string[][], actions: string[]): number { if (actions.length < 4) return 0; // Create a mapping from action to cluster ID const actionToCluster = new Map<string, number>(); 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 */ private extractStateFeatures(context: string): string[] { // Extract various types of state information that might be present const features: string[] = []; // 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 */ private hashStateFeatures(features: string[]): string { const sortedFeatures = features.sort().join('|'); return createHash('md5').update(sortedFeatures).digest('hex'); } /** * Calculate semantic similarity between state hashes using their features */ private calculateSemanticStateSimilarity( hash1: string, hash2: string, currentFeatures: string[] ): number { // 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) */ private detectStateConvergence(currentFeatures: string[]): number { 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; } }