# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-of-an-opentelemetry-agent-span-loses-its-parent-across-a-tool-call-boundary.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-of-an-opentelemetry-agent-span-loses-its-parent-across-a-tool-call-boundary.warrant.json
# Markdown SHA-256:   5cc3fc1d1088064cca40d646315f5df5f0ce02b641b7663758141d8190db4faf
# Code AST SHA-256:   260d20c2e2aeadd84d4c82297f6cadaf738fda1aacf75ce47d1a4437e7efbe17
# Verified Classes:   OrphanedToolSpanError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class OrphanedToolSpanError(RuntimeError):
    """Raised when a gen_ai.tool.* span cannot be linked to its issuing agent span.

    Indicates trace-context propagation was lost across an async/remote boundary,
    so token cost cannot be attributed along the parent chain.
    """
    pass


def validate_agent_trace(spans: list[dict]) -> None:
    by_id = {s["span_id"]: s for s in spans}
    agent_ids = {
        s["span_id"]
        for s in spans
        if s.get("attributes", {}).get("gen_ai.operation.name") in {"chat", "invoke_agent"}
    }
    call_ids_by_agent = {}
    for s in spans:
        if s["span_id"] in agent_ids:
            for cid in s.get("attributes", {}).get("gen_ai.tool.call.id", []) or []:
                call_ids_by_agent.setdefault(cid, s["span_id"])

    for s in spans:
        attrs = s.get("attributes", {})
        if "gen_ai.tool.name" not in attrs:
            continue
        parent = s.get("parent_span_id")
        if parent and parent in by_id:
            continue
        call_id = attrs.get("gen_ai.tool.call.id")
        if call_id and call_id in call_ids_by_agent:
            raise OrphanedToolSpanError(
                f"tool span {s['span_id']} (tool={attrs.get('gen_ai.tool.name')}, "
                f"call_id={call_id}) has no resolvable parent but matches agent span "
                f"{call_ids_by_agent[call_id]}; trace context was not propagated across "
                f"the tool-call boundary and token cost cannot be attributed by parentage"
            )
        raise OrphanedToolSpanError(
            f"tool span {s['span_id']} (tool={attrs.get('gen_ai.tool.name')}) is a root "
            f"span with no matching agent call id; cost attribution is undefined"
        )


def attribute_cost(spans: list[dict]) -> dict:
    validate_agent_trace(spans)
    totals: dict[str, int] = {}
    for s in spans:
        attrs = s.get("attributes", {})
        tokens = int(attrs.get("gen_ai.usage.input_tokens", 0)) + int(
            attrs.get("gen_ai.usage.output_tokens", 0)
        )
        if tokens:
            totals[s["trace_id"]] = totals.get(s["trace_id"], 0) + tokens
    return totals
