Summary
Every channel bot (Telegram, Discord, Slack, WhatsApp, Email, AgentMail) delivers agent responses as a single blocking message: the user stares at a typing indicator for the full duration of the LLM turn, then receives the whole answer at once. The core SDK already emits token-level stream events, and every bot adapter already implements edit_message() β but nothing bridges the two. The only consumer of stream events today is the WebSocket gateway relay for browser clients. For a gateway/bot product that aims to be best-in-class on chat platforms, live progressive responses are the single most visible quality feature missing.
Current behaviour
- Core emits stream events per agent:
praisonaiagents/streaming/events.py β StreamEventEmitter with DELTA_TEXT, DELTA_TOOL_CALL, FIRST_TOKEN, STREAM_END; exposed via the lazy Agent.stream_emitter property (praisonaiagents/agent/agent.py:285-294).
- The gateway relays these deltas only to WebSocket clients:
_make_stream_relay() in src/praisonai/praisonai/gateway/server.py:925-975 maps DELTA_TEXT β EventType.TOKEN_STREAM.
- The channel path is fully blocking:
BotSessionManager.chat() (src/praisonai/praisonai/bots/_session.py:166-279) swaps history, calls agent.chat(prompt) in an executor, and returns only the final string, which the bot then sends.
- Every adapter already has the primitive needed for progressive delivery but it is never used for streaming:
src/praisonai/praisonai/bots/telegram.py:591 β edit_message()
src/praisonai/praisonai/bots/discord.py:353, slack.py:515, whatsapp.py:913, email.py:520, agentmail.py:753
grep -rn "stream" src/praisonai/praisonai/bots/ returns no hits in the session/delivery path; praisonaiagents/bots/config.py (BotConfig) has no streaming option.
- The typing-indicator renewal loop in
gateway/server.py:1928-1937 exists precisely because long turns currently show nothing β treating the symptom, not the cause.
Desired behaviour
A bot-layer stream consumer that subscribes to the agent's stream_emitter during a channel turn and progressively edits a single placeholder message as tokens arrive, with:
- A configurable minimum edit interval (e.g. 700Β ms default) so platform rate limits are respected, integrating with the existing
RateLimiter (bots/_rate_limit.py).
- Adaptive backoff when the platform signals flood control (reuse
_resilience.is_recoverable_error).
- Markdown/code-fence-safe partial rendering (reuse
bots/_chunk.py fence logic so a half-open code block never renders broken).
- Overflow handling: when the streamed text exceeds the platform message limit, finalise the current message and continue in a new one.
- Graceful fallback: platforms whose adapter lacks reliable edits (email) keep today's blocking behaviour automatically.
- Off by default (current behaviour preserved), opt-in per bot/channel.
Layer placement
- Primary layer: wrapper (
src/praisonai/praisonai/bots/)
- Why not core: core already provides the right contract (
StreamEventEmitter, GatewayProtocol TOKEN_STREAM events); platform delivery mechanics (edit cadence, flood control, per-platform limits) belong with the adapters. Only a small BotConfig field is a core touch.
- Why not wrapper-only without core touch: the
streaming toggle should live in praisonaiagents/bots/config.py so BotConfig stays the single config surface.
- Why not tools: this is framework-driven delivery of agent output, not an agent-callable capability.
- Why not plugins: it is per-platform transport logic tied to bot adapters, not a cross-cutting lifecycle hook.
- Secondary touch (optional): core β add
streaming: bool = False and stream_edit_interval_ms: int to BotConfig.
- 3-way surface (CLI + YAML + Python): yes β
praisonai bot start --stream, channels.<name>.streaming: true in gateway.yaml, BotConfig(streaming=True).
Proposed approach
- Extension point: config (
BotConfig.streaming) + a new bots/_stream.py consumer wired inside BotSessionManager.chat().
- Minimal API sketch:
# bots/_stream.py
class ChannelStreamConsumer:
"""Bridges Agent.stream_emitter deltas to progressive adapter edits."""
def __init__(self, adapter, channel_id, *, edit_interval_ms=700): ...
async def start(self, placeholder_msg_id: str): ...
def on_delta(self, event): ... # sync callback from agent worker thread β queue
async def finalize(self, final_text: str): ...
Resolution sketch
# Before (today): user waits 40s staring at "typingβ¦", then one wall of text
channels:
telegram:
platform: telegram
token: "${TELEGRAM_BOT_TOKEN}"
# After (proposed): tokens appear live in an edited message
channels:
telegram:
platform: telegram
token: "${TELEGRAM_BOT_TOKEN}"
streaming: true
stream_edit_interval_ms: 700
# Python, after
from praisonai.bots import Bot
Bot(platform="telegram", agent=agent, streaming=True).start()
Severity
High β the most user-visible quality gap on every supported chat platform; all the building blocks (stream events, edit_message, rate limiter, chunker) already exist but are unconnected.
Validation
- Read
src/praisonai/praisonai/bots/_session.py (blocking chat() flow, lines 166β279); confirmed no streaming references in bots/ delivery path via grep.
- Confirmed
edit_message() exists on all six adapters (paths above) and is only used for command UI (e.g. pairing buttons, telegram.py:353).
- Confirmed core
StreamEventEmitter (praisonaiagents/streaming/events.py) and the WebSocket-only relay (gateway/server.py:925-975).
- Confirmed
BotConfig (praisonaiagents/bots/config.py) has no streaming field.
Summary
Every channel bot (Telegram, Discord, Slack, WhatsApp, Email, AgentMail) delivers agent responses as a single blocking message: the user stares at a typing indicator for the full duration of the LLM turn, then receives the whole answer at once. The core SDK already emits token-level stream events, and every bot adapter already implements
edit_message()β but nothing bridges the two. The only consumer of stream events today is the WebSocket gateway relay for browser clients. For a gateway/bot product that aims to be best-in-class on chat platforms, live progressive responses are the single most visible quality feature missing.Current behaviour
praisonaiagents/streaming/events.pyβStreamEventEmitterwithDELTA_TEXT,DELTA_TOOL_CALL,FIRST_TOKEN,STREAM_END; exposed via the lazyAgent.stream_emitterproperty (praisonaiagents/agent/agent.py:285-294)._make_stream_relay()insrc/praisonai/praisonai/gateway/server.py:925-975mapsDELTA_TEXT β EventType.TOKEN_STREAM.BotSessionManager.chat()(src/praisonai/praisonai/bots/_session.py:166-279) swaps history, callsagent.chat(prompt)in an executor, and returns only the final string, which the bot then sends.src/praisonai/praisonai/bots/telegram.py:591βedit_message()src/praisonai/praisonai/bots/discord.py:353,slack.py:515,whatsapp.py:913,email.py:520,agentmail.py:753grep -rn "stream" src/praisonai/praisonai/bots/returns no hits in the session/delivery path;praisonaiagents/bots/config.py(BotConfig) has no streaming option.gateway/server.py:1928-1937exists precisely because long turns currently show nothing β treating the symptom, not the cause.Desired behaviour
A bot-layer stream consumer that subscribes to the agent's
stream_emitterduring a channel turn and progressively edits a single placeholder message as tokens arrive, with:RateLimiter(bots/_rate_limit.py)._resilience.is_recoverable_error).bots/_chunk.pyfence logic so a half-open code block never renders broken).Layer placement
src/praisonai/praisonai/bots/)StreamEventEmitter,GatewayProtocolTOKEN_STREAM events); platform delivery mechanics (edit cadence, flood control, per-platform limits) belong with the adapters. Only a smallBotConfigfield is a core touch.streamingtoggle should live inpraisonaiagents/bots/config.pysoBotConfigstays the single config surface.streaming: bool = Falseandstream_edit_interval_ms: inttoBotConfig.praisonai bot start --stream,channels.<name>.streaming: truein gateway.yaml,BotConfig(streaming=True).Proposed approach
BotConfig.streaming) + a newbots/_stream.pyconsumer wired insideBotSessionManager.chat().Resolution sketch
Severity
High β the most user-visible quality gap on every supported chat platform; all the building blocks (stream events, edit_message, rate limiter, chunker) already exist but are unconnected.
Validation
src/praisonai/praisonai/bots/_session.py(blockingchat()flow, lines 166β279); confirmed no streaming references inbots/delivery path via grep.edit_message()exists on all six adapters (paths above) and is only used for command UI (e.g. pairing buttons,telegram.py:353).StreamEventEmitter(praisonaiagents/streaming/events.py) and the WebSocket-only relay (gateway/server.py:925-975).BotConfig(praisonaiagents/bots/config.py) has no streaming field.