Streaming de tool calls: acumular argumentos sin ejecutar a medias

Streaming de tool calls: acumular argumentos sin ejecutar a medias

El JSON no llega enteroenlace

En una respuesta sin streaming podríamos recibir directamente {"key":"Objective"}. Con streaming, el proveedor empieza a contestar mientras el modelo todavía está generando. El mismo JSON puede llegar dividido en varios eventos:

La llamada completa{"key":"Objective"}
Los deltas recibidos1 · {"key":"Obj2 · ective"}

El tamaño de cada lectura de red no sirve como frontera. Un chunk puede contener medio evento SSE, varios eventos completos o un JSON cortado en mitad de una palabra. La frontera útil es la que define el protocolo: una línea en blanco termina un mensaje SSE y un evento terminal termina el turno del proveedor.

La regla:

acumula texto durante el stream; interpreta los argumentos solo cuando el proveedor confirma que la llamada y el turno han terminado. Mostrar un borrador es válido. Ejecutarlo no.

Una secuencia real de deltasenlace

Esta es una versión pequeña de lo que puede recibir el parser de OpenAI. El modelo quiere llamar a lookup_demo_value, pero los argumentos se completan en dos deltas:

  1. Se anuncia la llamada

    El evento inicial trae el identificador y el nombre, pero todavía no hay un JSON completo que interpretar.

    response.output_item.added
    item.id = "fc_objective"
    item.name = "lookup_demo_value"
  2. Llega el primer delta

    El acumulador guarda el texto tal cual, aunque todavía no se pueda parsear.

    response.function_call_arguments.delta
    arguments = "{\"key\":\"Obj"
  3. Llega el segundo delta

    La cadena ya representa el JSON completo, pero la capa de tools sigue esperando la confirmación del turno.

    response.function_call_arguments.delta
    arguments = "ective\"}"
  4. El proveedor termina su turno

    response.completed sí es un evento del stream SSE que llega del proveedor. Le dice al parser que OpenAI terminó de enviar este turno. La llamada a executeTool ocurre después y la genera localmente el harness; no la envía el proveedor.

    // Provider SSE event
    response.completed
    // Local harness action
    executeTool("lookup_demo_value", { key: "Objective" })

El ejemplo usa el Responses API de OpenAI, pero la idea no depende de ese protocolo. En Anthropic Messages, los fragmentos llegan dentro del bloque tool_use y message_stop cierra el mensaje. En Google Interactions, interaction.created abre la interacción, step.start anuncia el function_call, los step.delta llevan arguments_delta, step.stop cierra el paso y interaction.completed cierra la interacción. Cuando Google termina con estado requires_action, el harness también tiene que ejecutar las tools; el nombre de los eventos cambia, pero la frontera de seguridad es la misma.

Qué significa acumular una llamadaenlace

Una tool call en streaming no aparece como un objeto completo. Durante unos instantes es un registro incompleto: sabemos qué tool ha pedido el modelo, pero todavía estamos reuniendo sus argumentos. Cada delta añade una pequeña parte de ese registro.

Por eso el parser mantiene un estado pendiente para cada llamada. “Acumular” solo significa conservar ese registro y añadir cada fragmento al texto de argumentos hasta que llega la señal final. El acumulador no ejecuta la tool ni decide que el JSON ya es válido: reconstruye lo que el proveedor ha enviado para que otra capa pueda decidir después.

1. Identidad

¿A qué llamada pertenece?

El parser necesita una clave de correlación. OpenAI usa item_id y output_index; Anthropic usa el índice del bloque tool_use; Google usa el índice del step para correlacionar los arguments_delta. El id del function_call se conserva para el resultado final y se expone como call_id.

2. Contenido

¿Qué fragmento acaba de llegar?

El delta aporta texto de argumentos. Se añade al registro correcto sin intentar interpretar cada fragmento por separado.

3. Estado

¿Ya se puede cerrar?

El registro permanece abierto hasta el evento terminal. Que el texto parezca un JSON completo no basta.

for (const delta of event.arguments) {
  const call = pendingCalls.get(delta.callId);
  call.argumentText += delta.text;
}

// No se ejecuta aquí: todavía falta el evento terminal.
const completeArgs = JSON.parse(pendingCalls.get(callId).argumentText);

Este pseudocódigo no describe un SDK concreto. Muestra la idea: el mapa separa las llamadas, argumentText guarda el JSON todavía incompleto y JSON.parse queda después de la frontera terminal. El loop de tools no debe recibir ese texto antes de comprobar que el turno terminó.

Cuándo puede actuar el harnessenlace

Hasta aquí solo hemos hablado de recibir y reconstruir datos. Ejecutar una tool es otra fase y ocurre en el harness, no dentro del stream. El recorrido completo es: el proveedor envía eventos, el parser los convierte en un resultado —o en un error— y el harness decide qué hacer con ese resultado. En OpenAI y Anthropic, truncated: false significa que el turno terminó con normalidad y truncated: true significa que terminó demasiado pronto. En Google, un corte antes de interaction.completed se comunica como un error truncated, por lo que el harness no recibe un turno ejecutable.

La tabla se lee de izquierda a derecha: primero el evento que llegó del proveedor, después lo que el parser entrega al harness y, por último, la acción que el harness puede tomar. El proveedor nunca envía executeTool.

Evento del proveedorQué recibe el harnessQué puede hacer después
OpenAI envió response.completed.truncated: false
Turno completo.
Parsear los argumentos acumulados y ejecutar las llamadas completas.
Anthropic envió message_stop.truncated: false
Turno completo.
Parsear los bloques tool_use cerrados y ejecutar las llamadas completas.
Google envió interaction.completed después de cerrar sus step.Interacción completa con status: "requires_action".Parsear y validar los function_call completos antes de ejecutar las tools.
La conexión se cerró antes del evento terminal.OpenAI/Anthropic: truncated: true. Google: error truncated.Rechazar el turno. No llamar a executeTool con argumentos parciales.
La decisión no la marca el JSON.

Un fragmento que se puede parsear solo demuestra que el texto actual tiene forma de JSON. No demuestra que el proveedor haya terminado. El harness puede actuar únicamente después de que el parser confirme que el turno está completo.

Qué enseñar mientras llegaenlace

Esperar no obliga a dejar la interfaz congelada. La UI puede enseñar que el agente está preparando una acción sin presentar el borrador como un hecho ni prometer que la tool se ejecutará:

Mientras llega

«Preparando una consulta…» o una tarjeta de progreso. El texto parcial de los argumentos se queda en diagnóstico, no en la conversación normal.

Cuando termina

Si el turno es completo, se ejecuta la llamada y se muestra el resultado que devuelva la tool.

Si se corta

Se informa de que la respuesta se interrumpió y se permite reintentar. No se ejecuta una acción con datos incompletos.

Qué pasa si el stream se cortaenlace

Un corte a mitad de {"key":"Obj no es una llamada con argumentos vacíos ni una llamada con JSON “casi válido”. Es un turno incompleto. En OpenAI y Anthropic, el parser conserva la información suficiente para diagnosticarlo y devuelve truncated: true; en Google comunica un error truncated. En los tres casos, la capa de tools detiene el flujo antes de ejecutar.

Mensaje local:

«La respuesta del proveedor se cortó antes de completarse. Vuelve a intentarlo.» El harness puede adaptar el nombre del proveedor, pero nunca convertir este error en una llamada ejecutable.

Estas son dos comprobaciones distintas que hace el harness después de recibir el resultado del parser. Primero pregunta ¿recibimos todo el turno?: comprueba truncated: false en OpenAI y Anthropic, o comprueba que Google no haya devuelto el error truncated. Solo entonces pregunta ¿los argumentos tienen la forma permitida?: los parsea cuando hace falta y los valida contra el schema de la tool. Reemplazar un JSON inválido por {} solo evita que una capa antigua falle al intentar leerlo; no demuestra que el stream terminara ni que los argumentos sean aceptables. La secuencia segura es: resultado completo del parser, validación del harness y, únicamente si todo es correcto, executeTool.

Testsenlace

La prueba base para este problema no llama a un proveedor real. Reproduce un SSE grabado y comprueba la propiedad que queremos proteger: con los mismos eventos, el parser y el harness deben esperar al evento terminal antes de ejecutar, aunque la red entregue los datos en fronteras arbitrarias. Una llamada real añade variabilidad de red y del modelo, así que no demuestra por sí sola este comportamiento local; por eso esta prueba se complementa con tests de contrato o E2E del proveedor.

Acumulación

Varios deltas terminan en {"key":"Objective"}.

Intercalado

Dos llamadas reciben cada fragmento por su item_id/output_index, índice de bloque tool_use o índice de step de Google.

Truncamiento

Un stream sin evento terminal marca el turno como truncado y no llama a la tool.

Fronteras de red

El mismo stream se corta en posiciones arbitrarias sin cambiar el resultado final.

Aprender a construir un agente completoenlace

Este artículo pertenece a una serie sobre cómo desarrollar un agente o harness de principio a fin. El siguiente paso después de reconstruir los argumentos es validar que cumplen el esquema declarado antes de ejecutar la tool.

Ver el índice completo de la serie sobre el agente de Gymnasia.

Seguir leyendo

Últimos posts -->

¿Has visto estos proyectos?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

App de fitness con dos agentes que se ejecutan íntegramente en el dispositivo, sin backend, de forma que los datos del usuario nunca salen del móvil. Un coach conversacional BYOK con tools locales y system prompt remoto con fallback offline, y un estimador de comidas que saca los macronutrientes de una foto del plato, con lectura de códigos de barras contra OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Sistema multiagente de investigación construido con LangGraph. Un supervisor descompone tu pregunta en temas y lanza subagentes de búsqueda en paralelo; cada uno comprime sus hallazgos antes de pasarlos a un agente redactor que escribe el informe final en markdown con sus fuentes. Streaming en vivo por WebSockets, modelo configurable por rol y claves de API propias que nunca se guardan en el servidor.

Tau

Tau Tau
Python
LangChain

Sistema multiagente de tutoría para estudiantes de secundaria, con un agente por asignatura y material de curso elaborado y validado por un equipo de profesores. Llegó a usarse con alumnos reales en un colegio privado en España y en un instituto en Colombia.

Ver todos los proyectos -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres ver alguna charla?

Últimas charlas -->

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Flow edit

Flow edit Flow edit

Edita imágenes con este modelo de Flow. Basándose en SD3 o FLUX puedes editar cualquier imagen y generar nuevas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos los contenedores -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

HuggingFace

Dataset de chistes en inglés

Uso: Fine-tuning de modelos de generación de texto humorístico

231K filas 2 columnas 45 MB
Ver en HuggingFace →

opus100

HuggingFace

Dataset con traducciones de inglés a español

Uso: Entrenamiento de modelos de traducción inglés-español

1M filas 2 columnas 210 MB
Ver en HuggingFace →

netflix_titles

HuggingFace

Dataset con películas y series de Netflix

Uso: Análisis de catálogo de Netflix y sistemas de recomendación

8.8K filas 12 columnas 3.5 MB
Ver en HuggingFace →
Ver más datasets -->