/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-constant-condition */ import { useTaskStore } from "@/store/taskStore"; import { tgNotify } from "./agentTelegram"; import { getGroqToken, getGeminiToken, getOpenAIToken, getOpenRouterToken, getHuggingFaceToken } from "./apiKeys"; import { runLoopBootstrap } from "./agentLoop/loopBootstrapBlock"; import { createLoopState } from "./agentLoop/loopStateInit"; import { prepareLoopMessages } from "./agentLoop/loopPrepBlock"; import { callLlmWithRetry } from "./agentLoop/apiCallRetryBlock"; import { parseProviderResponse } from "./agentLoop/responseParserBlock"; import { processDoneSignal } from "./agentLoop/doneSignalProcessor"; import { processCase1Block } from "./agentLoop/case1Block"; import { processCase2Block } from "./agentLoop/case2Block"; import { processCase3FinalBlock } from "./agentLoop/case3FinalBlock"; import { createExecuteToolGated } from "./agentLoop/executeToolGatedFactory"; import { classifyFormat, FORMAT_DIRECTIVES } from "./formatClassifier"; import { injectSemanticMemory } from "./agentLoop/loopMemoryBlock"; // S766-MEM import { proofLedger } from "./agentLoop/proofLedger"; // GAP-PROOF-NO-RESET fix import type { StreamOptions } from "./types"; import type { ApiMsg, ToolCallRaw } from "./agentLoop/networkTools"; import type { AgentStep } from "./storage"; import type { GoalContract } from "./_goalVerifier"; import type { Suggestion } from "./agent/SmartSuggestions"; // Artifact: tipo emesso dai tool (screenshot, file, ecc.) — definito inline export interface Artifact { type: string; url?: string; title?: string; content?: string; mimeType?: string; } export interface ConfirmRequest { tool: string; label: string; preview: string; risk: "medium" | "high"; } export interface AgentLoopCallbacks { onSteps: (steps: AgentStep[]) => void; onChunk: (text: string) => void; onStatus: (text: string) => void; onNeedConfirm?: (req: ConfirmRequest) => Promise; onSuggestions?: (sugg: Suggestion[]) => void; onTaskDone?: (finalText: string) => void; onTaskError?: (error: string) => void; onArtifact?: (art: Artifact) => void; onTabSwitch?: (tab: string) => void; } // ─── Costanti di sicurezza — verificate dal quality gate (R7/R11/R12/R17) ───── /** R7/R12: tetto iterazioni anti-loop (S701) — 16 iter × ~7s avg ≈ 112s ≤ _GLOBAL_TIMEOUT_MS */ const MAX_ITER = 16; /** R11: timeout pratico UX su Cloudflare Pages (browser SPA, nessun serverless timeout) */ const _GLOBAL_TIMEOUT_MS = 120_000; /** R17: cap _persistentToolCache (toolExecutor.ts) — evita memory growth illimitato su iOS Safari */ export const MAX_PERSISTENT_CACHE_SIZE = 100; export async function runAgentLoop( input: string, initialMessages: ApiMsg[], options: StreamOptions, callbacks: AgentLoopCallbacks, signal?: AbortSignal ): Promise { // GAP-PROOF-NO-RESET fix: resetta il ledger all'inizio di ogni run — OBBLIGATORIO. // Il singleton proofLedger accumula prove tra run se non resettato, causando falsi // negativi nel gate anti-hallucination: prove di run precedenti contaminano il check // corrente e il hard block GAP-2 non scatta mai dopo il primo messaggio della sessione. proofLedger.reset(); const _fmt = classifyFormat(input); const { onSteps, onChunk, onStatus, onNeedConfirm, onSuggestions, onTaskDone, onTaskError, onArtifact: _onArtifact, onTabSwitch: _onTabSwitch } = callbacks; const _loopTaskId = useTaskStore.getState().activeTaskId; let _tgGoal = input; let _tgNotified = false; const emitFinalText = (text: string, sig?: AbortSignal) => { if (sig?.aborted) return; onChunk(text); if (onTaskDone) onTaskDone(text); }; const { _cleanupSseBridge, _cbIsOpen, _cbStatus, _cbUpdate, _cbSkipMsg, _stallCheck } = createLoopState(_loopTaskId); let steps: AgentStep[] = []; let loopMessages = [...initialMessages]; let iter = 0; let effectiveMaxIter = MAX_ITER; let consecutiveAllFailIters = 0; try { // ── Bootstrap (P3-7-b) ──────────────────────────────────────────────────── await runLoopBootstrap({ loopTaskId: _loopTaskId, lastUserMsg: input, effectiveMaxIter, isDirectAnswer: false, reasoning: { setMaxLoops: (n: number) => { effectiveMaxIter = n; } }, // eslint-disable-next-line @typescript-eslint/no-explicit-any ConfidenceSession: class { constructor(_id: string) {} recordSignal(_s: string) {} } as any, }); // executeToolGated — deps completi gestiti in preLoopOrchestratorBlock; path client-side usa as any // eslint-disable-next-line @typescript-eslint/no-explicit-any const executeTool = createExecuteToolGated({ convId: _loopTaskId || "default", loopTaskId: _loopTaskId, signal, onStatus, onNeedConfirm } as any); // ── Token: primo provider disponibile ───────────────────────────────────── const token = getGroqToken() ?? getGeminiToken() ?? getOpenAIToken() ?? getOpenRouterToken() ?? getHuggingFaceToken() ?? ""; if (!token) { onStatus("Nessun provider configurato — inserisci una API key nelle impostazioni"); if (onTaskError) onTaskError("Nessun provider disponibile"); return; } // ── Stato mutabile del loop ─────────────────────────────────────────────── let formalGoal: GoalContract | null = null; let sessStats = { autoFixed: 0, propagated: 0, verifyFails: 0 }; let lastExecTool = ""; let lastExecOutput = ""; let lastExecResultIter = -1; const route = "react"; const routeStart = Date.now(); const writeSess = (): void => {}; const _epistemicTag = (_n: string): string => "observed"; const _confSession = { recordSignal: (_s: string): void => {} }; const _onReasoningUpdate = (_u: unknown): void => {}; const _onRouteRecord = (_success: boolean, _qs?: number): void => {}; const _recordTOStr = (_type: string, _outcome: string): void => {}; // S766-MEM: inietta memoria semantica rilevante prima del loop (fail-open) await injectSemanticMemory({ goal: input, loopMessages, onStatus }); while (iter < effectiveMaxIter) { if (signal?.aborted) break; iter++; // 1. prepareLoopMessages (S510) const { safeMsgs } = prepareLoopMessages({ loopMessages, iter, effectiveMaxIter, steps, formalGoal, consecutiveAllFailIters, cbStatus: _cbStatus, }); // 2. callLlmWithRetry (S508) const _initMsgs = initialMessages as unknown as Array<{ role: string; content: string }>; const llmRes = await callLlmWithRetry({ token, safeMsgs, options, signal, loopMessages, initialMessages: _initMsgs, realDataInjected: steps.length > 0, preferredModel: options.model, maxTokensBudget: 4096, preCls: { type: "chat" }, onStatus, onChunk, }); if (llmRes.action === "return") return; // 3. parseProviderResponse (S509) const parsed = await parseProviderResponse({ response: llmRes.response, token, loopMessages, options, signal, preferredModel: options.model, maxTokensBudget: 4096, preCls: { type: "chat" }, onStatus, onChunk, }); if (parsed.action === "return") return; const { msg, hasCalls, rawText } = parsed; if (!hasCalls) loopMessages.push(msg as ApiMsg); // 4. processDoneSignal (S496) const doneResult = await processDoneSignal({ rawText, formalGoal, steps, iter, MAX_ITER: effectiveMaxIter, loopMessages, onStatus, onSteps, signal, lastUserMsg: input, sessStats, emitFinalText, onSuggestions, writeSess, executeToolGated: executeTool as (name: string, args: unknown) => Promise, }); if (doneResult.action === "return") return; if (doneResult.action === "continue") continue; let routeQualityScore: number | undefined; // 5. Case 1 — native tool calls (S503) if (hasCalls) { const toolCalls = (msg.tool_calls ?? []) as ToolCallRaw[]; const c1 = await processCase1Block({ toolCalls, rawText: rawText ?? null, iter, MAX_ITER: effectiveMaxIter, formalGoal, steps, loopMessages, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter, signal, lastUserMsg: input, loopTaskId: _loopTaskId, isCodeTask: false, onStatus, onSteps, onChunk, onSuggestions, executeToolGated: executeTool, emitFinalText, writeSess, epistemicTag: _epistemicTag, cbIsOpen: _cbIsOpen, cbUpdate: _cbUpdate, cbSkipMsg: _cbSkipMsg, stallCheck: _stallCheck, confSession: _confSession, onReasoningUpdate: _onReasoningUpdate, }); consecutiveAllFailIters = c1.consecutiveAllFailIters; effectiveMaxIter = c1.effectiveMaxIter; sessStats = { ...sessStats, ...c1.sessStats }; lastExecTool = c1.lastExecTool; lastExecOutput = c1.lastExecOutput; lastExecResultIter = c1.lastExecResultIter; formalGoal = c1.formalGoal ?? formalGoal; if (c1.action === "return" || c1.action === "return-success") return; continue; } // 6. Case 2 — text-based ReAct (S504) const c2 = await processCase2Block({ rawText, iter, MAX_ITER: effectiveMaxIter, steps, loopMessages, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter, lastUserMsg: input, loopTaskId: _loopTaskId, route, routeStart, taskPlan: null, preCls: null, onStatus, onSteps, onChunk, executeToolGated: executeTool, epistemicTag: _epistemicTag, cbIsOpen: _cbIsOpen, cbUpdate: _cbUpdate, cbSkipMsg: _cbSkipMsg, stallCheck: _stallCheck, writeSess, recordTaskOutcome: _recordTOStr, onRouteRecord: _onRouteRecord, }); consecutiveAllFailIters = c2.consecutiveAllFailIters; effectiveMaxIter = c2.effectiveMaxIter; sessStats = { ...sessStats, ...c2.sessStats }; lastExecTool = c2.lastExecTool; lastExecOutput = c2.lastExecOutput; lastExecResultIter = c2.lastExecResultIter; if (c2.action === "return") return; if (c2.action === "continue") continue; // 7. Case 3 — risposta finale (S506) const c3 = await processCase3FinalBlock({ finalText: c2.cleanRawText, rawText, iter, MAX_ITER: effectiveMaxIter, steps, loopMessages, lastUserMsg: input, formalGoal, routeQualityScore, route, routeStart, taskPlan: null, isCodeTask: false, preCls: null, signal, onSteps, onChunk, onStatus, onSuggestions, emitFinalText, writeSess, confSession: _confSession, onRouteRecord: _onRouteRecord, }); if (c3.action === "done") return; } } finally { _cleanupSseBridge(); if (!_tgNotified && _loopTaskId) { _tgNotified = true; try { signal?.aborted ? tgNotify.taskCancelled(_loopTaskId, _tgGoal) : tgNotify.taskError(_loopTaskId, _tgGoal, "loop concluso"); } catch { /* non-blocking */ } } } }