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 };
}