Spaces:
Sleeping
Sleeping
File size: 4,579 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 | /**
* 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 };
}
}
|