AUDIT / src /lib /agentLoop /debateBlock.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
4.05 kB
import { getGroqTokens } from "@/lib/apiKeys"; // GAP-6: key rotation
import { acquireGroqHigh } from "@/lib/groqSemaphore"; // ORCH-5: priority lane per percorso critico
/**
* debateBlock.ts — S495: extracted from agentLoop.ts
*
* Wrapper per il Dual-Agent Debate (S224).
* Trigger: reasoning_depth="high" + task NON di codice.
* Output: stringa consenso da iniettare come contesto pre-loop, o null.
*
* Separa la logica di orchestrazione del debate dalle variabili di closure
* di agentLoop.ts, rendendo il debate block testabile in isolamento.
*
* @module debateBlock
*/
/**
* iOS-safe AbortSignal con timeout.
* AbortSignal.timeout() richiede iOS Safari >= 16.4 — non disponibile su iOS 15.
* Fallback: AbortController + setTimeout.
*/
function _makeTimedSignal(ms: number): AbortSignal {
if (typeof AbortSignal.timeout === "function") {
try { return AbortSignal.timeout(ms); } catch { /* iOS 15 safeguard */ }
}
const c = new AbortController();
setTimeout(() => c.abort(), ms);
return c.signal;
}
/**
* Esegue il Dual-Agent Debate se le condizioni sono soddisfatte.
* Restituisce il testo del consenso o null (mai lancia — tutto wrapped in try/catch).
*
* @param task — testo del task/messaggio utente
* @param groqToken — Bearer token Groq (null → skip debate)
* @param opts — opzioni: signal, onStatus callback
* @returns — testo consenso o null
*/
export async function runDebateIfNeeded(
task: string,
groqToken: string | null | undefined,
opts?: {
signal?: AbortSignal;
onStatus?: (msg: string) => void;
},
): Promise<string | null> {
if (!groqToken || opts?.signal?.aborted) return null;
try {
opts?.onStatus?.("Valuto da più angolazioni…");
const { debateToText } = await import("../agent/debateRunner");
// Groq 70B per il debate (ragionamento architetturale, non streaming)
// GAP-6: key rotation — tenta tutti gli slot Groq disponibili se 429
const _debateLLM = async (
msgs: Array<{ role: string; content: string }>,
llmOpts?: { signal?: AbortSignal },
): Promise<string> => {
// ORCH-5: acquireGroqHigh — debate è percorso critico, usa priority lane.
// GAP-3: coordina con il semaforo globale (MAX_CONCURRENT_GROQ=3).
// Debate fa 2 chiamate LLM sequenziali (BUILDER + CRITIC) senza passare per providerChain.
const _release = await acquireGroqHigh();
try {
const _pool = [...new Set([...getGroqTokens(), ...(groqToken ? [groqToken] : [])])];
if (_pool.length === 0) return "";
for (let _i = 0; _i < _pool.length; _i++) {
const _tok = _pool[_i];
const r = await fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${_tok}`, "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama-3.3-70b-versatile",
messages: msgs,
max_tokens: 1024,
temperature: 0.4,
}),
// iOS-safe: AbortSignal.timeout non disponibile su iOS Safari < 16.4
signal: llmOpts?.signal ?? _makeTimedSignal(22_000),
});
// 429 rate-limit: prova slot successivo se disponibile
if (r.status === 429 && _i < _pool.length - 1) continue;
const d = await r.json() as { choices?: Array<{ message?: { content?: string } }> };
return d.choices?.[0]?.message?.content ?? "";
}
return "";
} finally {
_release();
}
};
const _ct = await debateToText(task, _debateLLM, {
maxRounds: 2,
roundTimeoutMs: 20_000,
totalTimeoutMs: 40_000,
signal: opts?.signal,
});
if (_ct && !_ct.startsWith("❌")) {
opts?.onStatus?.("Analisi completata ✓");
return _ct;
}
return null;
} catch {
// Non-blocking — debate fallisce silenziosamente, loop prosegue invariato
return null;
}
}