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 { 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 => { // 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; } }