Ir al contenido principal
AbortController en agentes IA: cancela sin romper el estado

AbortController en agentes IA: cancela sin romper el estado

AI Integration
••5 min read•Por Daily Miranda Pardo

Son las 10:47 de la mañana. Tu agente procesa un PDF de 80 páginas para un cliente. El análisis tarda 25 segundos. A mitad del proceso, el usuario hace clic en "Cancelar".

Tú crees que paró. Pero no paró.

El stream de tokens sigue corriendo en el servidor. La herramienta de escritura en base de datos ya arrancó. Cuando revises la factura de Anthropic a final de mes, ese request cancelado aparece entero en el historial de costes.

El ciclo de vida que nadie te explica

Cuando un usuario cancela en el frontend, el navegador cierra la conexión HTTP. El servidor Next.js recibe la señal de aborto. Pero si no propagas esa señal hacia el agente —hacia el SDK, hacia tus tools, hacia tu transacción de base de datos— cada componente sigue ejecutándose en su propio mundo.

El resultado típico sin AbortController bien implementado:

  • LLM sigue generando tokens (cobrado al 100%)
  • Tool call en vuelo termina su trabajo (side effects ejecutados)
  • Base de datos recibe una escritura parcial (estado inconsistente)
  • El usuario ya no está, así que nadie detecta el error hasta semanas después

Es el bug invisible por excelencia. No lanza excepción. No aparece en los logs de error. Solo cobra.

AbortController: la señal que conecta toda la cadena

AbortController es el estándar de JavaScript para coordinar cancelaciones. Creas un controlador, pasas su signal a cada operación que quieres poder interrumpir, y cuando llamas a controller.abort(), todo lo que tenga esa señal se cancela de forma coordinada.

En una Route Handler de Next.js, la señal ya existe gratis: req.signal detecta cuándo el cliente cierra la conexión.

// app/api/agent/route.ts
export async function POST(req: Request) {
  const { messages } = await req.json();
  const abortSignal = req.signal; // ← disponible automáticamente

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        await runAgent(messages, abortSignal, (chunk) => {
          controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
        });
      } catch (err) {
        if ((err as Error).name === "AbortError") {
          return; // cancelación limpia, no es un error real
        }
        throw err;
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream" },
  });
}

La clave está en runAgent: recibe la señal y la propaga hacia todo lo que puede bloquearse.

Propagar el signal al SDK de Anthropic

El SDK de Anthropic acepta signal como opción tanto en messages.create como en el streaming. Cuando la señal se activa, el SDK lanza un AbortError inmediatamente en lugar de esperar la respuesta completa.

async function runAgent(
  messages: MessageParam[],
  signal: AbortSignal,
  onChunk: (text: string) => void
) {
  const anthropic = new Anthropic();

  const stream = await anthropic.messages.stream(
    {
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      system: "Eres un asistente especializado en análisis de documentos.",
      messages,
    },
    { signal } // ← propaga la señal al SDK
  );

  for await (const event of stream) {
    if (signal.aborted) break;

    if (
      event.type === "content_block_delta" &&
      event.delta.type === "text_delta"
    ) {
      onChunk(event.delta.text);
    }
  }
}

El caso del tool call que ya salió

El escenario más delicado: el LLM ya emitió el bloque tool_use y tu agente está ejecutando la herramienta cuando llega la cancelación. La señal abortó el stream, pero la tool ya arrancó.

Hay dos estrategias según el tipo de operación:

Operaciones de solo lectura (buscar en base de datos, consultar API externa): déjalas terminar. Sin side effects, ignorar el AbortError es seguro y más sencillo.

Operaciones de escritura (crear registro, enviar email, llamar a webhook): envuélvelas en una transacción y ejecuta rollback si la señal se activa antes del commit.

Rollback en escrituras: el patrón correcto

async function writeToDbSafe(data: InvoiceData, signal: AbortSignal) {
  if (signal.aborted) {
    throw new DOMException("Señal abortada antes de escribir", "AbortError");
  }

  const client = await db.connect();
  try {
    await client.query("BEGIN");
    await client.query(
      "INSERT INTO invoices (data, created_at) VALUES ($1, NOW())",
      [JSON.stringify(data)]
    );

    // Comprobar señal justo antes del COMMIT
    if (signal.aborted) {
      await client.query("ROLLBACK");
      throw new DOMException("Señal abortada: rollback ejecutado", "AbortError");
    }

    await client.query("COMMIT");
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

La clave: comprobar signal.aborted después de la última operación asíncrona y antes del COMMIT. Si el usuario canceló durante la escritura, el rollback deja la base de datos exactamente en el estado anterior al request. Sin registros huérfanos, sin datos a medias.

Lo que la mayoría implementa (y lo que falta)

La implementación más habitual es poner el AbortController en la Route Handler y no propagarlo más allá. El stream se corta para el usuario, que ve la cancelación, pero el agente sigue corriendo en el servidor hasta terminar.

Cuando llevas servicios de integración IA a producción con carga real —50, 100 requests concurrentes— ese overhead acumulado importa. Si tu agente tiene una tasa de cancelación del 20% en picos de uso, ese 20% de trabajo completado para nadie aparece íntegro en la factura del LLM.

La propagación completa cuesta escribir dos o tres funciones de ayuda. En un agente que procesa documentos o hace tool calls costosos, esa inversión se recupera en la primera semana de producción real.

Para patrones avanzados de arquitectura en agentes IA y automatización, el manejo de cancelaciones es uno de los primeros puntos del checklist de producción.

Conclusión

Un AbortController bien implementado no es un detalle de UX: es infraestructura de producción. La señal tiene que viajar desde el cierre de conexión HTTP hasta el último query de base de datos, pasando por el SDK de Anthropic y cada tool que pueda escribir estado.

Sin esa cadena completa, estás pagando por trabajo que nadie vio, con datos parciales en tu base de datos que nadie limpia.

¿Tienes un agente en producción o estás implementando uno? Hablémoslo →

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Sin spam. Solo el PDF. Puedes darte de baja cuando quieras.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.