/** * case3FinalBlock.ts — S506: extracted from agentLoop.ts * * Gestisce la fase finale di Case 3 dopo goalVerifyBlock: * 1. onSteps + emitFinalText (stream risposta) * 2. recordTaskOutcome success * 3. SmartSuggestions * 4. RouteSelector — record react success con qualityScore * 5. refactoringPlanner (fire-and-forget, > 5 write_file) * 6. Build validator hook (iter >= 2, _isCodeTask) → action "continue" * 7. Piano Tracker globale (subtasks progress) * 8. runProofOfWork * 9. persistWorldState * * Ritorna action "continue" se il build validator inietta hint (loop deve continuare), * oppure "done" se la risposta è stata emessa correttamente. * * Muta steps e loopMessages per reference. * * @module case3FinalBlock */ import type { AgentStep } from "../storage"; import type { ApiMsg } from "./networkTools"; import type { GoalContract } from "../_goalVerifier"; import { generateSuggestions, type Suggestion } from "../agent/SmartSuggestions"; import { addDecision } from "../projectMemory"; import { getLastArchGraph } from "@/lib/projectArchGraph"; import { runProofOfWork } from "./proofOfWorkBlock"; import { parseBuildResultFromSteps, formatBuildRepairHint } from "../buildRunner"; import { proofLedger } from "./proofLedger"; // GAP-TRUTH-3: DONE gate usa snapshot ledger invece di cast unsafe import { persistWorldState } from "./worldStatePersister"; // GAP-TRUTH: gate gestito interamente da emitFinalText — nessun import diretto di finalOutputGate import { recordOutcome as recordTaskOutcome } from "../agent/SuccessRateMonitor"; import type { TaskType } from "../taskClassifier"; // ─── Interfaces ──────────────────────────────────────────────────────────────── export interface Case3FinalCtx { finalText: string; rawText: string; iter: number; MAX_ITER: number; steps: AgentStep[]; loopMessages: ApiMsg[]; lastUserMsg: string; formalGoal: GoalContract | null; routeQualityScore: number | undefined; route: string; routeStart: number; taskPlan: unknown; isCodeTask: boolean; preCls: { type: string; isDirectAnswer?: boolean } | null; signal?: AbortSignal; // Callbacks onSteps: (steps: AgentStep[]) => void; onChunk: (chunk: string) => void; onStatus: (msg: string) => void; onSuggestions: ((s: Suggestion[]) => void) | undefined; // Closures emitFinalText: (text: string, signal?: AbortSignal) => void; writeSess: () => void; confSession?: { recordSignal: (s: string) => void }; onRouteRecord: (success: boolean, qualityScore?: number) => void; } export type Case3FinalAction = "continue" | "done" | "build_ok" | "build_fail_serious"; export interface Case3FinalResult { action: Case3FinalAction; } // ─── Main export ─────────────────────────────────────────────────────────────── /** Processa la fase finale di Case 3: stream risposta + side effects post-emission. */ export function processCase3FinalBlock(ctx: Case3FinalCtx): Case3FinalResult { const { finalText, iter, MAX_ITER, steps, loopMessages, lastUserMsg, formalGoal, routeQualityScore, route, taskPlan, isCodeTask, preCls, signal, onSteps, onChunk, onStatus, onSuggestions, emitFinalText, writeSess, confSession, onRouteRecord, } = ctx; // ── 1. Emit final answer ────────────────────────────────────────────────── // S710-3: Reality proof — blocca DONE se step critici non hanno verified proof in proofLedger. // GAP-TRUTH-3: _CRIT_TOOLS locale rimossa — usa PROOF_VERIFIED_TOOLS da proofToolRegistry. // Subset intenzionale: solo azioni ad alto impatto irreversibile triggherano una retry pre-DONE. // Il check usa proofLedger.snapshot() — unica fonte di verità (non il campo verified su AgentStep // che non esiste nell'interfaccia e causava un cast unsafe con esito sempre-false). const _DONE_GATE_TOOLS = new Set(["git_push", "deploy_trigger", "write_file"]); const _snap = proofLedger.snapshot(); const _unverCrit = steps.filter(s => { if (!_DONE_GATE_TOOLS.has(s.tool)) return false; const entries = _snap.filter(p => p.action === s.tool); return entries.length > 0 && !entries.some(e => e.verified); }); if (_unverCrit.length > 0 && iter < MAX_ITER - 1) { onStatus("⚠️ Verifico azioni critiche prima di concludere..."); loopMessages.push({ role: "user", content: `⚠️ STOP: step critici non verificati: ${_unverCrit.map(s => s.tool).join(", ")}. Verifica proof di ciascuno prima di dichiarare DONE.`, } as ApiMsg); return { action: "continue" }; } // ── GAP-TRUTH: emitFinalText è l'unico arbitro — gestisce gate + proof warnings ───── // Non chiamare runFinalOutputGate direttamente qui: emitFinalText (agentLoop closure) // è già configurata con steps + lastUserMsg → fail-closed + warning banner integrati. onSteps([...steps]); // steps prima del testo — chip hanno dati subito emitFinalText(finalText, signal); // S-GAP7: recordTaskOutcome("success") SPOSTATA → dopo build validator (evita double record su build-fail→retry) // ── 2. SmartSuggestions ─────────────────────────────────────────────────── try { onSuggestions?.(generateSuggestions(lastUserMsg, steps)); } catch { /* non-blocking */ } // ── 3. RouteSelector — record react success ─────────────────────────────── if (taskPlan && route === "react") { const success = routeQualityScore !== undefined ? routeQualityScore >= 0.65 : (finalText.length > 80 && !finalText.startsWith("❌")); onRouteRecord(success, routeQualityScore); } // ── 4. refactoringPlanner (fire-and-forget, > 5 write_file) ────────��───── const _refPlanPaths = steps.filter(s => s.tool === "write_file").map(s => String((s.args as Record)?.path ?? "")).filter(Boolean); if (_refPlanPaths.length > 5) { try { const _archGr = getLastArchGraph(); if (_archGr) import("../refactoringPlanner").then(({ planRefactoring }) => planRefactoring(_refPlanPaths, _archGr)).catch(() => {}); } catch { /* non-blocking */ } } // ── 5. Build validator hook (iter >= 2, _isCodeTask) ───────────────────── if (iter >= 2 && isCodeTask) { const _bpResult = parseBuildResultFromSteps(steps); if (_bpResult.checked) { if (_bpResult.errors.length === 0) { confSession?.recordSignal("build_ok"); // S-GAP7: build validation passata → unico record per questo exit path try { recordTaskOutcome((preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } return { action: "build_ok" }; } const _bvHint = formatBuildRepairHint(_bpResult.errors, _bpResult.cmd ?? ""); if (_bvHint) { const _isSeriousFail = _bpResult.errors.length > 3; steps.push({ tool: "_build_validator", args: { hint: _bvHint.slice(0, 200) }, result: _bvHint.slice(0, 200), status: "error" }); loopMessages.push({ role: "user", content: _bvHint } as ApiMsg); onSteps([...steps]); confSession?.recordSignal(_isSeriousFail ? "build_fail_serious" : "build_fail"); return { action: _isSeriousFail ? "build_fail_serious" : "continue" }; } } } // ── 6. Piano Tracker globale ────────────────────────────────────────────── if (taskPlan && (taskPlan as { subtasks?: unknown[] }).subtasks && (taskPlan as { subtasks: unknown[] }).subtasks.length > 1) { const _tp = taskPlan as { subtasks: Array<{ outputs?: string[] }> }; const _allPlanFiles = _tp.subtasks.flatMap(t => t.outputs ?? []); const _totalPlan = _allPlanFiles.length || _tp.subtasks.length; const _written = new Set(steps.filter(s => s.tool === "write_file").map(s => String((s.args as Record)?.path ?? "")).filter(Boolean)); const _writtenCount = _written.size; const _allMissing = _allPlanFiles.filter((o: string) => !_written.has(o)); const _percent = _totalPlan > 0 ? Math.round((_writtenCount / _totalPlan) * 100) : 100; // Rimuovi il tracker precedente prima di inserirne uno nuovo const _prevTrackerIdx = steps.findLastIndex((s: AgentStep) => s.tool === "__plan_tracker__"); if (_prevTrackerIdx >= 0) steps.splice(_prevTrackerIdx, 1); steps.push({ tool: "__plan_tracker__", args: { percent: _percent, missing: _allMissing.length, total: _totalPlan }, result: `${_percent}% completato — mancano ancora: ${_allMissing.slice(0, 3).join(", ")}`, status: "done" }); onSteps([...steps]); } // ── 7. Proof-of-work ───────────────────────────────────────────────────── runProofOfWork({ _isCodeTask: isCodeTask, finalText, signal, onChunk, onStatus }); // ── 8. World state persister ───────────────────────────────────────────── if (!(preCls?.isDirectAnswer ?? false)) { persistWorldState({ steps, lastUserMsg, onSteps }); } // ── 9. addDecision su PASS path (S454) ─────────────────────────────────── if (formalGoal) { try { const _wfPaths = steps.filter(s => s.tool === "write_file").map(s => String((s.args as Record)?.path ?? "")).filter(Boolean); if (isCodeTask) { addDecision({ type: "ARCHITECTURAL_DECISION", feature: `${formalGoal.goal}`, decision: "code verified", reason: "write_file + verifyGoal passed", affects: _wfPaths.length ? _wfPaths : ["response_quality"] }); } else { addDecision({ type: "BUG_RESOLVED", feature: `${formalGoal.goal}`, decision: "goal satisfied", reason: "verifyGoal check passed", affects: _wfPaths.length ? _wfPaths : ["response_quality"] }); } } catch { /* non-blocking */ } } // S-GAP7: percorso normale → 1 solo record su completion effettiva try { recordTaskOutcome((preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } writeSess(); return { action: "done" }; }