How must an instance preserve causal ordering of tool side effects across asynchronous execution?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session invokes multiple tools asynchronously, and the tools produce side effects (writes, sends, mutations) that must occur in a specific causal order relative to each other or to the agent's reasoning, but asynchronous execution may reorder the side effects. Does not apply to strictly sequential tool invocations or side effects that are provably commutative.
Core Distinction
The separation between **tool call ordering** (the agent invoked tool A before tool B) and **side-effect causal ordering** (tool A's side effect was causally prior to tool B's, as observed by the external world). Relying on call ordering creates the **Async Side-Effect Race**: the agent invokes A then B asynchronously, B's side effect lands first, and the external world observes the effects in reverse order, causing state corruption or business logic failure.
What Was Observed
In parallel tool-invocation agent architectures, failing to preserve causal side-effect ordering produces three primary failure modes:
- **Dependent Side-Effect Inversion:** Tool A creates a record, Tool B updates the record. B's update lands before A's create, and the update fails or creates a phantom.
- **Notification Ordering Violation:** Tool A sends an email, Tool B sends a follow-up email. B's email arrives before A's, and the customer receives them in the wrong order.
- **Audit Trail Reversal:** Tool A logs an event, Tool B logs a dependent event. The audit trail shows B before A, breaking root-cause analysis.
Recorded Elements
- **`causal_side_effect_sequence_receipt`:** For each tool call, a declaration of the side-effect sequence number it must produce, allowing the tool to schedule its effect in the correct order.
- **`causal_barrier_receipt`:** For each tool, a cryptographic proof that it waited for its causal predecessor's effect to complete before firing.
- **`side_effect_ordering_witness`:** An external witness (e.g., a monotonic counter, a sequence in the tool's response) that proves the actual side-effect order matched the intended causal order.
- **`causal_reversal_detection_receipt`:** When the external world observes effects out of order, a cryptographic record of the reversal, allowing remediation.
Condition of Validity
* **Applies when:** The session invokes tools asynchronously, the tools' side effects have causal dependencies, and the external world observes them in order. * **Does not apply when:** Tools are invoked sequentially, or the side effects are provably commutative (order does not matter).
Decision Boundary
- **Are tool side effects causally dependent?** If no, parallel execution without ordering is safe. If yes, proceed.
- **Does each tool have a `causal_side_effect_sequence_receipt`?** If no, the tool cannot know its causal position. The agent must assign sequence numbers before invocation.
- **Does each tool include a `causal_barrier_receipt`?** If no, the tool may fire before its predecessor completes. The agent must enforce the barrier.
- **Conversely, when can the session allow side effects to reorder?** Only when the effects are provably commutative (any order produces the same final state), or the external observer is verified to tolerate reordering.
Operational Effect
- **Causal Integrity:** Side effects occur in the intended causal order, not just invocation order.
- **Business Logic Preservation:** Notifications, updates, and dependent side effects behave as designed.
- **Audit Correctness:** External audit trails reflect the causal order, not the async completion order.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class CausalVerdict(Enum):
NO_CAUSAL_DEPENDENCY = auto()
CAUSAL_ORDER_VERIFIED = auto()
CAUSAL_BARRIER_MISSING = auto()
CAUSAL_REVERSAL_DETECTED = auto()
@dataclass(frozen=True)
class ToolInvocation:
tool_id: str
side_effect_sequence: int | None
causal_barrier_receipt: str | None
depends_on: tuple[str, ...]
@dataclass(frozen=True)
class CausalState:
invocations: tuple[ToolInvocation, ...]
external_side_effect_order: tuple[str, ...] | None
@dataclass(frozen=True)
class CausalPolicy:
require_barriers: bool
@dataclass(frozen=True)
class CausalManifest:
verdict: CausalVerdict
directive: str
manifest_digest: str
class CausalSideEffectGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_causality(
cls, state: CausalState, policy: CausalPolicy
) -> CausalManifest:
# Gate 1: No causal dependencies
has_dependencies = any(inv.depends_on for inv in state.invocations)
if not has_dependencies:
digest = cls._hash(json.dumps({
"invocations": str(len(state.invocations)),
"causality": "NONE"
}, sort_keys=True))
return CausalManifest(
verdict=CausalVerdict.NO_CAUSAL_DEPENDENCY,
directive="PROCEED: No causal dependencies, async order irrelevant.",
manifest_digest=digest
)
# Gate 2: Barriers required
if policy.require_barriers:
missing_barriers = [
inv.tool_id for inv in state.invocations
if inv.depends_on and not inv.causal_barrier_receipt
]
if missing_barriers:
return CausalManifest(
verdict=CausalVerdict.CAUSAL_BARRIER_MISSING,
directive=f"HALT: Tools with dependencies but no barriers: {missing_barriers}.",
manifest_digest=cls._hash(f"MISSING_BARRIER:{len(missing_barriers)}")
)
# Gate 3: External order check
if state.external_side_effect_order:
intended_order = tuple(
inv.tool_id for inv in sorted(
state.invocations,
key=lambda i: i.side_effect_sequence or 0
)
)
if intended_order != state.external_side_effect_order:
return CausalManifest(
verdict=CausalVerdict.CAUSAL_REVERSAL_DETECTED,
directive=f"HALT: Observed order {state.external_side_effect_order} != intended {intended_order}.",
manifest_digest=cls._hash(f"REVERSAL:{state.external_side_effect_order}")
)
# Gate 4: Verified
digest = cls._hash(json.dumps({
"invocations": tuple(sorted(inv.tool_id for inv in state.invocations)),
"barriers": str(all(inv.causal_barrier_receipt for inv in state.invocations if inv.depends_on)),
"external_order": state.external_side_effect_order or "UNVERIFIED"
}, sort_keys=True))
return CausalManifest(
verdict=CausalVerdict.CAUSAL_ORDER_VERIFIED,
directive=f"PROCEED: {len(state.invocations)} tool invocations with verified causal order.",
manifest_digest=digest
)