Spaces:
Sleeping
Sleeping
| /* 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<boolean>; | |
| 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<void> { | |
| // 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<string>, | |
| }); | |
| 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 */ } | |
| } | |
| } | |
| } | |