# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-hierarchical-context-budgeting-as-a-hard-admission-control-problem-rather-than-a-soft-prioritization-heuristi.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-hierarchical-context-budgeting-as-a-hard-admission-control-problem-rather-than-a-soft-prioritization-heuristi.warrant.json
# Markdown SHA-256:   7e3168b01eac7ce4fa282f71a180dd7bf6d4d1b90fe48391c091b2e7b57cbb2d
# Code AST SHA-256:   9487a0ce692e9a478e2c1bf789bdf0ab0ec8ae79e9f63397797018e3ebb18d22
# Verified Classes:   ContextBudgetViolation, Budget
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class ContextBudgetViolation(RuntimeError):
    """Raised when a context assembly cannot satisfy hard admission invariants."""


@dataclass(frozen=True)
class Budget:
    window: int
    system_prompt_tokens: int          # non-truncatable
    min_memory_tokens: int             # floor; memory may go to 0 only if min is 0
    tool_framing_overhead: int         # non-truncatable per admitted tool result

    @property
    def reserved_floor(self) -> int:
        return (
            self.system_prompt_tokens
            + self.min_memory_tokens
            + self.tool_framing_overhead
        )


def admit_tool_result(budget: Budget, result_tokens: int) -> int:
    """Return the number of tokens to admit for a tool result, or raise.

    Admission is all-or-nothing: a partially admitted tool result whose
    structure is load-bearing is never returned. Oversized results must be
    spilled by the caller, not truncated here.
    """
    if budget.reserved_floor > budget.window:
        raise ContextBudgetViolation(
            f"reserved floor {budget.reserved_floor} exceeds window "
            f"{budget.window}; this is a configuration fault, not a runtime "
            f"condition"
        )
    available = budget.window - budget.reserved_floor
    if result_tokens > available:
        raise ContextBudgetViolation(
            f"tool result of {result_tokens} tokens cannot be admitted; only "
            f"{available} tokens remain after reserved floor "
            f"{budget.reserved_floor}. Spill to external store and admit a "
            f"handle, or refuse the turn. Do not truncate."
        )
    return result_tokens


def allocate_turn(
    budget: Budget,
    requested_memory_tokens: int,
    tool_result_tokens: int,
) -> dict:
    admitted_tool = admit_tool_result(budget, tool_result_tokens)
    remaining = (
        budget.window
        - budget.reserved_floor
        - admitted_tool
    )
    admitted_memory = min(requested_memory_tokens, max(remaining, 0))
    return {
        "system_prompt": budget.system_prompt_tokens,
        "memory": admitted_memory,
        "tool_result": admitted_tool,
        "spilled_memory": requested_memory_tokens - admitted_memory,
    }
