File size: 4,579 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
/**
 * efficiencyCritic.ts — V7-4: LLM sanity check prima di azioni ad alto impatto.
 *
 * Eseguito su git_push / git_push_multiple / deploy_trigger / execute_shell
 * DOPO preExecCritic (statico) e PRIMA dell'esecuzione reale del tool.
 *
 * Caratteristiche:
 *   - max 100 token output, temperatura 0 (deterministico)
 *   - timeout 4s hard (non blocca mai il loop)
 *   - cooldown 30s (protegge il free tier Groq/Cerebras)
 *   - non-blocking: su timeout/errore → proceed:true (fail-open)
 *
 * Fix V7-4: usa callWithFallback(hfToken, payload, signal) — non-streaming —
 * e parsa la risposta come JSON dalla Response. Il precedente pattern streaming
 * (msgs, onChunk, opts) era errato: falliva silenziosamente → catch → skipped:true.
 */

import { callWithFallback, getHFToken } from "@/lib/providerChain";
import type { ChatMessage } from "@/lib/types";

// Tool soggetti al check — irreversibili e ad alto impatto
const _CRITIC_TOOLS = new Set([
  "git_push",
  "git_push_multiple",
  "deploy_trigger",
  "execute_shell",
]);

const _COOLDOWN_MS = 30_000;  // 30s — protegge il free tier
const _TIMEOUT_MS  = 4_000;   // 4s — safe anche su cold-start Railway
const _MAX_TOKENS  = 100;     // budget minimo

let _lastFiredAt = -Infinity;

export interface EfficiencyCriticResult {
  proceed: boolean;
  reason:  string;
  skipped: boolean;  // true = cooldown/timeout → allow by default (fail-open)
}

/** Ritorna true se il tool è soggetto all'efficiency critic. */
export function isEfficiencyCriticTool(name: string): boolean {
  return _CRITIC_TOOLS.has(name);
}

/**
 * Esegue il check LLM leggero (callWithFallback non-streaming).
 *
 * @param name        - Nome del tool
 * @param args        - Argomenti del tool
 * @param goal        - Messaggio utente / obiettivo corrente (lastUserMsg)
 * @param recentSteps - Stringa compatta degli step recenti (ultimi 3)
 */
export async function runEfficiencyCritic(
  name: string,
  args:        Record<string, unknown>,
  goal:        string,
  recentSteps: string,
): Promise<EfficiencyCriticResult> {
  // Cooldown guard — evita over-firing sul free tier
  if (Date.now() - _lastFiredAt < _COOLDOWN_MS) {
    return { proceed: true, reason: "cooldown", skipped: true };
  }
  _lastFiredAt = Date.now();

  const _argsPreview = JSON.stringify(args).slice(0, 200);

  const _msgs: ChatMessage[] = [
    {
      role: "system",
      content:
        "Sei un critic che valuta se un'azione ad alto impatto è necessaria ora. " +
        'Rispondi SOLO con JSON valido: {"ok":true} o {"ok":false,"reason":"..."}. ' +
        "La reason deve essere max 15 parole. Nessun altro testo.",
    },
    {
      role: "user",
      content:
        `Obiettivo: "${goal.slice(0, 150)}"\n` +
        `Step recenti: ${recentSteps.slice(0, 300) || "nessuno"}\n\n` +
        `Tool da eseguire: ${name}\n` +
        `Args: ${_argsPreview}\n\n` +
        "Questa azione è necessaria e corretta adesso per raggiungere l'obiettivo?",
    },
  ];

  // iOS-safe AbortController + setTimeout (AbortSignal.timeout non su iOS < 16.4)
  const _ctrl = new AbortController();
  const _tid  = setTimeout(() => _ctrl.abort(), _TIMEOUT_MS);

  try {
    const _hfToken = getHFToken() ?? "";

    const _resp = await callWithFallback(
      _hfToken,
      {
        messages:    _msgs,
        stream:      false as const,
        max_tokens:  _MAX_TOKENS,
        temperature: 0,
      },
      _ctrl.signal,
    );
    clearTimeout(_tid);

    if (!_resp.ok) {
      return { proceed: true, reason: "api_error", skipped: true };
    }

    const _data = await _resp.json() as {
      choices?: Array<{ message?: { content?: string } }>;
    };
    const _raw = _data.choices?.[0]?.message?.content ?? "";

    // indexOf/lastIndexOf — immune a brace annidati in string values
    const _fb = _raw.indexOf('{');
    const _lb = _raw.lastIndexOf('}');
    if (_fb === -1 || _lb <= _fb) {
      return { proceed: true, reason: "parse_error", skipped: true };
    }

    const _parsed = JSON.parse(_raw.slice(_fb, _lb + 1)) as {
      ok:      boolean;
      reason?: string;
    };

    if (_parsed.ok === false) {
      const _reason = (_parsed.reason ?? "azione non necessaria al momento").slice(0, 120);
      return { proceed: false, reason: _reason, skipped: false };
    }
    return { proceed: true, reason: "ok", skipped: false };

  } catch {
    clearTimeout(_tid);
    // Fail-open: su timeout/errore il tool viene eseguito normalmente
    return { proceed: true, reason: "timeout_or_error", skipped: true };
  }
}