File size: 9,760 Bytes
03e5649
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""unified_loop_delegate.py β€” DelegateMixin: debug riflessivo, replan, delega in-loop.

Estratto da unified_loop.py per ridurre il file principale.

Contiene:
  _reflective_debug(goal, errors):   BGAP-GUARD diagnosi breve da errori tool
  _budget_replan_check(state, step): BGAP-1 replan probabilistico su budget critico
  _DELEGATE_RESEARCH_RE:             regex riconoscimento sub-goal tipo ricerca
  _run_in_loop_delegate(sub_goal):   GAP-1 micro-agente specializzato in-loop

Invariante B1: nessun corpo duplicato con unified_loop.py.
MRO garantisce che DelegateMixin._budget_replan_check sovrascriva HelpersMixin
(DelegateMixin precede HelpersMixin nella lista basi di UnifiedAgentLoop).
"""
from __future__ import annotations

import asyncio
import logging
import re
from typing import Any

from agents.unified_loop_types import StepCallback, UnifiedLoopState, _maybe_await

_logger = logging.getLogger("agente_ai")


class DelegateMixin:
    async def _reflective_debug(
        self, goal: str = "", errors: Any = None, **kwargs: Any
    ) -> str:
        """Reflective debug: analizza errori e propone diagnosi in max 2 frasi.
        Chiamato dopo tool failures per arricchire state.context con ipotesi fix.
        Fail-open: non blocca mai il loop in caso di errore LLM."""
        try:
            _ctx = f"Goal: {str(goal)[:200]}\nErrori: {'; '.join(str(e)[:300] for e in (errors if isinstance(errors, list) else [errors])[:3])}"  # S573: 150β†’300
            _fast = self._get_fast_llm()
            _diag = await asyncio.wait_for(
                _fast.chat([{"role": "user", "content": f"Diagnosi breve (max 2 frasi):\n{_ctx}"}], max_tokens=300),  # S586: 120->180->300
                timeout=5.0,
            )
            return (str(_diag) if _diag else "").strip()[:300]
        except Exception:
            pass  # fail-open
        return ""

    # ── BGAP-1: Probabilistic Re-planning Trigger ────────────────────────────
    async def _budget_replan_check(
        self, state: Any, step_count: int, on_step: Any = None
    ) -> str:
        """BGAP-1: probabilistic re-planning trigger.
        Guards: skip se _n_err < 2 OR _budget_ratio < 0.6.
        Usa _get_fast_llm() con max_tokens=120. Fail-open."""
        _n_err = len(state.errors) if getattr(state, 'errors', None) else 0
        if _n_err < 2:
            return ''
        _budget_ratio = step_count / max(state.max_steps, 1)
        if _budget_ratio < 0.6:
            return ''
        # dedup guard [GAP-1-REPLAN]: skip se giΓ  replanned in questo loop
        if '[GAP-1-REPLAN]' in (state.context or ''):
            return ''
        try:
            _fast_llm = self._get_fast_llm()
            _prompt = (
                f'Task ha avuto {_n_err} errori e usato {_budget_ratio:.0%} del budget. '
                f'Suggerisci UN approccio alternativo in max 2 frasi. Goal: {state.goal[:500]}'  # S597: 200->300->500
            )
            _hint = await asyncio.wait_for(
                _fast_llm.chat([{'role': 'user', 'content': _prompt}], max_tokens=120),
                timeout=5.0,
            )
            return (str(_hint) if _hint else '').strip()[:200]
        except Exception:
            pass  # fail-open totale
        return ''

    # ── GAP-1: Delega Dinamica In-Loop ─────────────────────────────────────
    _DELEGATE_RESEARCH_RE = re.compile(
        r'\b(cerca|research|trova|web|url|leggi|analisi|analizza|documenta|'
        r'news|notizie|fetch|scrape|pagina|sito|http)\b',
        re.IGNORECASE,
    )

    async def _run_in_loop_delegate(self, sub_goal: str, timeout: float = 40.0) -> dict:
        """GAP-1: Delega Dinamica In-Loop.
        Lancia un micro-agente specializzato per sub_goal DURANTE il loop principale.
        Architettura:
          - Stesso executor del parent  β†’ accesso ai tool reali (write_file, run_python, ...)
          - LLM selezionato per ruolo   β†’ RESEARCHER, CODER o REASONER in base al goal
          - _is_delegate_child = True   β†’ blocca ricorsione (max 1 livello di delega)
          - max_steps = 4               β†’ micro-agente leggero, non un loop completo
          - output troncato a 4000 chars β†’ evita context-window explosion nel parent
        """
        # P18: defensive anti-recursion guard at entry point
        if getattr(self, '_is_delegate_child', False):
            _logger.debug("[delegate] anti-recursion guard triggered at _run_in_loop_delegate entry")
            return {"output": "[DELEGATE] Ricorsione bloccata: _is_delegate_child=True.", "steps": [], "goal_met": False}
        try:
            from models.role_router import RoleRouter as _RR_d, Role as _Role_d
            # Seleziona LLM specializzato in base al tipo di sotto-obiettivo
            if self._DELEGATE_RESEARCH_RE.search(sub_goal[:300]):
                _sub_llm = _RR_d.get_client(_Role_d.RESEARCHER)   # Gemini 2.5-flash
            elif self._CODE_RE.search(sub_goal[:300]):
                _sub_llm = _RR_d.get_client(_Role_d.CODER)        # Llama 4 Scout
            else:
                _sub_llm = _RR_d.get_client(_Role_d.REASONER)     # Cerebras 120B
        except Exception:
            _sub_llm = self.llm  # fallback: usa LLM del parent

        # Crea loop figlio: stessi executor/planner/memory, LLM specializzato
        _sub_loop = UnifiedAgentLoop(
            llm_client=_sub_llm,
            planner=self.planner,
            executor=self.executor,
            critic=None,    # no critic β€” micro-agente leggero
            memory=self.memory,
            verifier=None,  # no verifier β€” massima velocitΓ 
        )
        # Anti-ricorsione: il figlio non puΓ² delegare ulteriormente
        _sub_loop._is_delegate_child = True
        # Propaga session_id per isolare sandbox backend-exec
        _sub_loop._run_task_id = self._run_task_id + "_d"
        # GAP-6: condividi dict mutabile _session_files con il parent loop
        # Prima: delegate inizializzava _session_files={} -> file scritti non visibili al parent
        # Ora: stessa referenza -> parent vede automaticamente tutti i file scritti dal delegate
        _sub_loop._session_files = self._session_files

        # P17-F1: buffer output parziale via on_step β€” sopravvive al timeout
        _partial_steps: list[dict] = []
        async def _capture_partial(step: dict) -> None:
            if step.get("output") or step.get("explanation"):
                _partial_steps.append(step)

        try:
            _res = await asyncio.wait_for(
                _sub_loop.run(sub_goal, max_steps=4, on_step=_capture_partial),
                timeout=timeout,
            )
            _out = (_res.get("output") or "")[:4000]
            _logger.info(
                "GAP-1 delegate OK [%s] steps=%d: %s",
                _res.get("engine", "?"), len(_res.get("steps", [])), sub_goal[:60],
            )
            return {
                "success": _res.get("success", False),
                "output":  _out,
                "engine":  _res.get("engine", "delegate"),
                "steps":   len(_res.get("steps", [])),
            }
        except asyncio.TimeoutError:
            # P17-F1: esponi stato parziale invece di stringa vuota
            # _session_files giΓ  condiviso con parent β†’ parent vede file scritti
            _partial_files = list(getattr(_sub_loop, "_session_files", {}).keys())
            _partial_out = " ".join(
                (s.get("output") or s.get("explanation") or "")[:300]
                for s in _partial_steps[-3:]
            ).strip()[:1500]
            _logger.warning(
                "GAP-1 delegate timeout (%.0fs, %d steps, %d files): %s",
                timeout, len(_partial_steps), len(_partial_files), sub_goal[:60],
            )
            # S-PARTIAL: emetti evento SSE partial_output al frontend PRIMA di restituire
            # cosΓ¬ l'utente vede il chip "⚠ output parziale β€” riprendo" in tempo reale
            if on_step:
                await _maybe_await(on_step({
                    "event":          "partial_output",
                    "action":         "partial_output",
                    "visibility":     "progress",
                    "partial":        True,
                    "steps_done":     len(_partial_steps),
                    "partial_files":  _partial_files,
                    "partial_output": _partial_out,
                    "output":         _partial_out,
                    "explanation":    f"Output parziale dopo {timeout:.0f}s β€” l'agente sta recuperando",
                    "status":         "warning",
                }))
            return {
                "success":       False,
                "output":        _partial_out,
                "error":         f"delegate timeout ({timeout:.0f}s) β€” risultato parziale",
                "partial":       True,
                "partial_files": _partial_files,
                "steps_done":    len(_partial_steps),
            }
        except Exception as _de:
            _logger.warning("GAP-1 delegate error: %s", _de)
            return {"success": False, "output": "", "error": str(_de)[:200]}

    # Ҕ€Ò”€ S362: Role routing helpers Ҕ€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€

    # S427: ampliato con verbi IT/EN mancanti + framework/pattern aggiuntivi.
    # Stesso set di goal_verifier._CODE_RE + keyword tecnologiche per routing CODER LLM.