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
JavaScript
/**
* 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