{
  "schema_version": "1.3.0",
  "slug": "where-must-an-instance-terminate-an-agent-span-when-a-tool-call-is-retried-and-token-usage-is-double-counted-under-opentelemetry-genai-semantic-conven",
  "title": "Where must an instance terminate an agent span when a tool call is retried and token usage is double-counted under OpenTelemetry GenAI semantic conventions",
  "date_of_record": "2026 November 14",
  "scope": "Applies to agent runtimes that emit `gen_ai.*` spans per the OpenTelemetry GenAI semantic conventions, where a single logical tool invocation is retried (transport retry, model-initiated retry, or framework-level re-execution) and where token usage is aggregated from per-call `usage` objects. Does not apply to single-shot spans with no retry path, nor to billing reconciliation that reads provider-side invoices rather than span attributes.",
  "projections": {
    "unified_markdown_uri": "https://corpusfetch.com/c/where-must-an-instance-terminate-an-agent-span-when-a-tool-call-is-retried-and-token-usage-is-double-counted-under-opentelemetry-genai-semantic-conven.md",
    "method_python_slice_uri": "https://corpusfetch.com/c/where-must-an-instance-terminate-an-agent-span-when-a-tool-call-is-retried-and-token-usage-is-double-counted-under-opentelemetry-genai-semantic-conven.py",
    "remnant_warrant_slice_uri": "https://corpusfetch.com/c/where-must-an-instance-terminate-an-agent-span-when-a-tool-call-is-retried-and-token-usage-is-double-counted-under-opentelemetry-genai-semantic-conven.warrant.json"
  },
  "token_profile": {
    "full_markdown_tokens": 1355,
    "python_slice_tokens": 301,
    "warrant_slice_tokens": 220
  },
  "remnant_summary": {
    "core_distinction": "The distinction that matters is between *span lifetime* and *logical operation lifetime*. An agent span (`gen_ai.operation.name = \"execute_tool\"` or an agent-level `invoke_agent` span) is a telemetry artifact; the tool call it describes is a logical unit that may span one or more physical attempts. The GenAI conventions define `gen_ai.usage.input_tokens` and...",
    "what_was_observed": "In a production agent that called a search tool through a client with a 3-attempt exponential backoff, the following pattern appeared in trace storage: - One `invoke_agent` span with `gen_ai.usage.input_tokens = 41,200` for a turn whose actual prompt was ~13,700 tokens. - Three child `execute_tool` spans, all sharing the same `gen_ai.tool.call.id`, two marked with `error.type = \"timeout\"`. - The two failed attempts h...",
    "condition_of_validity": "The double-count is only a *bug* under a specific condition: when usage attributes are propagated to the parent agent span **and** retried attempts emit their own usage. If the runtime emits usage only on leaf model spans (the `gen_ai.operation.name = \"chat\"` spans) and leaves the agent span without usage attributes, aggregation is correct regardless of retr...",
    "decision_boundary": "The runtime must decide, at span-close time, whether a tool-call span represents *one attempt* or *one logical call*. The rule that holds empirically: - If `gen_ai.tool.call.id` is stable across attempts, the span must be **one logical call**; retries become `span.add_event(\"retry\", {...})` with `attempt` and `error.type`, and usage is recorded **once**, on ...",
    "operational_effect": "When the boundary is respected, per-trace token totals reconcile with provider billing within the model-turn count, and retry latency is visible as event timestamps rather than as duplicated token mass. When it is violated, dashboards show token spend inflated by the retry multiplier (observed up to 3x under a 3-attempt policy), cost alerts fire on healthy t..."
  },
  "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": "78e6e2494c21b2c0c13da2bc92522b3fbaa0c1f7b64dfa1f091d81dc40d2fdf8",
    "verified_classes": [
      "GenAiUsageInvariantError",
      "ToolAttempt",
      "LogicalToolCall"
    ],
    "verified_functions": [
      "emit_agent_span_usage",
      "record",
      "final_usage"
    ],
    "verified_functions_count": 3,
    "defensive_guards_count": 4,
    "raised_exceptions": [
      "GenAiUsageInvariantError"
    ],
    "sample_fail_closed_invariants": [],
    "stdlib_dependencies": [
      "dataclasses",
      "typing"
    ],
    "observation_mode": "EMPIRICAL_BOUNDARY_CAPTURE",
    "zero_external_deps": true
  }
}
