@llblab/pi-telegram 0.35.2 → 0.36.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/menu-queue.ts CHANGED
@@ -88,7 +88,7 @@ function buildTelegramQueueMenuReplyMarkup(
88
88
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
89
89
  const rows = items.map((item, index) => {
90
90
  const prefix = item.reactionSuppressionEmoji
91
- ? `${item.reactionSuppressionEmoji} suppressed · `
91
+ ? `${item.reactionSuppressionEmoji} `
92
92
  : item.isPriority
93
93
  ? `${item.priorityEmoji ?? "⚡"} `
94
94
  : item.hasAttachments
@@ -163,17 +163,15 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
163
163
  ? ` ${item.priorityEmoji ?? "⚡"}`
164
164
  : "";
165
165
  const heading = `<b>${item.queuePosition}.</b>${badge}`;
166
- const suppression = item.reactionSuppressionEmoji
167
- ? "\n<i>Suppressed by reaction. Remove it to restore this turn.</i>"
168
- : "";
169
166
  const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
170
- return `${heading}${suppression}\n${preview}`;
167
+ return `${heading}\n${preview}`;
171
168
  }
172
169
 
173
170
  function buildTelegramQueueItemSubmenuReplyMarkup(
174
171
  chatId: number,
175
172
  replyToMessageId: number,
176
173
  isPriority: boolean,
174
+ isSkipped: boolean,
177
175
  ): TelegramQueueMenuReplyMarkup {
178
176
  return {
179
177
  inline_keyboard: [
@@ -190,33 +188,18 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
190
188
  ],
191
189
  [
192
190
  {
193
- text: "🗑 Delete",
194
- callback_data: `queue:delete:${chatId}:${replyToMessageId}`,
191
+ text: isSkipped ? "⚫️ Keep" : "🟢 Keep",
192
+ callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:keep`,
195
193
  },
196
- ],
197
- ],
198
- };
199
- }
200
-
201
- function buildTelegramQueueDeleteConfirmationReplyMarkup(
202
- chatId: number,
203
- replyToMessageId: number,
204
- ): TelegramQueueMenuReplyMarkup {
205
- return {
206
- inline_keyboard: [
207
- [
208
194
  {
209
- text: "🗑 Yes, delete",
210
- callback_data: `queue:confirm-delete:${chatId}:${replyToMessageId}`,
211
- },
212
- {
213
- text: "❌ No",
214
- callback_data: `queue:keep:${chatId}:${replyToMessageId}`,
195
+ text: isSkipped ? "🔴 Skip" : "⚫️ Skip",
196
+ callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:skip`,
215
197
  },
216
198
  ],
217
199
  ],
218
200
  };
219
201
  }
202
+
220
203
  interface TelegramQueueMenuCallbackDeps<Context = unknown> {
221
204
  getQueuedItems: () => TelegramQueueMenuItem[];
222
205
  findItem: (
@@ -229,10 +212,10 @@ interface TelegramQueueMenuCallbackDeps<Context = unknown> {
229
212
  replyToMessageId: number,
230
213
  enabled: boolean,
231
214
  ) => boolean;
232
- cancelItem: (
215
+ setSkipped: (
233
216
  chatId: number,
234
217
  replyToMessageId: number,
235
- ctx: Context,
218
+ skipped: boolean,
236
219
  ) => boolean;
237
220
  updateQueueMessage: (
238
221
  chatId: number,
@@ -322,38 +305,17 @@ async function handleTelegramQueueMenuCallback<Context>(
322
305
  );
323
306
  return true;
324
307
  }
325
- const deleteMatch = data.match(/^queue:(?:delete|cancel):(\d+):(\d+)$/);
326
- if (deleteMatch) {
327
- await handleTelegramQueueMenuDeleteRequest(
328
- callbackQueryId,
329
- replyChatId,
330
- replyMessageId,
331
- Number(deleteMatch[1]),
332
- Number(deleteMatch[2]),
333
- deps,
334
- );
335
- return true;
336
- }
337
- const keepMatch = data.match(/^queue:keep:(\d+):(\d+)$/);
338
- if (keepMatch) {
339
- await handleTelegramQueueMenuKeep(
340
- callbackQueryId,
341
- replyChatId,
342
- replyMessageId,
343
- Number(keepMatch[1]),
344
- Number(keepMatch[2]),
345
- deps,
346
- );
347
- return true;
348
- }
349
- const confirmDeleteMatch = data.match(/^queue:confirm-delete:(\d+):(\d+)$/);
350
- if (confirmDeleteMatch) {
351
- await handleTelegramQueueMenuConfirmDelete(
308
+ const skipSetMatch = data.match(
309
+ /^queue:skip-set:(\d+):(\d+):(skip|keep)$/,
310
+ );
311
+ if (skipSetMatch) {
312
+ await handleTelegramQueueMenuSkipSet(
352
313
  callbackQueryId,
353
314
  replyChatId,
354
315
  replyMessageId,
355
- Number(confirmDeleteMatch[1]),
356
- Number(confirmDeleteMatch[2]),
316
+ Number(skipSetMatch[1]),
317
+ Number(skipSetMatch[2]),
318
+ skipSetMatch[3] === "skip",
357
319
  ctx,
358
320
  deps,
359
321
  );
@@ -427,7 +389,12 @@ async function handleTelegramQueueMenuPick<Context>(
427
389
  replyChatId,
428
390
  replyMessageId,
429
391
  getTelegramQueueMenuItemText(item),
430
- buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority),
392
+ buildTelegramQueueItemSubmenuReplyMarkup(
393
+ chatId,
394
+ msgId,
395
+ item.isPriority,
396
+ item.reactionSuppressionEmoji !== undefined,
397
+ ),
431
398
  );
432
399
  await deps.answerCallbackQuery(callbackQueryId);
433
400
  }
@@ -518,7 +485,12 @@ async function updateTelegramQueueMenuPriority<Context>(
518
485
  replyChatId,
519
486
  replyMessageId,
520
487
  getTelegramQueueMenuItemText(updated),
521
- buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, updated.isPriority),
488
+ buildTelegramQueueItemSubmenuReplyMarkup(
489
+ chatId,
490
+ msgId,
491
+ updated.isPriority,
492
+ updated.reactionSuppressionEmoji !== undefined,
493
+ ),
522
494
  );
523
495
  await deps.answerCallbackQuery(
524
496
  callbackQueryId,
@@ -526,12 +498,14 @@ async function updateTelegramQueueMenuPriority<Context>(
526
498
  );
527
499
  }
528
500
 
529
- async function handleTelegramQueueMenuDeleteRequest<Context>(
501
+ async function handleTelegramQueueMenuSkipSet<Context>(
530
502
  callbackQueryId: string,
531
503
  replyChatId: number,
532
504
  replyMessageId: number,
533
505
  chatId: number,
534
506
  msgId: number,
507
+ skipped: boolean,
508
+ ctx: Context,
535
509
  deps: TelegramQueueMenuCallbackDeps<Context>,
536
510
  ): Promise<void> {
537
511
  const item = deps.findItem(chatId, msgId);
@@ -543,25 +517,10 @@ async function handleTelegramQueueMenuDeleteRequest<Context>(
543
517
  deps,
544
518
  );
545
519
  }
546
- await deps.updateQueueMessage(
547
- replyChatId,
548
- replyMessageId,
549
- "<b>Delete this queued prompt?</b>",
550
- buildTelegramQueueDeleteConfirmationReplyMarkup(chatId, msgId),
551
- );
552
- await deps.answerCallbackQuery(callbackQueryId);
553
- }
554
-
555
- async function handleTelegramQueueMenuKeep<Context>(
556
- callbackQueryId: string,
557
- replyChatId: number,
558
- replyMessageId: number,
559
- chatId: number,
560
- msgId: number,
561
- deps: TelegramQueueMenuCallbackDeps<Context>,
562
- ): Promise<void> {
563
- const item = deps.findItem(chatId, msgId);
564
- if (!item) {
520
+ deps.setSkipped(chatId, msgId, skipped);
521
+ deps.updateStatus(ctx);
522
+ const updated = deps.findItem(chatId, msgId);
523
+ if (!updated) {
565
524
  return refreshStaleTelegramQueueMenuItem(
566
525
  callbackQueryId,
567
526
  replyChatId,
@@ -572,30 +531,15 @@ async function handleTelegramQueueMenuKeep<Context>(
572
531
  await deps.updateQueueMessage(
573
532
  replyChatId,
574
533
  replyMessageId,
575
- getTelegramQueueMenuItemText(item),
576
- buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority),
577
- );
578
- await deps.answerCallbackQuery(callbackQueryId, "Kept in queue.");
579
- }
580
-
581
- async function handleTelegramQueueMenuConfirmDelete<Context>(
582
- callbackQueryId: string,
583
- replyChatId: number,
584
- replyMessageId: number,
585
- chatId: number,
586
- msgId: number,
587
- ctx: Context,
588
- deps: TelegramQueueMenuCallbackDeps<Context>,
589
- ): Promise<void> {
590
- const removed = deps.cancelItem(chatId, msgId, ctx);
591
- deps.updateStatus(ctx);
592
- await updateTelegramQueueMenuList(
593
- callbackQueryId,
594
- replyChatId,
595
- replyMessageId,
596
- deps,
597
- removed ? "Deleted from queue." : "Item not found.",
534
+ getTelegramQueueMenuItemText(updated),
535
+ buildTelegramQueueItemSubmenuReplyMarkup(
536
+ chatId,
537
+ msgId,
538
+ updated.isPriority,
539
+ updated.reactionSuppressionEmoji !== undefined,
540
+ ),
598
541
  );
542
+ await deps.answerCallbackQuery(callbackQueryId);
599
543
  }
600
544
 
601
545
  interface TelegramQueueMenuCallbackQuery {
@@ -803,8 +747,8 @@ function createQueueMenuCallbackHandler<
803
747
  queueMutationRuntime: deps.queueMutationRuntime,
804
748
  });
805
749
  },
806
- cancelItem: (cId, rId, c) => {
807
- return cancelQueuedTelegramItem(cId, rId, c, {
750
+ setSkipped: (cId, rId, skipped) => {
751
+ return setQueuedTelegramPromptSkipped(cId, rId, skipped, ctx, {
808
752
  getQueueSnapshot,
809
753
  queueMutationRuntime: deps.queueMutationRuntime,
810
754
  });
@@ -817,6 +761,40 @@ function createQueueMenuCallbackHandler<
817
761
  };
818
762
  }
819
763
 
764
+ function getQueueMenuReactionDisposition<Context>(
765
+ item: Queue.TelegramQueueItem<Context>,
766
+ priority: boolean,
767
+ skipped: boolean,
768
+ ): Queue.TelegramQueueReactionDisposition {
769
+ if (priority && skipped) {
770
+ return {
771
+ kind: "priority-suppressed",
772
+ priorityEmoji:
773
+ item.kind === "prompt" ? item.priorityEmoji ?? "⚡" : "⚡",
774
+ suppressionEmoji:
775
+ item.kind === "prompt"
776
+ ? item.reactionSuppressionEmoji ?? "👎"
777
+ : "👎",
778
+ };
779
+ }
780
+ if (priority) {
781
+ return {
782
+ kind: "priority",
783
+ emoji: item.kind === "prompt" ? item.priorityEmoji ?? "⚡" : "⚡",
784
+ };
785
+ }
786
+ if (skipped) {
787
+ return {
788
+ kind: "suppressed",
789
+ emoji:
790
+ item.kind === "prompt"
791
+ ? item.reactionSuppressionEmoji ?? "👎"
792
+ : "👎",
793
+ };
794
+ }
795
+ return { kind: "default" };
796
+ }
797
+
820
798
  function toggleQueuedTelegramPromptPriority<Context>(
821
799
  chatId: number,
822
800
  replyToMessageId: number,
@@ -834,9 +812,12 @@ function toggleQueuedTelegramPromptPriority<Context>(
834
812
  if (!item) return false;
835
813
  deps.queueMutationRuntime.applyReactionByMessageId(
836
814
  replyToMessageId,
837
- item.queueLane === "priority"
838
- ? { kind: "default" }
839
- : { kind: "priority", emoji: "⚡" },
815
+ getQueueMenuReactionDisposition(
816
+ item,
817
+ item.queueLane !== "priority",
818
+ item.kind === "prompt" &&
819
+ item.reactionSuppressionEmoji !== undefined,
820
+ ),
840
821
  ctx,
841
822
  );
842
823
  return true;
@@ -860,15 +841,21 @@ function setQueuedTelegramPromptPriority<Context>(
860
841
  if (!item) return false;
861
842
  deps.queueMutationRuntime.applyReactionByMessageId(
862
843
  replyToMessageId,
863
- enabled ? { kind: "priority", emoji: "⚡" } : { kind: "default" },
844
+ getQueueMenuReactionDisposition(
845
+ item,
846
+ enabled,
847
+ item.kind === "prompt" &&
848
+ item.reactionSuppressionEmoji !== undefined,
849
+ ),
864
850
  ctx,
865
851
  );
866
852
  return true;
867
853
  }
868
854
 
869
- function cancelQueuedTelegramItem<Context>(
855
+ function setQueuedTelegramPromptSkipped<Context>(
870
856
  chatId: number,
871
857
  replyToMessageId: number,
858
+ skipped: boolean,
872
859
  ctx: Context,
873
860
  deps: {
874
861
  getQueueSnapshot: () => Queue.TelegramQueueItem<Context>[];
@@ -880,11 +867,17 @@ function cancelQueuedTelegramItem<Context>(
880
867
  chatId,
881
868
  replyToMessageId,
882
869
  );
883
- if (!item) return false;
884
- return (
885
- deps.queueMutationRuntime.removeByMessageIds([item.replyToMessageId], ctx) >
886
- 0
870
+ if (!item || item.kind !== "prompt") return false;
871
+ deps.queueMutationRuntime.applyReactionByMessageId(
872
+ replyToMessageId,
873
+ getQueueMenuReactionDisposition(
874
+ item,
875
+ item.queueLane === "priority",
876
+ skipped,
877
+ ),
878
+ ctx,
887
879
  );
880
+ return true;
888
881
  }
889
882
 
890
883
  function createQueueMenuSendMessageAdapter(
@@ -24,9 +24,16 @@ import {
24
24
  const TELEGRAM_BUTTON_CALLBACK_PREFIX = "tgbtn";
25
25
  const TELEGRAM_BUTTON_ACTION_TTL_MS = 24 * 60 * 60 * 1000;
26
26
 
27
+ export interface TelegramOutboundButtonBinding {
28
+ generation: string;
29
+ app: string;
30
+ revision: number;
31
+ }
32
+
27
33
  export interface TelegramOutboundButtonAction {
28
34
  text: string;
29
35
  prompt: string;
36
+ binding?: TelegramOutboundButtonBinding;
30
37
  selectedStyle?: TelegramInlineKeyboardButtonStyle;
31
38
  }
32
39
 
@@ -72,6 +79,11 @@ export interface TelegramButtonCallbackHandlerDeps<TContext = unknown> {
72
79
  action: TelegramOutboundButtonAction,
73
80
  ctx: TContext,
74
81
  ) => boolean | void;
82
+ invokeBoundAction?: (
83
+ query: TelegramButtonCallbackQuery,
84
+ action: TelegramOutboundButtonAction,
85
+ ctx: TContext,
86
+ ) => Promise<false | "new" | "edit">;
75
87
  editMessageReplyMarkup?: (
76
88
  chatId: number,
77
89
  messageId: number,
@@ -146,6 +158,7 @@ export function createTelegramButtonActionStore(
146
158
  return {
147
159
  text: action.text,
148
160
  prompt: action.prompt,
161
+ ...(action.binding ? { binding: action.binding } : {}),
149
162
  ...(action.selectedStyle
150
163
  ? { selectedStyle: action.selectedStyle }
151
164
  : {}),
@@ -159,7 +172,10 @@ const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN =
159
172
 
160
173
  export function planTelegramButtonReply(
161
174
  markdown: string,
162
- deps: { registerAction: (action: TelegramOutboundButtonAction) => string },
175
+ deps: {
176
+ registerAction: (action: TelegramOutboundButtonAction) => string;
177
+ binding?: TelegramOutboundButtonBinding;
178
+ },
163
179
  ): TelegramButtonReplyPlan {
164
180
  const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
165
181
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
@@ -167,15 +183,20 @@ export function planTelegramButtonReply(
167
183
  parseTopLevelTelegramComment(comment, candidate),
168
184
  );
169
185
  if (!command) return comment.raw;
170
- const payloadRows = parseTelegramActionPayloadRows(comment, command) ?? [];
171
- for (const payloadRow of payloadRows) {
172
- const row = payloadRow.flatMap((payload) => {
173
- const action = parseTelegramButtonAction(payload);
174
- return action
175
- ? [{ text: action.text, callback_data: deps.registerAction(action) }]
176
- : [];
177
- });
178
- if (row.length > 0) keyboard.push(row);
186
+ const payloadRows = parseTelegramActionPayloadRows(comment, command);
187
+ if (!payloadRows) return "";
188
+ const actionRows = payloadRows.map((payloadRow) =>
189
+ payloadRow.map(parseTelegramButtonAction),
190
+ );
191
+ if (actionRows.some((row) => row.some((action) => !action))) return "";
192
+ for (const actionRow of actionRows) {
193
+ keyboard.push(actionRow.map((action) => ({
194
+ text: action!.text,
195
+ callback_data: deps.registerAction({
196
+ ...action!,
197
+ ...(deps.binding ? { binding: deps.binding } : {}),
198
+ }),
199
+ })));
179
200
  }
180
201
  return "";
181
202
  });
@@ -262,6 +283,33 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
262
283
  return true;
263
284
  }
264
285
 
286
+ if (deps.invokeBoundAction) {
287
+ try {
288
+ const viewMode = await deps.invokeBoundAction(query, action, ctx);
289
+ if (viewMode) {
290
+ if (viewMode === "new" && query.data && query.message?.reply_markup) {
291
+ const selectedMarkup = markTelegramButtonSelected(
292
+ query.message.reply_markup,
293
+ query.data,
294
+ action.selectedStyle,
295
+ );
296
+ if (selectedMarkup && deps.editMessageReplyMarkup) {
297
+ try {
298
+ await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
299
+ } catch {
300
+ // The action already succeeded; old-surface styling is best-effort.
301
+ }
302
+ }
303
+ }
304
+ await deps.answerCallbackQuery(query.id, "Done.");
305
+ return true;
306
+ }
307
+ } catch (error) {
308
+ await deps.answerCallbackQuery(query.id, "Generative App action failed.");
309
+ throw error;
310
+ }
311
+ }
312
+
265
313
  const enqueued = deps.enqueueButtonPrompt(query, action, ctx);
266
314
  if (enqueued === false) {
267
315
  await deps.answerCallbackQuery(query.id, "Already queued.");
@@ -207,7 +207,7 @@ export function parseTelegramActionPayload(
207
207
 
208
208
  const TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
209
209
 
210
- function parseTelegramCompactActionPayloadRows(
210
+ function parseTelegramAdaptiveActionPayloadRows(
211
211
  source: string,
212
212
  ): Record<string, unknown>[][] | undefined {
213
213
  let offset = 0;
@@ -219,18 +219,23 @@ function parseTelegramCompactActionPayloadRows(
219
219
  const skipWhitespace = (): void => {
220
220
  while (isStructuralWhitespace(source[offset])) offset += 1;
221
221
  };
222
+ const consumeOptionalSeparator = (): boolean => {
223
+ skipWhitespace();
224
+ if (source[offset] !== ",") return true;
225
+ offset += 1;
226
+ skipWhitespace();
227
+ return source[offset] !== "," && source[offset] !== "]";
228
+ };
222
229
  const normalizeAtom = (value: string): string | undefined => {
223
230
  const normalized = value.trim();
224
231
  return normalized && !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
225
232
  ? normalized
226
233
  : undefined;
227
234
  };
228
- const parseCell = (): Record<string, unknown> | undefined => {
235
+ const parseCompactCell = (): Record<string, unknown> | undefined => {
229
236
  if (source[offset] !== "{") return undefined;
230
237
  offset += 1;
231
- const keySource: string[] = [];
232
- const valueSource: string[] = [];
233
- let hasSeparator = false;
238
+ const atomSources: string[][] = [[]];
234
239
  while (offset < source.length) {
235
240
  const character = source[offset]!;
236
241
  if (character === "\\") {
@@ -238,31 +243,85 @@ function parseTelegramCompactActionPayloadRows(
238
243
  if (escaped !== "|" && escaped !== "}" && escaped !== "\\") {
239
244
  return undefined;
240
245
  }
241
- if (hasSeparator) valueSource.push(escaped);
242
- else keySource.push(escaped);
246
+ atomSources.at(-1)!.push(escaped);
243
247
  offset += 2;
244
248
  continue;
245
249
  }
246
250
  if (character === "|") {
247
- if (hasSeparator) return undefined;
248
- hasSeparator = true;
251
+ if (atomSources.length >= 3) return undefined;
252
+ atomSources.push([]);
249
253
  offset += 1;
250
254
  continue;
251
255
  }
252
256
  if (character === "}") {
253
257
  offset += 1;
254
- const key = normalizeAtom(keySource.join(""));
255
- if (!key) return undefined;
256
- if (!hasSeparator) return { value: key };
257
- const value = normalizeAtom(valueSource.join(""));
258
- return value ? { label: key, prompt: value } : undefined;
258
+ const atoms = atomSources.map((atom) =>
259
+ normalizeAtom(atom.join("")),
260
+ );
261
+ if (atoms.some((atom) => atom === undefined)) return undefined;
262
+ const [label, prompt, selectedStyle] = atoms as string[];
263
+ if (atoms.length === 1) return { value: label };
264
+ if (atoms.length === 2) return { label, prompt };
265
+ if (
266
+ selectedStyle !== "primary" &&
267
+ selectedStyle !== "success" &&
268
+ selectedStyle !== "danger"
269
+ ) return undefined;
270
+ return { label, prompt, selected_style: selectedStyle };
259
271
  }
260
- if (hasSeparator) valueSource.push(character);
261
- else keySource.push(character);
272
+ atomSources.at(-1)!.push(character);
262
273
  offset += 1;
263
274
  }
264
275
  return undefined;
265
276
  };
277
+ const parseJsonObjectCell = (): Record<string, unknown> | undefined => {
278
+ if (source[offset] !== "{") return undefined;
279
+ const start = offset;
280
+ const stack: string[] = [];
281
+ let inString = false;
282
+ let escaped = false;
283
+ for (let index = start; index < source.length; index += 1) {
284
+ const character = source[index]!;
285
+ if (inString) {
286
+ if (escaped) escaped = false;
287
+ else if (character === "\\") escaped = true;
288
+ else if (character === '"') inString = false;
289
+ continue;
290
+ }
291
+ if (character === '"') {
292
+ inString = true;
293
+ continue;
294
+ }
295
+ if (character === "{" || character === "[") {
296
+ stack.push(character);
297
+ continue;
298
+ }
299
+ if (character !== "}" && character !== "]") continue;
300
+ const opening = stack.pop();
301
+ if (
302
+ (character === "}" && opening !== "{") ||
303
+ (character === "]" && opening !== "[")
304
+ ) return undefined;
305
+ if (stack.length > 0) continue;
306
+ const candidate = source.slice(start, index + 1);
307
+ try {
308
+ const value: unknown = JSON.parse(candidate);
309
+ if (!isTelegramActionPayload(value)) return undefined;
310
+ offset = index + 1;
311
+ return value;
312
+ } catch {
313
+ return undefined;
314
+ }
315
+ }
316
+ return undefined;
317
+ };
318
+ const parseCell = (): Record<string, unknown> | undefined => {
319
+ const start = offset;
320
+ const jsonCell = parseJsonObjectCell();
321
+ if (jsonCell) return jsonCell;
322
+ offset = start;
323
+ return parseCompactCell();
324
+ };
266
325
  const parseRow = (): Record<string, unknown>[] | undefined => {
267
326
  if (source[offset] !== "[") return undefined;
268
327
  offset += 1;
@@ -277,6 +336,7 @@ function parseTelegramCompactActionPayloadRows(
277
336
  const cell = parseCell();
278
337
  if (!cell) return undefined;
279
338
  row.push(cell);
339
+ if (!consumeOptionalSeparator()) return undefined;
280
340
  }
281
341
  return undefined;
282
342
  };
@@ -295,15 +355,14 @@ function parseTelegramCompactActionPayloadRows(
295
355
  const cell = parseCell();
296
356
  if (!cell) return undefined;
297
357
  rows.push([cell]);
298
- continue;
299
- }
300
- if (character === "[") {
358
+ } else if (character === "[") {
301
359
  const row = parseRow();
302
360
  if (!row) return undefined;
303
361
  rows.push(row);
304
- continue;
362
+ } else {
363
+ return undefined;
305
364
  }
306
- return undefined;
365
+ if (!consumeOptionalSeparator()) return undefined;
307
366
  }
308
367
  return undefined;
309
368
  };
@@ -349,7 +408,7 @@ export function parseTelegramActionPayloadRows(
349
408
  }
350
409
  return rows;
351
410
  } catch {
352
- return parseTelegramCompactActionPayloadRows(payload.source);
411
+ return parseTelegramAdaptiveActionPayloadRows(payload.source);
353
412
  }
354
413
  }
355
414
  if (payload.hasBody) return undefined;
package/lib/outbound.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  import {
22
22
  planTelegramButtonReply,
23
23
  type TelegramButtonActionStore,
24
+ type TelegramOutboundButtonBinding,
24
25
  type TelegramOutboundButtonMarkup,
25
26
  } from "./outbound-buttons.ts";
26
27
  import {
@@ -824,6 +825,7 @@ export {
824
825
  type TelegramButtonCallbackQuery,
825
826
  type TelegramButtonReplyPlan,
826
827
  type TelegramOutboundButtonAction,
828
+ type TelegramOutboundButtonBinding,
827
829
  type TelegramOutboundButtonMarkup,
828
830
  type TelegramOutboundButtonStoredAction,
829
831
  } from "./outbound-buttons.ts";
@@ -832,10 +834,12 @@ export function createTelegramOutboundReplyPlanner(
832
834
  store: Pick<TelegramButtonActionStore, "register">,
833
835
  ): (
834
836
  markdown: string,
837
+ options?: { binding?: TelegramOutboundButtonBinding },
835
838
  ) => TelegramOutboundReplyPlan<TelegramOutboundButtonMarkup> {
836
- return (markdown) => {
839
+ return (markdown, options) => {
837
840
  const buttonReply = planTelegramButtonReply(markdown, {
838
841
  registerAction: store.register,
842
+ ...(options?.binding ? { binding: options.binding } : {}),
839
843
  });
840
844
 
841
845
  // Button replies can also contain <!-- telegram_voice --> markup
package/lib/prompts.ts CHANGED
@@ -33,6 +33,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
33
33
 
34
34
  const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
35
35
  "telegram_attach",
36
+ "telegram_bind",
36
37
  "telegram_message",
37
38
  ]);
38
39
  const TELEGRAM_MODEL_CONTEXT_MEMORY_KEY = Symbol.for(