AUDIT / src /lib /agentLoop /case2Block.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
17.2 kB
/**
* case2Block.ts — S504: extracted from agentLoop.ts
*
* Gestisce il Case 2 completo: text-based ReAct actions (HF/text models).
* Pipeline:
* 1. Stripping blocchi <think>…</think> da rawText → cleanRawText
* 2. Parse textActions da cleanRawText
* 3. Se textActions.length > 0 && iter < MAX_ITER-1:
* - Esecuzione sequenziale tool con CB + stall detection
* - Error recovery, live debug fix, belief revision, exec-reinject tracking
* - Stagnation guard (3 iters consecutive fallite → return)
* → action "continue"
* 4. Altrimenti: action "skip" (il caller processa Case 3 con cleanRawText)
*
* Muta steps e loopMessages per reference.
* Restituisce cleanRawText sempre (usato da Case 3 per candidateText).
*
* @module case2Block
*/
import type { AgentStep } from "../storage";
import { narrativeLabel } from "./toolStatusLabels"; // S766
import { parseTextActions } from "./intentParser";
import { _tryLiveDebugFix } from "./toolExecutor";
import { verifyToolResult, annotateWithProof } from "../agent/verifier";
import { updateWorldModel } from "../worldModel";
import { selfLearning } from "../selfLearning";
import { recordErrorAsync } from "../selfLearningAsync"; // S649
import { errorLog } from "../agentBrain";
import { recordAndSuggestFix, notifyToolSuccess } from "../agent/errorRecovery";
import { persistBeliefState, handleToolFailureBelief, shouldReviseBeliefs, buildRevisionPrompt } from "../beliefRevision";
import { predictOutcome, shouldReviseImmediately } from "./actionSimulator"; // S-PRED
import { EXEC_REINJECT_TOOLS } from "./toolGate";
import { infraCategory } from "./stallDetector"; // P32-GE-1
import { buildExecReinjectMessage } from "./execLoopGate";
import type { ApiMsg } from "./networkTools";
// ─── Interfaces ────────────────────────────────────────────────────────────────
export interface Case2BlockCtx {
rawText: string;
iter: number;
MAX_ITER: number;
// Mutable state (returned updated)
steps: AgentStep[];
loopMessages: ApiMsg[];
consecutiveAllFailIters: number;
effectiveMaxIter: number;
sessStats: { autoFixed: number; propagated: number; verifyFails: number };
lastExecTool: string;
lastExecOutput: string;
lastExecResultIter: number;
// Read-only context
lastUserMsg: string;
loopTaskId: string | null;
route: string;
routeStart: number;
taskPlan: unknown;
preCls: { type: string } | null;
// Callbacks
onStatus: (msg: string) => void;
onSteps: (steps: AgentStep[]) => void;
onChunk: (chunk: string) => void;
// Closures
executeToolGated: (name: string, args: unknown) => Promise<string>;
epistemicTag: (toolName: string) => string;
cbIsOpen: (name: string, iter: number) => boolean;
cbUpdate: (name: string, result: string, iter: number) => void;
cbSkipMsg: (name: string) => string;
stallCheck: (name: string, args: Record<string, unknown>) => number;
writeSess: () => void;
recordTaskOutcome: (type: string, outcome: string) => void;
onRouteRecord: (success: boolean) => void;
}
export type Case2BlockAction = "continue" | "return" | "skip";
export interface Case2BlockResult {
action: Case2BlockAction;
cleanRawText: string;
consecutiveAllFailIters: number;
effectiveMaxIter: number;
sessStats: { autoFixed: number; propagated: number; verifyFails: number };
lastExecTool: string;
lastExecOutput: string;
lastExecResultIter: number;
}
// ─── Main export ───────────────────────────────────────────────────────────────
/** Processa il Case 2 (ReAct text actions). Ritorna "skip" se non ci sono azioni → il caller processa Case 3. */
export async function processCase2Block(ctx: Case2BlockCtx): Promise<Case2BlockResult> {
const {
rawText, iter, MAX_ITER, lastUserMsg, loopTaskId, route: _route, routeStart: _routeStart, taskPlan: _taskPlan, preCls,
onStatus, onSteps, onChunk, executeToolGated, epistemicTag, cbIsOpen, cbUpdate, cbSkipMsg,
stallCheck, writeSess, recordTaskOutcome, onRouteRecord,
} = ctx;
const steps = ctx.steps;
const loopMessages = ctx.loopMessages;
let consecutiveAllFailIters = ctx.consecutiveAllFailIters;
let effectiveMaxIter = ctx.effectiveMaxIter;
const sessStats = { ...ctx.sessStats };
let lastExecTool = ctx.lastExecTool;
let lastExecOutput = ctx.lastExecOutput;
let lastExecResultIter = ctx.lastExecResultIter;
// ── 1. Stripping <think>…</think> + parse textActions ─────────────────────
// S188: estrai + rimuovi tutti i blocchi <think> (ovunque, non solo in testa)
const _thinkBlocks = rawText.match(/<think>([\s\S]*?)<\/think>/gi) ?? [];
const _thinkContent = _thinkBlocks.map(b => b.replace(/<\/?think>/gi, "").trim()).filter(t => t.length > 50).join("\n\n");
// S188+S189: strip blocchi chiusi + blocco aperto/incompleto (finish_reason=length)
const cleanRawText = rawText.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<think>[\s\S]*/gi, "").trim();
if (_thinkContent.length > 50 && !steps.some(s => s.tool === "__thinking__")) {
const thinkStep: AgentStep = { tool: "__thinking__", args: {}, result: _thinkContent, status: "done" };
steps.push(thinkStep); onSteps([...steps]);
}
const textActions = parseTextActions(cleanRawText);
// ── 2. Guard: solo se ci sono azioni e siamo prima dell'ultima iter ─────────
if (!(textActions.length > 0 && iter < MAX_ITER - 1)) {
return { action: "skip", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter };
}
// ── 3. Esecuzione sequenziale tool ────────────────────────────────────────
// Show reasoning text (stripped of action tags) as status
const thinkingText = rawText.replace(/<action>[\s\S]*?<\/input>/gi, "").trim();
if (thinkingText.length > 10) onStatus(thinkingText.slice(0, 120) + (thinkingText.length > 120 ? "…" : ""));
loopMessages.push({ role: "assistant", content: rawText } as ApiMsg);
let _beliefRevisionDoneThisIter = false; // S-GAP2a guard: al massimo 1 revisione per iterazione
for (const ta of textActions) {
// S766: label contestuale invece del tipo tool generico
onStatus(narrativeLabel(ta.name, ta.args as Record<string, unknown>));
// S766: context-aware explanation field
const step: AgentStep = { tool: ta.name, args: ta.args, result: "", status: "running", explanation: narrativeLabel(ta.name, ta.args as Record<string, unknown>) };
steps.push(step);
onSteps([...steps]);
// S-PRED: pre-esecuzione prediction — avvisa su tool rischiosi con precedenti non risolti
const _pred = (() => {
try { return predictOutcome(ta.name, ta.args as Record<string, unknown>, errorLog.recent(50)); }
catch { return null; }
})();
if (_pred?.predictedOutcome === "likely_fail") {
onStatus(`⚠️ Predizione rischio: ${_pred.reason}`);
loopMessages.push({
role: "user",
content: `[PRE-EXECUTION WARNING] ${_pred.reason}. Valuta un approccio alternativo prima di procedere con "${ta.name}".`,
} as ApiMsg);
}
// Circuit breaker + stall detection
const _c2StallN = stallCheck(ta.name, ta.args as Record<string, unknown>);
// S444: hint cross-sessione per messaggio stallo ReAct
const _c2ErrHint = _c2StallN >= 3 ? (() => {
try {
const _wf = errorLog.resolvedWithFix().find(e => e.context.includes(ta.name));
return _wf ? `\n💡 Fix noto (${_wf.count}× visto): ${_wf.fix!.slice(0, 200)}` : "";
} catch { return ""; }
})() : "";
const result: string = _c2StallN >= 3
? `❌ Stallo rilevato — "${ta.name}" chiamato ${_c2StallN}× con gli stessi argomenti. Cambia approccio o scrivi DONE: se il task è completato.${_c2ErrHint}`
: cbIsOpen(ta.name, iter)
? cbSkipMsg(ta.name)
: await (executeToolGated(ta.name, ta.args) as Promise<string>)
.then(r => { cbUpdate(ta.name, r, iter); return r; });
step.result = result;
step.status = result.startsWith("❌") ? "error" : "done";
onSteps([...steps]);
let _reactRecovery: ReturnType<typeof recordAndSuggestFix> | null = null;
if (result.startsWith("❌")) {
// S444: registra su errorLog per hint cross-sessione (ReAct path)
try { errorLog.record(result.slice(0, 300), `tool:${ta.name}`, undefined, (typeof ta.args === "object" && ta.args !== null && "code" in ta.args) ? String((ta.args as Record<string, unknown>).code).slice(0, 500) : undefined); } catch { /* non-blocking */ }
// S204 GAP1: deriva fix dall'alternativa nota
const _g1bfix = (() => {
try { const _a = selfLearning.getAlternative(ta.name, result.slice(0, 80)); return _a ? `Usa ${_a.tool}: ${_a.hint.slice(0, 200)}` : ""; } catch { return ""; }
})();
recordErrorAsync(result, `tool:${ta.name}`, _g1bfix); // S649
// S103/S351: belief revision ReAct path — S495: extracted to beliefRevision.ts
handleToolFailureBelief({ taskId: loopTaskId ?? "", lastUserMsg, toolName: ta.name, errorResult: result, iter, path: "ReAct", loopMessages, steps, onSteps, onStatus });
// S-PRED: se la predizione era "likely_fail" e l'esito reale è errore → revisione immediata
if (_pred && shouldReviseImmediately(_pred, true) && !_beliefRevisionDoneThisIter) {
onStatus("⚠️ Predizione confermata — revisione strategia immediata");
}
try { persistBeliefState(loopTaskId ?? "", lastUserMsg.slice(0, 40)); } catch { /* non-blocking */ } // S351: persist dopo Case 2 belief revision
// S-GAP2a: belief revision narrative — rende visibile il cambio di strategia
try {
if (!_beliefRevisionDoneThisIter && shouldReviseBeliefs(loopTaskId ?? "")) {
const _br = buildRevisionPrompt(loopTaskId ?? "", lastUserMsg);
if (_br.shouldRevise) {
const _cats = _br.distinctErrorCategories;
let _narrativeMsg = "Sto ripensando l’approccio al task";
if (_cats.includes("format_error")) _narrativeMsg = "Formato incompatibile — rianalizzando il task da zero";
else if (_cats.includes("not_found")) _narrativeMsg = "Risorse non trovate — ridefinendo la strategia";
else if (_cats.includes("logic_error")) _narrativeMsg = "Errore logico identificato — riparto dall’analisi";
else if (_cats.includes("network_error")) _narrativeMsg = "Problema connessione — cambio approccio";
onStatus(_narrativeMsg);
// Emetti uno step visibile con explanation per AgentNarration / AgentStepCards
steps.push({
tool: "__belief_revision__",
args: { categories: _cats },
result: `Revisione attivata: ${_cats.join(", ")}`,
status: "done",
explanation: _narrativeMsg,
durationMs: 0,
} as AgentStep);
_beliefRevisionDoneThisIter = true;
onSteps([...steps]);
loopMessages.push({ role: "user", content: _br.revisionPrompt } as ApiMsg);
}
}
} catch { /* non-blocking */ }
try { _reactRecovery = recordAndSuggestFix({ toolName: ta.name, errorMsg: result }); } catch { /* non-blocking */ }
// S59: live debug-fix per code execution tools
const _reactFixed = await _tryLiveDebugFix(ta.name, ta.args as Record<string, unknown>, result, `react${iter}`, onStatus);
if (_reactFixed) {
// S204+: registra fix immediatamente anche nel ReAct path
try {
recordErrorAsync(result.slice(0, 100), `livefix:react:${ta.name}`, _reactFixed.slice(0, 250)); // S649
} catch { /* non-blocking */ }
// S444: marca errorLog come risolto (ReAct path, cross-sessione)
try { errorLog.markFixed(errorLog.hashOf(result.slice(0, 300)), _reactFixed.slice(0, 500)); } catch { /* non-blocking */ }
step.result = _reactFixed; step.status = "done";
sessStats.autoFixed++; // S205: error resolved autonomously (ReAct)
// Fix 8 (S421): extra iteration in repair intensivo
if (sessStats.autoFixed >= 2 && effectiveMaxIter < MAX_ITER) {
effectiveMaxIter = Math.min(effectiveMaxIter + 1, MAX_ITER);
}
} else {
sessStats.propagated++; // S205: error reaches model unchanged (ReAct)
}
}
// S83: track exec result for loop closure (Case 2 - ReAct)
if (EXEC_REINJECT_TOOLS.has(ta.name)) {
lastExecResultIter = iter;
lastExecOutput = (step.result || result).slice(0, 1000);
lastExecTool = ta.name;
loopMessages.push({ role: "user", content: buildExecReinjectMessage(lastExecTool, lastExecOutput) } as ApiMsg);
}
if (!result.startsWith("❌")) {
try { notifyToolSuccess(ta.name); } catch { /* non-blocking */ }
}
// Annotate with verifier proof + epistemic tag prima di inviare al modello
const _tvr = verifyToolResult(ta.name, ta.args as Record<string, unknown>, result);
const _tannotated = annotateWithProof(ta.name, result, _tvr);
const _reactFixHint = _reactRecovery?.hadFix
? `\n💡 Fix noto (${_reactRecovery.count}x): ${_reactRecovery.fix}`
: (_reactRecovery?.hint ? `\n⚡ ${_reactRecovery.hint}` : "");
// Step2: epistemic tag su OBSERVATION; Step5: update world model
const _epTag2 = epistemicTag(ta.name);
loopMessages.push({ role: "user", content: `OBSERVATION [${ta.name}|${_epTag2}]:\n${_tannotated}${_reactFixHint}` } as ApiMsg);
updateWorldModel(ta.name, ta.args as Record<string, unknown>, step.result || result).catch(() => {});
}
// ── 4. Stagnation guard ────────────────────────────────────────────────────
{
const _c2StartIdx = Math.max(0, steps.length - textActions.length);
const _c2AllFailed = steps.slice(_c2StartIdx).every(s => s.status === "error");
consecutiveAllFailIters = (textActions.length > 0 && _c2AllFailed) ? consecutiveAllFailIters + 1 : 0;
if (consecutiveAllFailIters >= 3) {
onChunk("_(Stagnazione rilevata: " + consecutiveAllFailIters + " iterazioni consecutive con tutti i tool falliti. Interrompo il loop. Prova a riformulare il task o cambia provider.)_");
try { recordTaskOutcome(preCls?.type ?? "unknown", "failure"); } catch { /* non-blocking */ }
onRouteRecord(false);
writeSess(); // S205
return { action: "return", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter };
}
}
// ── P32-GE-1: Infra error graceful exit (ReAct path) ─────────────────────
// Scansiona le OBSERVATION messages recenti: ≥3 errori infra consecutivi
// della stessa categoria → exit onesto prima che il loop si inceppi.
{
const _c2ObsMsgs = (loopMessages as Array<{ role: string; content?: unknown }>)
.filter(m => m.role === "user" && typeof m.content === "string" &&
(m.content as string).startsWith("OBSERVATION"))
.slice(-9); // ultime 9 osservazioni ReAct
let _c2InfraCount = 0;
let _c2InfraCat: string | null = null;
for (let _i = _c2ObsMsgs.length - 1; _i >= 0; _i--) {
const _obs = _c2ObsMsgs[_i].content as string;
const _obsContent = _obs.slice(_obs.indexOf(":\n") + 3).trimStart();
if (!_obsContent.startsWith("❌")) break;
const _cat = infraCategory(_obsContent);
if (_cat === null) break;
if (_c2InfraCat === null) _c2InfraCat = _cat;
if (_cat !== _c2InfraCat) break;
_c2InfraCount++;
}
if (_c2InfraCount >= 3 && _c2InfraCat) {
const _infraMsgMap2: Record<string, string> = {
rate_limit: "Il provider è in rate limit (429) ripetuto",
timeout: "Timeout ripetuto dal provider o backend",
server_5xx: "Errore server 5xx ripetuto dal backend",
network_fail: "Errore di rete persistente",
};
const _infraLabel2 = _infraMsgMap2[_c2InfraCat] ?? "Errore infrastrutturale ripetuto";
onChunk(`_(${_infraLabel2} — rilevato ${_c2InfraCount}× di seguito. Il servizio è temporaneamente non disponibile. Riprova tra qualche minuto o cambia provider nelle impostazioni.)_`);
try { recordTaskOutcome(preCls?.type ?? "unknown", "failure"); } catch { /* non-blocking */ }
onRouteRecord(false);
writeSess();
return { action: "return", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter };
}
}
return { action: "continue", cleanRawText, consecutiveAllFailIters, effectiveMaxIter, sessStats, lastExecTool, lastExecOutput, lastExecResultIter };
}