Spaces:
Sleeping
Sleeping
| /** | |
| * efficiencyCritic.ts — V7-4: LLM sanity check prima di azioni ad alto impatto. | |
| * | |
| * Eseguito su git_push / git_push_multiple / deploy_trigger / execute_shell | |
| * DOPO preExecCritic (statico) e PRIMA dell'esecuzione reale del tool. | |
| * | |
| * Caratteristiche: | |
| * - max 100 token output, temperatura 0 (deterministico) | |
| * - timeout 4s hard (non blocca mai il loop) | |
| * - cooldown 30s (protegge il free tier Groq/Cerebras) | |
| * - non-blocking: su timeout/errore → proceed:true (fail-open) | |
| * | |
| * Fix V7-4: usa callWithFallback(hfToken, payload, signal) — non-streaming — | |
| * e parsa la risposta come JSON dalla Response. Il precedente pattern streaming | |
| * (msgs, onChunk, opts) era errato: falliva silenziosamente → catch → skipped:true. | |
| */ | |
| import { callWithFallback, getHFToken } from "@/lib/providerChain"; | |
| import type { ChatMessage } from "@/lib/types"; | |
| // Tool soggetti al check — irreversibili e ad alto impatto | |
| const _CRITIC_TOOLS = new Set([ | |
| "git_push", | |
| "git_push_multiple", | |
| "deploy_trigger", | |
| "execute_shell", | |
| ]); | |
| const _COOLDOWN_MS = 30_000; // 30s — protegge il free tier | |
| const _TIMEOUT_MS = 4_000; // 4s — safe anche su cold-start Railway | |
| const _MAX_TOKENS = 100; // budget minimo | |
| let _lastFiredAt = -Infinity; | |
| export interface EfficiencyCriticResult { | |
| proceed: boolean; | |
| reason: string; | |
| skipped: boolean; // true = cooldown/timeout → allow by default (fail-open) | |
| } | |
| /** Ritorna true se il tool è soggetto all'efficiency critic. */ | |
| export function isEfficiencyCriticTool(name: string): boolean { | |
| return _CRITIC_TOOLS.has(name); | |
| } | |
| /** | |
| * Esegue il check LLM leggero (callWithFallback non-streaming). | |
| * | |
| * @param name - Nome del tool | |
| * @param args - Argomenti del tool | |
| * @param goal - Messaggio utente / obiettivo corrente (lastUserMsg) | |
| * @param recentSteps - Stringa compatta degli step recenti (ultimi 3) | |
| */ | |
| export async function runEfficiencyCritic( | |
| name: string, | |
| args: Record<string, unknown>, | |
| goal: string, | |
| recentSteps: string, | |
| ): Promise<EfficiencyCriticResult> { | |
| // Cooldown guard — evita over-firing sul free tier | |
| if (Date.now() - _lastFiredAt < _COOLDOWN_MS) { | |
| return { proceed: true, reason: "cooldown", skipped: true }; | |
| } | |
| _lastFiredAt = Date.now(); | |
| const _argsPreview = JSON.stringify(args).slice(0, 200); | |
| const _msgs: ChatMessage[] = [ | |
| { | |
| role: "system", | |
| content: | |
| "Sei un critic che valuta se un'azione ad alto impatto è necessaria ora. " + | |
| 'Rispondi SOLO con JSON valido: {"ok":true} o {"ok":false,"reason":"..."}. ' + | |
| "La reason deve essere max 15 parole. Nessun altro testo.", | |
| }, | |
| { | |
| role: "user", | |
| content: | |
| `Obiettivo: "${goal.slice(0, 150)}"\n` + | |
| `Step recenti: ${recentSteps.slice(0, 300) || "nessuno"}\n\n` + | |
| `Tool da eseguire: ${name}\n` + | |
| `Args: ${_argsPreview}\n\n` + | |
| "Questa azione è necessaria e corretta adesso per raggiungere l'obiettivo?", | |
| }, | |
| ]; | |
| // iOS-safe AbortController + setTimeout (AbortSignal.timeout non su iOS < 16.4) | |
| const _ctrl = new AbortController(); | |
| const _tid = setTimeout(() => _ctrl.abort(), _TIMEOUT_MS); | |
| try { | |
| const _hfToken = getHFToken() ?? ""; | |
| const _resp = await callWithFallback( | |
| _hfToken, | |
| { | |
| messages: _msgs, | |
| stream: false as const, | |
| max_tokens: _MAX_TOKENS, | |
| temperature: 0, | |
| }, | |
| _ctrl.signal, | |
| ); | |
| clearTimeout(_tid); | |
| if (!_resp.ok) { | |
| return { proceed: true, reason: "api_error", skipped: true }; | |
| } | |
| const _data = await _resp.json() as { | |
| choices?: Array<{ message?: { content?: string } }>; | |
| }; | |
| const _raw = _data.choices?.[0]?.message?.content ?? ""; | |
| // indexOf/lastIndexOf — immune a brace annidati in string values | |
| const _fb = _raw.indexOf('{'); | |
| const _lb = _raw.lastIndexOf('}'); | |
| if (_fb === -1 || _lb <= _fb) { | |
| return { proceed: true, reason: "parse_error", skipped: true }; | |
| } | |
| const _parsed = JSON.parse(_raw.slice(_fb, _lb + 1)) as { | |
| ok: boolean; | |
| reason?: string; | |
| }; | |
| if (_parsed.ok === false) { | |
| const _reason = (_parsed.reason ?? "azione non necessaria al momento").slice(0, 120); | |
| return { proceed: false, reason: _reason, skipped: false }; | |
| } | |
| return { proceed: true, reason: "ok", skipped: false }; | |
| } catch { | |
| clearTimeout(_tid); | |
| // Fail-open: su timeout/errore il tool viene eseguito normalmente | |
| return { proceed: true, reason: "timeout_or_error", skipped: true }; | |
| } | |
| } | |