Spaces:
Sleeping
Sleeping
| // 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<string>(); | |
| 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<string>(); | |
| 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 ""; } | |
| } | |