Saltar al contenido principal
ctx.stream(source, opts) envía una respuesta que todavía se está generando (tokens de un LLM, un informe largo) a medida que se produce, con la mejor estrategia que admite el canal: borradores nativos en Telegram, ediciones en vivo en Discord y mensajes del tamaño de una frase con “escribiendo…” en WhatsApp. El mismo handler funciona en todos los canales.
La fuente puede ser cualquier AsyncIterable de strings o bytes (un generador async, un Readable de Node), un ReadableStream web o un Iterable de strings. Los fragmentos en bytes se decodifican como UTF-8.

Estrategias por canal

El provider declara capabilities.streaming ('draft' | 'edit' | 'none') y ctx.stream() elige la estrategia a partir de ahí:
  • Los borradores solo se intentan en chats privados. Si el transporte rechaza un borrador en tiempo de ejecución, el stream baja a ediciones (si el canal puede editar) o a trozos.
  • strategy fuerza una opción; una no soportada baja draft → edit → chunk. 'chunk' y 'buffer' siempre están disponibles.
  • Ningún mensaje supera maxLength (por defecto capabilities.maxTextLength, si no 4096): el texto largo se parte por límites naturales (párrafos, líneas, frases, palabras) y sigue en un mensaje nuevo. En los canales que cuentan bytes (capabilities.maxTextBytes, Instagram) ningún mensaje supera tampoco esos bytes UTF-8.

Opciones

ctx.stream() resuelve con un StreamResult: text, messages, strategy (la que se usó al final, tras cualquier fallback), cancelled y stoppedByUser. Rechaza con el error de la fuente (después de enviar lo producido) o con el primer error de envío.

Cancelación

Un stream se detiene cuando se aborta signal o, en los borradores de Telegram con canStop: true, cuando el usuario pulsa el botón de parar (stoppedByUser: true). Se deja de leer y se llama al return() de la fuente, así que una petición de IA también se aborta. bot.stop() también cancela todos los ctx.stream() en curso (después del drain, si lo hay), pero no envía nada más: ni envío final, ni edición, ni borrado, diga lo que diga onCancel.
  • onCancel: 'flush' (por defecto) envía el texto producido hasta entonces.
  • onCancel: 'discard' no envía nada más: un borrador simplemente caduca y, con la estrategia de edición, se borra el mensaje que se estaba editando (si el canal puede borrar; si no, solo se quita el cursor). Los mensajes ya completados antes de cancelar se quedan.
El long-poll de Telegram pide las actualizaciones stopped_message_generation, que traen las pulsaciones del botón de parar.

Límites de envío

Las actualizaciones en vivo se espacian y nunca corren en paralelo. Si el transporte responde con un RateLimitedError, se pausan durante su retryAfterMs. La edición final (que quita el cursor y muestra el texto completo) espera a que pase la pausa y reintenta un límite hasta tres veces.

Hooks y el log de mensajes

Todo lo que envía ctx.stream() pasa por los hooks before('send'), así que un hook que censura, traduce o aborta respuestas también cubre el texto en streaming. El payload lleva una marca stream:
Un hook que aborta un borrador o una edición se salta esa actualización. El log de mensajes guarda el texto final editado de cada mensaje.

Con @botmesh/ai

El autoresponder de IA transmite su respuesta con la opción stream:
  • stream acepta true o las StreamOptions de arriba (sin signal ni sendOptions), más enabled: false para apagarlo conservando las opciones. Por defecto está apagado (un mensaje al final).
  • El “escribiendo…” sigue a humanizeTyping salvo que fijes typing.
  • Un rechazo responde refusalMessage y no se guarda; una respuesta vacía responde errorMessage, igual que un stream que falla.
  • Con onCancel: 'discard', una respuesta cancelada tampoco entra en el contexto de IA, porque el usuario nunca la vio completa.
  • Con tools configuradas los turnos no se transmiten: el bucle de herramientas necesita la respuesta completa, así que el autoresponder vuelve a un solo mensaje.
En tus propios handlers, ctx.tools.ai.streamReplyInChat() es el gemelo en streaming de replyInChat(): lee y guarda el historial del chat, aplica memoria y RAG y entrega la respuesta por partes.
Detener el stream aborta la petición al proveedor; la respuesta parcial se guarda salvo que pases keepPartial: false (combínalo con onCancel: 'discard'). El timeoutMs del plugin funciona como timeout de inactividad: si no llega ningún fragmento en ese tiempo, lanza AITimeoutError. Con @botmesh/voice y autoReplyWithVoice, la salida en streaming siempre se queda en texto.

Destinos propios

ctx.stream() es streamText() ligado al chat actual. Para otros destinos (un widget web, un test) úsalo directamente con un StreamTarget:
resolveStreamStrategy(target, requested) dice qué estrategia tendría un destino, y findSplitPoint(text, max) dónde se cortaría el texto. Los dos vienen de @botmesh/core/internal, sin garantía semver.

Testing

MockProvider simula cada modo con su opción streaming:
streaming: 'edit' también expone editMessage (se registra en edits) y deleteMessage (deleted); streaming: 'draft' añade sendDraft (se registra en drafts). maxTextLength y maxTextBytes simulan los límites de longitud y supports activa u oculta cada método por separado.

Siguientes pasos