AUDIT / src /lib /agentLoop.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
11.5 kB
/* 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 */ }
}
}
}