AUDIT / src /lib /agentLoop /doneSignalProcessor.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
10.5 kB
/**
* doneSignalProcessor.ts — S496: extracted from agentLoop.ts
*
* S99-Cosa3: gestisce il segnale DONE: {summary} che il modello emette
* quando si auto-dichiara completato.
*
* Comportamento:
* 1. Se rawText NON contiene "DONE:" → action="skip" (nessun processing)
* 2. Salva il summary nel worldState (fire-and-forget)
* 3. Estrae finalText dal testo dopo DONE:
* 4. Se formalGoal → verifica con verifyGoal:
* - Non soddisfatto + iter disponibili → inietta recovery → action="continue"
* - Soddisfatto → addDecision
* 5. Streama finalText e segnala action="return" (caller esce dalla funzione)
*
* Muta steps e loopMessages direttamente (by reference).
*
* @module doneSignalProcessor
*/
import type { AgentStep } from "../storage";
import type { ApiMsg } from "./networkTools";
import type { GoalContract } from "../_goalVerifier";
import { verifyGoal, buildRecoveryPrompt } from "../_goalVerifier";
import { normalizeLlmText } from "../utils/normalizeLlmOutput";
import { validateGate as _doneVg } from "../agent/validationGate"; // P30-TG
import { recordErrorAsync } from "../selfLearningAsync"; // S649
import { addDecision } from "../projectMemory";
import { generateSuggestions, type Suggestion } from "../agent/SmartSuggestions";
export interface DoneSignalCtx {
rawText: string;
formalGoal: GoalContract | null;
steps: AgentStep[];
iter: number;
MAX_ITER: number;
loopMessages: ApiMsg[];
onStatus: (msg: string) => void;
onSteps: (steps: AgentStep[]) => void;
signal?: AbortSignal;
lastUserMsg: string;
sessStats: { verifyFails: number };
emitFinalText: (text: string, signal?: AbortSignal) => void;
onSuggestions: ((s: Suggestion[]) => void) | undefined;
writeSess: () => void;
/** GAP-N3: auto-validate VFS project prima del DONE — opzionale, no-op se assente */
executeToolGated?: (tool: string, args: Record<string, unknown>) => Promise<string>;
}
export type DoneSignalAction = "skip" | "continue" | "return";
export interface DoneSignalResult {
action: DoneSignalAction;
finalText: string;
routeQualityScore: number | undefined;
}
export async function processDoneSignal(ctx: DoneSignalCtx): Promise<DoneSignalResult> {
const {
rawText, formalGoal, steps, iter, MAX_ITER,
loopMessages, onStatus, onSteps, signal, lastUserMsg,
sessStats, emitFinalText, onSuggestions, writeSess,
} = ctx;
// GAP-DONE-REGEX-MULTILINE: strip code blocks prima del match
const _rawStripped = rawText.replace(/```[\s\S]*?```/g, '').replace(/<code>[\s\S]*?<\/code>/gi, '');
const _doneMatch = _rawStripped.match(/^DONE:\s*(.+)$/m);
if (!_doneMatch) return { action: "skip", finalText: rawText, routeQualityScore: undefined };
const _doneSummary = _doneMatch[1].trim();
// Fire-and-forget: salva summary nel worldState per continuità sessione
(async () => {
try {
const { vfsDb: _vfsDbDone } = await import("../vfsDb");
await _vfsDbDone.worldState.add({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
type: "build_ok" as const,
path: "",
summary: _doneSummary,
timestamp: Date.now(),
topic: lastUserMsg.slice(0, 50),
});
} catch { /* non-blocking */ }
})();
// Estrai finalText: testo dopo la riga DONE: (strip think tags)
let finalText = rawText
.replace(/<think>[\s\S]*?<\/think>/gi, "")
.replace(/<think>[\s\S]*/gi, "")
.replace(/^DONE:.*$/m, "")
.trim();
if (!finalText) finalText = _doneSummary;
finalText = normalizeLlmText(finalText) || finalText;
let routeQualityScore: number | undefined;
// S203+: VERIFY prima dello streaming — se fallisce e ci sono iter, riprendi
if (formalGoal && !signal?.aborted) {
try {
const _vr = verifyGoal(formalGoal, steps, finalText, signal);
routeQualityScore = _vr.score;
steps.push(_vr.step);
onSteps([...steps]);
if (!_vr.satisfied && iter < MAX_ITER - 1) { // S701: -2→-1
sessStats.verifyFails++;
// S204 GAP4: registra failure in selfLearning
try {
const _g4fix = _vr.failedDimensions.map(d => `[${d.name}] ${d.detail}`).join("; ");
recordErrorAsync( // S649
`VERIFY_FAIL: DONE_WHEN="${formalGoal.doneWhen.slice(0, 80)}" score=${Math.round(_vr.score * 100)}%`,
`verify:goal="${formalGoal.goal.slice(0, 60)}"`,
_g4fix,
);
} catch { /* non-blocking */ }
onStatus(`Verifica fallita (${Math.round(_vr.score * 100)}%) — ripristino autonomo…`);
const _recov = buildRecoveryPrompt(_vr, formalGoal);
loopMessages.push({ role: "user", content: _recov } as ApiMsg);
return { action: "continue", finalText, routeQualityScore };
} else if (!_vr.satisfied && _vr.score < 0.35 && iter < MAX_ITER) { // S701: hard block — score critico (<35%) → forza ultimo tentativo (GAP-VERIFY-S701-OVERLAP: else if)
sessStats.verifyFails++;
try {
recordErrorAsync(
`VERIFY_CRITICAL: score=${Math.round(_vr.score * 100)}% iter=${iter}/${MAX_ITER}`,
`verify:goal="${formalGoal.goal.slice(0, 60)}"`,
_vr.failedDimensions.map(d => `[${d.name}] ${d.detail}`).join('; '),
);
} catch { /* non-blocking */ }
onStatus(`Score critico (${Math.round(_vr.score * 100)}%) — ultimo tentativo forzato…`);
const _recovCrit = buildRecoveryPrompt(_vr, formalGoal);
loopMessages.push({ role: 'user', content: `${_recovCrit}\n\n⚠️ SCORE CRITICO — ricostruire la risposta da zero.` } as ApiMsg);
return { action: 'continue', finalText, routeQualityScore };
}
// S454: addDecision su PASS
if (_vr.satisfied) {
try {
addDecision({
type: "BUG_RESOLVED",
feature: formalGoal.goal.slice(0, 40),
decision: finalText.slice(0, 120),
reason: `GoalVerifier PASS score=${Math.round(_vr.score * 100)}%`,
affects: [],
});
} catch { /* non-blocking */ }
// S302: Audit Engine (Profilo D) — double-check zona grigia [0.35, 0.82]
// Previene false chiusure: score convincente ma non pieno → provider alternativo.
// Fail-open: UNKNOWN non blocca mai il task.
if (_vr.score >= 0.35 && _vr.score <= 0.82 && iter < MAX_ITER - 1 && !signal?.aborted) {
try {
const { runGoalAudit } = await import("./goalAuditEngine");
onStatus("🔍 Audit indipendente (Profilo D)…");
const _s302 = await runGoalAudit(
formalGoal.goal, formalGoal.doneWhen, finalText, "groq",
);
if (_s302.verdict === "FAIL") {
sessStats.verifyFails++;
onStatus("⚠️ Audit Profilo D: risposta incompleta — mi autocorreggo…");
loopMessages.push({
role: "user",
content: `[S302 AUDIT ENGINE — ${_s302.provider}]\n${_s302.reason} (score audit=${Math.round(_s302.score * 100)}%)\n\nRiprendi il task e completa quanto rilevato mancante prima di concludere.`,
} as ApiMsg);
return { action: "continue", finalText, routeQualityScore };
}
} catch { /* fail-open — non blocca mai il task */ }
}
// S766-MEM-RANK: feedback positivo sulle memorie iniettate (fire-and-forget)
try {
const { sendMemoryFeedback } = await import("./loopMemoryBlock");
sendMemoryFeedback("PASS");
} catch { /* non-blocking */ }
}
} catch { /* non-blocking — verifica mai interrompe il flusso */ }
}
// GAP-1: auto-validate + repair loop (max 2 iter) prima del DONE — never blocking
if (ctx.executeToolGated && !signal?.aborted) {
try {
const { vfsDb: _vfsDbVal } = await import("../vfsDb");
const _allFiles = await _vfsDbVal.files.toArray();
const _hasCode = (_allFiles as Array<{ path: string }>).some(f => /\.(ts|tsx|py|js|jsx)$/.test(f.path));
if (_hasCode) {
const MAX_REPAIR_ITER = 2;
// Conta i repair già tentati in questa sessione per evitare loop infiniti
const _repairsDone = steps.filter(s => s.tool === "_repair_validator").length;
if (_repairsDone < MAX_REPAIR_ITER) {
onStatus(`Verifico il progetto… (${_repairsDone + 1}/${MAX_REPAIR_ITER})`);
const _valResult = await ctx.executeToolGated("validate_project", {});
if (_valResult && _valResult.startsWith("❌")) {
// Traccia il tentativo di repair nello steps array
steps.push({ tool: "_repair_validator", args: { iter: _repairsDone + 1 }, result: _valResult.slice(0, 200), status: "error" });
onSteps([...steps]);
if (_repairsDone + 1 < MAX_REPAIR_ITER && iter + 1 < MAX_ITER) {
// Budget repair e iter disponibili → rilancia il loop per auto-riparazione
onStatus("Trovati errori — auto-riparazione in corso…");
loopMessages.push({ role: "user", content: [
"Il progetto ha fallito la validazione automatica.",
"Report errori:\n" + _valResult.slice(0, 600),
"Fixa TUTTI gli errori usando write_file o apply_patch.",
"Poi scrivi DONE quando il progetto è pulito.",
].join("\n") } as ApiMsg);
return { action: "continue", finalText, routeQualityScore };
} else {
// Budget esaurito → appende il report come fallback informativo
finalText = finalText + "\n\n---\n⚠️ **Validazione automatica:**\n" + _valResult.slice(2).trim().slice(0, 500);
}
}
}
}
} catch { /* non-blocking — la validazione non blocca mai il completamento */ }
}
// Goal soddisfatto (o iter esaurite) → stream + return
onSteps([...steps]);
// P30-TG: validateGate normalizza markdown + ripara code blocks prima dell'emissione.
try { const _g = _doneVg(finalText, { taskHint: lastUserMsg.slice(0, 80), minQuality: 0 }) } catch { /* non-blocking */ }
emitFinalText(finalText, signal);
try { onSuggestions?.(generateSuggestions(lastUserMsg, steps)); } catch { /* non-blocking */ }
writeSess();
return { action: "return", finalText, routeQualityScore };
}