File size: 11,545 Bytes
95eb75a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* 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 */ }
    }
  }
}