@llblab/pi-telegram 0.13.2 → 0.14.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 +5 -3
- package/CHANGELOG.md +14 -12
- package/README.md +6 -6
- package/docs/architecture.md +6 -5
- package/docs/locks.md +4 -2
- package/docs/outbound.md +6 -3
- package/docs/public-api.md +7 -6
- package/docs/sections.md +4 -4
- package/index.ts +33 -18
- package/lib/bindings.ts +32 -3
- package/lib/command-templates.ts +155 -10
- package/lib/commands.ts +14 -12
- package/lib/lifecycle.ts +34 -0
- package/lib/locks.ts +16 -2
- package/lib/outbound-attachments.ts +253 -31
- package/lib/prompts.ts +4 -4
- package/lib/queue.ts +33 -32
- package/lib/routing.ts +7 -7
- package/lib/runtime.ts +45 -17
- package/lib/sections.ts +95 -31
- package/lib/status.ts +1 -1
- package/package.json +1 -1
|
@@ -53,9 +53,28 @@ export interface TelegramOutboundAttachmentToolRegistrationDeps extends Telegram
|
|
|
53
53
|
maxAttachmentsPerTurn?: number;
|
|
54
54
|
maxAttachmentSizeBytes?: number;
|
|
55
55
|
getActiveTurn: () => TelegramOutboundAttachmentQueueTargetView | undefined;
|
|
56
|
+
getDefaultChatId?: () => number | undefined;
|
|
57
|
+
canSendDirect?: () => boolean;
|
|
58
|
+
sendMultipart?: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
56
59
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
export interface TelegramOutboundMessagePlan {
|
|
63
|
+
markdown: string;
|
|
64
|
+
replyMarkup?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
|
|
68
|
+
getDefaultChatId: () => number | undefined;
|
|
69
|
+
canSendDirect: () => boolean;
|
|
70
|
+
planMessage: (markdown: string) => TelegramOutboundMessagePlan;
|
|
71
|
+
sendMarkdownMessage: (
|
|
72
|
+
chatId: number,
|
|
73
|
+
markdown: string,
|
|
74
|
+
options?: { replyMarkup?: unknown },
|
|
75
|
+
) => Promise<number | undefined>;
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
export interface TelegramQueuedOutboundAttachmentView {
|
|
60
79
|
path: string;
|
|
61
80
|
fileName: string;
|
|
@@ -90,9 +109,71 @@ function formatTelegramOutboundAttachmentSizeLimitError(
|
|
|
90
109
|
return path ? `${message}: ${path}` : message;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
function formatTelegramOutboundAttachmentToolResultText(
|
|
112
|
+
function formatTelegramOutboundAttachmentToolResultText(
|
|
113
|
+
count: number,
|
|
114
|
+
mode: "queued" | "sent" = "queued",
|
|
115
|
+
): string {
|
|
94
116
|
// Pi's compact tool rows need an empty first line to visually separate header and result
|
|
95
|
-
|
|
117
|
+
const verb = mode === "queued" ? "Queued" : "Sent";
|
|
118
|
+
return ["", `${verb} ${count} Telegram attachment(s).`].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatTelegramOutboundMessageToolResultText(chatId: number): string {
|
|
122
|
+
return ["", `Sent Telegram message to ${chatId}.`].join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertTelegramDirectDeliveryAllowed(
|
|
126
|
+
canSendDirect: (() => boolean) | undefined,
|
|
127
|
+
): void {
|
|
128
|
+
if (canSendDirect?.()) return;
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Telegram direct delivery requires this π instance to own /telegram-connect",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveTelegramOutboundChatId(options: {
|
|
135
|
+
chatId?: number;
|
|
136
|
+
getDefaultChatId?: () => number | undefined;
|
|
137
|
+
}): number {
|
|
138
|
+
const chatId = options.chatId ?? options.getDefaultChatId?.();
|
|
139
|
+
if (chatId === undefined) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Telegram chat_id is required when no paired/default Telegram chat is available",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return chatId;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function buildTelegramOutboundAttachmentViews(options: {
|
|
148
|
+
paths: string[];
|
|
149
|
+
maxAttachmentSizeBytes?: number;
|
|
150
|
+
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
151
|
+
}): Promise<TelegramQueuedOutboundAttachmentView[]> {
|
|
152
|
+
const pendingAttachments: TelegramQueuedOutboundAttachmentView[] = [];
|
|
153
|
+
for (const inputPath of options.paths) {
|
|
154
|
+
const stats = await (options.statPath ?? stat)(inputPath);
|
|
155
|
+
if (!stats.isFile()) {
|
|
156
|
+
throw new Error(`Not a file: ${inputPath}`);
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
options.maxAttachmentSizeBytes !== undefined &&
|
|
160
|
+
stats.size !== undefined &&
|
|
161
|
+
stats.size > options.maxAttachmentSizeBytes
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
formatTelegramOutboundAttachmentSizeLimitError(
|
|
165
|
+
stats.size,
|
|
166
|
+
options.maxAttachmentSizeBytes,
|
|
167
|
+
inputPath,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
pendingAttachments.push({
|
|
172
|
+
path: inputPath,
|
|
173
|
+
fileName: basename(inputPath),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return pendingAttachments;
|
|
96
177
|
}
|
|
97
178
|
|
|
98
179
|
export function registerTelegramOutboundAttachmentTool(
|
|
@@ -107,24 +188,43 @@ export function registerTelegramOutboundAttachmentTool(
|
|
|
107
188
|
name: "telegram_attach",
|
|
108
189
|
label: "Telegram Attach",
|
|
109
190
|
description:
|
|
110
|
-
"Queue one or more local files
|
|
111
|
-
promptSnippet:
|
|
191
|
+
"Queue one or more local files for the active Telegram reply, or send them immediately to Telegram when no Telegram turn is active.",
|
|
192
|
+
promptSnippet:
|
|
193
|
+
"Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.",
|
|
112
194
|
promptGuidelines: [
|
|
113
195
|
"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
|
+
"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.",
|
|
114
197
|
],
|
|
115
198
|
parameters: Type.Object({
|
|
116
199
|
paths: Type.Array(
|
|
117
200
|
Type.String({ description: "Local file path to attach" }),
|
|
118
201
|
{ minItems: 1, maxItems: maxAttachmentsPerTurn },
|
|
119
202
|
),
|
|
203
|
+
chat_id: Type.Optional(
|
|
204
|
+
Type.Number({
|
|
205
|
+
description:
|
|
206
|
+
"Optional Telegram chat id for immediate delivery when no Telegram turn is active",
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
caption: Type.Optional(
|
|
210
|
+
Type.String({
|
|
211
|
+
description:
|
|
212
|
+
"Optional caption for immediate delivery; ignored when queued for an active turn",
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
120
215
|
}),
|
|
121
216
|
async execute(_toolCallId, params) {
|
|
122
217
|
try {
|
|
123
218
|
return await queueTelegramOutboundAttachments({
|
|
124
219
|
activeTurn: deps.getActiveTurn(),
|
|
125
220
|
paths: params.paths,
|
|
221
|
+
chatId: params.chat_id,
|
|
222
|
+
caption: params.caption,
|
|
126
223
|
maxAttachmentsPerTurn,
|
|
127
224
|
maxAttachmentSizeBytes,
|
|
225
|
+
sendMultipart: deps.sendMultipart,
|
|
226
|
+
getDefaultChatId: deps.getDefaultChatId,
|
|
227
|
+
canSendDirect: deps.canSendDirect,
|
|
128
228
|
statPath: deps.statPath,
|
|
129
229
|
});
|
|
130
230
|
} catch (error) {
|
|
@@ -138,6 +238,46 @@ export function registerTelegramOutboundAttachmentTool(
|
|
|
138
238
|
});
|
|
139
239
|
}
|
|
140
240
|
|
|
241
|
+
export function registerTelegramOutboundMessageTool(
|
|
242
|
+
pi: ExtensionAPI,
|
|
243
|
+
deps: TelegramOutboundMessageToolRegistrationDeps,
|
|
244
|
+
): void {
|
|
245
|
+
pi.registerTool({
|
|
246
|
+
name: "telegram_message",
|
|
247
|
+
label: "Telegram Message",
|
|
248
|
+
description:
|
|
249
|
+
"Send a Markdown text message directly to the paired/default Telegram chat or an explicit chat_id. Hidden telegram_button comments in the text become attached inline prompt buttons.",
|
|
250
|
+
promptSnippet:
|
|
251
|
+
"Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.",
|
|
252
|
+
promptGuidelines: [
|
|
253
|
+
"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.",
|
|
254
|
+
"Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
255
|
+
"Do not use this tool for ordinary Telegram-originated replies; answer normally so the bridge can deliver the active turn reply.",
|
|
256
|
+
],
|
|
257
|
+
parameters: Type.Object({
|
|
258
|
+
text: Type.String({ description: "Message text to send" }),
|
|
259
|
+
chat_id: Type.Optional(
|
|
260
|
+
Type.Number({ description: "Optional Telegram chat id" }),
|
|
261
|
+
),
|
|
262
|
+
}),
|
|
263
|
+
async execute(_toolCallId, params) {
|
|
264
|
+
try {
|
|
265
|
+
return await sendTelegramOutboundMessage({
|
|
266
|
+
text: params.text,
|
|
267
|
+
chatId: params.chat_id,
|
|
268
|
+
getDefaultChatId: deps.getDefaultChatId,
|
|
269
|
+
canSendDirect: deps.canSendDirect,
|
|
270
|
+
planMessage: deps.planMessage,
|
|
271
|
+
sendMarkdownMessage: deps.sendMarkdownMessage,
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
deps.recordRuntimeEvent?.("message", error, { phase: "direct" });
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
141
281
|
export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
|
|
142
282
|
sendMultipart: (
|
|
143
283
|
method: string,
|
|
@@ -163,14 +303,32 @@ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
|
|
|
163
303
|
export async function queueTelegramOutboundAttachments(options: {
|
|
164
304
|
activeTurn: TelegramOutboundAttachmentQueueTargetView | undefined;
|
|
165
305
|
paths: string[];
|
|
306
|
+
chatId?: number;
|
|
307
|
+
caption?: string;
|
|
166
308
|
maxAttachmentsPerTurn: number;
|
|
167
309
|
maxAttachmentSizeBytes?: number;
|
|
310
|
+
sendMultipart?: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
311
|
+
getDefaultChatId?: () => number | undefined;
|
|
312
|
+
canSendDirect?: () => boolean;
|
|
168
313
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
169
314
|
}): Promise<TelegramOutboundAttachmentToolResult> {
|
|
170
315
|
if (!options.activeTurn) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
316
|
+
if (!options.sendMultipart) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"telegram_attach can only queue files while replying to an active Telegram turn; provide Telegram send ports for immediate delivery",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return sendTelegramOutboundFiles({
|
|
322
|
+
paths: options.paths,
|
|
323
|
+
chatId: options.chatId,
|
|
324
|
+
caption: options.caption,
|
|
325
|
+
maxAttachmentsPerTurn: options.maxAttachmentsPerTurn,
|
|
326
|
+
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
327
|
+
sendMultipart: options.sendMultipart,
|
|
328
|
+
getDefaultChatId: options.getDefaultChatId,
|
|
329
|
+
canSendDirect: options.canSendDirect,
|
|
330
|
+
statPath: options.statPath,
|
|
331
|
+
});
|
|
174
332
|
}
|
|
175
333
|
if (
|
|
176
334
|
options.activeTurn.queuedAttachments.length + options.paths.length >
|
|
@@ -180,30 +338,11 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
180
338
|
`Attachment limit reached (${options.maxAttachmentsPerTurn})`,
|
|
181
339
|
);
|
|
182
340
|
}
|
|
183
|
-
const pendingAttachments
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
if (
|
|
190
|
-
options.maxAttachmentSizeBytes !== undefined &&
|
|
191
|
-
stats.size !== undefined &&
|
|
192
|
-
stats.size > options.maxAttachmentSizeBytes
|
|
193
|
-
) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
formatTelegramOutboundAttachmentSizeLimitError(
|
|
196
|
-
stats.size,
|
|
197
|
-
options.maxAttachmentSizeBytes,
|
|
198
|
-
inputPath,
|
|
199
|
-
),
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
pendingAttachments.push({
|
|
203
|
-
path: inputPath,
|
|
204
|
-
fileName: basename(inputPath),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
341
|
+
const pendingAttachments = await buildTelegramOutboundAttachmentViews({
|
|
342
|
+
paths: options.paths,
|
|
343
|
+
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
344
|
+
statPath: options.statPath,
|
|
345
|
+
});
|
|
207
346
|
options.activeTurn.queuedAttachments.push(...pendingAttachments);
|
|
208
347
|
const added = pendingAttachments.map((attachment) => attachment.path);
|
|
209
348
|
return {
|
|
@@ -217,6 +356,89 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
217
356
|
};
|
|
218
357
|
}
|
|
219
358
|
|
|
359
|
+
export async function sendTelegramOutboundMessage(options: {
|
|
360
|
+
text: string;
|
|
361
|
+
chatId?: number;
|
|
362
|
+
getDefaultChatId?: () => number | undefined;
|
|
363
|
+
canSendDirect: () => boolean;
|
|
364
|
+
planMessage: (markdown: string) => TelegramOutboundMessagePlan;
|
|
365
|
+
sendMarkdownMessage: (
|
|
366
|
+
chatId: number,
|
|
367
|
+
markdown: string,
|
|
368
|
+
options?: { replyMarkup?: unknown },
|
|
369
|
+
) => Promise<number | undefined>;
|
|
370
|
+
}): Promise<{
|
|
371
|
+
content: Array<{ type: "text"; text: string }>;
|
|
372
|
+
details: { chatId: number; messageId?: number };
|
|
373
|
+
}> {
|
|
374
|
+
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
375
|
+
const chatId = resolveTelegramOutboundChatId({
|
|
376
|
+
chatId: options.chatId,
|
|
377
|
+
getDefaultChatId: options.getDefaultChatId,
|
|
378
|
+
});
|
|
379
|
+
const plan = options.planMessage(options.text);
|
|
380
|
+
const messageId = await options.sendMarkdownMessage(chatId, plan.markdown, {
|
|
381
|
+
replyMarkup: plan.replyMarkup,
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: formatTelegramOutboundMessageToolResultText(chatId) }],
|
|
385
|
+
details: { chatId, messageId },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function sendTelegramOutboundFiles(options: {
|
|
390
|
+
paths: string[];
|
|
391
|
+
chatId?: number;
|
|
392
|
+
caption?: string;
|
|
393
|
+
maxAttachmentsPerTurn: number;
|
|
394
|
+
maxAttachmentSizeBytes?: number;
|
|
395
|
+
sendMultipart: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
396
|
+
getDefaultChatId?: () => number | undefined;
|
|
397
|
+
canSendDirect?: () => boolean;
|
|
398
|
+
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
399
|
+
}): Promise<TelegramOutboundAttachmentToolResult & { details: { paths: string[]; chatId: number } }> {
|
|
400
|
+
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
401
|
+
if (options.paths.length > options.maxAttachmentsPerTurn) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Attachment limit reached (${options.maxAttachmentsPerTurn})`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const chatId = resolveTelegramOutboundChatId({
|
|
407
|
+
chatId: options.chatId,
|
|
408
|
+
getDefaultChatId: options.getDefaultChatId,
|
|
409
|
+
});
|
|
410
|
+
const pendingAttachments = await buildTelegramOutboundAttachmentViews({
|
|
411
|
+
paths: options.paths,
|
|
412
|
+
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
413
|
+
statPath: options.statPath,
|
|
414
|
+
});
|
|
415
|
+
for (const [index, attachment] of pendingAttachments.entries()) {
|
|
416
|
+
const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
|
|
417
|
+
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
418
|
+
const fieldName = isPhoto ? "photo" : "document";
|
|
419
|
+
await options.sendMultipart(
|
|
420
|
+
method,
|
|
421
|
+
{
|
|
422
|
+
chat_id: String(chatId),
|
|
423
|
+
...(options.caption && index === 0 ? { caption: options.caption } : {}),
|
|
424
|
+
},
|
|
425
|
+
fieldName,
|
|
426
|
+
attachment.path,
|
|
427
|
+
attachment.fileName,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const added = pendingAttachments.map((attachment) => attachment.path);
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: formatTelegramOutboundAttachmentToolResultText(added.length, "sent"),
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
details: { paths: added, chatId },
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
220
442
|
export function createTelegramQueuedOutboundAttachmentSender(
|
|
221
443
|
deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
|
|
222
444
|
) {
|
package/lib/prompts.ts
CHANGED
|
@@ -23,13 +23,13 @@ Telegram-visible output:
|
|
|
23
23
|
- Telegram is often phone-width; keep tables, dense list items, and compact text blocks at or below 37 visible cells when possible.
|
|
24
24
|
- Count display width, not raw characters: emoji and some glyphs are wide, so prefer shorter labels when unsure.
|
|
25
25
|
- Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
|
|
26
|
-
- For requested/generated files, call
|
|
26
|
+
- For requested/generated files, call \`telegram_attach(local_path)\`; during Telegram turns it attaches files to the active reply, and during explicit local/TUI Telegram-delivery requests it sends files directly to the paired/default chat or an explicit \`chat_id\`. If a local/TUI user explicitly asks to send a text message to Telegram, use \`telegram_message\` with Markdown text; embed the same top-level \`telegram_button\` comments when inline prompt buttons are needed, because Telegram buttons must belong to a message. Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
|
|
27
27
|
|
|
28
28
|
Native outbound actions:
|
|
29
|
-
- Use top-level column-zero hidden Markdown comments outside code, quotes, and lists; the bridge
|
|
29
|
+
- Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
|
|
30
30
|
- \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. 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.
|
|
31
|
-
- \`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.
|
|
32
|
-
- If only hidden action comments would remain, add visible parent text like "Choose one:".
|
|
31
|
+
- \`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. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
|
|
32
|
+
- If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.
|
|
33
33
|
`;
|
|
34
34
|
|
|
35
35
|
export function buildTelegramBridgeSystemPrompt(options: {
|
package/lib/queue.ts
CHANGED
|
@@ -242,12 +242,12 @@ export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
|
|
|
242
242
|
|
|
243
243
|
export function planTelegramPromptEnqueue<TContext = unknown>(
|
|
244
244
|
items: TelegramQueueItem<TContext>[],
|
|
245
|
-
|
|
245
|
+
foldQueuedPromptsIntoHistory: boolean,
|
|
246
246
|
): {
|
|
247
247
|
historyTurns: PendingTelegramTurn[];
|
|
248
248
|
remainingItems: TelegramQueueItem<TContext>[];
|
|
249
249
|
} {
|
|
250
|
-
if (!
|
|
250
|
+
if (!foldQueuedPromptsIntoHistory) {
|
|
251
251
|
return { historyTurns: [], remainingItems: items };
|
|
252
252
|
}
|
|
253
253
|
return partitionTelegramQueueItemsForHistory(items);
|
|
@@ -512,11 +512,11 @@ export function planNextTelegramQueueAction<TContext = unknown>(
|
|
|
512
512
|
export function shouldDispatchAfterTelegramAgentEnd(options: {
|
|
513
513
|
hasTurn: boolean;
|
|
514
514
|
stopReason?: string;
|
|
515
|
-
|
|
515
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
516
516
|
}): boolean {
|
|
517
517
|
if (!options.hasTurn) return true;
|
|
518
518
|
if (options.stopReason === "aborted") {
|
|
519
|
-
return !options.
|
|
519
|
+
return !options.foldQueuedPromptsIntoHistory;
|
|
520
520
|
}
|
|
521
521
|
return true;
|
|
522
522
|
}
|
|
@@ -529,6 +529,7 @@ export interface TelegramAgentStartPlan<TContext = unknown> {
|
|
|
529
529
|
shouldResetPendingModelSwitch: boolean;
|
|
530
530
|
shouldResetToolExecutions: boolean;
|
|
531
531
|
shouldClearDispatchPending: boolean;
|
|
532
|
+
shouldClearAbortHistory: boolean;
|
|
532
533
|
}
|
|
533
534
|
|
|
534
535
|
export interface TelegramAgentStartRuntimeDeps<
|
|
@@ -542,6 +543,7 @@ export interface TelegramAgentStartRuntimeDeps<
|
|
|
542
543
|
resetPendingModelSwitch: () => void;
|
|
543
544
|
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
544
545
|
clearDispatchPending: () => void;
|
|
546
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
545
547
|
setActiveTurn: (turn: TTurn) => void;
|
|
546
548
|
createPreviewState: () => void;
|
|
547
549
|
startTypingLoop: () => void;
|
|
@@ -560,6 +562,7 @@ export interface TelegramAgentStartHookRuntimeDeps<
|
|
|
560
562
|
resetPendingModelSwitch: () => void;
|
|
561
563
|
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
562
564
|
clearDispatchPending: () => void;
|
|
565
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
563
566
|
setActiveTurn: (turn: TTurn) => void;
|
|
564
567
|
createPreviewState: () => void;
|
|
565
568
|
startTypingLoop: (ctx: TContext) => void;
|
|
@@ -598,6 +601,8 @@ export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
|
|
|
598
601
|
shouldResetPendingModelSwitch: true,
|
|
599
602
|
shouldResetToolExecutions: true,
|
|
600
603
|
shouldClearDispatchPending: options.hasPendingDispatch,
|
|
604
|
+
shouldClearAbortHistory:
|
|
605
|
+
!options.hasActiveTurn && !options.hasPendingDispatch,
|
|
601
606
|
};
|
|
602
607
|
}
|
|
603
608
|
const nextDispatch = consumeDispatchedTelegramPrompt(
|
|
@@ -610,6 +615,7 @@ export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
|
|
|
610
615
|
shouldResetPendingModelSwitch: true,
|
|
611
616
|
shouldResetToolExecutions: true,
|
|
612
617
|
shouldClearDispatchPending: options.hasPendingDispatch,
|
|
618
|
+
shouldClearAbortHistory: false,
|
|
613
619
|
};
|
|
614
620
|
}
|
|
615
621
|
|
|
@@ -624,6 +630,9 @@ export function handleTelegramAgentStartRuntime<
|
|
|
624
630
|
});
|
|
625
631
|
if (startPlan.shouldResetToolExecutions) deps.resetToolExecutions();
|
|
626
632
|
if (startPlan.shouldResetPendingModelSwitch) deps.resetPendingModelSwitch();
|
|
633
|
+
if (startPlan.shouldClearAbortHistory) {
|
|
634
|
+
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
635
|
+
}
|
|
627
636
|
deps.setQueuedItems(startPlan.remainingItems);
|
|
628
637
|
if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending();
|
|
629
638
|
if (startPlan.activeTurn) {
|
|
@@ -651,6 +660,7 @@ export function createTelegramAgentStartHook<
|
|
|
651
660
|
resetPendingModelSwitch: deps.resetPendingModelSwitch,
|
|
652
661
|
setQueuedItems: deps.setQueuedItems,
|
|
653
662
|
clearDispatchPending: deps.clearDispatchPending,
|
|
663
|
+
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
654
664
|
setActiveTurn: deps.setActiveTurn,
|
|
655
665
|
createPreviewState: deps.createPreviewState,
|
|
656
666
|
startTypingLoop: () => deps.startTypingLoop(ctx),
|
|
@@ -791,10 +801,10 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
791
801
|
> {
|
|
792
802
|
turn: TTurn | undefined;
|
|
793
803
|
assistant: TelegramAgentEndAssistantResult;
|
|
794
|
-
|
|
804
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
795
805
|
resetRuntimeState: () => void;
|
|
806
|
+
waitForTypingIdle?: () => Promise<void>;
|
|
796
807
|
updateStatus: () => void;
|
|
797
|
-
isCurrentOwner?: () => boolean;
|
|
798
808
|
dispatchNextQueuedTelegramTurn: () => void;
|
|
799
809
|
clearPreview: (chatId: number) => Promise<void>;
|
|
800
810
|
setPreviewPendingText: (text: string) => void;
|
|
@@ -849,10 +859,10 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
849
859
|
extractAssistant: (
|
|
850
860
|
messages: readonly TMessage[],
|
|
851
861
|
) => TelegramAgentEndAssistantResult;
|
|
852
|
-
|
|
862
|
+
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
853
863
|
resetRuntimeState: () => void;
|
|
864
|
+
waitForTypingIdle?: () => Promise<void>;
|
|
854
865
|
updateStatus: (ctx: TContext) => void;
|
|
855
|
-
isCurrentOwner?: (ctx: TContext) => boolean;
|
|
856
866
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
857
867
|
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
858
868
|
dispatch: (ctx: TContext) => void,
|
|
@@ -890,12 +900,12 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
890
900
|
stopReason?: string;
|
|
891
901
|
hasFinalText: boolean;
|
|
892
902
|
hasQueuedAttachments: boolean;
|
|
893
|
-
|
|
903
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
894
904
|
}): TelegramAgentEndPlan {
|
|
895
905
|
const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
|
|
896
906
|
hasTurn: options.hasTurn,
|
|
897
907
|
stopReason: options.stopReason,
|
|
898
|
-
|
|
908
|
+
foldQueuedPromptsIntoHistory: options.foldQueuedPromptsIntoHistory,
|
|
899
909
|
});
|
|
900
910
|
if (!options.hasTurn) {
|
|
901
911
|
return {
|
|
@@ -974,12 +984,10 @@ export function createTelegramAgentEndHook<
|
|
|
974
984
|
turn,
|
|
975
985
|
assistant:
|
|
976
986
|
turn || proactiveEnabled ? deps.extractAssistant(event.messages) : {},
|
|
977
|
-
|
|
987
|
+
foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
|
|
978
988
|
resetRuntimeState: deps.resetRuntimeState,
|
|
989
|
+
waitForTypingIdle: deps.waitForTypingIdle,
|
|
979
990
|
updateStatus: () => deps.updateStatus(ctx),
|
|
980
|
-
isCurrentOwner: deps.isCurrentOwner
|
|
981
|
-
? () => deps.isCurrentOwner?.(ctx) ?? false
|
|
982
|
-
: undefined,
|
|
983
991
|
dispatchNextQueuedTelegramTurn: () => {
|
|
984
992
|
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
985
993
|
deps.dispatchNextQueuedTelegramTurn,
|
|
@@ -1039,17 +1047,14 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1039
1047
|
!!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
|
|
1040
1048
|
const replyMarkup = outboundReply?.replyMarkup;
|
|
1041
1049
|
deps.resetRuntimeState();
|
|
1050
|
+
await deps.waitForTypingIdle?.();
|
|
1042
1051
|
deps.updateStatus();
|
|
1043
|
-
if (deps.isCurrentOwner && !deps.isCurrentOwner()) {
|
|
1044
|
-
if (turn) await deps.clearPreview(turn.chatId);
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
1052
|
const endPlan = buildTelegramAgentEndPlan({
|
|
1048
1053
|
hasTurn: !!turn,
|
|
1049
1054
|
stopReason: assistant.stopReason,
|
|
1050
1055
|
hasFinalText: !!finalText || hasOutboundArtifacts,
|
|
1051
1056
|
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
1052
|
-
|
|
1057
|
+
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1053
1058
|
});
|
|
1054
1059
|
if (!turn) {
|
|
1055
1060
|
if (
|
|
@@ -1072,10 +1077,6 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1072
1077
|
return;
|
|
1073
1078
|
}
|
|
1074
1079
|
if (turn.guestQueryId) {
|
|
1075
|
-
if (deps.isCurrentOwner && !deps.isCurrentOwner()) {
|
|
1076
|
-
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
1080
|
if (assistant.errorMessage) {
|
|
1080
1081
|
await deps.answerGuestQuery?.(
|
|
1081
1082
|
turn.guestQueryId,
|
|
@@ -1199,7 +1200,7 @@ export interface TelegramSessionShutdownState<TQueueItem> {
|
|
|
1199
1200
|
pendingTelegramModelSwitch: undefined;
|
|
1200
1201
|
telegramTurnDispatchPending: boolean;
|
|
1201
1202
|
compactionInProgress: boolean;
|
|
1202
|
-
|
|
1203
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
1203
1204
|
}
|
|
1204
1205
|
|
|
1205
1206
|
export interface TelegramSessionRuntimeCounterState {
|
|
@@ -1212,7 +1213,7 @@ export interface TelegramSessionRuntimeFlagState {
|
|
|
1212
1213
|
activeTelegramToolExecutions?: number;
|
|
1213
1214
|
telegramTurnDispatchPending?: boolean;
|
|
1214
1215
|
compactionInProgress?: boolean;
|
|
1215
|
-
|
|
1216
|
+
foldQueuedPromptsIntoHistory?: boolean;
|
|
1216
1217
|
}
|
|
1217
1218
|
|
|
1218
1219
|
export interface TelegramSessionStateApplierDeps<TQueueItem, TModel> {
|
|
@@ -1342,8 +1343,8 @@ export interface TelegramPromptEnqueueRuntimeDeps<
|
|
|
1342
1343
|
TMessage,
|
|
1343
1344
|
TContext = unknown,
|
|
1344
1345
|
> extends TelegramQueueStore<TContext> {
|
|
1345
|
-
|
|
1346
|
-
|
|
1346
|
+
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
1347
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1347
1348
|
createTurn: (
|
|
1348
1349
|
messages: TMessage[],
|
|
1349
1350
|
historyTurns: PendingTelegramTurn[],
|
|
@@ -1356,8 +1357,8 @@ export interface TelegramPromptEnqueueControllerDeps<
|
|
|
1356
1357
|
TMessage,
|
|
1357
1358
|
TContext = unknown,
|
|
1358
1359
|
> extends TelegramQueueStore<TContext> {
|
|
1359
|
-
|
|
1360
|
-
|
|
1360
|
+
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
1361
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1361
1362
|
createTurn: (
|
|
1362
1363
|
messages: TMessage[],
|
|
1363
1364
|
historyTurns: PendingTelegramTurn[],
|
|
@@ -1406,7 +1407,7 @@ export function buildTelegramSessionShutdownState<
|
|
|
1406
1407
|
pendingTelegramModelSwitch: undefined,
|
|
1407
1408
|
telegramTurnDispatchPending: false,
|
|
1408
1409
|
compactionInProgress: false,
|
|
1409
|
-
|
|
1410
|
+
foldQueuedPromptsIntoHistory: false,
|
|
1410
1411
|
};
|
|
1411
1412
|
}
|
|
1412
1413
|
|
|
@@ -1654,9 +1655,9 @@ export async function enqueueTelegramPromptTurnRuntime<
|
|
|
1654
1655
|
): Promise<void> {
|
|
1655
1656
|
const enqueuePlan = planTelegramPromptEnqueue(
|
|
1656
1657
|
deps.getQueuedItems(),
|
|
1657
|
-
deps.
|
|
1658
|
+
deps.getFoldQueuedPromptsIntoHistory(),
|
|
1658
1659
|
);
|
|
1659
|
-
deps.
|
|
1660
|
+
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
1660
1661
|
const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
|
|
1661
1662
|
deps.setQueuedItems(
|
|
1662
1663
|
appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
|
package/lib/routing.ts
CHANGED
|
@@ -342,7 +342,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
342
342
|
message: TMessage,
|
|
343
343
|
ctx: TContext,
|
|
344
344
|
): Promise<void> => {
|
|
345
|
-
deps.bridgeRuntime.lifecycle.
|
|
345
|
+
deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory(false);
|
|
346
346
|
const continueMessage = {
|
|
347
347
|
...message,
|
|
348
348
|
text: "continue",
|
|
@@ -374,8 +374,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
374
374
|
clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
|
|
375
375
|
hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
|
|
376
376
|
clearQueuedTelegramItems: deps.queueMutationRuntime.clear,
|
|
377
|
-
|
|
378
|
-
deps.bridgeRuntime.lifecycle.
|
|
377
|
+
setFoldQueuedPromptsIntoHistory:
|
|
378
|
+
deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
|
|
379
379
|
abortCurrentTurn: deps.bridgeRuntime.abort.abortTurn,
|
|
380
380
|
isIdle: deps.isIdle,
|
|
381
381
|
hasPendingMessages: deps.hasPendingMessages,
|
|
@@ -420,10 +420,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
420
420
|
TContext
|
|
421
421
|
>({
|
|
422
422
|
...deps.telegramQueueStore,
|
|
423
|
-
|
|
424
|
-
deps.bridgeRuntime.lifecycle.
|
|
425
|
-
|
|
426
|
-
deps.bridgeRuntime.lifecycle.
|
|
423
|
+
getFoldQueuedPromptsIntoHistory:
|
|
424
|
+
deps.bridgeRuntime.lifecycle.shouldFoldQueuedPromptsIntoHistory,
|
|
425
|
+
setFoldQueuedPromptsIntoHistory:
|
|
426
|
+
deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
|
|
427
427
|
createTurn: promptTurnBuilder,
|
|
428
428
|
updateStatus: deps.updateStatus,
|
|
429
429
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|