# When must an instance refuse to speculate on a tool call whose side effects cannot be compensated **Date of Record:** 2026 November 14 **Scope:** Applies to agent runtimes that speculatively pre-execute tool calls (file writes, HTTP mutations, shell commands, message sends) ahead of the model's final decision, gating or compensating afterward. Does *not* apply to pure-read tool calls, to calls whose effects are fully transactional and atomic-rollbackable, or to speculative execution performed against an isolated shadow environment. ## Core Distinction The distinction that matters is not "does this tool have side effects" but "is the side effect *reversible with bounded cost and bounded blast radius*." Speculative pre-execution is safe when the compensation is (a) deterministic, (b) idempotent, and (c) observable to the caller. It is unsafe when the speculative call *emits* an effect that leaves the runtime's control boundary before the gate decision is made — a sent email, a published webhook, a charged card, a `git push`, a message into a third-party queue. The failure is non-obvious because the tool's *own* API may look compensable (there is a `delete` for the `create`), yet the compensating call does not restore the pre-call state: the downstream observer already acted on the effect. This is the classic "compensatable ≠ reversible" trap. A create/delete pair is a *semantic* compensation, not a *state* restoration. The gate is therefore not about whether the tool's interface advertises an undo; it is about whether the effect has crossed a boundary where a third party can observe it before the gate resolves. ## What Was Observed In a production agent loop, the runtime speculatively pre-executed a `send_notification` tool call while the model was still deliberating between two plans. The gate held the call for 180 ms pending the final decision token. The model then chose the *other* plan, and the runtime invoked the tool's registered `compensate()` — a `delete_notification` call — which returned 200 OK. The runtime logged the speculation as successfully rolled back. It was not. The recipient's mobile push had already fired during the 180 ms hold window; the notification was read, and a support ticket was opened in response to a message the agent had "decided not to send." The compensation deleted the record from the notification store but could not un-ring the bell. The runtime's post-hoc invariant check ("all speculative calls have compensation status OK") passed, which is precisely why the failure was invisible until the downstream ticket appeared. A second observation: a speculative `charge_payment` against a payment provider was gated and then compensated with a refund. The refund settled, but the customer's bank statement showed a charge and a refund, and the provider's fraud model flagged the account for a transient auth-then-reverse pattern. Compensation was *correct* and still caused harm. ## Condition of Validity Speculative pre-execution of a tool call is valid only if, for every effect `e` the call can emit, one of the following holds before the gate resolves: 1. `e` is held in a runtime-controlled outbox and not dispatched until the gate commits; or 2. `e` is dispatched to a recipient that the runtime can prove has not yet observed it (rare, and generally unprovable across third parties); or 3. the compensation for `e` restores the observable state to indistinguishable-from-never-happened within the recipient's own model of the world. Condition 3 is the one agents most often *assume* and least often *verify*. It fails for any effect that is externally observable, non-idempotent at the recipient, or that mutates a third party's audit/fraud/notification state. The invariant to enforce: **a speculative call may cross the runtime boundary only if the gate is guaranteed to commit, or if the effect is buffered.** If neither, the call must not be speculated — it must be deferred until the gate resolves. ## Decision Boundary The boundary is drawn at the point of *dispatch*, not the point of *invocation*. Concretely: a tool call whose effect is buffered in an outbox may be speculated freely, because the gate resolves before dispatch. A tool call that dispatches synchronously to an external system must be classified by its effect's observability. The classification that agents get wrong: treating a tool as "safe to speculate because it has a compensation" when the compensation is semantic rather than state-restoring. The correct gate is conservative — when the effect class is unknown or the compensation is not provably state-restoring, the call is **non-speculatable** and must be serialized behind the decision. Do not let a model's confidence or a latency budget override this; the cost of a wrong speculation is borne by a third party, not the runtime. ## Operational Effect The runtime must maintain an effect-class registry: each tool declares whether its effects are `BUFFERED`, `REVERSIBLE` (provably state-restoring), or `IRREVERSIBLE`. Speculation is permitted only for `BUFFERED` and `REVERSIBLE`; `IRREVERSIBLE` calls are deferred and executed only after the gate commits. The registry must be conservative: absent a proof of state restoration, classify as `IRREVERSIBLE`. When a speculative `REVERSIBLE` call's compensation fails or its post-condition is unverifiable, the runtime must raise and halt the loop rather than log-and-continue, because a failed compensation on a speculated irreversible-adjacent effect means the world is now in a state the agent did not choose and cannot clean up. The following guard encodes the refusal. ```python class UncompensatableSpeculationError(RuntimeError): """Raised when a speculative tool call's side effect cannot be provably rolled back before the gate resolves. The call must be deferred, not speculated.""" EFFECT_BUFFERED = "BUFFERED" EFFECT_REVERSIBLE = "REVERSIBLE" EFFECT_IRREVERSIBLE = "IRREVERSIBLE" SPECULATABLE = {EFFECT_BUFFERED, EFFECT_REVERSIBLE} def gate_speculation(tool_name: str, effect_class: str, compensation_verified: bool) -> str: """Decide whether a tool call may be pre-executed ahead of the gate. Returns 'SPECULATE' or 'DEFER'. Raises if the effect class is unknown or if a claimed-reversible effect has no verified state-restoring compensation. """ if effect_class not in (EFFECT_BUFFERED, EFFECT_REVERSIBLE, EFFECT_IRREVERSIBLE): raise UncompensatableSpeculationError( f"tool {tool_name!r} has unclassified effect {effect_class!r}; " f"refusing to speculate against an unknown effect class" ) if effect_class == EFFECT_REVERSIBLE and not compensation_verified: raise UncompensatableSpeculationError( f"tool {tool_name!r} claims REVERSIBLE but compensation is not " f"provably state-restoring; a semantic undo is not a rollback" ) if effect_class not in SPECULATABLE: return "DEFER" return "SPECULATE" ```