DEV Community

Silviu Technology
Silviu Technology

Posted on

Agentes LLM: estados antes que reintentos

Los agentes LLM que prueban un registro o un flujo de recuperación de contraseña tratan el inbox temporal como si fuera una caja negra: crean una dirección, preguntan si llegó algo y repiten hasta que aparece un mensaje. Ese patrón parece sencillo, pero mezcla adquisición, espera, lectura y consumo. Un contrato de estados hace el flujo mas predecible y deja claro cuándo el agente debe actuar o detenerse.

El fallo: un agente no debe adivinar el estado del inbox

Un modelo puede decidir bien con información incompleta, pero no debería inventar que un email existe porque ya pasó cierto tiempo. Para un flujo de automatización, estas cuatro situaciones son diferentes:

  • creado: existe un inbox y tiene un identificador de ejecución;
  • esperando: todavía no se ha encontrado el mensaje dentro del plazo;
  • recibido: hay un mensaje que coincide con el criterio solicitado;
  • caducado: el inbox o la espera ya no pueden producir un resultado válido.

Si la herramienta solo devuelve true o false, el agente pierde contexto. Tampoco puede distinguir un timeout de una bandeja vacía. Esa ambigüedad produce reintentos duplicados, conversaciones largas y tests que pasan por casualidad.

La primera decisión de arquitectura es separar el estado del recurso de la acción del agente. El modelo decide “esperar 10 segundos” o “abrir el mensaje”, pero el servicio conserva la autoridad sobre el lease, el cursor y la fecha de expiración.

El contrato mínimo de una herramienta de email

Un contrato útil no necesita exponer todos los detalles del proveedor. Necesita devolver hechos pequeños y verificables. Un ejemplo podría ser:

type InboxResult =
  | { state: "created"; inboxId: string; expiresAt: string }
  | { state: "waiting"; inboxId: string; nextPollAt: string }
  | { state: "received"; inboxId: string; messageId: string; subject: string }
  | { state: "expired"; inboxId: string; reason: "lease" | "timeout" };
Enter fullscreen mode Exit fullscreen mode

El inboxId permite correlacionar todas las operaciones. expiresAt evita que un agente use una dirección fuera de su ventana de prueba. Y messageId permite reclamar un mensaje una sola vez, en vez de procesarlo de nuevo cada vez que el modelo reevalúa la tarea.

También conviene hacer explícita la intención: wait_for_message, read_message y claim_message son operaciones distintas. Una sola función llamada check_email termina acumulando reglas que el agente no puede ver. Para entender mejor este límite, es útil aislar emails de agentes LLM por ejecución, no solo por usuario.

Una máquina de estados con lease

Imaginemos el flujo como un diagrama en palabras: el agente crea un inbox; el sistema asigna un lease; un worker consulta mensajes; una coincidencia pasa a received; y cualquier camino llega a expired cuando vence el tiempo. No existe una transición directa de expired a received, aunque una respuesta atrasada aparezca después.

El lease debe tener dos límites. El primero es el tiempo total del recurso, por ejemplo diez minutos. El segundo es el presupuesto de polling, como doce consultas con un intervalo creciente. Esto protege al proveedor y también limita el coste de los modelos LLM que reciben una observación en cada ciclo.

Para un inbox usado en CI, guardaría esta información:

run_id, inbox_id, state, expires_at, last_cursor,
poll_count, claimed_message_id, trace_id
Enter fullscreen mode Exit fullscreen mode

El cursor evita volver a examinar toda la bandeja. trace_id une la acción del agente con la solicitud de red y con el resultado del test. Este detalle es pequeño, pero ayuda muchisimo cuando una prueba falla de madrugada y solo queda el registro de la automatización.

Idempotencia, trazas y límites de reintento

Los agentes reintentan por buenas razones: una llamada puede tardar, una respuesta puede perderse o un worker puede reiniciarse. El problema aparece cuando cada reintento crea un inbox nuevo. Una clave de idempotencia basada en run_id y propósito permite devolver el recurso existente:

POST /test-inboxes
Idempotency-Key: signup-check:run-1842
Enter fullscreen mode Exit fullscreen mode

La operación claim_message también necesita ser idempotente. Si el mismo messageId se reclama dos veces, la segunda respuesta debería decir “ya reclamado” junto con la referencia original, no entregar un cuerpo diferente. Así el agente puede continuar sin interpretar una repetición como un email nuevo.

Los reintentos deben tener una razón observable. retry_after, attempt, last_error y next_action son mejores señales que un mensaje genérico como “intenta otra vez”. Cuando la API devuelve expired, el agente debe crear una nueva ejecución solo si la política lo permite; no debe prolongar silenciosamente un lease vencido.

En sistemas con FastAPI, esta frontera se puede complementar con reenvíos seguros en FastAPI, manteniendo la validación del evento separada de la lectura del inbox. El backend sabe si el evento es válido; el agente sabe qué paso de la tarea sigue.

Implementación y checkpoints

Antes de conectar un agente real, probaría el contrato con un fake determinista. El fake debe poder devolver waiting dos veces, luego received, o terminar en expired. No hace falta un proveedor externo para comprobar las transiciones.

Los checkpoints que suelo revisar son:

  1. Una repetición con la misma clave devuelve el mismo inboxId.
  2. Un mensaje que llega después de expiresAt no cambia el estado.
  3. Reclamar dos veces el mismo mensaje no duplica el efecto.
  4. El cursor avanza aunque la búsqueda no encuentre coincidencias.
  5. Los logs contienen run_id y trace_id, pero nunca el contenido completo del email.
  6. El agente recibe una acción siguiente concreta para cada estado.

Los casos de lenguaje real tambien son una prueba interesante. Una consulta como tamp mail com o temp gamil com puede llegar por un typo del usuario; el sistema debe conservarla como texto de búsqueda o rechazarla con una explicación, nunca convertirla en una URL o en una cuenta diferente sin confirmación.

Preguntas frecuentes

¿Por qué no basta con hacer polling hasta que llegue el email?

Porque polling no define propiedad, expiración ni consumo. Sin esos límites, un agente puede mantener una tarea abierta indefinidamente o procesar el mismo mensaje varias veces.

¿El modelo debe conocer todos los estados internos?

No. Debe conocer los estados que cambian su siguiente acción. La API puede ocultar detalles de infraestructura y exponer solo created, waiting, received y expired.

¿Qué aporta un lease frente a un timeout?

El timeout limita una llamada; el lease limita la vida completa del recurso. Esa diferencia evita que una secuencia de reintentos mantenga vivo un inbox mas alla de la ejecución que lo creó.

La idea central es sencilla: un agente LLM es mas fiable cuando recibe un contrato que puede verificar, no cuando se le pide que adivine si el correo ya llegó. Estados explícitos, leases cortos e idempotencia convierten un inbox temporal en una dependencia controlable de la automatización.

Top comments (0)