Spaces:
Sleeping
Sleeping
| /** | |
| * 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, | |
| }; | |
| } | |