Spaces:
Sleeping
Sleeping
| // 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<typeof capRoute>; | |
| /** 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<CapabilityRouteOutput> { | |
| 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<typeof detectAmbiguity>[1], [_cls as Parameters<typeof detectAmbiguity>[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<string, string> = { | |
| premium: "🔥 alta potenza", standard: "⚡ standard", basic: "💨 veloce", | |
| }; | |
| const _TYPE_LABEL: Record<string, string> = { | |
| 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<typeof detectTopicBoundary>[0], _cls.type as Parameters<typeof detectTopicBoundary>[1]); | |
| updateCurrentSegment(_cls.type as Parameters<typeof updateCurrentSegment>[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 }; | |
| } | |