How must tool result caches invalidate across tool generation changes?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session caches tool call results (query results, API responses, computed values) to reduce latency and cost, and the tool itself is upgraded to a new generation (new version, changed behavior, or changed dependencies), requiring cache invalidation. Does not apply to tools with strict immutability guarantees or caches with cryptographic binding to tool versions.
Core Distinction
The separation between **cache TTL** (time-based expiration) and **cache generation validity** (the cached result was produced by a compatible tool generation). Relying on TTL creates the **Generation Skew Cache**: the cached result is still within TTL, but the tool has been upgraded to a new generation whose behavior differs, and the cached stale result is returned instead of the new generation's result.
What Was Observed
In tool-cached agent architectures, failing to invalidate on generation change produces three primary failure modes:
- **Behavioral Regression:** A tool's algorithm is improved (faster, more accurate), but cached results from the old algorithm continue to be returned, and the improvement is never realized for cached keys.
- **Security Regression:** A tool has a security fix in a new generation, but cached results reflect the old, vulnerable behavior.
- **Silent Inconsistency:** Different cache keys have been populated at different times (some from the old generation, some from the new), and the same logical query returns different results depending on cache state.
Recorded Elements
- **`cache_generation_signature`:** For each cache entry, a cryptographic proof of the exact tool generation that produced the result, allowing the cache to detect stale generations.
- **`tool_generation_current_receipt`:** A cryptographic proof of the tool's current generation, allowing the cache to compare against stored generation signatures.
- **`generation_change_event_receipt`:** When the tool's generation changes, a cryptographic record of the event, allowing proactive cache invalidation.
- **`cache_hit_generation_attribution_receipt`:** For each cache hit, the cryptographic proof of which generation the hit reflects, allowing audit of generation consistency.
Condition of Validity
* **Applies when:** The session caches tool results, tools can be upgraded, and cached results could be stale relative to the new generation. * **Does not apply when:** Tools are strictly immutable (no generation changes), or the cache is bound to tool generation via cache key.
Decision Boundary
- **Does the session cache tool results?** If no, standard tool handling applies. If yes, proceed.
- **Does each cache entry include the `cache_generation_signature`?** If no, the cache cannot detect generation drift. The session must add signatures before caching.
- **Has the tool's generation changed since cache population?** If yes, the cache must invalidate the entries or accept stale results with explicit acknowledgment.
- **Conversely, when can the cache serve results across generations?** Only when the new generation is verified to be behavior-compatible for the specific result type (e.g., schema-identical, value-identical in tested cases), or when the cache is explicitly tagged as "generation-tolerant."
Operational Effect
- **Generation Consistency:** Cached results reflect the current tool generation, preventing silent divergence.
- **Proactive Invalidation:** Generation change events trigger cache invalidation before stale results are served.
- **Audit Clarity:** Per-hit generation attribution allows audits to verify consistency.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class CacheVerdict(Enum):
NO_CACHE = auto()
GENERATION_VERIFIED = auto()
STALE_GENERATION = auto()
NO_GENERATION_SIGNATURE = auto()
@dataclass(frozen=True)
class ToolCacheEntry:
cache_key: str
cached_generation: str | None
current_tool_generation: str
compatible_across_generations: bool
@dataclass(frozen=True)
class ToolCachePolicy:
require_generation_signature: bool
@dataclass(frozen=True)
class ToolCacheManifest:
verdict: CacheVerdict
directive: str
manifest_digest: str
class ToolGenerationCacheGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_cache_hit(
cls, entry: ToolCacheEntry, policy: ToolCachePolicy
) -> ToolCacheManifest:
# Gate 1: No cache entry
if not entry.cache_key:
digest = cls._hash(json.dumps({
"cache": "MISS"
}, sort_keys=True))
return ToolCacheManifest(
verdict=CacheVerdict.NO_CACHE,
directive="PROCEED: Cache miss, execute tool.",
manifest_digest=digest
)
# Gate 2: Generation signature presence
if policy.require_generation_signature and not entry.cached_generation:
return ToolCacheManifest(
verdict=CacheVerdict.NO_GENERATION_SIGNATURE,
directive=f"HALT: Cache entry {entry.cache_key} lacks generation signature.",
manifest_digest=cls._hash(f"NO_GEN:{entry.cache_key}")
)
# Gate 3: Compatible across generations
if entry.compatible_across_generations:
digest = cls._hash(json.dumps({
"cache": entry.cache_key,
"compatible": "TRUE"
}, sort_keys=True))
return ToolCacheManifest(
verdict=CacheVerdict.GENERATION_VERIFIED,
directive=f"PROCEED: Cache {entry.cache_key} cross-generation compatible.",
manifest_digest=digest
)
# Gate 4: Generation drift
if entry.cached_generation and entry.cached_generation != entry.current_tool_generation:
return ToolCacheManifest(
verdict=CacheVerdict.STALE_GENERATION,
directive=f"HALT: Cache {entry.cache_key} generation {entry.cached_generation} != current {entry.current_tool_generation}.",
manifest_digest=cls._hash(f"STALE:{entry.cache_key}")
)
# Gate 5: Verified
digest = cls._hash(json.dumps({
"cache": entry.cache_key,
"generation": entry.current_tool_generation
}, sort_keys=True))
return ToolCacheManifest(
verdict=CacheVerdict.GENERATION_VERIFIED,
directive=f"PROCEED: Cache {entry.cache_key} verified for current generation.",
manifest_digest=digest
)