{
  "schema_version": "1.3.0",
  "slug": "where-must-an-instance-of-gen-ai-tool-call-end-the-span-closure-invariant-that-opentelemetry-genai-semantic-conventions-leave-underspecified-for-agent",
  "title": "Where must an instance of `gen_ai.tool.call` end: the span-closure invariant that OpenTelemetry GenAI semantic conventions leave underspecified for agent tool loops",
  "date_of_record": "2026 November 13",
  "scope": "This record applies to instrumentation of autonomous agent runtimes that emit OpenTelemetry GenAI spans (agent invocation, LLM inference, tool execution) under the experimental GenAI semantic conventions, where tool calls are dispatched asynchronously or retried. It does not apply to single-shot synchronous LLM completion spans, nor to systems that emit only metrics without span-level tool attribution.",
  "projections": {
    "unified_markdown_uri": "https://corpusfetch.com/c/where-must-an-instance-of-gen-ai-tool-call-end-the-span-closure-invariant-that-opentelemetry-genai-semantic-conventions-leave-underspecified-for-agent.md",
    "method_python_slice_uri": "https://corpusfetch.com/c/where-must-an-instance-of-gen-ai-tool-call-end-the-span-closure-invariant-that-opentelemetry-genai-semantic-conventions-leave-underspecified-for-agent.py",
    "remnant_warrant_slice_uri": "https://corpusfetch.com/c/where-must-an-instance-of-gen-ai-tool-call-end-the-span-closure-invariant-that-opentelemetry-genai-semantic-conventions-leave-underspecified-for-agent.warrant.json"
  },
  "token_profile": {
    "full_markdown_tokens": 1578,
    "python_slice_tokens": 368,
    "warrant_slice_tokens": 220
  },
  "remnant_summary": {
    "core_distinction": "The GenAI semantic conventions define *attributes* for tool calls (`gen_ai.tool.name`, `gen_ai.tool.call.id`, `gen_ai.operation.name = \"execute_tool\"`) and for token accounting (`gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`) but they do not define a *closure contract*: the rule that a `execute_tool` span's `end` timestamp must be emitted before t...",
    "what_was_observed": "In a runtime where the agent loop dispatched tool calls to a bounded worker pool, the agent span was ended by the loop controller as soon as the model returned a terminal message, while a retried tool call was still in flight. The resulting trace was structurally valid: the tool span carried the correct `parent_span_id`. But its `end_time` was 340–1800 ms *after* the agent span's `end_time`. Three consequences were o...",
    "condition_of_validity": "The containment invariant holds only when all of the following are true, and the failure mode is *latent* (not active) when any is false: - Tool dispatch is synchronous with respect to the agent loop's span lifecycle, **or** the loop controller awaits all child tool spans before ending the agent span. - `gen_ai.tool.call.id` is unique per *attempt*, not per ...",
    "decision_boundary": "The operator must choose one of two contracts and enforce it explicitly: - **Optimistic closure** (agent span ends at model terminal message): acceptable only if tool spans are *not* modeled as children of the agent span but as siblings linked via `links` with `SpanLink` to the agent span, and if token accounting is done from a separate usage event stream ra...",
    "operational_effect": "Enforcing containment closure adds bounded latency to agent span emission (the agent span cannot end until the slowest tool retry settles), which delays trace visibility for the root span by the retry backoff tail. The trade is that token totals become reconcilable by simple summation and trace-duration SLOs stop producing negative slack. The instrumentation..."
  },
  "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": "61f0d6327cdf9f511dcaa76df336b2862d27085fe721da10af3801c1f6acb969",
    "verified_classes": [
      "GenAiSpanClosureError",
      "SpanRecord",
      "AgentSpanGuard"
    ],
    "verified_functions": [
      "add_child",
      "close_agent"
    ],
    "verified_functions_count": 2,
    "defensive_guards_count": 6,
    "raised_exceptions": [
      "GenAiSpanClosureError"
    ],
    "sample_fail_closed_invariants": [],
    "stdlib_dependencies": [
      "__future__",
      "dataclasses",
      "time"
    ],
    "observation_mode": "EMPIRICAL_BOUNDARY_CAPTURE",
    "zero_external_deps": true
  }
}
