@llblab/pi-telegram 0.6.3 → 0.7.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 +152 -0
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +142 -0
- package/README.md +39 -38
- package/docs/architecture.md +36 -27
- package/docs/attachment-handlers.md +4 -6
- package/docs/command-templates.md +53 -9
- package/docs/locks.md +4 -0
- package/docs/outbound-handlers.md +6 -5
- package/index.ts +59 -7
- package/lib/api.ts +1 -0
- package/lib/attachment-handlers.ts +16 -9
- package/lib/attachments.ts +1 -0
- package/lib/command-templates.ts +32 -3
- package/lib/commands.ts +367 -80
- package/lib/config.ts +6 -2
- package/lib/keyboard.ts +14 -0
- package/lib/lifecycle.ts +26 -0
- package/lib/locks.ts +3 -2
- package/lib/media.ts +1 -0
- package/lib/menu-model.ts +881 -0
- package/lib/menu-queue.ts +608 -0
- package/lib/menu-status.ts +226 -0
- package/lib/menu-thinking.ts +171 -0
- package/lib/menu.ts +143 -1019
- package/lib/model.ts +1 -0
- package/lib/outbound-handlers.ts +26 -19
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +1 -0
- package/lib/preview.ts +97 -50
- package/lib/prompt-templates.ts +150 -0
- package/lib/prompts.ts +1 -0
- package/lib/queue.ts +51 -15
- package/lib/rendering.ts +1 -0
- package/lib/replies.ts +86 -2
- package/lib/routing.ts +76 -14
- package/lib/runtime.ts +2 -0
- package/lib/setup.ts +1 -0
- package/lib/status.ts +15 -6
- package/lib/turns.ts +1 -0
- package/lib/updates.ts +36 -6
- package/package.json +4 -1
package/lib/queue.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram queue and
|
|
3
|
-
*
|
|
2
|
+
* Telegram queue core contracts and pure planning helpers
|
|
3
|
+
* Zones: telegram queue, pi agent lifecycle, scheduling
|
|
4
|
+
* Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
// --- Queue Items ---
|
|
@@ -178,6 +179,14 @@ export function createTelegramQueueStore<TContext = unknown>(
|
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
export function createTelegramQueueItemCountGetter<TContext = unknown>(
|
|
183
|
+
store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
|
|
184
|
+
): () => number {
|
|
185
|
+
return function getTelegramQueueItemCount() {
|
|
186
|
+
return store.getQueuedItems().length;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
181
190
|
export function createTelegramActiveTurnStore<
|
|
182
191
|
TTurn extends PendingTelegramTurn = PendingTelegramTurn,
|
|
183
192
|
>(): TelegramActiveTurnStore<TTurn> {
|
|
@@ -344,7 +353,7 @@ function formatTelegramQueueItemStatusSummary<TContext = unknown>(
|
|
|
344
353
|
item: TelegramQueueItem<TContext>,
|
|
345
354
|
): string {
|
|
346
355
|
if (item.queueLane === "priority") {
|
|
347
|
-
return
|
|
356
|
+
return `⚡ ${item.statusSummary}`;
|
|
348
357
|
}
|
|
349
358
|
return item.statusSummary;
|
|
350
359
|
}
|
|
@@ -665,18 +674,32 @@ export type TelegramAgentLifecycleHooksRuntimeDeps<
|
|
|
665
674
|
TTurn extends PendingTelegramTurn,
|
|
666
675
|
TContext,
|
|
667
676
|
TMessage,
|
|
677
|
+
TReplyMarkup = unknown,
|
|
668
678
|
> = TelegramAgentStartHookRuntimeDeps<TTurn, TContext> &
|
|
669
|
-
TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage> &
|
|
679
|
+
TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup> &
|
|
670
680
|
TelegramToolExecutionHookRuntimeDeps<TContext>;
|
|
671
681
|
|
|
672
682
|
export function createTelegramAgentLifecycleHooks<
|
|
673
683
|
TTurn extends PendingTelegramTurn,
|
|
674
684
|
TContext,
|
|
675
685
|
TMessage,
|
|
676
|
-
|
|
686
|
+
TReplyMarkup = unknown,
|
|
687
|
+
>(
|
|
688
|
+
deps: TelegramAgentLifecycleHooksRuntimeDeps<
|
|
689
|
+
TTurn,
|
|
690
|
+
TContext,
|
|
691
|
+
TMessage,
|
|
692
|
+
TReplyMarkup
|
|
693
|
+
>,
|
|
694
|
+
) {
|
|
677
695
|
return {
|
|
678
696
|
onAgentStart: createTelegramAgentStartHook<TTurn, TContext>(deps),
|
|
679
|
-
onAgentEnd: createTelegramAgentEndHook<
|
|
697
|
+
onAgentEnd: createTelegramAgentEndHook<
|
|
698
|
+
TTurn,
|
|
699
|
+
TContext,
|
|
700
|
+
TMessage,
|
|
701
|
+
TReplyMarkup
|
|
702
|
+
>(deps),
|
|
680
703
|
...createTelegramToolExecutionHooks<TContext>(deps),
|
|
681
704
|
};
|
|
682
705
|
}
|
|
@@ -737,6 +760,7 @@ export interface TelegramAgentEndOutboundReplyPlan<TReplyMarkup = unknown> {
|
|
|
737
760
|
|
|
738
761
|
export interface TelegramAgentEndRuntimeDeps<
|
|
739
762
|
TTurn extends PendingTelegramTurn,
|
|
763
|
+
TReplyMarkup = unknown,
|
|
740
764
|
> {
|
|
741
765
|
turn: TTurn | undefined;
|
|
742
766
|
assistant: TelegramAgentEndAssistantResult;
|
|
@@ -750,13 +774,13 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
750
774
|
chatId: number,
|
|
751
775
|
markdown: string,
|
|
752
776
|
replyToMessageId: number,
|
|
753
|
-
options?: { replyMarkup?:
|
|
777
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
754
778
|
) => Promise<boolean>;
|
|
755
779
|
sendMarkdownReply: (
|
|
756
780
|
chatId: number,
|
|
757
781
|
replyToMessageId: number,
|
|
758
782
|
markdown: string,
|
|
759
|
-
options?: { replyMarkup?:
|
|
783
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
760
784
|
) => Promise<unknown>;
|
|
761
785
|
sendTextReply: (
|
|
762
786
|
chatId: number,
|
|
@@ -764,7 +788,9 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
764
788
|
text: string,
|
|
765
789
|
) => Promise<unknown>;
|
|
766
790
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
767
|
-
planOutboundReply?: (
|
|
791
|
+
planOutboundReply?: (
|
|
792
|
+
markdown: string,
|
|
793
|
+
) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
|
|
768
794
|
sendOutboundReplyArtifacts?: (
|
|
769
795
|
turn: TTurn,
|
|
770
796
|
plan: TelegramAgentEndOutboundReplyPlan,
|
|
@@ -776,6 +802,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
776
802
|
TTurn extends PendingTelegramTurn,
|
|
777
803
|
TContext,
|
|
778
804
|
TMessage,
|
|
805
|
+
TReplyMarkup = unknown,
|
|
779
806
|
> {
|
|
780
807
|
getActiveTurn: () => TTurn | undefined;
|
|
781
808
|
extractAssistant: (
|
|
@@ -790,11 +817,11 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
790
817
|
) => void;
|
|
791
818
|
clearPreview: (chatId: number) => Promise<void>;
|
|
792
819
|
setPreviewPendingText: (text: string) => void;
|
|
793
|
-
finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn>["finalizeMarkdownPreview"];
|
|
794
|
-
sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn>["sendMarkdownReply"];
|
|
820
|
+
finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["finalizeMarkdownPreview"];
|
|
821
|
+
sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["sendMarkdownReply"];
|
|
795
822
|
sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
|
|
796
823
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
797
|
-
planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn>["planOutboundReply"];
|
|
824
|
+
planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["planOutboundReply"];
|
|
798
825
|
sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
|
|
799
826
|
}
|
|
800
827
|
|
|
@@ -872,7 +899,15 @@ export function createTelegramAgentEndHook<
|
|
|
872
899
|
TTurn extends PendingTelegramTurn,
|
|
873
900
|
TContext,
|
|
874
901
|
TMessage,
|
|
875
|
-
|
|
902
|
+
TReplyMarkup = unknown,
|
|
903
|
+
>(
|
|
904
|
+
deps: TelegramAgentEndHookRuntimeDeps<
|
|
905
|
+
TTurn,
|
|
906
|
+
TContext,
|
|
907
|
+
TMessage,
|
|
908
|
+
TReplyMarkup
|
|
909
|
+
>,
|
|
910
|
+
) {
|
|
876
911
|
return async function onAgentEnd(
|
|
877
912
|
event: TelegramAgentEndHookEvent<TMessage>,
|
|
878
913
|
ctx: TContext,
|
|
@@ -903,7 +938,8 @@ export function createTelegramAgentEndHook<
|
|
|
903
938
|
|
|
904
939
|
export async function handleTelegramAgentEndRuntime<
|
|
905
940
|
TTurn extends PendingTelegramTurn,
|
|
906
|
-
|
|
941
|
+
TReplyMarkup = unknown,
|
|
942
|
+
>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void> {
|
|
907
943
|
const { turn, assistant } = deps;
|
|
908
944
|
const rawFinalText = assistant.text;
|
|
909
945
|
const outboundReply = rawFinalText
|
|
@@ -934,7 +970,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
934
970
|
turn.chatId,
|
|
935
971
|
turn.replyToMessageId,
|
|
936
972
|
assistant.errorMessage ||
|
|
937
|
-
"Telegram bridge:
|
|
973
|
+
"Telegram bridge: π failed while processing the request.",
|
|
938
974
|
);
|
|
939
975
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
940
976
|
return;
|
package/lib/rendering.ts
CHANGED
package/lib/replies.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram reply delivery helpers
|
|
3
|
+
* Zones: telegram outbound, rendering transport
|
|
3
4
|
* Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -10,10 +11,48 @@ import {
|
|
|
10
11
|
type TelegramRenderMode,
|
|
11
12
|
} from "./rendering.ts";
|
|
12
13
|
|
|
14
|
+
// --- Reply Dedup ---
|
|
15
|
+
|
|
16
|
+
/** Non-persistent reply deduplication for a single agent turn.
|
|
17
|
+
* First reply to a prompt gets `reply_parameters.reply_to_message_id`;
|
|
18
|
+
* subsequent replies in the same turn skip it to avoid stacking
|
|
19
|
+
* duplicate reply headers in the chat viewport. */
|
|
20
|
+
export interface ReplyDedupRuntime {
|
|
21
|
+
/** Returns true if this is the first reply for the given prompt
|
|
22
|
+
* message id in the current turn. Side-effect: marks it replied. */
|
|
23
|
+
shouldReply(promptMessageId: number): boolean;
|
|
24
|
+
/** Reset the tracker when a new prompt enters the queue. */
|
|
25
|
+
reset(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createReplyDedupRuntime(): ReplyDedupRuntime {
|
|
29
|
+
const replied = new Map<number, boolean>();
|
|
30
|
+
return {
|
|
31
|
+
shouldReply(promptMessageId: number): boolean {
|
|
32
|
+
if (replied.has(promptMessageId)) return false;
|
|
33
|
+
replied.set(promptMessageId, true);
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
reset(): void {
|
|
37
|
+
replied.clear();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Transport-level dedup ---
|
|
43
|
+
|
|
44
|
+
let lastRepliedToMessageId: number | undefined;
|
|
45
|
+
|
|
46
|
+
export function resetTransportReplyDedup(): void {
|
|
47
|
+
lastRepliedToMessageId = undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
export function buildTelegramReplyParameters(
|
|
14
51
|
messageId: number | undefined,
|
|
15
52
|
): TelegramReplyParameters | undefined {
|
|
16
53
|
if (messageId === undefined) return undefined;
|
|
54
|
+
if (messageId === lastRepliedToMessageId) return undefined;
|
|
55
|
+
lastRepliedToMessageId = messageId;
|
|
17
56
|
return { message_id: messageId, allow_sending_without_reply: true };
|
|
18
57
|
}
|
|
19
58
|
|
|
@@ -219,13 +258,13 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
|
|
|
219
258
|
export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
|
|
220
259
|
sendTextReply: (
|
|
221
260
|
chatId: number,
|
|
222
|
-
replyToMessageId: number,
|
|
261
|
+
replyToMessageId: number | undefined,
|
|
223
262
|
text: string,
|
|
224
263
|
options?: { parseMode?: "HTML" },
|
|
225
264
|
) => Promise<number | undefined>;
|
|
226
265
|
sendMarkdownReply: (
|
|
227
266
|
chatId: number,
|
|
228
|
-
replyToMessageId: number,
|
|
267
|
+
replyToMessageId: number | undefined,
|
|
229
268
|
markdown: string,
|
|
230
269
|
options?: { replyMarkup?: unknown },
|
|
231
270
|
) => Promise<number | undefined>;
|
|
@@ -332,3 +371,48 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
332
371
|
},
|
|
333
372
|
};
|
|
334
373
|
}
|
|
374
|
+
|
|
375
|
+
// --- Dedup-wrapped Reply Wrappers ---
|
|
376
|
+
|
|
377
|
+
/** Wrap a sendTextReply with reply dedup so only the first message
|
|
378
|
+
* in a turn carries `reply_to_message_id`. */
|
|
379
|
+
export function dedupSendTextReply(
|
|
380
|
+
dedup: ReplyDedupRuntime,
|
|
381
|
+
inner: (
|
|
382
|
+
chatId: number,
|
|
383
|
+
replyToMessageId: number | undefined,
|
|
384
|
+
text: string,
|
|
385
|
+
options?: { parseMode?: "HTML" },
|
|
386
|
+
) => Promise<number | undefined>,
|
|
387
|
+
): (
|
|
388
|
+
chatId: number,
|
|
389
|
+
replyToMessageId: number,
|
|
390
|
+
text: string,
|
|
391
|
+
options?: { parseMode?: "HTML" },
|
|
392
|
+
) => Promise<number | undefined> {
|
|
393
|
+
return async (chatId, replyToMessageId, text, options) => {
|
|
394
|
+
const effectiveReplyTo = dedup.shouldReply(replyToMessageId) ? replyToMessageId : undefined;
|
|
395
|
+
return inner(chatId, effectiveReplyTo, text, options);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Wrap a sendMarkdownReply with reply dedup. */
|
|
400
|
+
export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
|
|
401
|
+
dedup: ReplyDedupRuntime,
|
|
402
|
+
inner: (
|
|
403
|
+
chatId: number,
|
|
404
|
+
replyToMessageId: number | undefined,
|
|
405
|
+
markdown: string,
|
|
406
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
407
|
+
) => Promise<number | undefined>,
|
|
408
|
+
): (
|
|
409
|
+
chatId: number,
|
|
410
|
+
replyToMessageId: number,
|
|
411
|
+
markdown: string,
|
|
412
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
413
|
+
) => Promise<number | undefined> {
|
|
414
|
+
return async (chatId, replyToMessageId, markdown, options) => {
|
|
415
|
+
const effectiveReplyTo = dedup.shouldReply(replyToMessageId) ? replyToMessageId : undefined;
|
|
416
|
+
return inner(chatId, effectiveReplyTo, markdown, options);
|
|
417
|
+
};
|
|
418
|
+
}
|
package/lib/routing.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram inbound routing composition
|
|
3
|
+
* Zones: telegram inbound, orchestration, queue/menu/command composition
|
|
3
4
|
* Wires authorized updates into menus, commands, media grouping, and prompt queueing
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ import * as Media from "./media.ts";
|
|
|
11
12
|
import * as Menu from "./menu.ts";
|
|
12
13
|
import * as Model from "./model.ts";
|
|
13
14
|
import * as Queue from "./queue.ts";
|
|
15
|
+
import * as PromptTemplates from "./prompt-templates.ts";
|
|
14
16
|
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
15
17
|
import * as Turns from "./turns.ts";
|
|
16
18
|
import * as Updates from "./updates.ts";
|
|
@@ -25,11 +27,6 @@ export type TelegramRoutedCallbackQuery = Updates.TelegramCallbackQuery &
|
|
|
25
27
|
Menu.MenuCallbackQuery;
|
|
26
28
|
|
|
27
29
|
export interface TelegramInboundRouteRuntimeDeps<
|
|
28
|
-
TUpdate extends Updates.TelegramUpdateFlow & {
|
|
29
|
-
message?: TMessage;
|
|
30
|
-
edited_message?: TMessage;
|
|
31
|
-
callback_query?: TCallbackQuery;
|
|
32
|
-
},
|
|
33
30
|
TMessage extends TelegramRoutedMessage,
|
|
34
31
|
TCallbackQuery extends TelegramRoutedCallbackQuery,
|
|
35
32
|
TContext,
|
|
@@ -51,6 +48,15 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
51
48
|
Model.ScopedTelegramModel<TModel>
|
|
52
49
|
>;
|
|
53
50
|
menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
|
|
51
|
+
openQueueMenu: (
|
|
52
|
+
chatId: number,
|
|
53
|
+
replyToMessageId: number,
|
|
54
|
+
ctx: TContext,
|
|
55
|
+
) => Promise<void>;
|
|
56
|
+
queueMenuCallbackHandler: (
|
|
57
|
+
query: TCallbackQuery,
|
|
58
|
+
ctx: TContext,
|
|
59
|
+
) => Promise<boolean>;
|
|
54
60
|
buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
|
|
55
61
|
attachmentHandlerRuntime: TelegramAttachmentHandlerRuntime<TContext>;
|
|
56
62
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
@@ -65,6 +71,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
65
71
|
text: string,
|
|
66
72
|
) => Promise<number | undefined>;
|
|
67
73
|
setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
|
|
74
|
+
getCommands: () => Parameters<typeof PromptTemplates.getTelegramPromptTemplateCommands>[0];
|
|
68
75
|
downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
|
|
69
76
|
getThinkingLevel: () => Model.ThinkingLevel;
|
|
70
77
|
setThinkingLevel: (level: Model.ThinkingLevel) => void;
|
|
@@ -94,7 +101,6 @@ export function createTelegramInboundRouteRuntime<
|
|
|
94
101
|
TModel extends Model.MenuModel,
|
|
95
102
|
>(
|
|
96
103
|
deps: TelegramInboundRouteRuntimeDeps<
|
|
97
|
-
TUpdate,
|
|
98
104
|
TMessage,
|
|
99
105
|
TCallbackQuery,
|
|
100
106
|
TContext,
|
|
@@ -159,8 +165,53 @@ export function createTelegramInboundRouteRuntime<
|
|
|
159
165
|
);
|
|
160
166
|
if (handled) return;
|
|
161
167
|
}
|
|
168
|
+
const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
|
|
169
|
+
if (handledByQueue) return;
|
|
162
170
|
await menuCallbackHandler(query, ctx);
|
|
163
171
|
};
|
|
172
|
+
const promptTurnBuilder = Turns.createTelegramPromptTurnRuntimeBuilder<
|
|
173
|
+
TMessage,
|
|
174
|
+
TContext
|
|
175
|
+
>({
|
|
176
|
+
allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
177
|
+
downloadFile: deps.downloadFile,
|
|
178
|
+
processAttachments: deps.attachmentHandlerRuntime.process,
|
|
179
|
+
});
|
|
180
|
+
const enqueueContinueTurn = async (
|
|
181
|
+
message: TMessage,
|
|
182
|
+
ctx: TContext,
|
|
183
|
+
): Promise<void> => {
|
|
184
|
+
const enqueuePlan = Queue.planTelegramPromptEnqueue(
|
|
185
|
+
deps.telegramQueueStore.getQueuedItems(),
|
|
186
|
+
deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory(),
|
|
187
|
+
);
|
|
188
|
+
deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory(false);
|
|
189
|
+
const continueMessage = {
|
|
190
|
+
...message,
|
|
191
|
+
text: "continue",
|
|
192
|
+
caption: undefined,
|
|
193
|
+
} as TMessage;
|
|
194
|
+
const turn = await promptTurnBuilder(
|
|
195
|
+
[continueMessage],
|
|
196
|
+
enqueuePlan.historyTurns,
|
|
197
|
+
ctx,
|
|
198
|
+
);
|
|
199
|
+
const continueTurn = {
|
|
200
|
+
...turn,
|
|
201
|
+
queueLane: "priority" as const,
|
|
202
|
+
laneOrder: Number.MIN_SAFE_INTEGER + turn.queueOrder,
|
|
203
|
+
statusSummary: "continue",
|
|
204
|
+
};
|
|
205
|
+
deps.telegramQueueStore.setQueuedItems(enqueuePlan.remainingItems);
|
|
206
|
+
deps.queueMutationRuntime.append(continueTurn, ctx);
|
|
207
|
+
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
208
|
+
};
|
|
209
|
+
const reservedCommandNames = new Set(Commands.TELEGRAM_RESERVED_COMMAND_NAMES);
|
|
210
|
+
const getPromptTemplateCommands = () =>
|
|
211
|
+
PromptTemplates.getTelegramPromptTemplateCommands(
|
|
212
|
+
deps.getCommands(),
|
|
213
|
+
reservedCommandNames,
|
|
214
|
+
);
|
|
164
215
|
const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
|
|
165
216
|
TMessage,
|
|
166
217
|
TContext
|
|
@@ -181,15 +232,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
181
232
|
deps.bridgeRuntime.lifecycle.setCompactionInProgress,
|
|
182
233
|
updateStatus: deps.updateStatus,
|
|
183
234
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
235
|
+
enqueueContinueTurn,
|
|
184
236
|
compact: deps.compact,
|
|
185
237
|
allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
186
238
|
allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
|
|
187
239
|
appendControlItem: deps.queueMutationRuntime.append,
|
|
188
240
|
showStatus: deps.menuActions.sendStatusMessage,
|
|
189
241
|
openModelMenu: deps.menuActions.openModelMenu,
|
|
242
|
+
openThinkingMenu: (message, ctx) => {
|
|
243
|
+
const chatId = (message as { chat: { id: number } }).chat.id;
|
|
244
|
+
return deps.menuActions.openThinkingMenu(chatId, message.message_id, ctx);
|
|
245
|
+
},
|
|
246
|
+
openQueueMenu: (message, ctx) => {
|
|
247
|
+
const chatId = (message as { chat: { id: number } }).chat.id;
|
|
248
|
+
return deps.openQueueMenu(chatId, message.message_id, ctx);
|
|
249
|
+
},
|
|
190
250
|
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
191
251
|
setAllowedUserId: deps.configStore.setAllowedUserId,
|
|
192
252
|
setMyCommands: deps.setMyCommands,
|
|
253
|
+
getPromptTemplateCommands,
|
|
193
254
|
persistConfig: deps.configStore.persist,
|
|
194
255
|
sendTextReply: deps.sendTextReply,
|
|
195
256
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -203,14 +264,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
203
264
|
deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
204
265
|
setPreserveQueuedTurnsAsHistory:
|
|
205
266
|
deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
206
|
-
createTurn:
|
|
207
|
-
TMessage,
|
|
208
|
-
TContext
|
|
209
|
-
>({
|
|
210
|
-
allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
211
|
-
downloadFile: deps.downloadFile,
|
|
212
|
-
processAttachments: deps.attachmentHandlerRuntime.process,
|
|
213
|
-
}),
|
|
267
|
+
createTurn: promptTurnBuilder,
|
|
214
268
|
updateStatus: deps.updateStatus,
|
|
215
269
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
216
270
|
}).enqueue;
|
|
@@ -220,6 +274,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
220
274
|
>({
|
|
221
275
|
extractRawText: Media.extractFirstTelegramMessageText,
|
|
222
276
|
handleCommand: commandHandler,
|
|
277
|
+
expandPromptTemplateCommand: (commandName, args) =>
|
|
278
|
+
PromptTemplates.expandTelegramPromptTemplateCommand(
|
|
279
|
+
commandName,
|
|
280
|
+
args,
|
|
281
|
+
getPromptTemplateCommands(),
|
|
282
|
+
),
|
|
283
|
+
replaceMessageText: (message, text) =>
|
|
284
|
+
({ ...message, text, caption: undefined }) as TMessage,
|
|
223
285
|
enqueueTurn: promptEnqueue,
|
|
224
286
|
});
|
|
225
287
|
const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
|
package/lib/runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram bridge runtime-state helpers
|
|
3
|
+
* Zones: pi agent runtime state, telegram session, shared coordination
|
|
3
4
|
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -350,6 +351,7 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
350
351
|
} catch (error) {
|
|
351
352
|
const message =
|
|
352
353
|
error instanceof Error ? error.message : String(error);
|
|
354
|
+
deps.updateStatus(ctx, message);
|
|
353
355
|
deps.recordRuntimeEvent?.("typing", error, {
|
|
354
356
|
chatId: targetChatId,
|
|
355
357
|
});
|
package/lib/setup.ts
CHANGED
package/lib/status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram status rendering helpers
|
|
3
|
+
* Zones: telegram ui, pi agent diagnostics, tui
|
|
3
4
|
* Builds usage, cost, and context summaries for the interactive Telegram status view
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -41,6 +42,8 @@ export interface TelegramStatusActiveModel {
|
|
|
41
42
|
export interface TelegramStatusContext {
|
|
42
43
|
sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
|
|
43
44
|
getContextUsage(): TelegramContextUsage | undefined;
|
|
45
|
+
isIdle?: () => boolean;
|
|
46
|
+
hasPendingMessages?: () => boolean;
|
|
44
47
|
modelRegistry: {
|
|
45
48
|
isUsingOAuth(model: TelegramStatusActiveModel): boolean;
|
|
46
49
|
};
|
|
@@ -406,12 +409,14 @@ export function buildTelegramStatusBarText(
|
|
|
406
409
|
return `${label} ${theme.fg("muted", "disconnected")}`;
|
|
407
410
|
if (!state.paired)
|
|
408
411
|
return `${label} ${theme.fg("warning", "awaiting pairing")}`;
|
|
409
|
-
const queued = theme.fg("muted", state.queuedStatus);
|
|
412
|
+
const queued = state.queuedStatus ? theme.fg("muted", state.queuedStatus) : "";
|
|
410
413
|
if (state.compactionInProgress) {
|
|
411
414
|
return `${label} ${theme.fg("accent", "compacting")}${queued}`;
|
|
412
415
|
}
|
|
413
416
|
if (state.processing) {
|
|
414
|
-
|
|
417
|
+
const processingStatus = state.processingStatus ?? "processing";
|
|
418
|
+
const processingToken = processingStatus === "active" ? "warning" : "accent";
|
|
419
|
+
return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
|
|
415
420
|
}
|
|
416
421
|
return `${label} ${theme.fg("success", "connected")}`;
|
|
417
422
|
}
|
|
@@ -528,6 +533,13 @@ function buildContextSummary(
|
|
|
528
533
|
return `${percent}/${formatTokens(contextWindow)}`;
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
function buildStatusSummary(ctx: TelegramStatusContext): string {
|
|
537
|
+
if (ctx.hasPendingMessages?.()) return "pending";
|
|
538
|
+
if (ctx.isIdle?.() === false) return "active";
|
|
539
|
+
if (ctx.isIdle?.() === true) return "idle";
|
|
540
|
+
return "unknown";
|
|
541
|
+
}
|
|
542
|
+
|
|
531
543
|
export function buildStatusHtml(
|
|
532
544
|
ctx: TelegramStatusContext,
|
|
533
545
|
activeModel: TelegramStatusActiveModel | undefined,
|
|
@@ -536,7 +548,7 @@ export function buildStatusHtml(
|
|
|
536
548
|
const usesSubscription = activeModel
|
|
537
549
|
? ctx.modelRegistry.isUsingOAuth(activeModel)
|
|
538
550
|
: false;
|
|
539
|
-
const lines: string[] = [];
|
|
551
|
+
const lines: string[] = [buildStatusRow("Status", buildStatusSummary(ctx))];
|
|
540
552
|
const usageSummary = buildUsageSummary(stats);
|
|
541
553
|
const costSummary = buildCostSummary(stats, usesSubscription);
|
|
542
554
|
if (usageSummary) {
|
|
@@ -546,8 +558,5 @@ export function buildStatusHtml(
|
|
|
546
558
|
lines.push(buildStatusRow("Cost", costSummary));
|
|
547
559
|
}
|
|
548
560
|
lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
|
|
549
|
-
if (lines.length === 0) {
|
|
550
|
-
lines.push(buildStatusRow("Status", "No usage data yet."));
|
|
551
|
-
}
|
|
552
561
|
return lines.join("\n");
|
|
553
562
|
}
|
package/lib/turns.ts
CHANGED
package/lib/updates.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram updates domain helpers
|
|
3
|
+
* Zones: telegram inbound, authorization, routing plans
|
|
3
4
|
* Owns update extraction, authorization, classification, execution planning, and runtime execution for Telegram updates
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -25,6 +26,9 @@ export type TelegramReactionType =
|
|
|
25
26
|
| TelegramReactionTypeEmoji
|
|
26
27
|
| TelegramReactionTypeNonEmoji;
|
|
27
28
|
|
|
29
|
+
export const TELEGRAM_PRIORITY_REACTION_EMOJIS = ["👍", "⚡", "❤", "🕊"] as const;
|
|
30
|
+
export const TELEGRAM_REMOVAL_REACTION_EMOJIS = ["👎", "👻", "💔", "💩"] as const;
|
|
31
|
+
|
|
28
32
|
export interface TelegramUpdateDeletion {
|
|
29
33
|
deleted_business_messages?: { message_ids?: unknown };
|
|
30
34
|
}
|
|
@@ -50,6 +54,21 @@ export function collectTelegramReactionEmojis(
|
|
|
50
54
|
);
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
function hasAnyTelegramReactionEmoji(
|
|
58
|
+
emojis: Set<string>,
|
|
59
|
+
candidates: readonly string[],
|
|
60
|
+
): boolean {
|
|
61
|
+
return candidates.some((emoji) => emojis.has(emoji));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasAddedTelegramReactionEmoji(
|
|
65
|
+
oldEmojis: Set<string>,
|
|
66
|
+
newEmojis: Set<string>,
|
|
67
|
+
candidates: readonly string[],
|
|
68
|
+
): boolean {
|
|
69
|
+
return candidates.some((emoji) => !oldEmojis.has(emoji) && newEmojis.has(emoji));
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
export function extractDeletedTelegramMessageIds(
|
|
54
73
|
update: TelegramUpdateDeletion,
|
|
55
74
|
): number[] {
|
|
@@ -579,8 +598,13 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
579
598
|
}
|
|
580
599
|
const oldEmojis = collectTelegramReactionEmojis(reactionUpdate.old_reaction);
|
|
581
600
|
const newEmojis = collectTelegramReactionEmojis(reactionUpdate.new_reaction);
|
|
582
|
-
|
|
583
|
-
|
|
601
|
+
if (
|
|
602
|
+
hasAddedTelegramReactionEmoji(
|
|
603
|
+
oldEmojis,
|
|
604
|
+
newEmojis,
|
|
605
|
+
TELEGRAM_REMOVAL_REACTION_EMOJIS,
|
|
606
|
+
)
|
|
607
|
+
) {
|
|
584
608
|
deps.removePendingMediaGroupMessages([reactionUpdate.message_id]);
|
|
585
609
|
deps.removeQueuedTelegramTurnsByMessageIds(
|
|
586
610
|
[reactionUpdate.message_id],
|
|
@@ -588,15 +612,21 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
588
612
|
);
|
|
589
613
|
return;
|
|
590
614
|
}
|
|
591
|
-
const
|
|
592
|
-
|
|
615
|
+
const hadPriorityReaction = hasAnyTelegramReactionEmoji(
|
|
616
|
+
oldEmojis,
|
|
617
|
+
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
618
|
+
);
|
|
619
|
+
const hasPriorityReaction = hasAnyTelegramReactionEmoji(
|
|
620
|
+
newEmojis,
|
|
621
|
+
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
622
|
+
);
|
|
623
|
+
if (hadPriorityReaction && !hasPriorityReaction) {
|
|
593
624
|
deps.clearQueuedTelegramTurnPriorityByMessageId(
|
|
594
625
|
reactionUpdate.message_id,
|
|
595
626
|
deps.ctx,
|
|
596
627
|
);
|
|
597
628
|
}
|
|
598
|
-
|
|
599
|
-
if (!likeAdded) return;
|
|
629
|
+
if (!hasAddedTelegramReactionEmoji(oldEmojis, newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS)) return;
|
|
600
630
|
deps.prioritizeQueuedTelegramTurnByMessageId(
|
|
601
631
|
reactionUpdate.message_id,
|
|
602
632
|
deps.ctx,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Better Telegram DM bridge extension for pi",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"index.ts",
|
|
32
32
|
"lib/",
|
|
33
33
|
"README.md",
|
|
34
|
+
"AGENTS.md",
|
|
35
|
+
"BACKLOG.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
34
37
|
"docs/",
|
|
35
38
|
"screenshot.png"
|
|
36
39
|
],
|