# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-opentelemetry-genai-tool-call-span-attributes-as-authoritative-versus-advisory-when-reconstructing-agent-turn.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-opentelemetry-genai-tool-call-span-attributes-as-authoritative-versus-advisory-when-reconstructing-agent-turn.warrant.json
# Markdown SHA-256:   c8f23ce4b781fbc378f30e057696e69152a269c28429fd62fe179eeb0e8390c8
# Code AST SHA-256:   b1e0a2ac5bc055fc736e51e754abf928fd6a680b2af3199eca3cdbcf3bd56bdf
# Verified Classes:   TraceReconstructionError
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class TraceReconstructionError(RuntimeError):
    """Raised when a GenAI span tree cannot be safely treated as authoritative."""
    pass


def classify_conversation(spans: list[dict]) -> str:
    """Return 'authoritative' or 'advisory' for a set of GenAI spans.

    Downgrades to advisory if any execute_tool span has an unresolvable
    parent or if no turn ordinal is present on invoke_agent spans.
    """
    agent_spans = {
        s["span_id"]: s
        for s in spans
        if s.get("gen_ai.operation.name") == "invoke_agent"
    }
    tool_spans = [
        s for s in spans
        if s.get("gen_ai.operation.name") == "execute_tool"
    ]

    if not agent_spans:
        raise TraceReconstructionError(
            "no invoke_agent spans present; conversation has no agent anchor"
        )

    has_ordinal = all(
        "gen_ai.turn.index" in s for s in agent_spans.values()
    )
    orphaned = [
        s for s in tool_spans
        if s.get("parent_span_id") not in agent_spans
    ]

    if orphaned and not has_ordinal:
        # Orphans + no explicit ordering: cannot trust trace as record.
        raise TraceReconstructionError(
            f"{len(orphaned)} orphaned execute_tool spans and no turn "
            f"ordinal; conversation must be demoted to advisory mode"
        )

    if not has_ordinal:
        return "advisory"

    call_ids = [s.get("gen_ai.tool.call.id") for s in tool_spans]
    if any(cid is None for cid in call_ids):
        raise TraceReconstructionError(
            "execute_tool span missing gen_ai.tool.call.id; "
            "deduplication key unavailable"
        )

    return "authoritative"


def reconstruct_turn(spans: list[dict]) -> dict:
    mode = classify_conversation(spans)
    if mode == "advisory":
        raise TraceReconstructionError(
            "refusing authoritative reconstruction: emitter is in "
            "advisory mode; source turn structure from runtime event log"
        )
    tool_ids = {
        s["gen_ai.tool.call.id"]
        for s in spans
        if s.get("gen_ai.operation.name") == "execute_tool"
    }
    return {"mode": mode, "distinct_tool_calls": len(tool_ids)}
