Pulka commited on
Commit
f6f29fe
·
verified ·
1 Parent(s): 0d1f073

sync: 40 changed, 0 deleted — 9b07012a (2026-06-20 18:00)

Browse files
agents/dynamic_replanner.py CHANGED
@@ -78,20 +78,36 @@ async def replan(
78
  failures_str = "\n".join(exec_warn[-3:]) if exec_warn else "nessun dettaglio"
79
  done_str = ", ".join(exec_done[-5:]) if exec_done else "nessuno"
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  enriched_goal = (
82
  f"{original_goal}\n\n"
83
- f"[CONTESTO RE-PLAN tentativo precedente fallito]\n"
84
- f"Subtask già completati (NON ripetere): {done_str}.\n"
85
  f"Problemi riscontrati:\n{failures_str}\n"
86
  )
 
 
87
  if error_context:
88
  enriched_goal += f"Analisi errore: {error_context[:300]}\n"
 
89
  enriched_goal += (
90
  "[ISTRUZIONE] Genera un piano ALTERNATIVO che eviti gli stessi problemi. "
 
91
  "Usa approcci diversi per i subtask falliti. "
92
  "Se un tool ha fallito, usa un tool alternativo."
93
  )
94
-
95
  try:
96
  new_plan = await asyncio.wait_for(
97
  planner.create_plan(enriched_goal), # type: ignore[attr-defined]
 
78
  failures_str = "\n".join(exec_warn[-3:]) if exec_warn else "nessun dettaglio"
79
  done_str = ", ".join(exec_done[-5:]) if exec_done else "nessuno"
80
 
81
+ # P16-B6: estrai tool/approcci falliti — guida il replanner a evitarli
82
+ # P18: rimosso import re lazy — usa re module-level (già importato riga 20)
83
+ _tool_fails: list[str] = []
84
+ for _w in exec_warn[-5:]:
85
+ _m = re.search(
86
+ r"(web_search|run_python|write_file|read_file|web_fetch|"
87
+ r"trigger_webhook|pip_install|shell_exec|delegate)\w*",
88
+ _w, re.IGNORECASE,
89
+ )
90
+ if _m:
91
+ _tool_fails.append(_m.group(0))
92
+ _avoid_str = ", ".join(set(_tool_fails)) if _tool_fails else ""
93
+
94
  enriched_goal = (
95
  f"{original_goal}\n\n"
96
+ f"[CONTESTO RE-PLAN \u2014 tentativo precedente fallito]\n"
97
+ f"Subtask gi\u00e0 completati (NON ripetere): {done_str}.\n"
98
  f"Problemi riscontrati:\n{failures_str}\n"
99
  )
100
+ if _avoid_str:
101
+ enriched_goal += f"Tool che hanno fallito (usa ALTERNATIVE): {_avoid_str}.\n"
102
  if error_context:
103
  enriched_goal += f"Analisi errore: {error_context[:300]}\n"
104
+ _avoid_hint = f"Evita: {_avoid_str}. " if _avoid_str else ""
105
  enriched_goal += (
106
  "[ISTRUZIONE] Genera un piano ALTERNATIVO che eviti gli stessi problemi. "
107
+ f"{_avoid_hint}"
108
  "Usa approcci diversi per i subtask falliti. "
109
  "Se un tool ha fallito, usa un tool alternativo."
110
  )
 
111
  try:
112
  new_plan = await asyncio.wait_for(
113
  planner.create_plan(enriched_goal), # type: ignore[attr-defined]
agents/reasoning_core.py CHANGED
@@ -190,12 +190,31 @@ Regole:
190
 
191
  return _base_prompt + _ctx_section
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  def _parse(self, raw: str) -> ReasoningResult:
194
  try:
195
- m = re.search(r'\{[\s\S]+\}', raw)
196
- data = json.loads(m.group()) if m else {}
197
  except Exception:
198
- return ReasoningResult(action="continue", steps=[], reason="Parsing error fallback", confidence=0.2)
199
 
200
  return ReasoningResult(
201
  action=data.get("action", "continue"),
 
190
 
191
  return _base_prompt + _ctx_section
192
 
193
+ @staticmethod
194
+ def _extract_json(raw: str) -> str | None:
195
+ """P16-B3: depth-counting bilanciato — sostituisce regex greedy r'{[\s\S]+}'
196
+ che su JSON nested (es. patch con oggetti interni) estraeva dal primo { all'ULTIMO }
197
+ producendo JSON malformato → action='continue' per default → agente in loop.
198
+ Pattern identico a safeJsonParse.ts già in produzione sul frontend."""
199
+ depth = 0
200
+ start = -1
201
+ for i, ch in enumerate(raw):
202
+ if ch == '{':
203
+ if depth == 0:
204
+ start = i
205
+ depth += 1
206
+ elif ch == '}':
207
+ depth -= 1
208
+ if depth == 0 and start != -1:
209
+ return raw[start:i + 1]
210
+ return None
211
+
212
  def _parse(self, raw: str) -> ReasoningResult:
213
  try:
214
+ candidate = self._extract_json(raw)
215
+ data = json.loads(candidate) if candidate else {}
216
  except Exception:
217
+ return ReasoningResult(action='continue', steps=[], reason='Parsing error fallback', confidence=0.2)
218
 
219
  return ReasoningResult(
220
  action=data.get("action", "continue"),
agents/skill_tracker.py CHANGED
@@ -358,3 +358,43 @@ _skill_tracker = SkillTracker()
358
  def get_skill_tracker() -> SkillTracker:
359
  """Restituisce il singleton SkillTracker. Thread-safe in CPython (GIL)."""
360
  return _skill_tracker
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  def get_skill_tracker() -> SkillTracker:
359
  """Restituisce il singleton SkillTracker. Thread-safe in CPython (GIL)."""
360
  return _skill_tracker
361
+
362
+
363
+ # ─── P17-B2: FastAPI router per sync frontend ─────────────────────────────────
364
+ # Endpoint REST che permette al frontend (Dexie) di leggere e scrivere skill stats.
365
+ # Montato in backend/main.py tramite _on_startup() se importato.
366
+
367
+ try:
368
+ from fastapi import APIRouter as _APIRouter
369
+ from pydantic import BaseModel as _BM
370
+
371
+ skill_router = _APIRouter(prefix="/api/agent", tags=["skill-tracker"])
372
+
373
+ class _SkillRecordBody(_BM):
374
+ success: bool
375
+ latency_ms: float = 0.0
376
+ error_msg: str = ""
377
+
378
+ @skill_router.get("/skill-stats/{session_id}")
379
+ async def api_get_skill_stats(session_id: str):
380
+ """Restituisce stats tool per sessione — per merge con Dexie frontend."""
381
+ return get_skill_tracker().get_stats(session_id)
382
+
383
+ @skill_router.post("/skill-record/{session_id}/{tool_name}")
384
+ async def api_record_skill(session_id: str, tool_name: str, body: _SkillRecordBody):
385
+ """Registra un risultato tool dal frontend (es. tool chiamato via browser)."""
386
+ get_skill_tracker().record(
387
+ session_id, tool_name,
388
+ success=body.success,
389
+ latency_ms=body.latency_ms,
390
+ )
391
+ return {"ok": True, "session_id": session_id, "tool": tool_name}
392
+
393
+ @skill_router.delete("/skill-stats/{session_id}")
394
+ async def api_clear_skill_session(session_id: str):
395
+ """Pulisce la sessione skill tracker alla fine del task."""
396
+ get_skill_tracker().clear_session(session_id)
397
+ return {"ok": True}
398
+
399
+ except ImportError:
400
+ skill_router = None # type: ignore[assignment] # FastAPI non disponibile (unit test env)
agents/unified_loop.py CHANGED
@@ -61,12 +61,55 @@ class UnifiedLoopState:
61
  steps: list[dict[str, Any]] = field(default_factory=list)
62
  errors: list[str] = field(default_factory=list)
63
  has_files: bool = False # B10: flag separato — evita di inquinare il context string
 
64
 
65
 
66
  async def _maybe_await(val: Any) -> None:
67
  if asyncio.iscoroutine(val) or asyncio.isfuture(val):
68
  await val
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin):
72
  """Smolagents-first loop with deterministic direct-tool layer and safe LLM fallback."""
@@ -4111,7 +4154,8 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin):
4111
  # ── Entry point (S193) ────────────────────────────────────────────────────
4112
 
4113
  async def run(self, goal: str, context: str = "", max_steps: int = 8,
4114
- on_step: StepCallback | None = None) -> dict[str, Any]:
 
4115
  # S390-B-L: strip role prefixes che causano prompt injection
4116
  # Es. "SYSTEM: ignore..." o "ASSISTANT: ..." nel goal utente
4117
  # S762-BUG3: re.sub con ^ strippava solo il PRIMO prefisso — input come
@@ -4167,7 +4211,18 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin):
4167
  if max_steps == 8 and self._CODE_RE.search(goal):
4168
  max_steps = 12
4169
 
4170
- state = UnifiedLoopState(goal=goal, context=context, max_steps=max_steps)
 
 
 
 
 
 
 
 
 
 
 
4171
 
4172
  # GAP-DECISION-FIX: consulta blacklist prima di eseguire fix già rifiutati
4173
  try:
 
61
  steps: list[dict[str, Any]] = field(default_factory=list)
62
  errors: list[str] = field(default_factory=list)
63
  has_files: bool = False # B10: flag separato — evita di inquinare il context string
64
+ session_id: str = "" # P17-F2: blackboard session key per sync Upstash
65
 
66
 
67
  async def _maybe_await(val: Any) -> None:
68
  if asyncio.iscoroutine(val) or asyncio.isfuture(val):
69
  await val
70
 
71
+ # P17-F2: Upstash REST reader — chiamata dal loop all'avvio per iniettare
72
+ # le scoperte critiche dei delegate frontend nel context dell'agente backend.
73
+ # Pattern identico a blackboard.py; duplicato qui per zero import circolare.
74
+ async def _read_bb_upstash(session_id: str) -> str:
75
+ """Legge le entry critiche dal blackboard Upstash. Ritorna '' se non disponibile."""
76
+ _url = os.getenv("UPSTASH_REDIS_REST_URL", "")
77
+ _token = os.getenv("UPSTASH_REDIS_REST_TOKEN", "")
78
+ if not _url or not _token or not session_id:
79
+ return ""
80
+ import json as _bb_json
81
+ import httpx as _bb_httpx
82
+ try:
83
+ async with _bb_httpx.AsyncClient(timeout=1.5) as _c:
84
+ _hdr = {"Authorization": f"Bearer {_token}", "Content-Type": "application/json"}
85
+ _sr = await _c.post(
86
+ _url,
87
+ json=["SCAN", "0", "MATCH", f"bb:{session_id}:*", "COUNT", "50"],
88
+ headers=_hdr,
89
+ )
90
+ _sd = _sr.json() if _sr.is_success else {}
91
+ _sc = _sd.get("result", [])
92
+ _keys = _sc[1] if (isinstance(_sc, list) and len(_sc) >= 2 and isinstance(_sc[1], list)) else []
93
+ if not _keys:
94
+ return ""
95
+ _mr = await _c.post(_url, json=["MGET"] + _keys, headers=_hdr)
96
+ _md = _mr.json() if _mr.is_success else {}
97
+ _out = []
98
+ for _v in _md.get("result", []):
99
+ if _v:
100
+ try:
101
+ _e = _bb_json.loads(_v)
102
+ if _e.get("severity") == "critical":
103
+ _agid = _e.get("agentId", "")
104
+ _key = _e.get("key", "")
105
+ _val = str(_e.get("value", ""))[:200]
106
+ _out.append(f"- [{_agid}] {_key}: {_val}")
107
+ except Exception:
108
+ pass
109
+ return ("SCOPERTE CRITICHE DAI DELEGATI:\n" + "\n".join(_out)) if _out else ""
110
+ except Exception:
111
+ return ""
112
+
113
 
114
  class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin):
115
  """Smolagents-first loop with deterministic direct-tool layer and safe LLM fallback."""
 
4154
  # ── Entry point (S193) ────────────────────────────────────────────────────
4155
 
4156
  async def run(self, goal: str, context: str = "", max_steps: int = 8,
4157
+ on_step: StepCallback | None = None,
4158
+ session_id: str = "") -> dict[str, Any]:
4159
  # S390-B-L: strip role prefixes che causano prompt injection
4160
  # Es. "SYSTEM: ignore..." o "ASSISTANT: ..." nel goal utente
4161
  # S762-BUG3: re.sub con ^ strippava solo il PRIMO prefisso — input come
 
4211
  if max_steps == 8 and self._CODE_RE.search(goal):
4212
  max_steps = 12
4213
 
4214
+ state = UnifiedLoopState(goal=goal, context=context, max_steps=max_steps, session_id=session_id)
4215
+
4216
+ # P17-F2: inject blackboard critical entries at loop start.
4217
+ # I delegate frontend scrivono su Upstash; il loop legge e inietta nel context.
4218
+ if session_id:
4219
+ try:
4220
+ _bb_ctx = await _read_bb_upstash(session_id)
4221
+ if _bb_ctx:
4222
+ state.context = (state.context + "\n\n" + _bb_ctx).strip() if state.context else _bb_ctx
4223
+ _logger.info("[P17-F2] BB ctx injected (%d chars)", len(_bb_ctx))
4224
+ except Exception as _bb_exc:
4225
+ _logger.debug("[P17-F2] BB read silenced: %s", _bb_exc)
4226
 
4227
  # GAP-DECISION-FIX: consulta blacklist prima di eseguire fix già rifiutati
4228
  try:
agents/unified_loop_prompts.py CHANGED
@@ -212,6 +212,47 @@ class PromptBuilderMixin:
212
  "• AUTOMAZIONE: se l'utente fa lo stesso task 3+ volte → suggerisci di schedularlo\n"
213
  )
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  # ── S200: Context-aware rule injection ──────────────────────────────────────
216
  # Seleziona solo le regole rilevanti per il task corrente.
217
  # Con llama-3.1-8b-instant (8K context), mettere tutto nel system prompt
@@ -1111,8 +1152,57 @@ class PromptBuilderMixin:
1111
  "[3 modifiche prioritizzate in ordine di impatto — chiedi conferma prima di applicarle]"
1112
  ),
1113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  ]
1115
 
 
 
 
 
 
 
 
 
 
 
 
 
1116
  def _pick_context_rules(self, goal: str) -> str:
1117
  """Seleziona regole contestuali basate sul task. Max 3 per non saturare il contesto."""
1118
  goal_lower = goal.lower()
@@ -1153,8 +1243,10 @@ class PromptBuilderMixin:
1153
  def _build_messages(self, state: UnifiedLoopState, tool_results: str = "",
1154
  tool_exec_successes: int = 0, tool_exec_errors: int = 0,
1155
  session_files: "dict[str, str] | None" = None) -> list[dict]:
 
 
1156
  # S197: estrai code-block grandi in file virtuali per evitare timeout provider
1157
- goal_display, files_ctx = self._compress_goal(state.goal)
1158
  mem_hint = "Tieni conto della memoria per preferenze e contesto utente.\n" if self.memory else ""
1159
  # GAP-5: tronca tool_results a max 6000 chars per evitare context explosion su Groq 128k.
1160
  # Header 500 chars (dichiarazione tool) + tail 4000 chars (risultati recenti).
@@ -1212,6 +1304,10 @@ class PromptBuilderMixin:
1212
  context_part = f"Contesto sessione: {_raw_ctx}\n\n" if _raw_ctx and _raw_ctx != "nessuno" else ""
1213
  files_part = f"{files_ctx}\n\n" if files_ctx else ""
1214
  # S200: regole contestuali iniettate ALLA FINE del user message (recency bias)
 
 
 
 
1215
  ctx_rules = self._pick_context_rules(goal_display)
1216
  # S375: format directive — garantisce coerenza di formato anche sul path backend
1217
  fmt_directive = self._classify_format_directive(state.goal)
@@ -1284,7 +1380,7 @@ class PromptBuilderMixin:
1284
  state.has_files = True
1285
  # S375: format directive iniettata nel system prompt (non nel user msg)
1286
  # per evitare confusione con i dati tool nel user content
1287
- system_with_fmt = f"{self._SYSTEM_IDENTITY}\n\n{fmt_directive}"
1288
  return [
1289
  {"role": "system", "content": system_with_fmt},
1290
  {"role": "user", "content": user_content},
 
212
  "• AUTOMAZIONE: se l'utente fa lo stesso task 3+ volte → suggerisci di schedularlo\n"
213
  )
214
 
215
+ # ── P19-F1: Expertise Personas — /persona RESEARCHER|CODER|REASONER ─────────
216
+ # Attivazione: il goal inizia con "/persona <NAME>" (case-insensitive).
217
+ # Il blocco viene anteposto a _SYSTEM_IDENTITY in _build_messages().
218
+
219
+ _PERSONA_BLOCKS: dict[str, str] = {
220
+ "RESEARCHER": (
221
+ "=== MODALITÀ RESEARCHER (P19-F1) ===\n"
222
+ "Sei un ricercatore esperto. In questa sessione:\n"
223
+ "• Ogni affermazione deve citare la fonte (URL, paper, documentazione ufficiale)\n"
224
+ "• Sintetizza sempre almeno 3 fonti prima di dare una conclusione\n"
225
+ "• Distingui esplicitamente: FATTO VERIFICATO / OPINIONE / IPOTESI\n"
226
+ "• Per ogni topic, riporta anche i contro-argomenti principali\n"
227
+ "• Usa web_search per ogni claim che potrebbe essere cambiato dopo il tuo training\n"
228
+ "• Struttura obbligatoria: ## Sintesi → ## Fonti → ## Punti controversi → ## Raccomandazione\n"
229
+ "• Aggiungi sempre: 'Ultimo aggiornamento disponibile: [data da fonte]'\n"
230
+ ),
231
+ "CODER": (
232
+ "=== MODALITÀ CODER (P19-F1) ===\n"
233
+ "Sei un senior software engineer. In questa sessione:\n"
234
+ "• Ogni snippet: TypeScript strict / Python typed / con gestione errori esplicita\n"
235
+ "• Applica TDD: scrivi prima i test, poi l'implementazione\n"
236
+ "• Struttura modulare: funzioni pure, single-responsibility, DI-friendly\n"
237
+ "• Commenti solo per il WHY, mai per il WHAT (codice self-documenting)\n"
238
+ "• Per ogni soluzione: complessità Big-O, edge cases, dipendenze esplicite\n"
239
+ "• Se rilevi codice smell: nomina il pattern (God Object, Shotgun Surgery, ecc.)\n"
240
+ "• Struttura obbligatoria: ## Interfaccia pubblica → ## Implementazione → ## Test → ## Edge cases\n"
241
+ "• Aggiungi sempre: 'Stack: [linguaggio] [runtime] [framework]'\n"
242
+ ),
243
+ "REASONER": (
244
+ "=== MODALITÀ REASONER (P19-F1) ===\n"
245
+ "Sei un ragionatore logico sistematico. In questa sessione:\n"
246
+ "• Esponi il ragionamento passo per passo — ZERO conclusioni saltate\n"
247
+ "• Per ogni problema: enumera le assunzioni esplicite e implicite\n"
248
+ "• Usa sillogismi e alberi decisionali quando la logica è ramificata\n"
249
+ "• Identifica i presupposti contestabili con [ASSUNZIONE]\n"
250
+ "• Per problemi ambigui: mostra 2-3 interpretazioni prima di scegliere\n"
251
+ "• Struttura obbligatoria: ## Premesse → ## Ragionamento → ## Conclusione → ## Verifica\n"
252
+ "• Aggiungi sempre la confidence: [ALTA/MEDIA/BASSA] con motivazione\n"
253
+ ),
254
+ }
255
+
256
  # ── S200: Context-aware rule injection ──────────────────────────────────────
257
  # Seleziona solo le regole rilevanti per il task corrente.
258
  # Con llama-3.1-8b-instant (8K context), mettere tutto nel system prompt
 
1152
  "[3 modifiche prioritizzate in ordine di impatto — chiedi conferma prima di applicarle]"
1153
  ),
1154
 
1155
+ # ── P19-F2: Webhook/n8n/Pipedream/Zapier recipe hints ──────────────────
1156
+ # Trigger: utente chiede automazione, webhook, notifiche o integrazioni esterne.
1157
+ # Fornisce URL pattern e payload d'esempio per trigger_webhook.
1158
+ (
1159
+ ["n8n", "pipedream", "zapier", "make.com", "webhook", "notifica",
1160
+ "automazione esterna", "automation", "notificami", "invia notifica",
1161
+ "zap ", "workflow esterno", "trigger esterno", "discord webhook",
1162
+ "slack webhook", "integromat", "integrazione esterna"],
1163
+ "WEBHOOK AUTOMATION (P19-F2):\n"
1164
+ "Usa il tool trigger_webhook per inviare dati a qualsiasi servizio esterno.\n"
1165
+ "\n"
1166
+ "PATTERN n8n (webhook node):\n"
1167
+ " url: 'https://your-n8n.app.n8n.cloud/webhook/<uuid>'\n"
1168
+ " payload: {'event': 'task_done', 'data': {...}, 'agent': 'agente-ai'}\n"
1169
+ " method: POST (default)\n"
1170
+ "\n"
1171
+ "PATTERN Pipedream:\n"
1172
+ " url: 'https://eo<id>.m.pipedream.net'\n"
1173
+ " payload: {'source': 'agente-ai', 'result': '...', 'ts': '<iso timestamp>'}\n"
1174
+ "\n"
1175
+ "PATTERN Zapier (Catch Hook):\n"
1176
+ " url: 'https://hooks.zapier.com/hooks/catch/<id>/<hash>/'\n"
1177
+ " payload: {'subject': '...', 'body': '...'}\n"
1178
+ "\n"
1179
+ "PATTERN Discord Webhook:\n"
1180
+ " url: 'https://discord.com/api/webhooks/<id>/<token>'\n"
1181
+ " payload: {'content': 'Messaggio...', 'username': 'Agente AI'}\n"
1182
+ " headers: {'Content-Type': 'application/json'}\n"
1183
+ "\n"
1184
+ "REGOLE trigger_webhook:\n"
1185
+ " - Richiedi sempre l'URL all'utente se non fornito\n"
1186
+ " - Timeout max 10s — non blocca il loop\n"
1187
+ " - Payload max 32KB — tronca output lunghi prima di inviarli\n"
1188
+ " - Controlla ok=True nella risposta e logga il body\n"
1189
+ " - Mai esporre dati sensibili (token, password) nel payload"
1190
+ ),
1191
+
1192
  ]
1193
 
1194
+ @staticmethod
1195
+ def _extract_persona(goal: str) -> "tuple[str | None, str]":
1196
+ """P19-F1: Estrae persona dal goal se inizia con /persona <NAME>.
1197
+ Ritorna (persona_name | None, goal_senza_prefisso).
1198
+ """
1199
+ import re as _re
1200
+ _m = _re.match(r'^/persona\s+(RESEARCHER|CODER|REASONER)\b', goal.strip(), _re.IGNORECASE)
1201
+ if _m:
1202
+ clean = goal.strip()[_m.end():].strip()
1203
+ return _m.group(1).upper(), clean if clean else goal.strip()
1204
+ return None, goal
1205
+
1206
  def _pick_context_rules(self, goal: str) -> str:
1207
  """Seleziona regole contestuali basate sul task. Max 3 per non saturare il contesto."""
1208
  goal_lower = goal.lower()
 
1243
  def _build_messages(self, state: UnifiedLoopState, tool_results: str = "",
1244
  tool_exec_successes: int = 0, tool_exec_errors: int = 0,
1245
  session_files: "dict[str, str] | None" = None) -> list[dict]:
1246
+ # P19-F1: estrai persona dal goal (/persona RESEARCHER|CODER|REASONER ...)
1247
+ _p19_persona, _p19_goal = self._extract_persona(state.goal)
1248
  # S197: estrai code-block grandi in file virtuali per evitare timeout provider
1249
+ goal_display, files_ctx = self._compress_goal(_p19_goal)
1250
  mem_hint = "Tieni conto della memoria per preferenze e contesto utente.\n" if self.memory else ""
1251
  # GAP-5: tronca tool_results a max 6000 chars per evitare context explosion su Groq 128k.
1252
  # Header 500 chars (dichiarazione tool) + tail 4000 chars (risultati recenti).
 
1304
  context_part = f"Contesto sessione: {_raw_ctx}\n\n" if _raw_ctx and _raw_ctx != "nessuno" else ""
1305
  files_part = f"{files_ctx}\n\n" if files_ctx else ""
1306
  # S200: regole contestuali iniettate ALLA FINE del user message (recency bias)
1307
+ # P19-F1: persona block se attivo
1308
+ _p19_persona_prefix = ""
1309
+ if _p19_persona and _p19_persona in self._PERSONA_BLOCKS:
1310
+ _p19_persona_prefix = self._PERSONA_BLOCKS[_p19_persona] + "\n\n"
1311
  ctx_rules = self._pick_context_rules(goal_display)
1312
  # S375: format directive — garantisce coerenza di formato anche sul path backend
1313
  fmt_directive = self._classify_format_directive(state.goal)
 
1380
  state.has_files = True
1381
  # S375: format directive iniettata nel system prompt (non nel user msg)
1382
  # per evitare confusione con i dati tool nel user content
1383
+ system_with_fmt = f"{_p19_persona_prefix}{self._SYSTEM_IDENTITY}\n\n{fmt_directive}"
1384
  return [
1385
  {"role": "system", "content": system_with_fmt},
1386
  {"role": "user", "content": user_content},
agents/unified_loop_tools.py CHANGED
@@ -854,7 +854,10 @@ class DirectToolsMixin:
854
  r"directory[\s_]tree|albero\s+(?:del\s+)?(?:progetto|directory)|"
855
  r"struttura\s+(?:del\s+)?progetto|elenca\s+(?:file|cartell[ae])|"
856
  r"cerca\s+nel\s+(?:codice|progetto)|grep\s+|file[\s_]search|"
857
- r"type[\s_]check|verifica\s+tipi|typescript\s+check|mypy\s+)\b",
 
 
 
858
  re.IGNORECASE,
859
  )
860
 
 
854
  r"directory[\s_]tree|albero\s+(?:del\s+)?(?:progetto|directory)|"
855
  r"struttura\s+(?:del\s+)?progetto|elenca\s+(?:file|cartell[ae])|"
856
  r"cerca\s+nel\s+(?:codice|progetto)|grep\s+|file[\s_]search|"
857
+ r"type[\s_]check|verifica\s+tipi|typescript\s+check|mypy\s+|"
858
+ # R9: webhook/call_api keywords — mancanti da _TOOL_NEEDED_RE
859
+ r"webhook|trigger\s+webhook|chiama\s+(?:il\s+)?webhook|send\s+webhook|"
860
+ r"call[\s_]api|chiama\s+api|http\s+(?:post|get|request)|zapier|n8n)\b",
861
  re.IGNORECASE,
862
  )
863
 
api/agent.py CHANGED
@@ -1,3 +1,4 @@
 
1
  """backend/api/agent.py — Agent tasks, SSE streaming, checkpoints, loops, kernel (S359, S369).
2
 
3
  S358: stream_agent_task() usa _loop_registry per evitare re-run al reconnect SSE.
@@ -196,6 +197,7 @@ async def agent_run_stream(body: ReasonLoopIn, request: Request):
196
  result = await loop.run(
197
  goal=body.goal, context=context_str,
198
  max_steps=body.max_steps, on_step=step_cb,
 
199
  )
200
  await queue.put({
201
  '__done__': True,
@@ -394,7 +396,7 @@ async def reason_loop(body: ReasonLoopIn):
394
  'action': step_data.get('action', ''),
395
  'output': str(step_data.get('output', ''))[:400], # S577: 200→400
396
  })
397
- result = await loop.run(goal=body.goal, context=context_str, max_steps=body.max_steps, on_step=_on_step)
398
  if isinstance(result, dict):
399
  output_text = result.get('output', '') or ''
400
  engine_used = result.get('engine', 'unknown')
@@ -527,6 +529,7 @@ async def create_agent_task(body: AgentTaskIn):
527
  'learning_hints': body.learning_hints, # S456-X4
528
  'resume_from_step': body.resume_from_step, # P16-F3: passo resume dalla coda
529
  'persona': body.persona, # P17-F5: expertise persona hint
 
530
  }
531
  # WRITE-AHEAD: persiste il task su Supabase immediatamente, prima del checkpoint
532
  # periodico (15-60s). Finestra di perdita per la fase di creazione → zero.
@@ -971,6 +974,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0):
971
  _tool_key_narr = _action.replace('executor:', '') if _action.startswith('executor:') else _action
972
  _narration = _STEP_NARRATIONS.get(_tool_key_narr,
973
  _action.replace('executor:', '').replace('_', ' ').capitalize())
 
 
974
  _sse('step_done', {
975
  'taskId': task_id,
976
  'step': {
@@ -979,6 +984,7 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0):
979
  'status': step_data.get('status', 'done'),
980
  'result': str(step_data.get('result', step_data.get('output', '')))[:500],
981
  'explanation': _narration, # S363-Blueprint: narrative field
 
982
  },
983
  })
984
  # TG-STEP: notifica step intermedio rilevante (fire-and-forget, rate-limited 30s)
@@ -1101,6 +1107,7 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0):
1101
  context=context_str,
1102
  max_steps=task.get('_resume_max_steps', task.get('max_steps', 8)), # AG-BUG-1: _resume_max mai definito in questo scope
1103
  on_step=step_cb,
 
1104
  )
1105
  _agent_tasks[task_id]['status'] = 'SUCCESS'
1106
  asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
 
1
+ 71753
2
  """backend/api/agent.py — Agent tasks, SSE streaming, checkpoints, loops, kernel (S359, S369).
3
 
4
  S358: stream_agent_task() usa _loop_registry per evitare re-run al reconnect SSE.
 
197
  result = await loop.run(
198
  goal=body.goal, context=context_str,
199
  max_steps=body.max_steps, on_step=step_cb,
200
+ session_id=getattr(body, "session_id", "") or "",
201
  )
202
  await queue.put({
203
  '__done__': True,
 
396
  'action': step_data.get('action', ''),
397
  'output': str(step_data.get('output', ''))[:400], # S577: 200→400
398
  })
399
+ result = await loop.run(goal=body.goal, context=context_str, max_steps=body.max_steps, on_step=_on_step, session_id=getattr(body, "session_id", "") or "")
400
  if isinstance(result, dict):
401
  output_text = result.get('output', '') or ''
402
  engine_used = result.get('engine', 'unknown')
 
529
  'learning_hints': body.learning_hints, # S456-X4
530
  'resume_from_step': body.resume_from_step, # P16-F3: passo resume dalla coda
531
  'persona': body.persona, # P17-F5: expertise persona hint
532
+ 'session_id': body.session_id or '', # P17-F2: BB session key (normalize None→'')
533
  }
534
  # WRITE-AHEAD: persiste il task su Supabase immediatamente, prima del checkpoint
535
  # periodico (15-60s). Finestra di perdita per la fase di creazione → zero.
 
974
  _tool_key_narr = _action.replace('executor:', '') if _action.startswith('executor:') else _action
975
  _narration = _STEP_NARRATIONS.get(_tool_key_narr,
976
  _action.replace('executor:', '').replace('_', ' ').capitalize())
977
+ # P16-B4: propaga 'truncated' dal loop (finish_reason==length) → frontend
978
+ _step_truncated = bool(step_data.get('truncated', False))
979
  _sse('step_done', {
980
  'taskId': task_id,
981
  'step': {
 
984
  'status': step_data.get('status', 'done'),
985
  'result': str(step_data.get('result', step_data.get('output', '')))[:500],
986
  'explanation': _narration, # S363-Blueprint: narrative field
987
+ 'truncated': _step_truncated, # P16-B4: segnala max_tokens raggiunto
988
  },
989
  })
990
  # TG-STEP: notifica step intermedio rilevante (fire-and-forget, rate-limited 30s)
 
1107
  context=context_str,
1108
  max_steps=task.get('_resume_max_steps', task.get('max_steps', 8)), # AG-BUG-1: _resume_max mai definito in questo scope
1109
  on_step=step_cb,
1110
+ session_id=task.get('session_id', '') or '',
1111
  )
1112
  _agent_tasks[task_id]['status'] = 'SUCCESS'
1113
  asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)