/** * crossCriticBlock.ts — P-CROSS-CRITIC: verifica incrociata multi-modello * * Tier 4 del sistema di verifica (dopo postResponseVerifier Tier 3). * Si attiva SOLO per task ad alta complessità (complexity >= 7) dove * GoalVerifier non era pienamente sicuro (routeQualityScore < 0.85). * * Architettura: * - Fire-and-forget: si esegue DOPO che la risposta è già visibile all'utente * - Provider fisso: Groq llama-3.1-8b-instant (veloce, economico, diverso dal main) * - Timeout: 5000ms con AbortController — fail-open silenzioso * - Rate limit: max 1 per sessione browser (module-level flag, reset su ricarica) * - Output: recordError (selfLearning) + badge opzionale — mai blocca UI * * Trigger: * complexity >= 7 AND routeQualityScore < 0.85 * (task davvero complesso + GoalVerifier non pienamente sicuro) * * Invarianti: * - Mai lancia eccezioni — ogni path ha try/catch * - Non modifica mai la risposta visibile all'utente * - Non usa callBudgetCoordinator (turno già concluso al momento del trigger) * - Fail-open totale: timeout/rate-limit/token-missing → noop silenzioso * - Safari-safe: no Worker, no SharedArrayBuffer * - Zero regressione sul path normale (complexity < 7 → mai invocato) */ import * as telemetry from "../agentTelemetry"; const CRITIC_MODEL = "llama-3.1-8b-instant"; const GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"; const CRITIC_TIMEOUT = 5000; const CRITIC_MAX_TOK = 60; /** Rate limiter per-sessione: max 1 call critica per caricamento pagina. */ let _criticUsedThisSession = false; /** * Tenta di riservare il slot critica per questa sessione. * Chiamare PRIMA di scheduleCrossCritic — se false, non schedulare. * @returns true se disponibile, false se già usato questa sessione. */ export function acquireCriticCall(): boolean { if (_criticUsedThisSession) return false; _criticUsedThisSession = true; return true; } /** Solo per unit test — non usare in produzione. */ export function _resetCriticRateLimit(): void { _criticUsedThisSession = false; } export interface CrossCriticOpts { finalText: string; lastUserMsg: string; routeQualityScore: number; groqToken: string | null; recordError: (error: string, context: string, hint: string) => void; scheduleIdle: (fn: () => void, delayMs?: number) => void; onBadge?: (status: "ok" | "warn", label: string) => void; } function _buildCriticPrompt(lastUserMsg: string, finalText: string): string { const q = lastUserMsg.slice(0, 200).replace(/\n/g, " "); const a = finalText.slice(0, 900).replace(/\n/g, " "); return [ "You are a concise quality reviewer. Evaluate this AI response.", `USER QUESTION: ${q}`, `AI RESPONSE (truncated): ${a}`, "", "Reply with exactly ONE of:", "PASS", "FAIL: ", "UNCERTAIN", "", "No explanation. No formatting. One line only.", ].join("\n"); } async function _callCriticLlm( prompt: string, groqToken: string, ): Promise<"pass" | "fail" | "uncertain"> { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), CRITIC_TIMEOUT); try { const res = await fetch(GROQ_ENDPOINT, { method: "POST", signal: ctrl.signal, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${groqToken}`, }, body: JSON.stringify({ model: CRITIC_MODEL, max_tokens: CRITIC_MAX_TOK, temperature: 0.0, messages: [{ role: "user", content: prompt }], }), }); if (!res.ok) return "uncertain"; const data = await res.json() as { choices?: Array<{ message?: { content?: string } }>; }; const text = (data.choices?.[0]?.message?.content ?? "").trim().toUpperCase(); if (text.startsWith("PASS")) return "pass"; if (text.startsWith("FAIL")) return "fail"; return "uncertain"; } catch { return "uncertain"; // timeout o network error — fail-open } finally { clearTimeout(timer); } } /** * Schedula la verifica critica fire-and-forget. * Chiamare DOPO processCase3FinalBlock (risposta già streamata all'utente). * Usa scheduleIdle per non bloccare il render post-risposta. * * @param opts - Parametri iniettati (no import diretto da selfLearning/agentLoop) */ export function scheduleCrossCritic(opts: CrossCriticOpts): void { const { finalText, lastUserMsg, routeQualityScore, groqToken, recordError, scheduleIdle, onBadge, } = opts; if (!groqToken || !finalText || !lastUserMsg) return; scheduleIdle(async () => { try { const prompt = _buildCriticPrompt(lastUserMsg, finalText); const verdict = await _callCriticLlm(prompt, groqToken); telemetry.record("crossCritic", verdict); if (verdict === "fail") { try { recordError( `CROSS_CRITIC_FAIL: task complesso (score=${Math.round(routeQualityScore * 100)}%) segnalato come errato dal critic cross-model`, lastUserMsg.slice(0, 80), "Su task ad alta complessità, verifica proattivamente la correttezza della risposta prima di concludere.", ); } catch { /* non-blocking */ } try { onBadge?.("warn", "⚠ Cross-model: verifica manuale"); } catch { /* non-blocking */ } } else if (verdict === "pass") { try { onBadge?.("ok", "✓ Cross-model OK"); } catch { /* non-blocking */ } } // UNCERTAIN → noop silenzioso (non abbastanza informazioni per giudicare) } catch { /* fail-open totale — nessuna eccezione propaga */ } }, 1000); // 1000ms delay — più tardivo di postResponseVerify (500ms) per non competere su rete }