/** * apiErrorRecovery.ts — S502: extracted from agentLoop.ts * * Gestisce la risposta HTTP in errore (!res.ok) dopo callWithFallback: * - Codici 400/401/403/422/503: recovery progressivo con tool + streamFallback * - Codici non gestiti: segnala al caller di throw HFApiError * * Invarianti: * - Non modifica loopMessages (solo steps e display via onSteps) * - Non emette testo al di fuori di streamFallback / onChunk * - Fire-and-forget per recordTaskOutcome, contextBridge, invalidateRepoMap * * @module apiErrorRecovery */ import type { AgentStep } from "../storage"; import type { StreamOptions } from "../types"; import type { TaskType } from "../taskClassifier"; import { HFApiError } from "../types"; import { selfLearning } from "../selfLearning"; import { contextBridge } from "../contextBridge"; import { semanticSearch } from "../agent/SemanticSearch"; import { invalidateRepoMap } from "../context/repoMap"; import { detectIntent } from "./intentParser"; import { streamFallback } from "./streamHelpers"; import { _localSynthesize } from "./toolExecutor"; import { TOOL_STATUS_LABELS } from "./toolStatusLabels"; import { narrateApiError, narrateLoopPhase } from "./errorNarrator"; // S768+S769 import { recordOutcome as recordTaskOutcome } from "../agent/SuccessRateMonitor"; import { generateSuggestions } from "../agent/SmartSuggestions"; import type { ApiMsg } from "./networkTools"; export interface ApiErrorCtx { res: Response; iter: number; token: string; options: StreamOptions; signal?: AbortSignal; initialMessages: Array<{ role: string; content: string }>; loopMessages: ApiMsg[]; lastUserMsg: string; steps: AgentStep[]; onStatus: (msg: string) => void; onChunk: (chunk: string) => void; onSteps: (steps: AgentStep[]) => void; detectedIntent: { tool: string; args: Record } | null; realDataInjected: boolean; preCls: { type: string; isDirectAnswer?: boolean } | null; preferredModel?: string; maxTokensBudget: number; executeToolGated: (name: string, args: Record) => Promise; writeSess: () => void; cleanupSseBridge: () => void; onSuggestions?: (s: ReturnType) => void; } export type ApiErrorAction = "return" | "throw-hf-error"; export interface ApiErrorResult { action: ApiErrorAction; hfError?: HFApiError; } /** * Gestisce !res.ok. * Ritorna "return" se il caller deve fare return, * "throw-hf-error" se deve throw HFApiError. */ export async function handleApiError(ctx: ApiErrorCtx): Promise { const { res, iter, token, options, signal, initialMessages, loopMessages, lastUserMsg, steps, onStatus, onChunk, onSteps, detectedIntent: _detectedIntent, realDataInjected: _realDataInjected, preCls: _preCls, preferredModel: _preferredModel, executeToolGated, writeSess: _writeSess, cleanupSseBridge: _cleanupSseBridge, onSuggestions, } = ctx; const writeSess = _writeSess; const errText = await res.text().catch(() => ""); console.error(`[AgentLoop] Errore API ${res.status}:`, errText); // check-console-log: ok narrateApiError(res.status, errText, undefined, onStatus); // S768: friendly error narration if ([400, 422, 503, 401, 403].includes(res.status)) { // On first iteration: run detected tool first, then stream with enriched context if (iter === 0) { // DIRECT-FIX: per task a risposta diretta, non fare fallback a web_search in recovery const fi = (_preCls?.isDirectAnswer ?? false) ? null : (_detectedIntent ?? detectIntent(lastUserMsg) ?? { tool: "web_search", args: { query: lastUserMsg.slice(0, 200) } }); // S609: 120→200 if (!fi) { // isDirectAnswer in errore API → stream diretto senza tool await streamFallback(token || "", initialMessages, onChunk, signal, { ...options, ...(_preferredModel ? { model: _preferredModel } : {}), }); try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } writeSess(); // S-ARC3: salva sessione su isDirectAnswer fallback return { action: "return" }; } // S380-C: preCheck proattivo (selfLearning) — usa fi come sorgente di tool/args const _diPreChk = (() => { try { return selfLearning.preCheck(fi.tool, fi.args as Record); } catch { return null; } })(); const _diTool = _diPreChk != null && _diPreChk.hasKnownIssue && _diPreChk.altTool ? _diPreChk.altTool : fi.tool; const _diArgs = fi.args as Record; onStatus(TOOL_STATUS_LABELS[_diTool] ?? `${_diTool}…`); const fr = await executeToolGated(_diTool, _diArgs); try { contextBridge.recordToolResult(_diTool, _diArgs, fr); } catch { /* non-blocking */ } try { semanticSearch.invalidateCache(); } catch { /* non-blocking */ } try { invalidateRepoMap(); } catch { /* non-blocking — S206 */ } const fs: AgentStep = { tool: _diTool, args: _diArgs, result: fr, status: fr.startsWith("❌") ? "error" : "done" }; steps.push(fs); onSteps([...steps]); if (!fr.startsWith("❌")) { const enriched = [ ...initialMessages, { role: "user" as const, content: `OSSERVAZIONE [${fi.tool}]:\n${fr}\n\nUsa questi dati reali per rispondere in italiano in modo chiaro e completo.` }, ]; narrateLoopPhase("elaborating", onStatus); // S769 await streamFallback(token || "", enriched, onChunk, signal, { ...options, ...(_preferredModel ? { model: _preferredModel } : {}), }); try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } writeSess(); // S-ARC3: salva sessione su enriched fallback return { action: "return" }; } // S303-INARRESTABILE: tool primario fallito → tenta web_search come backup prima di cedere if (fi.tool !== "web_search") { try { const _wsBak = await executeToolGated("web_search", { query: lastUserMsg.slice(0, 200) }); // S609: 100→200 if (!_wsBak.startsWith("❌") && _wsBak.length > 30) { const _enrichedWs = [ ...initialMessages, { role: "user" as const, content: `OSSERVAZIONE [web_search]:\n${_wsBak}\n\nUsa questi dati reali per rispondere in italiano in modo chiaro e completo.` }, ]; narrateLoopPhase("elaborating", onStatus); // S769 await streamFallback(token || "", _enrichedWs, onChunk, signal, { ...options }); try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } writeSess(); // S-ARC3: salva sessione su API error recovery web fallback return { action: "return" }; } } catch { /* non-blocking */ } } } narrateLoopPhase("direct", onStatus); // S769 // S303-INARRESTABILE: se steps ha risultati validi, usa loopMessages arricchiti // S255: se dati reali già iniettati in loopMessages, NON perdere il contesto const _hasStepsData = steps.some((s: AgentStep) => !s.result.startsWith("❌") && s.result.length > 30); if (_realDataInjected || _hasStepsData) { // S304-B: prova sintesi locale istantanea per dati fattuali — se riesce non serve LLM if (_localSynthesize(steps, onChunk, signal)) { narrateLoopPhase("data-ready", onStatus); // S769 try { onSuggestions?.(generateSuggestions(lastUserMsg, steps)); } catch {} try { recordTaskOutcome(_preCls!.type as TaskType, "success"); } catch { /* non-blocking */ } _writeSess(); _cleanupSseBridge(); signal?.removeEventListener("abort", _cleanupSseBridge); return { action: "return" }; } narrateLoopPhase("with-data", onStatus); // S769 await streamFallback( token || "", loopMessages as Array<{ role: string; content: string }>, onChunk, signal, options, ); try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } writeSess(); // S-ARC3: salva sessione su loopMessages fallback return { action: "return" }; } // S169-FixD / S255: prima di cedere, tenta web_search di recovery (solo se iter>0) if (iter > 0 && !steps.some(s => s.tool === "web_search" && !s.result.startsWith("❌"))) { try { const _wsResult = await executeToolGated("web_search", { query: lastUserMsg.slice(0, 200) }); // S609: 100→200 if (!_wsResult.startsWith("❌") && _wsResult.length > 20) { const _enriched = [ ...initialMessages, { role: "user" as const, content: `OBSERVATION [web_search|REAL_DATA]:\n${_wsResult}\n\nUsa questi dati per rispondere in modo completo e diretto.` }, ]; await streamFallback(token || "", _enriched, onChunk, signal, options); try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } writeSess(); // S-ARC3: salva sessione su API error recovery web enriched return { action: "return" }; } } catch { /* non-blocking */ } } // S471: graceful 503 — disclaimer anti-hallucination + fallback finale const _noDataMsg = `⚠️ NOTA SISTEMA: i tool di ricerca non hanno restituito dati (errore/timeout). Rispondi SOLO con ciò che sai con certezza. Se la domanda riguarda meteo, notizie o dati in tempo reale, dì esplicitamente "Non ho dati aggiornati in questo momento — riprova tra poco" e NON inventare numeri o fatti specifici.`; const _noDataMsgs = [...initialMessages, { role: "user" as const, content: _noDataMsg }]; try { await streamFallback(token || "", _noDataMsgs, onChunk, signal, options); } catch { onChunk("\n\n⚠️ Tutti i provider AI sono occupati in questo momento — di solito si liberano in pochi secondi. Riprova tra un momento."); try { recordTaskOutcome(_preCls!.type as TaskType, "failure"); } catch { /* non-blocking */ } } writeSess(); // S-ARC3: salva sessione su API error — tutti i provider down return { action: "return" }; } // Codice HTTP non gestito (500, 404, ecc.) → throw HFApiError al caller return { action: "throw-hf-error", hfError: new HFApiError(errText || `HTTP ${res.status}`, res.status) }; }