AUDIT / src /lib /agentLoop /case1Block.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
18.3 kB
/**
* case1Block.ts — S503: extracted from agentLoop.ts
*
* Gestisce il Case 1 completo: native tool calls (Groq, OpenAI, OpenRouter).
* Pipeline:
* 1. Sintetizza formalGoal a iter 0 se null
* 2. Registra gli step come "running"
* 3. Esegue i tool in parallelo (max 4, pLimitPool)
* 4. Processa i risultati: annotation, world model, error recovery, live debug fix
* 5. ReasoningCore + belief state persistence
* 6. Delega a case1TailBlock (stagnation → return, DONE → return-success, continue)
*
* Muta steps e loopMessages per reference.
* Restituisce lo stato aggiornato dei campi mutabili del loop.
*
* @module case1Block
*/
import type { AgentStep } from "../storage";
import type { GoalContract } from "../_goalVerifier";
import { makePlanStep } from "../_goalVerifier";
import { TOOL_STATUS_LABELS, narrativeLabel } from "./toolStatusLabels"; // S766
import { _parseToolArgs, _tryLiveDebugFix } from "./toolExecutor";
import type { Suggestion } from "../agent/SmartSuggestions";
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 } from "../beliefRevision";
import { EXEC_REINJECT_TOOLS } from "./toolGate";
import { buildExecReinjectMessage } from "./execLoopGate";
import { processCase1Tail } from "./case1TailBlock";
import { infraCategory } from "./stallDetector"; // P32-GE-1
import { buildErrorSignature } from "./errorSignatureBuilder"; // S700
import { selfHealingEngine } from "./selfHealingEngine"; // S700
import type { ToolCallRaw, ApiMsg } from "./networkTools";
// ─── Interfaces ────────────────────────────────────────────────────────────────
export interface Case1BlockCtx {
toolCalls: ToolCallRaw[];
rawText: string | null;
iter: number;
MAX_ITER: number;
// Mutable state (returned updated)
formalGoal: GoalContract | null;
steps: AgentStep[];
loopMessages: ApiMsg[];
consecutiveAllFailIters: number;
effectiveMaxIter: number;
sessStats: { autoFixed: number; propagated: number; verifyFails: number };
lastExecTool: string;
lastExecOutput: string;
lastExecResultIter: number;
// Read-only context
signal?: AbortSignal;
lastUserMsg: string;
loopTaskId: string | null;
isCodeTask: boolean;
// Callbacks
onStatus: (msg: string) => void;
onSteps: (steps: AgentStep[]) => void;
onChunk: (chunk: string) => void;
onSuggestions?: (s: Suggestion[]) => void;
// Closures from outer scope
executeToolGated: (name: string, args: Record<string, unknown>) => Promise<string>;
emitFinalText: (text: string, signal?: AbortSignal) => void;
writeSess: () => void;
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;
confSession: { recordSignal: (s: string) => void };
onReasoningUpdate: (u: { last_result?: string; error?: string; completed_step?: string }) => void;
}
export type Case1BlockAction = "continue" | "return" | "return-success";
export interface Case1BlockResult {
action: Case1BlockAction;
formalGoal: GoalContract | null;
consecutiveAllFailIters: number;
effectiveMaxIter: number;
sessStats: { autoFixed: number; propagated: number; verifyFails: number };
lastExecTool: string;
lastExecOutput: string;
lastExecResultIter: number;
}
// ─── Internal helpers ──────────────────────────────────────────────────────────
/** S274: pool limitato a 4 paralleli — evita saturazione banda su mobile */
async function pLimitPool<T>(tasks: (() => Promise<T>)[], limit = 4): Promise<T[]> {
const out: T[] = new Array(tasks.length);
let i = 0;
async function worker() {
while (i < tasks.length) { const idx = i++; out[idx] = await tasks[idx](); }
}
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
return out;
}
// ─── Main export ───────────────────────────────────────────────────────────────
/** Processa il Case 1 completo (native tool calls). Sempre seguito da continue o return nel caller. */
export async function processCase1Block(ctx: Case1BlockCtx): Promise<Case1BlockResult> {
const {
toolCalls, rawText, iter, MAX_ITER, signal, lastUserMsg, loopTaskId, isCodeTask,
onStatus, onSteps, onChunk, onSuggestions, executeToolGated, emitFinalText, writeSess,
epistemicTag, cbIsOpen, cbUpdate, cbSkipMsg, stallCheck, confSession, onReasoningUpdate,
} = ctx;
// Local mutable copies — returned at end
let formalGoal = ctx.formalGoal;
const steps = ctx.steps; // mutated by reference
const loopMessages = ctx.loopMessages; // mutated by reference
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. Sintetizza formalGoal a iter 0 ─────────────────────────────────────
// Step1/S104: Case 1 non produce blocchi GOAL/DONE_WHEN — sintetizza da lastUserMsg (parity con Case 2)
if (iter === 0 && formalGoal === null) {
formalGoal = {
goal: lastUserMsg.slice(0, 200), // S610: 120→200
doneWhen: lastUserMsg.slice(0, 200), // S610: 120→200
outOfScope: "",
};
// S203: emetti __plan__ step per rendere il contratto visibile in UI
try {
const _planStep = makePlanStep(formalGoal);
steps.unshift(_planStep);
onSteps([...steps]);
} catch { /* non-blocking */ }
}
loopMessages.push({ role: "assistant", content: rawText ?? null, tool_calls: toolCalls } as ApiMsg);
const toolNames = toolCalls.map(tc => TOOL_STATUS_LABELS[tc.function.name] ?? tc.function.name);
onStatus(toolNames.length > 1 ? `${toolNames.length} tool in parallelo…` : (toolNames[0] ?? "Tool…"));
// ── 2. Registra tutti gli step come "running" ──────────────────────────────
const newStepIndices: number[] = [];
for (const tc of toolCalls) {
const _parsed0 = _parseToolArgs(tc.function.arguments); // S203: validated args
// S766: context-aware label — mostra query/path/comando specifico invece del tipo tool generico
steps.push({ tool: tc.function.name, args: _parsed0.args, result: "", status: "running", explanation: narrativeLabel(tc.function.name, _parsed0.args) });
newStepIndices.push(steps.length - 1);
}
onSteps([...steps]);
// ── 3. Esecuzione parallela (max 4) con CB + stall detection ──────────────
const results = await pLimitPool(
toolCalls.map((tc) => async () => {
// S203: Validate + repair LLM-generated args before ANY execution
const _pv = _parseToolArgs(tc.function.arguments);
const args: Record<string, unknown> = _pv.args;
if (!_pv.valid) {
const _errMsg = `❌ [ARGS_INVALID] "${tc.function.name}" non eseguito: argomenti JSON non riparabili. ${_pv.error ?? ""} Rigenera la chiamata con JSON valido.`;
console.warn("[S203] tool args JSON non riparabile", tc.function.name, tc.function.arguments);
return { tc, result: _errMsg, durationMs: 0, args };
}
if (_pv.repaired) {
console.log("[S203] tool args riparati da repairJson", tc.function.name); // check-console-log: ok
}
// Circuit breaker: skip tripped tools
if (cbIsOpen(tc.function.name, iter)) {
onStatus(`⚡ ${tc.function.name} saltato (circuit open)`);
return { tc, result: cbSkipMsg(tc.function.name), durationMs: 0, args };
}
// Gap B (S122): stall detection — abort repeated identical calls
const _c1StallN = stallCheck(tc.function.name, args);
if (_c1StallN >= 3) {
// S444: hint cross-sessione per messaggio stallo
const _c1ErrHint = (() => {
try {
const _wf = errorLog.resolvedWithFix().find(e => e.context.includes(tc.function.name));
return _wf ? `\n💡 Fix noto (${_wf.count}× visto): ${_wf.fix!.slice(0, 200)}` : "";
} catch { return ""; }
})();
return { tc, result: `❌ Stallo rilevato — "${tc.function.name}" chiamato ${_c1StallN}× con gli stessi argomenti. Cambia approccio.${_c1ErrHint}`, durationMs: 0, args };
}
const start = Date.now();
const result = await executeToolGated(tc.function.name, args);
cbUpdate(tc.function.name, result, iter);
return { tc, result, durationMs: Date.now() - start, args };
}),
);
// ── 4. Processa i risultati ────────────────────────────────────────────────
let anyError = false;
const _failureRecoveries: Array<{ toolName: string; result: ReturnType<typeof recordAndSuggestFix> }> = [];
for (let i = 0; i < results.length; i++) {
const { tc, result, durationMs, args } = results[i];
const si = newStepIndices[i];
const _vr = verifyToolResult(tc.function.name, args, result);
const _annotated = annotateWithProof(tc.function.name, result, _vr);
steps[si].result = result;
steps[si].status = result.startsWith("❌") ? "error" : "done";
steps[si].durationMs = durationMs;
// Step2: push annotated result con epistemic tag al modello
const _epTag1 = epistemicTag(tc.function.name);
const _taggedAnnotated = `[EPISTEMIC:${_epTag1}] ${_annotated}`;
loopMessages.push({ role: "tool", content: _taggedAnnotated, tool_call_id: tc.id, name: tc.function.name } as ApiMsg);
// Step5: update world model (fire-and-forget)
updateWorldModel(tc.function.name, args, result).catch(() => {});
if (result.startsWith("❌")) {
anyError = true;
try { confSession.recordSignal("tool_failure"); } catch { /* non-blocking */ }
// S444: registra su errorLog per hint cross-sessione
try { errorLog.record(result.slice(0, 300), `tool:${tc.function.name}`, undefined, typeof args.code === "string" ? args.code.slice(0, 500) : undefined); } catch { /* non-blocking */ }
// S204 GAP1: deriva fix dall'alternativa nota prima di registrare l'errore
const _g1fix = (() => {
try { const _a = selfLearning.getAlternative(tc.function.name, result.slice(0, 150)); return _a ? `Usa ${_a.tool}: ${_a.hint.slice(0, 200)}` : ""; } catch { return ""; } // S610: 80→150
})();
recordErrorAsync(result, `tool:${tc.function.name}`, _g1fix); // S649
try { _failureRecoveries.push({ toolName: tc.function.name, result: recordAndSuggestFix({ toolName: tc.function.name, errorMsg: result, args }) }); } catch { /* non-blocking */ }
// S59: live debug-fix per code execution tools
let _liveFixed: string | null = null;
// S59 original: EXEC_TOOLS → debugLoop + selfHealingEngine (code)
if (!_liveFixed) {
_liveFixed = await _tryLiveDebugFix(tc.function.name, args, result, `iter${iter}`, onStatus);
}
// S697: selfHealingEngine for ALL tool types (transient/structural errors)
if (!_liveFixed) {
const _s697sig = buildErrorSignature(result, tc.function.name, args, iter, 0);
const _hasQuickStrategy =
_s697sig.category === 'not_found' ||
_s697sig.category === 'timeout' ||
_s697sig.category === 'rate_limit' ||
_s697sig.category === 'cors' ||
_s697sig.category === 'network_fail' ||
_s697sig.isTransient;
if (_hasQuickStrategy) {
const _s697healed = await selfHealingEngine.run({
tool: tc.function.name, args, errorResult: result,
taskId: loopTaskId ?? `iter${iter}`,
iteration: iter, onStatus, signal,
executeToolFn: executeToolGated,
});
if (_s697healed.healed) _liveFixed = _s697healed.result;
}
}
if (_liveFixed) {
// S204+: registra fix immediatamente — il modello riceverà il risultato corretto, non l'errore
try {
recordErrorAsync(result.slice(0, 100), `livefix:${tc.function.name}`, _liveFixed.slice(0, 250)); // S649
} catch { /* non-blocking */ }
// S444: marca errorLog come risolto con fix applicato (cross-sessione)
try { errorLog.markFixed(errorLog.hashOf(result.slice(0, 300)), _liveFixed.slice(0, 500)); } catch { /* non-blocking */ }
steps[si].result = _liveFixed; steps[si].status = "done";
loopMessages[loopMessages.length - 1] = { role: "tool", content: _liveFixed, tool_call_id: tc.id, name: tc.function.name } as ApiMsg;
anyError = false;
sessStats.autoFixed++; // S205: error resolved autonomously
// Fix 8 (S421): extra iteration quando l'agente è in modalità repair intensivo
if (sessStats.autoFixed >= 2 && effectiveMaxIter < MAX_ITER) {
effectiveMaxIter = Math.min(effectiveMaxIter + 1, MAX_ITER);
}
} else {
sessStats.propagated++; // S205: error reaches model unchanged
}
} else {
try { notifyToolSuccess(tc.function.name); } catch { /* non-blocking */ }
try { confSession.recordSignal("tool_success"); } catch { /* non-blocking */ }
}
// S83: track exec result for loop closure (Case 1)
if (EXEC_REINJECT_TOOLS.has(tc.function.name)) {
lastExecResultIter = iter;
lastExecOutput = result.slice(0, 1000);
lastExecTool = tc.function.name;
loopMessages.push({ role: "user", content: buildExecReinjectMessage(lastExecTool, lastExecOutput) } as ApiMsg);
}
}
onSteps([...steps]);
// ── P32-GE-1: Infra error graceful exit ─────────────────────────────────────
// Scansiona le ultime tool-result messages per rilevare ≥3 errori consecutivi
// della stessa categoria infrastrutturale → exit onesto invece di loop infinito.
{
const _c1ToolMsgs = (loopMessages as Array<{ role: string; content?: unknown }>)
.filter(m => m.role === "tool" && typeof m.content === "string")
.slice(-9); // ultime 9 risposte (max 3 iter × 3 tool)
let _c1InfraCount = 0;
let _c1InfraCat: string | null = null;
for (let _i = _c1ToolMsgs.length - 1; _i >= 0; _i--) {
const _cnt = _c1ToolMsgs[_i].content as string;
// rimuovi prefix epistemic tag [EPISTEMIC:...] per l'analisi
const _raw = _cnt.replace(/^[EPISTEMIC:[^]]+]s*/, "");
if (!_raw.startsWith("❌")) break;
const _cat = infraCategory(_raw);
if (_cat === null) break;
if (_c1InfraCat === null) _c1InfraCat = _cat;
if (_cat !== _c1InfraCat) break;
_c1InfraCount++;
}
if (_c1InfraCount >= 3 && _c1InfraCat) {
const _infraMsgMap: 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 _infraLabel = _infraMsgMap[_c1InfraCat] ?? "Errore infrastrutturale ripetuto";
onChunk(`_(${_infraLabel} — rilevato ${_c1InfraCount}× di seguito. Il servizio è temporaneamente non disponibile. Riprova tra qualche minuto o cambia provider nelle impostazioni.)_`);
writeSess();
return {
action: "return",
formalGoal,
consecutiveAllFailIters,
effectiveMaxIter,
sessStats,
lastExecTool,
lastExecOutput,
lastExecResultIter,
};
}
}
// ── 5. ReasoningCore update + belief state persistence ────────────────────
onReasoningUpdate({
last_result: results.filter(r => !r.result.startsWith("❌")).map(r => r.tc.function.name + "✓").join(" ").slice(0, 200) || undefined,
error: anyError ? results.filter(r => r.result.startsWith("❌")).map(r => r.tc.function.name).join(", ") : undefined,
completed_step: results.filter(r => !r.result.startsWith("❌")).map(r => r.tc.function.name).join(", ") || undefined,
});
// S351: persist belief state per batch error Case 1 (non-blocking)
if (anyError) {
for (const { result: _brr } of results) {
if (_brr.startsWith("❌")) { try { persistBeliefState(loopTaskId ?? "", lastUserMsg.slice(0, 40)); } catch { /* non-blocking */ } break; }
}
}
// ── 6. Case 1 tail (stagnation + belief revision + DONE check + synthesis) ─
// S496: → agentLoop/case1TailBlock.ts
const _c1t = processCase1Tail({
results, failureRecoveries: _failureRecoveries, anyError,
consecutiveAllFailIters, effectiveMaxIter,
steps, loopMessages, iter, isCodeTask,
rawText: rawText ?? null, signal, lastUserMsg,
loopTaskId, onChunk, onSteps, onStatus,
onSuggestions,
emitFinalText, writeSess,
});
consecutiveAllFailIters = _c1t.newConsecutiveAllFailIters;
effectiveMaxIter = _c1t.newEffectiveMaxIter;
return {
action: _c1t.action === "return" ? "return"
: _c1t.action === "return-success" ? "return-success"
: "continue",
formalGoal,
consecutiveAllFailIters,
effectiveMaxIter,
sessStats,
lastExecTool,
lastExecOutput,
lastExecResultIter,
};
}