Spaces:
Sleeping
Sleeping
| /** | |
| * case2Block.ts — S504: extracted from agentLoop.ts | |
| * | |
| * Gestisce il Case 2 completo: text-based ReAct actions (HF/text models). | |
| * Pipeline: | |
| * 1. Stripping blocchi <think>…</think> 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<string>; | |
| 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<string, unknown>) => 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<Case2BlockResult> { | |
| 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 <think>…</think> + parse textActions ───────────────────── | |
| // S188: estrai + rimuovi tutti i blocchi <think> (ovunque, non solo in testa) | |
| const _thinkBlocks = rawText.match(/<think>([\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(/<think>[\s\S]*?<\/think>/gi, "").replace(/<think>[\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(/<action>[\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<string, unknown>)); | |
| // 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<string, unknown>) }; | |
| 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<string, unknown>, 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<string, unknown>); | |
| // 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<string>) | |
| .then(r => { cbUpdate(ta.name, r, iter); return r; }); | |
| step.result = result; | |
| step.status = result.startsWith("❌") ? "error" : "done"; | |
| onSteps([...steps]); | |
| let _reactRecovery: ReturnType<typeof recordAndSuggestFix> | 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<string, unknown>).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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, string> = { | |
| 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 }; | |
| } | |