En un agente, las tools se reintentan. Mucho. Las razones:
Si la tool tiene side effects (crea un registro, cobra plata, manda un mail), reintentar sin protección significa: dos registros, doble cobro, dos mails. Para el usuario, eso es un bug en producción que cuesta confianza y dinero.
La solución es idempotencia: diseñar la tool de modo que llamarla N veces con los mismos parámetros equivale a llamarla 1 vez.
Naturalmente idempotente. Algunas operaciones ya lo son. set_status("active") llamada 5 veces deja el mismo estado. No hace falta hacer nada extra.
Por idempotency_key. El cliente (o el agente) genera una key única por intent, y la pasa. El servidor recuerda esa key y el resultado. Si la key se repite, devuelve el resultado anterior sin re-ejecutar. Este es el patrón canónico para crear cosas (pagos, órdenes, registros).
Por chequeo de estado. Antes de actuar, el handler chequea si el efecto ya ocurrió. assign_shift puede chequear si el crewmate ya está asignado a ese turno y, si sí, devolver { ok: true, already_assigned: true }.
idempotency_keyCuando soportás idempotency_key:
req-{uuid} o similar.replayed o equivalente. El agente necesita saber si la operación creó algo nuevo o devolvió un resultado existente.async function handle({ ..., idempotency_key }) {
if (idempotency_key) {
const prev = await db.payments.findByIdempotencyKey(idempotency_key);
if (prev) {
return { ok: true, payment_id: prev.payment_id, replayed: true };
}
}
const payment = await db.payments.create({ ..., idempotency_key });
return { ok: true, payment_id: payment.payment_id, replayed: false };
}A la derecha tenés register_payment. El starter cobra cada vez que se llama. Convertilo en idempotente.
En agentes que tocan dinero, estado externo, o humanos, la idempotencia no es opcional. Es la diferencia entre un sistema que se puede operar y un sistema que da soporte cada semana porque "se duplicó".