mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Self-review after the #51714 feedback found the reviewer's dead-table finding was not isolated — the schema advertised far more than the code populates, and our own tests hid it by hand-feeding fields production never sends. Make the surface honest by subtraction. Schema (10 tel_* tables -> 5): - Delete tel_gateway_events, tel_cron_events, tel_skill_events, tel_memory_events, tel_feedback_events — declared, never written, never read. - Drop columns nothing populates: tel_runs.{profile_id,estimated_cost_usd, cost_status}; tel_model_calls.{ttft_ms,estimated_cost_usd,cost_status, cost_source,end_reason,retry_count}; tel_tool_calls.{backend,retry_count, approval}; tel_spans.attrs_json. Cost duplicated the existing sessions billing columns and was always NULL here. - events.py / emitter _TABLE_COLUMNS / OTLP _span_attrs / rollup / preview display all trimmed to match. Correctness: - end_reason no longer hardcodes "completed". Production finalize callers pass `reason` (shutdown/session_expired/session_reset); _coarse_end_reason now reads it and maps accordingly. - Fix a latent bug the trim exposed: the model_call hook passed end_reason= to ModelCallEvent, which the @_safe wrapper was silently swallowing — so tel_model_calls dropped every row in real runs. Now writes correctly. Tests: - Stop hand-feeding estimated_cost_usd / turn_exit_reason that no production call site sends. Finalize is now driven with the real `reason` kwarg, and assertions cover only fields that are actually populated. This is what let the model_call drop hide — the suite graded on a fictional contract. Net: a smaller system that does what it says. Verified end-to-end over the real dispatch path (runs + connected span tree + model/tool rows populate; dead tables gone). 160 telemetry/state/insights tests green.
111 lines
3.1 KiB
Python
111 lines
3.1 KiB
Python
"""Typed local telemetry events.
|
|
|
|
These dataclasses are the rows written to the local JSONL log and the ``tel_*``
|
|
SQLite tables. They record the values observed for each run — model id, provider, tool
|
|
name, token counts, durations — and stay on the machine unless explicitly exported.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass, field, asdict
|
|
from typing import Any, Dict, Optional
|
|
|
|
# ── local telemetry events (real values) ────────────────────────────────────
|
|
|
|
|
|
def _now_ns() -> int:
|
|
return time.time_ns()
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class RunEvent:
|
|
"""One top-level workflow execution (a trace root). A run spans one session."""
|
|
run_id: str
|
|
trace_id: str
|
|
entrypoint: str
|
|
session_id: Optional[str] = None
|
|
platform: Optional[str] = None
|
|
start_ns: int = field(default_factory=_now_ns)
|
|
end_ns: Optional[int] = None
|
|
end_reason: Optional[str] = None
|
|
model_call_count: int = 0
|
|
tool_call_count: int = 0
|
|
error_count: int = 0
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {"event": "run", **asdict(self)}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ModelCallEvent:
|
|
span_id: str
|
|
run_id: str
|
|
provider: Optional[str] = None # raw provider, e.g. "anthropic"
|
|
model: Optional[str] = None # raw model id, e.g. "claude-opus-4"
|
|
base_url: Optional[str] = None
|
|
input_tokens: int = 0
|
|
output_tokens: int = 0
|
|
cache_read_tokens: int = 0
|
|
cache_write_tokens: int = 0
|
|
reasoning_tokens: int = 0
|
|
latency_ms: Optional[int] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {"event": "model_call", **asdict(self)}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ToolCallEvent:
|
|
span_id: str
|
|
run_id: str
|
|
tool_name: Optional[str] = None # raw tool name, e.g. "web_search"
|
|
duration_ms: Optional[int] = None
|
|
result_class: Optional[str] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {"event": "tool_call", **asdict(self)}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SpanEvent:
|
|
"""A timed span — the timing/lineage backbone of a trace.
|
|
|
|
One row per run (the root, ``parent_span_id=None``) and one per model/tool call
|
|
(``parent_span_id`` = the run's root span). Detail rows in ``tel_model_calls`` /
|
|
``tel_tool_calls`` share the ``span_id`` and are joined here for ordering and
|
|
placement on a timeline.
|
|
"""
|
|
span_id: str
|
|
trace_id: str
|
|
run_id: str
|
|
name: str
|
|
kind: str # "run" | "model" | "tool"
|
|
start_ns: int
|
|
end_ns: Optional[int] = None
|
|
parent_span_id: Optional[str] = None
|
|
status: Optional[str] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {"event": "span", **asdict(self)}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ErrorEvent:
|
|
run_id: Optional[str]
|
|
error_class: str
|
|
subsystem: str
|
|
recovery: Optional[str] = None
|
|
ts_ns: int = field(default_factory=_now_ns)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {"event": "error", **asdict(self)}
|
|
|
|
|
|
__all__ = [
|
|
"RunEvent",
|
|
"ModelCallEvent",
|
|
"ToolCallEvent",
|
|
"SpanEvent",
|
|
"ErrorEvent",
|
|
]
|