// S536: emitFinalText — GAP-2: gestione hard block "no-proof" // // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // GAP-2 change vs versione precedente: // - runFinalOutputGate ora può restituire pass:false reason:"no-proof" // (claim critica senza prova verificata in proofLedger) // - Questo file gestisce "no-proof" emettendo NO_PROOF_RETRY_TEXT // (messaggio specifico, diverso da SAFE_FALLBACK_TEXT) // - "no-proof" != "garbage/empty": il testo era valido, ma le azioni // dichiarate non hanno prova. Il messaggio informa l'utente che // l'agente ritenterà e spiega perché. // // Zero regressioni: i path garbage/empty sono invariati. // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ import { runFinalOutputGate, type GateVerdict } from "./finalOutputGate"; import { detectClaimedCategories, checkClaimsAgainstLedger } from "./actionClaimDetector"; import { PROOF_VERIFIED_TOOLS, TOOL_TO_CATEGORY_LABEL } from "./proofToolRegistry"; import { proofLedger } from "./proofLedger"; import type { AgentStep } from "../storage"; export const EMIT_CHUNK_SIZE = 100; // S765 Gap 1: filtro garbage per chunk in-stream const _STREAM_GARBAGE_RE = /[^\x09\x0A\x0D\x20-\x7E\u00A0-\u024F\u0370-\u03FF]{4,}/g; // ─── Messaggi di fallback ───────────────────────────────────────────────────── /** Emesso per pass:false reason:"garbage"|"empty" — output irrecuperabile */ const SAFE_FALLBACK_TEXT = "⚠️ Non sono riuscito a generare una risposta valida in questo turno " + "(output non leggibile o vuoto). Riprova oppure riformula la richiesta."; /** * GAP-2: Emesso per pass:false reason:"no-proof". * L'agente ha DICHIARATO di aver completato un'azione critica * (scrittura file, push Git, deploy) ma proofLedger non ha prova verificata. * Il messaggio: * 1. Informa l'utente in modo trasparente * 2. Spiega il meccanismo di self-healing * 3. Chiede di riprovare (il ciclo agente può re-triggerare il tool) * * Design: testo markdown-friendly, visibile anche in plain text. */ const NO_PROOF_RETRY_TEXT = "🔄 **Verifica azione non superata — retry automatico richiesto.**\n\n" + "L'agente ha dichiarato di aver completato un'azione critica " + "(scrittura file / push Git / deploy), ma il sistema non ha trovato " + "conferma verificata nel registro prove.\n\n" + "**Cosa succede ora:** il sistema annullerà l'output e rieseguirà " + "l'azione dal principio per garantire che sia stata completata " + "con prova concreta.\n\n" + "_Se il problema persiste, verifica la connessione al backend-exec " + "e i permessi del tool._"; // ─── Unverified claims calculator (invariato da versione precedente) ────────── function _computeUnverifiedClaims(text: string, steps?: AgentStep[]): string[] { const warnings = new Set(); try { const claimed = detectClaimedCategories(text); if (claimed.size > 0) { const snap = proofLedger.snapshot(); for (const w of checkClaimsAgainstLedger(claimed, snap)) warnings.add(w); } } catch { /* best-effort — non-blocking */ } try { if (steps?.length) { const snap = proofLedger.snapshot(); const seen = new Set(); for (const s of steps) { if (!PROOF_VERIFIED_TOOLS.has(s.tool) || seen.has(s.tool)) continue; seen.add(s.tool); const entries = snap.filter(p => p.action === s.tool); if (entries.length > 0 && !entries.some(e => e.verified)) { const _label = TOOL_TO_CATEGORY_LABEL.get(s.tool) ?? s.tool; warnings.add(`${_label} (eseguito ma non verificato)`); } } } } catch { /* best-effort — non-blocking */ } return [...warnings]; } function _safeChunk(c: string, cb: (s: string) => void): void { const safe = c.replace(_STREAM_GARBAGE_RE, ""); if (safe) cb(safe); } function _emitChunks( text: string, onChunk: (c: string) => void, abort?: AbortSignal, chunkSize = EMIT_CHUNK_SIZE, ): void { const stepRe = /(\u0060{3}step\n[\w_]+\n\u0060{3})/g; let last = 0, m: RegExpExecArray | null; while ((m = stepRe.exec(text)) !== null) { if (abort?.aborted) return; const before = text.slice(last, m.index); for (let i = 0; i < before.length; i += chunkSize) { if (abort?.aborted) return; _safeChunk(before.slice(i, i + chunkSize), onChunk); } _safeChunk(m[1], onChunk); last = m.index + m[0].length; } const tail = text.slice(last); for (let i = 0; i < tail.length; i += chunkSize) { if (abort?.aborted) return; _safeChunk(tail.slice(i, i + chunkSize), onChunk); } } /** * GAP-2: emitFinalText con hard block "no-proof". * * Nuova gestione pass:false: * - reason "garbage" | "empty" → SAFE_FALLBACK_TEXT (invariato) * - reason "no-proof" [GAP-2] → NO_PROOF_RETRY_TEXT (nuovo) * * Il caller (case3FinalBlock, doneSignalProcessor) riceverà: * - SAFE_FALLBACK_TEXT: se l'output era garbage/vuoto * - NO_PROOF_RETRY_TEXT: se l'azione dichiarata non ha prova * * In entrambi i casi il testo originale non viene MAI emesso (fail-closed). * * @param text Testo grezzo da validare * @param onChunk Callback per ogni chunk sanitizzato * @param abort AbortSignal opzionale * @param steps Step sessione per check "tool critico non verificato" * @param taskHint Riservato per estensioni future * @param chunkSize Dimensione chunk (default EMIT_CHUNK_SIZE) */ export function emitFinalText( text: string, onChunk: (c: string) => void, abort?: AbortSignal, steps?: AgentStep[], taskHint?: string, chunkSize = EMIT_CHUNK_SIZE, ): void { if (abort?.aborted) return; let _g: GateVerdict; try { _g = runFinalOutputGate(text, { unverifiedClaims: _computeUnverifiedClaims(text, steps), taskHint, // hardProofGating: true (default) — attiva GAP-2 hard block in produzione // Per disabilitare: passa hardProofGating: false nel GateCtx }); } catch { _g = { pass: false, reason: "garbage", text: "" }; } if (!_g.pass) { // GAP-2: distingue tra garbage/empty e no-proof const fallbackText = _g.reason === "no-proof" ? NO_PROOF_RETRY_TEXT : SAFE_FALLBACK_TEXT; _emitChunks(fallbackText, onChunk, abort, chunkSize); return; } let outText = _g.text; // Soft mode legacy (hardProofGating: false): appende warnings if (_g.warnings?.length) { const list = _g.warnings.join("; "); const plural = _g.warnings.length === 1 ? "questa azione" : "queste azioni"; outText += `\n\n---\n⚠️ **Verifica automatica:** ${list}. ` + `Il sistema non ha trovato prove confermate per ${plural} — ` + `controlla manualmente prima di considerarle completate.`; } _emitChunks(outText, onChunk, abort, chunkSize); } export { detectClaimedCategories, checkClaimsAgainstLedger } from "./actionClaimDetector"; export function buildClaimWarningBanner(text: string, steps?: AgentStep[]): string { try { const warnings = _computeUnverifiedClaims(text, steps); if (!warnings.length) return ""; const list = warnings.join("; "); const plural = warnings.length === 1 ? "questa azione" : "queste azioni"; return ( `\n\n---\n⚠️ **Verifica automatica:** ${list}. ` + `Il sistema non ha trovato prove confermate per ${plural} — ` + `controlla manualmente prima di considerarle completate.` ); } catch { return ""; } }