Send Telegram intermediate responses separately

Anmol Malik committed May 28, 2026 at 21:24 UTC 8bc81c7a6a5030f4899f3e4810480915b0950c3c
4 files changed +104 -2
plugins/_telegram_integration/README.md
+1 -1
@@ -35,7 +35,7 @@ This plugin connects one or more Telegram bots to Agent Zero. Each bot runs inde
35 - **Reply delivery**
36 - Streams tool progress and response text through real Telegram messages updated with `editMessageText`.
37 - Tool progress and the AI response are separate messages; only the AI response replies to the user message.
38 - - `tool_execute_after` intercepts the `response` tool — sends inline progress for `break_loop=false`.
38 + - `tool_execute_after` intercepts the `response` tool — sends `break_loop=false` updates as separate intermediate Telegram messages.
39 - `process_chain_end` auto-sends the final response, with retry logic on failure.
40 - **Formatting**
41 - Converts Markdown output to Telegram-compatible HTML (bold, italic, strikethrough, code, links, blockquotes, lists).
plugins/_telegram_integration/extensions/python/tool_execute_after/_50_telegram_response.py
+7 -1
@@ -47,6 +47,7 @@ class TelegramResponseIntercept(Extension):
47 async def _send_inline(self, context, tool, response: Response):
48 ensure_dependencies()
49 from plugins._telegram_integration.helpers.handler import send_telegram_reply
50 + from plugins._telegram_integration.helpers import draft_stream
51
52 agent = self.agent
53 assert agent is not None
@@ -55,7 +56,12 @@ class TelegramResponseIntercept(Extension):
56 attachments = context.data.pop(CTX_TG_ATTACHMENTS, [])
57 keyboard = context.data.pop(CTX_TG_KEYBOARD, None)
58
58 - error = await send_telegram_reply(context, text, attachments or None, keyboard)
59 + if attachments:
60 + error = await send_telegram_reply(context, text, attachments or None, keyboard)
61 + elif await draft_stream.send_intermediate_response(context, text, keyboard):
62 + error = None
63 + else:
64 + error = "Telegram intermediate update was not sent"
65
66 if error:
67 result = agent.read_prompt("fw.telegram.update_error.md", error=error)
plugins/_telegram_integration/helpers/draft_stream.py
+26
@@ -84,6 +84,32 @@ async def update_response(context: AgentContext, response_text: str) -> None:
84 context.data[CTX_TG_RESPONSE_LAST_UPDATE] = now
85
86
87 +async def send_intermediate_response(
88 + context: AgentContext,
89 + response_text: str,
90 + keyboard: list[list[dict]] | None = None,
91 +) -> bool:
92 + html = _format_response(response_text)
93 + if not html:
94 + return False
95 + bot = _bot_instance(context)
96 + chat_id = context.data.get(CTX_TG_CHAT_ID)
97 + if not bot or not chat_id:
98 + return False
99 + try:
100 + sent_id = await tc.raw_send_text(
101 + bot.bot.token,
102 + int(chat_id),
103 + html,
104 + parse_mode="HTML",
105 + reply_markup=_keyboard_markup(keyboard),
106 + )
107 + return bool(sent_id)
108 + except Exception as e:
109 + PrintStyle.debug(f"Telegram intermediate response failed: {e}")
110 + return False
111 +
112 +
113 async def finalize_response(
114 context: AgentContext,
115 response_text: str,
tests/test_telegram_intermediate_response.py new
+70
@@ -0,0 +1,70 @@
1 +import asyncio
2 +
3 +from plugins._telegram_integration.helpers import draft_stream
4 +from plugins._telegram_integration.helpers.constants import (
5 + CTX_TG_BOT,
6 + CTX_TG_CHAT_ID,
7 + CTX_TG_REPLY_TO,
8 + CTX_TG_RESPONSE_MESSAGE_ID,
9 +)
10 +
11 +
12 +class FakeBot:
13 + class Bot:
14 + token = "token"
15 +
16 + bot = Bot()
17 +
18 +
19 +class FakeContext:
20 + def __init__(self):
21 + self.data = {
22 + CTX_TG_BOT: "main",
23 + CTX_TG_CHAT_ID: 123,
24 + CTX_TG_REPLY_TO: 456,
25 + }
26 +
27 + def get_data(self, key):
28 + return self.data.get(key)
29 +
30 +
31 +def test_intermediate_response_sends_separate_non_reply_message(monkeypatch):
32 + calls = []
33 +
34 + async def fake_send(token, chat_id, text, reply_to_message_id=None, parse_mode="HTML", reply_markup=None):
35 + calls.append(
36 + {
37 + "token": token,
38 + "chat_id": chat_id,
39 + "text": text,
40 + "reply_to_message_id": reply_to_message_id,
41 + "parse_mode": parse_mode,
42 + "reply_markup": reply_markup,
43 + }
44 + )
45 + return 789
46 +
47 + context = FakeContext()
48 + monkeypatch.setattr(draft_stream, "_bot_instance", lambda ctx: FakeBot())
49 + monkeypatch.setattr(draft_stream.tc, "raw_send_text", fake_send)
50 +
51 + sent = asyncio.run(
52 + draft_stream.send_intermediate_response(
53 + context,
54 + "**Working** on the brief.",
55 + keyboard=[[{"text": "Open", "callback_data": "open"}]],
56 + )
57 + )
58 +
59 + assert sent is True
60 + assert calls == [
61 + {
62 + "token": "token",
63 + "chat_id": 123,
64 + "text": "<b>Working</b> on the brief.",
65 + "reply_to_message_id": None,
66 + "parse_mode": "HTML",
67 + "reply_markup": {"inline_keyboard": [[{"text": "Open", "callback_data": "open"}]]},
68 + }
69 + ]
70 + assert CTX_TG_RESPONSE_MESSAGE_ID not in context.data