The Idempotency Illusion: Why AI Agents Need Stable Intent Over Generated Hashes A developer writing on tamiz.pro argues that AI agent frameworks relying on freshly generated UUIDs or prompt hashes as idempotency keys suffer from an "idempotency illusion," because hashes derived from an agent's reasoning trace change on every run even when the underlying request is identical. The proposed fix is to key deduplication on a canonical, normalized representation of user intent, with generated hashes demoted to fast-path cache keys or trace correlation IDs and the intent registry backed by a durable key-value store with TTL. Originally published on tamiz.pro https://tamiz.pro/insights/ai-agents-idempotency-stable-intent-generated-hashes . The Idempotency Illusion: Why AI Agents Need Stable Intent Over Generated Hashes In distributed agent architectures, idempotency is the safety net that prevents a single user request from spawning duplicate actions. Yet a growing pattern—using freshly generated UUIDs or cryptographic hashes as the sole key for idempotency checks—creates a silent failure mode: the idempotency illusion. This article unpacks why generated hashes break idempotency, what stable intent replaces them with, and how to wire your agents for true repeatability. Idempotency seems straightforward: repeat the same input, get the same result. In HTTP APIs, this maps to retry-safe headers and deduplication keys. In AI agent frameworks, the contract extends to tool calls, state mutations, and cross-turn continuations. The assumption is that if two requests are "the same," the system can recognize them and suppress the duplicate. Many agent platforms generate a fresh hash—often a UUIDv4 or a SHA-256 of the prompt—for every inbound request. The intent is to use this value as a cache key for "already processed?" lookups. But several structural flaws make this approach brittle: Stable intent shifts the key from a generated artifact to a canonical representation of what the user actually wants. Instead of hash prompt , intent is derived from the user’s goal, normalized against a stable schema. This can take several forms: To replace hash-based idempotency, integrate the pattern at three layers: php import hashlib, json, redis.asyncio as redis def normalize intent raw: str - str: Strip noise, lowercase, sort keys, extract canonical form cleaned = ‘ ’.join raw.lower .split return hashlib.sha256 cleaned.encode .hexdigest async def idempotent call redis conn: redis.Redirect, intent: str, coro : key = ‘intent:’ + intent cached = await redis conn.get key if cached: return json.loads cached result = await coro await redis conn.set key, json.dumps result , ex=3600 return result Generated hashes are not obsolete. They excel as transient session identifiers, trace correlation keys, or “poison-pill” markers for dead-letter queues. Use them when the cost of a duplicate is low and the input space is bounded. Pair them with a secondary intent check for safety-critical paths. Q: Can I use both generated hashes and stable intent together? A: Yes. Treat the hash as a fast-path cache key and the stable intent as the authoritative deduplication source. Collisions in the hash layer are caught by the intent layer. Q: What if my agent framework does not expose structured outputs? A: Normalize the raw text on the ingestion side. A preprocessing middleware that runs deterministic normalization before the model sees the prompt is a low-effort entry point. Q: How do I choose a persistence layer for the intent registry? A: If your agents are stateless serverless functions, use a durable KV store with TTL DynamoDB, Redis with Expiry . For long-running agent processes, an in-memory map with checkpoint persistence works fine. -- Links: tamiz.pro https://tamiz.pro | Tamiz’s Insights https://tamiz.pro/insights The core problem with using generated hashes is that they are often derived rather than intrinsic . If your agent generates a hash based on the current state of its internal reasoning trace, that hash changes every time the agent "thinks" differently, even if the ultimate intent remains identical. To solve this, we must shift the idempotency key from a derivative of the process to a canonical representation of the intent . Consider a user request: "Refactor the payment service to use Stripe instead of Braintree." hash step 1 reasoning + step 2 plan + current time → Unique every time. hash "refactor" + "payment service" + "stripe" → Stable, repeatable. We need a layer that extracts the "semantic core" of a task before executing it. This core becomes the idempotency key. Before generating a key, normalize all inputs. This means: user id=123 and userId=123 as equivalent if the schema is known . python import json import hashlib from typing import Any, Dict class IntentNormalizer: def init self, excluded fields: set = None : self.excluded fields = excluded fields or {"timestamp", "request id", "session token"} def normalize self, payload: Dict str, Any - bytes: """ Creates a canonical JSON representation of the payload excluding ephemeral fields. """ 1. Filter out ephemeral fields clean payload = {k: v for k, v in payload.items if k not in self.excluded fields} 2. Sort keys recursively for deterministic serialization clean payload = self. sort keys clean payload 3. Serialize with sorted keys and compact separators canonical json = json.dumps clean payload, sort keys=True, separators= ',', ':' return canonical json.encode 'utf-8' def sort keys self, obj: Any - Any: if isinstance obj, dict : return {k: self. sort keys obj k for k in sorted obj } if isinstance obj, list : Note: Sorting lists can be tricky if order matters. Only sort if the field is defined as a "set" in your schema. return self. sort keys item for item in obj return obj def generate stable intent key payload: Dict str, Any - str: normalizer = IntentNormalizer canonical bytes = normalizer.normalize payload return hashlib.sha256 canonical bytes .hexdigest Now, wrap your agent’s execution loop with this gate. The agent should always check the store before doing expensive work LLM calls, tool invocations . python class IdempotentAgentExecutor: def init self, kv store, agent logic : self.kv store = kv store self.agent logic = agent logic def execute self, task payload: Dict str, Any : 1. Generate the stable key idempotency key = f"agent:run:{generate stable intent key task payload }" 2. Check if already completed cached result = self.kv store.get idempotency key if cached result: Log that we're reusing a result return {"status": "replayed", "result": cached result, "idempotency key": idempotency key} 3. Lock to prevent concurrent execution of the same intent Use a distributed lock e.g., Redis SETNX with timeout if not self. acquire lock idempotency key : return {"status": "in progress", "idempotency key": idempotency key} try: 4. Execute the actual agent logic result = self.agent logic.run task payload 5. Persist the result with a TTL self.kv store.set idempotency key, result, ex=3600 24 24h TTL return {"status": "success", "result": result, "idempotency key": idempotency key} except Exception as e: Do NOT persist errors as successful results raise e finally: self. release lock idempotency key def acquire lock self, key: str - bool: Simplified for illustration; use a proper distributed lock library in production return self.kv store.set f"lock:{key}", "locked", nx=True, ex=300 def release lock self, key: str : self.kv store.delete f"lock:{key}" This is the hardest part of the "Idempotency Illusion." Two requests look the same but might have different underlying contexts. Scenario: If your task payload only contains {"action": "summarize"} , both generate the same hash. This is a false positive . Solution: Context Pinning You must include immutable references to the context in the hash, not the content itself. article id or a content hash that is already known to be stable. Update your IntentNormalizer to include context refs : payload = { "action": "summarize", "context refs": { "article id": "abc123", "model version": "gpt-4o-2024-01-01" Model version matters for determinism } } This will produce different keys for Article A vs B, even if the instruction is identical. Unit tests for idempotency are straightforward. You just assert that the key generation is deterministic across varying non-semantic inputs. python import pytest def test intent hash is stable to noise : payload1 = { "task": "calculate", "inputs": {"a": 10, "b": 20}, "request id": "req-123", "timestamp": "2023-10-27T10:00:00Z" } payload2 = { "task": "calculate", "inputs": {"b": 20, "a": 10}, Different order, same semantic "request id": "req-456", "timestamp": "2023-10-27T11:00:00Z" } key1 = generate stable intent key payload1 key2 = generate stable intent key payload2 assert key1 == key2, "Hashes should match despite noise and key ordering" def test intent hash changes with semantics : payload1 = {"task": "calculate", "inputs": {"a": 10, "b": 20}} payload2 = {"task": "calculate", "inputs": {"a": 10, "b": 21}} key1 = generate stable intent key payload1 key2 = generate stable intent key payload2 assert key1 = key2, "Hashes should differ for different inputs" The "Idempotency Illusion" stems from treating AI agents like stateless functions where hash input = output . In reality, agents are stateful, non-deterministic systems that interact with dynamic environments. To achieve true idempotency: By shifting from generated hashes to stable intents , you turn retries from a source of chaos into a reliable safety net, allowing your agent systems to operate with the confidence of traditional, deterministic infrastructure. Links: tamiz.pro https://tamiz.pro | Tamiz’s Insights https://tamiz.pro/insights