/** * case2Block.ts — S504: extracted from agentLoop.ts * * Gestisce il Case 2 completo: text-based ReAct actions (HF/text models). * Pipeline: * 1. Stripping blocchi da rawText → cleanRawText * 2. Parse textActions da cleanRawText * 3. Se textActions.length > 0 && iter < MAX_ITER-1: * - Esecuzione sequenziale tool con CB + stall detection * - Error recovery, live debug fix, belief revision, exec-reinject tracking * - Stagnation guard (3 iters consecutive fallite → return) * → action "continue" * 4. Altrimenti: action "skip" (il caller processa Case 3 con cleanRawText) * * Muta steps e loopMessages per reference. * Restituisce cleanRawText sempre (usato da Case 3 per candidateText). * * @module case2Block */ import type { AgentStep } from "../storage"; import { narrativeLabel } from "./toolStatusLabels"; // S766 import { parseTextActions } from "./intentParser"; import { _tryLiveDebugFix } from "./toolExecutor"; import { verifyToolResult, annotateWithProof } from "../agent/verifier"; import { updateWorldModel } from "../worldModel"; import { selfLearning } from "../selfLearning"; import { recordErrorAsync } from "../selfLearningAsync"; // S649 import { errorLog } from "../agentBrain"; import { recordAndSuggestFix, notifyToolSuccess } from "../agent/errorRecovery"; import { persistBeliefState, handleToolFailureBelief, shouldReviseBeliefs, buildRevisionPrompt } from "../beliefRevision"; import { predictOutcome, shouldReviseImmediately } from "./actionSimulator"; // S-PRED import { EXEC_REINJECT_TOOLS } from "./toolGate"; import { infraCategory } from "./stallDetector"; // P32-GE-1 import { buildExecReinjectMessage } from "./execLoopGate"; import type { ApiMsg } from "./networkTools"; // ─── Interfaces ──────────────────────────────────────────────────────────────── export interface Case2BlockCtx { rawText: string; iter: number; MAX_ITER: number; // Mutable state (returned updated) steps: AgentStep[]; loopMessages: ApiMsg[]; consecutiveAllFailIters: number; effectiveMaxIter: number; sessStats: { autoFixed: number; propagated: number; verifyFails: number }; lastExecTool: string; lastExecOutput: string; lastExecResultIter: number; // Read-only context lastUserMsg: string; loopTaskId: string | null; route: string; routeStart: number; taskPlan: unknown; preCls: { type: string } | null; // Callbacks onStatus: (msg: string) => void; onSteps: (steps: AgentStep[]) => void; onChunk: (chunk: string) => void; // Closures executeToolGated: (name: string, args: unknown) => Promise; epistemicTag: (toolName: string) => string; cbIsOpen: (name: string, iter: number) => boolean; cbUpdate: (name: string, result: string, iter: number) => void; cbSkipMsg: (name: string) => string; stallCheck: (name: string, args: Record) => number; writeSess: () => void; recordTaskOutcome: (type: string, outcome: string) => void; onRouteRecord: (success: boolean) => void; } export type Case2BlockAction = "continue" | "return" | "skip"; export interface Case2BlockResult { action: Case2BlockAction; cleanRawText: string; consecutiveAllFailIters: number; effectiveMaxIter: number; sessStats: { autoFixed: number; propagated: number; verifyFails: number }; lastExecTool: string; lastExecOutput: string; lastExecResultIter: number; } // ─── Main export ─────────────────────────────────────────────────────────────── /** Processa il Case 2 (ReAct text actions). Ritorna "skip" se non ci sono azioni → il caller processa Case 3. */ export async function processCase2Block(ctx: Case2BlockCtx): Promise { const { rawText, iter, MAX_ITER, lastUserMsg, loopTaskId, route: _route, routeStart: _routeStart, taskPlan: _taskPlan, preCls, onStatus, onSteps, onChunk, executeToolGated, epistemicTag, cbIsOpen, cbUpdate, cbSkipMsg, stallCheck, writeSess, recordTaskOutcome, onRouteRecord, } = ctx; const steps = ctx.steps; const loopMessages = ctx.loopMessages; let consecutiveAllFailIters = ctx.consecutiveAllFailIters; let effectiveMaxIter = ctx.effectiveMaxIter; const sessStats = { ...ctx.sessStats }; let lastExecTool = ctx.lastExecTool; let lastExecOutput = ctx.lastExecOutput; let lastExecResultIter = ctx.lastExecResultIter; // ── 1. Stripping + parse textActions ───────────────────── // S188: estrai + rimuovi tutti i blocchi (ovunque, non solo in testa) const _thinkBlocks = rawText.match(/([\s\S]*?)<\/think>/gi) ?? []; const _thinkContent = _thinkBlocks.map(b => b.replace(/<\/?think>/gi, "").trim()).filter(t => t.length > 50).join("\n\n"); // S188+S189: strip blocchi chiusi + blocco aperto/incompleto (finish_reason=length) const cleanRawText = rawText.replace(/[\s\S]*?<\/think>/gi, "").replace(/[\s\S]*/gi, "").trim(); if (_thinkContent.length > 50 && !steps.some(s => s.tool === "__thinking__")) { const thinkStep: AgentStep = { tool: "__thinking__", args: {}, result: _thinkContent, status: "done" }; steps.push(thinkStep); onSteps([...steps]); } const textActions = parseTextActions(cleanRawText); // ── 2. Guard: solo se ci sono azioni e siamo prima dell'ultima iter ───────── if (!(textActions.length > 0 && iter < MAX_ITER - 1)) { return { action: "skip", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter }; } // ── 3. Esecuzione sequenziale tool ──────────────────────────────────────── // Show reasoning text (stripped of action tags) as status const thinkingText = rawText.replace(/[\s\S]*?<\/input>/gi, "").trim(); if (thinkingText.length > 10) onStatus(thinkingText.slice(0, 120) + (thinkingText.length > 120 ? "…" : "")); loopMessages.push({ role: "assistant", content: rawText } as ApiMsg); let _beliefRevisionDoneThisIter = false; // S-GAP2a guard: al massimo 1 revisione per iterazione for (const ta of textActions) { // S766: label contestuale invece del tipo tool generico onStatus(narrativeLabel(ta.name, ta.args as Record)); // S766: context-aware explanation field const step: AgentStep = { tool: ta.name, args: ta.args, result: "", status: "running", explanation: narrativeLabel(ta.name, ta.args as Record) }; steps.push(step); onSteps([...steps]); // S-PRED: pre-esecuzione prediction — avvisa su tool rischiosi con precedenti non risolti const _pred = (() => { try { return predictOutcome(ta.name, ta.args as Record, errorLog.recent(50)); } catch { return null; } })(); if (_pred?.predictedOutcome === "likely_fail") { onStatus(`⚠️ Predizione rischio: ${_pred.reason}`); loopMessages.push({ role: "user", content: `[PRE-EXECUTION WARNING] ${_pred.reason}. Valuta un approccio alternativo prima di procedere con "${ta.name}".`, } as ApiMsg); } // Circuit breaker + stall detection const _c2StallN = stallCheck(ta.name, ta.args as Record); // S444: hint cross-sessione per messaggio stallo ReAct const _c2ErrHint = _c2StallN >= 3 ? (() => { try { const _wf = errorLog.resolvedWithFix().find(e => e.context.includes(ta.name)); return _wf ? `\n💡 Fix noto (${_wf.count}× visto): ${_wf.fix!.slice(0, 200)}` : ""; } catch { return ""; } })() : ""; const result: string = _c2StallN >= 3 ? `❌ Stallo rilevato — "${ta.name}" chiamato ${_c2StallN}× con gli stessi argomenti. Cambia approccio o scrivi DONE: se il task è completato.${_c2ErrHint}` : cbIsOpen(ta.name, iter) ? cbSkipMsg(ta.name) : await (executeToolGated(ta.name, ta.args) as Promise) .then(r => { cbUpdate(ta.name, r, iter); return r; }); step.result = result; step.status = result.startsWith("❌") ? "error" : "done"; onSteps([...steps]); let _reactRecovery: ReturnType | null = null; if (result.startsWith("❌")) { // S444: registra su errorLog per hint cross-sessione (ReAct path) try { errorLog.record(result.slice(0, 300), `tool:${ta.name}`, undefined, (typeof ta.args === "object" && ta.args !== null && "code" in ta.args) ? String((ta.args as Record).code).slice(0, 500) : undefined); } catch { /* non-blocking */ } // S204 GAP1: deriva fix dall'alternativa nota const _g1bfix = (() => { try { const _a = selfLearning.getAlternative(ta.name, result.slice(0, 80)); return _a ? `Usa ${_a.tool}: ${_a.hint.slice(0, 200)}` : ""; } catch { return ""; } })(); recordErrorAsync(result, `tool:${ta.name}`, _g1bfix); // S649 // S103/S351: belief revision ReAct path — S495: extracted to beliefRevision.ts handleToolFailureBelief({ taskId: loopTaskId ?? "", lastUserMsg, toolName: ta.name, errorResult: result, iter, path: "ReAct", loopMessages, steps, onSteps, onStatus }); // S-PRED: se la predizione era "likely_fail" e l'esito reale è errore → revisione immediata if (_pred && shouldReviseImmediately(_pred, true) && !_beliefRevisionDoneThisIter) { onStatus("⚠️ Predizione confermata — revisione strategia immediata"); } try { persistBeliefState(loopTaskId ?? "", lastUserMsg.slice(0, 40)); } catch { /* non-blocking */ } // S351: persist dopo Case 2 belief revision // S-GAP2a: belief revision narrative — rende visibile il cambio di strategia try { if (!_beliefRevisionDoneThisIter && shouldReviseBeliefs(loopTaskId ?? "")) { const _br = buildRevisionPrompt(loopTaskId ?? "", lastUserMsg); if (_br.shouldRevise) { const _cats = _br.distinctErrorCategories; let _narrativeMsg = "Sto ripensando l’approccio al task"; if (_cats.includes("format_error")) _narrativeMsg = "Formato incompatibile — rianalizzando il task da zero"; else if (_cats.includes("not_found")) _narrativeMsg = "Risorse non trovate — ridefinendo la strategia"; else if (_cats.includes("logic_error")) _narrativeMsg = "Errore logico identificato — riparto dall’analisi"; else if (_cats.includes("network_error")) _narrativeMsg = "Problema connessione — cambio approccio"; onStatus(_narrativeMsg); // Emetti uno step visibile con explanation per AgentNarration / AgentStepCards steps.push({ tool: "__belief_revision__", args: { categories: _cats }, result: `Revisione attivata: ${_cats.join(", ")}`, status: "done", explanation: _narrativeMsg, durationMs: 0, } as AgentStep); _beliefRevisionDoneThisIter = true; onSteps([...steps]); loopMessages.push({ role: "user", content: _br.revisionPrompt } as ApiMsg); } } } catch { /* non-blocking */ } try { _reactRecovery = recordAndSuggestFix({ toolName: ta.name, errorMsg: result }); } catch { /* non-blocking */ } // S59: live debug-fix per code execution tools const _reactFixed = await _tryLiveDebugFix(ta.name, ta.args as Record, result, `react${iter}`, onStatus); if (_reactFixed) { // S204+: registra fix immediatamente anche nel ReAct path try { recordErrorAsync(result.slice(0, 100), `livefix:react:${ta.name}`, _reactFixed.slice(0, 250)); // S649 } catch { /* non-blocking */ } // S444: marca errorLog come risolto (ReAct path, cross-sessione) try { errorLog.markFixed(errorLog.hashOf(result.slice(0, 300)), _reactFixed.slice(0, 500)); } catch { /* non-blocking */ } step.result = _reactFixed; step.status = "done"; sessStats.autoFixed++; // S205: error resolved autonomously (ReAct) // Fix 8 (S421): extra iteration in repair intensivo if (sessStats.autoFixed >= 2 && effectiveMaxIter < MAX_ITER) { effectiveMaxIter = Math.min(effectiveMaxIter + 1, MAX_ITER); } } else { sessStats.propagated++; // S205: error reaches model unchanged (ReAct) } } // S83: track exec result for loop closure (Case 2 - ReAct) if (EXEC_REINJECT_TOOLS.has(ta.name)) { lastExecResultIter = iter; lastExecOutput = (step.result || result).slice(0, 1000); lastExecTool = ta.name; loopMessages.push({ role: "user", content: buildExecReinjectMessage(lastExecTool, lastExecOutput) } as ApiMsg); } if (!result.startsWith("❌")) { try { notifyToolSuccess(ta.name); } catch { /* non-blocking */ } } // Annotate with verifier proof + epistemic tag prima di inviare al modello const _tvr = verifyToolResult(ta.name, ta.args as Record, result); const _tannotated = annotateWithProof(ta.name, result, _tvr); const _reactFixHint = _reactRecovery?.hadFix ? `\n💡 Fix noto (${_reactRecovery.count}x): ${_reactRecovery.fix}` : (_reactRecovery?.hint ? `\n⚡ ${_reactRecovery.hint}` : ""); // Step2: epistemic tag su OBSERVATION; Step5: update world model const _epTag2 = epistemicTag(ta.name); loopMessages.push({ role: "user", content: `OBSERVATION [${ta.name}|${_epTag2}]:\n${_tannotated}${_reactFixHint}` } as ApiMsg); updateWorldModel(ta.name, ta.args as Record, step.result || result).catch(() => {}); } // ── 4. Stagnation guard ──────────────────────────────────────────────────── { const _c2StartIdx = Math.max(0, steps.length - textActions.length); const _c2AllFailed = steps.slice(_c2StartIdx).every(s => s.status === "error"); consecutiveAllFailIters = (textActions.length > 0 && _c2AllFailed) ? consecutiveAllFailIters + 1 : 0; if (consecutiveAllFailIters >= 3) { onChunk("_(Stagnazione rilevata: " + consecutiveAllFailIters + " iterazioni consecutive con tutti i tool falliti. Interrompo il loop. Prova a riformulare il task o cambia provider.)_"); try { recordTaskOutcome(preCls?.type ?? "unknown", "failure"); } catch { /* non-blocking */ } onRouteRecord(false); writeSess(); // S205 return { action: "return", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter }; } } // ── P32-GE-1: Infra error graceful exit (ReAct path) ───────────────────── // Scansiona le OBSERVATION messages recenti: ≥3 errori infra consecutivi // della stessa categoria → exit onesto prima che il loop si inceppi. { const _c2ObsMsgs = (loopMessages as Array<{ role: string; content?: unknown }>) .filter(m => m.role === "user" && typeof m.content === "string" && (m.content as string).startsWith("OBSERVATION")) .slice(-9); // ultime 9 osservazioni ReAct let _c2InfraCount = 0; let _c2InfraCat: string | null = null; for (let _i = _c2ObsMsgs.length - 1; _i >= 0; _i--) { const _obs = _c2ObsMsgs[_i].content as string; const _obsContent = _obs.slice(_obs.indexOf(":\n") + 3).trimStart(); if (!_obsContent.startsWith("❌")) break; const _cat = infraCategory(_obsContent); if (_cat === null) break; if (_c2InfraCat === null) _c2InfraCat = _cat; if (_cat !== _c2InfraCat) break; _c2InfraCount++; } if (_c2InfraCount >= 3 && _c2InfraCat) { const _infraMsgMap2: Record = { rate_limit: "Il provider è in rate limit (429) ripetuto", timeout: "Timeout ripetuto dal provider o backend", server_5xx: "Errore server 5xx ripetuto dal backend", network_fail: "Errore di rete persistente", }; const _infraLabel2 = _infraMsgMap2[_c2InfraCat] ?? "Errore infrastrutturale ripetuto"; onChunk(`_(${_infraLabel2} — rilevato ${_c2InfraCount}× di seguito. Il servizio è temporaneamente non disponibile. Riprova tra qualche minuto o cambia provider nelle impostazioni.)_`); try { recordTaskOutcome(preCls?.type ?? "unknown", "failure"); } catch { /* non-blocking */ } onRouteRecord(false); writeSess(); return { action: "return", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter }; } } return { action: "continue", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter }; }