Un agente de IA puede llegar a una API y perder la respuesta durante el regreso. El agente ve un timeout y supone que no ocurrió nada. Si reintenta, puede crear otro pedido, enviar otro mensaje o escribir dos veces el mismo registro.

La solución es tratar la respuesta y el efecto como hechos separados. Antes de ejecutar, el runtime crea un identificador estable para la acción. Después, guarda el recibo del executor o consulta una poscondición en el sistema externo. El estado final debe indicar si el efecto está confirmado, ausente o sigue siendo desconocido.

Este problema es más específico que evitar tool calls duplicadas al cambiar de LLM. El fallback decide cuándo cambiar el intento del modelo. Este artículo explica qué hacer cuando la operación externa pudo haber comenzado. Para elegir la capa que conserva ese estado entre intentos, consulta también ejecución durable para agentes de IA. El código es ilustrativo y no se ejecutó contra un proveedor ni un sistema de producción.

Diagrama muestra una tool call de un agente cruzando una frontera de evidencia externa antes de continuar o verificarse.

Respuesta corta

  • Una respuesta ausente indica lo que recibió el cliente, no lo que hizo el servidor.
  • Persiste un actionId antes de enviar una operación con efectos.
  • Consulta una poscondición independiente y clasifícala como confirmed, not_found o unknown.
  • Reintenta solo después de not_found, o cuando la API usa la misma clave de idempotencia para deduplicar el intento.

¿Por qué un timeout no demuestra que la tool call falló?

Un timeout separa el envío del acuse. La petición pudo descartarse antes de que el servidor actuara, aceptarse y procesarse sin respuesta, o aplicar solo una parte del cambio. El runtime no debe convertir todo error de transporte en failed. En 2026, Isham Kalappurackal Mansoor, Abhishek Phadke y Pratip Rana describen esta brecha entre el canal de respuesta y el canal de efecto en "Verified Tool Calls Improve LLM Agent Reliability Under Non-Atomic Failures" (arXiv, 2026).

El riesgo aparece cuando una tool cambia el estado. Una consulta de solo lectura suele poder repetirse. Crear un pago, conceder acceso, enviar un correo o cambiar un registro exige saber qué operación lógica se está retomando. El texto que recibió el modelo no es un recibo del servicio.

En 2026, la tarea simulada record_invoice del artículo produjo efectos duplicados en el 32%, 52% y 76% de las ejecuciones base con niveles bajo, medio y alto de fallos. Su wrapper de verificación registró 0%, 16% y 20%. Son cifras de un entorno controlado con fallos inyectados, no tasas de producción. Sí muestran por qué "verificar y después reintentar" necesita su propio contrato.

El caso que falta suele ser un tercer resultado. success y failure describen lo que observó el cliente. unknown describe el límite del conocimiento actual: la acción pudo ocurrir, pero todavía no hay evidencia suficiente para reintentarla o cerrarla.

¿Qué debe registrar el runtime antes de la operación?

Registra la intención antes del envío, con una clave que permanezca igual en todos los intentos de la misma acción lógica. El registro mínimo debe conectar la tarea del agente con el efecto esperado sin depender del ID de un mensaje concreto del proveedor.

Una tool del dominio puede recibir un actionId, argumentos normalizados y la poscondición que verificará después. El ID de la llamada del modelo ayuda a seguir la conversación, pero no debe ser la única clave. Un retry o fallback puede producir otro mensaje del proveedor para la misma intención.

type EffectState = "planned" | "sent" | "confirmed" | "not_found" | "unknown";

type ActionRecord = {
  actionId: string;
  toolName: string;
  inputHash: string;
  expectedEffect: string;
  state: EffectState;
  externalId?: string;
  receiptId?: string;
  updatedAt: string;
};

async function prepareAction(
  actionId: string,
  toolName: string,
  inputHash: string,
  expectedEffect: string,
): Promise<ActionRecord> {
  const current = await actionStore.get(actionId);
  if (current) return current;

  const record: ActionRecord = {
    actionId,
    toolName,
    inputHash,
    expectedEffect,
    state: "planned",
    updatedAt: new Date().toISOString(),
  };

  await actionStore.insertIfAbsent(record);
  return record;
}

insertIfAbsent debe ser atómico en el almacén que coordina a los workers. Un Map local al proceso no cubre reinicios, dos réplicas ni una nueva entrega de la cola. El ejemplo tampoco resuelve la concurrencia por sí solo. Hace visible el registro que una implementación real debe proteger.

El AWS Well-Architected Framework, "Make mutating operations idempotent" recomienda tokens únicos y su seguimiento para que los mensajes repetidos no produzcan la misma acción dos veces. La clave ayuda al servicio a reconocer una repetición. No sustituye una consulta cuando el primer resultado es ambiguo.

¿Qué hace confiable a un recibo?

Un recibo debe indicar lo que observó el executor. No necesita el payload secreto ni otra versión de la historia del modelo. Necesita la identidad de la acción, la decisión del servicio, un identificador externo cuando exista y el momento en que se registró la evidencia.

Un resultado útil separa el transporte del efecto:

Estado Lo que sabemos Siguiente paso
confirmed La poscondición o el recibo prueba el efecto Continuar sin ejecutarlo otra vez
not_found Una consulta confiable no encontró el efecto Revisar la política y reintentar con la misma clave
unknown La consulta no permite decidir Detener, esperar, reconciliar o pedir revisión
rejected La operación fue rechazada antes del efecto Corregir la entrada o la autorización

No conviertas HTTP 200 en confirmed sin definir qué se confirmó. Una API puede aceptar un comando asíncrono, devolver un identificador y aplicar el efecto después. El recibo prueba la aceptación; la poscondición prueba el cambio de estado que necesita el agente.

El OpenAI Agents SDK expone elementos separados para llamadas y resultados de tools, además de requestId cuando el proveedor lo envía. Su documentación de Results también trata el estado serializable como una superficie para retries y reanudación. Esto ayuda a auditar el run, pero la aplicación aún debe probar el efecto en el servicio modificado.

¿Cómo verificar la poscondición antes de reintentar?

Consulta una condición que no dependa solo de la respuesta perdida. Puede buscar un ID externo, una fila que contenga la clave de operación, el estado de un workflow u otro hecho que el servicio reconozca. Cuando las lecturas tienen consistencia eventual, espera la ventana documentada o conserva el estado como desconocido. Una lectura vacía inmediata no siempre demuestra ausencia.

Mantén el orden estricto:

  1. Crea o recupera el registro de la acción lógica.
  2. Envía la tool call con la misma clave de idempotencia cuando el servicio la admita.
  3. Guarda el recibo si llega la respuesta.
  4. Si se pierde la respuesta, consulta la poscondición con actionId o externalId.
  5. Con confirmed, continúa. Con not_found, reintenta según la política. Con unknown, no inventes un resultado.
type Verification =
  | { state: "confirmed"; externalId?: string }
  | { state: "not_found" }
  | { state: "unknown"; reason: string };

async function retryIfAbsent(actionId: string): Promise<Verification> {
  const action = await actionStore.get(actionId);
  if (!action) throw new Error("action was not prepared");

  const observed = await verifyPostcondition(action);

  if (observed.state === "confirmed") {
    await actionStore.markConfirmed(actionId, observed.externalId);
    return observed;
  }

  if (observed.state === "unknown") {
    await actionStore.markUnknown(actionId, observed.reason);
    return observed;
  }

  const result = await executeTool({
    actionId,
    idempotencyKey: actionId,
    inputHash: action.inputHash,
  });

  await actionStore.saveReceipt(actionId, result.receiptId);
  return { state: "confirmed", externalId: result.externalId };
}

Este es un límite didáctico. En producción, la verificación y el envío pueden competir con otro worker, y el almacén puede cambiar entre ambos pasos. La operación remota debe aceptar la clave, o el executor necesita propiedad y reconciliación para cubrir la carrera.

Este artículo no presenta un benchmark privado. La prueba práctica es sencilla: si el equipo no puede señalar el hecho externo que convierte unknown en confirmed, su retry todavía depende de una suposición.

¿Cuándo resuelve el problema una clave de idempotencia?

La clave resuelve la deduplicación cuando el servicio receptor la incluye en su contrato y persiste el resultado asociado. La misma clave debe representar la misma intención lógica. No crees una clave nueva solo porque el modelo fue consultado otra vez.

El servicio también necesita una regla para recibir la misma clave con argumentos diferentes. Una política segura rechaza el conflicto o devuelve el resultado original. Tratar la segunda carga como una intención nueva elimina la utilidad de la clave.

La orientación de AWS indica que servicios y consumidores deben pasar el token a los servicios posteriores y evitar repetir un efecto al procesar el mismo mensaje (AWS Well-Architected Framework, 2026). Es una responsabilidad de cada frontera, no una garantía automática del agente.

Si la API no ofrece idempotencia, la verificación todavía puede reducir duplicados. Queda una carrera entre una consulta not_found y dos intentos concurrentes. Serializa la propiedad de la acción, usa una restricción única donde se escribe o deja el estado unknown para reconciliación. No prometas ejecución exactamente una vez si la infraestructura no ofrece esa semántica.

¿Cómo probar timeouts, lecturas atrasadas y efectos parciales?

Prueba el contrato con un executor falso que separe el envío del momento en que el estado se vuelve visible. El caso importante no es solo una excepción antes de la llamada. Es una interrupción después del envío y antes del recibo.

Escenario Observación simulada Resultado esperado
Fallo antes del envío No existe efecto externo Reintentar con la misma acción
Timeout después del envío El efecto existe, pero no llegó el recibo Consultar antes de reintentar
Lectura atrasada La primera consulta no ve el efecto Mantener unknown o esperar la ventana
Efecto parcial Solo se cumple parte de la poscondición Reconciliar, compensar o detener
Intentos concurrentes Dos workers usan el mismo actionId Uno posee la acción; el otro no la duplica
Misma clave, entrada distinta Los hashes son diferentes Rechazar el conflicto

Observa el efecto del dominio, no solo la cantidad de llamadas a un mock. Si una tool crea una fila y publica un mensaje, verifica ambos hechos o documenta cuál es la poscondición principal. Un mock que siempre devuelve "ok" no ejercita la frontera que causa el fallo.

El estudio de 2026 usado arriba evaluó su wrapper en un entorno simulado con fallos inyectados y distintas tareas de tools. Es evidencia de que la técnica puede probarse, no una garantía de que cualquier arquitectura tendrá la misma tasa. Usa el patrón con la base de datos, la cola o la API que tu agente realmente llama.

¿Qué registrar cuando el efecto sigue siendo desconocido?

Haz que unknown sea un estado operativo visible. Registra el motivo, la hora de la última consulta, la clave de acción, el tipo de efecto y la siguiente acción permitida. Un operador debe encontrar el caso sin preguntar al modelo qué cree que ocurrió.

El OpenAI Agents SDK, "Tools" documenta timeouts por tool, resultados de error y metadatos de ejecución. Esas funciones delimitan la ejecución local. No confirman un cambio en un servicio externo. La aplicación debe combinar el evento de la tool con un recibo o una lectura de poscondición.

No escondas el estado en un resumen de conversación. El transcript puede decir que el agente pidió create_invoice, pero no demuestra que exista una factura. Una investigación necesita actionId, hash de argumentos, estado observado, ID externo e intentos. Elimina secretos y datos personales de los logs.

Una buena observabilidad de agentes pregunta más que "¿qué tool se llamó?". Pregunta "¿qué efecto se probó, con qué evidencia y qué acción quedó bloqueada mientras el estado era desconocido?". Eso conecta el runtime con el sistema que puede causar el daño.

Checklist para añadir verificación a un runtime de agentes

Usa esta secuencia al añadir una tool con efectos externos:

  1. Clasifica la tool como lectura, escritura reversible, escritura irreversible u operación asíncrona.
  2. Define actionId en el runtime antes de la llamada del modelo o en el primer límite determinista de intención.
  3. Persiste el hash de argumentos y rechaza la misma clave con una entrada diferente.
  4. Define la poscondición en el estado externo, no en texto generado por el modelo.
  5. Haz que el executor devuelva un recibo con estado e ID externo cuando exista.
  6. Modela confirmed, not_found, unknown y rejected por separado.
  7. Consulta el efecto después de un timeout, cancelación o pérdida de conexión.
  8. Reintenta solo con una clave que el servicio deduplique, o después de comprobar de forma confiable la ausencia.
  9. Prueba lecturas atrasadas, concurrencia, efectos parciales y conflictos de entrada.
  10. Muestra unknown en la observabilidad y asigna quién hará la reconciliación.

Para conservar este estado entre procesos, consulta cómo pausar y reanudar un agente de IA sin reiniciar. Para definir la frontera entre un intento del modelo y una acción del dominio, consulta validación de tool calls en TypeScript.

Preguntas frecuentes

¿Un timeout significa que debo reintentar la tool call?

No. El timeout solo demuestra que el cliente no recibió una respuesta a tiempo. Consulta la poscondición con el identificador de acción o usa la clave de idempotencia del servicio. Si la consulta no puede decidir, conserva unknown y no crees una segunda acción por suposición.

¿HTTP 200 demuestra que ocurrió el efecto?

No necesariamente. 200 puede confirmar que el servicio aceptó un comando asíncrono, no que terminó el procesamiento. Define el recibo o estado externo que representa la poscondición. Marca confirmed solo cuando la evidencia está vinculada con la acción correcta.

¿Puedo usar como clave el ID de tool call del proveedor?

Úsalo para rastrear el mensaje del proveedor, pero no lo conviertas en la única clave del dominio. Un retry o fallback puede crear otro ID para la misma intención. Crea una clave en el runtime y pásala al executor cuando el contrato del servicio lo permita.

¿Qué ocurre si la API no admite idempotencia?

Consulta el efecto antes de reintentar, serializa la propiedad de la acción y conserva unknown cuando la consulta no sea confiable. Sin deduplicación en el receptor, aún existe una carrera. Declara la garantía real en lugar de prometer ejecución exactamente una vez.

Conclusión

Una tool call no termina cuando el modelo recibe una respuesta. Para los efectos, el runtime debe distinguir el acuse de transporte del estado externo. Un actionId, un recibo y una poscondición hacen verificable esa diferencia.

El camino seguro es corto: registra la intención, envía con una clave estable, guarda el resultado, consulta después de perder la respuesta y reintenta solo cuando se prueba la ausencia o la API deduplica la operación. Cuando nada de eso es posible, unknown es un estado honesto. Puede pausar el flujo, pero no crea un duplicado silencioso.

Nota de producción

Samuel Fajreldines es el responsable editorial de este artículo. La investigación combinó una publicación primaria reciente, documentación oficial del OpenAI Agents SDK y orientación de confiabilidad de AWS. El modelo de estados, las tablas y los fixtures son síntesis original. El código es ilustrativo y no se ejecutó contra un proveedor ni un sistema de producción. La asistencia de IA apoyó el descubrimiento, la comparación de fuentes, la redacción, la creación de la imagen, la traducción y la revisión de consistencia. No aportó pruebas de producción ni experiencia de primera mano. Uso RemoteCode, una herramienta mía, para mantener visibles las sesiones largas de agentes; eso no es una medición de este artículo.

Fuentes consultadas