Spaces:
Sleeping
Sleeping
| // ─── 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<RuntimeListener>(); | |
| 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<TaskResult> { | |
| // 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<string, string> = {}; | |
| 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<string, unknown> = {}; | |
| 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; | |
| } | |