Spaces:
Sleeping
Sleeping
| // S535: executeToolGated factory — estratto da agentLoop.ts | |
| // Riceve tutte le dipendenze chiuse e ritorna la funzione executeToolGated. | |
| import { useTaskStore } from "@/store/taskStore"; | |
| import { AGENT_TOOLS } from "./toolDefinitions"; | |
| import { executeToolWithFallback } from "./toolExecutor"; | |
| import { CB_ALT } from "./circuitBreaker"; | |
| import { | |
| CACHEABLE_TOOLS, | |
| NETWORK_TOOLS, | |
| HIGH_RISK_TOOLS as HIGH_RISK_TOOLS_M, | |
| TOOL_TIMEOUTS, | |
| toolCacheKey, | |
| } from "./toolGate"; | |
| import { TOOL_STATUS_LABELS, toolNameToActionType as _toolNameToActionType } from "./toolStatusLabels"; | |
| import { TOOL_CONTRACTS } from "./toolOutputValidators"; // P2-6: renamed from toolContracts | |
| import { recordRoutingEvent } from "../agent/SuccessRateMonitor"; // S686 | |
| // GAP-TRUTH-3: PROOF_VERIFIED_TOOLS sostituisce il set _CRITICAL_VERIFY locale | |
| import { PROOF_VERIFIED_TOOLS } from "./proofToolRegistry"; | |
| // GAP-2: simboli dichiarati → cross-check semantico in verifyToolResult | |
| import { detectClaimedSymbols } from "./symbolClaimDetector"; | |
| // V7-4: efficiency critic — LLM check prima di git_push/deploy_trigger/execute_shell | |
| import { isEfficiencyCriticTool, runEfficiencyCritic } from "./efficiencyCritic"; | |
| // TG-WEBHOOK: notifica Telegram con confirmKey prima di onNeedConfirm → bottone "🚫 Rifiuta" su iPhone | |
| import { tgNotify } from "../agentTelegram"; | |
| export interface ExecuteToolGatedDeps { | |
| signal: AbortSignal | undefined; | |
| onStatus: (msg: string) => void; | |
| preExecTools: Set<string>; | |
| preExecCritic: (name: string, args: Record<string, unknown>) => { blocked: boolean; reason: string; alt: string }; | |
| onNeedConfirm?: ((req: { tool: string; label: string; preview: string; risk: "medium" | "high" }) => Promise<boolean>) | undefined; | |
| skillRegistry: { requiresConfirm: (n: string) => boolean; recordUse: (n: string, ok: boolean) => void; userApprove: (n: string) => void }; | |
| toolCache: Map<string, string>; | |
| persistentToolCache: Map<string, { result: string; at: number }>; | |
| persistentTtl: Record<string, number>; | |
| prunePersistentCache: () => void; | |
| incrementPersistentCacheCallCount: () => number; | |
| maxPersistentCacheSize: number; | |
| convId: string; | |
| loopTaskId: string | null; | |
| contextBridge: { recordToolResult: (name: string, args: Record<string, unknown>, result: string) => void }; | |
| semanticSearch: { invalidateCache: () => void; cacheTs: number; contextForAsync: (q: string) => Promise<string> }; | |
| invalidateRepoMap: () => void; | |
| lastUserMsg: string; | |
| onSemanticCtxUpdate: (ctx: string, ts: number) => void; | |
| getSemanticCtxTs: () => number; | |
| } | |
| export function createExecuteToolGated(deps: ExecuteToolGatedDeps): (name: string, args: Record<string, unknown>) => Promise<string> { | |
| const { | |
| signal, onStatus, preExecTools, preExecCritic, onNeedConfirm, skillRegistry, | |
| toolCache, persistentToolCache, persistentTtl, prunePersistentCache, | |
| incrementPersistentCacheCallCount, maxPersistentCacheSize, convId, loopTaskId, | |
| contextBridge, semanticSearch, invalidateRepoMap, lastUserMsg, | |
| onSemanticCtxUpdate, getSemanticCtxTs, | |
| } = deps; | |
| const HIGH_RISK_TOOLS = HIGH_RISK_TOOLS_M; | |
| const _CACHEABLE_TOOLS = CACHEABLE_TOOLS; | |
| const _NETWORK_TOOLS = NETWORK_TOOLS; | |
| const _TOOL_TIMEOUTS = TOOL_TIMEOUTS; | |
| const _toolCacheKey = toolCacheKey; | |
| const _PERSISTENT_TTL = persistentTtl; | |
| const _TOOL_CONTRACTS = TOOL_CONTRACTS; | |
| return async function executeToolGated(name: string, args: Record<string, unknown>): Promise<string> { | |
| // AbortSignal early-exit | |
| if (signal?.aborted) { onStatus("🛑 Operazione annullata"); return "❌ [ABORT] Operazione annullata dall'utente."; } | |
| // Step6: Pre-execution critic for high-impact tools | |
| if (preExecTools.has(name)) { | |
| const _criticism = preExecCritic(name, args); | |
| if (_criticism.blocked) { | |
| return `❌ [PRE-EXEC CRITIC] ${_criticism.reason} — Alternativa: ${_criticism.alt}`; | |
| } | |
| } | |
| // V7-4: Efficiency critic — check LLM leggero (100tok, 4s, 30s cooldown) | |
| // su git_push/git_push_multiple/deploy_trigger/execute_shell prima dell'esecuzione. | |
| // Fail-open: se timeout/cooldown → proceed:true → nessun impatto sul loop. | |
| if (isEfficiencyCriticTool(name)) { | |
| const _recentCtx = (() => { | |
| if (!loopTaskId) return ""; | |
| try { | |
| const _task = useTaskStore.getState().tasks.find(t => t.id === loopTaskId); | |
| return _task?.actions.slice(-3).map(a => `${a.label}: ${a.done ? "✓" : "…"}`).join(", ") ?? ""; | |
| } catch { return ""; } | |
| })(); | |
| const _ec = await runEfficiencyCritic(name, args, lastUserMsg, _recentCtx); | |
| if (!_ec.proceed && !_ec.skipped) { | |
| onStatus(`🛑 Efficiency critic: ${_ec.reason.slice(0, 60)}`); | |
| return `❌ [EFFICIENCY CRITIC] Azione bloccata: ${_ec.reason}. Verifica il piano e riprova.`; | |
| } | |
| } | |
| if (HIGH_RISK_TOOLS.includes(name) && onNeedConfirm && skillRegistry.requiresConfirm(name)) { | |
| const preview = name === 'run_code' | |
| ? (args.code as string ?? '').slice(0, 320) | |
| : String(args.path ?? '') + (args.content ? ' (' + String(args.content).length + ' chars)' : ''); | |
| const label = name === 'write_file' | |
| ? 'Salva ' + String(args.path ?? 'file') | |
| : (args.code as string ?? '').slice(0, 90); | |
| // REJECT-WIRE: corto-circuita confirm se questo tool è già stato rifiutato (decisionMemory) | |
| // — chiude il cerchio: _dmIsRejected() in agentLoop era sempre false perché rejectProposal() non veniva mai chiamata | |
| const _confirmKey = `confirm_${name}_${lastUserMsg.slice(0, 40).replace(/\W+/g, "_")}`; | |
| let _alreadyRejected = false; | |
| try { const { isRejected: _isDmRejected } = await import("../decisionMemory"); _alreadyRejected = _isDmRejected(_confirmKey); } catch { /* non-blocking */ } | |
| if (_alreadyRejected) { skillRegistry.recordUse(name, false); return '⏭️ Azione già rifiutata in precedenza (memoria sessione) — usa un approccio diverso.'; } | |
| // TG-WEBHOOK: manda notifica Telegram con bottone "🚫 Rifiuta" (confirm_key → Realtime → rejectProposal) | |
| try { tgNotify.taskApproval(loopTaskId, label, 85, 'medio', undefined, undefined, _confirmKey); } catch { /* non-blocking */ } | |
| const allowed = await onNeedConfirm({ tool: name, label, preview, risk: 'medium' }); | |
| if (!allowed) { | |
| skillRegistry.recordUse(name, false); | |
| // REJECT-WIRE: registra rifiuto → future iterazioni non riproporranno lo stesso tool in questo task | |
| try { const { rejectProposal } = await import("../decisionMemory"); rejectProposal(_confirmKey, `utente ha annullato: ${label}`); } catch { /* non-blocking */ } | |
| return '⏭️ Azione annullata dall\'utente.'; | |
| } | |
| skillRegistry.userApprove(name); | |
| } | |
| // ── Offline detection ────────────────────────────────────────────────────── | |
| if ( | |
| _NETWORK_TOOLS.has(name) && | |
| typeof navigator !== "undefined" && | |
| !navigator.onLine | |
| ) { | |
| onStatus("📵 Offline — " + name + " saltato"); | |
| return `❌ [OFFLINE] Connessione internet non disponibile — "${name}" richiede rete. Basati sulle tue conoscenze o usa tool locali (read_file, recall, math_eval).`; | |
| } | |
| // ── Required arg validation ──────────────────────────────────────────────── | |
| { | |
| const _toolDef = (typeof AGENT_TOOLS !== "undefined" ? (AGENT_TOOLS as any[]) : []) | |
| .find((t: { type: string; function: { name: string; parameters?: { required?: string[] } } }) => t.function.name === name); | |
| const _required: string[] = (_toolDef?.function?.parameters?.required as string[] | undefined) ?? []; | |
| const _missing = _required.filter(k => { | |
| const v = args[k]; | |
| return v === undefined || v === null || v === ""; | |
| }); | |
| if (_missing.length > 0) { | |
| return `❌ [ARG VALIDATION] Tool "${name}" richiede: ${_missing.map(k => `"${k}"`).join(", ")}. Fornisci i campi mancanti e riprova.`; | |
| } | |
| } | |
| // ── Duplicate call cache ─────────────────────────────────────────────────── | |
| if (_CACHEABLE_TOOLS.has(name)) { | |
| const _ck = _toolCacheKey(name, args); | |
| const _cv = toolCache.get(_ck); | |
| if (_cv !== undefined) { | |
| onStatus(`♻️ ${name} (da cache)`); | |
| return `${_cv}\n\n[CACHE: risultato identico già ottenuto in questo task]`; | |
| } | |
| } | |
| // ── Per-tool timeout ─────────────────────────────────────────────────────── | |
| const _toolMs = _TOOL_TIMEOUTS[name] ?? 35_000; | |
| let _toolTimeoutId: ReturnType<typeof setTimeout>; | |
| const _toolTimeout = new Promise<string>(resolve => { | |
| _toolTimeoutId = setTimeout( | |
| () => resolve( | |
| `❌ [TIMEOUT] "${name}" non ha risposto in ${Math.round(_toolMs / 1000)}s — server troppo lento.` + | |
| (CB_ALT[name] ? ` Usa invece: ${CB_ALT[name]}.` : ""), | |
| ), | |
| _toolMs, | |
| ); | |
| }); | |
| // S274 Fix#7: add action card to taskStore before tool execution | |
| const _tsActStart = Date.now(); | |
| const _tsActId: string | null = (() => { | |
| if (!loopTaskId) return null; | |
| try { | |
| const _tsLocal = useTaskStore.getState(); | |
| _tsLocal.addAction(loopTaskId, { | |
| type: _toolNameToActionType(name), | |
| label: TOOL_STATUS_LABELS[name] ?? name, | |
| detail: (() => { | |
| const _v0 = Object.values(args)[0]; | |
| return _v0 != null ? String(_v0).slice(0, 120) : undefined; | |
| })(), | |
| done: false, | |
| }); | |
| return _tsLocal.tasks.find(t => t.id === loopTaskId)?.actions.at(-1)?.id ?? null; | |
| } catch { return null; } | |
| })(); | |
| const result = await Promise.race([ | |
| executeToolWithFallback(name, args, signal).finally(() => clearTimeout(_toolTimeoutId)), | |
| _toolTimeout, | |
| ]); | |
| // S274 Fix#7: complete action card after tool returns | |
| if (_tsActId && loopTaskId) { | |
| try { useTaskStore.getState().completeAction(loopTaskId, _tsActId, Date.now() - _tsActStart); } catch { /* non-blocking */ } | |
| } | |
| // Context overflow: cap result to 12KB | |
| // GAP-SAFARI2 v2: smart size-guard — hard limit 300KB + troncatura intelligente. | |
| // v1 tagliava raw a metà stringa → JSON invalido, righe spezzate, indentazione rotta. | |
| // v2: (1) rileva binary/base64 → skip payload, ritorna solo metadati | |
| // (2) tronca all'ultimo newline (testi/codice) o ',' (JSON array) | |
| // così il risultato resta leggibile e parsabile dal LLM. | |
| const _SAFARI_MAX_RAW = 300_000; | |
| // Rilevamento binary/base64: >97% chars alfanumerici + varietà reale o data URI. | |
| // Requisiti: (1) data URI prefix, oppure (2) upper+lower+digit+special misti | |
| // Evita falsi positivi su stringhe alfanumeriche monotone (es. rep('A', 300_000)). | |
| const _isBinaryPayload = (() => { | |
| if (result.length < 50_000) return false; | |
| const _sample = result.slice(0, 600); | |
| const _b64Count = _sample.split('').filter((c: string) => /[A-Za-z0-9+/=\n]/.test(c)).length; | |
| if ((_b64Count / _sample.length) <= 0.97) return false; | |
| // data URI prefix → certamente base64 | |
| if (/^data:[a-z]+\/[a-z+]+;base64,/i.test(_sample.trim())) return true; | |
| // Base64 reale ha varietà: uppercase + lowercase + digit + almeno un char speciale (+/=) | |
| const _hasUpper = /[A-Z]/.test(_sample); | |
| const _hasLower = /[a-z]/.test(_sample); | |
| const _hasDigit = /[0-9]/.test(_sample); | |
| const _hasSpec = /[+/=]/.test(_sample); | |
| return _hasUpper && _hasLower && _hasDigit && _hasSpec; | |
| })(); | |
| let _resultSafe: string; | |
| if (_isBinaryPayload) { | |
| // Payload binario/base64 → non inviare dati raw (inutili e pesanti per l'LLM) | |
| _resultSafe = `[GAP-SAFARI2: payload binario/base64 (${Math.round(result.length / 1024)}KB) — contenuto non testuale non inviato al LLM. Usa il path del file invece del contenuto.]`; | |
| } else if (result.length > _SAFARI_MAX_RAW) { | |
| // Tronca al confine intelligente: ultimo newline o ',' entro il limite. | |
| // FIX R2-P1: usa _isLikelyJson300 per decidere se la virgola è un confine valido. | |
| // Stesso pattern del guard 12KB (bug: il 300KB mancava di questo check). | |
| // Scenario senza fix: log >300KB con virgole ma senza newlines → taglio a metà riga. | |
| const _rawCut = result.slice(0, _SAFARI_MAX_RAW); | |
| const _isLikelyJson300 = /^\s*(\{|\[\s*[\[{"\]])/.test(result.slice(0, 60)); | |
| const _lastNL = _rawCut.lastIndexOf('\n'); | |
| const _lastCm = _isLikelyJson300 ? _rawCut.lastIndexOf(',') : -1; | |
| const _boundary = _lastNL > _SAFARI_MAX_RAW * 0.85 ? _lastNL | |
| : _lastCm > _SAFARI_MAX_RAW * 0.85 ? _lastCm | |
| : _SAFARI_MAX_RAW; | |
| _resultSafe = result.slice(0, _boundary) | |
| + `\n[GAP-SAFARI2: troncato al confine di testo (${Math.round(_boundary / 1024)}KB) — originale ${Math.round(result.length / 1024)}KB]`; | |
| } else { | |
| _resultSafe = result; | |
| } | |
| const MAX_TOOL_RESULT = 12_000; | |
| // Troncatura finale a 12KB: cerca confine pulito (newline → virgola-JSON → raw cut) | |
| // NOTA: virgola usata SOLO se il contenuto è probabilmente JSON (inizia con { o [{) | |
| // Scenari non-JSON (log, CSV con virgole, codice) usano il newline o il raw cut. | |
| // isLikelyJson esclude log prefix "[2026-..." ([ seguito da digit) e plain text. | |
| const _isLikelyJson = /^\s*(\{|\[\s*[\[{"\]])/.test(_resultSafe.slice(0, 60)); | |
| const _resultFinal = (() => { | |
| if (_resultSafe.length <= MAX_TOOL_RESULT) return _resultSafe; | |
| const _cut = _resultSafe.slice(0, MAX_TOOL_RESULT); | |
| const _nl = _cut.lastIndexOf('\n'); | |
| // Usa confine virgola solo per JSON reale (evita tagli a metà-riga in log/CSV) | |
| const _cm = _isLikelyJson ? _cut.lastIndexOf(',') : -1; | |
| const _smartCut = _nl > MAX_TOOL_RESULT * 0.80 ? _nl | |
| : _cm > MAX_TOOL_RESULT * 0.80 ? _cm | |
| : MAX_TOOL_RESULT; | |
| return _resultSafe.slice(0, _smartCut) | |
| + `\n\n[TRONCATO: risposta originale ${Math.round(_resultSafe.length / 1024)}KB — mostrati primi ${Math.round(_smartCut / 1024)}KB]`; | |
| })(); | |
| // Cache the result | |
| if (_CACHEABLE_TOOLS.has(name) && !_resultFinal.startsWith("❌") && !_resultFinal.includes("[CACHE:")) { | |
| const _ck2 = _toolCacheKey(name, args); | |
| try { toolCache.set(_ck2, _resultFinal); } catch { /* non-blocking */ } | |
| const _ttl2 = _PERSISTENT_TTL[name] ?? 0; | |
| if (_ttl2 > 0) { | |
| try { | |
| if (persistentToolCache.size >= maxPersistentCacheSize) prunePersistentCache(); | |
| persistentToolCache.set(convId + "::" + _ck2, { result: _resultFinal, at: Date.now() }); | |
| if (incrementPersistentCacheCallCount() % 50 === 0) prunePersistentCache(); | |
| } catch { /* non-blocking */ } | |
| } | |
| } | |
| try { skillRegistry.recordUse(name, !_resultFinal.startsWith('❌')); } catch { /* non-blocking */ } | |
| try { contextBridge.recordToolResult(name, args, _resultFinal); } catch { /* non-blocking */ } | |
| try { semanticSearch.invalidateCache(); } catch { /* non-blocking */ } | |
| try { invalidateRepoMap(); } catch { /* non-blocking */ } | |
| // S686: wire recordRoutingEvent — misura quante volte l'LLM sceglie i tool suggeriti | |
| // da AgentDispatcher (S680/S681 routing accuracy). Non-blocking, solo su successo. | |
| try { | |
| if (!_resultFinal.startsWith("❌")) recordRoutingEvent(lastUserMsg, name); | |
| } catch { /* non-blocking */ } | |
| // S167-Fix8: ricalcola ctx se la memoria è cambiata | |
| if (semanticSearch.cacheTs !== getSemanticCtxTs()) { | |
| semanticSearch.contextForAsync(lastUserMsg) | |
| .then(ctx => { onSemanticCtxUpdate(ctx, semanticSearch.cacheTs); }) | |
| .catch(() => {}); | |
| } | |
| // Quality filter: ghost success detection | |
| if (!_resultFinal.startsWith("❌")) { | |
| const _qt = _resultFinal.trim(); | |
| const _isGhost = | |
| _qt.length < 8 || | |
| _qt === "null" || _qt === "undefined" || | |
| _qt === "{}" || _qt === "[]" || | |
| /^Error:/i.test(_qt) || | |
| /^(no results?|nessun risultato|nothing found|0 results?)/i.test(_qt); | |
| if (_isGhost) { | |
| return "❌ [QUALITY] " + name + ": risultato vuoto/non valido (" + _qt.slice(0, 80) + "). Prova approccio alternativo."; | |
| } | |
| // T005: Per-tool semantic contract validators | |
| const _contractCheck = _TOOL_CONTRACTS[name]; | |
| if (_contractCheck) { | |
| const _contractErr = _contractCheck(_qt, args); | |
| if (_contractErr) { | |
| return "⚠️ [CONTRACT] " + _contractErr; | |
| } | |
| } | |
| } | |
| // S304-A: stale cache fallback su errore | |
| if (_resultFinal.startsWith("❌") && _CACHEABLE_TOOLS.has(name)) { | |
| const _staleKey = _toolCacheKey(name, args); | |
| let _bestStale: { result: string; at: number } | undefined; | |
| for (const [_pk, _pv] of persistentToolCache) { | |
| const _pkCore = _pk.includes("::") ? _pk.slice(_pk.indexOf("::") + 2) : _pk; | |
| if (_pkCore === _staleKey && _pv.result && !_pv.result.startsWith("❌") && !_pv.result.includes("Cache locale")) { | |
| if (!_bestStale || _pv.at > _bestStale.at) _bestStale = _pv; | |
| } | |
| } | |
| if (_bestStale) { | |
| const _ageMin = Math.round((Date.now() - _bestStale.at) / 60_000); | |
| const _ageStr = _ageMin > 60 ? `${Math.round(_ageMin / 60)}h` : `${_ageMin}min`; | |
| return _bestStale.result + `\n\n_(⚠️ Cache locale [${_ageStr} fa] — connessione temporaneamente non disponibile)_`; | |
| } | |
| } | |
| // Gap 2 — verifier come autorità: ogni tool critico viene verificato prima di ritornare | |
| // il risultato. L'OBSERVATION che il modello riceve include prova di verifica. | |
| // Ledger recording delegato ad annotateWithProof (verifier.ts) — unico writer per case1/case2 path. | |
| if (PROOF_VERIFIED_TOOLS.has(name) && !_resultFinal.startsWith("❌")) { | |
| try { | |
| const { verifyToolResult } = await import("../agent/verifier"); | |
| // GAP-2: estrai simboli dichiarati dal testo del task/args per cross-check semantico | |
| const _claimedSyms = detectClaimedSymbols(String(args.description ?? args.goal ?? args.path ?? "")); | |
| const _vr = verifyToolResult(name, args, _resultFinal, _claimedSyms.length > 0 ? _claimedSyms : undefined); | |
| if (!_vr.verified) { | |
| onStatus(`⚠️ ${name}: verifica fallita`); | |
| // Gap 5: trustScore basso codificato nell'OBSERVATION per AgentStepCards | |
| return `${_resultFinal} | |
| ⚠️ VERIFICA: non confermato — ${_vr.proof}`; | |
| } | |
| // Gap 5: trustScore alto codificato nell'OBSERVATION | |
| return `${_resultFinal} | |
| ✅ VERIFICATO: ${_vr.proof}`; | |
| } catch { /* non-blocking: verifier errore non blocca il tool */ } | |
| } | |
| return _resultFinal; | |
| }; | |
| } | |