AUDIT / src /lib /agentLoop /emitFinalText.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
7.91 kB
// 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 ""; }
}