// ─── AgentRuntime.ts ───────────────────────────────────────────────────────── // Fase 1+2 — Nucleo unico di controllo + state machine rigida. // // Regola fondamentale: // Nessun componente UI chiama direttamente provider, verifier, retry o memory. // Tutto passa dal runtime. // // State machine (per ogni task): // IDLE → PLANNING → EXECUTING → VERIFYING → DONE // ↘ FAILED // // Solo il verifier può portare a DONE. // Browser-safe, iPhone compatible, zero side-effects globali automatici. import { planTask } from "./planner"; import type { Plan } from "./planner"; import { verifyToolResult, annotateWithProof } from "./verifier"; import { decideRetry } from "./retryEngine"; import { contextBuilder } from "./contextBuilder"; import { sessionMemory } from "./MemoryUpdater"; // S-RETRY-WIRE: validateGate sostituisce validateOutput + pipeline manuale import { validateGate } from "./validationGate"; import type { ValidationGateResult } from "./validationGate"; import { acquireExtraCall } from "../agentLoop/callBudgetCoordinator"; // GAP-G: call amplification limiter // ─── Tipi (estratti in agentRuntimeTypes.ts — S553 — re-esportati per backward compat) ────────── import type { TaskState, TaskResult, RuntimeSnapshot, RuntimeListener, RuntimeOptions, LLMCallerFn, ToolExecutorFn } from "./agentRuntimeTypes"; import type { AgentRuntimeConfig } from "./agentRuntimeTypes"; export type { TaskState, TaskResult, RuntimeSnapshot, RuntimeListener, RuntimeOptions, LLMCallerFn, ToolExecutorFn, AgentRuntimeConfig } from "./agentRuntimeTypes"; // ─── Helper: estrai codice di riferimento dal task ────────────────────────── // S-RETRY-WIRE: cerca blocco TypeScript/JS nel body del task per antiRewriteGuard. // Il benchmark inserisce il codice originale come blocco fenced nel task. function _extractReferenceCode(task: string): string | undefined { const m = task.match(/```(?:typescript|javascript|ts|js|tsx|jsx)\n([\s\S]*?)```/); return m?.[1]?.trim() || undefined; } // ─── AgentRuntime ───────────────────────────────────────────────────────────── class AgentRuntime { private _state: TaskState = "IDLE"; private _task: string | null = null; private _progress: string = ""; private _lastResult?: TaskResult; private _errors: string[] = []; private _listeners = new Set(); private _llmCaller: LLMCallerFn | null = null; // ─── Registrazione dipendenze ───────────────────────────────────────────── /** * Inietta la funzione di chiamata LLM. * Deve essere chiamata una volta al boot. */ registerLLMCaller(fn: LLMCallerFn): void { this._llmCaller = fn; } /** * Inietta la funzione di chiamata tool. * Tenuto per compatibilità futura — non usato internamente in questa versione. */ registerToolExecutor(_fn: ToolExecutorFn): void { // reserved for future use } // ─── Accesso stato ──────────────────────────────────────────────────────── getSnapshot(): RuntimeSnapshot { return { state: this._state, task: this._task, progress: this._progress, lastResult: this._lastResult, errors: [...this._errors], }; } getState(): TaskState { return this._state; } isIdle(): boolean { return this._state === "IDLE"; } /** * GAP-A: Avanza lo stato della state machine dal loop esterno (agentLoop.ts). * Rende la state machine autoritativa — agentRuntime.getState() riflette * lo stato live del loop, non solo l'ultima run completata. * Safari-safe: chiama _notify() → tutti i subscriber ricevono lo snap aggiornato. */ advanceTo(state: TaskState, progress?: string): void { this._transition(state, progress); } // ─── Subscribe ──────────────────────────────────────────────────────────── subscribe(fn: RuntimeListener): () => void { this._listeners.add(fn); fn(this.getSnapshot()); return () => this._listeners.delete(fn); } private _notify(): void { const snap = this.getSnapshot(); this._listeners.forEach(fn => fn(snap)); } // ─── Transizioni state machine ──────────────────────────────────────────── private _transition(next: TaskState, progress?: string): void { this._state = next; this._progress = progress ?? this._progress; this._notify(); } // ─── Metodo principale ──────────────────────────────────────────────────── /** * Esegue un task completo attraverso la state machine. * Unico punto di ingresso per qualsiasi operazione agentica. * * IDLE → PLANNING → EXECUTING → VERIFYING → DONE/FAILED */ async run( task: string, rawOutput: string, opts?: RuntimeOptions, ): Promise { // Non eseguire se già occupato if (this._state !== "IDLE" && this._state !== "DONE" && this._state !== "FAILED") { return this._lastResult ?? this._makeFailResult(task, "runtime occupato", 0); } const startMs = Date.now(); const maxRetries = opts?.maxRetries ?? 1; this._task = task; this._errors = []; const toolResults: Record = {}; let retries = 0; let plan: Plan | undefined; // ── PLANNING ──────────────────────────────────────────────────────────── this._transition("PLANNING", "Pianificazione task..."); try { plan = planTask(task); } catch (e) { // Planning non bloccante — continua senza piano this._errors.push("planning fallito: " + String(e)); } // GAP-4: thinkingExpander — 2 approcci alternativi per task ad alta incertezza // GAP-G: thinking ha priorità massima nel budget — acquisisce prima degli altri let _thinkingHint = ""; if (this._llmCaller && acquireExtraCall('thinking')) { try { const { expandThinking } = await import("./thinkingExpander"); const thinking = await expandThinking(task, this._llmCaller); if (thinking && thinking.confidence >= 6) _thinkingHint = thinking.chosenApproach; } catch { /* non bloccante */ } } // ── EXECUTING ─────────────────────────────────────────────────────────── this._transition("EXECUTING", "Elaborazione risposta..."); let currentOutput = rawOutput; // S-RETRY-WIRE: estrai referenceCode per antiRewriteGuard const _referenceCode = _extractReferenceCode(task); let gate: ValidationGateResult = validateGate(currentOutput, { taskHint: task, referenceCode: _referenceCode }); opts?.onPartialOutput?.(gate.text); // ── Retry loop (max 1 retry di default) ────────────────────────────── // S-RETRY-WIRE: retry se quality < 4 OPPURE antiRewriteGuard richiede retry while ((gate.quality < 4 || gate.verdict === "retry") && retries < maxRetries && this._llmCaller) { retries++; this._progress = `Retry ${retries}/${maxRetries} — qualita bassa (${gate.quality}/10)`; this._notify(); const retryCtx = decideRetry({ task, output: currentOutput, iter: retries, maxIter: maxRetries + 1, toolResults: Object.values(toolResults), referenceCode: _referenceCode, // S-RETRY-WIRE: attiva antiRewriteGuard }); if (!retryCtx.shouldRetry) break; // Contesto leggero per retry (Fase 6 — solo quello che serve) const ctx = contextBuilder.getState(); const ctxStr = [ ctx.summary ? `Contesto: ${ctx.summary}` : "", ctx.currentTask ? `Task corrente: ${ctx.currentTask}` : "", ctx.recentErrors.length ? `Errori recenti: ${ctx.recentErrors.join("; ")}` : "", _thinkingHint ? `Approccio suggerito: ${_thinkingHint}` : "", // GAP-4 ].filter(Boolean).join("\n"); try { const retryRaw = await this._llmCaller([ { role: "system", content: ctxStr || "Sei un assistente AI preciso che risponde in italiano." }, { role: "user", content: retryCtx.augmentedPrompt ?? task }, ]); currentOutput = retryRaw; gate = validateGate(currentOutput, { taskHint: task, referenceCode: _referenceCode }); opts?.onPartialOutput?.(gate.text); } catch (e) { this._errors.push("retry LLM fallito: " + String(e)); break; } } // GAP-1 + ORCH-4: Dual-Agent Debate — se quality < 7 dopo retry // ORCH-4: skip se pre-loop debate (debateBlock.ts) ha già prodotto consensus // in questo turno — evita 4 LLM calls debate × turno su Groq free tier. // GAP-G: dual_debate ha priorità più bassa — acquisisce solo se budget residuo if (gate.quality < 7 && this._llmCaller && acquireExtraCall('dual_debate')) { try { const { isDebateUsed } = await import("../agentLoop/debateBudget"); if (!isDebateUsed()) { const { runDualAgentDebate } = await import("./dualAgentDebate"); const debate = await runDualAgentDebate(task, gate.text, this._llmCaller, 2); if (debate.merged) { currentOutput = debate.finalOutput; gate = validateGate(currentOutput, { taskHint: task, referenceCode: _referenceCode }); } } } catch { /* non bloccante */ } } // ── VERIFYING ──────────────────────────────────────────────────────────── this._transition("VERIFYING", "Verifica output..."); let verified = true; for (const [key, result] of Object.entries(toolResults)) { const [toolName, ...argsParts] = key.split("|"); let parsedArgs: Record = {}; try { parsedArgs = JSON.parse(argsParts.join("|")); } catch { /* ignora */ } const vr = verifyToolResult(toolName, parsedArgs, result); if (!vr.verified) { verified = false; this._errors.push(`Tool "${toolName}" non verificato: ${vr.warning ?? vr.proof}`); } const annotated = annotateWithProof(toolName, result, vr); toolResults[key] = annotated; } // Se l'output è troppo corto o vuoto → FAILED if (gate.text.length < 5) { this._transition("FAILED", "Output vuoto o troppo breve"); const result = this._makeFailResult(task, "output vuoto", Date.now() - startMs); this._lastResult = result; return result; } // ── Aggiorna memoria leggera (Fase 6) ──────────────────────────────────── try { sessionMemory.add("agent", task.slice(0, 120), currentOutput.slice(0, 500)); // S610: task 60→120, output 300→500 } catch { /* non bloccante */ } // ── DONE ───────────────────────────────────────────────────────────────── // Solo il verifier porta a DONE — qui abbiamo completato la verifica this._transition("DONE", "Completato"); const result: TaskResult = { output: gate.text, state: "DONE", plan, toolResults, quality: gate.quality, verified, retries, durationMs: Date.now() - startMs, errors: [...this._errors], }; this._lastResult = result; // Aggiorna contesto per sessione futura try { if (!verified || gate.quality < 4) { contextBuilder.recordError(`quality ${gate.quality}/10 per: ${task.slice(0, 120)}`); // S610: 60→120 } } catch { /* non bloccante */ } // Reset a IDLE dopo breve delay (pronto per prossimo task) setTimeout(() => { if (this._state === "DONE" || this._state === "FAILED") { this._transition("IDLE", "Pronto"); } }, 500); return result; } // ─── Abort ──────────────────────────────────────────────────────────────── abort(): void { if (this._state === "EXECUTING" || this._state === "VERIFYING") { this._transition("FAILED", "Interrotto dall'utente"); setTimeout(() => this._transition("IDLE", "Pronto"), 300); } } // ─── Helpers privati ────────────────────────────────────────────────────── private _makeFailResult(task: string, reason: string, durationMs: number): TaskResult { void task; return { output: "", state: "FAILED", toolResults: {}, quality: 0, verified: false, retries: 0, durationMs, errors: [reason], }; } } // ─── Singleton (compatibilità retroattiva) ──────────────────────────────────── export const agentRuntime = new AgentRuntime(); // Tipo esportato per i consumer export type { AgentRuntime }; // ─── S219: Factory multi-istanza per il Dual-Agent Debate ───────────────────── // Permette di creare istanze isolate (Builder, Critic) senza toccare il singleton. // La memory è separata per istanza — nessun cross-talk tra Builder e Critic. // AgentRuntimeConfig estratto in agentRuntimeTypes.ts — S553 /** * Crea un'istanza isolata di AgentRuntime per il Dual-Agent Debate. * * Uso: * const builder = createAgentRuntime({ role: "builder", llmCaller: myFn }); * const critic = createAgentRuntime({ role: "critic", llmCaller: myFn }); * // Le due istanze non condividono stato, memory o listeners. * * Il singleton `agentRuntime` rimane invariato — zero breaking changes. */ export function createAgentRuntime(config?: AgentRuntimeConfig): AgentRuntime { const instance = new AgentRuntime(); if (config?.llmCaller) { instance.registerLLMCaller(config.llmCaller); } return instance; }