File size: 4,867 Bytes
95eb75a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
 * 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 };
}