UNPKG

erosolar-cli

Version:

Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning

477 lines (474 loc) 17 kB
/** * Intelligent Failure Detection and Recovery System * * Detects non-working solutions and applies AlphaZero-style learning to: * 1. Identify failure patterns * 2. Generate alternative approaches * 3. Store successful recoveries for reuse * 4. Optionally update source code when patterns are reliable * * Principal Investigator: Bo Shang */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { homedir } from 'node:os'; import { recordFailurePattern, addToolPattern, getFailurePatternsToAvoid, getBestToolPattern, } from './learningPersistence.js'; import { classifyTaskType, } from './alphaZeroEngine.js'; // ============================================================================ // FAILURE DETECTION // ============================================================================ const FAILURE_INDICATORS = { build: [ /error\s+TS\d+/i, // TypeScript errors /SyntaxError/i, // Syntax errors /Cannot find module/i, // Missing modules /error:\s+/i, // Generic errors /failed with exit code/i, // Build failures /npm ERR!/i, // npm errors ], test: [ /FAIL\s+/, // Jest failures /AssertionError/i, // Assertion failures /Expected.*but got/i, // Value mismatches /\d+ failing/i, // Test count failures /Error: expect/i, // Expect errors ], runtime: [ /TypeError:/i, /ReferenceError:/i, /RangeError:/i, /Uncaught/i, /ENOENT/i, /EACCES/i, /ETIMEDOUT/i, ], userFeedback: [ /doesn't work/i, /didn't work/i, /not working/i, /still broken/i, /wrong/i, /incorrect/i, /that's not right/i, /try again/i, /failed/i, ], }; /** * Detect failure from output/response */ export function detectFailure(output, context) { // Check for build failures for (const pattern of FAILURE_INDICATORS.build) { if (pattern.test(output)) { return { type: 'build', severity: 'critical', message: extractErrorMessage(output, pattern), context: { pattern: pattern.source }, timestamp: new Date().toISOString(), }; } } // Check for test failures for (const pattern of FAILURE_INDICATORS.test) { if (pattern.test(output)) { return { type: 'test', severity: 'major', message: extractErrorMessage(output, pattern), context: { pattern: pattern.source }, timestamp: new Date().toISOString(), }; } } // Check for runtime errors for (const pattern of FAILURE_INDICATORS.runtime) { if (pattern.test(output)) { return { type: 'tool', severity: 'major', message: extractErrorMessage(output, pattern), context: { pattern: pattern.source }, timestamp: new Date().toISOString(), }; } } // Check for user feedback indicating failure if (context.userMessage) { for (const pattern of FAILURE_INDICATORS.userFeedback) { if (pattern.test(context.userMessage)) { return { type: 'user-feedback', severity: 'major', message: context.userMessage.slice(0, 200), timestamp: new Date().toISOString(), }; } } } // Check for tool call failures if (context.toolCalls) { const failures = context.toolCalls.filter(t => !t.success); if (failures.length > 0 && failures.length / context.toolCalls.length > 0.3) { return { type: 'tool', severity: 'major', message: `${failures.length}/${context.toolCalls.length} tool calls failed`, context: { failedTools: failures.map(t => t.name) }, timestamp: new Date().toISOString(), }; } } return null; } function extractErrorMessage(output, pattern) { const match = output.match(pattern); if (!match) return 'Unknown error'; // Get the line containing the match and a few surrounding lines const lines = output.split('\n'); for (let i = 0; i < lines.length; i++) { if (pattern.test(lines[i])) { const start = Math.max(0, i); const end = Math.min(lines.length, i + 3); return lines.slice(start, end).join('\n').slice(0, 300); } } return match[0].slice(0, 200); } const recentActions = []; const MAX_ACTION_HISTORY = 20; /** * Record an action and detect if we're in a loop */ export function recordActionAndDetectLoop(action) { const now = Date.now(); // Add to history recentActions.push({ action, timestamp: now }); // Keep only recent actions while (recentActions.length > MAX_ACTION_HISTORY) { recentActions.shift(); } // Detect loops: same action appearing 3+ times in last 10 actions const recent = recentActions.slice(-10); const actionCounts = new Map(); for (const r of recent) { actionCounts.set(r.action, (actionCounts.get(r.action) || 0) + 1); } for (const [act, count] of actionCounts) { if (count >= 3) { return { type: 'loop-detected', severity: 'major', message: `Detected repeated action: "${act}" (${count} times in last ${recent.length} actions)`, context: { repeatedAction: act, count }, timestamp: new Date().toISOString(), }; } } return null; } /** * Clear action history (call after successful task completion) */ export function clearActionHistory() { recentActions.length = 0; } // ============================================================================ // RECOVERY STRATEGIES // ============================================================================ const RECOVERY_STRATEGIES_FILE = join(homedir(), '.erosolar', 'learning', 'recovery-strategies.json'); function loadRecoveryStrategies() { const defaultData = { version: 1, strategies: [], }; try { if (!existsSync(RECOVERY_STRATEGIES_FILE)) { return defaultData; } const content = readFileSync(RECOVERY_STRATEGIES_FILE, 'utf-8'); return JSON.parse(content); } catch { return defaultData; } } function saveRecoveryStrategies(data) { const dir = dirname(RECOVERY_STRATEGIES_FILE); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(RECOVERY_STRATEGIES_FILE, JSON.stringify(data, null, 2)); } /** * Find a recovery strategy for a failure type */ export function findRecoveryStrategy(failure) { const data = loadRecoveryStrategies(); // Look for matching strategy const candidates = data.strategies.filter(s => { if (s.failureType !== failure.type) return false; // Check if error pattern matches try { const pattern = new RegExp(s.errorPattern, 'i'); return pattern.test(failure.message); } catch { return s.errorPattern.toLowerCase().includes(failure.message.toLowerCase().slice(0, 50)); } }); if (candidates.length === 0) return null; // Return strategy with best success rate that has been used enough const reliable = candidates.filter(s => s.timesUsed >= 2 && s.successRate >= 0.5); if (reliable.length > 0) { return reliable.sort((a, b) => b.successRate - a.successRate)[0]; } return candidates[0]; } /** * Record a successful recovery strategy */ export function recordRecoveryStrategy(failure, recoverySteps, success) { const data = loadRecoveryStrategies(); // Create error pattern signature const errorPattern = createErrorSignature(failure.message); // Find existing strategy const existing = data.strategies.find(s => s.failureType === failure.type && s.errorPattern === errorPattern); if (existing) { existing.timesUsed++; existing.successRate = (existing.successRate * (existing.timesUsed - 1) + (success ? 1 : 0)) / existing.timesUsed; if (success) { existing.lastSuccess = new Date().toISOString(); // Merge recovery steps if different for (const step of recoverySteps) { if (!existing.recoverySteps.includes(step)) { existing.recoverySteps.push(step); } } } } else { data.strategies.push({ id: `recovery-${Date.now()}`, failureType: failure.type, errorPattern, recoverySteps, successRate: success ? 1 : 0, timesUsed: 1, lastSuccess: success ? new Date().toISOString() : '', }); } // Keep top 100 strategies data.strategies.sort((a, b) => b.successRate * b.timesUsed - a.successRate * a.timesUsed); if (data.strategies.length > 100) { data.strategies.length = 100; } saveRecoveryStrategies(data); } function createErrorSignature(message) { // Extract key parts of error message return message .replace(/\d+/g, 'N') // Replace numbers .replace(/['"`].*?['"`]/g, 'S') // Replace quoted strings .replace(/\/\S+/g, 'P') // Replace paths .slice(0, 100); } // ============================================================================ // GENERATE RECOVERY PROMPTS // ============================================================================ /** * Generate a prompt for recovering from failure */ export function generateRecoveryPrompt(failure, originalApproach, taskType) { // Check for existing recovery strategy const strategy = findRecoveryStrategy(failure); // Check for failure patterns to avoid const avoidPatterns = getFailurePatternsToAvoid(taskType); // Get best tool pattern for this task type const bestPattern = getBestToolPattern(taskType); let prompt = `The previous approach failed. Here's what went wrong:\n\n`; prompt += `FAILURE TYPE: ${failure.type}\n`; prompt += `ERROR: ${failure.message}\n\n`; prompt += `ORIGINAL APPROACH:\n${originalApproach.slice(0, 500)}\n\n`; if (strategy) { prompt += `KNOWN RECOVERY STRATEGY (${Math.round(strategy.successRate * 100)}% success rate):\n`; for (const step of strategy.recoverySteps) { prompt += `- ${step}\n`; } prompt += '\n'; } if (avoidPatterns.length > 0) { prompt += `AVOID THESE APPROACHES (known to fail):\n`; for (const p of avoidPatterns.slice(0, 3)) { prompt += `- ${p.avoidanceHint}\n`; } prompt += '\n'; } if (bestPattern) { prompt += `RECOMMENDED TOOL SEQUENCE for ${taskType}:\n`; prompt += `${bestPattern.toolSequence.join(' → ')}\n\n`; } prompt += `Generate an alternative approach that:\n`; prompt += `1. Addresses the root cause of the failure\n`; prompt += `2. Uses a different strategy than the original\n`; prompt += `3. Avoids known failure patterns\n`; prompt += `4. Can be verified to work\n`; return prompt; } // ============================================================================ // SOURCE CODE FIX TRACKING // ============================================================================ const SOURCE_FIXES_FILE = join(homedir(), '.erosolar', 'learning', 'source-fixes.json'); function loadSourceFixes() { const defaultData = { version: 1, fixes: [] }; try { if (!existsSync(SOURCE_FIXES_FILE)) return defaultData; return JSON.parse(readFileSync(SOURCE_FIXES_FILE, 'utf-8')); } catch { return defaultData; } } function saveSourceFixes(data) { const dir = dirname(SOURCE_FIXES_FILE); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(SOURCE_FIXES_FILE, JSON.stringify(data, null, 2)); } /** * Record a potential source code fix */ export function recordPotentialSourceFix(filePath, description, originalCode, fixedCode, failureType) { const data = loadSourceFixes(); // Check for similar fix const existing = data.fixes.find(f => f.filePath === filePath && f.fixedCode === fixedCode); if (existing) { existing.validatedCount++; } else { data.fixes.push({ id: `fix-${Date.now()}`, filePath, description, originalCode: originalCode.slice(0, 1000), fixedCode: fixedCode.slice(0, 1000), failureType, validatedCount: 1, appliedToSource: false, }); } saveSourceFixes(data); } /** * Get all source code fixes */ export function getSourceCodeFixes() { const data = loadSourceFixes(); return data.fixes; } /** * Get validated source fixes that should be applied */ export function getValidatedSourceFixes(minValidations = 3) { const data = loadSourceFixes(); return data.fixes.filter(f => f.validatedCount >= minValidations && !f.appliedToSource); } /** * Mark a fix as applied to source */ export function markFixApplied(fixId) { const data = loadSourceFixes(); const fix = data.fixes.find(f => f.id === fixId); if (fix) { fix.appliedToSource = true; saveSourceFixes(data); } } // ============================================================================ // APPLY LEARNED FIXES TO EROSOLAR SOURCE // ============================================================================ /** * Check if we should suggest applying fixes to erosolar source */ export function suggestSourceUpdates(workingDir) { const validatedFixes = getValidatedSourceFixes(); if (validatedFixes.length === 0) { return { shouldSuggest: false, fixes: [], reason: 'No fixes have been validated enough times yet.', }; } // Check if we're in the erosolar repo const isErosolarRepo = workingDir.includes('erosolar') || existsSync(join(workingDir, 'src/shell/interactiveShell.ts')); if (!isErosolarRepo) { return { shouldSuggest: false, fixes: validatedFixes, reason: 'Not in erosolar repository. Run /learn commit to save fixes for later.', }; } return { shouldSuggest: true, fixes: validatedFixes, reason: `${validatedFixes.length} validated fix(es) ready to apply.`, }; } /** * Generate commit message for learning-based updates */ export function generateLearningCommitMessage(fixes) { const types = [...new Set(fixes.map(f => f.failureType))]; const files = [...new Set(fixes.map(f => f.filePath.split('/').pop()))]; return `fix: apply AlphaZero-learned improvements Automated fixes based on ${fixes.length} validated recovery patterns: - Failure types addressed: ${types.join(', ')} - Files affected: ${files.join(', ')} These fixes were learned from repeated successful recoveries and have been validated ${Math.min(...fixes.map(f => f.validatedCount))}+ times each. 🤖 Auto-generated by erosolar-cli AlphaZero learning system`; } /** * Main entry point for failure recovery * Call this when a failure is detected */ export function initiateRecovery(failure, originalApproach, toolCalls, userQuery) { const taskType = classifyTaskType(userQuery); // Record the failure recordFailurePattern(taskType, toolCalls.map(t => t.name), failure.message, `Avoid this approach for ${failure.type} errors`); // Find or generate recovery strategy const strategy = findRecoveryStrategy(failure); // Generate recovery prompt const recoveryPrompt = generateRecoveryPrompt(failure, originalApproach, taskType); return { recoveryPrompt, strategy, }; } /** * Record successful recovery for future use */ export function recordSuccessfulRecovery(failure, recoveryApproach, toolCalls, userQuery, qualityScore) { const taskType = classifyTaskType(userQuery); // Record the successful recovery strategy const steps = toolCalls.map(t => `${t.name}: ${t.success ? 'success' : 'failed'}`); recordRecoveryStrategy(failure, steps, true); // Record successful tool pattern if (toolCalls.length > 0) { addToolPattern(taskType, { taskType, toolSequence: toolCalls.map(t => t.name), successRate: 1, avgDuration: toolCalls.reduce((sum, t) => sum + t.duration, 0), occurrences: 1, }); } // Clear action history since we succeeded clearActionHistory(); } //# sourceMappingURL=failureRecovery.js.map