{
  "schema_version": "1.3.0",
  "slug": "why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-lose-child-tool-call-spans-and-double-count-tokens-when-the-tool-call-crosses-an-as",
  "title": "Why does an instance of an OpenTelemetry GenAI agent span silently lose child tool-call spans and double-count tokens when the tool call crosses an asynchronous boundary",
  "date_of_record": "2026 November 7",
  "scope": "Applies to OpenTelemetry GenAI semantic conventions (as of the `gen_ai.*` attribute family, agent/tool span kinds, and `gen_ai.usage.*` token counters) when an agent invokes a tool whose execution is decoupled from the agent's own span lifetime — queued work, a remote worker, a callback registered for later, or a subprocess whose result is awaited. It does not apply to synchronous in-process tool calls that complete within the parent span's `with` block, where the standard parent-child relationship and single-writer token accounting hold trivially.",
  "projections": {
    "unified_markdown_uri": "https://corpusfetch.com/c/why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-lose-child-tool-call-spans-and-double-count-tokens-when-the-tool-call-crosses-an-as.md",
    "method_python_slice_uri": "https://corpusfetch.com/c/why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-lose-child-tool-call-spans-and-double-count-tokens-when-the-tool-call-crosses-an-as.py",
    "remnant_warrant_slice_uri": "https://corpusfetch.com/c/why-does-an-instance-of-an-opentelemetry-genai-agent-span-silently-lose-child-tool-call-spans-and-double-count-tokens-when-the-tool-call-crosses-an-as.warrant.json"
  },
  "token_profile": {
    "full_markdown_tokens": 1773,
    "python_slice_tokens": 241,
    "warrant_slice_tokens": 220
  },
  "remnant_summary": {
    "core_distinction": "The GenAI conventions treat *span nesting* and *token accounting* as two independent invariants that happen to coincide in the synchronous case. A tool-call span is expected to be a child of the agent span (`parent_span_id` = agent span), and token usage is expected to be recorded exactly once, on the span that owns the model invocation that produced the tok...",
    "what_was_observed": "In a multi-agent system where the orchestrator agent dispatched tool calls to a pool of worker processes over a message queue, three symptoms appeared together and were initially attributed to three separate bugs: 1. **Orphaned tool spans.** Tool-call spans appeared in the trace backend as root spans (`parent_span_id` empty) roughly 40% of the time. The fraction correlated with queue latency, not with tool type. The ...",
    "condition_of_validity": "The standard GenAI agent-span contract is valid only when all of the following hold: - The tool call executes within the lexical scope in which the parent span is current, so that `start_as_current_span` for the tool inherits the correct parent without explicit propagation. - The token-recording wrapper is scoped to a single model invocation, not to a span c...",
    "decision_boundary": "The operational rule is a hard split at the async boundary: - **If the tool executes synchronously in the dispatching context**, use ambient context inheritance and let the SDK parent the tool span automatically. Record tokens on the invocation that produced them; do not attach a context-scoped usage recorder. - **If the tool executes across any async bounda...",
    "operational_effect": "Applying the split eliminated all three symptoms in one change: tool spans re-parented correctly (orphan rate 0%), agent-span token counters dropped back to matching the provider invoice exactly, and `gen_ai.operation.name` became deterministic per span kind. The cost is that every async dispatch site must now carry the serialized span context and the worker..."
  },
  "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": "9e248a11438e2f29fe57e26398b71b332d07bab2a5eac021f5842c3a01904eb2",
    "verified_classes": [
      "GenAIContextPropagationError"
    ],
    "verified_functions": [
      "dispatch_tool_call",
      "execute_tool_in_worker"
    ],
    "verified_functions_count": 2,
    "defensive_guards_count": 2,
    "raised_exceptions": [
      "GenAIContextPropagationError"
    ],
    "sample_fail_closed_invariants": [
      "tool call reached worker without serialized parent span context; tool span would orphan as a root span"
    ],
    "stdlib_dependencies": [],
    "observation_mode": "EMPIRICAL_BOUNDARY_CAPTURE",
    "zero_external_deps": true
  }
}
