// 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; preExecCritic: (name: string, args: Record) => { blocked: boolean; reason: string; alt: string }; onNeedConfirm?: ((req: { tool: string; label: string; preview: string; risk: "medium" | "high" }) => Promise) | undefined; skillRegistry: { requiresConfirm: (n: string) => boolean; recordUse: (n: string, ok: boolean) => void; userApprove: (n: string) => void }; toolCache: Map; persistentToolCache: Map; persistentTtl: Record; prunePersistentCache: () => void; incrementPersistentCacheCallCount: () => number; maxPersistentCacheSize: number; convId: string; loopTaskId: string | null; contextBridge: { recordToolResult: (name: string, args: Record, result: string) => void }; semanticSearch: { invalidateCache: () => void; cacheTs: number; contextForAsync: (q: string) => Promise }; invalidateRepoMap: () => void; lastUserMsg: string; onSemanticCtxUpdate: (ctx: string, ts: number) => void; getSemanticCtxTs: () => number; } export function createExecuteToolGated(deps: ExecuteToolGatedDeps): (name: string, args: Record) => Promise { 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): Promise { // 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; const _toolTimeout = new Promise(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; }; }