Idempotencia en tools de agentes: una clave, un efecto
Resumen
El retry de una tool no es un segundo cobro. La clave se genera antes del POST, se reusa en el timeout y se rechaza si el payload cambió. Stripe Idempotency-Key, AWS ClientToken e IETF 400/422/409. Distinto de webhooks, de retry y de ejecución durable.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente que reintenta una tool no está “intentando otra vez”: está volviendo a empujar un efecto. Si la tool cobra, crea un ticket o manda un mensaje, el timeout no te dice si el POST llegó. Sin clave de idempotencia, el segundo intento duplica. Con clave, el servidor reconoce el mismo intento y devuelve el mismo resultado.
Esto no es idempotencia de webhooks: ahí el proveedor reenvía un evento y tú deduplicas event.id. Aquí tú eres el cliente de una API mutante. Tampoco es reintentos y rate limits: el backoff decide cuándo reintentar; la clave decide qué reintento es el mismo. Ni ejecución durable: el checkpoint omite pasos ya confirmados; la clave protege el paso que todavía no confirmó.
Contrato: la clave nace antes del POST, se reusa en el timeout y se rechaza si el payload cambió.
Qué es (y qué no es) una tool idempotente
Por RFC9110, GET, PUT y DELETE ya son idempotentes. POST y PATCH no. Las tools peligrosas de un agente casi siempre son POST: cobrar, crear, notificar.
Stripe (docs markdown, HTTP 200 el 2026-09-06): safely retrying requests without accidentally performing the same operation twice. Guardas el status y el body del primer request de esa clave, including 500 errors. Las claves duran al menos 24 horas y luego se podan. Hasta 255 caracteres. Sugieren UUID v4. GET/DELETE no las necesitan.
AWS EC2 (HTML canónico, HTTP 200 el 2026-09-06): un client token es unique, case-sensitive string of up to 64 ASCII characters. Misma token + mismos parámetros = el retry no crea otro recurso. Misma token + parámetros distintos (salvo Region/AZ) = IdempotentParameterMismatch. Algunas acciones (TerminateInstances) ya son idempotentes por defecto; RunInstances pide token.
El draft IETF Idempotency-Key (07, 15 Oct 2025, expires 18 Apr 2026; HTTP 200): el cliente manda Idempotency-Key; el recurso puede añadir un fingerprint del payload. Primera vez: procesa. Duplicado después de terminar: replay del resultado. Concurrente: 409. Misma clave, payload distinto: 422. Clave ausente cuando es obligatoria: 400.
Dónde vive la clave (no en el modelo)
El LLM no inventa la clave en cada turno. Si la genera el prompt, cada retry es un UUID nuevo y duplicas el cobro. La genera el runtime antes de llamar la tool:
- Identidad estable del intento:
tenant,run_id,step_id, nombre de la tool. - UUID v4 o hash de esa tupla. Nunca email, NIT ni número de tarjeta — Stripe lo dice: Avoid using sensitive data.
- Guardar
{key, args_hash, status}antes del fetch. - Timeout o 5xx: la misma key y los mismos args.
- Si el humano cambia el monto o el destinatario: clave nueva. Stripe compara parámetros y error si no coinciden; IETF responde 422.
El adapter de la tool —el mismo patrón de API existente como tools— inyecta el header. El modelo solo ve ok / conflict / in_flight, no el token.
Tabla: retry sí, duplicar no
| Señal | Reusar la misma clave | Nueva clave | Por qué |
|---|---|---|---|
| Timeout / socket close / 502/503/504 | Sí | No | No sabes si el POST ejecutó. Stripe: network errors son el caso de la clave |
| 500 ya cacheado por el proveedor | Sí (replay) | No | Stripe guarda también el 500 |
| 409 concurrente (IETF) | Sí, esperar y reusar | No | Hay un request outstanding; no corrijas payload |
422 / IdempotentParameterMismatch | No | Sí, si el cambio es deliberado | Misma clave, args distintos |
| 401 / 400 de validación / 429 | Depende | Suele sí | Stripe: rate limiter y auth corren antes de la capa de idempotencia |
| 4xx de negocio (tarjeta declinada) ya ejecutado | Sí (replay del 4xx) | No, salvo que corrijas el request | Stripe cachea el 400 si el endpoint empezó |
| GET / DELETE | Irrelevante | Irrelevante | Idempotentes por definición |

Fingerprint y conflicto
IETF 2.4: el fingerprint puede ser checksum del payload, de campos seleccionados o una firma. Úsalo para detectar abuso, no para “arreglar” un retry. Si el agente reintenta con la misma key y el adapter reordenó JSON, un checksum naive dispara 422. Canonicaliza: claves ordenadas, sin whitespace, sin campos que el SDK inyecta (timestamp de envío).
IETF 2.6 concurrente: si el primer POST sigue en vuelo, el segundo 409. El agente no debe “corregir” y reenviar con otra key: espera, backoff, misma key. Si el techo de reintentos se agota, el paso va a DLQ con razón idempotency_in_flight, no a un segundo cobro.
IETF 5 (Security): keys de baja entropía filtran cache de otros clientes. En el servidor, la lookup key es compuesta: tenant_id + Idempotency-Key. Valida formato (UUID) antes de tocar el cache. No pongas la clave en logs ni en el system prompt.
Qué no hace el runtime
- No regenera la clave porque “el modelo lo pidió otra vez”. El
tool_call_iddel proveedor de LLM no es la clave de negocio: un segundo turno puede emitir otrotool_call_idpara el mismo cobro. - No usa
Date.now()ni un contador de reintentos dentro de la clave. Eso convierte cada retry en un intento nuevo. - No manda
Idempotency-Keyen GET. Stripe: Don’t send idempotency keys in GET and DELETE… it has no effect. - No trata
storede credenciales ni el body del webhook como esta capa. Webhooks: dedup de evento. Tools: clave de salida. - No asume que el 24 h de Stripe es tu TTL. Publica el expiry (IETF 2.3). Para un agente, TTL ≥ ventana de retry + buffer; si el durable workflow puede reanudarse a los 7 días, 24 h no basta: guarda el resultado en tu store.
Checklist
- Lista tools mutantes (
POST/PATCH, side-effect). Las de lectura no llevan clave. - El runtime genera la clave antes del primer fetch; el modelo no la ve.
- Persistencia:
key, hash canónico de args, tenant, resultado (oin_flight). - Header
Idempotency-KeyoClientTokensegún el proveedor. Stripe ≤255; AWS ≤64 ASCII. - Retry de red: mismos args, misma clave. Cambio de args: clave nueva.
- Mapea 409 → espera; 422 /
IdempotentParameterMismatch→ no reintentar a ciegas. - Lookup compuesta
tenant + key. Formato fijo. Cero PII en la key. - TTL publicado ≥ ventana de retry. Tras TTL, una clave reusada es un request nuevo (Stripe lo dice).
- Tests sin LLM: timeout simulado no duplica; payload distinto con misma key falla cerrado.
- Observabilidad:
idempotency_replay,idempotency_conflict,idempotency_mismatch. No loguees la key cruda.

FAQ
¿Puedo usar el tool_call_id del modelo como Idempotency-Key? No. Ese id identifica un mensaje en el chat, no un efecto de negocio. Un loop de agente puede emitir dos tool calls para “el mismo” cobro. La clave sale de run_id + step_id (o del id que el humano ya tenía: invoice_id).
¿Stripe guarda los 400? Si el endpoint empezó a ejecutarse, sí, incluso un 400. Si falló validación antes (falta API key, 429, params inválidos), a menudo no cachea. Stripe: the safest strategy where 4xx errors are concerned is to always generate a new idempotency key — después de corregir el request, no en el retry ciego de red.
¿Qué hago con 409? Nada de payload nuevo. Es el caso concurrente del draft IETF. Backoff y la misma key. Si el original nunca termina, DLQ, no un segundo POST.
¿Esto sustituye durable execution? No. Durable confirma después del efecto. La clave cubre el hueco durante el efecto. Las dos capas se complementan: checkpoint + idempotency key en cada tool mutante.
Si estás armando el primer agente, el curso de instalar un agente cubre el loop; esta guía es la compuerta para que el loop no cobre dos veces. El hub de seguridad, coste y operación agrupa retry, breaker, DLQ y esta clave.
Lecturas relacionadas
Sigue explorando AgentOps y otras piezas para builders.



