# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-double-counts-tool-call-tokens-across-nested-agent-spans.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-double-counts-tool-call-tokens-across-nested-agent-spans.warrant.json
# Markdown SHA-256:   fd9b11f4e4f68063c234f8df5ac7e7516c90d535e3464a77658018d3b69b7cd5
# Code AST SHA-256:   53bd14d7649a3ca5179c1d9a33dd55a9a8d3901e4e589ef48460f646f710a683
# Verified Classes:   TokenAccountingError, Span
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass


class TokenAccountingError(RuntimeError):
    """Raised when a span tree violates the usage-composition invariant."""


@dataclass(frozen=True)
class Span:
    span_id: str
    parent_id: str | None
    name: str
    input_tokens: int | None  # None means attribute absent
    output_tokens: int | None
    usage_scope: str | None  # None, "inclusive", or "exclusive"


def validate_usage_tree(spans: list[Span]) -> None:
    """Reject trees where an inclusive span has a usage-emitting descendant.

    A span that emits usage and has a descendant that also emits usage is only
    safe if the ancestor is explicitly marked inclusive AND the descendant is
    marked exclusive, so a summing rollup can filter on scope.
    """
    by_id = {s.span_id: s for s in spans}
    children: dict[str, list[Span]] = {}
    for s in spans:
        if s.parent_id is not None:
            children.setdefault(s.parent_id, []).append(s)

    def emits_usage(s: Span) -> bool:
        return s.input_tokens is not None or s.output_tokens is not None

    def descendants(s: Span):
        for c in children.get(s.span_id, []):
            yield c
            yield from descendants(c)

    for s in spans:
        if not emits_usage(s):
            continue
        for d in descendants(s):
            if not emits_usage(d):
                continue
            if s.usage_scope != "inclusive":
                raise TokenAccountingError(
                    f"span {s.span_id!r} ({s.name}) emits usage and has "
                    f"usage-emitting descendant {d.span_id!r} ({d.name}) "
                    f"but is not marked usage_scope='inclusive'; a summing "
                    f"rollup will double-count {d.input_tokens}/{d.output_tokens}"
                )
            if d.usage_scope != "exclusive":
                raise TokenAccountingError(
                    f"descendant {d.span_id!r} of inclusive span {s.span_id!r} "
                    f"must be marked usage_scope='exclusive' to be filtered"
                )


def rollup_tokens(spans: list[Span]) -> tuple[int, int]:
    """Sum usage over spans that a rollup should count: not exclusive children."""
    validate_usage_tree(spans)
    tin = tout = 0
    for s in spans:
        if s.usage_scope == "exclusive":
            continue
        tin += s.input_tokens or 0
        tout += s.output_tokens or 0
    return tin, tout
