diff --git a/apps/docs/providers/communications/telegram.mdx b/apps/docs/providers/communications/telegram.mdx index 47786f5b3..bcc8246ca 100644 --- a/apps/docs/providers/communications/telegram.mdx +++ b/apps/docs/providers/communications/telegram.mdx @@ -119,12 +119,12 @@ Roomote user. `/new` starts a fresh conversation instead of continuing the current one, opening a new topic when Telegram supports it; in a plain private chat the request joins that chat's conversation. -While a private-chat Fast turn is running, Telegram shows its native -**Thinking** status. Roomote refreshes the temporary draft for long turns and -starts filling that draft with the response when generation takes long enough -to stream. The completed response is always sent as a normal message so it -remains in the conversation. Roomote keeps activity active across intermediate -replies while more work remains, and a final reply clears it naturally. +While a private-chat Fast turn is running, Telegram shows a non-empty +**Roomote is working...** native draft, then replaces it with response text as +generation continues. The completed response is always sent as a normal message +so it remains in the conversation. Roomote keeps activity active across +intermediate replies while more work remains, and a final reply clears it +naturally. Telegram does not support native drafts in group chats, so groups use Telegram's standard typing status and receive completed replies instead. diff --git a/packages/sdk/src/server/lib/fast-agent-telegram-activity.test.ts b/packages/sdk/src/server/lib/fast-agent-telegram-activity.test.ts index 8e7c82527..ee3441feb 100644 --- a/packages/sdk/src/server/lib/fast-agent-telegram-activity.test.ts +++ b/packages/sdk/src/server/lib/fast-agent-telegram-activity.test.ts @@ -10,12 +10,47 @@ describe('Fast Telegram activity', () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); - it('refreshes one native Thinking draft below its TTL in private chats', async () => { + it('shows non-empty Thinking before replacing it with the first partial', async () => { const sendMessageDraft = vi.fn().mockResolvedValue(undefined); + const sendChatAction = vi.fn().mockResolvedValue(undefined); + const activity = createFastAgentTelegramActivity({ + provider: { sendMessageDraft, sendChatAction }, + replyTarget: { channelId: '123', threadId: '77' }, + }); + + activity.start(); + await vi.advanceTimersByTimeAsync(0); + const thinkingDraftId = sendMessageDraft.mock.calls[0]![0].draftId; + expect(thinkingDraftId).not.toBe(0); + expect(sendMessageDraft).toHaveBeenCalledWith({ + channelId: '123', + threadId: '77', + draftId: thinkingDraftId, + text: 'Roomote is working...', + }); + expect(sendChatAction).not.toHaveBeenCalled(); + + const stream = activity.createReplyStream(vi.fn()); + await stream.append('Partial answer'); + expect(sendMessageDraft).toHaveBeenCalledWith( + expect.objectContaining({ + draftId: thinkingDraftId, + text: 'Partial answer', + }), + ); + expect( + sendMessageDraft.mock.calls.some(([input]) => input.text === ''), + ).toBe(false); + await activity.dispose(); + }); + + it('refreshes one non-empty Thinking draft below its TTL in private chats', async () => { + const sendMessageDraft = vi.fn().mockResolvedValue(undefined); + const sendChatAction = vi.fn().mockResolvedValue(undefined); const activity = createFastAgentTelegramActivity({ provider: { sendMessageDraft, - sendChatAction: vi.fn(), + sendChatAction, }, replyTarget: { channelId: '123', threadId: '77' }, }); @@ -28,7 +63,7 @@ describe('Fast Telegram activity', () => { channelId: '123', threadId: '77', draftId: firstDraftId, - text: '', + text: 'Roomote is working...', }); await vi.advanceTimersByTimeAsync(FAST_AGENT_TELEGRAM_DRAFT_REFRESH_MS); @@ -39,10 +74,11 @@ describe('Fast Telegram activity', () => { it('restores Thinking after an intermediate post but cancels it on true completion', async () => { const sendMessageDraft = vi.fn().mockResolvedValue(undefined); + const sendChatAction = vi.fn().mockResolvedValue(undefined); const activity = createFastAgentTelegramActivity({ provider: { sendMessageDraft, - sendChatAction: vi.fn(), + sendChatAction, }, replyTarget: { channelId: '123' }, }); @@ -56,11 +92,15 @@ describe('Fast Telegram activity', () => { expect(sendMessageDraft).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(1); expect(sendMessageDraft).toHaveBeenCalledTimes(2); + expect(sendMessageDraft).toHaveBeenLastCalledWith( + expect.objectContaining({ text: 'Roomote is working...' }), + ); activity.reassert(); await activity.settle(); await vi.advanceTimersByTimeAsync(FAST_AGENT_TELEGRAM_REASSERT_DELAY_MS); expect(sendMessageDraft).toHaveBeenCalledTimes(2); + expect(sendChatAction).not.toHaveBeenCalled(); }); it('writes the first partial immediately, then paces later coalesced drafts before final delivery', async () => { @@ -88,7 +128,7 @@ describe('Fast Telegram activity', () => { sendMessageDraft.mock.calls .filter(([input]) => input.text) .map(([input]) => input.text), - ).toEqual(['Partial ']); + ).toEqual(['Roomote is working...', 'Partial ']); await vi.advanceTimersByTimeAsync( FAST_AGENT_TELEGRAM_STREAM_INTERVAL_MS / 2, ); @@ -99,7 +139,11 @@ describe('Fast Telegram activity', () => { sendMessageDraft.mock.calls .filter(([input]) => input.text) .map(([input]) => input.text), - ).toEqual(['Partial ', 'Partial answer in progress']); + ).toEqual([ + 'Roomote is working...', + 'Partial ', + 'Partial answer in progress', + ]); await expect( stream.finish({ purpose: 'closeout', message: 'Final answer' }), diff --git a/packages/sdk/src/server/lib/fast-agent-telegram-activity.ts b/packages/sdk/src/server/lib/fast-agent-telegram-activity.ts index ca8e240da..d7388c3dd 100644 --- a/packages/sdk/src/server/lib/fast-agent-telegram-activity.ts +++ b/packages/sdk/src/server/lib/fast-agent-telegram-activity.ts @@ -16,8 +16,9 @@ import { createFastAgentTypingActivity } from './fast-agent-typing-activity'; export const FAST_AGENT_TELEGRAM_DRAFT_REFRESH_MS = 25_000; export const FAST_AGENT_TELEGRAM_TYPING_REFRESH_MS = 4_000; export const FAST_AGENT_TELEGRAM_REASSERT_DELAY_MS = 500; -// Telegram allows 40 draft updates per 30 seconds; stay just above its 750ms floor. +// Pace draft updates independently of model token cadence. export const FAST_AGENT_TELEGRAM_STREAM_INTERVAL_MS = 800; +const FAST_AGENT_TELEGRAM_THINKING_TEXT = 'Roomote is working...'; function isTelegramPrivateChatId(channelId: string): boolean { const parsed = Number(channelId); @@ -25,8 +26,8 @@ function isTelegramPrivateChatId(channelId: string): boolean { } /** - * Uses Telegram's native Thinking draft in private chats. Groups do not - * support drafts, so they retain Telegram's ordinary typing action. + * Uses a non-empty Thinking draft in private chats, then replaces it with + * streamed response text. Groups retain Telegram's ordinary typing action. */ export function createFastAgentTelegramActivity({ provider, @@ -56,7 +57,10 @@ export function createFastAgentTelegramActivity({ await provider.sendMessageDraft({ ...replyTarget, draftId: draftId!, - text: draftText.slice(0, TELEGRAM_MAX_MESSAGE_LENGTH), + text: (draftText || FAST_AGENT_TELEGRAM_THINKING_TEXT).slice( + 0, + TELEGRAM_MAX_MESSAGE_LENGTH, + ), }); lastDraftWriteAtMs = Date.now(); return;