From f4b7148cf967c038e28d30356cd3f5203526280c Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 25 Sep 2026 13:32:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(diagnostic):=20Agent-Historie=20pro=20Agen?= =?UTF-8?q?t=20=E2=80=94=20Akkordeon=20je=20Nachricht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jede Host-Aktion wird als Historie-Eintrag festgehalten und in der Diagnostic pro User-Nachricht gruppiert aufklappbar dargestellt (mit Screenshot-Thumbnails). - Brain: trace_id+msg pro chat(); _dispatch_host als Wrapper um _dispatch_host_inner emittiert je Aktion einen Eintrag an POST /internal/agent-history (Datei-Pfad aus Screenshot-Text erkannt). - Bridge: /internal/agent-history persistiert nach /shared/config/agent_history/ .jsonl (Ringpuffer 200) + broadcastet agent_history; agent_history_query -> agent_history_list. - RVS: agent_history/_query/_list in ALLOWED_TYPES (sonst still verworfen). - Diagnostic-Server: relay browser<->RVS + /uploads/ Bild-Endpoint (nur /shared/uploads, kein Traversal). - index.html: 'Historie'-Button je Host, Modal mit Akkordeon (Gruppe=Nachricht, aufklappbar, Screenshot-Thumbs), Live-Refresh bei agent_history. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 48 ++++++++++++++++++- bridge/aria_bridge.py | 67 ++++++++++++++++++++++++++ diagnostic/index.html | 107 +++++++++++++++++++++++++++++++++++++++++- diagnostic/server.js | 22 +++++++++ rvs/server.js | 5 ++ 5 files changed, 247 insertions(+), 2 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 122fe11..a1674a3 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -2200,6 +2200,11 @@ class Agent: # Events vom letzten Turn weglassen self._pending_events = [] + # Trace fuer die Agent-Historie (Diagnostic gruppiert Host-Aktionen pro + # dieser einen User-Nachricht). Best-effort, nur Anzeige. + self._trace_id = os.urandom(6).hex() + self._trace_msg = user_message[:160] + # Projekt-Kontext pro Request statt aus globalem State active_project_id = (project_id or "").strip() active_project = projects_mod.get_project(active_project_id) if active_project_id else None @@ -2658,8 +2663,49 @@ class Agent: return f"FEHLER: Satellit/Bridge nicht erreichbar: {exc}" def _dispatch_host(self, name: str, arguments: dict) -> str: + """Wrapper um _dispatch_host_inner: fuehrt die Host-Aktion aus und schreibt + danach einen Historie-Eintrag an die Bridge (fuer die Diagnostic-Ansicht + 'was wurde pro Nachricht auf diesem Agent gemacht').""" + text = self._dispatch_host_inner(name, arguments) + try: + self._emit_agent_history(name, arguments, text) + except Exception: + pass + return text + + def _emit_agent_history(self, name: str, arguments: dict, text: str) -> None: + """Best-effort: ein Historie-Eintrag pro Host-Aktion an die Bridge.""" + if name == "host_list": + return + host = (arguments.get("host") or "").strip() + if not host: + return + ok = not text.lstrip().upper().startswith("FEHLER") + m = re.search(r"/shared/uploads/\S+?\.(?:png|jpg|jpeg|webp)", text) + body = { + "host": host, + "trace_id": getattr(self, "_trace_id", ""), + "msg": getattr(self, "_trace_msg", ""), + "tool": name, + "action": name[len("host_"):] if name.startswith("host_") else name, + "args": {k: v for k, v in arguments.items() if k != "host"}, + "ok": ok, + "summary": (text.strip().splitlines()[0] if text.strip() else "")[:200], + "file": m.group(0) if m else None, + "ts": int(time.time() * 1000), + } + try: + data = json.dumps(body).encode("utf-8") + req = urllib.request.Request(f"{BRIDGE_URL}/internal/agent-history", + data=data, method="POST", + headers={"Content-Type": "application/json"}) + urllib.request.urlopen(req, timeout=5).close() + except Exception: + pass + + def _dispatch_host_inner(self, name: str, arguments: dict) -> str: """host_list / host_exec / host_read / host_write / host_info / - host_screenshot — via Bridge (/internal/host*) → RVS → Host-Agent.""" + host_screenshot / host_ui_* — via Bridge (/internal/host*) → RVS → Host-Agent.""" import base64 as _b64 def _post(path: str, body: dict, timeout: float) -> dict: diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 3aa0813..ebd5130 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -3589,6 +3589,17 @@ class ARIABridge: self._hosts[hid]["last_seen"] = time.time() return + elif msg_type == "agent_history_query": + # Diagnostic fragt die gespeicherte Historie eines Agenten ab. + hid = (payload.get("host") or "").strip() + events = self._agent_history_read(hid) if hid else [] + await self._send_to_rvs({ + "type": "agent_history_list", + "payload": {"host": hid, "events": events}, + "timestamp": int(time.time() * 1000), + }) + return + elif msg_type == "worker_hello": iid = (payload.get("instanceId") or "").strip() if iid: @@ -4580,6 +4591,17 @@ class ARIABridge: timeout=float(data.get("timeout") or 60.0), ) await _send_response(writer, 200, result) + elif method == "POST" and path == "/internal/agent-history": + # Brain: ein Historie-Eintrag pro Host-Aktion (fuer die Diagnostic). + try: + data = json.loads(body.decode("utf-8", "ignore")) + except Exception as exc: + await _send_response(writer, 400, {"error": f"bad json: {exc}"}) + return + self._agent_history_append(data) + await self._send_to_rvs({"type": "agent_history", "payload": data, + "timestamp": int(time.time() * 1000)}) + await _send_response(writer, 200, {"ok": True}) elif method == "POST" and path == "/internal/satellite": # Brain-Tool: Discovery oder Command an einen Satelliten. # body: {op:'discover'|'command', satellite, device?, action?, params?} @@ -4929,6 +4951,51 @@ class ARIABridge: "online": (now - h.get("last_seen", 0)) < 300, } for h in self._hosts.values()] + _AGENT_HISTORY_DIR = "/shared/config/agent_history" + _AGENT_HISTORY_MAX = 200 + + def _agent_history_path(self, host: str) -> str: + safe = re.sub(r"[^a-zA-Z0-9_-]+", "-", host).strip("-") or "host" + return os.path.join(self._AGENT_HISTORY_DIR, f"{safe}.jsonl") + + def _agent_history_append(self, entry: dict) -> None: + """Haengt einen Historie-Eintrag an .jsonl (Ringpuffer).""" + host = (entry.get("host") or "").strip() + if not host: + return + try: + os.makedirs(self._AGENT_HISTORY_DIR, exist_ok=True) + path = self._agent_history_path(host) + lines = [] + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + lines.append(json.dumps(entry, ensure_ascii=False)) + lines = lines[-self._AGENT_HISTORY_MAX:] + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + except Exception as exc: + logger.warning("[history] append fehlgeschlagen: %s", exc) + + def _agent_history_read(self, host: str, limit: int = 200) -> list: + try: + path = self._agent_history_path(host) + if not os.path.exists(path): + return [] + out = [] + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + out.append(json.loads(line)) + except Exception: + pass + return out[-limit:] + except Exception: + return [] + async def _host_request(self, host: str = "", action: str = "", params: Optional[dict] = None, timeout: float = 60.0) -> dict: diff --git a/diagnostic/index.html b/diagnostic/index.html index 962f4ab..598961b 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -1594,6 +1594,20 @@ + + +