@@ -93,9 +93,11 @@ import { sendTtsResponseForSession } from "./utils/send-tts-response.js";
9393import { deliverThinkingMessage } from "./utils/thinking-message.js" ;
9494import { shouldSuppressUserAbortSessionError } from "./utils/abort-error-suppression.js" ;
9595import {
96+ completeDraftPart ,
9697 editRenderedBotPart ,
9798 getTelegramRenderedPartSignature ,
9899 sendBotText ,
100+ sendDraftBotPart ,
99101 sendRenderedBotPart ,
100102} from "./utils/telegram-text.js" ;
101103import { formatAssistantRunFooter } from "./utils/assistant-run-footer.js" ;
@@ -134,6 +136,7 @@ let unsubscribeReadyRestore: (() => void) | null = null;
134136
135137const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024 ;
136138const RESPONSE_STREAM_THROTTLE_MS = config . bot . responseStreamThrottleMs ;
139+ const RESPONSE_STREAMING_MODE = config . bot . responseStreamingMode ;
137140const RESPONSE_STREAM_TEXT_LIMIT = 3800 ;
138141const SESSION_RETRY_PREFIX = "🔁" ;
139142const SUBAGENT_STREAM_PREFIX = "🧩" ;
@@ -237,64 +240,116 @@ const toolMessageBatcher = new ToolMessageBatcher({
237240 } ,
238241} ) ;
239242
240- const responseStreamer = new ResponseStreamer ( {
241- throttleMs : RESPONSE_STREAM_THROTTLE_MS ,
242- sendPart : async ( part , options ) => {
243- if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
244- throw new Error ( "Bot context missing for streamed send" ) ;
245- }
243+ let nextDraftId = 1 ;
244+ function getNextDraftId ( ) : number {
245+ const id = nextDraftId ;
246+ nextDraftId += 1 ;
247+ return id ;
248+ }
246249
247- return sendRenderedBotPart ( {
248- api : botInstance . api ,
249- chatId : chatIdInstance ,
250- part,
251- options,
252- } ) ;
253- } ,
254- editPart : async ( messageId , part , options ) => {
255- if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
256- throw new Error ( "Bot context missing for streamed edit" ) ;
257- }
250+ const responseStreamer = RESPONSE_STREAMING_MODE === "draft"
251+ ? new ResponseStreamer ( {
252+ throttleMs : RESPONSE_STREAM_THROTTLE_MS ,
253+ sendPart : async ( part ) => {
254+ if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
255+ throw new Error ( "Bot context missing for draft send" ) ;
256+ }
258257
259- try {
260- return await editRenderedBotPart ( {
261- api : botInstance . api ,
262- chatId : chatIdInstance ,
263- messageId,
264- part,
265- options,
266- } ) ;
267- } catch ( error ) {
268- const errorMessage =
269- error instanceof Error ? error . message . toLowerCase ( ) : String ( error ) . toLowerCase ( ) ;
270- if ( errorMessage . includes ( "message is not modified" ) ) {
271- return {
272- deliveredSignature : getTelegramRenderedPartSignature ( part ) ,
273- } ;
274- }
258+ const draftId = getNextDraftId ( ) ;
259+ const result = await sendDraftBotPart ( {
260+ api : botInstance . api ,
261+ chatId : chatIdInstance ,
262+ draftId,
263+ part,
264+ } ) ;
265+ return { messageId : draftId , deliveredSignature : result . deliveredSignature } ;
266+ } ,
267+ editPart : async ( messageId , part ) => {
268+ if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
269+ throw new Error ( "Bot context missing for draft edit" ) ;
270+ }
275271
276- throw error ;
277- }
278- } ,
279- deleteText : async ( messageId ) => {
280- if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
281- throw new Error ( "Bot context missing for streamed delete" ) ;
282- }
272+ return sendDraftBotPart ( {
273+ api : botInstance . api ,
274+ chatId : chatIdInstance ,
275+ draftId : messageId ,
276+ part,
277+ } ) ;
278+ } ,
279+ deleteText : async ( ) => {
280+ // Drafts are ephemeral — no need to delete
281+ } ,
282+ completePart : async ( part , options ) => {
283+ if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
284+ throw new Error ( "Bot context missing for draft complete" ) ;
285+ }
283286
284- await botInstance . api . deleteMessage ( chatIdInstance , messageId ) . catch ( ( error ) => {
285- const errorMessage =
286- error instanceof Error ? error . message . toLowerCase ( ) : String ( error ) . toLowerCase ( ) ;
287- if (
288- errorMessage . includes ( "message to delete not found" ) ||
289- errorMessage . includes ( "message identifier is not specified" )
290- ) {
291- return ;
292- }
287+ return completeDraftPart ( {
288+ api : botInstance . api ,
289+ chatId : chatIdInstance ,
290+ part,
291+ options,
292+ } ) ;
293+ } ,
294+ } )
295+ : new ResponseStreamer ( {
296+ throttleMs : RESPONSE_STREAM_THROTTLE_MS ,
297+ sendPart : async ( part , options ) => {
298+ if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
299+ throw new Error ( "Bot context missing for streamed send" ) ;
300+ }
293301
294- throw error ;
302+ return sendRenderedBotPart ( {
303+ api : botInstance . api ,
304+ chatId : chatIdInstance ,
305+ part,
306+ options,
307+ } ) ;
308+ } ,
309+ editPart : async ( messageId , part , options ) => {
310+ if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
311+ throw new Error ( "Bot context missing for streamed edit" ) ;
312+ }
313+
314+ try {
315+ return await editRenderedBotPart ( {
316+ api : botInstance . api ,
317+ chatId : chatIdInstance ,
318+ messageId,
319+ part,
320+ options,
321+ } ) ;
322+ } catch ( error ) {
323+ const errorMessage =
324+ error instanceof Error ? error . message . toLowerCase ( ) : String ( error ) . toLowerCase ( ) ;
325+ if ( errorMessage . includes ( "message is not modified" ) ) {
326+ return {
327+ deliveredSignature : getTelegramRenderedPartSignature ( part ) ,
328+ } ;
329+ }
330+
331+ throw error ;
332+ }
333+ } ,
334+ deleteText : async ( messageId ) => {
335+ if ( ! botInstance || ! chatIdInstance || chatIdInstance <= 0 ) {
336+ throw new Error ( "Bot context missing for streamed delete" ) ;
337+ }
338+
339+ await botInstance . api . deleteMessage ( chatIdInstance , messageId ) . catch ( ( error ) => {
340+ const errorMessage =
341+ error instanceof Error ? error . message . toLowerCase ( ) : String ( error ) . toLowerCase ( ) ;
342+ if (
343+ errorMessage . includes ( "message to delete not found" ) ||
344+ errorMessage . includes ( "message identifier is not specified" )
345+ ) {
346+ return ;
347+ }
348+
349+ throw error ;
350+ } ) ;
351+ } ,
295352 } ) ;
296- } ,
297- } ) ;
298353
299354setResponseStreamerForReconciliation ( responseStreamer ) ;
300355
0 commit comments