/** * 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 { 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(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 }; }