/** * case1TailBlock.ts — S496: extracted from agentLoop.ts * * Post-processing del Case 1 (native tool calls) dopo che tutti i risultati * dei tool sono stati processati e iniettati nel contesto. * * Pipeline (in ordine): * 1. Stagnation guard — 5 iter consecutive con tutti i tool falliti → action "return" * (rescue cascade inietta hint a soglia 2 e 4 in agentLoop.ts prima del hard-exit) * 2. Belief revision per tool in errore → push recovery message * 3. DONE: signal check — rawText può contenere DONE: anche con tool_calls (Gap C/S122) * → emitFinalText + action "return" * 4. Synthesis directive — per query non-code con tool riusciti → cap maxIter * → action "continue" * * @module case1TailBlock */ import type { AgentStep } from "../storage"; import type { ApiMsg, ToolCallRaw } from "./networkTools"; import { normalizeLlmText } from "../utils/normalizeLlmOutput"; import { handleToolFailureBelief, shouldReviseBeliefs, buildRevisionPrompt, persistBeliefState } from "../beliefRevision"; import { buildRecoveryMessage, recordAndSuggestFix } from "../agent/errorRecovery"; import { generateSuggestions, type Suggestion } from "../agent/SmartSuggestions"; export interface Case1TailCtx { results: Array<{ tc: ToolCallRaw; result: string; durationMs: number; args: Record }>; failureRecoveries: Array<{ toolName: string; result: ReturnType }>; anyError: boolean; consecutiveAllFailIters: number; effectiveMaxIter: number; steps: AgentStep[]; loopMessages: ApiMsg[]; iter: number; isCodeTask: boolean; rawText: string | null; signal?: AbortSignal; lastUserMsg: string; loopTaskId: string | null; onChunk: (c: string) => void; onSteps: (steps: AgentStep[]) => void; onStatus: (msg: string) => void; onSuggestions: ((s: Suggestion[]) => void) | undefined; emitFinalText: (text: string, signal?: AbortSignal) => void; writeSess: () => void; } export type Case1TailAction = "return" | "return-success" | "continue"; export interface Case1TailResult { action: Case1TailAction; newConsecutiveAllFailIters: number; newEffectiveMaxIter: number; } export function processCase1Tail(ctx: Case1TailCtx): Case1TailResult { const { results, failureRecoveries, anyError, steps, loopMessages, iter, isCodeTask, rawText, signal, lastUserMsg, onSteps, onStatus, onSuggestions, emitFinalText, writeSess, loopTaskId, } = ctx; let newConsecutiveAllFailIters = ctx.consecutiveAllFailIters; let newEffectiveMaxIter = ctx.effectiveMaxIter; // 1. Stagnation guard: 3 consecutive all-fail iters → return { const _c1AllFailed = results.every(r => r.result.startsWith("❌")); newConsecutiveAllFailIters = _c1AllFailed ? newConsecutiveAllFailIters + 1 : 0; if (newConsecutiveAllFailIters >= 5) { // S-RESCUE-C1: raise da 3→5 — rescue hints girano a 2 e 4 in agentLoop.ts // GAP-TRUTH: usa emitFinalText (con gate) invece di onChunk diretto — consistenza con tutti gli altri exit path emitFinalText( "_(Stagnazione rilevata: " + newConsecutiveAllFailIters + " iterazioni consecutive con tutti i tool falliti. Interrompo il loop. Prova a riformulare il task o cambia provider.)_", signal, ); writeSess(); // S-ARC3: salva sessione su stagnazione return { action: "return", newConsecutiveAllFailIters, newEffectiveMaxIter }; } } // 2. Belief revision per tool in errore (S103/S351: extracted to beliefRevision.ts) if (anyError) { for (const { tc: _brtc, result: _brr } of results) { if (_brr.startsWith("❌")) { handleToolFailureBelief({ taskId: loopTaskId ?? "", lastUserMsg, toolName: _brtc.function.name, errorResult: _brr, iter, path: "tool_calls", loopMessages: loopMessages as ApiMsg[], steps, onSteps, onStatus, }); } } // S351: persiste belief state cross-sessione (case1 path — parità con case2 ReAct) try { persistBeliefState(loopTaskId ?? "", lastUserMsg.slice(0, 40)); } catch { /* non-blocking */ } // S-GAP2a (case1 path): belief revision narrative — visibile anche nel path tool_calls try { if (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); steps.push({ tool: "__belief_revision__", args: { categories: _cats }, result: `Revisione attivata: ${_cats.join(", ")}`, status: "done", explanation: _narrativeMsg, durationMs: 0, } as AgentStep); onSteps([...steps]); loopMessages.push({ role: "user", content: _br.revisionPrompt } as ApiMsg); } } } catch { /* non-blocking */ } const _recoveryMsg = buildRecoveryMessage(failureRecoveries); loopMessages.push({ role: "user", content: _recoveryMsg || `Alcuni tool hanno fallito: ${results.filter(r => r.result.startsWith("❌")).map(r => r.tc.function.name).join(", ")}. Cambia strategia. Continua il task.`, } as ApiMsg); } // 3. DONE: signal check alongside tool_calls (Gap C/S122) // F4 fix (S131): worldState.add("build_ok") rimosso — si scrive solo nell'handler DONE: finale if (rawText) { const _c1DoneMatch = rawText.match(/^DONE:\s*(.+)$/m); if (_c1DoneMatch) { const _c1Sum = _c1DoneMatch[1].trim(); const _c1Final = normalizeLlmText( rawText .replace(/[\s\S]*?<\/think>/gi, "") .replace(/[\s\S]*/gi, "") .replace(/[\w_]+<\/action>\s*[\s\S]*?<\/input>/gi, "") .replace(/^(GOAL|DONE_WHEN|OUT_OF_SCOPE)\s*[::]\s*.*/gim, "") .replace(/^DONE:.*$/m, "") .trim(), ) || _c1Sum; if (_c1Final) { emitFinalText(_c1Final, signal); try { onSuggestions?.(generateSuggestions(lastUserMsg, steps)); } catch { /* non-blocking */ } writeSess(); return { action: "return-success", newConsecutiveAllFailIters, newEffectiveMaxIter }; } } } // 4. Synthesis directive (S426-FixC + S434): per query non-code con tool riusciti if (!isCodeTask && !anyError) { const _c1RealSuccessSteps = steps.filter(s => !s.tool.startsWith("__") && s.status === "done"); if (_c1RealSuccessSteps.length >= 1) { loopMessages.push({ role: "user", content: "Hai ottenuto i dati necessari. Presentali all'utente in modo diretto e completo nella tua prossima risposta. Non chiamare altri tool — scrivi solo la risposta finale.", } as ApiMsg); // S434: cap al massimo 1 altra iterazione per la sintesi newEffectiveMaxIter = Math.min(newEffectiveMaxIter, iter + 2); } } return { action: "continue", newConsecutiveAllFailIters, newEffectiveMaxIter }; }