Bot Streaming Replies

Show live draft replies on Telegram/Discord/Slack as your agent thinks

Channel bots can post a quick placeholder message and progressively edit it in place as the agent’s answer streams in β€” replacing the old “stare at a typing indicator for 45s, then a wall of text lands in one burst” experience.

python
from praisonaiagents import Agent

agent = Agent(name="assistant", instructions="You are a helpful assistant.")
agent.start("Explain quantum computing in simple terms.")

The user waits on Telegram, Discord, or Slack; a placeholder message updates in place as the agent streams its answer.

Diagram

Bot Streaming Flow

πŸ“± User Message

πŸ€” Placeholder

✨ Progressive Edits

βœ… Final Message

Quick Start

  1. Enable with one flag (CLI)
    bash
    praisonai bot telegram --token $TELEGRAM_BOT_TOKEN --stream
  2. Enable in YAML
    yaml
    channels:
      telegram:
        token: ${TELEGRAM_BOT_TOKEN}
        streaming: true
        stream_edit_interval_ms: 700
      discord:
        token: ${DISCORD_BOT_TOKEN}
        streaming: true
        stream_edit_interval_ms: 700
      slack:
        token: ${SLACK_BOT_TOKEN}
        app_token: ${SLACK_APP_TOKEN}
        streaming: true
        stream_edit_interval_ms: 700
  3. Enable in Python

    The API is identical across all three bots β€” create an Agent, then pass it to the bot with streaming=True.

    Telegram

    python
    from praisonaiagents import Agent, BotConfig
    from praisonai.bots import TelegramBot
    
    agent = Agent(name="assistant", instructions="Be helpful and concise.")
    
    bot = TelegramBot(
        token="...",
        agent=agent,
        config=BotConfig(
            token="...",
            streaming=True,
            stream_edit_interval_ms=700,
        ),
    )
    bot.start()

    Discord

    python
    from praisonaiagents import Agent, BotConfig
    from praisonai.bots import DiscordBot
    
    agent = Agent(name="assistant", instructions="Be helpful and concise.")
    
    bot = DiscordBot(
        token="...",
        agent=agent,
        config=BotConfig(
            token="...",
            streaming=True,
            stream_edit_interval_ms=700,
        ),
    )
    bot.start()

    Slack

    python
    from praisonaiagents import Agent, BotConfig
    from praisonai.bots import SlackBot
    
    agent = Agent(name="assistant", instructions="Be helpful and concise.")
    
    bot = SlackBot(
        token="...",
        agent=agent,
        config=BotConfig(
            token="...",
            streaming=True,
            stream_edit_interval_ms=700,
        ),
    )
    bot.start()
Diagram

How much control
do you need?

Just enable it

Tune the speed

Custom placeholder
or progress mode

streaming: true
(or –stream)

streaming: true
stream_edit_interval_ms: 1000

StreamingConfig with
mode=PROGRESS, …


How It Works

Diagram
Agentagent.stream_emitterDraftStreamerTelegramBotUserAgentagent.stream_emitterDraftStreamerTelegramBotUserloop[For each token]Send messagestart()send_message(placeholder)add_callback(bridged_handler)astart(prompt, stream=True, cancel_token=…)emit(DELTA_TEXT)bridged_handler(event)edit_message(updated_content)emit(STREAM_END)edit_message(final_response)remove_callback(bridged_handler)
ComponentPurposeResponsibility
DraftStreamerManages live updatesCoalesces edits, respects rate limits
BotAdapterPlatform interfaceSends/edits messages
StreamingConfigUser preferencesTiming, text, mode settings

Works with recipe/YAML-backed agents too

Recipe-backed bots (praisonai serve recipe … or a channel whose capabilities name a recipe:) stream identically to Python-Agent-backed ones β€” DraftStreamer edits, status phrases, and SSE chunks all apply.

bash
praisonai serve recipe my_recipe --stream

Bind a channel to a recipe and streaming is on with no extra flag:

yaml
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    capabilities:
      recipe: my_recipe

Turning on streaming at the gateway is enough β€” the recipe surface participates automatically. See Recipe Serve for the async surface and the recipe-event mapping.


Configuration Options

OptionTypeDefaultDescription
streamingboolFalseEnable progressive streaming β€” bot sends a placeholder then edits it live as the agent produces tokens
stream_edit_interval_msint700Minimum milliseconds between message edits (respects platform rate limits)

Per-channel streaming support

ChannelLive editsDefault edit intervalText limitNotes
TelegramYesplatform default4096 (UTF-16)Replies over the cap auto-chunk: the placeholder is edited with chunk 1, the rest arrive as follow-on messages.
SlackYes1.0s40000Threaded replies (thread_ts set, or reply_in_thread: true) bypass streaming and use the standard thread-aware send path.
DiscordYesplatform default2000Replies over the cap auto-chunk: the placeholder is edited with chunk 1, the rest arrive as follow-on messages (not reference=-tagged β€” the placeholder is already in the conversation).
WhatsAppNo (single message)β€”4096Single message.
EmailNo (single message)β€”unlimitedSingle message.

See Channel Capabilities for the full matrix including reactions and typing.


Advanced configuration (StreamingConfig)

For progress mode, custom placeholder text, or fine-grained min_delta control, use the lower-level StreamingConfig API.

Streaming Modes

Four modes are available to match different use cases:

Diagram

Mode Selection

What’s your priority?

Long answers
Low edit cost

Tool-heavy agents
User wants progress

Rate limits
Shared channels

Mixed channels
Not sure which edit

draft mode

progress mode

off mode

auto mode

ModeBehaviorBest For
offSingle final message after completionDefault behavior, zero impact
draftProgressive edits with growing answerLong responses, platforms like Telegram
progressTool status updates, then final answer. When activity_phrases is configured, the raw tool name is replaced with a curated phrase drawn only from that catalogueTool-heavy agents, user wants progress
autoResolves to draft where the channel can edit, else status text when the channel declares supports_status_text, else off; other config fields are preservedMulti-channel bots where you don’t want to hard-code per-channel modes

Auto mode

auto picks the right mode per channel: it streams (draft) where the channel can edit and falls back to off where it can’t β€” one config, no per-channel branching.

The same StreamingConfig runs on both an editable and a non-editable channel. Telegram streams live; WhatsApp delivers a single final message.

python
from praisonaiagents import Agent, BotConfig
from praisonai.bots import TelegramBot, WhatsAppBot, StreamingConfig, StreamingMode

agent = Agent(name="assistant", instructions="Be helpful and concise.")

# Same StreamingConfig works on both channels β€” Telegram streams live,
# WhatsApp delivers a single final message. No per-channel branching.
cfg = StreamingConfig(mode=StreamingMode.AUTO, min_interval=1.0)

telegram = TelegramBot(token="...", agent=agent, config=BotConfig(token="..."))
telegram.configure_streaming(cfg)

whatsapp = WhatsAppBot(token="...", agent=agent, config=BotConfig(token="..."))
whatsapp.configure_streaming(cfg)

The same YAML block drops onto every channel β€” each resolves on its own capabilities:

yaml
channels:
  telegram:                 # live_edit=True β†’ resolves to draft
    token: ${TELEGRAM_BOT_TOKEN}
    streaming:
      mode: auto
      min_interval: 1.0
  whatsapp:                 # live_edit=False β†’ resolves to off
    token: ${WHATSAPP_BOT_TOKEN}
    streaming:
      mode: auto
      min_interval: 1.0

On Telegram the user sees a placeholder that edits in place; on WhatsApp the same agent replies with one final message. Tail the bot log to confirm: INFO ... 'auto' resolved to 'draft' for Telegram, WARNING ... 'auto' resolved to 'off' for WhatsApp.

Diagram

Channel supports
live edits?

Yes
e.g. Telegram/Slack/Discord

No
e.g. WhatsApp/Email

Mixed / not sure

draft or progress

off

auto
resolves per channel

At init time, auto resolves against the channel’s can_edit capability and logs the outcome:

Diagram

True

False

mode: auto

can_edit?

β†’ draft
INFO: resolved to ‘draft’

β†’ off
WARNING: resolved to ‘off’

Edit-less channels with status text

A channel that can’t edit or react can still show progress if its adapter sets supports_status_text=True and implements set_status_text. The activity line then goes to that status line instead of being dropped.

Diagram

Yes

No

Yes

No

Channel can edit?

draft

supports_status_text
and set_status_text?

progress via set_status_text

off

  • Only auto and progress are routed to the status line. draft still falls back to off on channels that can’t edit.
  • If a status line hasn’t changed, it isn’t sent again, so there is no extra API cost. finalize() sends the answer, then clears the line with an empty string.
  • No built-in adapter turns this on yet. See Textual Status Line to add it to a custom adapter.

StreamingConfig options

OptionTypeDefaultDescription
modeStreamingModeStreamingMode.OFFStreaming mode: OFF, DRAFT, PROGRESS, or AUTO. AUTO uses DRAFT where the channel can edit, else status text when the channel declares supports_status_text, else OFF
min_intervalfloat1.5Minimum seconds between message edits (rate-limit safe)
min_deltaint120Minimum new characters in the buffer before an edit fires
placeholder_textstr"πŸ€” Thinking..."Initial message text sent before any content arrives
progress_prefixstr"πŸ€” "Prefix used in progress mode tool-status updates
progress_stylestr"line"Progress-mode rendering style: "line" (single overwritten tool label, unchanged default) or "feed" (bounded multi-line rolling status feed with per-step ⏳/βœ“/βœ— glyphs)
progress_max_linesint8Max trailing lines shown when progress_style="feed"; older lines scroll off the top of the window
progress_max_line_charsint120Per-line character cap in feed style (includes the leading glyph); words are truncated at a boundary with an ellipsis
disable_progressive_edits_afterint3Consecutive recoverable edit failures before progressive editing is turned off for the rest of the stream β€” the completed answer is then delivered as a single final send
flood_backoff_factorfloat2.0Multiplier applied to the edit interval on each recoverable (flood / 429 / transient) failure
max_intervalfloat30.0Cap in seconds for the adaptively widened edit interval β€” backoff stops growing past this
strip_reasoning_tagsboolTrueStrip <think>…</think> and <reasoning>…</reasoning> spans from streamed and final output. Default ON β€” set to false to opt out
activity_phrasesOptional[Dict[str, str]]NoneOpt-in privacy-safe activity surface for progress mode. When set, the progress line is drawn exclusively from this catalogue (keyed by tool category: web, search, shell, code, file, mcp, default). Tool names, args, command strings, URLs and file paths are never interpolated. Unset (default) preserves today’s Running <tool>... behaviour β€” see Privacy-safe activity surface

Per-bot configure_streaming

configure_streaming has the same shape on every bot β€” pass a StreamingConfig and the override sticks.

Telegram

python
from praisonai.bots import TelegramBot, StreamingConfig, StreamingMode

bot = TelegramBot(token="...", agent=agent)
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.DRAFT,
    min_interval=1.0,
    min_delta=120,
))

Discord

python
from praisonai.bots import DiscordBot, StreamingConfig, StreamingMode

bot = DiscordBot(token="...", agent=agent)
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.DRAFT,
    min_interval=2.0,          # Discord β€” stricter edit rate limits
    min_delta=120,
))

Slack

python
from praisonai.bots import SlackBot, StreamingConfig, StreamingMode

bot = SlackBot(token="...", agent=agent)
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.DRAFT,
    min_interval=1.0,          # Slack chat.update is more generous
    min_delta=120,
))

Long streamed replies auto-chunk (every channel)

Every platform caps message length (Telegram 4096 UTF-16 units, Discord 2000 characters). A streamed answer above the cap can’t be delivered by editing a single placeholder, so the shared DraftStreamer splits it automatically β€” the same way on Telegram, Discord, Slack, and any future adapter.

When the final answer, measured in the adapter’s length_unit, exceeds the channel’s text_limit, finalize():

  1. splits it with chunk_message + enforce_hard_cap (paragraph β†’ newline β†’ sentence boundaries),
  2. edits the placeholder with chunk 1 (falling back to a fresh send_message if the edit fails), and
  3. sends chunks 2..N as follow-on messages through adapter.send_message.

The user sees one coherent reply split across multiple messages, not a truncated or dropped answer. No config needed β€” this is on whenever streaming is enabled.

Diagram

No

Yes

✨ Draft placeholder

Agent produces answer

Longer than text_limit
(in length_unit)?

edit_message(placeholder, answer)

chunk_message + enforce_hard_cap

edit_message(placeholder, chunk 1)
fallback: send_message(chunk 1)

send_message(chunk 2..N)

Follow-on chunks are not wrapped in deliver_with_retry β€” the adapter’s send_message already retries and parks exhausted sends in the DLQ, so wrapping again would re-send a parked chunk and duplicate it. A failing follow-on chunk is logged and the remaining chunks still ship.

Slack: threaded replies skip streaming (by design)

Slack’s shared DraftStreamer posts and edits a placeholder at the channel root, so a reply that must land in a thread bypasses streaming β€” streaming at the root would strand the answer in the main channel.

For a reply destined for a thread β€” either the inbound message was itself in a thread (thread_ts is set) or the bot is configured with reply_in_thread: true β€” the adapter falls back to the existing thread-aware single-message send path (_send_response_with_media), which honours thread_ts and uploads any MEDIA: attachments correctly. A threaded answer is never stranded in the main channel.

Slack: MEDIA: directives still upload audio

A MEDIA:/path/to/reply.mp3 directive still works in streamed Slack replies β€” the text is edited into the placeholder and the audio is uploaded afterwards.

The final content is parsed via split_media_from_output(...), the text portion is edited into the placeholder, and any audio files are uploaded via files_upload_v2(channel=..., file=...). The directive line itself is never written literally into the chat.


Progress feed style

Set progress_style: feed to render tool calls as a bounded rolling multi-line status view instead of a single overwritten label.

A research agent on Telegram chains web_search, fetch_url, and summarize. With the default progress_style="line", each tool overwrites the last, so a failed fetch_url vanishes. With feed, every step keeps its own line and outcome glyph:

python
from praisonaiagents import Agent
from praisonai.bots import TelegramBot, StreamingConfig, StreamingMode
from praisonaiagents import BotConfig

agent = Agent(
    name="research-assistant",
    instructions="Research the user's question with web tools and summarise.",
    tools=[web_search, fetch_url, summarize],
)

bot = TelegramBot(
    token="...",
    agent=agent,
    config=BotConfig(token="...", streaming=True),
)
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.PROGRESS,
    progress_style="feed",     # enable the multi-line rolling feed
    progress_max_lines=6,      # keep it short on mobile
    progress_max_line_chars=100,
))
bot.start()

The user’s Telegram message updates in place with a live per-step audit trail:

βœ“ web_search β€” 5 results
βœ— fetch_url β€” timeout
⏳ summarize

Each tool gets its own line with a state glyph β€” ⏳ running, βœ“ done, βœ— error β€” instead of one label that overwrites itself.

Diagram

progress_style: feed

βœ“ web_search β€” 5 results

βœ— fetch_url β€” timeout

⏳ summarize

progress_style: line

overwrites

overwrites

πŸ€” Running web_search…

πŸ€” Running fetch_url…

πŸ€” Running summarize…

When to use which style

Diagram

Does your agent
chain many tools?

One or two tools
Short runs

Many tools
Users want an audit trail

progress_style: line
(default)

progress_style: feed
per-step glyphs

How one tool flows through the feed

A tool’s start event and its matching finish event share one correlation id, so the line updates in place instead of duplicating.

Diagram
Rendered feedmerge_progress_lineAgentRendered feedmerge_progress_lineAgentTOOL_CALL_START (id=abc, web_search)⏳ web_searchTOOL_CALL_RESULT (id=abc)βœ“ web_search β€” 5 resultsTOOL_CALL_START (id=xyz, fetch_url)βœ“ web_search β€” 5 results⏳ fetch_url

Privacy-safe activity surface

During a long, output-silent tool run in progress mode, show a curated phrase like “Searching the web…” instead of the raw tool name (Running web_fetch...) β€” tool names, args, command strings, URLs and file paths are never surfaced.

Diagram

Privacy-safe activity surface

πŸ”§ Tool runs
web_fetch_secret_url

πŸ—‚οΈ resolve_activity_category
name only β†’ web

πŸ”’ Curated phrase
Searching the web…

πŸ’¬ User sees phrase
not the tool name

Quick Start

  1. Enable in YAML
    yaml
    channels:
      telegram:
        token: ${TELEGRAM_BOT_TOKEN}
        streaming:
          mode: progress
          activity_status:
            enabled: true
            phrases:
              web:     "Searching the web…"
              search:  "Looking that up…"
              shell:   "Running a command…"
              code:    "Writing and running code…"
              file:    "Reading files…"
              mcp:     "Talking to a tool…"
              default: "Working on it…"

    enabled: false disables the surface (yields activity_phrases=None). An inline activity_phrases: {...} shorthand is also accepted in place of the activity_status block.

  2. Enable in Python
    python
    from praisonaiagents import Agent, BotConfig
    from praisonai.bots import TelegramBot, StreamingConfig, StreamingMode
    
    agent = Agent(name="assistant", instructions="Be helpful and concise.")
    
    bot = TelegramBot(token="...", agent=agent, config=BotConfig(token="..."))
    bot.configure_streaming(StreamingConfig(
        mode=StreamingMode.PROGRESS,
        activity_phrases={
            "web":     "Searching the web…",
            "shell":   "Running a command…",
            "default": "Working on it…",
        },
    ))
    bot.start()

Category β†’ phrase mapping

Each tool name is lowercased and matched by substring against these hints β€” the first match wins, and anything unmatched falls to default.

CategorySubstrings that map to it (lowercased tool name)
webweb, browser, fetch, url, http, crawl, scrape
searchsearch, google, duckduckgo, tavily, serper, wiki
shellshell, bash, command, terminal, exec, subprocess
codecode, python, interpreter, repl, compile, script
filefile, read, write, open, save, load, csv, pdf, doc
mcpmcp
defaultanything else (also the fallback when a matched category has no phrase)

resolve_activity_category(None) and resolve_activity_category("") both return "default".

What the user sees

A tool named web_fetch_secret_url maps to web, so the user only ever sees the curated phrase β€” never the tool name.

Diagram

After (activity_status.enabled: true)

Searching the web…

Before (unconfigured)

πŸ€” Running web_fetch_secret_url…

Interaction flow

The tool name never reaches the chat surface when a catalogue is configured β€” the phrase is chosen from the category alone.

Diagram
DraftStreamerTool (silent)AgentBotUserDraftStreamerTool (silent)AgentBotUserSend messageRunCall web_fetch(secret_url)TOOL_CALL_START (name=web_fetch)resolve_activity_category(“web_fetch”) β†’ webedit_message(“Searching the web…”)Searching the web…resultfinalize(answer)edit_message(answer)Final answer

Which progress rendering to choose

Three renderings live on this page β€” pick per channel.

Diagram

Who reads
this channel?

Public / support users

Internal ops team

Just want the default

activity_status
curated phrase
hides tool identity

progress_style: feed
per-step audit trail

progress_style: line
raw tool name (default)

RenderingShowsChoose when
line (default)Raw tool name β€” πŸ€” Running web_search…Internal bot; tool names are safe to show
feedPer-step audit trail with ⏳/βœ“/βœ— glyphsInternal ops bot; you want the full step history
activity_statusCurated phrase only β€” Searching the web…Public/support bot; tool identity must not leak

Precedence with progress_style: feed

When activity_phrases is set, the raw feed is not rendered β€” showing it would fold tool names/summaries into the output and bypass the privacy guarantee. The curated single-line phrase is shown instead. Opting into the catalogue therefore disables the multi-line feed on that channel; choose one per channel.

Partial-catalogue safety

If a phrase for the matched category is missing and no default is configured, the surface falls back to the safe generic line Working on it… β€” never the raw tool name. A catalogue with only {"web": "Searching the web…"} shows Working on it… for a shell tool, not Running bash....

Best practices

Always set a `default` phrase

default covers every unmapped tool with your own wording. Without it, unmatched categories fall back to the safe generic Working on it… β€” correct, but not your voice.

yaml
streaming:
  mode: progress
  activity_status:
    enabled: true
    phrases:
      web:     "Searching the web…"
      default: "Working on it…"   # covers everything else
Keep phrases generic β€” the whole point is not to leak

A phrase is broadcast to every user whose tool falls in that category. Never put a URL, filename, argument, or command name in a phrase β€” that reintroduces exactly the leak the surface prevents. Write "Reading files…", not "Reading /etc/secrets.env…".

Use `activity_status` for public bots; `progress_style: feed` for internal ops

The curated surface hides tool identity β€” right for public and support bots. The feed style exposes a per-step audit trail β€” right for internal ops bots where operators want to see exactly which tool ran. They are mutually exclusive per channel.

Opting in disables the multi-line feed

When a catalogue is set, the SDK renders the curated single line, not the feed. If you need both a curated phrase and a feed, split them across channels/profiles β€” one per channel.


Flood-control

Your Telegram bot is streaming a long answer during peak hours. Telegram starts returning 429 after the third edit. The streamer doubles its edit interval (1.5s β†’ 3s β†’ 6s β†’ capped at max_interval). After disable_progressive_edits_after consecutive failures it stops editing entirely and waits β€” when finalize() fires it delivers the completed answer as a fresh message. The user never sees a stuck placeholder, never sees a partial reply, and the rest of your bot’s chats are unaffected because the backoff is per-stream.

Diagram

Yes

No, recoverable

No

Yes

✏️ Try edit

πŸ” Succeeded?

βœ… Reset fail streak

πŸ“ˆ fail_streak += 1
interval *= backoff_factor
capped at max_interval

πŸ” fail_streak β‰₯
disable_progressive_edits_after?

πŸ”Œ Progressive edits OFF
wait for finalize

πŸ“¨ finalize: edit-then-send fallback
full answer delivered

EventWhat the streamer does
Recoverable edit failure (flood / 429 / transient)fail_streak += 1; current_min_interval *= flood_backoff_factor (capped at max_interval). Per-stream β€” does not mutate the shared StreamingConfig
fail_streak >= disable_progressive_edits_afterProgressive editing turns OFF for the rest of this stream. Subsequent token events skip _perform_update. The placeholder is left in place until finalize()
Non-recoverable edit failureLogged at WARNING. Fail streak does NOT increment; progressive stays enabled
Successful editfail_streak resets to 0 (recovery)
finalize(final_content)Strip reasoning tags (if enabled), chunk if over text_limit, try edit-in-place, fall back to send_message if edit fails, then send any follow-on chunks

Reasoning-tag filtering

Default ON. <think> and <reasoning> spans (case-insensitive, multi-line) are stripped from both streamed and final output. A trailing unclosed opening tag is also dropped so internal reasoning never leaks mid-stream while a block is still being produced. Set strip_reasoning_tags: false to opt out (for example, on a reasoning-transparency bot).

python
from praisonai.bots._streaming import strip_reasoning_tags

strip_reasoning_tags("Hi <think>secret</think> there")        # -> "Hi  there"
strip_reasoning_tags("a <REASONING>x\ny</REASONING> b")       # -> "a  b"
strip_reasoning_tags("Answer <think>still going")             # -> "Answer "

YAML vs Python vs CLI

YAML Configuration

Add to your bot.yaml under the channel configuration:

yaml
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    streaming:
      mode: progress                # off | draft | progress | auto (see Auto mode)
      progress_style: feed          # "line" (default) or "feed"
      progress_max_lines: 8
      progress_max_line_chars: 120
      min_interval: 1.5
      min_delta: 120
      placeholder_text: "πŸ€” Thinking..."
      progress_prefix: "πŸ€” "
      # New in #2356
      disable_progressive_edits_after: 3
      flood_backoff_factor: 2.0
      max_interval: 30.0
      strip_reasoning_tags: true
      # Privacy-safe activity surface (opt-in). Draws the progress line
      # exclusively from these phrases; tool names/args/URLs never leak.
      activity_status:
        enabled: true
        phrases:
          web:     "Searching the web…"
          search:  "Looking that up…"
          shell:   "Running a command…"
          code:    "Writing and running code…"
          file:    "Reading files…"
          mcp:     "Talking to a tool…"
          default: "Working on it…"

Python API

Configure streaming programmatically with the configure_streaming() method:

python
from praisonai.bots import TelegramBot, StreamingConfig, StreamingMode

bot = TelegramBot(token="...", agent=agent)

# Enable with defaults
bot.configure_streaming(StreamingConfig(mode=StreamingMode.DRAFT))

# Custom configuration
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.PROGRESS,
    min_interval=2.0,
    min_delta=200,
    placeholder_text="Working...",
    progress_prefix="⚑ "
))

# Multi-line feed style (per-step ⏳/βœ“/βœ— glyphs)
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.PROGRESS,
    progress_style="feed",
    progress_max_lines=6,
    progress_max_line_chars=100,
))

Manual Streamer Usage

For advanced use cases, you can use DraftStreamer directly:

python
from praisonai.bots import DraftStreamer, StreamingConfig, StreamingMode

streamer = DraftStreamer(
    adapter=bot_adapter,              # implements BotAdapter Protocol
    channel_id="123456",
    config=StreamingConfig(mode=StreamingMode.DRAFT, min_interval=1.5),
    rate_limiter=None,                # optional, defaults from platform
    platform="telegram",              # enables platform-specific recoverable-error classification
)

message_id = await streamer.start()                      # sends placeholder
# ... agent run subscribes streamer.on_event through agent.stream_emitter.add_callback(...) ...
await streamer.finalize(final_response_text)             # final edit with full text

Best Practices

Start with `–stream` β€” tune `stream_edit_interval_ms` only if needed

The default 700ms interval works well on Telegram. If you hit “message is not modified” or 429 errors on a shared/busy channel, raise it to 1000–2000. On Discord (stricter limits), prefer 2000 or use the advanced StreamingConfig form.

Tune min_interval to your platform's edit rate limit

Each platform has different edit rate limits. Telegram allows ~1 edit/sec/chat, while Slack’s chat_update API is more generous. Set min_interval to match your platform’s limits to avoid 429 errors.

yaml
# Telegram (liberal editing)
streaming:
  mode: draft
  min_interval: 1.0

# Discord (stricter limits)
streaming:
  mode: draft
  min_interval: 2.0
Use progress mode for tool-heavy agents

When your agent frequently calls tools (web search, calculations, etc.), progress mode keeps users informed about what’s happening instead of showing a static “thinking” message.

python
# Good for agents that use many tools
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.PROGRESS,
    progress_prefix="πŸ”§ "
))

On a public or support bot, pair progress mode with the Privacy-safe activity surface so users see a curated phrase (Searching the web…) instead of the raw tool name.

Use `feed` when your agent chains many tools

progress_style: feed turns a black-box “still thinking…” indicator into a live per-step audit trail. Every tool call keeps its own line with a ⏳/βœ“/βœ— glyph, so users see which step failed instead of watching one label overwrite itself. See the line-vs-feed comparison.

python
bot.configure_streaming(StreamingConfig(
    mode=StreamingMode.PROGRESS,
    progress_style="feed",
))
Keep `progress_max_lines` small on mobile channels

Telegram mobile crops long messages. Set progress_max_lines to 4–6 so the feed stays readable β€” older lines scroll off the top of the window while the newest steps stay visible.

yaml
streaming:
  mode: progress
  progress_style: feed
  progress_max_lines: 6
Streaming is off by default β€” opt in per channel

The feature has zero impact on existing bots. Only channels with explicit streaming: configuration will use the new behavior. All other channels continue with the original single-message approach.

Final delivery is guaranteed even when edits are flooded

When finalize() runs, the streamer first measures the final answer against the channel’s text_limit in the adapter’s platform_capabilities.length_unit. If it fits, the placeholder is edited in place; if the edit fails (or progressive editing was already disabled by a flood-control strike), a fresh send_message delivers the full answer. If the answer is over the cap, it is split at paragraph/newline/sentence boundaries, the placeholder is edited with chunk 1 (falling back to send_message if the edit fails), and the remaining chunks are posted as follow-on messages. A failing follow-on chunk is logged and the rest still ship. No stale “πŸ€” Thinking…” placeholder is left behind.

Diagram
UserAdapterDraftStreamerUserAdapterDraftStreamerfinalize(final_content)loop[chunks[1:]]alt[within text_limit][over text_limit]strip_reasoning_tags(final_content)measure in length_unitedit_message(placeholder, final_content)βœ… placeholder replaced (send_message fallback on edit failure)chunks = enforce_hard_cap(chunk_message(…))edit_message(placeholder, chunks[0])βœ… chunk 1 of Nsend_message(chunk)βœ… chunk k of N

Troubleshooting

Bot reply got stuck mid-edit on a busy chat?

Fixed in PR #2356. The streamer now backs off on 429/flood, and after 3 consecutive failures falls back to a single final send so you always get the completed answer. Tune disable_progressive_edits_after / flood_backoff_factor / max_interval per channel if your platform’s rate limits differ.

My `progress_style: feed` config was rejected at startup

The schema validator only accepts "line" or "feed". Any other value raises Invalid progress_style '<v>'. Must be one of: feed, line at load time. Check for typos like feeds or multiline.

An errored tool still shows the βœ“ glyph

Impossible by design. Once a line reaches error (βœ—) the compositor never downgrades it to done (βœ“), and a late done event never overwrites an error. If you ever see this, please file a bug against praisonai.

Why is my channel not streaming even though I set `streaming: true`?

That channel probably can’t edit messages. draft/progress on a channel without live_edit support (WhatsApp, Email) degrades to off, now logged at WARNING: Channel <id> doesn't support live editing; degrading streaming mode '<mode>' to 'off'. Switch to mode: auto so the fallback is expected β€” auto resolves to off there without treating it as an error you configured wrong. Channels that can’t edit but declare supports_status_text and implement set_status_text route auto/progress to a textual status line instead of falling back to off.

How do I tell whether `auto` resolved to `draft` or `off`?

Check the bot log for Channel <id> streaming mode 'auto' resolved to '<mode>' (can_edit=<bool>). It’s logged at INFO when it resolves to draft (can_edit=True) and at WARNING when it resolves to off (can_edit=False).

Chat users are seeing tool names like `Running web_fetch…` β€” how do I hide them?

Enable activity_status under the channel’s streaming: block. Once set, the progress line is drawn exclusively from your curated phrases and the raw tool name never reaches the chat. See Privacy-safe activity surface.

I set `activity_status.enabled: true` but the placeholder still shows the tool name

Confirm mode: progress (the catalogue only applies to progress mode), confirm enabled is truthy, and confirm the YAML block reached the config. The schema validator retains activity_status (introduced in PR #5315); a typo above the block that fails validation can prevent it from loading.

My curated phrases work, but the multi-line feed disappeared

Expected. When a catalogue is set, the raw feed is not rendered β€” folding tool names/summaries into the feed would bypass the privacy-safe surface. Choose one per channel: curated activity_status or progress_style: feed.

Discord/Slack ignored `streaming: true` β€” one wall of text landed at the end

Before PR #5417 (merged 2026-10-02) the DraftStreamer was wired only into Telegram β€” Discord and Slack silently fell back to a single final message. Upgrade praisonai/praisonai-bot to pick up the fix. The YAML / Python surface is unchanged (streaming: true or BotConfig(streaming=True)); you just needed the newer package.

My Slack reply in a thread didn't stream

By design. The shared DraftStreamer posts/edits its placeholder at the channel root, so when the reply must land in a thread (thread_ts set, or reply_in_thread: true configured) the adapter falls back to the standard thread-aware send path. Streaming at the channel root would strand the threaded answer in the main channel. If you need streamed thread replies, open a feature request against praisonai.

My long Discord answer arrived as several messages instead of one live edit

Expected. Discord enforces a hard 2000-character cap per message. When a streamed answer exceeds the cap, the shared DraftStreamer edits the placeholder with chunk 1 and posts chunks 2..N as follow-on messages. Editing a single placeholder with >2000 chars would 400 and drop the whole reply. Before PraisonAI PR #6081 this chunking lived in the Discord adapter and deleted the placeholder; the shared streamer now does it the same way on every channel.

My long Telegram answer never arrived

Fixed in PraisonAI PR #6081. Previously a streamed Telegram reply over the 4096 UTF-16 cap failed the final edit and the send fallback, and was lost with only a DraftStreamer final send fallback also failed: ... warning in the log. The shared streamer now chunks over-cap answers (measured in UTF-16 units) with the same splitter the non-streamed path uses. Upgrade praisonai-bot to pick up the fix.


Related