From 494e6fab0da1343c87404db7cc8f7ab41c0549f1 Mon Sep 17 00:00:00 2001 From: 0xallam Date: Sat, 25 Apr 2026 16:24:45 -0700 Subject: [PATCH] fix(telemetry): restore broken ``log_tool_start`` / ``log_tool_end`` interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit found ``hooks.on_tool_start`` / ``on_tool_end`` were calling ``tracer.log_tool_start`` / ``log_tool_end`` via ``hasattr()`` checks — but those methods didn't exist on ``Tracer``. The ``hasattr()`` always returned False, so the calls were silently no-ops, leaving ``tracer.tool_executions`` permanently empty. Four TUI render paths consume that dict and were therefore broken: - ``_get_agent_name_for_vulnerability`` always returned ``None`` (vuln panel couldn't show which agent reported the finding). - ``_agent_has_real_activity`` always returned ``False`` (animation logic stopped immediately). - ``_agent_vulnerability_count`` always returned ``0``. - ``_gather_agent_events`` only showed chat events, never tool events. Fix: add ``Tracer.log_tool_start(agent_id, tool_name) → exec_id`` and ``Tracer.log_tool_end(agent_id, tool_name, result)``. Hook bodies now call them directly (no ``hasattr`` guard). The exec-id counter ensures nested / overlapping tool calls within an agent don't clobber each other. Co-Authored-By: Claude Opus 4.7 (1M context) --- strix/orchestration/hooks.py | 6 ++++-- strix/telemetry/tracer.py | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/strix/orchestration/hooks.py b/strix/orchestration/hooks.py index 8576b49..90fbff6 100644 --- a/strix/orchestration/hooks.py +++ b/strix/orchestration/hooks.py @@ -176,12 +176,13 @@ class StrixOrchestrationHooks(RunHooks[Any]): agent: Any, tool: Any, ) -> None: + del agent try: ctx = context.context if not isinstance(ctx, dict): return tracer = ctx.get("tracer") - if tracer is not None and hasattr(tracer, "log_tool_start"): + if tracer is not None: tracer.log_tool_start(ctx.get("agent_id", "?"), tool.name) except Exception: logger.exception("on_tool_start failed") @@ -193,6 +194,7 @@ class StrixOrchestrationHooks(RunHooks[Any]): tool: Any, result: str, ) -> None: + del agent try: ctx = context.context if not isinstance(ctx, dict): @@ -200,7 +202,7 @@ class StrixOrchestrationHooks(RunHooks[Any]): if tool.name in ("agent_finish", "finish_scan"): ctx["agent_finish_called"] = True tracer = ctx.get("tracer") - if tracer is not None and hasattr(tracer, "log_tool_end"): + if tracer is not None: tracer.log_tool_end(ctx.get("agent_id", "?"), tool.name, result) except Exception: logger.exception("on_tool_end failed") diff --git a/strix/telemetry/tracer.py b/strix/telemetry/tracer.py index d69433e..3f46ecb 100644 --- a/strix/telemetry/tracer.py +++ b/strix/telemetry/tracer.py @@ -42,6 +42,7 @@ class Tracer: self.agents: dict[str, dict[str, Any]] = {} self.tool_executions: dict[int, dict[str, Any]] = {} self.chat_messages: list[dict[str, Any]] = [] + self._next_exec_id = 1 self.vulnerability_reports: list[dict[str, Any]] = [] self.final_scan_result: str | None = None @@ -380,6 +381,42 @@ class Tracer: except (OSError, RuntimeError): logger.exception("Failed to save scan data") + def log_tool_start(self, agent_id: str, tool_name: str) -> int: + """Record a tool invocation in flight. Returns an exec_id.""" + exec_id = self._next_exec_id + self._next_exec_id += 1 + self.tool_executions[exec_id] = { + "agent_id": agent_id, + "tool_name": tool_name, + "status": "running", + "result": None, + "timestamp": datetime.now(UTC).isoformat(), + } + return exec_id + + def log_tool_end(self, agent_id: str, tool_name: str, result: Any) -> None: + """Mark the most recent matching exec as completed.""" + for exec_id in reversed(self.tool_executions): + entry = self.tool_executions[exec_id] + if ( + entry.get("agent_id") == agent_id + and entry.get("tool_name") == tool_name + and entry.get("status") == "running" + ): + entry["status"] = "completed" + entry["result"] = result + return + # No matching start (e.g. hooks added later in life) — record as completed. + exec_id = self._next_exec_id + self._next_exec_id += 1 + self.tool_executions[exec_id] = { + "agent_id": agent_id, + "tool_name": tool_name, + "status": "completed", + "result": result, + "timestamp": datetime.now(UTC).isoformat(), + } + def get_real_tool_count(self) -> int: return sum( 1