AUDIT / src /lib /agentLoop /apiErrorRecovery.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
10.5 kB
/**
* 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<string, unknown> } | null;
realDataInjected: boolean;
preCls: { type: string; isDirectAnswer?: boolean } | null;
preferredModel?: string;
maxTokensBudget: number;
executeToolGated: (name: string, args: Record<string, unknown>) => Promise<string>;
writeSess: () => void;
cleanupSseBridge: () => void;
onSuggestions?: (s: ReturnType<typeof generateSuggestions>) => 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<ApiErrorResult> {
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<string, unknown>); } catch { return null; }
})();
const _diTool = _diPreChk != null && _diPreChk.hasKnownIssue && _diPreChk.altTool
? _diPreChk.altTool
: fi.tool;
const _diArgs = fi.args as Record<string, unknown>;
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) };
}