Spaces:
Sleeping
Sleeping
| /** | |
| * 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 }; | |
| } | |