/** * 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) => Promise; } export type DoneSignalAction = "skip" | "continue" | "return"; export interface DoneSignalResult { action: DoneSignalAction; finalText: string; routeQualityScore: number | undefined; } export async function processDoneSignal(ctx: DoneSignalCtx): Promise { 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(/[\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(/[\s\S]*?<\/think>/gi, "") .replace(/[\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 }; }