Skip to main content
Bot platform adapters now ship in the praisonai-bot package. praisonai bot serve still works exactly as documented here; for a standalone install see praisonai-bot Migration.
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.
The user waits on Telegram, Discord, or Slack; a placeholder message updates in place as the agent streams its answer.
Session-level streaming: true is a shortcut. For unified per-platform defaults and overrides, use Display Policy.
Progressive streaming on Telegram/Discord/Slack required a bug fix in PR #2004 — earlier versions crashed on the first message with TypeError: achat() got an unexpected keyword argument 'stream_callback'. Upgrade to the latest release if you hit that error.

Quick Start

1

Enable with one flag (CLI)

2

Enable in YAML

3

Enable in Python

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

How It Works

Streaming events flow through agent.stream_emitter, not as a stream_callback kwarg to astart(). The bot adds a temporary callback for the duration of the run and removes it on completion (and on timeout/cancel).

Configuration Options

Per-channel streaming support

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:
Live streaming (draft mode) works on Telegram, Slack, and Discord — each adapter honours its own edit_rate_limit and text_limit from Channel Capabilities. WhatsApp and Email can’t edit (live_edit=False): an explicit draft/progress mode degrades to off there, now logged at WARNING so the fallback is visible. Use auto to opt into “stream where supported, otherwise single-message” without raising a warning. The simple streaming: true / --stream form enables draft mode automatically.

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.
The same YAML block drops onto every channel — each resolves on its own capabilities:
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. At init time, auto resolves against the channel’s can_edit capability and logs the outcome:
auto only rewrites mode. All your other settings — min_interval, min_delta, placeholder_text, strip_reasoning_tags, … — are preserved via dataclasses.replace(). Earlier degradation reset the whole config to defaults; that no longer happens.

StreamingConfig options

Per-bot configure_streaming

configure_streaming has the same shape on every bot — pass a StreamingConfig and the override sticks.

Discord: long replies auto-chunk

Discord enforces a 2000-character cap per message — a streamed answer above the cap can’t be delivered by editing a single placeholder, so the adapter switches to the chunked send path automatically. When a streamed answer exceeds min(config.max_message_length, 2000) characters, the adapter:
  1. deletes the placeholder, and
  2. sends the full answer via the existing _send_long_message chunker, preserving the Discord reply reference to the user’s original message.
The user sees one coherent reply split across multiple messages, not a truncated or dropped answer. No config needed — this fallback is on whenever Discord streaming is enabled.

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.
If you want streamed thread replies on Slack, open a feature request against praisonai — it needs a thread-aware streamer, not just an adapter wiring change.

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:
The user’s Telegram message updates in place with a live per-step audit trail:
Each tool gets its own line with a state glyph — ⏳ running, ✓ done, ✗ error — instead of one label that overwrites itself.

When to use which style

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.
The feed style activates only when mode: progress and progress_style: feed. The default progress_style="line" preserves the existing single-line behaviour bit-for-bit — feed is strictly opt-in per channel.

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.

Quick Start

1

Enable in YAML

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

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. 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.

Interaction flow

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

Which progress rendering to choose

Three renderings live on this page — pick per channel.

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

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.
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…".
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.
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.

Streaming Tool Events

Where the tool events that trigger the activity line come from

Bot Status Reactions

The emoji-reaction equivalent for run progress

Outbound Secret Scrub

Related privacy surface that scrubs outbound text

Channel Capabilities

Which channels support the live edits progress needs

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.
Backoff is per-stream — _current_min_interval is mutated on the stream instance, not the shared StreamingConfig. A flood in one chat will never slow streaming in another.

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).
Used to see <think> or <reasoning> spans land in your Telegram/Slack/Discord chat? Fixed in PR #2356. Reasoning-tag filtering is now on by default. Upgrade praisonai.

YAML vs Python vs CLI

YAML Configuration

Add to your bot.yaml under the channel configuration:

Python API

Configure streaming programmatically with the configure_streaming() method:

Manual Streamer Usage

For advanced use cases, you can use DraftStreamer directly:

Best Practices

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.
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.
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.
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.
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.
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.
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.
When finalize() runs, the streamer first tries to edit the placeholder with the full answer. If that edit fails (or progressive editing was already disabled by a flood-control strike), the streamer falls back to a fresh send_message so you always receive the completed answer. No stale ”🤔 Thinking…” placeholder is left behind.

Troubleshooting

Fixed in PR #2356. Reasoning-tag filtering is now on by default. Upgrade praisonai to get the fix automatically.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
Expected. Discord enforces a hard 2000-character cap per message. When a streamed answer exceeds min(config.max_message_length, 2000), the adapter deletes the placeholder and delivers the full answer via _send_long_message, which chunks it across multiple messages while preserving the reply reference. Editing a single placeholder with >2000 chars would 400 and drop the whole reply.

Channel Capabilities

What each platform supports — live edits, reactions, typing

Bot Status Reactions

Show run progress as emoji reactions

Streaming Tool Events

Understand tool-event details used in progress mode

Progress Compositor

Fold typed StreamEvents into your own multi-line status view

Bot Gateway

Set up and run channel bots with gateway configuration

Messaging Bots

Complete guide to messaging bot setup and features

Bot Platform Capabilities

How platform capabilities drive this feature