AUDIT / src /lib /agentLoop /efficiencyCritic.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
4.58 kB
/**
* 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 };
}
}