Skip to main content
Glama
cyqlelabs

MCP Dual-Cycle Reasoner

by cyqlelabs
sentinel.ts44 kB
import { CognitiveTrace, LoopDetectionResult, SentinelConfig } from './types.js'; import { createHash } from 'crypto'; import * as ss from 'simple-statistics'; import { semanticAnalyzer } from './semantic-analyzer.js'; 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, semantic_intents: config.semantic_intents || [ 'performing action', 'checking status', 'retrieving information', 'processing data', 'handling error', 'completing task', 'initiating process', 'validating result', 'organizing information', 'communicating result', ], }; } /** * 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 */ async detectActionAnomalies( trace: CognitiveTrace & { recent_actions: string[] }, windowSize: number = 10, sessionId?: string ): Promise<LoopDetectionResult> { if ( !trace.recent_actions || !Array.isArray(trace.recent_actions) || trace.recent_actions.length === 0 ) { return { detected: false, confidence: 0, details: {}, }; } // Use configurable minimum actions threshold to avoid false positives on legitimate exploration const minActionsForDetection = Math.max( this.config.min_actions_for_detection, Math.floor(windowSize * 0.3) ); if (trace.recent_actions.length < minActionsForDetection) { return { detected: false, confidence: 0, details: {}, }; } const recentActions = trace.recent_actions.slice(-windowSize); // PERFORMANCE OPTIMIZATION: Compute similarity matrix once for all semantic operations const similarityMatrix = await semanticAnalyzer.computeSimilarityMatrix( recentActions, sessionId ); // Domain-agnostic semantic similarity analysis using precomputed matrix const semanticClusters = this.clusterWithPrecomputedSimilarity(recentActions, similarityMatrix); 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(recentActions, semanticClusters); // 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 using precomputed similarities const cyclicalScore = await this.detectCyclicalPatterns( recentActions, sessionId, similarityMatrix ); // Check for oscillating patterns using precomputed similarities const oscillationScore = await this.detectOscillationPatterns( recentActions, sessionId, similarityMatrix ); // 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 = await this.checkProgressIndicators(recentActions, sessionId); // 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.25; // Even more sensitive for better detection 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 } ); // Identify specific actions involved in the loop based on dominant method const specificActionsInvolved = this.getActionsInvolvedInLoop( dominantMethod.method, recentActions, semanticClusters ); return { detected: true, type: 'action_repetition', confidence: Math.min(0.95, combinedAnomalyScore + 0.3), details: { dominant_method: dominantMethod.method, anomaly_score: combinedAnomalyScore, actions_involved_count: specificActionsInvolved.length, recent_actions_count: recentActions.length, metrics: anomalyScores, }, actions_involved: specificActionsInvolved, 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: { anomaly_score: combinedAnomalyScore, metrics: anomalyScores, }, statistical_metrics: { entropy_score: anomalyScores.statistical_anomaly, variance_score: anomalyScores.parameter_repetition, trend_score: anomalyScores.temporal_pattern, cyclicity_score: anomalyScores.cyclical_pattern, }, }; } /** * Strategy 2: Domain-Agnostic State Invariance Tracking * Detects when the agent returns to functionally equivalent states */ detectStateInvariance( trace: CognitiveTrace & { recent_actions: string[] }, threshold: number = 2, windowSize: number = 10 ): LoopDetectionResult { if (!trace.current_context) { return { detected: false, confidence: 0, details: {}, }; } // Use same action history requirements as detectActionAnomalies for consistency const minActionsForDetection = Math.max( this.config.min_actions_for_detection, Math.floor(windowSize * 0.3) ); if (trace.recent_actions.length < minActionsForDetection) { return { detected: false, confidence: 0, details: {}, }; } 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 && Array.isArray(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); // Get actions that led to state revisitation (recent actions that brought us back to similar state) const recentActions = trace.recent_actions.slice(-windowSize); const actionCounts = new Map<string, number>(); recentActions.forEach((action) => { actionCounts.set(action, (actionCounts.get(action) || 0) + 1); }); const stateInvarianceActions = Array.from(actionCounts.entries()) .filter(([, count]) => count > 1) .map(([action]) => action); return { detected: true, type: 'state_invariance', confidence, details: { metrics: { total_similar_states: totalSimilarStates, exact_occurrences: exactOccurrences, semantic_similar_states: semanticSimilarStates, features: stateFeatures.slice(0, 3).join(', '), }, }, actions_involved: stateInvarianceActions, }; } // Check for gradual state convergence (states becoming more similar over time) const convergenceScore = this.detectStateConvergence(stateFeatures); if (convergenceScore > 0.7) { // Get actions involved in state convergence (recent actions) const recentActions = trace.recent_actions.slice(-windowSize); const actionCounts = new Map<string, number>(); recentActions.forEach((action) => { actionCounts.set(action, (actionCounts.get(action) || 0) + 1); }); const convergenceActions = Array.from(actionCounts.entries()) .filter(([, count]) => count > 1) .map(([action]) => action); return { detected: true, type: 'state_invariance', confidence: 0.8, details: { metrics: { convergence_score: convergenceScore, }, }, actions_involved: convergenceActions, }; } return { detected: false, confidence: 0.8, details: { metrics: { total_similar_states: totalSimilarStates, }, }, }; } /** * Strategy 3: Enhanced Progress Heuristic Evaluation * Uses domain-agnostic analysis with progressive thresholds for stagnation detection */ async detectProgressStagnation( trace: CognitiveTrace & { recent_actions: string[] }, windowSize: number = 8, sessionId?: string, similarityMatrix?: number[][] ): Promise<LoopDetectionResult> { const minActionsForDetection = Math.max( this.config.min_actions_for_detection, Math.floor(windowSize * 0.3) ); if ( !trace.recent_actions || !Array.isArray(trace.recent_actions) || trace.recent_actions.length < minActionsForDetection ) { return { detected: false, confidence: 0, details: {}, }; } const actionCount = trace.recent_actions.length; // Multi-window analysis: short-term vs long-term patterns const shortWindow = trace.recent_actions.slice(-Math.min(5, actionCount)); const longWindow = trace.recent_actions.slice(-Math.min(windowSize, actionCount)); // Calculate diversity for both windows const shortDiversity = new Set(shortWindow).size / shortWindow.length; const longDiversity = new Set(longWindow).size / longWindow.length; // Progressive threshold: becomes more lenient with more actions (allows for longer exploration) const baseThreshold = 0.25; // More permissive base threshold const progressiveFactor = Math.min(0.1, (actionCount - 6) * 0.01); // Gradual increase const diversityThreshold = baseThreshold + progressiveFactor; // Advanced pattern analysis with optional precomputed similarity matrix const timeSeriesAnalysis = this.analyzeActionTimeSeries(trace); const actionChangeVelocity = this.calculateActionChangeVelocity(trace.recent_actions); const semanticVariation = await this.calculateSemanticVariation( trace.recent_actions, similarityMatrix ); // Multi-factor stagnation score const diversityScore = 1 - longDiversity; const stagnationScore = diversityScore * 0.4 + timeSeriesAnalysis.stagnationScore * 0.3 + (1 - actionChangeVelocity) * 0.2 + (1 - semanticVariation) * 0.1; // Dynamic threshold that adapts to action count const dynamicThreshold = Math.min(0.75, 0.55 + (actionCount - 8) * 0.015); // Primary detection: extremely low diversity (likely stuck) if (longDiversity < diversityThreshold && longWindow.length >= 6) { const actionCounts = new Map<string, number>(); longWindow.forEach((action) => { actionCounts.set(action, (actionCounts.get(action) || 0) + 1); }); const lowDiversityActions = Array.from(actionCounts.entries()) .filter(([, count]) => count > 1) .map(([action]) => action); const confidence = Math.min(0.9, 0.6 + (1 - longDiversity) * 0.8); return { detected: true, type: 'progress_stagnation', confidence, details: { metrics: { diversity: longDiversity, threshold: diversityThreshold, actions_analyzed: longWindow.length, }, }, actions_involved: lowDiversityActions, }; } // Secondary detection: multi-factor stagnation analysis if (stagnationScore > dynamicThreshold && actionCount >= minActionsForDetection) { const confidence = Math.min(0.95, 0.5 + stagnationScore * 0.5); const recentActions = trace.recent_actions.slice(-windowSize); const actionCounts = new Map<string, number>(); recentActions.forEach((action) => { actionCounts.set(action, (actionCounts.get(action) || 0) + 1); }); const stagnationActions = Array.from(actionCounts.entries()) .filter(([, count]) => count > 1) .map(([action]) => action); return { detected: true, type: 'progress_stagnation', confidence, details: { metrics: { stagnation_score: stagnationScore, diversity: longDiversity, velocity: actionChangeVelocity, variation: semanticVariation, threshold: dynamicThreshold, }, }, actions_involved: stagnationActions, }; } return { detected: false, confidence: 0.8, details: { metrics: { diversity: longDiversity, stagnation_score: stagnationScore, velocity: actionChangeVelocity, }, }, }; } /** * Hybrid loop detection combining all three strategies */ async detectLoop( trace: CognitiveTrace & { recent_actions: string[] }, method: 'statistical' | 'pattern' | 'hybrid' = 'hybrid', windowSize: number = 10, sessionId?: string ): Promise<LoopDetectionResult> { switch (method) { case 'statistical': return await this.detectActionAnomalies(trace, windowSize, sessionId); case 'pattern': return this.detectStateInvariance(trace, 2, windowSize); case 'hybrid': default: const actionResult = await this.detectActionAnomalies(trace, windowSize, sessionId); const stateResult = this.detectStateInvariance(trace, 2, windowSize); const progressResult = await this.detectProgressStagnation(trace, windowSize, sessionId); // 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: { metrics: { action_anomaly_score: actionResult.details.anomaly_score, state_invariance_confidence: stateResult.confidence, progress_stagnation_score: progressResult.details.metrics?.stagnation_score, }, }, }; } // Return the highest confidence positive result const bestResult = positiveResults.reduce((best, current) => current.confidence > best.confidence ? current : best ); return { ...bestResult, details: { ...bestResult.details, }, }; } } // 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 & { recent_actions: string[] }): { trendScore: number; cyclicityScore: number; stagnationScore: number; } { const actions = trace.recent_actions; if (!actions || !Array.isArray(actions) || actions.length < 4) { return { trendScore: 0, cyclicityScore: 0, stagnationScore: 0 }; } // Convert actions to numerical sequence for analysis const actionSequence = actions.map((a: string) => this.hashAction(a)); // Calculate trend using linear regression const xValues = actionSequence.map((_: number, i: number) => i); const yValues = actionSequence; const n = actionSequence.length; const sumX = xValues.reduce((sum: number, x: number) => sum + x, 0); const sumY = yValues.reduce((sum: number, y: number) => sum + y, 0); const sumXY = xValues.reduce((sum: number, x: number, i: number) => sum + x * yValues[i], 0); const sumXX = xValues.reduce((sum: number, x: number) => 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 = []; } /** * Identify specific actions involved in the loop based on dominant detection method */ private getActionsInvolvedInLoop( dominantMethod: string, recentActions: string[], semanticClusters: string[][] ): string[] { switch (dominantMethod) { case 'semantic_repetition': // Return actions from the largest semantic cluster (most repeated semantic actions) const largestCluster = semanticClusters.reduce( (max, cluster) => (cluster.length > max.length ? cluster : max), [] ); return largestCluster.length > 1 ? [...new Set(largestCluster)] : []; case 'parameter_repetition': // Return actions with repeated parameters using existing logic return this.getParameterRepeatedActions(recentActions, semanticClusters); case 'exact_repetition': // Return actions that appear multiple times exactly const actionCounts = new Map<string, number>(); recentActions.forEach((action) => { actionCounts.set(action, (actionCounts.get(action) || 0) + 1); }); return [...new Set(recentActions.filter((action) => actionCounts.get(action)! > 1))]; case 'cyclical_pattern': case 'oscillation_pattern': case 'alternating_pattern': // For pattern-based detection, return actions from the most frequent semantic cluster const dominantCluster = semanticClusters.reduce( (max, cluster) => (cluster.length > max.length ? cluster : max), [] ); return dominantCluster.length > 1 ? [...new Set(dominantCluster)] : []; default: // Fallback: return actions from largest semantic cluster const defaultCluster = semanticClusters.reduce( (max, cluster) => (cluster.length > max.length ? cluster : max), [] ); return defaultCluster.length > 1 ? [...new Set(defaultCluster)] : []; } } /** * Get actions with repeated parameters by leveraging existing parameter detection logic */ private getParameterRepeatedActions( recentActions: string[], semanticClusters: string[][] ): string[] { const actionToParams = new Map<string, string[]>(); recentActions.forEach((action) => { actionToParams.set(action, this.extractActionParameters(action).params); }); const repeatedActions: string[] = []; // For each semantic cluster, find parameters with high similarity for (const cluster of semanticClusters) { if (cluster.length < 2) continue; for (let i = 0; i < cluster.length - 1; i++) { for (let j = i + 1; j < cluster.length; j++) { const params1 = actionToParams.get(cluster[i])!; const params2 = actionToParams.get(cluster[j])!; const similarity = this.parameterSimilarity(params1, params2); if (similarity > 0.7) { repeatedActions.push(cluster[i]); repeatedActions.push(cluster[j]); } } } } return [...new Set(repeatedActions)]; } // Domain-Agnostic Helper Methods /** * PERFORMANCE OPTIMIZED: Cluster actions using precomputed similarity matrix */ private clusterWithPrecomputedSimilarity( actions: string[], similarityMatrix: number[][] ): string[][] { if (actions.length === 0) return []; if (actions.length === 1) return [actions]; 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; // Use precomputed similarity instead of individual model call if (similarityMatrix[i][j] > 0.7) { cluster.push(actions[j]); processed.add(j); } } clusters.push(cluster); } return clusters; } /** * Calculate semantic similarity between two action strings using semantic analyzer */ private async semanticSimilarity( action1: string, action2: string, sessionId?: string ): Promise<number> { const result = await semanticAnalyzer.calculateSemanticSimilarity(action1, action2, sessionId); return result.similarity; } /** * Extract action name and parameters from action string */ private extractActionParameters(action: string): { name: string; params: string[] } { // Handle various action formats and normalize action names by extracting underscored parameters. // "click_element_by_index_index_0" -> name: "click_element_by_index", params: ["index_0"] const match = action.match(/^([^(]+)(?:\(([^)]*)\))?(.*)$/); if (!match) return { name: action, params: [] }; const originalName = 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) : []; // A regex to find and extract underscore-appended parameters like _index_0 or _id_12345 const paramRegex = /_([a-zA-Z][a-zA-Z0-9]*)_(\d+)$/; let name = originalName; const potentialParams: string[] = []; // Repeatedly match to handle multiple appended parameters like _x_1_y_2 let regexMatch; while ((regexMatch = name.match(paramRegex))) { // Found a parameter-like suffix const paramKey = regexMatch[1]; const paramValue = regexMatch[2]; // Add the full suffix to params to preserve info, e.g., "index_0" potentialParams.unshift(`${paramKey}_${paramValue}`); // Shorten the name by removing the matched suffix name = name.substring(0, regexMatch.index); } const allParams = [...parenParams, ...spaceParams, ...potentialParams]; return { name, params: allParams }; } /** * 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(recentActions: string[], semanticClusters: string[][]): number { if (recentActions.length < 3) return 0; const actionToParams = new Map<string, string[]>(); recentActions.forEach((action) => { actionToParams.set(action, this.extractActionParameters(action).params); }); let totalPatterns = 0; let totalComparisons = 0; // Look for parameter patterns within each semantic cluster for (const cluster of semanticClusters) { if (cluster.length < 2) continue; const paramsList = cluster.map((action) => actionToParams.get(action)!); 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; } /** * PERFORMANCE OPTIMIZED: Detect cyclical patterns using precomputed similarity matrix */ private async detectCyclicalPatterns( actions: string[], sessionId?: string, similarityMatrix?: number[][] ): Promise<number> { if (actions.length < 4) return 0; // Use existing matrix or compute once if not provided const matrix = similarityMatrix || (await semanticAnalyzer.computeSimilarityMatrix(actions, sessionId)); 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++; // Use precomputed similarity instead of individual model call if (matrix[i][i + cycleLen] > 0.7) { matches++; } } } if (comparisons > 0) { const cyclicity = matches / comparisons; maxCyclicity = Math.max(maxCyclicity, cyclicity); } } return maxCyclicity; } /** * PERFORMANCE OPTIMIZED: Detect oscillation patterns using precomputed similarity matrix */ private async detectOscillationPatterns( actions: string[], sessionId?: string, similarityMatrix?: number[][] ): Promise<number> { if (actions.length < 4) return 0; // Use existing matrix or compute once if not provided const matrix = similarityMatrix || (await semanticAnalyzer.computeSimilarityMatrix(actions, sessionId)); let oscillations = 0; let checks = 0; for (let i = 0; i < actions.length - 3; i++) { checks++; // Use precomputed similarities instead of individual model calls const sim1 = matrix[i][i + 2]; const sim2 = matrix[i + 1][i + 3]; const sim3 = matrix[i][i + 1]; if (sim1 > 0.7 && sim2 > 0.7 && sim3 < 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; } /** * Calculate the velocity of action changes (domain-agnostic pattern analysis) */ private calculateActionChangeVelocity(actions: string[]): number { if (actions.length < 3) return 0.5; let changes = 0; for (let i = 1; i < actions.length; i++) { if (actions[i] !== actions[i - 1]) { changes++; } } return actions.length > 1 ? changes / (actions.length - 1) : 0; } /** * PERFORMANCE OPTIMIZED: Calculate semantic variation using fast embedding-based diversity */ private async calculateSemanticVariation( actions: string[], similarityMatrix?: number[][] ): Promise<number> { if (actions.length < 4) return 0.5; // Use existing matrix or compute if not provided const matrix = similarityMatrix || (await semanticAnalyzer.computeSimilarityMatrix(actions)); // Calculate diversity based on average pairwise similarity let totalSimilarity = 0; let comparisons = 0; for (let i = 0; i < actions.length; i++) { for (let j = i + 1; j < actions.length; j++) { totalSimilarity += matrix[i][j]; comparisons++; } } const avgSimilarity = comparisons > 0 ? totalSimilarity / comparisons : 0; // High average similarity = low variation, low similarity = high variation const variation = 1 - avgSimilarity; // Calculate temporal evolution by comparing first and second half if (actions.length >= 8) { const midPoint = Math.floor(actions.length / 2); let firstHalfSim = 0; let secondHalfSim = 0; let firstHalfComps = 0; let secondHalfComps = 0; // Average similarity within first half for (let i = 0; i < midPoint; i++) { for (let j = i + 1; j < midPoint; j++) { firstHalfSim += matrix[i][j]; firstHalfComps++; } } // Average similarity within second half for (let i = midPoint; i < actions.length; i++) { for (let j = i + 1; j < actions.length; j++) { secondHalfSim += matrix[i][j]; secondHalfComps++; } } const firstAvg = firstHalfComps > 0 ? firstHalfSim / firstHalfComps : 0; const secondAvg = secondHalfComps > 0 ? secondHalfSim / secondHalfComps : 0; const evolution = Math.abs(firstAvg - secondAvg); return Math.max(0, Math.min(1, variation * 0.8 + evolution * 0.2)); } return Math.max(0, Math.min(1, variation)); } /** * PERFORMANCE OPTIMIZED: Check for progress indicators using batch processing */ private async checkProgressIndicators( recentActions: string[], sessionId?: string ): Promise<boolean> { if (this.config.progress_indicators.length === 0) { return false; } // Batch compute similarities between all actions and all indicators const allTexts = [...recentActions, ...this.config.progress_indicators]; const similarityMatrix = await semanticAnalyzer.computeSimilarityMatrix(allTexts, sessionId); const actionCount = recentActions.length; // Check if any action is similar to any progress indicator for (let i = 0; i < actionCount; i++) { for (let j = actionCount; j < allTexts.length; j++) { if (similarityMatrix[i][j] > 0.7) { return true; } } } return false; } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyqlelabs/mcp-dual-cycle-reasoner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server