Timeouts en tools HTTP: AbortSignal.timeout, no un sleep eterno
Resumen
Una tool de fetch sin signal cuelga el agente hasta el idle timeout del host. AbortSignal.timeout(ms) aborta con TimeoutError. Esta guía cubre fetch + signal, AbortSignal.any con cancelación del usuario, y por qué el timeout vive en la tool, no en un setTimeout olvidado. MDN + Node.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente que llama fetch(url) sin signal hereda el peor timeout del runtime: a veces minutos, a veces nunca. Mientras tanto el loop sigue “pensando”, el usuario espera, y un retry duplica el side effect. El estándar ya tiene el corte: AbortSignal.timeout(ms).
Esto no es el retry/backoff del loop entero. Es el techo de una tool. Sandbox y secretos cubren otro borde; aquí es red.
Una línea
const res = await fetch(url, { signal: AbortSignal.timeout(8_000) });
MDN: AbortSignal.timeout(n) devuelve un signal que se aborta pasado n ms. El abort es un DOMException cuyo name es TimeoutError. No inventes new Promise((_, rej) => setTimeout(rej, n)) alrededor del fetch: no cancelas el socket.

Timeout + cancelación del usuario
Si el harness ya pasa un AbortSignal (el humano pulsó stop), no lo pises. Combina:
function toolSignal(user: AbortSignal | undefined, ms: number) {
const t = AbortSignal.timeout(ms);
return user && "any" in AbortSignal
? AbortSignal.any([user, t])
: t;
}
AbortSignal.any aborta cuando cualquiera de la lista aborta. Node 20+ y browsers modernos. Si any no existe, usa el timeout solo y documenta el techo: el stop del usuario puede llegar tarde.
Catch:
try {
return await fetch(url, { signal: toolSignal(ctx.signal, 8_000) });
} catch (e) {
if (e instanceof DOMException && e.name === "TimeoutError") {
throw new Error("tool_timeout: fetch > 8s");
}
throw e;
}
El mensaje tool_timeout es lo que el modelo ve. “Failed to fetch” no le enseña a acortar la URL.
Dónde vive el número
| Tool | Techo de partida | Por qué |
|---|---|---|
| HTTP GET docs | 8 s | MDN/HTML cabe; si no, no es “un GET” |
| HTTP POST cobro | 15 s | El proveedor puede ser lento; igual hay techo |
| LLM interno (subllamada) | el del SDK | No envuelvas otro modelo en 8 s a ciegas |
| Shell | el del sandbox | timeout 30s cmd en Unix; no AbortSignal |
Un default por tool en el registro (timeoutMs: 8000) gana a un global de 120 s que nunca muerde.
Retries de la tool: si reintentas un POST, idempotency key. Timeout ≠ “no pasó”; el servidor pudo ejecutar.

Checklist
- Todo
fetchen tools llevasignal. AbortSignal.timeout, noPromise.raceque deja el fetch vivo.- Error tipado
TimeoutError→ string estable al modelo. - Combina con el signal del usuario si existe.
- POST + retry ⇒ clave de idempotencia (secretos no sustituyen esto).
- Test: mock de fetch que nunca resuelve + expect timeout (Vitest, fake timers o un delay real corto).
El curso instalar un agente no pone techos de red. Sandboxing corta el FS, no un GET a un host lento.
FAQ
¿undici / Node 18 fetch? AbortSignal.timeout está en Node 17.3+ / 18. El de node-fetch v2 no. Sube o envuelve.
¿Axios timeout? Vale. Sigue siendo un techo de tool, no del agente.
¿3 s como Discord? Eso es el ack HTTP del bot, no el GET interno. Números distintos.
¿Timeout del modelo (max_tokens / SDK)? Otra capa. Esta guía es I/O de tools.
Un Promise.race que “gana” el timeout no aborta el fetch: el proceso sigue descargando y puede disparar el handler después. El signal es el corte real. Si ves CPU/red residual tras “timeout”, buscaste race, no AbortSignal.
Verificado 2026-09-03 contra MDN AbortSignal.timeout y Fetch API.
Lecturas relacionadas
Sigue explorando AgentOps y otras piezas para builders.

Tests de tools de agentes sin LLM: Vitest primero, evals después

Idempotencia en webhooks de agentes: no cobres ni envíes dos veces

AWS empuja AgentOps con AgentCore: observabilidad, evals y gobernanza dejan de ser extras para agentes
