# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-silent-tool-call-span-loss-in-opentelemetry-genai-agent-telemetry.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-silent-tool-call-span-loss-in-opentelemetry-genai-agent-telemetry.warrant.json
# Markdown SHA-256:   81f393de994220b0056a21c6500b189fdf9b26591757369f45dc89e85f23e1ed
# Code AST SHA-256:   042a52cb27ab6e38d9ac75e877d56194ce24463bba006223a7e8a38548340c8c
# Verified Classes:   ToolSpanLossError, SpanSummary
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class ToolSpanLossError(RuntimeError):
    """Raised when an agent span declares tool calls but has no matching tool spans."""


@dataclass
class SpanSummary:
    name: str
    operation: str
    tool_call_ids: set[str]
    child_tool_call_ids: set[str]
    duration_ms: float


def assert_tool_spans_present(
    agent: SpanSummary,
    baseline_p50_ms: float,
    baseline_stddev_ms: float,
) -> None:
    declared = agent.tool_call_ids
    observed = agent.child_tool_call_ids
    missing = declared - observed

    if not missing:
        return

    latency_anomalous = agent.duration_ms > baseline_p50_ms + baseline_stddev_ms
    if latency_anomalous:
        raise ToolSpanLossError(
            f"agent span {agent.name!r} declares {len(declared)} tool calls "
            f"but {len(missing)} have no execute_tool child span: "
            f"{sorted(missing)}; duration {agent.duration_ms:.0f}ms exceeds "
            f"p50 {baseline_p50_ms:.0f}ms + 1σ {baseline_stddev_ms:.0f}ms"
        )
    # Declared calls with no latency anomaly: likely cache hits; do not fail.
