DEV Community

Silviu Technology
Silviu Technology

Posted on

Agentes LLM: un contrato para herramientas de email

Un agente LLM puede llamar una API de email en pocos segundos. El problema aparece cuando hay que saber si esa llamada fue correcta, si el mensaje llegó para esta tarea y si el siguiente intento puede repetir la operación sin crear ruido.

En flujos de automatización, una herramienta de correo desechable no debería devolver solamente ok: true. El agente necesita un resultado que explique qué pasó, qué evidencia existe y cuál es el siguiente paso seguro. Es una diferencia pequeña en la interfaz, pero cambia mucho la confiabilidad del sistema.

El problema no es enviar el email

Imaginemos un agente que prueba un onboarding: crea un usuario, solicita un enlace de verificación y lo consume. Hay varias operaciones con tiempos distintos:

  1. La API acepta la solicitud.
  2. El worker prepara el mensaje.
  3. El proveedor entrega el mensaje.
  4. La herramienta encuentra el mensaje correcto.
  5. El agente extrae y usa el enlace.

Si todas estas fases se presentan como una sola acción, el agente suele reintentar demasiado pronto. Puede crear dos usuarios, leer un mensaje viejo o concluir que el producto falló cuando solo estaba lento. Un prompt más largo ayuda poco si la herramienta no expone sus límites.

Los términos que aparecen en notas antiguas, como tepm mail com o tempail, no resuelven este problema. La decisión importante es otra: cada llamada debe tener una identidad de ejecución y una prueba observable.

Para definir esa frontera, resulta útil empezar por contratos mínimos para revisar prompts: el prompt puede decidir, pero la herramienta debe proteger las invariantes.

Un contrato pequeño para la herramienta

Un resultado útil puede tener esta forma:

type MailToolResult =
  | {
      status: "received";
      runId: string;
      messageId: string;
      receivedAt: string;
      verificationUrl: string;
    }
  | {
      status: "pending" | "expired" | "rejected";
      runId: string;
      retryable: boolean;
      reason: string;
      observedMessages: number;
      nextRetryAfterMs?: number;
    };
Enter fullscreen mode Exit fullscreen mode

No hace falta que el agente conoce los detalles del proveedor. Solo necesita distinguir entre un resultado final, una espera razonable y un error que no debe repetir. retryable es especialmente importante: evita que un modelo convierta un rechazo de autenticación en una tormenta de solicitudes.

También conviene que runId viaje en cada operación. Así se puede correlacionar la creación del buzón, el envío del formulario y la lectura del mensaje sin guardar el cuerpo completo del correo en los logs.

Reintentos con presupuesto y evidencia

Un reintento no debería ser una instrucción vaga como “prueba otra vez”. Definamos un presupuesto explícito:

type RetryBudget = {
  maxAttempts: number;
  deadlineAt: string;
  delayMs: number;
};

function canRetry(budget: RetryBudget, now = Date.now()) {
  return budget.maxAttempts > 0 && now < Date.parse(budget.deadlineAt);
}
Enter fullscreen mode Exit fullscreen mode

Después de cada intento, la herramienta devuelve un recibo breve: estado, duración, cursor usado, cantidad de mensajes observados y razón de salida. El agente puede comunicar “el mensaje todavía no aparece” en vez de inventar una causa.

Hay un coste que se suele olvida: esperar más no siempre aumenta la calidad de la prueba. Un mensaje fuera de la ventana de la ejecución debe marcarse como tardío, aunque contenga el token correcto. La validez depende del contexto, no solo del contenido.

El runner no deberia reusar un cursor después de un resultado received. Si el flujo continúa, debe consumir el messageId una vez o registrar que ya fue utilizado. Esto hace la operación más cercana a idempotente y facilita depurar una repetición accidental.

Aislamiento por ejecución

El diseño puede visualizarse así:

agente LLM
    │ solicita una acción con runId
    ▼
adaptador de email
    │ asigna identidad + cursor + vencimiento
    ▼
proveedor de mensajes
    │ devuelve evidencia acotada
    ▼
recibo tipado para el agente
Enter fullscreen mode Exit fullscreen mode

Cada ejecución debería tener una identidad de buzón, una hora de inicio y una hora de expiración. Si se usa un recurso compartido, al menos hay que separar por runId y filtrar por receivedAt. Un buzón común parece más barato al principio, pero los falsos positivos salen caros en la investigación.

Antes de liberar un flujo SaaS, también vale revisar un checklist de emails antes de lanzar. El agente no reemplaza las verificaciones de producto; puede hacerlas repetibles y dejar mejor evidencia.

Q&A

¿El agente debe leer el correo completo?

Normalmente no. La herramienta debería extraer solo los campos necesarios, como messageId, asunto, fecha y URL de verificación. Guardar el cuerpo completo complica privacidad, retención y depuración.

¿Cuántos reintentos son razonables?

Depende del SLA del proveedor y de la duración del flujo, pero la regla útil es tener un límite de intentos y otro de tiempo. Si solo existe uno, una respuesta lenta puede agotar el sistema de forma poco predecible.

¿Dónde entra el prompt?

El prompt define cómo interpretar received, pending o rejected. No debería decidir si una URL vencida es válida ni saltarse el límite. Esas reglas deben vivir en código, cerca de la herramienta.

Puntos de implementación

  • Crear un runId al inicio y propagarlo en cada llamada.
  • Devolver estados tipados, no mensajes ambiguos.
  • Separar el presupuesto de intentos del tiempo máximo.
  • Filtrar mensajes por cursor y ventana temporal.
  • Registrar recibos sin tokens ni cuerpos completos.
  • Expirar la identidad de prueba incluso si el agente termina con error.

La idea central es simple: los modelos pueden decidir mejor cuando las herramientas exponen límites claros. Un contrato de email bien definido convierte una espera incierta en un estado observable, y hace que la automatización sea más segura, más barata de investigar y un poco menos fragil.

Top comments (0)