Spaces:
Sleeping
Sleeping
File size: 10,216 Bytes
cc11e77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | // 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 };
}
|