File size: 10,467 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
 * 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) };
}