Spaces:
Sleeping
Sleeping
File size: 7,908 Bytes
95eb75a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | // 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 ""; }
}
|