@llblab/pi-telegram 0.6.2 → 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 +49 -42
- package/docs/architecture.md +49 -40
- 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 +20 -14
- package/index.ts +217 -153
- 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 +10 -8
- 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 +120 -45
- 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 +17 -9
- 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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π prompt-template bridge helpers
|
|
3
|
+
* Zones: pi agent prompts, telegram controls, filesystem
|
|
4
|
+
* Discovers π prompt-template slash commands and expands them before Telegram queue dispatch
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import type { PiSlashCommandInfo } from "./pi.ts";
|
|
9
|
+
|
|
10
|
+
export interface TelegramPromptTemplateCommand {
|
|
11
|
+
command: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TelegramPromptTemplateReader = (path: string) => string;
|
|
17
|
+
|
|
18
|
+
const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
|
19
|
+
|
|
20
|
+
function stripPromptTemplateFrontmatter(content: string): string {
|
|
21
|
+
if (!content.startsWith("---")) return content;
|
|
22
|
+
const lines = content.split("\n");
|
|
23
|
+
if (lines[0]?.trim() !== "---") return content;
|
|
24
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
25
|
+
if (lines[index]?.trim() === "---")
|
|
26
|
+
return lines.slice(index + 1).join("\n");
|
|
27
|
+
}
|
|
28
|
+
return content;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parsePromptTemplateArgs(argsString: string): string[] {
|
|
32
|
+
const args: string[] = [];
|
|
33
|
+
let current = "";
|
|
34
|
+
let quote: string | undefined;
|
|
35
|
+
for (const char of argsString) {
|
|
36
|
+
if (quote) {
|
|
37
|
+
if (char === quote) {
|
|
38
|
+
quote = undefined;
|
|
39
|
+
} else {
|
|
40
|
+
current += char;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '"' || char === "'") {
|
|
45
|
+
quote = char;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === " " || char === "\t") {
|
|
49
|
+
if (current) args.push(current);
|
|
50
|
+
current = "";
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
current += char;
|
|
54
|
+
}
|
|
55
|
+
if (current) args.push(current);
|
|
56
|
+
return args;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function substitutePromptTemplateArgs(
|
|
60
|
+
content: string,
|
|
61
|
+
args: readonly string[],
|
|
62
|
+
): string {
|
|
63
|
+
let result = content.replace(/\$(\d+)/g, (_, num: string) => {
|
|
64
|
+
const index = Number.parseInt(num, 10) - 1;
|
|
65
|
+
return args[index] ?? "";
|
|
66
|
+
});
|
|
67
|
+
result = result.replace(
|
|
68
|
+
/\$\{@:(\d+)(?::(\d+))?\}/g,
|
|
69
|
+
(_, startValue: string, lengthValue: string | undefined) => {
|
|
70
|
+
const start = Math.max(Number.parseInt(startValue, 10) - 1, 0);
|
|
71
|
+
if (lengthValue) {
|
|
72
|
+
const length = Number.parseInt(lengthValue, 10);
|
|
73
|
+
return args.slice(start, start + length).join(" ");
|
|
74
|
+
}
|
|
75
|
+
return args.slice(start).join(" ");
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
const allArgs = args.join(" ");
|
|
79
|
+
return result.replace(/\$ARGUMENTS/g, allArgs).replace(/\$@/g, allArgs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isTelegramPromptTemplateCommandName(name: string): boolean {
|
|
83
|
+
return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function mapPiPromptTemplateNameToTelegramCommandName(
|
|
87
|
+
name: string,
|
|
88
|
+
): string | undefined {
|
|
89
|
+
const command = name
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
92
|
+
.replace(/_+/g, "_")
|
|
93
|
+
.replace(/^_+|_+$/g, "")
|
|
94
|
+
.slice(0, 32)
|
|
95
|
+
.replace(/_+$/g, "");
|
|
96
|
+
return isTelegramPromptTemplateCommandName(command) ? command : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface TelegramPromptTemplateCommandGetterDeps {
|
|
100
|
+
getCommands: () => readonly PiSlashCommandInfo[];
|
|
101
|
+
reservedCommandNames?: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getTelegramPromptTemplateCommands(
|
|
105
|
+
commands: readonly PiSlashCommandInfo[],
|
|
106
|
+
reservedNames: ReadonlySet<string> = new Set(),
|
|
107
|
+
): TelegramPromptTemplateCommand[] {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
const promptCommands: TelegramPromptTemplateCommand[] = [];
|
|
110
|
+
for (const command of commands) {
|
|
111
|
+
if (command.source !== "prompt") continue;
|
|
112
|
+
const telegramCommand = mapPiPromptTemplateNameToTelegramCommandName(
|
|
113
|
+
command.name,
|
|
114
|
+
);
|
|
115
|
+
if (!telegramCommand) continue;
|
|
116
|
+
if (reservedNames.has(telegramCommand)) continue;
|
|
117
|
+
if (seen.has(telegramCommand)) continue;
|
|
118
|
+
seen.add(telegramCommand);
|
|
119
|
+
promptCommands.push({
|
|
120
|
+
command: telegramCommand,
|
|
121
|
+
description: command.description,
|
|
122
|
+
path: command.sourceInfo.path,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return promptCommands.sort((a, b) => a.command.localeCompare(b.command));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createTelegramPromptTemplateCommandGetter(
|
|
129
|
+
deps: TelegramPromptTemplateCommandGetterDeps,
|
|
130
|
+
): () => TelegramPromptTemplateCommand[] {
|
|
131
|
+
const reservedNames = new Set(deps.reservedCommandNames);
|
|
132
|
+
return function getPromptTemplateCommands() {
|
|
133
|
+
return getTelegramPromptTemplateCommands(deps.getCommands(), reservedNames);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function expandTelegramPromptTemplateCommand(
|
|
138
|
+
commandName: string,
|
|
139
|
+
args: string,
|
|
140
|
+
commands: readonly TelegramPromptTemplateCommand[],
|
|
141
|
+
readTemplate: TelegramPromptTemplateReader = (path) =>
|
|
142
|
+
readFileSync(path, "utf-8"),
|
|
143
|
+
): string | undefined {
|
|
144
|
+
const command = commands.find(
|
|
145
|
+
(candidate) => candidate.command === commandName,
|
|
146
|
+
);
|
|
147
|
+
if (!command) return undefined;
|
|
148
|
+
const content = stripPromptTemplateFrontmatter(readTemplate(command.path));
|
|
149
|
+
return substitutePromptTemplateArgs(content, parsePromptTemplateArgs(args));
|
|
150
|
+
}
|
package/lib/prompts.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram prompt injection helpers
|
|
3
|
+
* Zones: pi agent prompts, telegram guidance
|
|
3
4
|
* Owns Telegram-specific system prompt suffixes injected into pi agent turns
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -9,15 +10,22 @@ import { TELEGRAM_PREFIX } from "./turns.ts";
|
|
|
9
10
|
const SYSTEM_PROMPT_SUFFIX = `
|
|
10
11
|
|
|
11
12
|
Telegram bridge extension is active.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- [telegram]
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
13
|
+
|
|
14
|
+
Inbound context:
|
|
15
|
+
- \`[telegram]\` marks Telegram-originated messages.
|
|
16
|
+
- \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
|
|
17
|
+
- \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains attachment-handler stdout such as transcriptions or extracted text for those attachments.
|
|
18
|
+
|
|
19
|
+
Telegram-visible output:
|
|
20
|
+
- Telegram is often phone-width; prefer narrow table columns because wide monospace tables can become unreadable.
|
|
21
|
+
- For requested/generated files, call tool \`telegram_attach(local_path)\`; mentioning a local path in text does not send it.
|
|
22
|
+
|
|
23
|
+
Native outbound actions:
|
|
24
|
+
- Use top-level column-zero hidden Markdown comments outside code, quotes, and lists; the bridge handles them after agent_end, so do not call or register transport/TTS/text-to-OGG tools.
|
|
25
|
+
- \`telegram_voice\`: text is synthesized through the configured outbound-handler pipeline. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line voice, or \`<!-- telegram_voice: Short summary -->\` for one-line voice with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
|
|
26
|
+
- \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts.
|
|
27
|
+
- If only hidden action comments would remain, add visible parent text like "Choose one:".
|
|
28
|
+
`;
|
|
21
29
|
|
|
22
30
|
export function buildTelegramBridgeSystemPrompt(options: {
|
|
23
31
|
prompt: string;
|
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
|
});
|