File size: 7,875 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
/**
 * 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<string, unknown> }>;
  failureRecoveries:    Array<{ toolName: string; result: ReturnType<typeof recordAndSuggestFix> }>;
  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(/<think>[\s\S]*?<\/think>/gi, "")
          .replace(/<think>[\s\S]*/gi, "")
          .replace(/<action>[\w_]+<\/action>\s*<input>[\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 };
}