/** * actionSimulator.ts — S-PRED: Predizione esito tool prima dell'esecuzione * * Consulta lo storico errori (errorLog.recent — già in-memory, zero I/O) * e produce un "outcome prediction" PRIMA della tool call. * * Zero LLM calls, Safari-safe, additive, non-blocking. * Integrato in case2Block.ts prima di executeToolGated. * * @module actionSimulator */ import type { ErrorEntry } from "../agentBrain"; export interface ActionPrediction { toolName: string; predictedOutcome: "likely_success" | "likely_fail" | "uncertain"; reason: string; similarPastFailures: number; } // ── Tool rischiosi — modificano stato filesystem/git/rete ───────────────────── const RISKY_TOOLS = new Set([ "apply_patch", "write_file", "iterate_file", "move_file", "delete_file", "git_push", "git_push_multiple", "run_code", "run_terminal_cmd", "run_python", ]); function _kwFromText(text: string): string[] { return text .toLowerCase() .replace(/[^a-z0-9_./-]+/g, " ") .split(" ") .filter(w => w.length > 3) .slice(0, 15); } function _kwSim(a: string[], b: string[]): number { if (a.length === 0 || b.length === 0) return 0; const sa = new Set(a); return b.filter(w => sa.has(w)).length / Math.max(a.length, b.length); } /** * Predice l'esito di un tool call usando errori recenti in-memory. * * @param toolName Nome del tool da eseguire * @param args Argomenti del tool * @param pastErrors Errori recenti (da errorLog.recent(50)) */ export function predictOutcome( toolName: string, args: Record, pastErrors: ErrorEntry[], ): ActionPrediction { // Tool non rischiosi → sempre likely_success senza analisi if (!RISKY_TOOLS.has(toolName)) { return { toolName, predictedOutcome: "likely_success", reason: "tool non rischioso", similarPastFailures: 0 }; } const argSig = JSON.stringify(args).slice(0, 150); const argKw = _kwFromText(argSig); // Filtra errori non risolti per questo tool const toolErrors = pastErrors.filter(e => !e.resolved && e.context.includes(toolName)); // U6: path identity → similarity = 1.0 (exact path match = stesso file → forte segnale) // U6: threshold Jaccard 0.35 → 0.25 (più sensibile su argomenti parzialmente overlapping) const _argPath = String(args.path ?? args.file_path ?? ""); const similar = toolErrors.filter(e => { if (_argPath && e.context.includes(_argPath)) return true; // path identity return _kwSim(argKw, _kwFromText(e.message + " " + e.context)) > 0.25; }); if (similar.length >= 2) { return { toolName, predictedOutcome: "likely_fail", reason: `${similar.length} fallimenti simili non risolti su "${toolName}"`, similarPastFailures: similar.length, }; } if (toolErrors.length >= 4 && similar.length >= 1) { return { toolName, predictedOutcome: "likely_fail", reason: `${toolErrors.length} errori non risolti su "${toolName}" (1 con args simili)`, similarPastFailures: similar.length, }; } return { toolName, predictedOutcome: "uncertain", reason: toolErrors.length > 0 ? `${toolErrors.length} errori passati su "${toolName}", nessuno simile agli args correnti` : "nessun pattern noto", similarPastFailures: similar.length, }; } /** * Bypass immediato di REVISION_THRESHOLD: se la predizione era "likely_fail" * e l'esito reale è un errore → attiva belief revision al primo fallimento confermato. */ export function shouldReviseImmediately( predicted: ActionPrediction, actualFailed: boolean, ): boolean { return predicted.predictedOutcome === "likely_fail" && actualFailed; }