AUDIT / src /lib /adaptiveSolver.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
22.8 kB
/**
* adaptiveSolver.ts — Adaptive Problem Solver (S50)
*
* Pattern: quando un'azione fallisce per un BLOCCO SPECIFICO,
* non si ripete ne si chiede aiuto. Si IDENTIFICA il blocco
* e si ESEGUE un'alternativa nota automaticamente.
*
* Esempio reale che ispira questa feature:
* Operazione push bloccata → rilevato blocco "permission" →
* strategia "permission_rest" → usa API REST invece → successo.
*
* Strategie built-in (7):
* 1. cors_proxy — fetch CORS bloccato → 3 proxy in ordine
* 2. file_fuzzy — file not found → ricerca fuzzy VFS
* 3. rate_limit_wait — 429 → backoff esponenziale + retry
* 4. chunk_payload — payload troppo grande → suggerimento chunking
* 5. permission_rest — operazione negata → equivalente REST API
* 6. network_retry — errore rete transitorio → retry timeout aumentato
* 7. syntax_hint — errore sintattico → analisi riga + hint preciso
*
* Invarianti:
* - Safari-safe: no SharedArrayBuffer, no Worker
* - DI via _setTestDeps() per unit test senza vi.mock
* - Max 2 tentativi per blocco (no loop infiniti)
* - Ogni strategia e IDEMPOTENTE: fallire non peggiora lo stato
* - Emette sempre un messaggio leggibile su cosa sta facendo
*/
import { vfsAsync } from "@/lib/vfsDb";
import { agentMemory } from "@/lib/agentMemory";
// ─── Tipi pubblici (estratti in adaptiveSolverTypes.ts — S552 — re-esportati per backward compat) ──
import type { BlockadeKind, Blockade, SolveResult } from "./adaptiveSolverTypes";
export type { BlockadeKind, Blockade, SolveResult } from "./adaptiveSolverTypes";
interface Strategy {
id: string;
label: string;
handles: BlockadeKind[];
tryFix(
blockade: Blockade,
originalInput: string,
context: string,
): Promise<string | null>;
}
// ─── DI per test ─────────────────────────────────────────────────────────────
interface SolverDeps {
vfsListFn: () => Promise<{ path: string; content: string }[]>;
memorySetFn: (key: string, val: string, cat: string) => void;
/** Gap-5: lettura last_winner e win-count dal ring buffer di agentMemory.
* Separato da memorySetFn per DI pulita nei test — il reader può essere mockato
* indipendentemente dallo writer (pattern read-write separation). */
memoryGetFn: (key: string) => string | undefined;
fetchFn: (url: string, opts?: RequestInit) => Promise<Response>;
waitFn: (ms: number) => Promise<void>;
}
let _deps: SolverDeps = {
vfsListFn: () => vfsAsync.list() as Promise<{ path: string; content: string }[]>,
memorySetFn: (key, val, cat) => agentMemory.set(key, val, cat),
memoryGetFn: (key) => agentMemory.get(key) ?? undefined, // Fix-5a: agentMemory.get()→string|null; interfaccia dichiara string|undefined → null coerced
fetchFn: (url, opts) => fetch(url, opts),
waitFn: (ms) => new Promise(res => setTimeout(res, ms)),
};
/** Dependency injection per unit test */
export function _setTestDeps(deps: Partial<SolverDeps>): void {
_deps = { ..._deps, ...deps };
}
// ─── Rilevamento blocco ───────────────────────────────────────────────────────
const BLOCKADE_PATTERNS: Array<{ re: RegExp; kind: BlockadeKind; hint: string }> = [
{ re: /cors|cross.?origin|blocked by cors|no.*access-control/i, kind: "cors", hint: "Richiesta bloccata da policy CORS" },
{ re: /404|not found|no such file|cannot find|enoent|file non trovato/i, kind: "not_found", hint: "Risorsa non trovata — path errato o file mancante" },
{ re: /429|rate limit|too many requests/i, kind: "rate_limit", hint: "Rate limit raggiunto" },
{ re: /403|401|access denied|permission denied|not allowed|unauthorized/i, kind: "permission", hint: "Operazione non consentita" },
{ re: /timeout|timed out|aborted|etimedout/i, kind: "timeout", hint: "Timeout — operazione troppo lenta" },
{ re: /quota|storage full|no space|exceeded.*storage/i, kind: "quota", hint: "Storage quota superata" },
{ re: /syntaxerror|unexpected token|unterminated|parse error/i, kind: "syntax", hint: "Errore di sintassi" },
{ re: /network error|failed to fetch|net::err|econnrefused/i, kind: "network", hint: "Errore di rete" },
// Fix-1.1: import_error sblocca dependency_probe (riga ~332) — era mai attivata
{ re: /ModuleNotFoundError|No module named|ImportError|Cannot find module|Module not found/i, kind: "import_error", hint: "Libreria mancante — necessita installazione" },
];
export function detectBlockade(errorMessage: string): Blockade {
for (const { re, kind, hint } of BLOCKADE_PATTERNS) {
if (re.test(errorMessage)) {
return { kind, message: errorMessage.slice(0, 300), hint };
}
}
return { kind: "unknown", message: errorMessage.slice(0, 300), hint: "Errore non riconosciuto" };
}
// ─── Helper: Proxy CORS ───────────────────────────────────────────────────────
// S320-FIX: corsproxy.io rimosso (403 pagamento obbligatorio — test 2026-06-01)
const CORS_PROXIES: Array<(u: string) => string> = [
u => "https://api.allorigins.win/get?url=" + encodeURIComponent(u),
u => "https://api.codetabs.com/v1/proxy?quest=" + encodeURIComponent(u),
u => "https://api.allorigins.win/raw?url=" + encodeURIComponent(u),
];
async function viaProxy(url: string, timeoutMs = 8000): Promise<string> {
for (const make of CORS_PROXIES) {
try {
const ctrl = new AbortController();
const tid = setTimeout(() => ctrl.abort(), timeoutMs);
const res = await _deps.fetchFn(make(url), { signal: ctrl.signal });
clearTimeout(tid);
if (!res.ok) continue;
const raw = await res.text();
if (raw.startsWith('{"contents"')) {
const d = JSON.parse(raw) as { contents?: string };
if ((d.contents?.length ?? 0) > 20) return d.contents!;
} else if (raw.length > 20) {
return raw;
}
} catch { continue; }
}
throw new Error("Tutti i proxy CORS falliti");
}
// ─── Helper: fuzzy match VFS ──────────────────────────────────────────────────
async function fuzzyFindPath(target: string): Promise<string | null> {
const files = await _deps.vfsListFn();
if (!files.length) return null;
const t = target.toLowerCase().replace(/^\/+/, "");
const exact = files.find(f => f.path === target || f.path === "/" + t);
if (exact) return exact.path;
const fname = t.split("/").pop() ?? t;
const byName = files.find(f => f.path.toLowerCase().endsWith("/" + fname));
if (byName) return byName.path;
const bySub = files.find(f => f.path.toLowerCase().includes(fname));
return bySub?.path ?? null;
}
// ─── Strategie ────────────────────────────────────────────────────────────────
const STRATEGIES: Strategy[] = [
// 1. CORS → proxy automatico
{
id: "cors_proxy", label: "Proxy CORS automatico", handles: ["cors", "network"],
async tryFix(_blockade, originalInput) {
const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/);
if (!m) return null;
const url = m[0].replace(/[,;)]+$/, "");
try {
const content = await viaProxy(url);
return "✅ [proxy CORS] " + url + "\n" + content.slice(0, 3000);
} catch { return null; }
},
},
// 2. Not found → fuzzy search VFS
{
id: "file_fuzzy", label: "Ricerca fuzzy file nel workspace", handles: ["not_found"],
async tryFix(_blockade, originalInput) {
const m = originalInput.match(/['"]([/a-zA-Z0-9_\-\.]+\.[a-zA-Z]{1,5})['"]/);
if (!m) return null;
const searchPath = m[1];
const found = await fuzzyFindPath(searchPath);
if (!found) {
const list = (await _deps.vfsListFn()).slice(0, 10).map(f => f.path).join(", ");
return "❌ File \"" + searchPath + "\" non trovato.\nFile disponibili nel workspace: " + (list || "nessuno");
}
const file = (await _deps.vfsListFn()).find(f => f.path === found);
const content = (file?.content ?? "").slice(0, 3000);
return "✅ [file trovato] " + found + "\n" + content;
},
},
// 3. Rate limit → backoff esponenziale
{
id: "rate_limit_wait", label: "Backoff esponenziale e retry", handles: ["rate_limit"],
async tryFix(_blockade, originalInput) {
const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/);
if (!m) return null;
const url = m[0].replace(/[,;)]+$/, "");
// S324: cap a 2s iniziale (era 2s), abort dopo 4s — evita watchdog kill su iOS Safari
// (JS fermo >5s può essere terminato dal watchdog; 2+2=4s totale di attesa massima)
await _deps.waitFn(2000);
try {
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 4000); // S324: 10000→4000ms, Safari watchdog safe
const res = await _deps.fetchFn(url, { signal: ctrl.signal });
if (res.status === 429) { await _deps.waitFn(2000); return null; } // S324: 5000→2000ms
if (!res.ok) return null;
return "✅ [retry dopo backoff] " + url + "\n" + (await res.text()).slice(0, 2000);
} catch { return null; }
},
},
// 4. Quota/timeout → suggerimento chunking
{
id: "chunk_payload", label: "Suddivisione in chunk", handles: ["quota", "timeout"],
async tryFix(_blockade, originalInput) {
if (originalInput.length < 5000) return null;
const CHUNK = 3000;
const parts = Math.ceil(originalInput.length / CHUNK);
return (
"⚠️ Input troppo grande (" + originalInput.length + " chars) — strategia chunk.\n" +
"Dividi in " + parts + " chunk da " + CHUNK + " chars ciascuno.\n" +
"Chunk 1:\n" + originalInput.slice(0, CHUNK)
);
},
},
// 5. Permission negata → alternativa REST API
{
id: "permission_rest", label: "Alternativa via REST API", handles: ["permission"],
async tryFix(blockade, originalInput, context) {
const lc = (originalInput + " " + context).toLowerCase();
if (lc.includes("push") || lc.includes("commit")) {
return [
"⚠️ Operazione bloccata. Alternativa: usa API REST.",
"Per operazioni su repository:",
" 1. POST /repos/OWNER/REPO/git/blobs → carica contenuto file",
" 2. POST /repos/OWNER/REPO/git/trees → crea albero con i blob",
" 3. POST /repos/OWNER/REPO/git/commits → crea commit",
" 4. PATCH /repos/OWNER/REPO/git/refs/heads/main → aggiorna branch",
"Usa fetch con Authorization: token TOKEN_VALUE",
].join("\n");
}
if (lc.includes("localstorage") || lc.includes("storage")) {
return "⚠️ Storage bloccato. Alternativa: IndexedDB via Dexie (vfsDb.ts) o sessionStorage.";
}
if (lc.includes("clipboard")) {
return "⚠️ Clipboard API bloccata. Alternativa: mostra testo in <textarea> con selectAll() per copia manuale.";
}
return "⚠️ " + blockade.hint + "\nCerca API equivalente o workaround browser-safe.";
},
},
// 6. Errore di rete → retry con timeout aumentato
{
id: "network_retry", label: "Retry con timeout aumentato", handles: ["network", "timeout"],
async tryFix(_blockade, originalInput) {
const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/);
if (!m) return null;
const url = m[0].replace(/[,;)]+$/, "");
await _deps.waitFn(1500);
try {
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 15000);
const res = await _deps.fetchFn(url, { signal: ctrl.signal });
if (!res.ok) return null;
return "✅ [retry rete] " + url + "\n" + (await res.text()).slice(0, 2000);
} catch { return null; }
},
},
// 7. Errore di sintassi → analisi riga + hint preciso
{
id: "syntax_hint", label: "Analisi errore sintattico", handles: ["syntax"],
async tryFix(blockade, originalInput) {
const lines = originalInput.split("\n");
const m = blockade.message.match(/line (\d+)|:(\d+):/);
const lineNum = m ? parseInt(m[1] ?? m[2]) - 1 : -1;
const hint = (lineNum >= 0 && lineNum < lines.length)
? "Riga " + (lineNum + 1) + ": \"" + (lines[lineNum]?.trim().slice(0, 150) ?? "") + "\"" // S609: 80→150
: "Posizione non determinata";
return (
"⚠️ Errore sintattico — " + hint + "\n" +
blockade.message.slice(0, 300) + "\n" + // S609: 200→300
"Controlla: parentesi bilanciate, virgole mancanti, backtick non chiusi, import mancanti."
);
},
},
// S697-8: Arg mutation — cache-bust URL, shorter query, minimal args
{
id: "arg_mutate", label: "Mutazione argomenti (cache-bust, query corta)", handles: ["rate_limit", "timeout", "not_found"],
async tryFix(_blockade: Blockade, originalInput: string) {
const urlM = originalInput.match(/https?:\/\/[^\s"'`\)]+/);
if (urlM) {
const url = urlM[0].replace(/[,;)]+$/, "");
const busted = url + (url.includes("?") ? "&" : "?") + "_cb=" + Date.now();
try {
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 6000);
const r = await _deps.fetchFn(busted, { signal: ctrl.signal });
if (r.ok) return "✅ [cache-bust] " + busted + "\n" + (await r.text()).slice(0, 2000);
} catch { /* continua */ }
const base = url.split("?")[0];
if (base !== url) {
try {
const ctrl2 = new AbortController();
setTimeout(() => ctrl2.abort(), 5000);
const r2 = await _deps.fetchFn(base, { signal: ctrl2.signal });
if (r2.ok) return "✅ [no-query] " + base + "\n" + (await r2.text()).slice(0, 2000);
} catch { /* continua */ }
}
}
const qM = originalInput.match(/query[s:"']+([^"'\n]{10,})/i);
if (qM) {
const short = qM[1].trim().split(" ").slice(0, 6).join(" ");
return `💡 Riprova con query più corta: "${short}"`;
}
return null;
},
},
// S697-9: Tool substitute — mappa 20+ tool con alternative funzionali
{
id: "tool_substitute", label: "Sostituzione tool con alternativa funzionale", handles: ["timeout", "network", "cors", "permission", "rate_limit"],
async tryFix(_blockade: Blockade, _originalInput: string, context: string) {
const SUBS: Record<string, { alt: string; hint: string }> = {
"web_search": { alt: "fetch_url", hint: "Usa fetch_url con URL diretto" },
"fetch_url": { alt: "web_search", hint: "Usa web_search con la query" },
"get_weather": { alt: "fetch_url", hint: "Usa fetch_url su wttr.in" },
"get_news": { alt: "web_search", hint: "Usa web_search site:ansa.it" },
"get_stock": { alt: "fetch_url", hint: "Usa fetch_url su Yahoo Finance API" },
"run_code": { alt: "run_python", hint: "Prova run_python come alternativo" },
"run_python": { alt: "run_code", hint: "Prova run_code (Node.js) come alternativo" },
"translate": { alt: "web_search", hint: "Usa web_search per traduzione" },
"screenshot_url":{ alt: "fetch_url", hint: "Usa fetch_url + parse HTML" },
};
const toolM = context.match(/tool[:s"']+([a-z_]+)/i);
if (!toolM) return null;
const sub = SUBS[toolM[1]];
if (!sub) return null;
return "🔄 Tool fallito. Alternativa: " + sub.alt + ". " + sub.hint + ".";
},
},
// S697-10: Dependency probe — verifica disponibilità su npm/PyPI
{
id: "dependency_probe", label: "Verifica dipendenza su npm/PyPI", handles: ["dependency", "import_error", "not_found"],
async tryFix(blockade: Blockade, originalInput: string) {
const modM = blockade.message.match(/(?:module|package|import|require)s+["']?([a-zA-Z0-9@/._-]+)["']?/i);
if (!modM) return null;
const mod = modM[1];
const isPy = /python|.py/.test(originalInput.toLowerCase());
const url = isPy
? "https://pypi.org/pypi/" + encodeURIComponent(mod) + "/json"
: "https://registry.npmjs.org/" + encodeURIComponent(mod) + "/latest";
try {
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);
const res = await _deps.fetchFn(url, { signal: ctrl.signal });
if (res.ok) {
const d = await res.json() as Record<string, unknown>;
const v = isPy ? ((d["info"] as Record<string,string>)?.["version"]) : ((d as Record<string,string>)["version"]);
return "✅ " + mod + " disponibile" + (v ? " (v" + v + ")" : "") + ". Installa: " + (isPy ? "pip install " + mod : "npm install " + mod);
}
if (res.status === 404) return "❌ " + mod + " non trovato su " + (isPy ? "PyPI" : "npm") + ".";
} catch { /* timeout non-blocking */ }
return null;
},
},
];
// ─── AdaptiveSolver ───────────────────────────────────────────────────────────
class AdaptiveSolverImpl {
private readonly _maxAttempts = 2;
/**
* Dato un errore e l'input originale che lo ha causato:
* 1. Rileva il tipo di blocco
* 2. Trova le strategie applicabili
* 3. Prova in ordine finché una riesce
* 4. Memorizza il vincitore in agentMemory
*/
async solve(
errorMessage: string,
originalInput = "",
context = "",
_triedIds: string[] = [], // S697: already-tried strategy IDs for anti-repetition
): Promise<SolveResult> {
const blockade = detectBlockade(errorMessage);
if (blockade.kind === "unknown") {
return {
success: false,
output: errorMessage,
strategy: "none",
description: "Nessuna strategia adattiva disponibile.",
};
}
// Gap-5: score-based reorder — last_winner prioritizzato, poi win-count storico, poi ordine fisso.
// Evita di sprecare il budget di 2 tentativi (maxAttempts) su strategie storicamente perdenti.
const _winnerKey = "adaptive:" + blockade.kind + ":last_winner";
const _countKey = "adaptive:" + blockade.kind + ":win_counts";
const _lastWinner = (() => { try { return _deps.memoryGetFn(_winnerKey); } catch { return undefined; } })();
const _winCounts: Record<string, number> = (() => {
try { const raw = _deps.memoryGetFn(_countKey); return raw ? (JSON.parse(raw) as Record<string,number>) : {}; } catch { return {}; }
})();
const strategies = STRATEGIES
.filter(s => s.handles.includes(blockade.kind) && !_triedIds.includes(s.id))
.map(s => ({
s,
score: s.id === _lastWinner ? 100 // L1: confirmed last winner
: (_winCounts[s.id] ?? 0) > 0 ? _winCounts[s.id] // L2: historical wins
: 0, // L3: no history
}))
.sort((a, b) => b.score - a.score || 0) // stable: ties keep STRATEGIES insertion order
.map(x => x.s);
for (let i = 0; i < Math.min(strategies.length, this._maxAttempts); i++) {
const strategy = strategies[i];
try {
const result = await strategy.tryFix(blockade, originalInput, context);
if (result !== null) {
try {
// Gap-5: scrivi last_winner + incrementa win_count per scoring persistente
_deps.memorySetFn("adaptive:" + blockade.kind + ":last_winner", strategy.id, "adaptive_solver");
const _prevCounts: Record<string,number> = (() => {
try { const r = _deps.memoryGetFn("adaptive:" + blockade.kind + ":win_counts"); return r ? JSON.parse(r) as Record<string,number> : {}; } catch { return {}; }
})();
_prevCounts[strategy.id] = (_prevCounts[strategy.id] ?? 0) + 1;
_deps.memorySetFn("adaptive:" + blockade.kind + ":win_counts", JSON.stringify(_prevCounts), "adaptive_solver");
} catch { /* fire-and-forget */ }
return {
success: !result.startsWith("❌"),
output: result,
strategy: strategy.id,
description: blockade.hint + " → " + strategy.label,
};
}
} catch { continue; }
}
return {
success: false,
output: "⚠️ " + blockade.hint + "\nNessuna strategia alternativa ha avuto successo.",
strategy: "exhausted",
description: "Blocco \"" + blockade.kind + "\" — strategie esaurite.",
};
}
detect(errorMessage: string): Blockade {
return detectBlockade(errorMessage);
}
strategiesFor(kind: BlockadeKind): string[] {
return STRATEGIES.filter(s => s.handles.includes(kind)).map(s => s.label);
}
}
export const adaptiveSolver = new AdaptiveSolverImpl();
export default adaptiveSolver;
// ─── System prompt snippet ────────────────────────────────────────────────────
/**
* Insegna il pattern adattivo all'LLM.
* Viene iniettato nel system prompt di agentLoop.ts (S50).
*/
export const ADAPTIVE_SOLVER_SYSTEM_HINT = `
## PROBLEM SOLVING ADATTIVO — Regola fondamentale
Quando un'azione fallisce per un blocco specifico, NON ripetere la stessa azione.
Identifica il blocco e usa l'alternativa:
| Blocco rilevato | Alternativa da usare |
|---|---|
| Operazione push bloccata | API REST: fetch su api.github.com (blobs → trees → commits → refs) |
| fetch CORS bloccato | proxy: allorigins.win, codetabs.com (corsproxy.io rimosso: 403) |
| file not found (404/ENOENT) | usa read_file con path esatto da vfs.list() |
| rate limit (429) | aspetta 2s, riprova o cambia endpoint |
| operazione negata (403/401) | cerca API equivalente browser-safe o REST alternativa |
| storage quota superata | chunking — dividi dati in parti da max 3000 chars |
| errore di sintassi | identifica riga esatta, correggi solo quella, non riscrivere tutto |
| errore di rete generico | retry dopo 1.5s con timeout 15s, poi usa proxy |
Comunica sempre: "X bloccato → uso Y invece" prima di eseguire l'alternativa.
`.trim();