AUDIT / src /lib /agentLoop /case3FinalBlock.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
10.8 kB
/**
* 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<string, unknown>)?.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<string, unknown>)?.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<string, unknown>)?.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" };
}