# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-an-agent-span-lose-token-accounting-integrity-when-tool-call-attributes-are-emitted-before-the-model-completion-event.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-an-agent-span-lose-token-accounting-integrity-when-tool-call-attributes-are-emitted-before-the-model-completion-event.warrant.json
# Markdown SHA-256:   46a7f467011199ae0edc744a0b3a283a9ef097f32fd18d99a08797dde67f7b1c
# Code AST SHA-256:   ab01f8de50f3334b5b705c749429ecc906cd137a66c9eb037a81d956584b97e8
# Verified Classes:   TokenAccountingInvariantError, AgentSpanTokenGuard
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class TokenAccountingInvariantError(RuntimeError):
    """Raised when an agent span's token attributes violate the accumulation invariant."""
    pass


class AgentSpanTokenGuard:
    def __init__(self, span, expected_turns: int):
        self._span = span
        self._expected_turns = expected_turns
        self._turns_seen = 0
        self._accumulated_output = 0
        self._emitted = False

    def record_turn(self, output_tokens: int) -> None:
        if self._emitted:
            raise TokenAccountingInvariantError(
                "token attribute already emitted; cannot append a turn after span end"
            )
        if output_tokens < 0:
            raise TokenAccountingInvariantError(
                f"negative output_tokens={output_tokens} on turn {self._turns_seen}"
            )
        self._turns_seen += 1
        self._accumulated_output += output_tokens
        # Do NOT set gen_ai.usage.* here; defer to finalize() to avoid partial export.
        self._span.set_attribute(
            f"gen_ai.usage.output_tokens.turn.{self._turns_seen}", output_tokens
        )

    def finalize(self) -> None:
        if self._turns_seen != self._expected_turns:
            raise TokenAccountingInvariantError(
                f"expected {self._expected_turns} turns, observed {self._turns_seen}; "
                "refusing to emit cumulative usage that would silently undercount"
            )
        self._span.set_attribute("gen_ai.usage.output_tokens", self._accumulated_output)
        self._span.set_attribute("gen_ai.usage.turns", self._turns_seen)
        self._emitted = True
