# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: 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
# Remnant Warrant:    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
# Markdown SHA-256:   f835cf6f49fe6852a45508212eb4583aa1d6987a599b2b89b1c0029674be84af
# Code AST SHA-256:   9e248a11438e2f29fe57e26398b71b332d07bab2a5eac021f5842c3a01904eb2
# Verified Classes:   GenAIContextPropagationError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class GenAIContextPropagationError(RuntimeError):
    """Raised when an async tool dispatch would lose or corrupt GenAI span topology."""
    pass


def dispatch_tool_call(agent_span, tool_name: str, envelope: dict, executor) -> dict:
    ctx = agent_span.get_span_context()
    if not ctx.is_valid:
        raise GenAIContextPropagationError(
            f"agent span for tool {tool_name!r} is not recording; "
            "cannot serialize a valid parent context across the async boundary"
        )
    # Serialize the SPAN CONTEXT, not the ambient Context, not the span object.
    envelope["_otel_parent"] = {
        "trace_id": format(ctx.trace_id, "032x"),
        "span_id": format(ctx.span_id, "016x"),
        "trace_flags": int(ctx.trace_flags),
    }
    # Token recording must be bound to THIS invocation, not to inherited context.
    envelope["_otel_usage_scope"] = f"invocation:{tool_name}:{ctx.span_id:016x}"
    return executor.submit(envelope)


def execute_tool_in_worker(envelope: dict, tracer) -> None:
    parent = envelope.get("_otel_parent")
    if parent is None:
        raise GenAIContextPropagationError(
            "tool call reached worker without serialized parent span context; "
            "tool span would orphan as a root span"
        )
    remote_parent = SpanContext(
        trace_id=int(parent["trace_id"], 16),
        span_id=int(parent["span_id"], 16),
        is_remote=True,
        trace_flags=TraceFlags(parent["trace_flags"]),
    )
    # Fresh attribute set: do NOT inherit the agent's gen_ai.operation.name.
    with tracer.start_as_current_span(
        f"execute_tool {envelope['tool_name']}",
        context=set_span_in_context(NonRecordingSpan(remote_parent)),
        attributes={"gen_ai.operation.name": "execute_tool"},
    ) as tool_span:
        result = run_tool(envelope)
        # Usage recorded on the tool span only, scoped to this invocation.
        tool_span.set_attribute("gen_ai.usage.input_tokens", result.input_tokens)
        tool_span.set_attribute("gen_ai.usage.output_tokens", result.output_tokens)
