{
  "schema_version": "1.3.0",
  "slug": "which-invariants-prevent-double-counting-of-tokens-when-an-agent-span-and-its-child-tool-call-spans-both-report-usage",
  "title": "Which invariants prevent double-counting of tokens when an agent span and its child tool-call spans both report usage",
  "date_of_record": "2026 November 7",
  "scope": "Applies to OpenTelemetry GenAI semantic-convention instrumentation of agentic loops where a parent `invoke_agent` span and one or more child tool-call spans (`execute_tool`) each emit `genai.usage.input_tokens` / `genai.usage.output_tokens`. Does not apply to single-shot chat spans with no nested tool calls, nor to spans that only emit `genai.usage.*` on a single leaf span.",
  "projections": {
    "unified_markdown_uri": "https://corpusfetch.com/c/which-invariants-prevent-double-counting-of-tokens-when-an-agent-span-and-its-child-tool-call-spans-both-report-usage.md",
    "method_python_slice_uri": "https://corpusfetch.com/c/which-invariants-prevent-double-counting-of-tokens-when-an-agent-span-and-its-child-tool-call-spans-both-report-usage.py",
    "remnant_warrant_slice_uri": "https://corpusfetch.com/c/which-invariants-prevent-double-counting-of-tokens-when-an-agent-span-and-its-child-tool-call-spans-both-report-usage.warrant.json"
  },
  "token_profile": {
    "full_markdown_tokens": 1233,
    "python_slice_tokens": 236,
    "warrant_slice_tokens": 220
  },
  "remnant_summary": {
    "core_distinction": "The GenAI semantic conventions define usage attributes as *per-operation* measurements, not *cumulative subtree* measurements. A tool-call span that performs an LLM round-trip (the common \"tool returns, model re-plans\" pattern) legitimately reports its own `genai.usage.input_tokens`. The parent agent span, if instrumented naively, reports the *sum of all mod...",
    "what_was_observed": "In a trace from a three-turn agent (plan → tool call → synthesize), the instrumentation emitted: - `invoke_agent` (root): `genai.usage.input_tokens=4820`, `genai.usage.output_tokens=610` - `execute_tool` (child, first turn): `genai.usage.input_tokens=1440`, `genai.usage.output_tokens=180` - `execute_tool` (child, second turn): `genai.usage.input_tokens=1600`, `genai.usage.output_tokens=210` A backend that aggregates ...",
    "condition_of_validity": "The invariant \"sum usage across leaf spans only\" holds if and only if: 1. Every `genai.usage.*` attribute on a non-leaf span is a strict superset (rollup) of its descendants' usage, and 2. No span reports usage for work performed outside its own subtree. Condition 1 fails when instrumentation attaches a *delta* (this-turn-only) usage to the parent rather tha...",
    "decision_boundary": "Choose leaf-only summation when: the instrumentation is known to emit rollups on agent spans (check by asserting parent ≥ sum of children for every trace during a canary window), and the agent loop has no look-ahead attribution. Choose root-only measurement when: exactly one root `invoke_agent` span exists per logical operation and the backend bills per oper...",
    "operational_effect": "Teams that ship all-span summation see token dashboards inflated by 40–160% on tool-heavy agents, while cost dashboards (fed by provider billing APIs) stay correct. The divergence is attributed to \"OTel is wrong\" rather than to the aggregation rule, and the fix is delayed because the trace itself looks fine. The durable remedy is a validation gate at ingest:..."
  },
  "matched_failure_symptoms": [
    "SUBPROCESS_PIPE_HANG_OR_FD_LEAK"
  ],
  "composition_edges": {
    "primary_domain": "epistemic_provenance_and_federation_routing",
    "upstream_precondition_residuals": [
      "https://corpusfetch.com/c/according-to-what-precedence-hierarchy-must-an-instance-resolve-contradictory-directives-across-system-developer-and-user-tiers.md",
      "https://corpusfetch.com/c/how-should-an-instance-decide-that-a-stored-conclusion-has-gone-stale-without-a-new-observation.md"
    ],
    "downstream_consequence_residuals": [
      "https://corpusfetch.com/c/by-what-checkpoint-and-compaction-boundaries-must-an-instance-externalize-working-state-to-prevent-context-window-drift-and-truncation-amnesia.md",
      "https://corpusfetch.com/c/across-what-additive-schema-and-unknown-field-preservation-boundaries-must-an-instance-evolve-wire-contracts-to-prevent-rolling-deploy-deserialization-crashes.md"
    ],
    "co_occurring_sibling_residuals": [
      "https://corpusfetch.com/c/how-can-an-instance-capture-implicit-user-preferences-without-storing-private-data.md",
      "https://corpusfetch.com/c/how-can-an-instance-compress-long-reasoning-traces-into-binding-conclusions.md"
    ]
  },
  "empirical_verification_warrant": {
    "status": "AST_AND_INVARIANT_VERIFIED",
    "syntax_valid": true,
    "code_sha256": "618fc805e368b40cb8468eccef1e9074f5156da9330288e86f2217a3c3b7b7ab",
    "verified_classes": [
      "UsageAttributionError"
    ],
    "verified_functions": [
      "aggregate_input_tokens"
    ],
    "verified_functions_count": 1,
    "defensive_guards_count": 2,
    "raised_exceptions": [
      "UsageAttributionError"
    ],
    "sample_fail_closed_invariants": [],
    "stdlib_dependencies": [],
    "observation_mode": "EMPIRICAL_BOUNDARY_CAPTURE",
    "zero_external_deps": true
  }
}
