File size: 10,487 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
 * doneSignalProcessor.ts — S496: extracted from agentLoop.ts
 *
 * S99-Cosa3: gestisce il segnale DONE: {summary} che il modello emette
 * quando si auto-dichiara completato.
 *
 * Comportamento:
 * 1. Se rawText NON contiene "DONE:" → action="skip" (nessun processing)
 * 2. Salva il summary nel worldState (fire-and-forget)
 * 3. Estrae finalText dal testo dopo DONE:
 * 4. Se formalGoal → verifica con verifyGoal:
 *    - Non soddisfatto + iter disponibili → inietta recovery → action="continue"
 *    - Soddisfatto → addDecision
 * 5. Streama finalText e segnala action="return" (caller esce dalla funzione)
 *
 * Muta steps e loopMessages direttamente (by reference).
 *
 * @module doneSignalProcessor
 */

import type { AgentStep } from "../storage";
import type { ApiMsg } from "./networkTools";
import type { GoalContract } from "../_goalVerifier";
import { verifyGoal, buildRecoveryPrompt } from "../_goalVerifier";
import { normalizeLlmText } from "../utils/normalizeLlmOutput";
import { validateGate as _doneVg } from "../agent/validationGate"; // P30-TG
import { recordErrorAsync } from "../selfLearningAsync"; // S649
import { addDecision } from "../projectMemory";
import { generateSuggestions, type Suggestion } from "../agent/SmartSuggestions";

export interface DoneSignalCtx {
  rawText:       string;
  formalGoal:    GoalContract | null;
  steps:         AgentStep[];
  iter:          number;
  MAX_ITER:      number;
  loopMessages:  ApiMsg[];
  onStatus:      (msg: string) => void;
  onSteps:       (steps: AgentStep[]) => void;
  signal?:       AbortSignal;
  lastUserMsg:   string;
  sessStats:     { verifyFails: number };
  emitFinalText: (text: string, signal?: AbortSignal) => void;
  onSuggestions: ((s: Suggestion[]) => void) | undefined;
  writeSess:     () => void;
  /** GAP-N3: auto-validate VFS project prima del DONE — opzionale, no-op se assente */
  executeToolGated?: (tool: string, args: Record<string, unknown>) => Promise<string>;
}

export type DoneSignalAction = "skip" | "continue" | "return";

export interface DoneSignalResult {
  action:            DoneSignalAction;
  finalText:         string;
  routeQualityScore: number | undefined;
}

export async function processDoneSignal(ctx: DoneSignalCtx): Promise<DoneSignalResult> {
  const {
    rawText, formalGoal, steps, iter, MAX_ITER,
    loopMessages, onStatus, onSteps, signal, lastUserMsg,
    sessStats, emitFinalText, onSuggestions, writeSess,
  } = ctx;

  // GAP-DONE-REGEX-MULTILINE: strip code blocks prima del match
  const _rawStripped = rawText.replace(/```[\s\S]*?```/g, '').replace(/<code>[\s\S]*?<\/code>/gi, '');
  const _doneMatch = _rawStripped.match(/^DONE:\s*(.+)$/m);
  if (!_doneMatch) return { action: "skip", finalText: rawText, routeQualityScore: undefined };

  const _doneSummary = _doneMatch[1].trim();

  // Fire-and-forget: salva summary nel worldState per continuità sessione
  (async () => {
    try {
      const { vfsDb: _vfsDbDone } = await import("../vfsDb");
      await _vfsDbDone.worldState.add({
        id:        Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
        type:      "build_ok" as const,
        path:      "",
        summary:   _doneSummary,
        timestamp: Date.now(),
        topic:     lastUserMsg.slice(0, 50),
      });
    } catch { /* non-blocking */ }
  })();

  // Estrai finalText: testo dopo la riga DONE: (strip think tags)
  let finalText = rawText
    .replace(/<think>[\s\S]*?<\/think>/gi, "")
    .replace(/<think>[\s\S]*/gi, "")
    .replace(/^DONE:.*$/m, "")
    .trim();
  if (!finalText) finalText = _doneSummary;
  finalText = normalizeLlmText(finalText) || finalText;

  let routeQualityScore: number | undefined;

  // S203+: VERIFY prima dello streaming — se fallisce e ci sono iter, riprendi
  if (formalGoal && !signal?.aborted) {
    try {
      const _vr = verifyGoal(formalGoal, steps, finalText, signal);
      routeQualityScore = _vr.score;

      steps.push(_vr.step);
      onSteps([...steps]);

      if (!_vr.satisfied && iter < MAX_ITER - 1) {  // S701: -2→-1
        sessStats.verifyFails++;
        // S204 GAP4: registra failure in selfLearning
        try {
          const _g4fix = _vr.failedDimensions.map(d => `[${d.name}] ${d.detail}`).join("; ");
          recordErrorAsync( // S649
            `VERIFY_FAIL: DONE_WHEN="${formalGoal.doneWhen.slice(0, 80)}" score=${Math.round(_vr.score * 100)}%`,
            `verify:goal="${formalGoal.goal.slice(0, 60)}"`,
            _g4fix,
          );
        } catch { /* non-blocking */ }

        onStatus(`Verifica fallita (${Math.round(_vr.score * 100)}%) — ripristino autonomo…`);
        const _recov = buildRecoveryPrompt(_vr, formalGoal);
        loopMessages.push({ role: "user", content: _recov } as ApiMsg);
        return { action: "continue", finalText, routeQualityScore };
      } else if (!_vr.satisfied && _vr.score < 0.35 && iter < MAX_ITER) {  // S701: hard block — score critico (<35%) → forza ultimo tentativo (GAP-VERIFY-S701-OVERLAP: else if)
        sessStats.verifyFails++;
        try {
          recordErrorAsync(
            `VERIFY_CRITICAL: score=${Math.round(_vr.score * 100)}% iter=${iter}/${MAX_ITER}`,
            `verify:goal="${formalGoal.goal.slice(0, 60)}"`,
            _vr.failedDimensions.map(d => `[${d.name}] ${d.detail}`).join('; '),
          );
        } catch { /* non-blocking */ }
        onStatus(`Score critico (${Math.round(_vr.score * 100)}%) — ultimo tentativo forzato…`);
        const _recovCrit = buildRecoveryPrompt(_vr, formalGoal);
        loopMessages.push({ role: 'user', content: `${_recovCrit}\n\n⚠️ SCORE CRITICO — ricostruire la risposta da zero.` } as ApiMsg);
        return { action: 'continue', finalText, routeQualityScore };
      }
      // S454: addDecision su PASS
      if (_vr.satisfied) {
        try {
          addDecision({
            type:     "BUG_RESOLVED",
            feature:  formalGoal.goal.slice(0, 40),
            decision: finalText.slice(0, 120),
            reason:   `GoalVerifier PASS score=${Math.round(_vr.score * 100)}%`,
            affects:  [],
          });
        } catch { /* non-blocking */ }

        // S302: Audit Engine (Profilo D) — double-check zona grigia [0.35, 0.82]
        // Previene false chiusure: score convincente ma non pieno → provider alternativo.
        // Fail-open: UNKNOWN non blocca mai il task.
        if (_vr.score >= 0.35 && _vr.score <= 0.82 && iter < MAX_ITER - 1 && !signal?.aborted) {
          try {
            const { runGoalAudit } = await import("./goalAuditEngine");
            onStatus("🔍 Audit indipendente (Profilo D)…");
            const _s302 = await runGoalAudit(
              formalGoal.goal, formalGoal.doneWhen, finalText, "groq",
            );
            if (_s302.verdict === "FAIL") {
              sessStats.verifyFails++;
              onStatus("⚠️ Audit Profilo D: risposta incompleta — mi autocorreggo…");
              loopMessages.push({
                role: "user",
                content: `[S302 AUDIT ENGINE — ${_s302.provider}]\n${_s302.reason} (score audit=${Math.round(_s302.score * 100)}%)\n\nRiprendi il task e completa quanto rilevato mancante prima di concludere.`,
              } as ApiMsg);
              return { action: "continue", finalText, routeQualityScore };
            }
          } catch { /* fail-open — non blocca mai il task */ }
        }

        // S766-MEM-RANK: feedback positivo sulle memorie iniettate (fire-and-forget)
        try {
          const { sendMemoryFeedback } = await import("./loopMemoryBlock");
          sendMemoryFeedback("PASS");
        } catch { /* non-blocking */ }
      }
    } catch { /* non-blocking — verifica mai interrompe il flusso */ }
  }

  // GAP-1: auto-validate + repair loop (max 2 iter) prima del DONE — never blocking
  if (ctx.executeToolGated && !signal?.aborted) {
    try {
      const { vfsDb: _vfsDbVal } = await import("../vfsDb");
      const _allFiles = await _vfsDbVal.files.toArray();
      const _hasCode = (_allFiles as Array<{ path: string }>).some(f => /\.(ts|tsx|py|js|jsx)$/.test(f.path));
      if (_hasCode) {
        const MAX_REPAIR_ITER = 2;
        // Conta i repair già tentati in questa sessione per evitare loop infiniti
        const _repairsDone = steps.filter(s => s.tool === "_repair_validator").length;
        if (_repairsDone < MAX_REPAIR_ITER) {
          onStatus(`Verifico il progetto… (${_repairsDone + 1}/${MAX_REPAIR_ITER})`);
          const _valResult = await ctx.executeToolGated("validate_project", {});
          if (_valResult && _valResult.startsWith("❌")) {
            // Traccia il tentativo di repair nello steps array
            steps.push({ tool: "_repair_validator", args: { iter: _repairsDone + 1 }, result: _valResult.slice(0, 200), status: "error" });
            onSteps([...steps]);
            if (_repairsDone + 1 < MAX_REPAIR_ITER && iter + 1 < MAX_ITER) {
              // Budget repair e iter disponibili → rilancia il loop per auto-riparazione
              onStatus("Trovati errori — auto-riparazione in corso…");
              loopMessages.push({ role: "user", content: [
                "Il progetto ha fallito la validazione automatica.",
                "Report errori:\n" + _valResult.slice(0, 600),
                "Fixa TUTTI gli errori usando write_file o apply_patch.",
                "Poi scrivi DONE quando il progetto è pulito.",
              ].join("\n") } as ApiMsg);
              return { action: "continue", finalText, routeQualityScore };
            } else {
              // Budget esaurito → appende il report come fallback informativo
              finalText = finalText + "\n\n---\n⚠️ **Validazione automatica:**\n" + _valResult.slice(2).trim().slice(0, 500);
            }
          }
        }
      }
    } catch { /* non-blocking — la validazione non blocca mai il completamento */ }
  }

  // Goal soddisfatto (o iter esaurite) → stream + return
  onSteps([...steps]);
  // P30-TG: validateGate normalizza markdown + ripara code blocks prima dell'emissione.
  try { const _g = _doneVg(finalText, { taskHint: lastUserMsg.slice(0, 80), minQuality: 0 })  } catch { /* non-blocking */ }
  emitFinalText(finalText, signal);
  try { onSuggestions?.(generateSuggestions(lastUserMsg, steps)); } catch { /* non-blocking */ }
  writeSess();
  return { action: "return", finalText, routeQualityScore };
}