"""Agent orchestration observability model. Tracks OpenClaw, Hermes and sub-agent execution chains. Token usage is optional and depends on model adapter instrumentation. """ from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any def utc_now(): return datetime.now(timezone.utc).isoformat() @dataclass class AgentExecutionRecord: execution_id: str user_id: str visible_scope: str entry_agent: str path: list[str] used_hermes: bool closed_by: str duration_ms: int = 0 input_tokens: int | None = None output_tokens: int | None = None total_tokens: int | None = None status: str = "completed" started_at: str = field(default_factory=utc_now) ended_at: str = field(default_factory=utc_now) trace_id: str | None = None span_id: str | None = None parent_span_id: str | None = None correlation_id: str | None = None causation_id: str | None = None tenant_id: str | None = None project_id: str | None = None conversation_id: str | None = None task_id: str | None = None claim_id: str | None = None run_id: str | None = None message_id: str | None = None def to_dict(self): return asdict(self) class AgentObservability: def __init__(self): self.records: list[AgentExecutionRecord] = [] def record(self, record: AgentExecutionRecord): self.records.append(record) return record.to_dict() def summary(self, user_id: str | None = None) -> dict[str, Any]: records = [r for r in self.records if not user_id or r.user_id == user_id] hermes = [r for r in records if r.used_hermes] direct = [r for r in records if not r.used_hermes] return { "total_executions": len(records), "openclaw_independent_loops": len(direct), "hermes_orchestrated_loops": len(hermes), "average_duration_ms": ( sum(r.duration_ms for r in records) / len(records) if records else 0 ), "token_usage": { "input": sum(r.input_tokens or 0 for r in records), "output": sum(r.output_tokens or 0 for r in records), "total": sum(r.total_tokens or 0 for r in records), }, } def hermes_tree(self, user_id: str | None = None): return [ { "execution_id": r.execution_id, "entry": r.entry_agent, "used_hermes": r.used_hermes, "children": r.path, "duration_ms": r.duration_ms, "tokens": r.total_tokens, } for r in self.records if (not user_id or r.user_id == user_id) ] __all__ = ["AgentExecutionRecord", "AgentObservability"]