File size: 18,315 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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/**
 * case1Block.ts — S503: extracted from agentLoop.ts
 *
 * Gestisce il Case 1 completo: native tool calls (Groq, OpenAI, OpenRouter).
 * Pipeline:
 *   1. Sintetizza formalGoal a iter 0 se null
 *   2. Registra gli step come "running"
 *   3. Esegue i tool in parallelo (max 4, pLimitPool)
 *   4. Processa i risultati: annotation, world model, error recovery, live debug fix
 *   5. ReasoningCore + belief state persistence
 *   6. Delega a case1TailBlock (stagnation → return, DONE → return-success, continue)
 *
 * Muta steps e loopMessages per reference.
 * Restituisce lo stato aggiornato dei campi mutabili del loop.
 *
 * @module case1Block
 */

import type { AgentStep } from "../storage";
import type { GoalContract } from "../_goalVerifier";
import { makePlanStep } from "../_goalVerifier";
import { TOOL_STATUS_LABELS, narrativeLabel } from "./toolStatusLabels"; // S766
import { _parseToolArgs, _tryLiveDebugFix } from "./toolExecutor";
import type { Suggestion } from "../agent/SmartSuggestions";
import { verifyToolResult, annotateWithProof } from "../agent/verifier";
import { updateWorldModel } from "../worldModel";
import { selfLearning } from "../selfLearning";
import { recordErrorAsync } from "../selfLearningAsync"; // S649
import { errorLog } from "../agentBrain";
import { recordAndSuggestFix, notifyToolSuccess } from "../agent/errorRecovery";
import { persistBeliefState } from "../beliefRevision";
import { EXEC_REINJECT_TOOLS } from "./toolGate";
import { buildExecReinjectMessage } from "./execLoopGate";
import { processCase1Tail } from "./case1TailBlock";
import { infraCategory } from "./stallDetector"; // P32-GE-1
import { buildErrorSignature } from "./errorSignatureBuilder"; // S700
import { selfHealingEngine } from "./selfHealingEngine"; // S700
import type { ToolCallRaw, ApiMsg } from "./networkTools";

// ─── Interfaces ────────────────────────────────────────────────────────────────

export interface Case1BlockCtx {
  toolCalls: ToolCallRaw[];
  rawText: string | null;
  iter: number;
  MAX_ITER: number;
  // Mutable state (returned updated)
  formalGoal: GoalContract | null;
  steps: AgentStep[];
  loopMessages: ApiMsg[];
  consecutiveAllFailIters: number;
  effectiveMaxIter: number;
  sessStats: { autoFixed: number; propagated: number; verifyFails: number };
  lastExecTool: string;
  lastExecOutput: string;
  lastExecResultIter: number;
  // Read-only context
  signal?: AbortSignal;
  lastUserMsg: string;
  loopTaskId: string | null;
  isCodeTask: boolean;
  // Callbacks
  onStatus: (msg: string) => void;
  onSteps: (steps: AgentStep[]) => void;
  onChunk: (chunk: string) => void;
  onSuggestions?: (s: Suggestion[]) => void;
  // Closures from outer scope
  executeToolGated: (name: string, args: Record<string, unknown>) => Promise<string>;
  emitFinalText: (text: string, signal?: AbortSignal) => void;
  writeSess: () => void;
  epistemicTag: (toolName: string) => string;
  cbIsOpen: (name: string, iter: number) => boolean;
  cbUpdate: (name: string, result: string, iter: number) => void;
  cbSkipMsg: (name: string) => string;
  stallCheck: (name: string, args: Record<string, unknown>) => number;
  confSession: { recordSignal: (s: string) => void };
  onReasoningUpdate: (u: { last_result?: string; error?: string; completed_step?: string }) => void;
}

export type Case1BlockAction = "continue" | "return" | "return-success";

export interface Case1BlockResult {
  action: Case1BlockAction;
  formalGoal: GoalContract | null;
  consecutiveAllFailIters: number;
  effectiveMaxIter: number;
  sessStats: { autoFixed: number; propagated: number; verifyFails: number };
  lastExecTool: string;
  lastExecOutput: string;
  lastExecResultIter: number;
}

// ─── Internal helpers ──────────────────────────────────────────────────────────

/** S274: pool limitato a 4 paralleli — evita saturazione banda su mobile */
async function pLimitPool<T>(tasks: (() => Promise<T>)[], limit = 4): Promise<T[]> {
  const out: T[] = new Array(tasks.length);
  let i = 0;
  async function worker() {
    while (i < tasks.length) { const idx = i++; out[idx] = await tasks[idx](); }
  }
  await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
  return out;
}

// ─── Main export ───────────────────────────────────────────────────────────────

/** Processa il Case 1 completo (native tool calls). Sempre seguito da continue o return nel caller. */
export async function processCase1Block(ctx: Case1BlockCtx): Promise<Case1BlockResult> {
  const {
    toolCalls, rawText, iter, MAX_ITER, signal, lastUserMsg, loopTaskId, isCodeTask,
    onStatus, onSteps, onChunk, onSuggestions, executeToolGated, emitFinalText, writeSess,
    epistemicTag, cbIsOpen, cbUpdate, cbSkipMsg, stallCheck, confSession, onReasoningUpdate,
  } = ctx;

  // Local mutable copies — returned at end
  let formalGoal              = ctx.formalGoal;
  const steps                 = ctx.steps;        // mutated by reference
  const loopMessages          = ctx.loopMessages; // mutated by reference
  let consecutiveAllFailIters = ctx.consecutiveAllFailIters;
  let effectiveMaxIter        = ctx.effectiveMaxIter;
  const sessStats             = { ...ctx.sessStats };
  let lastExecTool            = ctx.lastExecTool;
  let lastExecOutput          = ctx.lastExecOutput;
  let lastExecResultIter      = ctx.lastExecResultIter;

  // ── 1. Sintetizza formalGoal a iter 0 ─────────────────────────────────────
  // Step1/S104: Case 1 non produce blocchi GOAL/DONE_WHEN — sintetizza da lastUserMsg (parity con Case 2)
  if (iter === 0 && formalGoal === null) {
    formalGoal = {
      goal:       lastUserMsg.slice(0, 200),  // S610: 120→200
      doneWhen:   lastUserMsg.slice(0, 200),  // S610: 120→200
      outOfScope: "",
    };
    // S203: emetti __plan__ step per rendere il contratto visibile in UI
    try {
      const _planStep = makePlanStep(formalGoal);
      steps.unshift(_planStep);
      onSteps([...steps]);
    } catch { /* non-blocking */ }
  }
  loopMessages.push({ role: "assistant", content: rawText ?? null, tool_calls: toolCalls } as ApiMsg);

  const toolNames = toolCalls.map(tc => TOOL_STATUS_LABELS[tc.function.name] ?? tc.function.name);
  onStatus(toolNames.length > 1 ? `${toolNames.length} tool in parallelo…` : (toolNames[0] ?? "Tool…"));

  // ── 2. Registra tutti gli step come "running" ──────────────────────────────
  const newStepIndices: number[] = [];
  for (const tc of toolCalls) {
    const _parsed0 = _parseToolArgs(tc.function.arguments); // S203: validated args
    // S766: context-aware label — mostra query/path/comando specifico invece del tipo tool generico
    steps.push({ tool: tc.function.name, args: _parsed0.args, result: "", status: "running", explanation: narrativeLabel(tc.function.name, _parsed0.args) });
    newStepIndices.push(steps.length - 1);
  }
  onSteps([...steps]);

  // ── 3. Esecuzione parallela (max 4) con CB + stall detection ──────────────
  const results = await pLimitPool(
    toolCalls.map((tc) => async () => {
      // S203: Validate + repair LLM-generated args before ANY execution
      const _pv = _parseToolArgs(tc.function.arguments);
      const args: Record<string, unknown> = _pv.args;
      if (!_pv.valid) {
        const _errMsg = `❌ [ARGS_INVALID] "${tc.function.name}" non eseguito: argomenti JSON non riparabili. ${_pv.error ?? ""} Rigenera la chiamata con JSON valido.`;
        console.warn("[S203] tool args JSON non riparabile", tc.function.name, tc.function.arguments);
        return { tc, result: _errMsg, durationMs: 0, args };
      }
      if (_pv.repaired) {
        console.log("[S203] tool args riparati da repairJson", tc.function.name); // check-console-log: ok
      }
      // Circuit breaker: skip tripped tools
      if (cbIsOpen(tc.function.name, iter)) {
        onStatus(`⚡ ${tc.function.name} saltato (circuit open)`);
        return { tc, result: cbSkipMsg(tc.function.name), durationMs: 0, args };
      }
      // Gap B (S122): stall detection — abort repeated identical calls
      const _c1StallN = stallCheck(tc.function.name, args);
      if (_c1StallN >= 3) {
        // S444: hint cross-sessione per messaggio stallo
        const _c1ErrHint = (() => {
          try {
            const _wf = errorLog.resolvedWithFix().find(e => e.context.includes(tc.function.name));
            return _wf ? `\n💡 Fix noto (${_wf.count}× visto): ${_wf.fix!.slice(0, 200)}` : "";
          } catch { return ""; }
        })();
        return { tc, result: `❌ Stallo rilevato — "${tc.function.name}" chiamato ${_c1StallN}× con gli stessi argomenti. Cambia approccio.${_c1ErrHint}`, durationMs: 0, args };
      }
      const start  = Date.now();
      const result = await executeToolGated(tc.function.name, args);
      cbUpdate(tc.function.name, result, iter);
      return { tc, result, durationMs: Date.now() - start, args };
    }),
  );

  // ── 4. Processa i risultati ────────────────────────────────────────────────
  let anyError = false;
  const _failureRecoveries: Array<{ toolName: string; result: ReturnType<typeof recordAndSuggestFix> }> = [];

  for (let i = 0; i < results.length; i++) {
    const { tc, result, durationMs, args } = results[i];
    const si = newStepIndices[i];
    const _vr         = verifyToolResult(tc.function.name, args, result);
    const _annotated  = annotateWithProof(tc.function.name, result, _vr);
    steps[si].result     = result;
    steps[si].status     = result.startsWith("❌") ? "error" : "done";
    steps[si].durationMs = durationMs;
    // Step2: push annotated result con epistemic tag al modello
    const _epTag1        = epistemicTag(tc.function.name);
    const _taggedAnnotated = `[EPISTEMIC:${_epTag1}] ${_annotated}`;
    loopMessages.push({ role: "tool", content: _taggedAnnotated, tool_call_id: tc.id, name: tc.function.name } as ApiMsg);
    // Step5: update world model (fire-and-forget)
    updateWorldModel(tc.function.name, args, result).catch(() => {});

    if (result.startsWith("❌")) {
      anyError = true;
      try { confSession.recordSignal("tool_failure"); } catch { /* non-blocking */ }
      // S444: registra su errorLog per hint cross-sessione
      try { errorLog.record(result.slice(0, 300), `tool:${tc.function.name}`, undefined, typeof args.code === "string" ? args.code.slice(0, 500) : undefined); } catch { /* non-blocking */ }
      // S204 GAP1: deriva fix dall'alternativa nota prima di registrare l'errore
      const _g1fix = (() => {
        try { const _a = selfLearning.getAlternative(tc.function.name, result.slice(0, 150)); return _a ? `Usa ${_a.tool}: ${_a.hint.slice(0, 200)}` : ""; } catch { return ""; }  // S610: 80→150
      })();
      recordErrorAsync(result, `tool:${tc.function.name}`, _g1fix); // S649
      try { _failureRecoveries.push({ toolName: tc.function.name, result: recordAndSuggestFix({ toolName: tc.function.name, errorMsg: result, args }) }); } catch { /* non-blocking */ }
      // S59: live debug-fix per code execution tools
      let _liveFixed: string | null = null;
      // S59 original: EXEC_TOOLS → debugLoop + selfHealingEngine (code)
      if (!_liveFixed) {
        _liveFixed = await _tryLiveDebugFix(tc.function.name, args, result, `iter${iter}`, onStatus);
      }
      // S697: selfHealingEngine for ALL tool types (transient/structural errors)
      if (!_liveFixed) {
        const _s697sig = buildErrorSignature(result, tc.function.name, args, iter, 0);
        const _hasQuickStrategy =
          _s697sig.category === 'not_found'    ||
          _s697sig.category === 'timeout'      ||
          _s697sig.category === 'rate_limit'   ||
          _s697sig.category === 'cors'         ||
          _s697sig.category === 'network_fail' ||
          _s697sig.isTransient;
        if (_hasQuickStrategy) {
          const _s697healed = await selfHealingEngine.run({
            tool: tc.function.name, args, errorResult: result,
            taskId: loopTaskId ?? `iter${iter}`,
            iteration: iter, onStatus, signal,
            executeToolFn: executeToolGated,
          });
          if (_s697healed.healed) _liveFixed = _s697healed.result;
        }
      }
      if (_liveFixed) {
        // S204+: registra fix immediatamente — il modello riceverà il risultato corretto, non l'errore
        try {
          recordErrorAsync(result.slice(0, 100), `livefix:${tc.function.name}`, _liveFixed.slice(0, 250)); // S649
        } catch { /* non-blocking */ }
        // S444: marca errorLog come risolto con fix applicato (cross-sessione)
        try { errorLog.markFixed(errorLog.hashOf(result.slice(0, 300)), _liveFixed.slice(0, 500)); } catch { /* non-blocking */ }
        steps[si].result = _liveFixed; steps[si].status = "done";
        loopMessages[loopMessages.length - 1] = { role: "tool", content: _liveFixed, tool_call_id: tc.id, name: tc.function.name } as ApiMsg;
        anyError = false;
        sessStats.autoFixed++; // S205: error resolved autonomously
        // Fix 8 (S421): extra iteration quando l'agente è in modalità repair intensivo
        if (sessStats.autoFixed >= 2 && effectiveMaxIter < MAX_ITER) {
          effectiveMaxIter = Math.min(effectiveMaxIter + 1, MAX_ITER);
        }
      } else {
        sessStats.propagated++; // S205: error reaches model unchanged
      }
    } else {
      try { notifyToolSuccess(tc.function.name); } catch { /* non-blocking */ }
      try { confSession.recordSignal("tool_success"); } catch { /* non-blocking */ }
    }
    // S83: track exec result for loop closure (Case 1)
    if (EXEC_REINJECT_TOOLS.has(tc.function.name)) {
      lastExecResultIter = iter;
      lastExecOutput     = result.slice(0, 1000);
      lastExecTool       = tc.function.name;
      loopMessages.push({ role: "user", content: buildExecReinjectMessage(lastExecTool, lastExecOutput) } as ApiMsg);
    }
  }
  onSteps([...steps]);


  // ── P32-GE-1: Infra error graceful exit ─────────────────────────────────────
  // Scansiona le ultime tool-result messages per rilevare ≥3 errori consecutivi
  // della stessa categoria infrastrutturale → exit onesto invece di loop infinito.
  {
    const _c1ToolMsgs = (loopMessages as Array<{ role: string; content?: unknown }>)
      .filter(m => m.role === "tool" && typeof m.content === "string")
      .slice(-9); // ultime 9 risposte (max 3 iter × 3 tool)
    let _c1InfraCount = 0;
    let _c1InfraCat: string | null = null;
    for (let _i = _c1ToolMsgs.length - 1; _i >= 0; _i--) {
      const _cnt = _c1ToolMsgs[_i].content as string;
      // rimuovi prefix epistemic tag [EPISTEMIC:...] per l'analisi
      const _raw = _cnt.replace(/^[EPISTEMIC:[^]]+]s*/, "");
      if (!_raw.startsWith("❌")) break;
      const _cat = infraCategory(_raw);
      if (_cat === null) break;
      if (_c1InfraCat === null) _c1InfraCat = _cat;
      if (_cat !== _c1InfraCat) break;
      _c1InfraCount++;
    }
    if (_c1InfraCount >= 3 && _c1InfraCat) {
      const _infraMsgMap: Record<string, string> = {
        rate_limit:   "Il provider è in rate limit (429) ripetuto",
        timeout:      "Timeout ripetuto dal provider o backend",
        server_5xx:   "Errore server 5xx ripetuto dal backend",
        network_fail: "Errore di rete persistente",
      };
      const _infraLabel = _infraMsgMap[_c1InfraCat] ?? "Errore infrastrutturale ripetuto";
      onChunk(`_(${_infraLabel} — rilevato ${_c1InfraCount}× di seguito. Il servizio è temporaneamente non disponibile. Riprova tra qualche minuto o cambia provider nelle impostazioni.)_`);
      writeSess();
      return {
        action: "return",
        formalGoal,
        consecutiveAllFailIters,
        effectiveMaxIter,
        sessStats,
        lastExecTool,
        lastExecOutput,
        lastExecResultIter,
      };
    }
  }

  // ── 5. ReasoningCore update + belief state persistence ────────────────────
  onReasoningUpdate({
    last_result:    results.filter(r => !r.result.startsWith("❌")).map(r => r.tc.function.name + "✓").join(" ").slice(0, 200) || undefined,
    error:          anyError ? results.filter(r => r.result.startsWith("❌")).map(r => r.tc.function.name).join(", ") : undefined,
    completed_step: results.filter(r => !r.result.startsWith("❌")).map(r => r.tc.function.name).join(", ") || undefined,
  });

  // S351: persist belief state per batch error Case 1 (non-blocking)
  if (anyError) {
    for (const { result: _brr } of results) {
      if (_brr.startsWith("❌")) { try { persistBeliefState(loopTaskId ?? "", lastUserMsg.slice(0, 40)); } catch { /* non-blocking */ } break; }
    }
  }

  // ── 6. Case 1 tail (stagnation + belief revision + DONE check + synthesis) ─
  // S496: → agentLoop/case1TailBlock.ts
  const _c1t = processCase1Tail({
    results, failureRecoveries: _failureRecoveries, anyError,
    consecutiveAllFailIters, effectiveMaxIter,
    steps, loopMessages, iter, isCodeTask,
    rawText: rawText ?? null, signal, lastUserMsg,
    loopTaskId, onChunk, onSteps, onStatus,
    onSuggestions,
    emitFinalText, writeSess,
  });
  consecutiveAllFailIters = _c1t.newConsecutiveAllFailIters;
  effectiveMaxIter        = _c1t.newEffectiveMaxIter;

  return {
    action: _c1t.action === "return" ? "return"
          : _c1t.action === "return-success" ? "return-success"
          : "continue",
    formalGoal,
    consecutiveAllFailIters,
    effectiveMaxIter,
    sessStats,
    lastExecTool,
    lastExecOutput,
    lastExecResultIter,
  };
}