AUDIT / src /lib /agentLoop /capabilityRouterBlock.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
10.2 kB
// 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 };
}