AUDIT / src /lib /agent /errorRecovery.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
9.2 kB
/**
* 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<toolName, {hash, fix}> 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<string, unknown>;
}
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<string, PendingFix>();
// ─── Strategie alternative per tool comuni ────────────────────────────────────
const FALLBACK_STRATEGIES: Record<string, string> = {
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();
}