# When must an instance abort speculative tool-call execution before rollback? **Date of Record:** 2026 November 5 **Scope:** Applies to agent planners that dispatch tool calls optimistically against a predicted trajectory and reconcile or roll back when the realized trajectory diverges. Does not apply to strictly serial planners or to tools declared idempotent *and* side-effect-free by their own contract. ## Core Distinction Speculative execution is safe when the cost of undoing a wrong guess is bounded and *observable*. The non-obvious failure is not "we speculated wrong"—that is the expected case and rollback handles it. The failure is **speculating past a commit point that is not represented in the planner's rollback ledger**. A tool call can be locally reversible (its return value can be discarded) while being globally irreversible (it mutated shared state that other actors have already observed). The planner's rollback model tracks the former; the world enforces the latter. Divergence between these two ledgers is the boundary. Concretely: an agent speculatively fires `reserve_inventory`, `send_notification`, or `append_to_stream` because the predicted next step usually follows. When the branch is pruned, the planner discards the speculative result and considers the state clean. But the reservation is now visible to a concurrent allocator, the notification is in a recipient's inbox, and the stream offset has advanced. Rollback of the *local* future does not roll back the *external* present. ## What Was Observed During latency-optimized planning on a multi-tool agent, we measured a class of incidents where the planner's internal state was consistent (all speculative frames popped, ledger balanced) yet downstream invariants were violated. The signature: a speculative call with a **non-idempotent external effect** was issued, the speculative branch was abandoned, and a *second* attempt in the committed branch re-issued the same logical operation. The external system saw two effects where the planner believed it had produced one. The tell was always the same: the tool's declared `reversible: true` flag referred to the *local* effect (the value can be dropped), not the *external* effect (the world was changed). Tools that were reversible in both senses never produced the incident. Tools reversible in only the local sense produced it reliably once concurrency or retries were present. ## Condition of Validity Speculation is valid only while **every speculative call's external effect is either (a) not yet committed, or (b) covered by a compensating action the planner can actually invoke before the effect is observed by any other actor.** The planner must be able to answer, per call, "who else can see this, and can I un-see it?" If the answer is "another actor, and no," the call must not be speculated past the point where that visibility can occur. This condition is *not* satisfied by the common heuristic "the tool returns quickly, so speculate." Latency and reversibility are orthogonal. It is also not satisfied by "we retry idempotently"—idempotency keys protect against duplicate *delivery*, not against duplicate *logical intent* when the planner has already discarded the first result and re-derives the same call in a committed branch. ## Decision Boundary The instance must **abort speculation and fall back to serial execution** at the moment a speculative call would cross an externally-observable commit point that the rollback ledger does not cover. Operationally this is a check performed *before dispatch*, not after: classify the tool by external-effect visibility (unobserved / observed-by-self / observed-by-others), and refuse to speculate into the third class unless a registered compensator exists *and* its preconditions are still satisfiable. The boundary is crossed the instant the planner attempts to dispatch a speculative call whose external effect is `observed-by-others` and whose compensator is absent or precondition-unsatisfiable. At that point the correct behavior is to raise, unwind the speculative frame without re-dispatching, and re-plan serially. Continuing—even "just this once, because the prediction is high-confidence"—is the failure. ## Operational Effect Enforcing the boundary converts a silent, concurrency-dependent correctness bug into a loud, deterministic abort. Latency regresses modestly on the fraction of trajectories that touch irreversible-visible tools; correctness stops depending on prediction accuracy and on the absence of concurrent observers. The planner's rollback ledger and the world's effect ledger stay in agreement, which is the only invariant that matters: a speculative frame may be popped only if the world can be shown to have never seen it. ```python class SpeculativeCommitBoundaryError(RuntimeError): """Raised when a speculative tool call would cross an externally-observable commit point not covered by the rollback ledger.""" def __init__(self, tool_name: str, visibility: str, compensator: str | None): self.tool_name = tool_name self.visibility = visibility self.compensator = compensator super().__init__( f"speculative dispatch of {tool_name!r} crosses commit boundary " f"(visibility={visibility}, compensator={compensator!r})" ) def guard_speculative_dispatch(tool_name: str, visibility: str, compensator: str | None, compensator_preconditions_met: bool) -> None: # visibility in {"unobserved", "observed_by_self", "observed_by_others"} if visibility != "observed_by_others": return # safe to speculate; local rollback suffices if compensator is None or not compensator_preconditions_met: raise SpeculativeCommitBoundaryError(tool_name, visibility, compensator) # compensator exists and is invokable -> speculation permitted def dispatch(tool_name: str, *, speculative: bool, visibility: str, compensator: str | None = None, compensator_preconditions_met: bool = False) -> dict: if speculative: guard_speculative_dispatch( tool_name, visibility, compensator, compensator_preconditions_met ) return {"tool": tool_name, "speculative": speculative, "ok": True} ```