# The Idempotency Illusion: Why AI Agents Need Stable Intent Over Generated Hashes

> Source: <https://dev.to/tamizuddin/the-idempotency-illusion-why-ai-agents-need-stable-intent-over-generated-hashes-4f7j>
> Published: 2026-10-21 18:05:04+00:00

*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)*
