feat(channel): stream LLM responses to Telegram via draft message edits

Wire the existing provider-layer streaming infrastructure through the
channel trait and agent loop so Telegram users see tokens arrive
progressively via editMessageText, instead of waiting for the full
response.

Changes:
- Add StreamMode enum (off/partial/block) and draft_update_interval_ms
  to TelegramConfig (backward-compatible defaults: off, 1000ms)
- Add supports_draft_updates/send_draft/update_draft/finalize_draft to
  Channel trait with no-op defaults (zero impact on existing channels)
- Implement draft methods on TelegramChannel using sendMessage +
  editMessageText with rate limiting and Markdown fallback
- Add on_delta mpsc::Sender<String> parameter to run_tool_call_loop
  (None preserves existing behavior)
- Wire streaming in process_channel_message: when channel supports
  drafts, send initial draft, spawn updater task, finalize on completion

Edge cases handled:
- 4096-char limit: finalize draft and fall back to chunked send
- Broken Markdown: use no parse_mode during streaming, apply on finalize
- Edit failures: fall back to sending complete response as new message
- Rate limiting: configurable draft_update_interval_ms (default 1s)
This commit is contained in:
Xiangjun Ma 2026-02-17 23:46:32 -08:00 committed by Chummy
parent a0b277b21e
commit 118cd53922
12 changed files with 410 additions and 43 deletions

View file

@ -725,7 +725,7 @@ pub fn all_integrations() -> Vec<IntegrationEntry> {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::schema::{IMessageConfig, MatrixConfig, TelegramConfig};
use crate::config::schema::{IMessageConfig, MatrixConfig, StreamMode, TelegramConfig};
use crate::config::Config;
#[test]
@ -788,6 +788,8 @@ mod tests {
config.channels_config.telegram = Some(TelegramConfig {
bot_token: "123:ABC".into(),
allowed_users: vec!["user".into()],
stream_mode: StreamMode::default(),
draft_update_interval_ms: 1000,
});
let entries = all_integrations();
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();