# Tiempos de espera de herramientas de agentes: planes de recuperación que evitan que se repitan

Un tiempo de espera es una observación sobre el cliente, no un veredicto sobre el trabajo. El solicitante dejó de esperar. Eso es todo lo que sabe. Cuando un agente convierte esa observación en «fallido» y repite una llamada que cambia el estado, puede crear un segundo pago, una segunda implementación, un segundo ticket de soporte o un comando remoto que se ejecute dos veces en una máquina ya sometida a presión.

Los tiempos de espera de las herramientas de los agentes necesitan un plan de recuperación porque los agentes actúan más rápido que las personas que los supervisan y suelen tratar la salida de una herramienta como la verdad absoluta. Una respuesta ausente no es la verdad absoluta. La ruta de recuperación debe decidir si reintentar, esperar, consultar pruebas o detenerse y pedir una decisión humana. Diseña esa ruta antes de dar permiso a un agente para realizar llamadas importantes.

## Un tiempo de espera deja tres historiales posibles

Después del tiempo de espera de un cliente, la solicitud pertenece a uno de tres historiales generales: el servicio nunca la recibió, la recibió y aún no ha terminado, o terminó el trabajo pero el cliente nunca recibió el resultado. Los fallos de red pueden producirse antes de abrir una conexión, mientras viaja el cuerpo de la solicitud, mientras trabaja el servicio o mientras regresa la respuesta. El mismo tipo de excepción puede abarcar los cuatro casos.

Esta distinción cambia la siguiente acción. Si falló una consulta DNS antes de abrir cualquier conexión, un reintento puede tener sentido. Si el servicio aceptó una solicitud para eliminar un recurso y la respuesta desapareció, repetirla puede ejecutar de nuevo una operación destructiva. Si el servicio puso un trabajo asíncrono en una cola, un segundo envío puede crear otro trabajo que compita con el primero mientras este sigue ejecutándose.

HTTP no ofrece un indicador mágico que diga al cliente cuál de esos historiales ocurrió. RFC 9110 describe los métodos de solicitud y el significado de las respuestas, incluida la diferencia entre métodos seguros e idempotentes. No promete que un cliente pueda deducir si el servidor ejecutó algo a partir de una respuesta perdida. Esa limitación es física, no una opción que falte en el SDK.

Los equipos suelen mezclar dos preguntas distintas:

- ¿Se puede volver a enviar esta solicitud sin cambiar el estado final previsto?
- ¿El intento original llegó realmente al servicio y tuvo algún efecto?

La idempotencia responde a la primera pregunta. La reconciliación responde a la segunda. Un sistema necesita ambas. Un `PUT` idempotente puede repetirse sin riesgo, pero un tiempo de espera aún te impide saber si terminó el trabajo posterior que esa solicitud desencadenó. Una consulta de estado puede establecer el resultado, pero no evita el trabajo duplicado si el servicio acepta dos creaciones indistinguibles.

Los agentes necesitan esta distinción en los contratos de sus herramientas. Un error de texto simple como `request timed out` invita a improvisar. Un resultado estructurado que indique `outcome: unknown` avisa al agente de que debe salir de la rama de reintento y entrar en la rama de búsqueda de pruebas.

## Mapea la ruta de la solicitud antes de elegir un reintento

Un buen plan de recuperación identifica los límites donde puede existir evidencia. Empieza por el proceso del agente, sigue con el envoltorio de la herramienta, el grupo de conexiones, la puerta de enlace o el proxy si existe, la entrada del servicio, la aplicación, el almacén persistente y cualquier trabajador que gestione el trabajo asíncrono. Un tiempo de espera en un límite no dice nada fiable sobre el siguiente.

Considera una llamada para crear una implementación. El agente envía una solicitud mediante una herramienta. El cliente escribe el cuerpo completo, el servidor confirma un registro de implementación y después la conexión se rompe antes de que la respuesta llegue al cliente. La herramienta emite un tiempo de espera. El agente reintenta con una solicitud nueva. Ahora el servidor tiene dos registros de implementación, ambos válidos desde su propio punto de vista.

Cambia un detalle: el cliente agota el tiempo mientras carga el cuerpo y el servidor rechaza el cuerpo incompleto antes de ejecutar el código de la aplicación. La misma herramienta podría devolver `timeout`. En este caso, un reintento puede crear exactamente una implementación. El solicitante no puede distinguir ambos casos basándose solo en el tiempo de espera.

Anota qué pruebas puede producir cada componente. En una acción HTTP habitual se incluyen:

- Marcas de tiempo del cliente, destino seleccionado, resumen del cuerpo de la solicitud y un ID de operación generado por el solicitante.
- Registros de acceso del servicio que indiquen si la entrada aceptó la solicitud.
- Un registro de aplicación que guarde el ID de operación junto con el resultado confirmado.
- Registros de trabajadores o colas para acciones que continúen después de la solicitud síncrona.
- Un endpoint de lectura que devuelva el estado actual o el estado de la operación.

No conviertas los registros de red en tu única fuente de verdad. El registro de un equilibrador de carga puede mostrar que llegaron bytes, pero no demostrar que se confirmó la transacción de la base de datos. Un registro de la base de datos puede demostrar una confirmación, pero quizá no que un proveedor externo recibiera un efecto posterior. El registro autoritativo debe corresponderse con la acción que intentas demostrar.

Para enviar un correo, el identificador de mensaje aceptado por el proveedor es una prueba más sólida que un registro de la aplicación que diga «a punto de enviar». Para una migración de base de datos, una tabla de migraciones o un registro de transacción es más fiable que un código de salida de un proceso de shell que el solicitante nunca recibió. Para crear un recurso en la nube, una URL de operación o una etiqueta del recurso con un ID generado por el solicitante es mejor que repetir la solicitud de creación.

## La idempotencia debe pertenecer a la operación, no al intento

Un sistema de reintentos solo funciona cuando cada intento de una misma acción prevista lleva el mismo identificador persistente. Genera el identificador antes de la primera llamada de red. Guárdalo junto con la descripción de la acción. Reutilízalo después de reiniciar el proceso o la herramienta, o al pasar el caso a un operador humano.

No generes un UUID nuevo dentro del bucle de reintento. Ese patrón parece cuidadoso durante una revisión de código, pero anula todo el propósito. El servidor ve cada repetición como una solicitud nueva, que es precisamente cómo aparecen las operaciones duplicadas.

Una solicitud puede llevar un valor de idempotencia en un encabezado o en un campo del cuerpo, según la API. El detalle del transporte importa menos que la regla del servidor. El servidor debe asociar atómicamente ese valor con la operación y su resultado. Si llegan dos solicitudes idénticas al mismo tiempo, debe serializarlas o hacer que una observe a la otra. Una caché que caduca antes de que terminen los reintentos tardíos no ofrece una protección fiable contra duplicados.

Una solicitud HTTP práctica podría verse así:

```http
POST /deployments HTTP/1.1
Content-Type: application/json
Idempotency-Key: op_7d5d4d8e4e5a
X-Correlation-ID: run_42_task_9

{"repository":"api","revision":"a1b2c3d4","environment":"staging"}
```

El servidor debe guardar el valor de idempotencia junto con una huella de los campos importantes de la solicitud y el ID de la implementación u operación resultante. Si el mismo valor llega con una revisión o un entorno diferentes, debe rechazarlo. Devolver el primer resultado para un contenido distinto aplica silenciosamente una intención equivocada.

Para una operación asíncrona, devuelve una referencia persistente a la operación en cuanto el servidor acepte el trabajo:

```json
{
  "operation_id": "dep_1842",
  "state": "accepted",
  "status_url": "/operations/dep_1842"
}
```

Después de un tiempo de espera, el agente consulta `op_7d5d4d8e4e5a` o `dep_1842` antes de considerar otro envío. Si la API no admite un valor de idempotencia ni una consulta mediante referencia externa, clasifica la escritura como ambigua por diseño. Puede ser aceptable para un recurso de prueba desechable. Es una mala opción para una acción autónoma que genere costes o cambie el estado de producción.

No consideres seguro un método solo porque use `POST` con una biblioteca de reintentos. Los nombres de los métodos HTTP son indicaciones sobre la semántica prevista, no una protección contra una implementación del servidor que duplique el trabajo. Lee la documentación específica de la API y prueba tú mismo el comportamiento ante duplicados.

## Da a los agentes un estado explícito de resultado desconocido

Una herramienta que puede afectar al mundo exterior no debe devolver al agente solo `success` o `error`. Necesita un tercer resultado: `unknown`. Ese estado evita el comportamiento más dañino del modelo, tratar un registro incompleto como permiso para probar una versión ligeramente distinta del mismo comando.

Usa un contrato de resultados que registre la fase que falló y reconozca que esa fase puede ser incierta. Por ejemplo:

```json
{
  "outcome": "unknown",
  "operation_id": "op_7d5d4d8e4e5a",
  "correlation_id": "run_42_task_9",
  "transport_observation": "response deadline exceeded after request write",
  "retry_allowed": false,
  "reconcile": {
    "method": "GET",
    "path": "/operations/by-id/op_7d5d4d8e4e5a"
  }
}
```

El campo `retry_allowed` debe proceder de la definición de la herramienta o de la acción, no de una suposición del agente basada en verbos en inglés. Un agente no puede deducir con seguridad que `create_release` es inofensivo porque el destino sea un entorno de pruebas. Una implementación en pruebas aún puede enviar notificaciones, consumir una cuota compartida o modificar un canal de versiones.

Haz que el agente siga una secuencia de recuperación limitada:

1. Conserva en el registro de ejecución la acción prevista, el ID de operación, el destino y la observación sobre el tiempo de espera.
2. Consulta la fuente de estado autoritativa usando el mismo ID de operación o una referencia emitida por el servicio.
3. Continúa solo con un resultado terminal confirmado. Reintenta únicamente si la definición de la acción lo permite y la fuente de estado muestra que no se aceptó ninguna operación.
4. Detente y presenta las pruebas cuando el servicio no pueda establecer el resultado dentro del plazo de recuperación de la acción.

La condición de detención importa. Un agente que consulta indefinidamente consume atención y puede mantener viva una tarea mucho después de que su propósito original haya desaparecido. Un agente que prueba cinco variantes de una solicitud de escritura puede crearle a otra persona un proyecto de limpieza. Da a cada operación un plazo de recuperación independiente del plazo de la solicitud.

Una aprobación humana no resuelve por sí sola un resultado desconocido. La aprobación responde a «¿puede este solicitante intentar esta acción?». No responde a «¿el intento anterior tuvo éxito?». Mantén separadas las pruebas de autorización y las pruebas de ejecución, tanto en la interfaz como en los registros.

## Los tiempos de espera de lectura requieren un tratamiento distinto al de las escrituras

Una lectura cuyo tiempo de espera se agotó suele implicar menos riesgo de duplicación, pero aún puede llevar al agente a tomar malas decisiones. El agente puede agotar el tiempo mientras enumera recursos, recibir en otro lugar una respuesta incompleta o antigua de la caché y concluir que un recurso no existe. Después intenta crearlo y choca con la realidad.

Clasifica las lecturas según la decisión que respaldan. Una actualización inofensiva de un panel puede reintentarse con un retroceso limitado. Una lectura que se use para decidir si emitir una escritura necesita una regla explícita de coherencia. Si el servicio ofrece una ETag, un número de versión o generación, o un endpoint de estado de lectura posterior a la escritura, úsalo. Si solo ofrece coherencia eventual, haz visibles para el agente el periodo de espera y la condición de fallo.

Evita un número de reintentos universal. Una consulta breve de metadatos puede tolerar dos reintentos rápidos. Una consulta de informes que cargue un almacén de datos puede necesitar un plazo largo y ningún intento inmediato adicional. Una llamada que devuelva `429 Too Many Requests` o un valor explícito de reintento del servicio requiere un tratamiento distinto al de un tiempo de espera del socket. Tratar todos los errores como problemas de red transitorios es la forma en que un agente convierte una interrupción parcial en carga evitable.

El retroceso ayuda a proteger los servicios, pero no resuelve la ambigüedad. Espacia más las solicitudes duplicadas. Combina el retroceso con un valor de idempotencia o una comprobación de estado para cada escritura importante.

Usa escrituras condicionales cuando la API las admita. Una solicitud `If-Match` con una ETag conocida puede impedir que un agente sobrescriba un recurso que cambió después de su lectura. Una creación que acepte un ID de recurso elegido por el cliente puede hacer que los reintentos converjan en un solo objeto. Estos mecanismos protegen las transiciones de estado, pero no sustituyen un registro que indique si se produjeron efectos externos a ese recurso.

## SSH oculta la ejecución remota detrás de un único flujo interrumpido

La recuperación ante tiempos de espera de SSH requiere más cautela que la de HTTP. Una sesión SSH perdida puede producirse después de que el host remoto inicie un comando, durante la transferencia de la salida, después de que termine el comando o mientras un proceso hijo continúa después de desconectarse su padre. Un mensaje del shell local no puede decirte cuál de estos casos ocurrió.

El patrón peligroso es un comando compuesto largo:

```sh
ssh deploy@host 'download-release \u0026\u0026 migrate-db \u0026\u0026 restart-service'
```

Si la conexión se interrumpe después de `migrate-db`, repetir el comando completo puede ejecutar dos veces las migraciones o reiniciar un servicio cuya nueva versión nunca terminó de descargarse. La transcripción de la terminal ha reducido varias transiciones de estado a un único resultado opaco.

Divide el trabajo remoto en operaciones con indicadores persistentes que se puedan inspeccionar. Una implementación puede registrar un ID de versión antes de comenzar, guardar las versiones de migración en la base de datos y mostrar la revisión activa mediante un comando de estado local. Una herramienta de recuperación se vuelve a conectar y consulta esos indicadores antes de hacer cualquier otra cosa.

Por ejemplo, un agente puede usar un comando de estado remoto cuya salida esté diseñada para máquinas y no para personas:

```sh
ssh deploy@host '/usr/local/bin/release-status --json'
```

```json
{
  "release_id": "rel_202",
  "phase": "migrated",
  "active_revision": "9f24c1",
  "migration_version": "20250308_02"
}
```

La acción de recuperación ya tiene una base para decidir. Si `phase` es `migrated`, no vuelvas a ejecutar las migraciones. Si el host no informa de ningún `release_id`, el agente puede iniciar la operación solo si el comando remoto garantiza que esa ausencia significa que no hubo una ejecución anterior. Si SSH no puede volver a conectarse, el resultado sigue siendo desconocido. No lo sustituyas por una repetición optimista cuando la acción cambie un host de producción.

Usa los bloqueos remotos con cuidado. Un bloqueo puede impedir ejecuciones simultáneas, pero un bloqueo obsoleto después de un fallo del host puede impedir la recuperación. Incluye el ID de operación y una política de caducidad en el registro del bloqueo, y permite inspeccionarlo sin borrarlo a ciegas. Un comando de limpieza que elimine todos los bloqueos antiguos también cambia el estado y necesita sus propias pruebas.

Sallyport puede mantener las credenciales SSH fuera del proceso del agente mientras su asistente incluido `sp-ssh` realiza la conexión, pero el aislamiento de credenciales no hace segura la repetición de una sesión interrumpida. El comando remoto sigue necesitando un ID de operación, indicadores persistentes y una ruta de reconciliación.

## Los registros de auditoría ayudan a investigar, pero no demuestran que el trabajo terminó

Un registro de acciones debe conservar suficiente información para reconstruir la intención y la recuperación sin guardar secretos. Registra la sesión del solicitante, la hora, la identidad del destino, el ID de operación, el resumen de la solicitud o la plantilla del comando, el evento de autorización, el resultado del transporte y el resultado final reconciliado. No registres tokens portadores, claves privadas ni cuerpos de solicitudes sin procesar que puedan contener credenciales o datos personales.

Separa una acción intentada de una acción completada. Una línea que diga `POST /deployments timeout` es un registro del intento. Una consulta posterior que devuelva una operación en estado `succeeded` es una prueba de finalización. Conserva ambas. Sustituir el primer registro por un éxito final borra la parte más útil del incidente: el periodo en que el solicitante no sabía qué había ocurrido.

La evidencia de manipulaciones importa cuando una ejecución del agente da lugar a una disputa posterior. Debes poder responder qué proceso hizo la llamada, qué estaba autorizado a hacer, si recibió un resultado y cómo estableció el equipo el estado final. Una tabla de actividad modificable es fácil de consultar, pero ofrece pruebas débiles si un proceso comprometido puede reescribir el historial.

Sallyport registra las sesiones de los agentes y las llamadas individuales en un registro de auditoría cifrado, encadenado mediante hashes y ciego a las escrituras; `sp audit verify` puede verificar la cadena sin conexión sobre el texto cifrado. Ese registro puede mostrar la acción de la puerta de enlace y el historial del solicitante, mientras que el servicio o el host remoto sigue siendo la autoridad sobre si terminó el trabajo previsto.

No confundas un evento de auditoría de la puerta de enlace con una transacción de la aplicación. Si la puerta de enlace registró una solicitud saliente, es posible que la solicitud fallara antes de que el servicio la confirmara. Si el servicio la confirmó, quizá la puerta de enlace nunca viera la respuesta. La investigación funciona cuando los registros de ambos lados comparten un ID de operación o de correlación.

## Prueba la ambigüedad deliberadamente antes de que una interrupción lo haga por ti

Un plan de tiempos de espera que nunca se ha enfrentado a una respuesta perdida es solo una suposición. Prueba el fallo exacto en el que el servicio completa la operación, pero el cliente pierde el resultado. Este es el caso que más equipos omiten porque los dobles de prueba habituales del camino feliz no pueden expresarlo.

Crea un endpoint de prueba o un accesorio de proxy que acepte una solicitud, confirme su registro persistente y después retrase o descarte la respuesta. Envía dos veces el mismo ID de operación. Verifica que el servicio devuelva una sola operación lógica, que el agente consulte el estado después del tiempo de espera y que el registro de auditoría conserve ambos intentos y el resultado de la reconciliación.

Después prueba el caso contrario: interrumpe la solicitud antes de que el servicio la acepte. Confirma que la recuperación solo pueda reintentar después de comprobar que no existe ningún registro de operación. Las dos pruebas pueden producir la misma excepción en el cliente. El contrato de la herramienta debe llevar a acciones distintas porque las pruebas del servicio son diferentes.

Prueba también estos casos:

- El servicio acepta la operación, pero su trabajador sigue pendiente después del plazo de recuperación del agente.
- Dos procesos del agente envían el mismo ID de operación casi al mismo tiempo.
- El endpoint de estado no está disponible mientras el endpoint principal de escritura funciona correctamente.
- Un comando SSH inicia un proceso hijo y después la conexión termina antes de la salida final.
- Una persona reanuda una ejecución pausada después de que otro operador ya haya reconciliado la acción.

El último caso detecta un problema que aparece en las operaciones reales: el estado de recuperación debe vivir fuera de la memoria conversacional del agente. Guarda el ID de operación y el hallazgo actual en un registro de ejecución persistente. Un agente reiniciado debe leer ese registro y continuar la reconciliación, no inventar una acción nueva porque no puede ver la transcripción anterior.

Configura alertas para los resultados desconocidos que superen sus plazos de recuperación. No alertes ante cada primer tiempo de espera si los reintentos habituales resuelven lecturas seguras. Avisa cuando una escritura importante no tenga un estado terminal confirmado, cuando varios ID de operación lleven contenidos diferentes o cuando los indicadores remotos contradigan la progresión prevista. Esos son los casos que necesitan a una persona antes de que el agente siga avanzando.

## Un plan de recuperación debe hacer habitual la negativa

La respuesta más sólida ante un tiempo de espera suele ser negarse: «No puedo confirmar si terminó la solicitud de implementación, así que no enviaré otra». Eso no es un fallo de la herramienta. Es la respuesta correcta cuando faltan pruebas sobre una acción irreversible o costosa.

Haz que esa respuesta sea útil. Muestra el ID de operación, el destino, el último estado confirmado, las marcas de tiempo y la consulta de estado exacta o la inspección remota que resolvería la duda. Si no existe una consulta autoritativa, dilo claramente y dirige la decisión a alguien que entienda las consecuencias de un duplicado.

Los equipos se resisten porque un reintento parece productivo y una pausa parece lenta. Después de suficientes escrituras duplicadas e implementaciones a medias, el intercambio queda claro. Un minuto dedicado a reconciliar cuesta menos que descubrir que dos sistemas creen haber realizado por separado la única acción que el agente debía ejecutar.

Empieza por las escrituras que mueven dinero, envían mensajes externos, realizan versiones, cambian accesos o eliminan datos. Para cada una, exige tres respuestas al responsable de la API: qué identificador vincula los reintentos a una sola operación, dónde puede consultar el resultado el solicitante y qué prueba existe cuando la conexión muere. Si falta alguna respuesta, haz que la herramienta devuelva `unknown` y exige una decisión humana deliberada en lugar de enseñar al agente a adivinar.
