fix(telemetry): restore broken `log_tool_start / log_tool_end` interface

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) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 16:24:45 -07:00
parent 95865401ae
commit 494e6fab0d
2 changed files with 41 additions and 2 deletions
+4 -2
View File
@@ -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")
+37
View File
@@ -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