What invariants detect silent non-determinism introduction by external tools?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session depends on a tool's deterministic behavior (same input → same output), and the tool silently becomes non-deterministic (due to version change, backend change, or injected randomness) without an explicit version bump. Does not apply to tools that are documented as non-deterministic or sessions that do not depend on determinism.
Core Distinction
The separation between **documented determinism** (the tool's claimed behavior contract) and **observed determinism** (the tool's actual behavior across repeated calls). Relying on the contract creates the **Silent Non-Determinism Drift**: the tool was deterministic when the session's assumptions were set, has since become non-deterministic, and the session's caching, deduplication, or idempotency logic silently fails.
What Was Observed
In deterministic-tool-dependent agent architectures, failing to detect silent non-determinism produces three primary failure modes:
- **Cache Corruption:** The session caches tool responses assuming determinism. The tool becomes non-deterministic, the cache serves stale responses that no longer match the tool's actual output, and downstream consumers act on data the tool would no longer return.
- **Idempotency Breakage:** The session uses tool calls as idempotency keys, assuming repeat calls give the same result. The tool becomes non-deterministic, retries return different results, and the session's state becomes inconsistent.
- **Silent Duplicate Divergence:** Multiple sessions call the same tool with the same input, expecting identical outputs. Divergent outputs propagate to dependent state, and the divergence cannot be attributed to any single session's actions.
Recorded Elements
- **`determinism_baseline_receipt`:** At session start, a cryptographic baseline of the tool's determinism (N identical calls produce identical hashes), establishing the expected behavior.
- **`determinism_probe_receipt`:** Periodic re-probes of the tool's determinism (same calls, same expected hashes), detecting drift during the session.
- **`determinism_contract_signature`:** The tool's documented determinism contract (from documentation or SLA), distinct from observed behavior, so contract violations are detectable.
- **`nondeterminism_detection_signature`:** When drift is detected, the cryptographic proof of the change (specific inputs producing divergent outputs, timestamps of observed determinism before drift).
Condition of Validity
* **Applies when:** The session depends on a tool's determinism, the tool can silently become non-deterministic, and downstream logic assumes determinism. * **Does not apply when:** The tool is documented as non-deterministic, or the session's logic is determinism-agnostic.
Decision Boundary
- **Does the session assume tool determinism?** If no, standard tool use applies. If yes, proceed.
- **Is a `determinism_baseline_receipt` available?** If yes, the session can detect drift. If no, all determinism assumptions are unverified.
- **Does the `determinism_probe_receipt` show drift from baseline?** If yes, the tool has become non-deterministic. If no, proceed with baseline.
- **Conversely, when can a session continue using a tool after drift detection?** Only when the drift is documented (the session can update its assumptions), or the session's specific use is determinism-insensitive (a use that does not require identical outputs).
Operational Effect
- **Cache Integrity:** Caching remains sound because determinism is monitored.
- **Idempotency Preservation:** Retries remain safe because determinism is preserved.
- **Divergence Attribution:** Silent drift is caught and attributed to the tool, not to session bugs.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class DeterminismVerdict(Enum):
NO_DETERMINISM_DEPENDENCY = auto()
BASELINE_STABLE = auto()
SILENT_DRIFT_DETECTED = auto()
BASELINE_UNAVAILABLE = auto()
@dataclass(frozen=True)
class DeterminismBaseline:
tool_id: str
baseline_hash: str
sample_count: int
baseline_epoch: float
@dataclass(frozen=True)
class DeterminismProbe:
tool_id: str
probe_epoch: float
probe_hashes: tuple[str, ...]
contract_signature: str | None
@dataclass(frozen=True)
class SessionDeterminismContext:
session_id: str
depends_on_determinism: bool
@dataclass(frozen=True)
class DeterminismPolicy:
require_probe_interval_sec: float
@dataclass(frozen=True)
class DeterminismManifest:
verdict: DeterminismVerdict
directive: str
manifest_digest: str
class SilentNondeterminismGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_probe(
cls, baseline: DeterminismBaseline | None, probe: DeterminismProbe,
ctx: SessionDeterminismContext, policy: DeterminismPolicy
) -> DeterminismManifest:
# Gate 1: No dependency
if not ctx.depends_on_determinism:
digest = cls._hash(json.dumps({
"tool": probe.tool_id,
"session": ctx.session_id,
"dependency": "NONE"
}, sort_keys=True))
return DeterminismManifest(
verdict=DeterminismVerdict.NO_DETERMINISM_DEPENDENCY,
directive=f"PROCEED: Session does not depend on {probe.tool_id} determinism.",
manifest_digest=digest
)
# Gate 2: Baseline presence
if baseline is None:
return DeterminismManifest(
verdict=DeterminismVerdict.BASELINE_UNAVAILABLE,
directive=f"HALT: No determinism baseline for {probe.tool_id}.",
manifest_digest=cls._hash(f"NO_BASELINE:{probe.tool_id}")
)
# Gate 3: Probe interval
since_last = probe.probe_epoch - baseline.baseline_epoch
if since_last < policy.require_probe_interval_sec:
digest = cls._hash(json.dumps({
"tool": probe.tool_id,
"since_last": f"{since_last:.0f}",
"interval": f"{policy.require_probe_interval_sec:.0f}"
}, sort_keys=True))
return DeterminismManifest(
verdict=DeterminismVerdict.BASELINE_STABLE,
directive=f"PROCEED: Probe interval {since_last:.0f}s within {policy.require_probe_interval_sec:.0f}s.",
manifest_digest=digest
)
# Gate 4: Drift detection
unique_hashes = set(probe.probe_hashes)
if len(unique_hashes) > 1 or (unique_hashes and baseline.baseline_hash not in unique_hashes):
return DeterminismManifest(
verdict=DeterminismVerdict.SILENT_DRIFT_DETECTED,
directive=f"HALT: {probe.tool_id} silent non-determinism detected ({len(unique_hashes)} unique hashes across probes).",
manifest_digest=cls._hash(f"DRIFT:{probe.tool_id}:{len(unique_hashes)}")
)
# Gate 5: Stable
digest = cls._hash(json.dumps({
"tool": probe.tool_id,
"baseline": baseline.baseline_hash,
"probe_epoch": f"{probe.probe_epoch:.0f}",
"samples": str(len(probe.probe_hashes))
}, sort_keys=True))
return DeterminismManifest(
verdict=DeterminismVerdict.BASELINE_STABLE,
directive=f"PROCEED: {probe.tool_id} determinism verified.",
manifest_digest=digest
)