# Trabajos de API asíncronos: flujos de agentes trazables

Un agente que envía una solicitud de API de larga duración necesita un flujo de trabajo, no un bucle que siga llamando a endpoints hasta que algo parezca terminado. La creación, la observación, la cancelación y la recuperación del resultado tienen distintos modos de fallo. Si los reduces a una sola instrucción y unos cuantos reintentos, tarde o temprano enviarás el trabajo dos veces, perderás un resultado completado o afirmarás que una cancelación tuvo efecto cuando nunca lo tuvo.

La dificultad no está en realizar una solicitud HTTP. Está en conservar la intención después de que el agente se reinicie, se produzca un tiempo de espera de red, haya una interrupción del proveedor o una persona diga «detente» cuando el servicio remoto ya ha empezado a trabajar. Basa el diseño en un registro local duradero del trabajo y haz que cada llamada externa pueda responderse más adelante: ¿qué pedimos?, ¿qué trabajo remoto lo gestiona?, ¿qué estado observamos y qué hicimos después?

## Una solicitud de creación debe establecer la propiedad

Un endpoint de creación inicia una operación asíncrona y responde antes de que la operación llegue a un estado terminal. Su primera respuesta debe dar al agente información suficiente para continuar sin repetir la solicitud. En una API bien diseñada, eso significa un ID de trabajo, un estado inicial y una URL de estado o una convención de endpoint para recuperar el trabajo.

No deduzcas que una solicitud fue aceptada porque se conectó el socket o porque el cliente agotó el tiempo después de enviar los bytes. La única prueba útil es una respuesta del servidor o una consulta posterior que vincule la solicitud lógica original con un trabajo. La entrega de red es incierta justo cuando el cliente más desea una respuesta sencilla.

Asigna a cada operación lógica un ID local antes de que el agente realice la solicitud. Ese identificador es tuyo, no del proveedor. Guárdalo junto con el cuerpo de la solicitud o con una huella normalizada de ese cuerpo, el destino previsto, el token de idempotencia, las marcas de tiempo y la persona o proceso que autorizó el trabajo. Escribe este registro de forma duradera antes de enviar la solicitud.

Un registro mínimo puede ser así:

```json
{
  "operation_id": "op_01J7Q5X4D4PA3D",
  "request_fingerprint": "sha256:4f8b...",
  "idempotency_token": "idem_5b5c76c7",
  "remote_job_id": null,
  "state": "create_pending",
  "created_at": "2025-03-08T14:22:11Z",
  "create_deadline": "2025-03-08T14:24:11Z",
  "result_deadline": "2025-03-08T15:22:11Z"
}
```

La huella detecta un error sutil y frecuente: un agente reintenta una llamada de creación después de cambiar un parámetro. Es una operación nueva, aunque una persona la describa como «la misma tarea». Un token vinculado a una forma de solicitud no debe autorizar otra en silencio.

Una respuesta de creación podría ser:

```http
HTTP/1.1 202 Accepted
Location: /v1/jobs/job_7ad2
Content-Type: application/json

{
  "job_id": "job_7ad2",
  "state": "queued",
  "status_url": "/v1/jobs/job_7ad2"
}
```

En cuanto llegue la respuesta, actualiza el registro duradero con `remote_job_id`, el estado observado y los metadatos de la respuesta. Solo entonces el agente puede pasar a la observación. Si la API devuelve un resultado correcto síncrono, registra ese resultado bajo la misma operación. El flujo debe admitir ambos caminos sin fingir que significan lo mismo.

## La idempotencia sirve para las entregas inciertas, no para cualquier reintento

Un token de idempotencia indica al servidor que la entrega repetida de una solicitud lógica de creación no debe producir trabajo repetido. No vuelve segura cualquier solicitud ni repara una API que nunca implementó la idempotencia en su endpoint de creación.

El token debe aparecer en la solicitud de creación según el contrato del proveedor. Algunas API aceptan una cabecera `Idempotency-Key`; otras requieren un campo de la solicitud. Usa la forma documentada y genera un token con suficiente entropía para que no colisionen operaciones que no tienen relación. Conserva el mismo token hasta resolver la operación original.

```http
POST /v1/reports HTTP/1.1
Content-Type: application/json
Idempotency-Key: idem_5b5c76c7
X-Trace-ID: tr_0830d3

{
  "account": "acct_218",
  "range": {"start": "2025-02-01", "end": "2025-02-28"},
  "format": "csv"
}
```

La secuencia segura de reintento es limitada:

1. Genera el token y guarda el registro de la operación.
2. Envía la solicitud de creación con ese token.
3. Si se pierde la respuesta o el cliente agota el tiempo, reintenta la misma solicitud con el mismo token.
4. Si el servidor devuelve el trabajo original, guarda su ID y continúa.
5. Si necesitas datos distintos, cierra o cancela la operación anterior si es posible. Después crea un registro y un token nuevos.

Esta diferencia importa porque los agentes suelen reescribir las solicitudes mientras razonan. Cambiar el intervalo de fechas, el destino, la cuenta o el formato de salida cambia el efecto. Reutilizar un token después de ese cambio crea una disputa entre cliente y servidor: un servidor cuidadoso rechaza la diferencia, mientras que uno menos cuidadoso puede devolver una respuesta antigua que ya no coincide con la intención del agente.

El estándar HTTP deja clara la distinción relevante. RFC 9110 define los métodos idempotentes como aquellos cuyo efecto previsto al repetir solicitudes idénticas es el mismo que el de una sola solicitud. POST no es idempotente por defecto. Un proveedor puede añadir comportamiento idempotente a un endpoint POST, pero el cliente debe tratarlo como un contrato explícito de la aplicación, no como una regla de HTTP.

Una recomendación popular y equivocada dice: «Reintenta POST solo una vez». El número de reintentos no es lo importante. Un único envío duplicado puede realizar un pago, aprovisionar un entorno o iniciar un lote costoso. Reintenta una solicitud de creación cuando el plazo y las indicaciones del proveedor lo permitan, pero solo con un token que permita al servidor reconocer la operación original.

## Un tiempo de espera deja desconocido el estado del trabajo

Que se agote el tiempo de una creación no significa que el servicio haya rechazado la solicitud. Significa que el agente no recibió una respuesta definitiva antes de su propio plazo. El servidor puede haber aceptado la solicitud, seguir procesándola o no haberla recibido nunca.

Este fallo deja al descubierto los flujos de agentes débiles. Un agente envía una solicitud de creación, espera treinta segundos, no recibe respuesta y envía otra solicitud con un token nuevo. Ahora se ejecutan dos informes. El segundo puede terminar primero, lo que dificulta detectar el incidente hasta que alguien compara los cargos, las exportaciones o los cambios posteriores.

Mantén un estado explícito `create_pending`. Cuando la llamada falle de forma ambigua, registra la clase de error, la marca de tiempo y el número de intentos, pero no descartes la operación. Después usa la vía de reconciliación de la API. Los proveedores lo resuelven de distintas formas:

- Un reintento con el mismo token de idempotencia puede devolver la respuesta de aceptación original.
- Un endpoint de lista o búsqueda puede filtrar por una referencia de solicitud del cliente.
- Una consulta de estado puede aceptar un ID de operación proporcionado por el cliente.
- El proveedor puede documentar una consulta compatible para creaciones recientes mediante un identificador de solicitud.

Si no existe ninguna de estas opciones, la API no puede ofrecer al cliente una semántica fiable de creación como máximo una vez cuando se pierde la respuesta. Dilo claramente en el diseño. Puedes reducir los duplicados con una cola de salida local y reintentos prudentes, pero no puedes demostrar que un reintento no creó más trabajo.

Trata como una infracción del contrato que merece atención un ID de trabajo desconocido devuelto con el mismo token. No sobrescribas el ID anterior. Conserva ambos registros de respuesta, detén la actividad automática para esa operación y exige una decisión humana. Elegir uno en silencio es la forma en que los historiales de auditoría se convierten en ficción.

Usa plazos que distingan la comunicación del trabajo. El plazo de creación determina cuánto tiempo intentará el agente establecer un ID de trabajo remoto. El plazo del resultado determina cuánto esperará el proceso de negocio a que termine. Un trabajo puede sobrevivir a un breve tiempo de espera de la respuesta de creación y aun así tener horas para completarse. Combinar ambos en un único temporizador hace que los agentes abandonen trabajo recuperable o lo reintenten en el momento equivocado.

## Las consultas necesitan retroceso, propiedad y un momento de finalización

Consultar es seguro cuando un único flujo duradero es responsable del trabajo y cada consulta registra una observación. Se vuelve abusivo cuando varias ejecuciones del agente redescubren el mismo trabajo y todas lo consultan por separado.

Guarda el ID del trabajo remoto en un único registro y asigna un arrendamiento al proceso que posee la observación en ese momento. Puede ser una fila de base de datos con una marca de expiración, un mensaje de cola con reglas de visibilidad u otro mecanismo de control de concurrencia duradero. Si el trabajador falla, otro puede hacerse cargo cuando expire el arrendamiento. Sin propiedad, los reintentos y reinicios multiplican las llamadas de estado.

Respeta `Retry-After` cuando la API lo envíe. Si la API no ofrece indicaciones, usa un retroceso exponencial limitado con variación aleatoria. El límite exacto depende de la rapidez con que el negocio necesite una respuesta y de los límites de frecuencia del proveedor, pero el patrón debe evitar ráfagas sincronizadas.

```text
intento 1: espera un intervalo aleatorio cercano a 2 segundos
intento 2: espera un intervalo aleatorio cercano a 4 segundos
intento 3: espera un intervalo aleatorio cercano a 8 segundos
intentos posteriores: sigue aumentando hasta el límite configurado
```

No calcules el siguiente retraso a partir del resumen escrito por el agente. Guarda la próxima hora de consulta en el registro del trabajo. Así, un trabajador reiniciado puede continuar la programación y un operador puede entender por qué el agente está esperando.

Una respuesta de estado debe actualizar únicamente hechos observados. Por ejemplo:

```json
{
  "job_id": "job_7ad2",
  "state": "running",
  "updated_at": "2025-03-08T14:26:40Z",
  "progress": {"completed": 146, "total": 500}
}
```

Registra `state`, la marca de tiempo del proveedor si la proporciona, la hora de recuperación, la referencia de la respuesta sin procesar y la próxima acción. No conviertas un campo de progreso impreciso en una promesa de que el trabajo terminará. Los proveedores suelen informar del progreso tarde o por lotes. El progreso ayuda a los operadores; el estado terminal controla el flujo.

Establece un plazo para el resultado y convierte su vencimiento en un estado, no en una excusa para olvidar el trabajo. `result_timed_out` significa que el agente dejó de consultar automáticamente porque expiró el acuerdo. No significa que el trabajo remoto se haya detenido. Si la acción tiene un coste o efectos secundarios reales, conserva suficiente información para reconciliarla más adelante y decidir si conviene cancelarla.

Los webhooks pueden mejorar la latencia, pero no eliminan el ciclo de estado. Los proveedores pueden repetir las devoluciones, entregarlas fuera de orden o no entregarlas. Verifica la devolución según la documentación del proveedor, elimina duplicados mediante un ID de evento cuando exista, actualiza el mismo registro del trabajo y realiza una lectura final del estado antes de declarar que la operación tuvo éxito.

## Las transiciones de estado deben rechazar las suposiciones optimistas

Una máquina de estados protege el flujo frente a un agente que interpreta las palabras con demasiada libertad. Define los estados locales y las transiciones permitidas antes de conectar las herramientas con la API. Los servicios remotos usan nombres distintos, pero tu registro debe hacer visible la incertidumbre.

Un modelo local práctico es:

```text
create_pending -> accepted -> observing -> result_collecting -> succeeded
create_pending -> create_unknown -> reconciliation
accepted or observing -> cancel_requested -> cancelling -> cancelled
accepted or observing -> failed
observing -> result_timed_out
```

Las flechas son reglas, no un diagrama decorativo para la documentación. Un trabajador debe rechazar una transición que no tenga pruebas. No puede marcar `succeeded` porque vio un progreso de 100. No puede marcar `cancelled` porque envió `DELETE /jobs/job_7ad2`. No puede pasar de `failed` a `observing` salvo que la API remota admita explícitamente una operación de reintento o reanudación y la nueva acción se registre por separado.

Mantén separados el estado remoto y el local. `cancel_requested` describe un hecho local: el agente envió una solicitud de cancelación y espera confirmación. `cancelled` describe un hecho remoto: el servicio informó de un estado terminal de cancelación. Esta pequeña diferencia evita mucha confusión durante un incidente.

Usa un historial de transiciones de solo anexado. Cada entrada necesita el ID de operación, el actor, la hora, el estado local anterior, el siguiente estado local, la solicitud o respuesta que la activó y el motivo. Un historial compacto es suficiente:

```json
{
  "at": "2025-03-08T14:29:02Z",
  "actor": "worker-3",
  "from": "observing",
  "to": "cancel_requested",
  "cause": "human_request:req_91af",
  "remote_job_id": "job_7ad2"
}
```

Evita usar un único campo mutable `status` como registro exclusivo. Te dice lo que el flujo cree ahora, pero no por qué lo creía hace cinco minutos. Cuando una API remota devuelva después un estado inesperado, el historial te dirá si cambió el proveedor, si el agente repitió una llamada o si intervino un operador.

## La cancelación necesita confirmación y un límite de daños

Cancelar es pedir que se detenga el trabajo futuro. No puede deshacer el trabajo que el proveedor ya confirmó, y algunos proveedores permiten una carrera en la que el trabajo termina al mismo tiempo que llega la solicitud de cancelación. Diseña el flujo teniendo en cuenta esa realidad.

Cuando una persona o una política decida detener un trabajo, registra primero la intención de cancelación. Incluye quién hizo la solicitud, por qué y qué efecto se esperaba. Después llama al endpoint de cancelación documentado usando el ID de trabajo remoto guardado. Conserva la respuesta aunque solo diga que el servidor aceptó la solicitud.

Sigue consultando después de la cancelación. Los resultados terminales aceptables suelen incluir `cancelled`, `succeeded` y `failed`. Un resultado completado después de una solicitud de cancelación no es automáticamente un error. Puede ser el resultado real de un trabajo que cruzó su punto de confirmación unos segundos antes. El flujo debe informar de la secuencia con precisión, en lugar de reescribir el historial para ajustarlo al resultado deseado.

Algunas operaciones necesitan un límite de daños separado de la cancelación. Si un trabajo de exportación escribe un archivo, cancelarlo puede dejar un archivo parcial. Si un trabajo de aprovisionamiento crea recursos, la cancelación puede dejar algunos recursos creados. El contrato de la API debe indicar si ofrece limpieza, reversión o detalles de resultados parciales. Si no lo hace, trata la cancelación como un control operativo, no como una transacción.

No envíes llamadas de cancelación repetidas desde cada trabajador de consulta. Guarda `cancel_requested`, haz que la operación de cancelación sea idempotente si el proveedor lo permite y deja que el propietario del arrendamiento gestione el seguimiento. Repetir una solicitud inofensiva desperdicia capacidad; repetir una cancelación con efectos secundarios puede ensuciar el registro de auditoría remoto.

También ayuda establecer un plazo de cancelación. Después de una espera razonable y documentada, pasa a `cancellation_unconfirmed` en lugar de afirmar que la operación tuvo éxito. Escala el caso con el ID de trabajo remoto, el ID de trazabilidad, el historial de solicitudes y los identificadores de solicitud del proveedor. Ese conjunto permite que una persona o el equipo de soporte del proveedor vea la secuencia real sin reconstruirla a partir de mensajes de chat.

## La recopilación del resultado es una acción independiente

Un estado terminal correcto significa que el trabajo remoto terminó. No garantiza que el resultado se haya descargado, validado, almacenado o entregado al sistema siguiente. Trata la recopilación como una acción propia y registrada.

Primero recupera el resultado usando el ID de trabajo o la referencia de resultado que proporcione la API. Valida el tipo de contenido, el esquema, la suma de comprobación, el tamaño o el número de registros esperados cuando el proveedor proporcione alguno de esos datos. Guarda la referencia del resultado y el resultado de la validación en el registro del trabajo. Si el resultado es grande, guarda una ubicación duradera y los datos de integridad en lugar de copiar contenido opaco en un registro de eventos.

Después decide si la propia recopilación necesita idempotencia. Muchos endpoints de resultados son lecturas seguras. Otros generan una descarga temporal, consumen un recurso de un solo uso o marcan un trabajo como entregado. Lee el contrato. Un agente que trata cada `GET` como inofensivo aún puede provocar un cambio de estado específico del proveedor.

No uses un estado HTTP correcto como única validación. Un endpoint de informes puede devolver un archivo válido que contenga una fila de error. Un lote de imágenes puede devolver un manifiesto con elementos fallidos. Una exportación de datos puede completarse omitiendo registros a los que la API dice que el usuario no puede acceder. Valida según la expectativa de negocio que dio origen al trabajo.

Para trabajos por lotes, registra los resultados por elemento cuando el proveedor lo permita. Un trabajo terminal puede contener 498 éxitos y dos fallos. Llamarlo simplemente «completado» obliga al siguiente agente a redescubrir el fallo parcial en el contenido del resultado. Tu estado local final puede seguir siendo correcto mientras el resumen del resultado contiene los recuentos y una lista de referencias de elementos fallidos.

Cierra la operación solo cuando la recopilación cumpla su contrato. `succeeded` debe significar que el resultado previsto está disponible y verificado según tus reglas. Si el proveedor terminó el trabajo pero la recopilación falló, usa un estado local distinto, como `result_unavailable` o `result_validation_failed`. El trabajo remoto puede haber terminado, pero tu flujo todavía no.

## Los ID de trazabilidad conectan acciones y los registros de auditoría establecen hechos

Usa un ID de trazabilidad para cada operación y envíalo en la creación, las lecturas de estado, la cancelación y la recopilación cuando la API acepte cabeceras personalizadas. Combínalo con el ID de trabajo remoto en cuanto lo conozcas. El ID de trazabilidad conecta los eventos dentro de tus sistemas; el ID de trabajo permite al proveedor encontrar su propio elemento de trabajo.

No confundas ninguno de los dos identificadores. Un ID de trazabilidad no debe convertirse en un token de idempotencia porque una operación puede incluir varias solicitudes con reglas de reintento distintas. Un ID de trabajo no debe convertirse en tu registro de autorización porque el proveedor lo generó después de tu decisión local de actuar.

Tu registro de auditoría debe responder preguntas que los registros normales a menudo no pueden contestar: qué proceso del agente inició la operación, qué aprobación humana la cubría, qué acción con credenciales se realizó y si alguien editó el historial después. Escribe un registro conciso de intención antes de la llamada de creación y añade observaciones después. Conserva los ID de solicitud y los metadatos de respuesta saneados. Nunca incluyas tokens portadores, contraseñas, claves privadas ni cargas completas sensibles en un registro general.

Sallyport puede ejecutar llamadas a API HTTP sin exponer las credenciales almacenadas al agente, y sus sesiones y llamadas individuales proporcionan un rastro resistente a manipulaciones que puede verificarse con `sp audit verify`. Eso protege la custodia de credenciales y las pruebas de las acciones. Tu flujo aún necesita su propio registro de operaciones porque solo él sabe si `job_7ad2` corresponde a la tarea de negocio solicitada.

Una trazabilidad solo resulta útil durante un día difícil si todos los componentes la registran de forma coherente. Inclúyela en el registro local del trabajo, el contexto de ejecución del agente, las cabeceras de solicitud cuando estén permitidas, las anotaciones de auditoría cuando estén permitidas y los tickets de los operadores. No fabriques una trazabilidad nueva para cada consulta. Son eventos secundarios de la misma operación.

## Un bucle de referencia gestiona los fallos habituales

El flujo siguiente mantiene explícitas las decisiones difíciles. Supone un proveedor con un contrato de creación idempotente, un endpoint de estado y un endpoint de cancelación. Adapta los nombres de los endpoints, pero no elimines las transiciones de estado persistentes.

```text
load operation by local operation ID

if no operation exists:
    create and persist record with fingerprint and idempotency token

if remote job ID is absent:
    send create with the stored token
    if response confirms job ID:
        persist job ID and move to observing
    if response is ambiguous:
        move to create_unknown and reconcile using the stored token
    if response rejects request definitively:
        move to failed

while local state requires observation and result deadline has not passed:
    acquire lease for the operation
    read remote status
    append the observation
    if cancellation was requested and remote state is nonterminal:
        send cancellation once and record the attempt
    if remote state is terminal:
        collect and validate result if appropriate
        persist final local state
    otherwise:
        persist next poll time and release lease

if the deadline expires before a terminal observation:
    move to result_timed_out and preserve the reconciliation record
```

Este bucle no tiene un número mágico de reintentos porque sus límites dependen del proveedor, del coste de la operación y del plazo de quien la solicita. Sí tiene una regla más importante: cada reintento se refiere a una operación guardada y cada acción visible externamente modifica el historial de esa operación.

Pruébalo inyectando fallos antes de entregarlo a agentes autónomos. Descarta la respuesta de creación después de que el servidor la acepte. Detén el trabajador después de guardar el ID de trabajo pero antes de programar la primera consulta. Devuelve un resultado terminal durante una carrera de cancelación. Entrega el mismo webhook dos veces. Reinicia con un arrendamiento obsoleto. Si el flujo no puede explicar y recuperarse de cada caso, no está listo para iniciar trabajos costosos o importantes.

La primera tarea de implementación no es atractiva: crea el registro duradero de la operación y rechaza cualquier solicitud de creación que no lo tenga. Esa única restricción obliga al agente a conservar la intención, hace posible evitar duplicados y proporciona a todos un registro factual cuando el sistema remoto se comporta de forma imperfecta.
