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