AUDIT / src /lib /agentLoop /dynamicReplanBlock.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
4.87 kB
/**
* dynamicReplanBlock.ts — Dynamic replan + iterGate expansion (estratto da agentLoop.ts — P3-6)
*
* Gestisce due logiche mid-loop strettamente correlate:
* 1. Budget expansion (checkIterBudgetExpansion): espande _effectiveMaxIter se il loop è bloccato
* 2. Dynamic replan: se ≥2 errori recenti → pianifica nuovo approccio via planTaskJit
*
* Invarianti (da ARCHITECTURE_INVARIANTS.md):
* - L3: decideRetry non può triggerare su ultima iterazione — questo blocco non chiama decideRetry
* - L4: stallo (stesso tool ≥3 volte) → il _shouldReplan lo interrompe cambiando approccio
*/
import { checkIterBudgetExpansion } from "./iterGateBlock";
import { planTaskJit } from "./jitPlanner";
import type { ApiMsg } from "./networkTools";
import type { AgentStep } from "../storage";
import type { ExecutionPlan } from "../executionPlanner"; // BUG-D1-FIX: proper type (was any)
export interface DynamicReplanParams {
iter: number;
effectiveMaxIter: number;
replanCount: number;
maxReplans: number;
recentToolCalls: Array<{ name: string; argsHash: string }>;
recentTexts: string[];
steps: AgentStep[];
loopMessages: ApiMsg[];
lastUserMsg: string;
hoistedEpPlan: ExecutionPlan | null; // BUG-D1-FIX: proper type
onStatus: (msg: string) => void;
reasoning: { setMaxLoops: (n: number) => void };
}
export interface DynamicReplanResult {
loopMessages: ApiMsg[];
replanCount: number;
effectiveMaxIter: number;
hoistedEpPlan: ExecutionPlan | null;
}
export async function runDynamicReplanBlock(p: DynamicReplanParams): Promise<DynamicReplanResult> {
let loopMessages = p.loopMessages;
let replanCount = p.replanCount;
let effectiveMaxIter = p.effectiveMaxIter;
let hoistedEpPlan: ExecutionPlan | null = p.hoistedEpPlan;
const _recentErrors = p.steps.slice(-4).filter(s => s.status === "error").length;
const _ibe = checkIterBudgetExpansion({
iter: p.iter,
effectiveMaxIter,
recentToolCalls: p.recentToolCalls,
recentTexts: p.recentTexts,
recentErrorCount: _recentErrors,
alreadyReplanned: replanCount > 0,
onStatus: p.onStatus,
});
// GAP-B v2: early trigger se ≥2 errori recenti, indipendente dal budget residuo
const _earlyReplanNeeded = _recentErrors >= 2 && p.iter > 1;
const _shouldReplan = (_ibe.triggerReplan || _earlyReplanNeeded) && replanCount < p.maxReplans;
if (_shouldReplan) {
replanCount++;
try {
const _failedTools = p.steps.slice(-4)
.filter(s => s.status === "error")
.map(s => s.tool)
.join(", ");
// Usa planTaskJit (JIT planner, 800ms timeout) — più rapido, funziona senza _hoistedEpPlan
const _replanGoal = p.lastUserMsg + ` [EVITA: ${_failedTools} — usa approccio alternativo]`;
// BUG-D3-FIX: outer timeout guard (planTaskJit internal 800ms + network slack)
const _newLightPlan = await Promise.race([
planTaskJit(_replanGoal, loopMessages.slice(-4) as Array<{role:string;content:string}>),
new Promise<null>(res => { setTimeout(() => res(null), 1500); }),
]);
if (!_newLightPlan) throw new Error("planTaskJit returned null or timed out");
const _newStepsText = _newLightPlan.subtasks.map((s: { description: string }) => s.description).join(" → ");
loopMessages = [
...loopMessages,
{
role: "user" as const,
content: `[REPLAN DINAMICO #${replanCount}${_recentErrors} errori su: ${_failedTools}]\nNuova strategia: ${_newStepsText}.\nCambia approccio — non ripetere i tool falliti.`,
},
] as ApiMsg[];
p.onStatus(`🔄 Replan #${replanCount} — cambio strategia…`);
// Se esiste anche il piano architettura, aggiorna anche quello
if (hoistedEpPlan) {
const _hep = hoistedEpPlan;
const _done = ((_hep.steps ?? []) as { completed?: boolean }[]).filter(s => !!s.completed);
const _newSteps = _newLightPlan.subtasks.map((s: { description: string; tool?: string }) => ({
description: s.description, filesToCreate: [] as string[], tool: s.tool ?? "", completed: false,
}));
_hep.steps = [..._done, ..._newSteps] as any;
}
} catch { /* replan non critico — il loop continua invariato */ }
}
if (_ibe.expanded) {
effectiveMaxIter = _ibe.newMaxIter;
p.reasoning.setMaxLoops(_ibe.newMaxIter); // S759: sync ReasoningCore — senza questo isExhausted() esce al vecchio limite
if (_ibe.strategyHint) {
loopMessages = [
...loopMessages,
{ role: "system" as const, content: `[STRATEGIA AGGIORNATA — iter ${p.iter + 1}] ${_ibe.strategyHint}` },
] as ApiMsg[];
}
}
return { loopMessages, replanCount, effectiveMaxIter, hoistedEpPlan };
}