Saltar al contenido
astorlm
Idioma: Español
← Mapa

Nivel 6

Streaming y eventos

Un modelo escribe su respuesta de a unos pocos tokens. El streaming te entrega cada pedacito apenas se escribe, y los eventos del bucle le cuentan al resto de tu app qué está haciendo el agente, mientras lo hace.
1/37 Pliegues del bandoneón:
  • user
  • assistant
  • tool_result
Empieza el show. El Oráculo canta, las notas bajan por el mástil y la persona de la guitarra las toca para el público. Primero, un pedido común: sin streaming.

EventBus

El problema

Llamar a un modelo y esperar la respuesta entera funciona en un script. Frente a una persona, no: una respuesta larga puede tardar diez o veinte segundos, y durante todos ellos la pantalla no muestra nada. Un agente lo empeora, porque un pedido puede correr varios turnos y herramientas antes de que exista la respuesta final.

Y no es solo la persona la que quiere seguir lo que pasa. La interfaz quiere el texto, tus logs quieren cada paso, la facturación quiere los tokens de cada turno. Si cada una de esas cosas hay que escribirla dentro del bucle, el bucle termina conociendo cada pantalla, archivo y base de datos de tu app.

La solución

Dos piezas que van juntas. Streaming: pídele al proveedor la respuesta como un flujo, y te manda cada pedacito de texto apenas el modelo lo escribe. El tiempo total es el mismo, pero la primera palabra aparece en una fracción de segundo en lugar de al final.

Eventos: el bucle anuncia cada paso en un bus, y no le importa quién escucha. Tu app suscribe las partes a las que les interesa: la pantalla escucha el texto, el log escucha todo, el medidor escucha el final de cada turno. Agregar un oyente no toca el bucle. Estos son los eventos que astorlm emite en una ejecución normal:

  • turn_start

    Empieza un turno: el bucle está por llamar al modelo.

  • text_delta

    Llegó un pedacito de la respuesta. Hay muchos por turno.

  • assistant_message

    El mensaje del modelo está completo, con el texto y las llamadas a herramientas.

  • tool_execution_start

    Una herramienta está por ejecutarse, con su entrada.

  • tool_execution_end

    La herramienta terminó: su salida, si falló y cuánto tardó.

  • turn_end

    El turno terminó, con los tokens que usó su llamada al modelo.

  • session_end

    La ejecución terminó: completada, cancelada o con error.

Las llamadas a herramientas también llegan del proveedor en streaming, de a unos pocos caracteres de sus argumentos. El bucle las vuelve a armar antes de ejecutarlas, así que en el bus una llamada a una herramienta aparece entera, en assistant_message.

El elenco

El mismo elenco de siempre, sobre un escenario de rock.

El Oráculo el modelo
El cantante, arriba en la tarima. Su respuesta baja por el mástil como notas, una por palabra.
El mástil el stream
Cada text_delta es un puñado de notas. Sin streaming, no baja nada hasta el final.
La persona de la guitarra tu app
Toca cada nota cuando llega a los trastes, y las letras aparecen para el público.
El cable el EventBus
Todos los eventos corren por él. Solo se encienden los oyentes que se suscribieron a ese evento.
Los oyentes agent.on(…)
La pantalla de letras ('text'), una casetera ('event') y un medidor de tokens (turn_end).
Astor el bucle
Corre el bucle como siempre, y anuncia cada paso en el bus a medida que avanza.
El escenario las herramientas
El borde del escenario, la consola de luces y la caja de pirotecnia: crowd_mood, set_lights y pyro.

El código

Con astorlm: el proveedor siempre hace streaming, así que no hay nada que activar. Suscríbete con agent.on() antes de llamar a run(): 'text' para cada pedacito de texto, 'tool-start' y 'tool-end' para las herramientas, 'event' para todo. Cada llamada devuelve una función que cancela la suscripción.

Desde cero: el bucle del nivel 2 con dos cambios. El pedido usa stream: true y lee la respuesta como server-sent events, pegando los pedacitos; y una lista de oyentes recibe cada paso a través de un único emit().

import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

// The stage's functions, wrapped as tools.
const crowdMood = tool({
  name: 'crowd_mood',
  description: 'Look at the crowd from the front of the stage: how they feel, and what they came to hear.',
  schema: z.object({}),
  execute: async () => stage.readCrowd(), // your code
})

const setLights = tool({
  name: 'set_lights',
  description: 'Set the color of every light on the stage.',
  schema: z.object({ color: z.enum(['amber', 'red', 'blue', 'white']) }),
  execute: async ({ color }) => stage.lights(color), // your code
})

const pyro = tool({
  name: 'pyro',
  description: 'Fire the pyrotechnics on both sides of the stage. Once per show.',
  schema: z.object({}),
  execute: async () => stage.firePyro(), // your code
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [crowdMood, setLights, pyro],
  maxTurns: 10,
})

// Streaming is always on: subscribe before run(). Each listener only hears what it asked for.
// The lyrics screen: every piece of text, the moment it arrives.
agent.on('text', (piece) => lyrics.append(piece))

// The tape deck: every event the loop emits, in order.
agent.on('event', (event) => tape.write(event))

// The meter: the tokens each turn reported.
agent.on('event', (event) => {
  if (event.type === 'turn_end' && event.usage) meter.add(event.usage.inputTokens + event.usage.outputTokens)
})

// Each on() returns a function that unsubscribes.
const stopTape = agent.on('tool-start', ({ name }) => console.log('→', name))

const last = await agent.run('Open the show: read the crowd, set the mood, and greet them.')
stopTape()
console.log(last.content) // the same text, whole, once the run is over

Qué vigilar

  • El streaming no hace más rápido al modelo. La respuesta tarda lo mismo que antes; lo que cambia es cuándo ve la persona la primera palabra. Mide el tiempo hasta el primer token, no solo el tiempo total.
  • Los oyentes miran; no manejan. Lo que devuelve un oyente se ignora, y el bucle no lo espera. Para bloquear una herramienta o cambiar lo que lee el modelo, necesitas un hook: el próximo nivel.
  • Que los oyentes sean rápidos. El bus de astorlm los llama uno detrás del otro, dentro del bucle. El trabajo lento, como escribir en una base de datos, va en una cola a la que el oyente solo empuja.
  • Un oyente roto falla en silencio. astorlm atrapa su error para que la ejecución siga, lo que también significa que nadie se entera. Registra los errores dentro de tus oyentes.
  • Los pedacitos no respetan las palabras. Un text_delta puede terminar en mitad de una palabra o de una tabla en Markdown. Muestra lo que tienes hasta ahora y vuelve a dibujar a medida que llega más.
  • Deja que la persona lo frene. Cuando puede ver llegar la respuesta, va a querer cortar una que va mal. Conecta un botón de stop a agent.abort().