feat(diagnostic): Agent-Historie pro Agent — Akkordeon je Nachricht
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/ <hostId>.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/<file> 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 <noreply@anthropic.com>
This commit is contained in:
+47
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user