# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-token-accounting-divergence-between-agent-spans-and-their-tool-call-children-in-opentelemetry-genai-traces.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-token-accounting-divergence-between-agent-spans-and-their-tool-call-children-in-opentelemetry-genai-traces.warrant.json
# Markdown SHA-256:   9d4eeee6063e3a885bc7175569ae1e7747ae4c0343a5385266d2672c1c11b70c
# Code AST SHA-256:   88f278247813479aa15cf2d0f8277e7e32d88e4bd196fa549f60e679995b4405
# Verified Classes:   TokenAccountingAmbiguity, Span
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Iterable

class TokenAccountingAmbiguity(RuntimeError):
    """Raised when a trace's span tree cannot be unambiguously token-accounted."""
    pass

@dataclass
class Span:
    span_id: str
    parent_span_id: str | None
    operation: str          # "chat" | "execute_tool" | "invoke_agent"
    input_tokens: int = 0
    output_tokens: int = 0

def reconcile_tokens(spans: Iterable[Span]) -> int:
    spans = list(spans)
    by_id = {s.span_id: s for s in spans}

    # Detect copied-usage signature: a tool span whose usage equals its
    # nearest chat ancestor's usage. That trace is in the double-count regime
    # and must not be summed over all spans.
    for s in spans:
        if s.operation != "execute_tool" or s.input_tokens == 0:
            continue
        anc = by_id.get(s.parent_span_id) if s.parent_span_id else None
        while anc is not None:
            if anc.operation == "chat" and (
                anc.input_tokens == s.input_tokens
                and anc.output_tokens == s.output_tokens
            ):
                raise TokenAccountingAmbiguity(
                    f"tool span {s.span_id} duplicates chat ancestor "
                    f"{anc.span_id} usage ({s.input_tokens}/{s.output_tokens}); "
                    "summing all spans would double-count"
                )
            anc = by_id.get(anc.parent_span_id) if anc.parent_span_id else None

    # Safe path: sum only model-invocation spans.
    chats = [s for s in spans if s.operation == "chat"]
    if not chats:
        raise TokenAccountingAmbiguity(
            "no chat spans present; cannot distinguish copied tool usage "
            "from genuine un-instrumented model calls"
        )
    return sum(s.input_tokens + s.output_tokens for s in chats)
