Ir al contenido principal
Streaming con Tool Use en Next.js: el agente que habla

Streaming con Tool Use en Next.js: el agente que habla

AI Integration
7 min readPor Daily Miranda Pardo

Tu agente de soporte arranca la respuesta: "Estoy revisando tu pedido...". Los tokens llegan uno a uno. Todo fluye. Entonces el agente necesita consultar la base de datos del cliente. El stream se para. El cursor parpadea. El usuario espera mirando una pantalla que no hace nada durante tres segundos. Cierra la pestaña.

Este no es un bug de Vercel ni un problema de timeouts. Es un problema de arquitectura de streaming. La mayoría de tutoriales de IA muestran cómo hacer stream de texto. Ninguno explica qué pasa cuando el agente llama a una herramienta en mitad del stream, y cómo mostrarle al usuario que algo está ocurriendo mientras tanto.

Por qué el streaming rompe con tool calls

El SDK de Anthropic emite eventos de distintos tipos cuando usas messages.stream(). La mayoría de implementaciones solo escuchan uno:

EventoQué significa
content_block_delta + text_deltaToken de texto generado
content_block_start + tool_useEl agente quiere llamar a una herramienta
content_block_delta + input_json_deltaParámetros de la herramienta (incremental)
message_stopEl agente terminó su turno

El error habitual: el servidor solo envía los deltas de texto, ejecuta la herramienta en silencio y luego reanuda el stream. El cliente ve texto, silencio, más texto. No sabe que el agente está trabajando activamente.

La solución es que el servidor emita un evento por cada fase del ciclo — incluyendo cuándo empieza a llamar a una herramienta y cuál es su nombre — para que el cliente pueda mostrar un indicador de progreso concreto.

El servidor: Route Handler en Next.js 16

El patrón correcto usa un bucle agentico explícito: el servidor sigue iterando mientras el modelo devuelva stop_reason: "tool_use". Cada iteración envía eventos SSE al cliente.

// app/api/agent/stream/route.ts
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const TOOLS: Anthropic.Messages.Tool[] = [
  {
    name: "buscar_cliente",
    description: "Obtiene historial y pedidos del cliente por ID",
    input_schema: {
      type: "object" as const,
      properties: { cliente_id: { type: "string", description: "ID del cliente" } },
      required: ["cliente_id"],
    },
  },
];

export async function POST(req: Request) {
  const { message } = await req.json();
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      const send = (data: object) =>
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        );

      const messages: Anthropic.Messages.MessageParam[] = [
        { role: "user", content: message },
      ];

      // Bucle agentico: el modelo puede llamar N herramientas antes de responder
      while (true) {
        const agentStream = anthropic.messages.stream({
          model: "claude-sonnet-4-6",
          max_tokens: 1024,
          tools: TOOLS,
          messages,
        });

        let activeToolBlock: Anthropic.Messages.ToolUseBlock | null = null;

        for await (const event of agentStream) {
          if (
            event.type === "content_block_delta" &&
            event.delta.type === "text_delta"
          ) {
            send({ type: "text", text: event.delta.text });
          } else if (
            event.type === "content_block_start" &&
            event.content_block.type === "tool_use"
          ) {
            // Avisar al cliente: el agente está llamando a esta herramienta
            activeToolBlock = event.content_block;
            send({ type: "tool_start", name: event.content_block.name });
          }
        }

        const finalMsg = await agentStream.finalMessage();

        if (finalMsg.stop_reason !== "tool_use" || !activeToolBlock) {
          send({ type: "done" });
          controller.close();
          break;
        }

        // Ejecutar herramienta y notificar al cliente del resultado
        const toolResult = await executeToolCall(activeToolBlock);
        send({ type: "tool_result", name: activeToolBlock.name });

        // Construir el historial para la siguiente iteración
        messages.push({ role: "assistant", content: finalMsg.content });
        messages.push({
          role: "user",
          content: [
            {
              type: "tool_result",
              tool_use_id: activeToolBlock.id,
              content: JSON.stringify(toolResult),
            },
          ],
        });
      }
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

async function executeToolCall(
  block: Anthropic.Messages.ToolUseBlock
): Promise<unknown> {
  const input = block.input as { cliente_id?: string };
  // Aquí iría la llamada real a tu base de datos o CRM
  return {
    nombre: "García López",
    pedidos: 3,
    último_pedido: "2026-09-08",
    estado: "activo",
  };
}

El detalle que más falla en implementaciones apresuradas: el historial debe incluir el bloque tool_use del asistente y el tool_result del usuario. Sin los dos, el modelo no tiene contexto para continuar.

El cliente: hook de React con estado de agente

El hook gestiona cuatro estados: idle, streaming, calling-tool y done. Esto permite renderizar feedback específico en cada fase.

// hooks/useAgentStream.ts
import { useState } from "react";

type AgentStatus = "idle" | "streaming" | "calling-tool" | "done";

export function useAgentStream() {
  const [text, setText] = useState("");
  const [status, setStatus] = useState<AgentStatus>("idle");
  const [activeTool, setActiveTool] = useState<string | null>(null);

  const send = async (message: string) => {
    setText("");
    setStatus("streaming");

    const response = await fetch("/api/agent/stream", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message }),
    });

    const reader = response.body!.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      for (const line of decoder.decode(value).split("\n")) {
        if (!line.startsWith("data: ")) continue;
        try {
          const event = JSON.parse(line.slice(6));

          switch (event.type) {
            case "text":
              setText((prev) => prev + event.text);
              break;
            case "tool_start":
              setStatus("calling-tool");
              setActiveTool(event.name);
              break;
            case "tool_result":
              setStatus("streaming");
              setActiveTool(null);
              break;
            case "done":
              setStatus("done");
              break;
          }
        } catch {
          // Ignorar líneas malformadas del stream
        }
      }
    }
  };

  return { text, status, activeTool, send };
}

En la UI, el estado calling-tool permite mostrar exactamente qué está haciendo el agente:

// Uso en componente
const { text, status, activeTool, send } = useAgentStream();

<div>
  {text && <p>{text}</p>}
  {status === "calling-tool" && (
    <span className="text-yellow-400 text-sm animate-pulse">
      Consultando {activeTool?.replace(/_/g, " ")}...
    </span>
  )}
</div>

Lo que cambia en producción

Sin este patrón: texto generado → silencio de 2-3 segundos → más texto. El usuario no sabe si la app se colgó.

Con este patrón: texto generado → "Consultando historial de cliente..." → texto con datos reales. El usuario ve un proceso coherente.

El impacto no es solo perceptual. Los agentes con herramientas tienen latencias reales que el usuario interpreta como errores si no reciben feedback. Un indicador correcto convierte una espera de 3 segundos en algo que el usuario tolera sin ansiedad.

Este es el patrón estándar en los proyectos de integración IA que hacemos con clientes: el agente explica en tiempo real lo que está haciendo antes de dar la respuesta final.

Tres gotchas que destrozan la implementación

1. No incluir el tool_use block en el historial del asistente

Si envías solo el tool_result sin el bloque tool_use previo del asistente, el modelo recibe una respuesta de herramienta sin contexto. El error aparece como un comportamiento extraño o una excepción del SDK.

2. Asumir que solo hay una llamada a herramienta

Un agente real puede encadenar múltiples llamadas. El patrón con while (true) es deliberado: el bucle solo termina cuando stop_reason no es "tool_use". Si lo implementas con una sola iteración, el agente trunca su respuesta cuando necesita más de una herramienta.

3. No manejar errores en la ejecución de herramientas

Si la llamada a tu base de datos falla, debes enviar un tool_result con el error igualmente — el modelo necesita saber que la herramienta respondió, aunque sea con un error. Si no envías nada, el bucle cuelga.

try {
  const result = await executeToolCall(activeToolBlock);
  messages.push({ role: "user", content: [{ type: "tool_result", tool_use_id: activeToolBlock.id, content: JSON.stringify(result) }] });
} catch (err) {
  messages.push({ role: "user", content: [{ type: "tool_result", tool_use_id: activeToolBlock.id, content: `Error: ${(err as Error).message}`, is_error: true }] });
}

El tiempo que cuesta hacerlo bien

Implementar este patrón desde cero lleva entre 3 y 8 días dependiendo de la complejidad del agente. El bucle agentico, el manejo correcto del historial, los errores en herramientas, la cancelación con AbortController, el reconectar si cae la conexión... cada pieza tiene sus edge cases.

En los proyectos que construimos con AI Driven Development, esto es infraestructura que entregamos desde el primer sprint, no algo que hay que descubrir después de que el agente esté en producción y los usuarios se quejen de pantallas congeladas.

Si estás construyendo un agente de producción con tool use y no quieres perder ese tiempo en infraestructura, escríbeme por WhatsApp. Lo que acabas de leer es exactamente cómo arrancamos.

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.