AUDIT / src /lib /agent /AgentRuntime.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
15.3 kB
// ─── 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;
}