Spaces:
Sleeping
Sleeping
| /** | |
| * exhaustionHandler.ts — S502: extracted from agentLoop.ts | |
| * | |
| * Gestisce i due casi di esaurimento del loop: | |
| * 1. handleEmptyOrNoText — risposta vuota (no rawText) con o senza steps precedenti | |
| * 2. handleMaxIterReached — iterazioni massime raggiunte (post-loop) | |
| * | |
| * Entrambe le funzioni sono terminali: il caller deve sempre fare return dopo. | |
| * | |
| * @module exhaustionHandler | |
| */ | |
| import type { AgentStep } from "../storage"; | |
| import type { StreamOptions } from "../types"; | |
| import type { GoalContract } from "../_goalVerifier"; | |
| import type { TaskType } from "../taskClassifier"; | |
| import { verifyGoal } from "../_goalVerifier"; | |
| import { recordErrorAsync } from "../selfLearningAsync"; // S649 | |
| import { streamFallback, truncateMessages } from "./streamHelpers"; | |
| import { recordOutcome as recordTaskOutcome } from "../agent/SuccessRateMonitor"; | |
| import { liveGuidanceStore } from "@/lib/liveGuidanceStore"; | |
| import type { ApiMsg } from "./networkTools"; | |
| // GAP-TRUTH: emitFinalText (gate fail-closed) e utility per streaming incrementale | |
| import { buildClaimWarningBanner } from "./emitFinalText"; | |
| // ─── handleEmptyOrNoText ────────────────────────────────────────────────────── | |
| export interface EmptyOrNoTextCtx { | |
| hasSteps: boolean; | |
| emptyCount: number; | |
| steps: AgentStep[]; | |
| loopMessages: ApiMsg[]; | |
| initialMessages: Array<{ role: string; content: string }>; | |
| iter: number; | |
| preCls: { type: string } | null; | |
| token: string; | |
| options: StreamOptions; | |
| signal?: AbortSignal; | |
| onChunk: (chunk: string) => void; | |
| /** GAP-TRUTH: unico chokepoint pre-UI — usato per testo finale (non per status/marker) */ | |
| emitFinalText: (text: string, signal?: AbortSignal) => void; | |
| onStatus: (msg: string) => void; | |
| writeSess: () => void; | |
| cleanupSseBridge: () => void; | |
| onReasoningUpdate: (update: { error: string }) => void; | |
| recordRouteFailure?: () => void; | |
| } | |
| /** | |
| * Gestisce risposta vuota (no rawText). Sempre terminale. | |
| */ | |
| export async function handleEmptyOrNoText(ctx: EmptyOrNoTextCtx): Promise<void> { | |
| const { | |
| hasSteps, emptyCount, steps, loopMessages, initialMessages, iter, | |
| preCls: _preCls, token, options, signal, | |
| onChunk, emitFinalText, onStatus: _onStatus, writeSess: _writeSess, cleanupSseBridge: _cleanupSseBridge, | |
| onReasoningUpdate, recordRouteFailure, | |
| } = ctx; | |
| if (hasSteps) { | |
| const newEmptyCount = emptyCount + 1; | |
| onReasoningUpdate({ error: `iter ${iter}: risposta vuota` }); | |
| if (newEmptyCount >= 2) { | |
| // Seconda risposta vuota consecutiva → errore definitivo | |
| const _doneTools = steps.filter(s => s.status === "done").map(s => s.tool); | |
| const _progressNote = _doneTools.length > 0 ? `Ho eseguito: ${_doneTools.join(", ")}. ` : ""; | |
| // GAP-TRUTH: usa emitFinalText invece di onChunk diretto — gate + proof check | |
| emitFinalText(`_(${_progressNote}Il modello non ha generato una risposta — a volte capita con richieste complesse o provider sotto carico. Riprova tra un momento o riformula la domanda.)_`, signal); | |
| try { recordTaskOutcome(_preCls!.type as TaskType, "failure"); } catch { /* non-blocking */ } | |
| try { recordRouteFailure?.(); } catch { /* non-blocking */ } | |
| _writeSess(); | |
| return; | |
| } | |
| // Prima risposta vuota: inietta richiesta di sintesi con contesto degli step | |
| const _doneSteps = steps.filter(s => s.status === "done"); | |
| const _stepSummary = _doneSteps.length > 0 | |
| ? `Ho già eseguito: ${_doneSteps.map(s => s.tool + "→" + s.result.slice(0, 60).replace(/\n/g, " ")).join(" | ")}.` | |
| : ""; | |
| const sumMsgs: ApiMsg[] = [ | |
| ...truncateMessages(loopMessages), | |
| { role: "user", content: `${_stepSummary} Riassumi in italiano i risultati trovati. Sii diretto e conciso.` } as ApiMsg, | |
| ]; | |
| // GAP-TRUTH: accumula il testo streamato per il claim check post-stream | |
| let _accumulated1 = ""; | |
| const _gatedChunk1 = (c: string) => { _accumulated1 += c; onChunk(c); }; | |
| await streamFallback( | |
| token || "", sumMsgs as Array<{ role: string; content: string }>, _gatedChunk1, signal, { ...options, maxTokens: 2048 }, | |
| ); | |
| const _banner1 = buildClaimWarningBanner(_accumulated1, steps); | |
| if (_banner1) onChunk(_banner1); | |
| } else { | |
| // Nessuno step eseguito, testo vuoto → fallback diretto (no claim risk, no steps) | |
| await streamFallback(token || "", initialMessages, onChunk, signal, options); | |
| } | |
| // Common tail — S499-FIX: path emptyCount=1 + no-steps convergono qui | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "success"); } catch { /* non-blocking */ } | |
| _writeSess(); | |
| liveGuidanceStore.clear(); | |
| _cleanupSseBridge(); signal?.removeEventListener("abort", _cleanupSseBridge); | |
| } | |
| // ─── handleMaxIterReached ───────────────────────────────────────────────────── | |
| export interface MaxIterCtx { | |
| formalGoal: GoalContract | null; | |
| steps: AgentStep[]; | |
| signal?: AbortSignal; | |
| loopMessages: ApiMsg[]; | |
| options: StreamOptions; | |
| preCls: { type: string } | null; | |
| token: string; | |
| onStatus: (msg: string) => void; | |
| onSteps: (steps: AgentStep[]) => void; | |
| onChunk: (chunk: string) => void; | |
| /** GAP-TRUTH: unico chokepoint pre-UI — usato per testo finale (non per status/marker) */ | |
| emitFinalText: (text: string, signal?: AbortSignal) => void; | |
| writeSess: () => void; | |
| cleanupSseBridge: () => void; | |
| recordRouteFailure?: () => void; | |
| } | |
| /** | |
| * Gestisce iterazioni massime raggiunte (post-loop). Sempre terminale. | |
| */ | |
| export async function handleMaxIterReached(ctx: MaxIterCtx): Promise<void> { | |
| const { | |
| formalGoal, steps, signal, loopMessages, options, | |
| preCls: _preCls, token, | |
| onStatus, onSteps, onChunk, | |
| writeSess: _writeSess, cleanupSseBridge: _cleanupSseBridge, | |
| recordRouteFailure, | |
| } = ctx; | |
| onStatus("Ho raggiunto il limite — sintetizzo quello che ho trovato…"); | |
| // S455-P6: verifyGoal anche su iter cap — logga failure per selfLearning, NON blocca | |
| if (formalGoal && steps.length > 0 && !signal?.aborted) { | |
| try { | |
| const _vrCap = verifyGoal(formalGoal, steps, "", signal); | |
| steps.push(_vrCap.step); | |
| onSteps([...steps]); | |
| if (!_vrCap.satisfied) { | |
| try { | |
| recordErrorAsync( // S649 | |
| `ITER_CAP_VERIFY_FAIL: score=${Math.round(_vrCap.score * 100)}%`, | |
| `verify:goal="${formalGoal.goal.slice(0, 60)}"`, | |
| _vrCap.failedDimensions | |
| .map((d: { name: string; detail: string }) => `[${d.name}] ${d.detail}`) | |
| .join("; "), | |
| ); | |
| } catch { /* non-blocking */ } | |
| } | |
| } catch { /* non-blocking — verifica mai interrompe il flusso */ } | |
| } | |
| const _goodSteps = steps.filter((s: AgentStep) => !s.result.startsWith("❌") && s.result.length > 30); | |
| const _stepsSummary = _goodSteps.length > 0 | |
| ? `\n\nDATI RACCOLTI DAI TOOL:\n${_goodSteps.map((s: AgentStep) => `[${s.tool}]: ${s.result.slice(0, 400)}`).join("\n\n---\n\n")}` | |
| : ""; | |
| const sumMsgs: ApiMsg[] = [ | |
| ...truncateMessages(loopMessages), | |
| { role: "user", content: `Raggiunto limite iterazioni. Riassumi brevemente tutto ciò che hai fatto e trovato.${_stepsSummary}` } as ApiMsg, | |
| ]; | |
| // GAP-TRUTH: accumula testo per claim check — LLM potrebbe dichiarare azioni | |
| // completate nel riepilogo iter-cap che non ha mai realmente eseguito. | |
| let _accumulated2 = ""; | |
| const _gatedChunk2 = (c: string) => { _accumulated2 += c; onChunk(c); }; | |
| await streamFallback( | |
| token || "", sumMsgs as Array<{ role: string; content: string }>, _gatedChunk2, signal, { ...options, maxTokens: 2048 }, | |
| ); | |
| const _banner2 = buildClaimWarningBanner(_accumulated2, steps); | |
| if (_banner2) onChunk(_banner2); | |
| try { recordTaskOutcome((_preCls?.type ?? "unknown") as TaskType, "failure"); } catch { /* non-blocking */ } | |
| try { recordRouteFailure?.(); } catch { /* non-blocking */ } | |
| _writeSess(); | |
| liveGuidanceStore.clear(); | |
| _cleanupSseBridge(); signal?.removeEventListener("abort", _cleanupSseBridge); | |
| } | |