// agentLoop/capabilityRouterBlock.ts // S567: estratto da agentLoop.ts — CapabilityRouter + Consensus routing block. // Determina: reasoningDepth, isCodeTask, capRouteResult, preferredModel, // useConsensus, topicBoundary, maxTokensBudget. // Se consensus raggiunge quorum (≥2 provider OK) → action:"return" (skip ReAct loop). // // GAP-CTX: maxTokensBudget ora calcolato dinamicamente da providerContextWindows // invece delle soglie fisse (2048/4096). Valori reali per ciascun modello: // Gemini-2.5 1M ctx → 8192 output, Cerebras 8k ctx → 2048 output, ecc. import { detectAmbiguity } from "../ambiguityDetector"; import { route as capRoute, shouldUseConsensus, routeWithLocalFallback, isLocalPyodideRoute, } from "../capabilityRouter"; import { setCurrentProvider } from "../contextBridge"; import { detectTopicBoundary, updateCurrentSegment } from "../topicBoundary"; import { getHFToken } from "../providerChain"; import { runConsensus } from "../consensusMode"; import type { ChatMessage } from "../types"; import { getModelContextWindow, computeMaxOutputTokens } from "../providerContextWindows"; // GAP-CTX import "../errorLogSink"; // side-effect: persistent error log sink (IndexedDB) import { decide as hybridDecide } from "../hybridRouter"; // AUT-E import { workerManager } from "../runtime/WorkerManager"; // AUT-E: Worker health import { getProviderBridgeState } from "../providerBridge"; // AUT-E: backend latency /** Tipo di risultato capRoute (alias per export forward-compat). */ type CapRouteResult = ReturnType; /** Classificazione task minima necessaria per il routing. */ interface TaskCls { type: string; modelTier: "premium" | "standard" | "basic" | string; complexity: number; } export interface CapabilityRouteOutput { /** "return" se consensus ha già prodotto la risposta — il loop deve uscire. */ action: "continue" | "return"; reasoningDepth: "low" | "medium" | "high"; isCodeTask: boolean; capRouteResult: CapRouteResult; preferredModel: string | undefined; useConsensus: boolean; topicBoundary: "same" | "continuation" | "new_topic"; /** AUT-E: Attiva delegazione al backend HF Space / Railway per code exec. */ useBackendDelegation: boolean; /** AUT-E: Forza Pyodide locale — ignora backend per code exec. */ forcePyodideMode: boolean; /** * Budget output token (max_tokens nell'API call). * GAP-CTX: calcolato dinamicamente da getModelContextWindow + computeMaxOutputTokens. * Esempi: Gemini-2.5 → 8192 (code), Cerebras 8k → 2048, Groq 131k → 8192 (code). */ maxTokensBudget: number; } export async function resolveCapabilityRoute(params: { lastUserMsg: string; preCls: TaskCls; prevTopicSegment: unknown; isBackendHealthy: () => boolean; initialMessages: Array<{ role: string; content: string }>; signal: AbortSignal | undefined; onStatus: (s: string) => void; onChunk: (s: string) => void; }): Promise { const { lastUserMsg, preCls: _cls, prevTopicSegment, isBackendHealthy, initialMessages, signal, onStatus, onChunk, } = params; let reasoningDepth: "low" | "medium" | "high" = "medium"; let isCodeTask = false; let capRouteResult: CapRouteResult = null; let preferredModel: string | undefined; let useConsensus = false; let topicBoundary: "same" | "continuation" | "new_topic" = "same"; try { // S104: ambiguity detection — non-blocking signal only try { const _ambig = detectAmbiguity(lastUserMsg, _cls as Parameters[1], [_cls as Parameters[2][0]]); if (_ambig.isAmbiguous) onStatus("Richiesta ambigua — procedo con l'interpretazione più probabile"); } catch { /* non-blocking */ } const _capReq = { reasoning_depth: ( _cls.modelTier === "premium" ? "high" : _cls.modelTier === "standard" ? "medium" : "low" ) as "low" | "medium" | "high", code: _cls.type === "code_generation" || _cls.type === "code_fix" || _cls.type === "refactor" || _cls.type === "debug", requires_tools: _cls.type === "search_and_report" || _cls.type === "debug", prefer_free: true, speed: _cls.complexity <= 3, requires_consensus: _cls.complexity >= 9 || _cls.modelTier === "premium", }; reasoningDepth = _capReq.reasoning_depth; isCodeTask = _capReq.code; // S222: routeWithLocalFallback — Pyodide locale se backend offline + task.code const _routeFallback = routeWithLocalFallback(_capReq, isBackendHealthy()); if (isLocalPyodideRoute(_routeFallback)) { onStatus("⚡ Backend offline — esecuzione codice via Pyodide locale…"); } else { capRouteResult = _routeFallback; if (capRouteResult) { // GAP-R1: Decision Transparency — routing narrato in italiano leggibile const _TIER_LABEL: Record = { premium: "🔥 alta potenza", standard: "⚡ standard", basic: "💨 veloce", }; const _TYPE_LABEL: Record = { code_generation: "scrittura codice", code_fix: "correzione codice", refactor: "refactoring", debug: "debug", search_and_report: "ricerca web", question_answer: "domanda", creative: "testo creativo", analysis: "analisi", planning: "pianificazione", translation: "traduzione", }; const _tier = _TIER_LABEL[_cls.modelTier] ?? _cls.modelTier; const _type = _TYPE_LABEL[_cls.type] ?? _cls.type; const _prov = capRouteResult.provider ?? capRouteResult.reason ?? "auto"; onStatus(`🧭 ${_type} (${_tier}) → ${_prov}`); preferredModel = ( capRouteResult.provider === "groq" ? "llama-3.3-70b-versatile" : capRouteResult.provider === "gemini" ? "gemini-2.5-flash" : capRouteResult.provider === "openai" ? "gpt-4o-mini" : // "openrouter": preferredModel intentionally undefined — callWithFallback uses // preferredModel only for HF Router; OR section uses OPENROUTER_MODELS + forcedModel. // Passing an OR model name (e.g. "*..:free") to HF Router wastes one HTTP call on a bad model ID. undefined ); } } if (capRouteResult?.provider) setCurrentProvider(capRouteResult.provider); useConsensus = shouldUseConsensus(_capReq); topicBoundary = detectTopicBoundary(prevTopicSegment as Parameters[0], _cls.type as Parameters[1]); updateCurrentSegment(_cls.type as Parameters[0], lastUserMsg); } catch { /* non-blocking — loop continues with default provider */ } // GAP-CTX: budget dinamico proporzionale alla context window del modello. // Prima: maxTokensBudget = useConsensus ? 4096 : 2048 (hardcoded, model-agnostic). // Ora: calcolato da getModelContextWindow(preferredModel) → 8192 su 131k+ ctx, // 2048 su modelli piccoli (Cerebras 8k), con cap al 12% della finestra. const _ctxWindow = getModelContextWindow(preferredModel); const maxTokensBudget = computeMaxOutputTokens(_ctxWindow, isCodeTask, useConsensus); // Fix 2 (S381): Wire runConsensus — solo quando _useConsensus e backend non ha già risposto if (useConsensus && !signal?.aborted) { try { const _hfTok = getHFToken() ?? ""; const _consensusRes = await runConsensus( _hfTok, initialMessages.slice(-8) as ChatMessage[], s => onStatus(s), c => onChunk(c), signal, ); const _consensusOk = _consensusRes.filter(r => r.ok).length >= 2; if (_consensusOk) { onStatus(""); // consensus con ≥2 provider — risposta sintetizzata return { action: "return", reasoningDepth, isCodeTask, capRouteResult, preferredModel, useConsensus, topicBoundary, maxTokensBudget, useBackendDelegation: false, forcePyodideMode: false }; } // < 2 provider disponibili → fall through al ReAct loop } catch { /* non-blocking — consensus fallisce silenziosamente, loop prosegue */ } } // AUT-E: hybridRouter.decide() — aggrega deviceMemory + WorkerStatus + backend latency. // Sync — tutti e 3 i segnali sono disponibili senza await. let useBackendDelegation = false; let forcePyodideMode = false; try { // Segnale 1: deviceMemory (navigator API, sync, può essere undefined su vecchi browser) let _devMem = 4; // default sicuro try { const dm = (navigator as Navigator & { deviceMemory?: number }).deviceMemory; if (typeof dm === "number" && dm > 0) _devMem = dm; } catch { /* SSR/JSDOM — usa default */ } // Segnale 2: Worker health (sync — workerManager è già inizializzato) const _wStatus = (() => { try { return workerManager.getAllStatuses(); } catch { return []; } })(); // Segnale 3: backend reachability (sync — getProviderBridgeState è sync Map read) const _bridge = (() => { try { const _s = getProviderBridgeState(); return { backendReachable: _s.backend_reachable, backendLatencyMs: (_s as unknown as { latency_ms?: number }).latency_ms ?? 0, }; } catch { return { backendReachable: false, backendLatencyMs: 0 }; } })(); const _hd = hybridDecide( { type: _cls?.type ?? "question_answer", modelTier: _cls?.modelTier ?? "standard", complexity: _cls?.complexity ?? 5 }, { deviceMemoryGb: _devMem, workerStatus: _wStatus, backendReachable: _bridge.backendReachable, backendLatencyMs: _bridge.backendLatencyMs }, ); useBackendDelegation = _hd.useBackendDelegation; forcePyodideMode = _hd.forcePyodideMode; if (isCodeTask && _hd.target !== "browser") { onStatus(`🔀 exec: ${_hd.target} — ${_hd.reason}`); } } catch { /* non-blocking — hybridRouter è best-effort */ } return { action: "continue", reasoningDepth, isCodeTask, capRouteResult, preferredModel, useConsensus, topicBoundary, maxTokensBudget, useBackendDelegation, forcePyodideMode }; }