/** * 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) => Promise; 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) => 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(tasks: (() => Promise)[], limit = 4): Promise { 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 { 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 = _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 }> = []; 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 = { 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, }; }