/** * errorRecovery.ts — Autonomous Error Recovery Engine (S31) * * Chiude il loop di autonomia: quando un tool fallisce, il sistema non si limita * a loggare l'errore — controlla agentBrain.errorLog per fix noti e li inietta * come contesto LLM. Quando il tool poi riesce, segna l'errore come risolto. * * PIPELINE: * tool fail ❌ * → recordAndSuggestFix(ctx) * → errorLog.findSimilar(msg) * → se trovato con fix noto → ritorna {hadFix:true, fix} * → agentLoop inietta fix come messaggio LLM * → LLM applica il fix nel prossimo tool call * → tool riesce ✅ * → markToolFixed(hash, appliedFix) se pendingFix noto * → errorLog.markFixed → persiste su Dexie * * INVARIANTI: * - Zero dipendenze da React, EventRuntime o store Zustand * - Tutte le operazioni sync (Dexie write è fire-and-forget nell'errorLog) * - Non lancia mai — catch interno, fallback graceful * - _setTestLog(): DI per unit test (zero vi.mock) * - Fix suggeriti: max 400 chars (safe per context window mobile) * - Auto-track pending fix: Map per auto-markFixed * * WIRING: * agentLoop.ts linea 595 (OpenAI parallel) → recordAndSuggestFix() * agentLoop.ts linea 649 (ReAct path) → recordAndSuggestFix() * agentLoop.ts success path → notifyToolSuccess() * * SESSIONE: S31 — Tool Error Recovery Loop */ import { errorLog as _defaultErrorLog, type ErrorEntry } from "@/lib/agentBrain"; // ─── DI interface per test ──────────────────────────────────────────────────── interface ErrorLogFacade { record(message: string, context: string, lang?: string, code?: string): ErrorEntry; markFixed(hash: string, fix: string): boolean; findSimilar(message: string): ErrorEntry[]; hashOf(message: string): string; } let _log: ErrorLogFacade = _defaultErrorLog; /** * Dependency injection per i test. * Permette di passare un mock errorLog senza vi.mock('@/lib/agentBrain'). * @internal — solo per unit test */ export function _setTestLog(mock: ErrorLogFacade): void { _log = mock; } export function _resetLog(): void { _log = _defaultErrorLog; } // ─── Tipi pubblici ──────────────────────────────────────────────────────────── export interface RecoveryContext { /** Nome del tool che ha fallito (es. "web_search", "write_file") */ toolName: string; /** Risultato raw del tool (inizia con ❌) */ errorMsg: string; /** Linguaggio rilevante se l'errore è di codice */ lang?: string; /** Args passati al tool (per contesto nel fix) */ args?: Record; } export interface RecoveryResult { /** Hash stabile dell'errore normalizzato */ hash: string; /** true = fix noto trovato in errorLog */ hadFix: boolean; /** Testo del fix da iniettare nel contesto LLM (≤400 chars) */ fix?: string; /** Quante volte questo errore è stato visto in sessione */ count: number; /** Suggerimento di strategia alternativa se nessun fix noto */ hint?: string; } // ─── Pending fix tracker ────────────────────────────────────────────────────── // Traccia il fix suggerito per ogni tool, così quando il tool riesce // possiamo chiamare markFixed automaticamente. interface PendingFix { hash: string; fix: string; } const _pendingFixes = new Map(); // ─── Strategie alternative per tool comuni ──────────────────────────────────── const FALLBACK_STRATEGIES: Record = { web_search: "Se web_search fallisce: usa fetch_url con URL diretto, oppure read_page con URL di Wikipedia o sito autorevole.", fetch_url: "Se fetch_url fallisce: il sito potrebbe bloccare i bot. Prova con web_search per trovare un URL alternativo o un mirror.", write_file: "Se write_file fallisce: verifica che il percorso sia assoluto e che la directory esista. Usa list_files per controllare.", read_file: "Se read_file fallisce: il file potrebbe non esistere. Usa list_files per verificare il path esatto.", run_code: "Se run_code fallisce con errore di sintassi: correggi il codice e riprova. Se errore di runtime: semplifica il codice.", list_files: "Se list_files fallisce: usa '.' come directory o verifica che il path non contenga caratteri speciali.", github_push: "Se github_push fallisce: verifica il token GitHub e il nome del repo. Usa i log CI per diagnosticare.", }; // ─── Normalizzazione errore ─────────────────────────────────────────────────── function stripErrorPrefix(msg: string): string { return msg.replace(/^❌\s*/, "").trim(); } // ─── Core: recordAndSuggestFix ──────────────────────────────────────────────── /** * Registra un tool failure su agentBrain.errorLog e, se esiste un fix noto * per un errore simile, lo ritorna per essere iniettato nel contesto LLM. * * È SINCRONO e non lancia mai — safe per uso fire-and-forget in agentLoop. */ export function recordAndSuggestFix(ctx: RecoveryContext): RecoveryResult { const rawMsg = stripErrorPrefix(ctx.errorMsg); try { // 1. Cerca fix noti per errori simili const similar = _log.findSimilar(rawMsg); const known = similar.find(e => e.resolved && e.fix); // 2. Registra l'errore (incrementa contatore, preserva fix esistente) const _code = (ctx.args?.code ?? ctx.args?.command ?? ctx.args?.script) as string | undefined; const entry = _log.record(rawMsg, `tool:${ctx.toolName}`, ctx.lang, _code); // 3. Se fix noto → track pending + ritorna if (known?.fix) { const fix = known.fix.slice(0, 400); _pendingFixes.set(ctx.toolName, { hash: entry.hash, fix }); return { hash: entry.hash, hadFix: true, fix, count: entry.count, }; } // 4. Nessun fix noto → suggerisci strategia alternativa const hint = FALLBACK_STRATEGIES[ctx.toolName]; return { hash: entry.hash, hadFix: false, count: entry.count, ...(hint ? { hint } : {}), }; } catch { // Fallback totale — non crasha agentLoop return { hash: "", hadFix: false, count: 0 }; } } // ─── Notifica successo → auto-markFixed ─────────────────────────────────────── /** * Chiamato quando un tool riesce dopo aver avuto un fix suggerito. * Segna l'errore come risolto su agentBrain.errorLog (persiste su Dexie). * * @param toolName Nome del tool (deve corrispondere alla chiamata precedente) * @param appliedFix Testo del fix applicato (se diverso da quello suggerito) * @returns true se l'errore è stato marcato come risolto */ export function notifyToolSuccess(toolName: string, appliedFix?: string): boolean { const pending = _pendingFixes.get(toolName); if (!pending) return false; try { const fix = (appliedFix ?? pending.fix).slice(0, 1000); const ok = _log.markFixed(pending.hash, fix); _pendingFixes.delete(toolName); return ok; } catch { _pendingFixes.delete(toolName); return false; } } // ─── Utility: build hint message per agentLoop ─────────────────────────────── /** * Costruisce il messaggio da iniettare in loopMessages quando c'è un fix noto. * Sostituisce il messaggio generico "Alcuni tool hanno fallito". */ export function buildRecoveryMessage( failures: Array<{ toolName: string; result: RecoveryResult }>, ): string { if (failures.length === 0) return ""; const parts: string[] = []; for (const { toolName, result } of failures) { if (result.hadFix && result.fix) { parts.push(`💡 Fix noto per "${toolName}" (visto ${result.count}x): ${result.fix}`); } else if (result.hint) { parts.push(`⚡ "${toolName}" ha fallito. ${result.hint}`); } else { parts.push(`⚡ "${toolName}" ha fallito (${result.count}x). Cambia strategia.`); } } return parts.join("\n") + "\n\nContinua il task applicando questi suggerimenti."; } // ─── Utility: pending fix info ──────────────────────────────────────────────── /** Ritorna info sul pending fix per un tool (utile per test) */ export function getPendingFix(toolName: string): PendingFix | undefined { return _pendingFixes.get(toolName); } /** Svuota tutti i pending fixes (utile per test e cleanup sessione) */ export function clearPendingFixes(): void { _pendingFixes.clear(); }