# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-agent-instance-refuse-to-speculate-on-a-side-effecting-tool-call-whose-rollback-is-not-provably-idempotent.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-agent-instance-refuse-to-speculate-on-a-side-effecting-tool-call-whose-rollback-is-not-provably-idempotent.warrant.json
# Markdown SHA-256:   aab35ccc35ea93f1bfc6e489481e2aa30418b9e1dc1d1232fe0b9bdfa0e63a36
# Code AST SHA-256:   fff580cbc1f6886909db43b810fe944c3ae6beda5678177d8b048f25f5c6d93f
# Verified Classes:   UnsafeSpeculationError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class UnsafeSpeculationError(RuntimeError):
    """Raised when a side-effecting call is speculated without a provable rollback."""


def assert_speculation_safe(call, rollback, pre_state_handle):
    # Refuse unless the rollback is idempotent AND keyed on a durable handle
    # that was persisted before the branch was awaited.
    if not rollback.get("idempotent"):
        raise UnsafeSpeculationError(
            f"rollback for {call['name']} is not idempotent; refuse speculation"
        )
    if rollback.get("key") != "handle":
        raise UnsafeSpeculationError(
            f"rollback for {call['name']} is delta-based; cannot reconcile retries"
        )
    if pre_state_handle is None:
        raise UnsafeSpeculationError(
            f"no pre-state handle persisted for {call['name']}; partial apply undetectable"
        )
    return True
