Transmisión
El streaming está basado en capacidades. Los proveedores que implementan los métodos de streaming y devuelven supports_streaming() == true pueden emitir deltas de tokens; otros proveedores utilizan la ruta de respuesta sin streaming. El runtime reenvía los streams disponibles a los adaptadores de canal que admiten actualizaciones parciales.
¿Qué se transmite?
El trait de proveedor emite valores StreamEvent a medida que el modelo genera salida: deltas de texto, llamadas a herramientas estructuradas, llamadas a herramientas preejecutadas del lado del proveedor y sus resultados, informes de uso de tokens y un marcador final de finalización. Las definiciones autoritativas por variante se encuentran junto al tipo en crates/zeroclaw-api/src/model_provider.rs (enum StreamEvent); los tokens de razonamiento llegan como deltas de texto, no como una variante separada.
El runtime consume estos eventos. El orquestador de canales utiliza los métodos de entrega de borradores y los indicadores de capacidad del trait Channel para mostrar salida progresiva donde esté disponible.
Banderas de capacidad
Un proveedor expone dos indicadores para que el tiempo de ejecución sepa qué puede esperar:
#![allow(unused)]
fn main() {
fn supports_streaming(&self) -> bool { false }
fn supports_streaming_tool_events(&self) -> bool { false }
}
supports_streaming: verdadero solo cuando el proveedor concreto habilita el streaming; el valor predeterminado del trait es falsesupports_streaming_tool_events: true cuando el proveedor emite eventosToolCalldurante el stream en lugar de al final
Los proveedores compatibles con OpenAI difieren: algunos transmiten los deltas de los argumentos de la llamada a la herramienta por fragmentos, mientras que otros solo emiten la llamada una vez que está completa. El analizador SSE de compatible.rs maneja ambos casos.
Transmisión por el lado del canal
Los canales anuncian sus propias capacidades de streaming a través del trait Channel:
#![allow(unused)]
fn main() {
fn supports_draft_updates(&self) -> bool; // editar un mensaje en su lugar
fn supports_multi_message_streaming(&self) -> bool; // dividir una respuesta en varios mensajes
}
La capacidad de un canal se deriva de su configuración: un canal con el enum stream_mode (off / partial / multi_message) admite tanto actualizaciones de borradores como multi-mensaje; un canal con el booleano stream_drafts admite solo actualizaciones de borradores. Esta tabla se genera a partir del esquema de configuración del canal, por lo que se mantiene correcta a medida que los canales ganan o pierden soporte de streaming:
| Canal | Actualizaciones de borrador | Mensajes múltiples |
|---|---|---|
discord | ✓ | ✓ |
lark | ✓ | ✓ |
matrix | ✓ | ✓ |
nextcloud_talk | ✓ | ✓ |
slack | ✓ | |
telegram | ✓ | ✓ |
wecom_ws | ✓ | ✓ |
Cuando tanto el proveedor como el canal admiten streaming, el flujo es: el proveedor emite TextDelta → el runtime lo pasa al canal → el canal edita el mensaje enviado. La cadencia de edición está limitada por la configuración draft_update_interval_ms de ese canal para evitar la limitación de velocidad; los valores predeterminados varían según el canal.
El modo de múltiples mensajes difiere según el canal: Matrix y Discord dividen en los límites de párrafo \n\n. Telegram multi_message envía un mensaje por cada turno de texto completado del agente (texto entre llamadas a herramientas), que se vacía mediante Channel::flush_draft_turn cuando termina el turno del LLM.
La política de salida es por turno. En el modo multi_message, cada turno de narración completado pasa por la política de salida (el hook on_message_sending y la redacción de fugas) exactamente una vez, con alcance limitado a ese turno y no a toda la transcripción acumulada. Así, un hook con estado no vuelve a procesar un turno anterior cuando se añade uno posterior. Como consecuencia, la garantía de que “la narración se entrega antes de una solicitud de aprobación” se cumple por cada turno entregado. Un secreto que atraviesa el límite entre dos turnos sigue detectándose antes de la respuesta final: cada turno de narración que se completa se analiza con un sufijo acotado del historial entregado protegido como contexto entre turnos (el historial que el canal recibió realmente, de modo que un fragmento creado por el propio hook de salida queda incluido y una redacción previa no se vuelve a marcar). Así, el fragmento que completa la división se redacta cuando ese turno pasa por la política de salida, y no se aplaza hasta la respuesta final totalmente redactada.
Orden en modo parcial (edición en el lugar). Una actualización de Status de herramienta se ordena después de cualquier vaciado pendiente de la narración. En los canales que editan un único mensaje en el lugar (Matrix y similares), la narración visible puede por tanto ser reemplazada por el estado de la herramienta hasta la siguiente actualización. La respuesta final completada siempre se entrega íntegra; solo la visualización intermedia en el lugar puede mostrar el estado de la herramienta en lugar de la narración anterior.
Bloques de razonamiento
StreamEvent no tiene una variante ReasoningDelta separada. Cuando un proveedor expone razonamiento durante el streaming, utiliza el campo reasoning en el StreamChunk transportado por TextDelta; la configuración del proveedor y del runtime determina si ese contenido se solicita o se expone. Los consumidores deben seguir el contrato de StreamChunk en lugar de hacer coincidir una variante de evento inexistente.
Llamadas de herramientas en medio del flujo
Cuando un proveedor de streaming decide llamar a una herramienta, emite un evento de streaming estructurado ToolCall. El entorno de ejecución:
- Lee el flujo hasta completarlo, recopila eventos estructurados de
ToolCally reenvía el texto visible hastaFinal - Recupera las llamadas a herramientas después de que finaliza el flujo
- Ejecuta las herramientas (sujeto a la validación de seguridad; consulta Seguridad → Descripción general)
- Abre una nueva llamada de streaming al proveedor para el siguiente turno del asistente, con los resultados de las herramientas añadidos a la conversación
El flujo actual del proveedor nunca se pausa ni se reanuda a mitad de una lectura; la ejecución de herramientas tiene lugar después de que ese flujo alcanza Final, y el turno siguiente es una nueva llamada de streaming.
Desde la perspectiva del usuario: texto, luego un indicador visible de que el agente ejecutó una herramienta (mediante pistas específicas del canal), y después más texto. Para los canales sin indicadores de escritura, la brecha entre la llamada a la herramienta y el siguiente fragmento de texto es la única señal.
Finalización del transporte y tiempos de espera
Los transportes de streaming no dependen del cierre de la conexión como señal de éxito. Los flujos compatibles con OpenAI finalizan con [DONE], los flujos de OpenAI Responses finalizan con su evento de respuesta terminal y los flujos de Anthropic finalizan con message_stop. Los servidores pueden mantener abierta la conexión HTTP después de esos eventos.
Los clientes de streaming usan tiempos de espera por inactividad de bytes: 300 segundos para OpenAI Responses y los proveedores compatibles con OpenAI, y 90 segundos para Anthropic. En OpenAI Responses y en los proveedores compatibles con OpenAI, definir timeout_secs por encima de 300 en el proveedor amplía el límite de inactividad para igualarlo; nunca se reduce por debajo del valor predeterminado. Cada lectura del cuerpo recibida restablece el tiempo de espera correspondiente, de modo que las generaciones activas no están limitadas por el tiempo de espera de la solicitud completa que se usa en las llamadas sin streaming. La configuración de la conexión, las cabeceras de respuesta y los cuerpos de error almacenados en búfer siguen teniendo un límite de tiempo.
Proveedores no basados en streaming
Cuando supports_streaming() es false, los llamadores utilizan la ruta de chat no-streaming del proveedor. Los adaptadores de canal aún pueden enviar la respuesta completada, pero no reciben eventos de stream incrementales del proveedor.
Referencias de código
crates/zeroclaw-api/src/model_provider.rs: traitModelProvider, enumStreamEventcrates/zeroclaw-providers/src/compatible.rs: analizador SSE compatible con OpenAIcrates/zeroclaw-providers/src/anthropic.rs: streaming de Anthropiccrates/zeroclaw-providers/src/ollama.rs: Streaming de Ollamacrates/zeroclaw-channels/src/orchestrator/mod.rs: consumo de streams del lado del canal