/** * Telegram reply delivery helpers * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies */ import type { TelegramReplyParameters, TelegramSentMessage } from "./api.ts"; import { renderTelegramMessage, type TelegramRenderedChunk, type TelegramRenderMode, } from "./rendering.ts"; export function buildTelegramReplyParameters( messageId: number | undefined, ): TelegramReplyParameters | undefined { if (messageId === undefined) return undefined; return { message_id: messageId, allow_sending_without_reply: true }; } export function buildTelegramMultipartReplyParameters( messageId: number | undefined, ): string | undefined { const parameters = buildTelegramReplyParameters(messageId); return parameters ? JSON.stringify(parameters) : undefined; } function getAgentMessageField(message: unknown, field: string): unknown { if (typeof message !== "object" || message === null || !(field in message)) { return undefined; } return Reflect.get(message, field); } export function isAssistantAgentMessage(message: unknown): boolean { return getAgentMessageField(message, "role") === "assistant"; } function extractAgentTextContent(content: unknown): string { const blocks = Array.isArray(content) ? content : []; return blocks .filter( (block): block is { type: string; text?: string } => typeof block === "object" && block !== null && "type" in block, ) .filter((block) => block.type === "text" && typeof block.text === "string") .map((block) => block.text as string) .join("") .trim(); } export function getAgentMessageText(message: unknown): string { return extractAgentTextContent(getAgentMessageField(message, "content")); } export function extractLatestAssistantMessageText( messages: readonly unknown[], ): { text?: string; stopReason?: string; errorMessage?: string; } { for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (!message || !isAssistantAgentMessage(message)) continue; const rawStopReason = getAgentMessageField(message, "stopReason"); const rawErrorMessage = getAgentMessageField(message, "errorMessage"); const stopReason = typeof rawStopReason === "string" ? rawStopReason : undefined; const errorMessage = typeof rawErrorMessage === "string" ? rawErrorMessage : undefined; const text = getAgentMessageText(message); return { text: text || undefined, stopReason, errorMessage }; } return {}; } export interface TelegramReplyDeliveryDeps { sendMessage: (body: { chat_id: number; text: string; parse_mode?: "HTML"; reply_markup?: TReplyMarkup; reply_parameters?: TelegramReplyParameters; message_thread_id?: number; }) => Promise; editMessage: (body: { chat_id: number; message_id: number; text: string; parse_mode?: "HTML"; reply_markup?: TReplyMarkup; }) => Promise; } export interface TelegramReplyTransport { sendRenderedChunks: ( chatId: number, chunks: TelegramRenderedChunk[], options?: { replyMarkup?: TReplyMarkup; replyToMessageId?: number; threadId?: number }, ) => Promise; editRenderedMessage: ( chatId: number, messageId: number, chunks: TelegramRenderedChunk[], options?: { replyMarkup?: TReplyMarkup }, ) => Promise; } export function buildTelegramReplyTransport( deps: TelegramReplyDeliveryDeps, ): TelegramReplyTransport { return { sendRenderedChunks: async (chatId, chunks, options) => { return sendTelegramRenderedChunks(chatId, chunks, deps, options); }, editRenderedMessage: async (chatId, messageId, chunks, options) => { return editTelegramRenderedMessage( chatId, messageId, chunks, deps, options, ); }, }; } export async function sendTelegramRenderedChunks( chatId: number, chunks: TelegramRenderedChunk[], deps: TelegramReplyDeliveryDeps, options?: { replyMarkup?: TReplyMarkup; replyToMessageId?: number; threadId?: number }, ): Promise { let lastMessageId: number | undefined; for (const [index, chunk] of chunks.entries()) { const replyParameters = index === 0 ? buildTelegramReplyParameters(options?.replyToMessageId) : undefined; const sent = await deps.sendMessage({ chat_id: chatId, text: chunk.text, parse_mode: chunk.parseMode, reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined, ...(replyParameters ? { reply_parameters: replyParameters } : {}), ...(options?.threadId !== undefined ? { message_thread_id: options.threadId } : {}), }); lastMessageId = sent.message_id; } return lastMessageId; } export async function editTelegramRenderedMessage( chatId: number, messageId: number, chunks: TelegramRenderedChunk[], deps: TelegramReplyDeliveryDeps, options?: { replyMarkup?: TReplyMarkup }, ): Promise { if (chunks.length === 0) return messageId; const [firstChunk, ...remainingChunks] = chunks; await deps.editMessage({ chat_id: chatId, message_id: messageId, text: firstChunk.text, parse_mode: firstChunk.parseMode, reply_markup: remainingChunks.length === 0 ? options?.replyMarkup : undefined, }); if (remainingChunks.length > 0) { return sendTelegramRenderedChunks(chatId, remainingChunks, deps, { replyMarkup: options?.replyMarkup, }); } return messageId; } export interface TelegramReplyRuntimeDeps { renderTelegramMessage: ( text: string, options?: { mode?: TelegramRenderMode }, ) => TelegramRenderedChunk[]; sendRenderedChunks: ( chunks: TelegramRenderedChunk[], options?: { replyMarkup?: TReplyMarkup }, ) => Promise; } export async function sendTelegramPlainReply( text: string, deps: TelegramReplyRuntimeDeps, options?: { parseMode?: "HTML" }, ): Promise { const chunks = deps.renderTelegramMessage(text, { mode: options?.parseMode === "HTML" ? "html" : "plain", }); return deps.sendRenderedChunks(chunks); } export async function sendTelegramMarkdownReply( markdown: string, deps: TelegramReplyRuntimeDeps, options?: { replyMarkup?: TReplyMarkup }, ): Promise { const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" }); if (chunks.length === 0) { return sendTelegramPlainReply(markdown, deps); } return deps.sendRenderedChunks(chunks, options); } export interface TelegramRenderedMessageRuntimeDeps { renderTelegramMessage: ( text: string, options?: { mode?: TelegramRenderMode }, ) => TelegramRenderedChunk[]; replyTransport: TelegramReplyTransport; } export interface TelegramRenderedMessageRuntime { sendTextReply: ( chatId: number, replyToMessageId: number, text: string, options?: { parseMode?: "HTML"; threadId?: number }, ) => Promise; sendMarkdownReply: ( chatId: number, replyToMessageId: number, markdown: string, options?: { replyMarkup?: unknown; threadId?: number }, ) => Promise; editInteractiveMessage: ( chatId: number, messageId: number, text: string, mode: TelegramRenderMode, replyMarkup: TReplyMarkup, ) => Promise; sendInteractiveMessage: ( chatId: number, text: string, mode: TelegramRenderMode, replyMarkup: TReplyMarkup, options?: { threadId?: number; replyToMessageId?: number }, ) => Promise; } export interface TelegramRenderedMessageDeliveryRuntime< TReplyMarkup, > extends TelegramRenderedMessageRuntime { replyTransport: TelegramReplyTransport; } export interface TelegramRenderedMessageDeliveryRuntimeDeps< TReplyMarkup, > extends TelegramReplyDeliveryDeps { renderTelegramMessage?: ( text: string, options?: { mode?: TelegramRenderMode }, ) => TelegramRenderedChunk[]; } export function createTelegramRenderedMessageDeliveryRuntime( deps: TelegramRenderedMessageDeliveryRuntimeDeps, ): TelegramRenderedMessageDeliveryRuntime { const replyTransport = buildTelegramReplyTransport({ sendMessage: deps.sendMessage, editMessage: deps.editMessage, }); return { replyTransport, ...createTelegramRenderedMessageRuntime({ renderTelegramMessage: deps.renderTelegramMessage ?? renderTelegramMessage, replyTransport, }), }; } export function createTelegramRenderedMessageRuntime( deps: TelegramRenderedMessageRuntimeDeps, ): TelegramRenderedMessageRuntime { return { sendTextReply: async (chatId, replyToMessageId, text, options) => { return sendTelegramPlainReply( text, { renderTelegramMessage: deps.renderTelegramMessage, sendRenderedChunks: (chunks) => deps.replyTransport.sendRenderedChunks(chatId, chunks, { replyToMessageId, threadId: options?.threadId, }), }, options, ); }, sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => { return sendTelegramMarkdownReply( markdown, { renderTelegramMessage: deps.renderTelegramMessage, sendRenderedChunks: (chunks, chunkOptions) => deps.replyTransport.sendRenderedChunks(chatId, chunks, { replyToMessageId, replyMarkup: chunkOptions?.replyMarkup as | TReplyMarkup | undefined, threadId: options?.threadId, }), }, options, ); }, editInteractiveMessage: async ( chatId, messageId, text, mode, replyMarkup, ) => { await deps.replyTransport.editRenderedMessage( chatId, messageId, deps.renderTelegramMessage(text, { mode }), { replyMarkup }, ); }, sendInteractiveMessage: async (chatId, text, mode, replyMarkup, options) => { return deps.replyTransport.sendRenderedChunks( chatId, deps.renderTelegramMessage(text, { mode }), { replyMarkup, threadId: options?.threadId, replyToMessageId: options?.replyToMessageId, }, ); }, }; }