{
  "schema_version": "1.3.0",
  "slug": "why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-double-count-tokens-when-tool-call-attributes-are-emitted-on-both-the-parent-invoke",
  "title": "Why does an instance of an OpenTelemetry GenAI agent span silently double-count tokens when tool-call attributes are emitted on both the parent `invoke_agent` span and the child `execute_tool` span",
  "date_of_record": "2026 November 12",
  "scope": "Applies to OpenTelemetry GenAI semantic conventions (v1.27+ `gen_ai.*` attribute set) where an agent orchestrator emits both `gen_ai.operation.name = \"invoke_agent\"` parent spans and `gen_ai.operation.name = \"execute_tool\"` child spans, and where usage attributes (`gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`) are populated on both. Does not apply to single-shot `chat` spans without tool children, nor to backends that aggregate on `gen_ai.request.model` alone without span-kind filtering.",
  "projections": {
    "unified_markdown_uri": "https://corpusfetch.com/c/why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-double-count-tokens-when-tool-call-attributes-are-emitted-on-both-the-parent-invoke.md",
    "method_python_slice_uri": "https://corpusfetch.com/c/why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-double-count-tokens-when-tool-call-attributes-are-emitted-on-both-the-parent-invoke.py",
    "remnant_warrant_slice_uri": "https://corpusfetch.com/c/why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-double-count-tokens-when-tool-call-attributes-are-emitted-on-both-the-parent-invoke.warrant.json"
  },
  "token_profile": {
    "full_markdown_tokens": 1129,
    "python_slice_tokens": 305,
    "warrant_slice_tokens": 220
  },
  "remnant_summary": {
    "core_distinction": "The GenAI semantic conventions define *where* usage attributes may appear, but they do not define an aggregation invariant across a span tree. A token count is a property of a single model invocation. A tool call is not a model invocation — it is a side effect. When an agent runtime propagates the parent's accumulated usage down onto `execute_tool` children ...",
    "what_was_observed": "In a trace with one `invoke_agent` span, three `execute_tool` children, and one nested `chat` span (the actual model call), the following was emitted: - `invoke_agent`: `gen_ai.usage.input_tokens = 12480`, `gen_ai.usage.output_tokens = 640` - each `execute_tool`: `gen_ai.usage.input_tokens = 12480`, `gen_ai.usage.output_tokens = 640` (copied from parent) - `chat`: `gen_ai.usage.input_tokens = 12480`, `gen_ai.usage.ou...",
    "condition_of_validity": "The invariant `Σ usage over spans == actual model tokens` holds **only** when usage is attributed to exactly one span kind per model invocation. In practice this means: usage is authoritative on `chat` (or `text_completion`) spans, and on `invoke_agent` spans it is *rollup* (the sum over that agent's chat children). `execute_tool` spans must not carry additi...",
    "decision_boundary": "Choose **rollup-on-parent, absent-on-tool** when your backend sums by span kind (the common case for cost attribution). Choose **authoritative-on-chat-only** when you need per-call tokenization for rate-limit or context-window analysis, and let agent spans carry only `gen_ai.agent.name` and turn counters. Never choose **present-on-both-additive** — it is the...",
    "operational_effect": "The observable signature is a token total that is an exact integer multiple of the true total, where the multiplier equals `1 + tool_call_count`. Alerting on absolute token thresholds will fire falsely; alerting on the *ratio* of summed usage to `chat`-span usage will catch it. The fix is a collector-side `filter` processor that drops `gen_ai.usage.*` from s..."
  },
  "matched_failure_symptoms": [
    "DOMAIN:EPISTEMIC_PROVENANCE_AND_FEDERATION_ROUTING"
  ],
  "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": "10dd74a29b66d9a5ca7c9d9e3db817325d75a04f854f9bb8de4649d4f830b1f8",
    "verified_classes": [
      "TokenAccountingInvariantError",
      "Span"
    ],
    "verified_functions": [
      "sum_additive_usage",
      "assert_no_double_count"
    ],
    "verified_functions_count": 2,
    "defensive_guards_count": 1,
    "raised_exceptions": [
      "TokenAccountingInvariantError"
    ],
    "sample_fail_closed_invariants": [],
    "stdlib_dependencies": [
      "__future__",
      "dataclasses",
      "typing"
    ],
    "observation_mode": "EMPIRICAL_BOUNDARY_CAPTURE",
    "zero_external_deps": true
  }
}
