@llblab/pi-telegram 0.17.5 → 0.18.0
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/AGENTS.md +67 -32
- package/BACKLOG.md +59 -19
- package/CHANGELOG.md +36 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/menu.ts
CHANGED
|
@@ -4,13 +4,6 @@
|
|
|
4
4
|
* Owns app-menu/status state, inline UI text, and callback composition while model/thinking/queue menu details live in dedicated domains
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
handleTelegramSectionCallback,
|
|
9
|
-
handleTelegramSectionOpen,
|
|
10
|
-
handleTelegramSectionSettingsOpen,
|
|
11
|
-
parseTelegramSectionCallback,
|
|
12
|
-
type TelegramSectionRegistry,
|
|
13
|
-
} from "./sections.ts";
|
|
14
7
|
import {
|
|
15
8
|
createTelegramModelMenuStateBuilder,
|
|
16
9
|
handleTelegramModelMenuCallbackAction,
|
|
@@ -39,6 +32,13 @@ import {
|
|
|
39
32
|
type ScopedTelegramModel,
|
|
40
33
|
type ThinkingLevel,
|
|
41
34
|
} from "./model.ts";
|
|
35
|
+
import {
|
|
36
|
+
handleTelegramSectionCallback,
|
|
37
|
+
handleTelegramSectionOpen,
|
|
38
|
+
handleTelegramSectionSettingsOpen,
|
|
39
|
+
parseTelegramSectionCallback,
|
|
40
|
+
type TelegramSectionRegistry,
|
|
41
|
+
} from "./sections.ts";
|
|
42
42
|
|
|
43
43
|
export {
|
|
44
44
|
applyTelegramModelPageSelection,
|
|
@@ -150,7 +150,11 @@ export interface TelegramMenuCallbackEntryDeps {
|
|
|
150
150
|
export interface MenuCallbackQuery {
|
|
151
151
|
id: string;
|
|
152
152
|
data?: string;
|
|
153
|
-
message?: {
|
|
153
|
+
message?: {
|
|
154
|
+
message_id?: number;
|
|
155
|
+
message_thread_id?: number;
|
|
156
|
+
chat?: { id?: number };
|
|
157
|
+
};
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
export interface StoredTelegramMenuCallbackDeps<
|
|
@@ -158,6 +162,7 @@ export interface StoredTelegramMenuCallbackDeps<
|
|
|
158
162
|
> {
|
|
159
163
|
getStoredModelMenuState: (
|
|
160
164
|
messageId: number | undefined,
|
|
165
|
+
chatId?: number,
|
|
161
166
|
) => TelegramModelMenuState<TModel> | undefined;
|
|
162
167
|
handleStatusAction: (
|
|
163
168
|
state: TelegramModelMenuState<TModel>,
|
|
@@ -180,6 +185,7 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
180
185
|
> {
|
|
181
186
|
getStoredModelMenuState: (
|
|
182
187
|
messageId: number | undefined,
|
|
188
|
+
chatId?: number,
|
|
183
189
|
) => TelegramModelMenuState<TModel> | undefined;
|
|
184
190
|
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
185
191
|
getThinkingLevel: () => ThinkingLevel;
|
|
@@ -236,8 +242,13 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
236
242
|
text: string,
|
|
237
243
|
mode: "markdown" | "html" | "plain",
|
|
238
244
|
replyMarkup: TelegramReplyMarkup,
|
|
245
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
239
246
|
) => Promise<number | undefined>;
|
|
240
|
-
enqueueSectionPrompt?: (
|
|
247
|
+
enqueueSectionPrompt?: (
|
|
248
|
+
prompt: string,
|
|
249
|
+
ctx: TContext,
|
|
250
|
+
target?: { chatId: number; threadId?: number },
|
|
251
|
+
) => Promise<void>;
|
|
241
252
|
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
|
|
242
253
|
isVoiceReplyActive?: () => boolean;
|
|
243
254
|
}
|
|
@@ -249,6 +260,7 @@ export interface TelegramMenuActionRuntimeDeps<
|
|
|
249
260
|
getModelMenuState: (
|
|
250
261
|
chatId: number,
|
|
251
262
|
ctx: TContext,
|
|
263
|
+
threadId?: number,
|
|
252
264
|
) => Promise<TelegramModelMenuState<TModel>>;
|
|
253
265
|
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
254
266
|
getThinkingLevel: () => ThinkingLevel;
|
|
@@ -261,6 +273,7 @@ export interface TelegramMenuActionRuntimeDeps<
|
|
|
261
273
|
chatId: number,
|
|
262
274
|
replyToMessageId: number,
|
|
263
275
|
text: string,
|
|
276
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
264
277
|
) => Promise<unknown>;
|
|
265
278
|
sectionRegistry?: TelegramSectionRegistry;
|
|
266
279
|
isVoiceReplyActive?: () => boolean;
|
|
@@ -286,11 +299,13 @@ export interface TelegramMenuActionRuntime<
|
|
|
286
299
|
chatId: number,
|
|
287
300
|
replyToMessageId: number,
|
|
288
301
|
ctx: TContext,
|
|
302
|
+
threadId?: number,
|
|
289
303
|
) => Promise<void>;
|
|
290
304
|
openModelMenu: (
|
|
291
305
|
chatId: number,
|
|
292
306
|
replyToMessageId: number,
|
|
293
307
|
ctx: TContext,
|
|
308
|
+
threadId?: number,
|
|
294
309
|
) => Promise<void>;
|
|
295
310
|
openThinkingMenu: (
|
|
296
311
|
chatId: number,
|
|
@@ -392,7 +407,10 @@ export async function handleStoredTelegramMenuCallback<
|
|
|
392
407
|
query: MenuCallbackQuery,
|
|
393
408
|
deps: StoredTelegramMenuCallbackDeps<TModel>,
|
|
394
409
|
): Promise<void> {
|
|
395
|
-
const state = deps.getStoredModelMenuState(
|
|
410
|
+
const state = deps.getStoredModelMenuState(
|
|
411
|
+
query.message?.message_id,
|
|
412
|
+
query.message?.chat?.id,
|
|
413
|
+
);
|
|
396
414
|
await handleTelegramMenuCallbackEntry(query.id, query.data, state, {
|
|
397
415
|
handleStatusAction: async () => {
|
|
398
416
|
if (!state) return false;
|
|
@@ -416,6 +434,7 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
416
434
|
> {
|
|
417
435
|
getStoredModelMenuState: (
|
|
418
436
|
messageId: number | undefined,
|
|
437
|
+
chatId?: number,
|
|
419
438
|
) => TelegramModelMenuState<TModel> | undefined;
|
|
420
439
|
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
421
440
|
getThinkingLevel: () => ThinkingLevel;
|
|
@@ -472,8 +491,13 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
472
491
|
text: string,
|
|
473
492
|
mode: "markdown" | "html" | "plain",
|
|
474
493
|
replyMarkup: TelegramReplyMarkup,
|
|
494
|
+
options?: { target?: { chatId: number; threadId?: number } },
|
|
475
495
|
) => Promise<number | undefined>;
|
|
476
|
-
enqueueSectionPrompt?: (
|
|
496
|
+
enqueueSectionPrompt?: (
|
|
497
|
+
prompt: string,
|
|
498
|
+
ctx: TContext,
|
|
499
|
+
target?: { chatId: number; threadId?: number },
|
|
500
|
+
) => Promise<void>;
|
|
477
501
|
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
|
|
478
502
|
}
|
|
479
503
|
|
|
@@ -532,7 +556,10 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
532
556
|
deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>,
|
|
533
557
|
): Promise<void> {
|
|
534
558
|
if (query.data === "menu:back") {
|
|
535
|
-
const state = deps.getStoredModelMenuState(
|
|
559
|
+
const state = deps.getStoredModelMenuState(
|
|
560
|
+
query.message?.message_id,
|
|
561
|
+
query.message?.chat?.id,
|
|
562
|
+
);
|
|
536
563
|
if (!state) {
|
|
537
564
|
await deps.answerCallbackQuery(query.id, "Interactive message expired.");
|
|
538
565
|
return;
|
|
@@ -545,14 +572,19 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
545
572
|
if (deps.sectionRegistry && query.data?.startsWith("section:")) {
|
|
546
573
|
const parsed = parseTelegramSectionCallback(query.data);
|
|
547
574
|
if (parsed) {
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
const messageId =
|
|
551
|
-
|
|
575
|
+
const message = query.message;
|
|
576
|
+
const chatId = message?.chat?.id;
|
|
577
|
+
const messageId = message?.message_id;
|
|
578
|
+
const target =
|
|
579
|
+
typeof chatId === "number"
|
|
580
|
+
? typeof message?.message_thread_id === "number"
|
|
581
|
+
? { chatId, threadId: message.message_thread_id }
|
|
582
|
+
: { chatId }
|
|
583
|
+
: undefined;
|
|
552
584
|
if (typeof chatId === "number" && typeof messageId === "number") {
|
|
553
585
|
const { token, action, payload } = parsed;
|
|
554
586
|
if (action === "open") {
|
|
555
|
-
const state = deps.getStoredModelMenuState(messageId);
|
|
587
|
+
const state = deps.getStoredModelMenuState(messageId, chatId);
|
|
556
588
|
if (!state) {
|
|
557
589
|
await deps.answerCallbackQuery(
|
|
558
590
|
query.id,
|
|
@@ -568,12 +600,14 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
568
600
|
query.id,
|
|
569
601
|
{
|
|
570
602
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
603
|
+
target,
|
|
571
604
|
editInteractiveMessage:
|
|
572
605
|
deps.editInteractiveMessage ?? (async () => {}),
|
|
573
606
|
sendInteractiveMessage:
|
|
574
607
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
575
608
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
576
|
-
? (prompt: string) =>
|
|
609
|
+
? (prompt: string) =>
|
|
610
|
+
deps.enqueueSectionPrompt!(prompt, ctx, target)
|
|
577
611
|
: async () => {},
|
|
578
612
|
deleteMessage: deps.deleteMessage ?? (async () => {}),
|
|
579
613
|
},
|
|
@@ -589,12 +623,14 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
589
623
|
query.id,
|
|
590
624
|
{
|
|
591
625
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
626
|
+
target,
|
|
592
627
|
editInteractiveMessage:
|
|
593
628
|
deps.editInteractiveMessage ?? (async () => {}),
|
|
594
629
|
sendInteractiveMessage:
|
|
595
630
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
596
631
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
597
|
-
? (prompt: string) =>
|
|
632
|
+
? (prompt: string) =>
|
|
633
|
+
deps.enqueueSectionPrompt!(prompt, ctx, target)
|
|
598
634
|
: async () => {},
|
|
599
635
|
deleteMessage: deps.deleteMessage ?? (async () => {}),
|
|
600
636
|
},
|
|
@@ -612,12 +648,14 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
612
648
|
query.id,
|
|
613
649
|
{
|
|
614
650
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
651
|
+
target,
|
|
615
652
|
editInteractiveMessage:
|
|
616
653
|
deps.editInteractiveMessage ?? (async () => {}),
|
|
617
654
|
sendInteractiveMessage:
|
|
618
655
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
619
656
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
620
|
-
? (prompt: string) =>
|
|
657
|
+
? (prompt: string) =>
|
|
658
|
+
deps.enqueueSectionPrompt!(prompt, ctx, target)
|
|
621
659
|
: async () => {},
|
|
622
660
|
deleteMessage: deps.deleteMessage ?? (async () => {}),
|
|
623
661
|
},
|
|
@@ -770,17 +808,18 @@ export function createTelegramMenuActionRuntime<
|
|
|
770
808
|
deps.sectionRegistry,
|
|
771
809
|
deps.isVoiceReplyActive?.(),
|
|
772
810
|
),
|
|
773
|
-
sendStatusMessage: (chatId, replyToMessageId, ctx) =>
|
|
811
|
+
sendStatusMessage: (chatId, replyToMessageId, ctx, threadId) =>
|
|
774
812
|
openTelegramStatusMenu({
|
|
775
813
|
isIdle: () => deps.isIdle(ctx),
|
|
776
814
|
sendBusyMessage: async () => {
|
|
777
815
|
await deps.sendTextReply(
|
|
778
816
|
chatId,
|
|
779
817
|
replyToMessageId,
|
|
780
|
-
"Cannot open status while
|
|
818
|
+
"Cannot open status while Pi is busy. Send /abort, /next, or /stop.",
|
|
819
|
+
{ target: { chatId, threadId } },
|
|
781
820
|
);
|
|
782
821
|
},
|
|
783
|
-
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
|
|
822
|
+
getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
|
|
784
823
|
buildStatusHtml: () => deps.buildStatusHtml(ctx),
|
|
785
824
|
getActiveModel: () => deps.getActiveModel(ctx),
|
|
786
825
|
getThinkingLevel: deps.getThinkingLevel,
|
|
@@ -804,7 +843,7 @@ export function createTelegramMenuActionRuntime<
|
|
|
804
843
|
),
|
|
805
844
|
storeModelMenuState: deps.storeModelMenuState,
|
|
806
845
|
}),
|
|
807
|
-
openModelMenu: (chatId, replyToMessageId, ctx) =>
|
|
846
|
+
openModelMenu: (chatId, replyToMessageId, ctx, threadId) =>
|
|
808
847
|
openTelegramModelMenu({
|
|
809
848
|
isIdle: () => deps.isIdle(ctx),
|
|
810
849
|
canOfferInFlightModelSwitch: () =>
|
|
@@ -813,7 +852,8 @@ export function createTelegramMenuActionRuntime<
|
|
|
813
852
|
await deps.sendTextReply(
|
|
814
853
|
chatId,
|
|
815
854
|
replyToMessageId,
|
|
816
|
-
"Cannot switch model while
|
|
855
|
+
"Cannot switch model while Pi is busy. Send /abort, /next, or /stop.",
|
|
856
|
+
{ target: { chatId, threadId } },
|
|
817
857
|
);
|
|
818
858
|
},
|
|
819
859
|
sendNoModelsMessage: async () => {
|
|
@@ -821,9 +861,10 @@ export function createTelegramMenuActionRuntime<
|
|
|
821
861
|
chatId,
|
|
822
862
|
replyToMessageId,
|
|
823
863
|
"No available models with configured auth.",
|
|
864
|
+
{ target: { chatId, threadId } },
|
|
824
865
|
);
|
|
825
866
|
},
|
|
826
|
-
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
|
|
867
|
+
getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
|
|
827
868
|
getActiveModel: () => deps.getActiveModel(ctx),
|
|
828
869
|
sendModelMenu: (state, activeModel) =>
|
|
829
870
|
sendTelegramModelMenuMessage(state, activeModel, deps),
|
|
@@ -11,6 +11,10 @@ import { Type } from "@sinclair/typebox";
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "./pi.ts";
|
|
13
13
|
import { buildTelegramMultipartReplyParameters } from "./replies.ts";
|
|
14
|
+
import {
|
|
15
|
+
getTelegramTargetThreadParams,
|
|
16
|
+
type TelegramTarget,
|
|
17
|
+
} from "./target.ts";
|
|
14
18
|
|
|
15
19
|
const MAX_ATTACHMENTS_PER_TURN = 10;
|
|
16
20
|
|
|
@@ -54,6 +58,7 @@ export interface TelegramOutboundAttachmentToolRegistrationDeps extends Telegram
|
|
|
54
58
|
maxAttachmentSizeBytes?: number;
|
|
55
59
|
getActiveTurn: () => TelegramOutboundAttachmentQueueTargetView | undefined;
|
|
56
60
|
getDefaultChatId?: () => number | undefined;
|
|
61
|
+
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
57
62
|
canSendDirect?: () => boolean;
|
|
58
63
|
sendMultipart?: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
59
64
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
@@ -66,12 +71,13 @@ export interface TelegramOutboundMessagePlan {
|
|
|
66
71
|
|
|
67
72
|
export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
|
|
68
73
|
getDefaultChatId: () => number | undefined;
|
|
74
|
+
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
69
75
|
canSendDirect: () => boolean;
|
|
70
76
|
planMessage: (markdown: string) => TelegramOutboundMessagePlan;
|
|
71
77
|
sendMarkdownMessage: (
|
|
72
78
|
chatId: number,
|
|
73
79
|
markdown: string,
|
|
74
|
-
options?: { replyMarkup?: unknown },
|
|
80
|
+
options?: { replyMarkup?: unknown; target?: TelegramTarget },
|
|
75
81
|
) => Promise<number | undefined>;
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -87,6 +93,7 @@ export interface TelegramOutboundAttachmentQueueTargetView {
|
|
|
87
93
|
export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
|
|
88
94
|
chatId: number;
|
|
89
95
|
replyToMessageId: number;
|
|
96
|
+
target?: TelegramTarget;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
|
|
@@ -113,7 +120,7 @@ function formatTelegramOutboundAttachmentToolResultText(
|
|
|
113
120
|
count: number,
|
|
114
121
|
mode: "queued" | "sent" = "queued",
|
|
115
122
|
): string {
|
|
116
|
-
// Pi's compact tool rows need
|
|
123
|
+
// Pi's compact tool rows need one leading newline to visually separate header and result.
|
|
117
124
|
const verb = mode === "queued" ? "Queued" : "Sent";
|
|
118
125
|
return ["", `${verb} ${count} Telegram attachment(s).`].join("\n");
|
|
119
126
|
}
|
|
@@ -122,26 +129,55 @@ function formatTelegramOutboundMessageToolResultText(chatId: number): string {
|
|
|
122
129
|
return ["", `Sent Telegram message to ${chatId}.`].join("\n");
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
function getTelegramMultipartTargetFields(
|
|
133
|
+
target: TelegramTarget | undefined,
|
|
134
|
+
): Record<string, string> {
|
|
135
|
+
if (!target) return {};
|
|
136
|
+
return Object.fromEntries(
|
|
137
|
+
Object.entries(getTelegramTargetThreadParams(target)).map(
|
|
138
|
+
([key, value]) => [key, String(value)],
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
function assertTelegramDirectDeliveryAllowed(
|
|
126
144
|
canSendDirect: (() => boolean) | undefined,
|
|
127
145
|
): void {
|
|
128
146
|
if (canSendDirect?.()) return;
|
|
129
147
|
throw new Error(
|
|
130
|
-
"Telegram direct delivery requires this
|
|
148
|
+
"Telegram direct delivery requires this Pi instance to own /telegram-connect or be registered with the Telegram multi-instance bus",
|
|
131
149
|
);
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
function
|
|
152
|
+
function resolveTelegramOutboundTarget(options: {
|
|
135
153
|
chatId?: number;
|
|
154
|
+
threadId?: number;
|
|
155
|
+
target?: TelegramTarget;
|
|
136
156
|
getDefaultChatId?: () => number | undefined;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
157
|
+
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
158
|
+
}): { chatId: number; target?: TelegramTarget } {
|
|
159
|
+
if (options.target)
|
|
160
|
+
return { chatId: options.target.chatId, target: options.target };
|
|
161
|
+
if (options.chatId !== undefined) {
|
|
162
|
+
return {
|
|
163
|
+
chatId: options.chatId,
|
|
164
|
+
target:
|
|
165
|
+
options.threadId !== undefined
|
|
166
|
+
? { chatId: options.chatId, threadId: options.threadId }
|
|
167
|
+
: undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const defaultTarget = options.getDefaultTarget?.();
|
|
171
|
+
if (defaultTarget) {
|
|
172
|
+
return { chatId: defaultTarget.chatId, target: defaultTarget };
|
|
173
|
+
}
|
|
174
|
+
const defaultChatId = options.getDefaultChatId?.();
|
|
175
|
+
if (defaultChatId === undefined) {
|
|
140
176
|
throw new Error(
|
|
141
177
|
"Telegram chat_id is required when no paired/default Telegram chat is available",
|
|
142
178
|
);
|
|
143
179
|
}
|
|
144
|
-
return chatId;
|
|
180
|
+
return { chatId: defaultChatId };
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
async function buildTelegramOutboundAttachmentViews(options: {
|
|
@@ -194,6 +230,7 @@ export function registerTelegramOutboundAttachmentTool(
|
|
|
194
230
|
promptGuidelines: [
|
|
195
231
|
"When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
|
|
196
232
|
"When a local/TUI user explicitly asks to send a generated file to Telegram, telegram_attach can deliver it to the paired/default Telegram chat even without an active Telegram turn.",
|
|
233
|
+
"For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
|
|
197
234
|
],
|
|
198
235
|
parameters: Type.Object({
|
|
199
236
|
paths: Type.Array(
|
|
@@ -206,6 +243,12 @@ export function registerTelegramOutboundAttachmentTool(
|
|
|
206
243
|
"Optional Telegram chat id for immediate delivery when no Telegram turn is active",
|
|
207
244
|
}),
|
|
208
245
|
),
|
|
246
|
+
thread_id: Type.Optional(
|
|
247
|
+
Type.Number({
|
|
248
|
+
description:
|
|
249
|
+
"Optional Telegram topic thread id for immediate delivery with chat_id",
|
|
250
|
+
}),
|
|
251
|
+
),
|
|
209
252
|
caption: Type.Optional(
|
|
210
253
|
Type.String({
|
|
211
254
|
description:
|
|
@@ -219,11 +262,13 @@ export function registerTelegramOutboundAttachmentTool(
|
|
|
219
262
|
activeTurn: deps.getActiveTurn(),
|
|
220
263
|
paths: params.paths,
|
|
221
264
|
chatId: params.chat_id,
|
|
265
|
+
threadId: params.thread_id,
|
|
222
266
|
caption: params.caption,
|
|
223
267
|
maxAttachmentsPerTurn,
|
|
224
268
|
maxAttachmentSizeBytes,
|
|
225
269
|
sendMultipart: deps.sendMultipart,
|
|
226
270
|
getDefaultChatId: deps.getDefaultChatId,
|
|
271
|
+
getDefaultTarget: deps.getDefaultTarget,
|
|
227
272
|
canSendDirect: deps.canSendDirect,
|
|
228
273
|
statPath: deps.statPath,
|
|
229
274
|
});
|
|
@@ -251,6 +296,7 @@ export function registerTelegramOutboundMessageTool(
|
|
|
251
296
|
"Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.",
|
|
252
297
|
promptGuidelines: [
|
|
253
298
|
"Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
|
|
299
|
+
"For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
|
|
254
300
|
"Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
255
301
|
"Do not use this tool for ordinary Telegram-originated replies; answer normally so the bridge can deliver the active turn reply.",
|
|
256
302
|
],
|
|
@@ -259,13 +305,20 @@ export function registerTelegramOutboundMessageTool(
|
|
|
259
305
|
chat_id: Type.Optional(
|
|
260
306
|
Type.Number({ description: "Optional Telegram chat id" }),
|
|
261
307
|
),
|
|
308
|
+
thread_id: Type.Optional(
|
|
309
|
+
Type.Number({
|
|
310
|
+
description: "Optional Telegram topic thread id with chat_id",
|
|
311
|
+
}),
|
|
312
|
+
),
|
|
262
313
|
}),
|
|
263
314
|
async execute(_toolCallId, params) {
|
|
264
315
|
try {
|
|
265
316
|
return await sendTelegramOutboundMessage({
|
|
266
317
|
text: params.text,
|
|
267
318
|
chatId: params.chat_id,
|
|
319
|
+
threadId: params.thread_id,
|
|
268
320
|
getDefaultChatId: deps.getDefaultChatId,
|
|
321
|
+
getDefaultTarget: deps.getDefaultTarget,
|
|
269
322
|
canSendDirect: deps.canSendDirect,
|
|
270
323
|
planMessage: deps.planMessage,
|
|
271
324
|
sendMarkdownMessage: deps.sendMarkdownMessage,
|
|
@@ -290,6 +343,7 @@ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
|
|
|
290
343
|
chatId: number,
|
|
291
344
|
replyToMessageId: number,
|
|
292
345
|
text: string,
|
|
346
|
+
options?: { target?: TelegramTarget },
|
|
293
347
|
) => Promise<unknown>;
|
|
294
348
|
recordRuntimeEvent?: (
|
|
295
349
|
category: string,
|
|
@@ -304,11 +358,13 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
304
358
|
activeTurn: TelegramOutboundAttachmentQueueTargetView | undefined;
|
|
305
359
|
paths: string[];
|
|
306
360
|
chatId?: number;
|
|
361
|
+
threadId?: number;
|
|
307
362
|
caption?: string;
|
|
308
363
|
maxAttachmentsPerTurn: number;
|
|
309
364
|
maxAttachmentSizeBytes?: number;
|
|
310
365
|
sendMultipart?: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
311
366
|
getDefaultChatId?: () => number | undefined;
|
|
367
|
+
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
312
368
|
canSendDirect?: () => boolean;
|
|
313
369
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
314
370
|
}): Promise<TelegramOutboundAttachmentToolResult> {
|
|
@@ -321,11 +377,13 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
321
377
|
return sendTelegramOutboundFiles({
|
|
322
378
|
paths: options.paths,
|
|
323
379
|
chatId: options.chatId,
|
|
380
|
+
threadId: options.threadId,
|
|
324
381
|
caption: options.caption,
|
|
325
382
|
maxAttachmentsPerTurn: options.maxAttachmentsPerTurn,
|
|
326
383
|
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
327
384
|
sendMultipart: options.sendMultipart,
|
|
328
385
|
getDefaultChatId: options.getDefaultChatId,
|
|
386
|
+
getDefaultTarget: options.getDefaultTarget,
|
|
329
387
|
canSendDirect: options.canSendDirect,
|
|
330
388
|
statPath: options.statPath,
|
|
331
389
|
});
|
|
@@ -359,29 +417,41 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
359
417
|
export async function sendTelegramOutboundMessage(options: {
|
|
360
418
|
text: string;
|
|
361
419
|
chatId?: number;
|
|
420
|
+
threadId?: number;
|
|
421
|
+
target?: TelegramTarget;
|
|
362
422
|
getDefaultChatId?: () => number | undefined;
|
|
423
|
+
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
363
424
|
canSendDirect: () => boolean;
|
|
364
425
|
planMessage: (markdown: string) => TelegramOutboundMessagePlan;
|
|
365
426
|
sendMarkdownMessage: (
|
|
366
427
|
chatId: number,
|
|
367
428
|
markdown: string,
|
|
368
|
-
options?: { replyMarkup?: unknown },
|
|
429
|
+
options?: { replyMarkup?: unknown; target?: TelegramTarget },
|
|
369
430
|
) => Promise<number | undefined>;
|
|
370
431
|
}): Promise<{
|
|
371
432
|
content: Array<{ type: "text"; text: string }>;
|
|
372
433
|
details: { chatId: number; messageId?: number };
|
|
373
434
|
}> {
|
|
374
435
|
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
375
|
-
const chatId =
|
|
436
|
+
const { chatId, target } = resolveTelegramOutboundTarget({
|
|
376
437
|
chatId: options.chatId,
|
|
438
|
+
threadId: options.threadId,
|
|
439
|
+
target: options.target,
|
|
377
440
|
getDefaultChatId: options.getDefaultChatId,
|
|
441
|
+
getDefaultTarget: options.getDefaultTarget,
|
|
378
442
|
});
|
|
379
443
|
const plan = options.planMessage(options.text);
|
|
380
444
|
const messageId = await options.sendMarkdownMessage(chatId, plan.markdown, {
|
|
381
445
|
replyMarkup: plan.replyMarkup,
|
|
446
|
+
target,
|
|
382
447
|
});
|
|
383
448
|
return {
|
|
384
|
-
content: [
|
|
449
|
+
content: [
|
|
450
|
+
{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: formatTelegramOutboundMessageToolResultText(chatId),
|
|
453
|
+
},
|
|
454
|
+
],
|
|
385
455
|
details: { chatId, messageId },
|
|
386
456
|
};
|
|
387
457
|
}
|
|
@@ -389,23 +459,33 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
389
459
|
export async function sendTelegramOutboundFiles(options: {
|
|
390
460
|
paths: string[];
|
|
391
461
|
chatId?: number;
|
|
462
|
+
threadId?: number;
|
|
463
|
+
target?: TelegramTarget;
|
|
392
464
|
caption?: string;
|
|
393
465
|
maxAttachmentsPerTurn: number;
|
|
394
466
|
maxAttachmentSizeBytes?: number;
|
|
395
467
|
sendMultipart: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
396
468
|
getDefaultChatId?: () => number | undefined;
|
|
469
|
+
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
397
470
|
canSendDirect?: () => boolean;
|
|
398
471
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
399
|
-
}): Promise<
|
|
472
|
+
}): Promise<
|
|
473
|
+
TelegramOutboundAttachmentToolResult & {
|
|
474
|
+
details: { paths: string[]; chatId: number };
|
|
475
|
+
}
|
|
476
|
+
> {
|
|
400
477
|
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
401
478
|
if (options.paths.length > options.maxAttachmentsPerTurn) {
|
|
402
479
|
throw new Error(
|
|
403
480
|
`Attachment limit reached (${options.maxAttachmentsPerTurn})`,
|
|
404
481
|
);
|
|
405
482
|
}
|
|
406
|
-
const chatId =
|
|
483
|
+
const { chatId, target } = resolveTelegramOutboundTarget({
|
|
407
484
|
chatId: options.chatId,
|
|
485
|
+
threadId: options.threadId,
|
|
486
|
+
target: options.target,
|
|
408
487
|
getDefaultChatId: options.getDefaultChatId,
|
|
488
|
+
getDefaultTarget: options.getDefaultTarget,
|
|
409
489
|
});
|
|
410
490
|
const pendingAttachments = await buildTelegramOutboundAttachmentViews({
|
|
411
491
|
paths: options.paths,
|
|
@@ -421,6 +501,7 @@ export async function sendTelegramOutboundFiles(options: {
|
|
|
421
501
|
{
|
|
422
502
|
chat_id: String(chatId),
|
|
423
503
|
...(options.caption && index === 0 ? { caption: options.caption } : {}),
|
|
504
|
+
...getTelegramMultipartTargetFields(target),
|
|
424
505
|
},
|
|
425
506
|
fieldName,
|
|
426
507
|
attachment.path,
|
|
@@ -432,7 +513,10 @@ export async function sendTelegramOutboundFiles(options: {
|
|
|
432
513
|
content: [
|
|
433
514
|
{
|
|
434
515
|
type: "text",
|
|
435
|
-
text: formatTelegramOutboundAttachmentToolResultText(
|
|
516
|
+
text: formatTelegramOutboundAttachmentToolResultText(
|
|
517
|
+
added.length,
|
|
518
|
+
"sent",
|
|
519
|
+
),
|
|
436
520
|
},
|
|
437
521
|
],
|
|
438
522
|
details: { paths: added, chatId },
|
|
@@ -442,9 +526,7 @@ export async function sendTelegramOutboundFiles(options: {
|
|
|
442
526
|
export function createTelegramQueuedOutboundAttachmentSender(
|
|
443
527
|
deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
|
|
444
528
|
) {
|
|
445
|
-
return async
|
|
446
|
-
turn: TelegramQueuedOutboundAttachmentTurnView,
|
|
447
|
-
): Promise<void> {
|
|
529
|
+
return async (turn: TelegramQueuedOutboundAttachmentTurnView): Promise<void> => {
|
|
448
530
|
await sendQueuedTelegramOutboundAttachments(turn, {
|
|
449
531
|
...deps,
|
|
450
532
|
maxAttachmentSizeBytes:
|
|
@@ -476,12 +558,14 @@ export async function sendQueuedTelegramOutboundAttachments(
|
|
|
476
558
|
const replyParameters = buildTelegramMultipartReplyParameters(
|
|
477
559
|
turn.chatId,
|
|
478
560
|
turn.replyToMessageId,
|
|
561
|
+
turn.target,
|
|
479
562
|
);
|
|
480
563
|
await deps.sendMultipart(
|
|
481
564
|
method,
|
|
482
565
|
{
|
|
483
566
|
chat_id: String(turn.chatId),
|
|
484
567
|
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
568
|
+
...getTelegramMultipartTargetFields(turn.target),
|
|
485
569
|
},
|
|
486
570
|
fieldName,
|
|
487
571
|
attachment.path,
|
|
@@ -496,6 +580,7 @@ export async function sendQueuedTelegramOutboundAttachments(
|
|
|
496
580
|
turn.chatId,
|
|
497
581
|
turn.replyToMessageId,
|
|
498
582
|
`Failed to send attachment ${attachment.fileName}: ${message}`,
|
|
583
|
+
{ target: turn.target },
|
|
499
584
|
);
|
|
500
585
|
}
|
|
501
586
|
}
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./outbound-markup.ts";
|
|
15
15
|
import {
|
|
16
16
|
type PendingTelegramTurn,
|
|
17
|
+
type TelegramQueueTarget,
|
|
17
18
|
truncateTelegramQueueSummary,
|
|
18
19
|
} from "./queue.ts";
|
|
19
20
|
|
|
@@ -48,6 +49,7 @@ export interface TelegramButtonCallbackQuery {
|
|
|
48
49
|
data?: string;
|
|
49
50
|
message?: {
|
|
50
51
|
message_id?: number;
|
|
52
|
+
message_thread_id?: number;
|
|
51
53
|
chat?: { id?: number };
|
|
52
54
|
};
|
|
53
55
|
}
|
|
@@ -114,11 +116,11 @@ export function createTelegramButtonActionStore(
|
|
|
114
116
|
): TelegramButtonActionStore {
|
|
115
117
|
const ttlMs = options.ttlMs ?? TELEGRAM_BUTTON_ACTION_TTL_MS;
|
|
116
118
|
const actions = new Map<string, TelegramOutboundButtonStoredAction>();
|
|
117
|
-
|
|
119
|
+
const cleanup = (currentTime: number): void => {
|
|
118
120
|
for (const [key, action] of actions) {
|
|
119
121
|
if (currentTime - action.createdAt > ttlMs) actions.delete(key);
|
|
120
122
|
}
|
|
121
|
-
}
|
|
123
|
+
};
|
|
122
124
|
return {
|
|
123
125
|
register: (action) => {
|
|
124
126
|
const currentTime = nowMs();
|
|
@@ -180,11 +182,13 @@ export function createTelegramButtonPromptTurn(options: {
|
|
|
180
182
|
replyToMessageId: number;
|
|
181
183
|
queueOrder: number;
|
|
182
184
|
action: TelegramOutboundButtonAction;
|
|
185
|
+
target?: TelegramQueueTarget;
|
|
183
186
|
}): PendingTelegramTurn {
|
|
184
187
|
const prompt = `[telegram] ${options.action.prompt}`;
|
|
185
188
|
return {
|
|
186
189
|
kind: "prompt",
|
|
187
190
|
chatId: options.chatId,
|
|
191
|
+
...(options.target ? { target: options.target } : {}),
|
|
188
192
|
replyToMessageId: options.replyToMessageId,
|
|
189
193
|
sourceMessageIds: [options.replyToMessageId],
|
|
190
194
|
queueOrder: options.queueOrder,
|