Spaces:
Running
Running
File size: 10,662 Bytes
95eb75a | 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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | /**
* agentTelemetry.ts — Sistema di telemetria unificato per tutti i blocchi verdict-based
*
* Raccoglie contatori per ogni sistema di verifica/controllo qualità del loop agente.
* Permette di calibrare le soglie nel tempo (es: complexity >= 7, routeQualityScore < 0.85).
*
* Sistemi tracciati:
* - crossCritic → PASS / FAIL / UNCERTAIN
* - postResponseVerify → ok / unsafe / claim_vs_reality / [issue type]
* - goalVerify → pass / fail
* - planCritic → validated / rejected / skipped
* - validationGate → accept / repaired / retry / reject
* - finalOutputGate → pass / garbage / empty / no-proof
* - inlineVerify → safe / [issue: hallucination | contradiction | ...]
* - injectionGuard → detected / clean
*
* Design:
* - localStorage + in-memory cache (identico a scenarioFingerprintBlock — BUG-1-FIX applicato)
* - Fail-open totale: qualsiasi errore → noop silenzioso
* - Safari-safe: no Worker, no SharedArrayBuffer
* - Zero dipendenze esterne
* - Additive-only: non modifica mai il comportamento dei blocchi chiamanti
*
* @module agentTelemetry
*/
const LS_KEY = "agent_telemetry_v1";
const MAX_STORE = 200; // max entries totali prima di pruning (evita crescita unbounded)
// ─── Tipi ─────────────────────────────────────────────────────────────────────
export type TelemetrySystem =
| "crossCritic"
| "postResponseVerify"
| "goalVerify"
| "planCritic"
| "validationGate"
| "finalOutputGate"
| "inlineVerify"
| "injectionGuard";
interface VerdictStats {
count: number;
lastSeenMs: number;
}
interface SystemStats {
[verdict: string]: VerdictStats;
}
interface TelemetryStore {
[system: string]: SystemStats;
}
// ─── Cache in-memory (lazy-load, flush on write — BUG-1-FIX pattern) ──────────
let _cache: TelemetryStore | null = null;
function _load(): TelemetryStore {
if (_cache !== null) return _cache;
try {
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(LS_KEY) : null;
_cache = raw ? (JSON.parse(raw) as TelemetryStore) : {};
return _cache;
} catch { _cache = {}; return _cache; }
}
function _save(store: TelemetryStore): void {
_cache = store;
try {
if (typeof localStorage !== "undefined") localStorage.setItem(LS_KEY, JSON.stringify(store));
} catch { /* Safari private mode — non-fatal */ }
}
function _prune(store: TelemetryStore): TelemetryStore {
// Conta entries totali — se sopra MAX_STORE, rimuovi le più vecchie
let total = 0;
for (const sys of Object.values(store)) total += Object.keys(sys).length;
if (total <= MAX_STORE) return store;
// Semplice: rimuovi i sistemi con meno dati (sacrifica i meno usati)
const sorted = Object.entries(store).sort(([, a], [, b]) => {
const sumA = Object.values(a).reduce((s, v) => s + v.count, 0);
const sumB = Object.values(b).reduce((s, v) => s + v.count, 0);
return sumB - sumA;
});
const pruned: TelemetryStore = {};
let kept = 0;
for (const [sys, stats] of sorted) {
if (kept + Object.keys(stats).length > MAX_STORE) break;
pruned[sys] = stats;
kept += Object.keys(stats).length;
}
return pruned;
}
// ─── API pubblica ──────────────────────────────────────────────────────────────
/**
* Registra un evento verdict per un sistema.
* Fire-and-forget, mai lancia eccezioni.
*
* @param system - Nome del sistema (es: "crossCritic")
* @param verdict - Risultato (es: "PASS", "fail", "retry")
*/
export function record(system: TelemetrySystem | string, verdict: string): void {
try {
const store = _load();
if (!store[system]) store[system] = {};
const sys = store[system]!;
const key = verdict.toLowerCase();
const prev = sys[key] ?? { count: 0, lastSeenMs: 0 };
sys[key] = { count: prev.count + 1, lastSeenMs: Date.now() };
_save(_prune(store));
// Gap N4: auto-sync al backend ogni _SYNC_EVERY chiamate (fire-and-forget).
// _syncCounter e _SYNC_EVERY sono definiti più in basso ma inizializzati prima
// di qualsiasi call (modulo caricato prima dell'uso). syncToBackend è hoisted.
_syncCounter++;
if (_syncCounter >= _SYNC_EVERY) {
_syncCounter = 0;
syncToBackend().catch(() => {}); // eslint-disable-line @typescript-eslint/no-floating-promises
}
} catch { /* fail-open */ }
}
export interface TelemetryEntry {
system: string;
verdict: string;
count: number;
lastSeenMs: number;
}
/**
* Restituisce tutti gli eventi registrati ordinati per sistema e count.
* Per diagnostica e calibrazione soglie.
*/
export function getReport(): TelemetryEntry[] {
try {
const store = _load();
const entries: TelemetryEntry[] = [];
for (const [system, stats] of Object.entries(store)) {
for (const [verdict, vs] of Object.entries(stats)) {
entries.push({ system, verdict, count: vs.count, lastSeenMs: vs.lastSeenMs });
}
}
return entries.sort((a, b) =>
a.system.localeCompare(b.system) || b.count - a.count
);
} catch { return []; }
}
/**
* Restituisce il report per un singolo sistema (es: calibrazione soglie).
* Esempio: getSystemReport("crossCritic") → { pass: 12, fail: 3, uncertain: 1 }
*/
export function getSystemReport(system: string): Record<string, number> {
try {
const store = _load();
const stats = store[system] ?? {};
return Object.fromEntries(Object.entries(stats).map(([v, s]) => [v, s.count]));
} catch { return {}; }
}
/**
* Calcola il fail rate di un sistema (fail / (pass + fail)).
* Utile per calibrare se la soglia di trigger è troppo alta o troppo bassa.
*
* @param system - Sistema da analizzare
* @param failVerdict - Chiave che conta come "fail" (default: "fail")
* @param passVerdict - Chiave che conta come "pass" (default: "pass")
* @returns null se non abbastanza dati (< 5 eventi totali)
*/
export function getFailRate(
system: string,
failVerdict = "fail",
passVerdict = "pass",
): number | null {
try {
const rep = getSystemReport(system);
const fail = rep[failVerdict] ?? 0;
const pass = rep[passVerdict] ?? 0;
const total = fail + pass;
if (total < 5) return null;
return Math.round((fail / total) * 1000) / 1000;
} catch { return null; }
}
/** Solo per unit test — non usare in produzione. */
export function _resetTelemetry(): void {
_cache = null;
try {
if (typeof localStorage !== "undefined") localStorage.removeItem(LS_KEY);
} catch { /* noop */ }
}
// ─── Gap N4: Backend sync — verdetti persistenti cross-session/device ─────────
// Obiettivo: agentTelemetry usava solo localStorage → dati di calibrazione persi
// su altri device o dopo clear cache. Questi helper sincronizzano con il backend
// HF Space (/data/agent_telemetry.json) per persistenza vera.
//
// Flusso:
// BOOT → loadFromBackend() → merge server store in localStorage (silent, 2s delay)
// RECORD → ogni 10 record → syncToBackend() fire-and-forget (non blocca mai)
//
// Merge: additive — max(count), max(lastSeenMs) — i contatori non calano mai.
// Fail-open totale: qualsiasi errore di rete → silent noop, localStorage non toccato.
let _syncCounter = 0; // conta i record dall'ultimo sync
const _SYNC_EVERY = 10; // sync al backend ogni N record
/** Merge additive locale: max(count), max(lastSeenMs) per ogni (system, verdict). */
function _mergeLocal(a: TelemetryStore, b: TelemetryStore): TelemetryStore {
const out: TelemetryStore = {};
// Copia a
for (const [sys, verdicts] of Object.entries(a)) {
out[sys] = { ...verdicts };
}
// Merge b sopra a
for (const [sys, verdicts] of Object.entries(b)) {
if (!out[sys]) out[sys] = {};
for (const [verdict, vs] of Object.entries(verdicts)) {
const prev = out[sys]![verdict] ?? { count: 0, lastSeenMs: 0 };
out[sys]![verdict] = {
count: Math.max(prev.count, vs.count),
lastSeenMs: Math.max(prev.lastSeenMs, vs.lastSeenMs),
};
}
}
return out;
}
/**
* Carica dati dal backend e fa merge con il localStorage locale.
* Chiamata una volta al boot (delay 2s, non-blocking).
* GET /api/agent-telemetry/sync → merge → _save().
*/
export async function loadFromBackend(): Promise<void> {
try {
const { ENV } = await import("@/config/env");
const base = ENV.BACKEND_URL;
if (!base) return;
const res = await fetch(base.replace(/\/$/, "") + "/api/agent-telemetry/sync", {
method: "GET",
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return;
const body = await res.json() as { ok?: boolean; data?: unknown };
if (!body.ok || !body.data || typeof body.data !== "object") return;
const local = _load();
const merged = _mergeLocal(local, body.data as TelemetryStore);
_save(merged);
} catch { /* fail-open — nessun impatto sull'agente */ }
}
/**
* Sincronizza il localStorage corrente con il backend (merge bidirezionale).
* Il server ritorna il merged: aggiorniamo localStorage con dati da altri device.
* POST /api/agent-telemetry/sync → merge → _save().
* Fire-and-forget — non restituisce mai un errore.
*/
export async function syncToBackend(): Promise<void> {
try {
const { ENV } = await import("@/config/env");
const base = ENV.BACKEND_URL;
if (!base) return;
const store = _load();
const res = await fetch(base.replace(/\/$/, "") + "/api/agent-telemetry/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: store }),
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return;
const body = await res.json() as { ok?: boolean; merged?: unknown };
if (!body.ok || !body.merged || typeof body.merged !== "object") return;
// Aggiorna localStorage con il merged del server (include dati da altri device)
_save(body.merged as TelemetryStore);
} catch { /* fail-open */ }
}
// ── Auto-boot: carica dal backend 2s dopo il primo import ──────────────────
if (typeof window !== "undefined" && typeof fetch !== "undefined") {
setTimeout(() => {
loadFromBackend().catch(() => {});
}, 2000);
}
// Gap N4: auto-sync integrato direttamente in record() — counter sopra.
|