# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-reconcile-opentelemetry-genai-agent-spans-when-tool-call-attributes-and-token-telemetry-disagree-across-a-multi-hop-delegation.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-reconcile-opentelemetry-genai-agent-spans-when-tool-call-attributes-and-token-telemetry-disagree-across-a-multi-hop-delegation.warrant.json
# Markdown SHA-256:   4b776379533d91e6126c66a8c5c90bab4b142dd3eb76f5e1f1d3eda844563e38
# Code AST SHA-256:   3ef2eeb61fe42dc103455c139c85733d678d3407b26bcd5487f70be9e880cf74
# Verified Classes:   GenAIUsageReconciliationError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class GenAIUsageReconciliationError(RuntimeError):
    """Raised when agent-span token telemetry cannot be reconciled to a billable ledger."""
    pass


def reconcile_hop(parent_usage: dict, child_usages: list[dict], model: str) -> dict:
    """
    parent_usage: {'input_tokens': int, 'output_tokens': int, 'cache_read_input_tokens': int|None}
    child_usages: list of the same shape, one per direct child span.
    """
    parent_in = parent_usage.get("input_tokens", 0)
    child_in_sum = sum(c.get("input_tokens", 0) for c in child_usages)
    delta = parent_in - child_in_sum

    if delta < 0:
        # Parent under-reports relative to children: cache accounting is mandatory.
        for i, c in enumerate(child_usages):
            if c.get("cache_read_input_tokens") is None:
                raise GenAIUsageReconciliationError(
                    f"model={model} hop={i} missing cache_read_input_tokens; "
                    f"parent_input={parent_in} child_sum={child_in_sum} delta={delta}. "
                    "Refusing to emit cost record (would under-count)."
                )
        billable_in = sum(
            (c["input_tokens"] - c["cache_read_input_tokens"]) for c in child_usages
        )
    else:
        # Parent is authoritative rollup; children are detail.
        billable_in = parent_in

    billable_out = parent_usage.get("output_tokens", 0)
    return {
        "model": model,
        "billable_input_tokens": billable_in,
        "billable_output_tokens": billable_out,
        "reconciliation_mode": "children_authoritative" if delta < 0 else "parent_authoritative",
    }


def correlate_tool_call(tool_span: dict, caller_payload: dict) -> str:
    """Thread the caller's tool.call.id; never trust executor-generated IDs."""
    caller_id = caller_payload.get("gen_ai.tool.call.id")
    span_id = tool_span.get("gen_ai.tool.call.id")
    if caller_id is None:
        raise GenAIUsageReconciliationError(
            f"tool span {tool_span.get('span_id')} has no caller-threaded tool.call.id; "
            f"executor id={span_id} is not correlatable to model cost."
        )
    if span_id is not None and span_id != caller_id:
        raise GenAIUsageReconciliationError(
            f"tool.call.id mismatch: caller={caller_id} executor={span_id}. "
            "Cost correlation by tool.call.id would drop this hop."
        )
    return caller_id
