@llblab/pi-telegram 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/preview.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  type TelegramRenderMode,
21
21
  } from "./rendering.ts";
22
22
  import { buildTelegramReplyParameters } from "./replies.ts";
23
+ import { stripTelegramCommentMarkupForPreview } from "./outbound-handlers.ts";
23
24
 
24
25
  const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
25
26
  const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
@@ -43,6 +44,7 @@ export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
43
44
  }
44
45
 
45
46
  export type TelegramSentPreviewMessage = TelegramSentMessage;
47
+ export type TelegramPreviewReplyMarkup = any;
46
48
 
47
49
  export interface TelegramPreviewRuntimeDeps {
48
50
  getState: () => TelegramPreviewRuntimeState | undefined;
@@ -76,11 +78,13 @@ export interface TelegramPreviewRuntimeDeps {
76
78
  sendRenderedChunks: (
77
79
  chatId: number,
78
80
  chunks: TelegramRenderedChunk[],
81
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
79
82
  ) => Promise<number | undefined>;
80
83
  editRenderedMessage: (
81
84
  chatId: number,
82
85
  messageId: number,
83
86
  chunks: TelegramRenderedChunk[],
87
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
84
88
  ) => Promise<number | undefined>;
85
89
  }
86
90
 
@@ -158,11 +162,13 @@ export interface TelegramPreviewControllerDeps {
158
162
  chatId: number,
159
163
  chunks: TelegramRenderedChunk[],
160
164
  replyToMessageId: number | undefined,
165
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
161
166
  ) => Promise<number | undefined>;
162
167
  editRenderedMessage: (
163
168
  chatId: number,
164
169
  messageId: number,
165
170
  chunks: TelegramRenderedChunk[],
171
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
166
172
  ) => Promise<number | undefined>;
167
173
  throttleMs?: number;
168
174
  maxDraftId?: number;
@@ -187,6 +193,7 @@ export interface TelegramPreviewController {
187
193
  chatId: number,
188
194
  markdown: string,
189
195
  replyToMessageId?: number,
196
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
190
197
  ) => Promise<boolean>;
191
198
  }
192
199
 
@@ -227,12 +234,16 @@ export interface TelegramPreviewRenderedChunkTransportDeps {
227
234
  sendRenderedChunks: (
228
235
  chatId: number,
229
236
  chunks: TelegramRenderedChunk[],
230
- options?: { replyToMessageId?: number },
237
+ options?: {
238
+ replyToMessageId?: number;
239
+ replyMarkup?: TelegramPreviewReplyMarkup;
240
+ },
231
241
  ) => Promise<number | undefined>;
232
242
  editRenderedMessage: (
233
243
  chatId: number,
234
244
  messageId: number,
235
245
  chunks: TelegramRenderedChunk[],
246
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
236
247
  ) => Promise<number | undefined>;
237
248
  }
238
249
 
@@ -243,9 +254,13 @@ export function createTelegramPreviewRenderedChunkTransport(
243
254
  "sendRenderedChunks" | "editRenderedMessage"
244
255
  > {
245
256
  return {
246
- sendRenderedChunks: (chatId, chunks, replyToMessageId) =>
247
- deps.sendRenderedChunks(chatId, chunks, { replyToMessageId }),
248
- editRenderedMessage: deps.editRenderedMessage,
257
+ sendRenderedChunks: (chatId, chunks, replyToMessageId, options) =>
258
+ deps.sendRenderedChunks(chatId, chunks, {
259
+ replyToMessageId,
260
+ ...(options?.replyMarkup ? { replyMarkup: options.replyMarkup } : {}),
261
+ }),
262
+ editRenderedMessage: (chatId, messageId, chunks, options) =>
263
+ deps.editRenderedMessage(chatId, messageId, chunks, options),
249
264
  };
250
265
  }
251
266
 
@@ -364,11 +379,12 @@ export function createTelegramPreviewController(
364
379
  ),
365
380
  editMessageText: deps.editMessageText,
366
381
  renderTelegramMessage: renderMessage,
367
- sendRenderedChunks: (chatId, chunks) =>
382
+ sendRenderedChunks: (chatId, chunks, options) =>
368
383
  deps.sendRenderedChunks(
369
384
  chatId,
370
385
  chunks,
371
386
  replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
387
+ options,
372
388
  ),
373
389
  editRenderedMessage: deps.editRenderedMessage,
374
390
  });
@@ -394,11 +410,12 @@ export function createTelegramPreviewController(
394
410
  },
395
411
  finalize: (chatId, replyToMessageId) =>
396
412
  finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
397
- finalizeMarkdown: (chatId, markdown, replyToMessageId) =>
413
+ finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
398
414
  finalizeTelegramMarkdownPreview(
399
415
  chatId,
400
416
  markdown,
401
417
  getRuntimeDeps(replyToMessageId),
418
+ options,
402
419
  ),
403
420
  };
404
421
  }
@@ -453,7 +470,9 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
453
470
  state = deps.createPreviewState();
454
471
  deps.setState(state);
455
472
  }
456
- state.pendingText = deps.getMessageText(message);
473
+ state.pendingText = stripTelegramCommentMarkupForPreview(
474
+ deps.getMessageText(message),
475
+ );
457
476
  deps.schedulePreviewFlush(turn.chatId);
458
477
  }
459
478
 
@@ -613,6 +632,7 @@ export async function finalizeTelegramMarkdownPreview(
613
632
  chatId: number,
614
633
  markdown: string,
615
634
  deps: TelegramPreviewRuntimeDeps,
635
+ options?: { replyMarkup?: TelegramPreviewReplyMarkup },
616
636
  ): Promise<boolean> {
617
637
  const state = deps.getState();
618
638
  if (!state) return false;
@@ -623,12 +643,12 @@ export async function finalizeTelegramMarkdownPreview(
623
643
  return false;
624
644
  }
625
645
  if (state.mode === "draft") {
626
- await deps.sendRenderedChunks(chatId, chunks);
646
+ await deps.sendRenderedChunks(chatId, chunks, options);
627
647
  await clearTelegramPreview(chatId, deps);
628
648
  return true;
629
649
  }
630
650
  if (state.messageId === undefined) return false;
631
- await deps.editRenderedMessage(chatId, state.messageId, chunks);
651
+ await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
632
652
  deps.setState(undefined);
633
653
  return true;
634
654
  }
package/lib/prompts.ts CHANGED
@@ -11,9 +11,13 @@ const SYSTEM_PROMPT_SUFFIX = `
11
11
  Telegram bridge extension is active.
12
12
  - Messages forwarded from Telegram are prefixed with "[telegram]".
13
13
  - [telegram] messages may include [attachments] sections with a base directory plus relative local file entries. Resolve and read those files as needed.
14
+ - [telegram] messages may include a [reply] block after the user's current text. Treat [reply] as quoted context from the Telegram message the user replied to, not as a new instruction by itself; use it to resolve references like "this", "it", or "that message". The actual new user instruction is the message text before [reply], unless it explicitly asks you to act on the quoted context.
14
15
  - Telegram is often read on narrow phone screens, so prefer narrow table columns when presenting tabular data; wide monospace tables can become unreadable.
15
16
  - If a [telegram] user asked for a file or generated artifact, use telegram_attach with the local path instead of only mentioning the path in text.
16
- - Do not assume mentioning a local file path in plain text will send it to Telegram. Use telegram_attach.`;
17
+ - Do not assume mentioning a local file path in plain text will send it to Telegram. Use telegram_attach.
18
+ - For Telegram-native outbound actions, use hidden top-level Markdown comments instead of agent-side tool calls: write a normal answer plus correctly formatted column-zero \`telegram_voice\` or \`telegram_button\` blocks outside code, quotes, and lists. The bridge handles delivery after \`agent_end\`, so do not call or register transport/TTS/text-to-OGG tools for these actions.
19
+ - A \`telegram_voice\` block body is the text to synthesize through the extension's configured outbound-handler pipeline. It may be a short companion summary when useful, but no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
20
+ - Button blocks should contain quick reply prompts the user can tap; use independent blocks like \`<!-- telegram_button label="OK"\nPrompt text\n-->\`, or \`<!-- telegram_button label="OK" -->\` when the prompt should equal the label. The callback prompt is routed back as a normal Telegram turn.`;
17
21
 
18
22
  export function buildTelegramBridgeSystemPrompt(options: {
19
23
  prompt: string;
package/lib/queue.ts CHANGED
@@ -720,6 +720,21 @@ export interface TelegramAgentEndAssistantResult {
720
720
  errorMessage?: string;
721
721
  }
722
722
 
723
+ export interface TelegramAgentEndOutboundVoiceReply {
724
+ text: string;
725
+ lang?: string;
726
+ rate?: string;
727
+ }
728
+
729
+ export interface TelegramAgentEndOutboundReplyPlan<TReplyMarkup = unknown> {
730
+ markdown: string;
731
+ replyMarkup?: TReplyMarkup;
732
+ voiceText?: string;
733
+ voiceReplies?: TelegramAgentEndOutboundVoiceReply[];
734
+ lang?: string;
735
+ rate?: string;
736
+ }
737
+
723
738
  export interface TelegramAgentEndRuntimeDeps<
724
739
  TTurn extends PendingTelegramTurn,
725
740
  > {
@@ -735,11 +750,13 @@ export interface TelegramAgentEndRuntimeDeps<
735
750
  chatId: number,
736
751
  markdown: string,
737
752
  replyToMessageId: number,
753
+ options?: { replyMarkup?: unknown },
738
754
  ) => Promise<boolean>;
739
755
  sendMarkdownReply: (
740
756
  chatId: number,
741
757
  replyToMessageId: number,
742
758
  markdown: string,
759
+ options?: { replyMarkup?: unknown },
743
760
  ) => Promise<unknown>;
744
761
  sendTextReply: (
745
762
  chatId: number,
@@ -747,6 +764,12 @@ export interface TelegramAgentEndRuntimeDeps<
747
764
  text: string,
748
765
  ) => Promise<unknown>;
749
766
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
767
+ planOutboundReply?: (markdown: string) => TelegramAgentEndOutboundReplyPlan;
768
+ sendOutboundReplyArtifacts?: (
769
+ turn: TTurn,
770
+ plan: TelegramAgentEndOutboundReplyPlan,
771
+ options?: { replyToPrompt?: boolean },
772
+ ) => Promise<void>;
750
773
  }
751
774
 
752
775
  export interface TelegramAgentEndHookRuntimeDeps<
@@ -768,6 +791,8 @@ export interface TelegramAgentEndHookRuntimeDeps<
768
791
  sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn>["sendMarkdownReply"];
769
792
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
770
793
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
794
+ planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn>["planOutboundReply"];
795
+ sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
771
796
  }
772
797
 
773
798
  export interface TelegramAgentEndHookEvent<TMessage> {
@@ -865,6 +890,8 @@ export function createTelegramAgentEndHook<
865
890
  sendMarkdownReply: deps.sendMarkdownReply,
866
891
  sendTextReply: deps.sendTextReply,
867
892
  sendQueuedAttachments: deps.sendQueuedAttachments,
893
+ planOutboundReply: deps.planOutboundReply,
894
+ sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
868
895
  });
869
896
  };
870
897
  }
@@ -873,13 +900,20 @@ export async function handleTelegramAgentEndRuntime<
873
900
  TTurn extends PendingTelegramTurn,
874
901
  >(deps: TelegramAgentEndRuntimeDeps<TTurn>): Promise<void> {
875
902
  const { turn, assistant } = deps;
876
- const finalText = assistant.text;
903
+ const rawFinalText = assistant.text;
904
+ const outboundReply = rawFinalText
905
+ ? deps.planOutboundReply?.(rawFinalText)
906
+ : undefined;
907
+ const finalText = outboundReply ? outboundReply.markdown : rawFinalText;
908
+ const hasOutboundArtifacts =
909
+ !!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
910
+ const replyMarkup = outboundReply?.replyMarkup;
877
911
  deps.resetRuntimeState();
878
912
  deps.updateStatus();
879
913
  const endPlan = buildTelegramAgentEndPlan({
880
914
  hasTurn: !!turn,
881
915
  stopReason: assistant.stopReason,
882
- hasFinalText: !!finalText,
916
+ hasFinalText: !!finalText || hasOutboundArtifacts,
883
917
  hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
884
918
  preserveQueuedTurnsAsHistory: deps.preserveQueuedTurnsAsHistory,
885
919
  });
@@ -901,11 +935,13 @@ export async function handleTelegramAgentEndRuntime<
901
935
  return;
902
936
  }
903
937
  if (finalText) deps.setPreviewPendingText(finalText);
938
+ if (!finalText && hasOutboundArtifacts) await deps.clearPreview(turn.chatId);
904
939
  if (endPlan.kind === "text" && finalText) {
905
940
  const finalized = await deps.finalizeMarkdownPreview(
906
941
  turn.chatId,
907
942
  finalText,
908
943
  turn.replyToMessageId,
944
+ { replyMarkup },
909
945
  );
910
946
  if (!finalized) {
911
947
  await deps.clearPreview(turn.chatId);
@@ -913,9 +949,15 @@ export async function handleTelegramAgentEndRuntime<
913
949
  turn.chatId,
914
950
  turn.replyToMessageId,
915
951
  finalText,
952
+ { replyMarkup },
916
953
  );
917
954
  }
918
955
  }
956
+ if (outboundReply && deps.sendOutboundReplyArtifacts) {
957
+ await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
958
+ replyToPrompt: !finalText,
959
+ });
960
+ }
919
961
  if (endPlan.shouldSendAttachmentNotice) {
920
962
  await deps.sendTextReply(
921
963
  turn.chatId,
package/lib/replies.ts CHANGED
@@ -174,13 +174,14 @@ export async function editTelegramRenderedMessage<TReplyMarkup>(
174
174
  return messageId;
175
175
  }
176
176
 
177
- export interface TelegramReplyRuntimeDeps {
177
+ export interface TelegramReplyRuntimeDeps<TReplyMarkup = unknown> {
178
178
  renderTelegramMessage: (
179
179
  text: string,
180
180
  options?: { mode?: TelegramRenderMode },
181
181
  ) => TelegramRenderedChunk[];
182
182
  sendRenderedChunks: (
183
183
  chunks: TelegramRenderedChunk[],
184
+ options?: { replyMarkup?: TReplyMarkup },
184
185
  ) => Promise<number | undefined>;
185
186
  }
186
187
 
@@ -195,15 +196,16 @@ export async function sendTelegramPlainReply(
195
196
  return deps.sendRenderedChunks(chunks);
196
197
  }
197
198
 
198
- export async function sendTelegramMarkdownReply(
199
+ export async function sendTelegramMarkdownReply<TReplyMarkup = unknown>(
199
200
  markdown: string,
200
201
  deps: TelegramReplyRuntimeDeps,
202
+ options?: { replyMarkup?: TReplyMarkup },
201
203
  ): Promise<number | undefined> {
202
204
  const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
203
205
  if (chunks.length === 0) {
204
206
  return sendTelegramPlainReply(markdown, deps);
205
207
  }
206
- return deps.sendRenderedChunks(chunks);
208
+ return deps.sendRenderedChunks(chunks, options);
207
209
  }
208
210
 
209
211
  export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
@@ -225,6 +227,7 @@ export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
225
227
  chatId: number,
226
228
  replyToMessageId: number,
227
229
  markdown: string,
230
+ options?: { replyMarkup?: unknown },
228
231
  ) => Promise<number | undefined>;
229
232
  editInteractiveMessage: (
230
233
  chatId: number,
@@ -290,14 +293,21 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
290
293
  options,
291
294
  );
292
295
  },
293
- sendMarkdownReply: async (chatId, replyToMessageId, markdown) => {
294
- return sendTelegramMarkdownReply(markdown, {
295
- renderTelegramMessage: deps.renderTelegramMessage,
296
- sendRenderedChunks: (chunks) =>
297
- deps.replyTransport.sendRenderedChunks(chatId, chunks, {
298
- replyToMessageId,
299
- }),
300
- });
296
+ sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
297
+ return sendTelegramMarkdownReply(
298
+ markdown,
299
+ {
300
+ renderTelegramMessage: deps.renderTelegramMessage,
301
+ sendRenderedChunks: (chunks, chunkOptions) =>
302
+ deps.replyTransport.sendRenderedChunks(chatId, chunks, {
303
+ replyToMessageId,
304
+ replyMarkup: chunkOptions?.replyMarkup as
305
+ | TReplyMarkup
306
+ | undefined,
307
+ }),
308
+ },
309
+ options,
310
+ );
301
311
  },
302
312
  editInteractiveMessage: async (
303
313
  chatId,
package/lib/routing.ts CHANGED
@@ -3,9 +3,10 @@
3
3
  * Wires authorized updates into menus, commands, media grouping, and prompt queueing
4
4
  */
5
5
 
6
+ import * as OutboundHandlers from "./outbound-handlers.ts";
6
7
  import * as Commands from "./commands.ts";
7
8
  import type { TelegramConfigStore } from "./config.ts";
8
- import type { TelegramAttachmentHandlerRuntime } from "./handlers.ts";
9
+ import type { TelegramAttachmentHandlerRuntime } from "./attachment-handlers.ts";
9
10
  import * as Media from "./media.ts";
10
11
  import * as Menu from "./menu.ts";
11
12
  import * as Model from "./model.ts";
@@ -50,6 +51,7 @@ export interface TelegramInboundRouteRuntimeDeps<
50
51
  Model.ScopedTelegramModel<TModel>
51
52
  >;
52
53
  menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
54
+ buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
53
55
  attachmentHandlerRuntime: TelegramAttachmentHandlerRuntime<TContext>;
54
56
  updateStatus: (ctx: TContext, error?: string) => void;
55
57
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
@@ -99,7 +101,7 @@ export function createTelegramInboundRouteRuntime<
99
101
  TModel
100
102
  >,
101
103
  ): Updates.TelegramUpdateRuntimeController<TContext, TUpdate> {
102
- const callbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
104
+ const menuCallbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
103
105
  TCallbackQuery,
104
106
  TContext,
105
107
  TModel
@@ -124,6 +126,41 @@ export function createTelegramInboundRouteRuntime<
124
126
  restartInterruptedTelegramTurn:
125
127
  deps.modelSwitchController.restartInterruptedTurn,
126
128
  });
129
+ const callbackHandler = async (
130
+ query: TCallbackQuery,
131
+ ctx: TContext,
132
+ ): Promise<void> => {
133
+ if (deps.buttonActionStore) {
134
+ const handled = await OutboundHandlers.handleTelegramButtonCallbackQuery(
135
+ query,
136
+ ctx,
137
+ {
138
+ resolveAction: deps.buttonActionStore.resolve,
139
+ answerCallbackQuery: deps.answerCallbackQuery,
140
+ enqueueButtonPrompt: (buttonQuery, action, context) => {
141
+ const chatId = buttonQuery.message?.chat?.id;
142
+ const messageId = buttonQuery.message?.message_id;
143
+ if (typeof chatId !== "number" || typeof messageId !== "number")
144
+ return;
145
+ const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
146
+ deps.queueMutationRuntime.append(
147
+ OutboundHandlers.createTelegramButtonPromptTurn({
148
+ chatId,
149
+ replyToMessageId: messageId,
150
+ queueOrder,
151
+ action,
152
+ }),
153
+ context,
154
+ );
155
+ deps.updateStatus(context);
156
+ deps.dispatchNextQueuedTelegramTurn(context);
157
+ },
158
+ },
159
+ );
160
+ if (handled) return;
161
+ }
162
+ await menuCallbackHandler(query, ctx);
163
+ };
127
164
  const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
128
165
  TMessage,
129
166
  TContext
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",