@llblab/pi-telegram 0.17.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -32
- package/BACKLOG.md +59 -14
- package/CHANGELOG.md +40 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/screenshot.png +0 -0
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/preview.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { normalizeTelegramNativeMarkdown } from "./replies.ts";
|
|
8
8
|
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
|
|
9
|
+
import {
|
|
10
|
+
getTelegramTargetThreadParams,
|
|
11
|
+
type TelegramTarget,
|
|
12
|
+
} from "./target.ts";
|
|
9
13
|
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
|
10
14
|
|
|
11
15
|
const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
|
|
@@ -29,9 +33,7 @@ export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
|
|
|
29
33
|
|
|
30
34
|
export type TelegramPreviewReplyMarkup = unknown;
|
|
31
35
|
|
|
32
|
-
export interface TelegramPreviewRuntimeDeps
|
|
33
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
34
|
-
> {
|
|
36
|
+
export interface TelegramPreviewRuntimeDeps {
|
|
35
37
|
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
36
38
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
37
39
|
clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
|
|
@@ -59,6 +61,8 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
59
61
|
|
|
60
62
|
export interface TelegramPreviewActiveTurn {
|
|
61
63
|
chatId: number;
|
|
64
|
+
replyToMessageId?: number;
|
|
65
|
+
target?: TelegramTarget;
|
|
62
66
|
voiceReplyPreferred?: boolean;
|
|
63
67
|
voiceReplyRequired?: boolean;
|
|
64
68
|
}
|
|
@@ -77,7 +81,7 @@ export interface TelegramAssistantMessagePreviewStartDeps<
|
|
|
77
81
|
chatId: number,
|
|
78
82
|
markdown: string,
|
|
79
83
|
replyToMessageId?: number,
|
|
80
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
84
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
81
85
|
) => Promise<boolean>;
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -88,7 +92,10 @@ export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
|
|
|
88
92
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
89
93
|
createPreviewState: () => TelegramPreviewRuntimeState;
|
|
90
94
|
getMessageText: (message: TMessage) => string;
|
|
91
|
-
schedulePreviewFlush: (
|
|
95
|
+
schedulePreviewFlush: (
|
|
96
|
+
chatId: number,
|
|
97
|
+
options?: { target?: TelegramTarget },
|
|
98
|
+
) => void;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
export type TelegramAssistantMessagePreviewHookDeps<
|
|
@@ -110,9 +117,7 @@ export interface TelegramAssistantMessagePreviewHooks<TMessage> {
|
|
|
110
117
|
) => Promise<void>;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
export interface TelegramPreviewControllerDeps
|
|
114
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
115
|
-
> {
|
|
120
|
+
export interface TelegramPreviewControllerDeps {
|
|
116
121
|
getDefaultReplyToMessageId?: () => number | undefined;
|
|
117
122
|
maxMessageLength?: number;
|
|
118
123
|
initialDraftSupport?: TelegramDraftSupport;
|
|
@@ -141,29 +146,36 @@ export interface TelegramPreviewControllerDeps<
|
|
|
141
146
|
) => void;
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
export interface TelegramPreviewController
|
|
145
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
146
|
-
> {
|
|
149
|
+
export interface TelegramPreviewController {
|
|
147
150
|
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
148
151
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
149
152
|
setPendingText: (text: string) => void;
|
|
150
153
|
createState: () => TelegramPreviewRuntimeState;
|
|
151
154
|
resetState: () => void;
|
|
152
|
-
clear: (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
clear: (
|
|
156
|
+
chatId: number,
|
|
157
|
+
options?: { target?: TelegramTarget },
|
|
158
|
+
) => Promise<void>;
|
|
159
|
+
flush: (
|
|
160
|
+
chatId: number,
|
|
161
|
+
options?: { target?: TelegramTarget },
|
|
162
|
+
) => Promise<void>;
|
|
163
|
+
scheduleFlush: (
|
|
164
|
+
chatId: number,
|
|
165
|
+
options?: { target?: TelegramTarget },
|
|
166
|
+
) => void;
|
|
167
|
+
finalize: (
|
|
168
|
+
chatId: number,
|
|
169
|
+
replyToMessageId?: number,
|
|
170
|
+
options?: { target?: TelegramTarget },
|
|
171
|
+
) => Promise<boolean>;
|
|
156
172
|
}
|
|
157
173
|
|
|
158
|
-
export type TelegramPreviewControllerRuntimeDeps
|
|
159
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
160
|
-
> = TelegramPreviewControllerDeps<TReplyMarkup>;
|
|
174
|
+
export type TelegramPreviewControllerRuntimeDeps = TelegramPreviewControllerDeps;
|
|
161
175
|
|
|
162
|
-
export function createTelegramPreviewControllerRuntime
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
deps: TelegramPreviewControllerRuntimeDeps<TReplyMarkup>,
|
|
166
|
-
): TelegramPreviewController<TReplyMarkup> {
|
|
176
|
+
export function createTelegramPreviewControllerRuntime(
|
|
177
|
+
deps: TelegramPreviewControllerRuntimeDeps,
|
|
178
|
+
): TelegramPreviewController {
|
|
167
179
|
return createTelegramPreviewController({
|
|
168
180
|
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
|
169
181
|
maxMessageLength: deps.maxMessageLength,
|
|
@@ -180,7 +192,7 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
180
192
|
export interface TelegramAssistantPreviewRuntimeDeps<
|
|
181
193
|
TMessage,
|
|
182
194
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
183
|
-
> extends TelegramPreviewControllerRuntimeDeps
|
|
195
|
+
> extends TelegramPreviewControllerRuntimeDeps {
|
|
184
196
|
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
185
197
|
isAssistantMessage: (message: TMessage) => boolean;
|
|
186
198
|
getMessageText: (message: TMessage) => string;
|
|
@@ -188,38 +200,43 @@ export interface TelegramAssistantPreviewRuntimeDeps<
|
|
|
188
200
|
chatId: number,
|
|
189
201
|
replyToMessageId: number | undefined,
|
|
190
202
|
markdown: string,
|
|
191
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
203
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
192
204
|
) => Promise<number | undefined>;
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
export type TelegramAssistantPreviewRuntime<
|
|
196
208
|
TMessage,
|
|
197
209
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
198
|
-
> = TelegramPreviewController
|
|
210
|
+
> = TelegramPreviewController &
|
|
199
211
|
TelegramAssistantMessagePreviewHooks<TMessage> & {
|
|
200
212
|
finalizeMarkdown: (
|
|
201
213
|
chatId: number,
|
|
202
214
|
markdown: string,
|
|
203
215
|
replyToMessageId?: number,
|
|
204
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
216
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
205
217
|
) => Promise<boolean>;
|
|
206
218
|
};
|
|
207
219
|
|
|
208
|
-
export function createTelegramNativeMarkdownPreviewFinalizer<
|
|
220
|
+
export function createTelegramNativeMarkdownPreviewFinalizer<
|
|
221
|
+
TReplyMarkup,
|
|
222
|
+
>(deps: {
|
|
209
223
|
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
210
|
-
clear: (
|
|
224
|
+
clear: (
|
|
225
|
+
chatId: number,
|
|
226
|
+
options?: { target?: TelegramTarget },
|
|
227
|
+
) => Promise<void>;
|
|
211
228
|
discard?: () => void;
|
|
212
229
|
sendMarkdownReply: (
|
|
213
230
|
chatId: number,
|
|
214
231
|
replyToMessageId: number | undefined,
|
|
215
232
|
markdown: string,
|
|
216
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
233
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
217
234
|
) => Promise<number | undefined>;
|
|
218
235
|
}): (
|
|
219
236
|
chatId: number,
|
|
220
237
|
markdown: string,
|
|
221
238
|
replyToMessageId?: number,
|
|
222
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
239
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
223
240
|
) => Promise<boolean> {
|
|
224
241
|
return async (chatId, markdown, replyToMessageId, options) => {
|
|
225
242
|
const state = deps.getState();
|
|
@@ -262,11 +279,9 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
262
279
|
};
|
|
263
280
|
}
|
|
264
281
|
|
|
265
|
-
export function createTelegramPreviewController
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
deps: TelegramPreviewControllerDeps<TReplyMarkup>,
|
|
269
|
-
): TelegramPreviewController<TReplyMarkup> {
|
|
282
|
+
export function createTelegramPreviewController(
|
|
283
|
+
deps: TelegramPreviewControllerDeps,
|
|
284
|
+
): TelegramPreviewController {
|
|
270
285
|
let state: TelegramPreviewRuntimeState | undefined;
|
|
271
286
|
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
272
287
|
const setTimer =
|
|
@@ -279,7 +294,7 @@ export function createTelegramPreviewController<
|
|
|
279
294
|
deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
|
|
280
295
|
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
|
281
296
|
let nextDraftId = 0;
|
|
282
|
-
const getRuntimeDeps = (): TelegramPreviewRuntimeDeps
|
|
297
|
+
const getRuntimeDeps = (): TelegramPreviewRuntimeDeps => ({
|
|
283
298
|
getState: () => state,
|
|
284
299
|
setState: (nextState) => {
|
|
285
300
|
state = nextState;
|
|
@@ -310,25 +325,27 @@ export function createTelegramPreviewController<
|
|
|
310
325
|
setPendingText: (text) => {
|
|
311
326
|
if (state) state.pendingText = text;
|
|
312
327
|
},
|
|
313
|
-
createState: () => createTelegramPreviewRuntimeState(
|
|
328
|
+
createState: () => createTelegramPreviewRuntimeState(),
|
|
314
329
|
resetState: () => {
|
|
315
|
-
state = createTelegramPreviewRuntimeState(
|
|
330
|
+
state = createTelegramPreviewRuntimeState();
|
|
316
331
|
},
|
|
317
|
-
clear: (chatId) =>
|
|
318
|
-
|
|
319
|
-
|
|
332
|
+
clear: (chatId, options) =>
|
|
333
|
+
clearTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
334
|
+
flush: (chatId, options) =>
|
|
335
|
+
flushTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
336
|
+
scheduleFlush: (chatId, options) => {
|
|
320
337
|
if (!state || state.flushTimer) return;
|
|
321
338
|
if (throttleMs <= 0) {
|
|
322
|
-
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
339
|
+
void flushTelegramPreview(chatId, getRuntimeDeps(), options);
|
|
323
340
|
return;
|
|
324
341
|
}
|
|
325
342
|
state.flushTimer = setTimer(() => {
|
|
326
|
-
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
343
|
+
void flushTelegramPreview(chatId, getRuntimeDeps(), options);
|
|
327
344
|
}, throttleMs);
|
|
328
345
|
state.flushTimer.unref?.();
|
|
329
346
|
},
|
|
330
|
-
finalize: (chatId, _replyToMessageId) =>
|
|
331
|
-
finalizeTelegramPreview(chatId, getRuntimeDeps()),
|
|
347
|
+
finalize: (chatId, _replyToMessageId, options) =>
|
|
348
|
+
finalizeTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
332
349
|
};
|
|
333
350
|
}
|
|
334
351
|
|
|
@@ -373,7 +390,14 @@ export async function handleTelegramAssistantMessagePreviewStart<
|
|
|
373
390
|
) {
|
|
374
391
|
const previousText = state.pendingText.trim();
|
|
375
392
|
if (previousText.length > 0) {
|
|
376
|
-
await deps.finalizeMarkdownPreview(
|
|
393
|
+
await deps.finalizeMarkdownPreview(
|
|
394
|
+
turn.chatId,
|
|
395
|
+
previousText,
|
|
396
|
+
turn.replyToMessageId,
|
|
397
|
+
{
|
|
398
|
+
target: turn.target,
|
|
399
|
+
},
|
|
400
|
+
);
|
|
377
401
|
} else {
|
|
378
402
|
await deps.finalizePreview(turn.chatId);
|
|
379
403
|
}
|
|
@@ -396,7 +420,7 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
|
|
|
396
420
|
state.pendingText = stripTelegramCommentMarkupForPreview(
|
|
397
421
|
deps.getMessageText(message),
|
|
398
422
|
);
|
|
399
|
-
deps.schedulePreviewFlush(turn.chatId);
|
|
423
|
+
deps.schedulePreviewFlush(turn.chatId, { target: turn.target });
|
|
400
424
|
}
|
|
401
425
|
|
|
402
426
|
export function buildTelegramPreviewFinalText(
|
|
@@ -407,9 +431,7 @@ export function buildTelegramPreviewFinalText(
|
|
|
407
431
|
return state.lastSentText.trim() || undefined;
|
|
408
432
|
}
|
|
409
433
|
|
|
410
|
-
export function createTelegramPreviewRuntimeState(
|
|
411
|
-
draftSupport: TelegramDraftSupport,
|
|
412
|
-
): TelegramPreviewRuntimeState {
|
|
434
|
+
export function createTelegramPreviewRuntimeState(): TelegramPreviewRuntimeState {
|
|
413
435
|
return {
|
|
414
436
|
mode: "draft",
|
|
415
437
|
pendingText: "",
|
|
@@ -435,12 +457,10 @@ export function shouldUseTelegramDraftPreview(_options: {
|
|
|
435
457
|
return true;
|
|
436
458
|
}
|
|
437
459
|
|
|
438
|
-
export async function clearTelegramPreview
|
|
439
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
440
|
-
>(
|
|
460
|
+
export async function clearTelegramPreview(
|
|
441
461
|
chatId: number,
|
|
442
|
-
deps: TelegramPreviewRuntimeDeps
|
|
443
|
-
options: { awaitFlush?: boolean } = {},
|
|
462
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
463
|
+
options: { awaitFlush?: boolean; target?: TelegramTarget } = {},
|
|
444
464
|
): Promise<void> {
|
|
445
465
|
const state = deps.getState();
|
|
446
466
|
if (!state) return;
|
|
@@ -453,7 +473,9 @@ export async function clearTelegramPreview<
|
|
|
453
473
|
deps.setState(undefined);
|
|
454
474
|
if (state.mode === "draft" && state.draftId !== undefined) {
|
|
455
475
|
try {
|
|
456
|
-
await deps.sendDraft(chatId, state.draftId, undefined
|
|
476
|
+
await deps.sendDraft(chatId, state.draftId, undefined, {
|
|
477
|
+
...getTelegramTargetThreadParams(options.target ?? { chatId }),
|
|
478
|
+
});
|
|
457
479
|
} catch (error) {
|
|
458
480
|
deps.recordRuntimeEvent?.("preview", error, {
|
|
459
481
|
phase: "clear-draft",
|
|
@@ -493,8 +515,11 @@ function createTelegramDraftInlineState(): TelegramDraftInlineState {
|
|
|
493
515
|
};
|
|
494
516
|
}
|
|
495
517
|
|
|
496
|
-
function isTelegramDraftInlineStateClosed(
|
|
497
|
-
|
|
518
|
+
function isTelegramDraftInlineStateClosed(
|
|
519
|
+
state: TelegramDraftInlineState,
|
|
520
|
+
): boolean {
|
|
521
|
+
return (
|
|
522
|
+
state.codeTicks === 0 &&
|
|
498
523
|
!state.htmlComment &&
|
|
499
524
|
!state.displayMath &&
|
|
500
525
|
!state.fence &&
|
|
@@ -504,7 +529,8 @@ function isTelegramDraftInlineStateClosed(state: TelegramDraftInlineState): bool
|
|
|
504
529
|
!state.emphasisUnderscore &&
|
|
505
530
|
!state.strike &&
|
|
506
531
|
!state.linkText &&
|
|
507
|
-
!state.linkDestination
|
|
532
|
+
!state.linkDestination
|
|
533
|
+
);
|
|
508
534
|
}
|
|
509
535
|
|
|
510
536
|
function countRepeatedChars(text: string, index: number, char: string): number {
|
|
@@ -515,16 +541,25 @@ function countRepeatedChars(text: string, index: number, char: string): number {
|
|
|
515
541
|
|
|
516
542
|
function isEscapedMarkdownChar(text: string, index: number): boolean {
|
|
517
543
|
let slashCount = 0;
|
|
518
|
-
for (
|
|
544
|
+
for (
|
|
545
|
+
let cursor = index - 1;
|
|
546
|
+
cursor >= 0 && text[cursor] === "\\";
|
|
547
|
+
cursor -= 1
|
|
548
|
+
) {
|
|
519
549
|
slashCount += 1;
|
|
520
550
|
}
|
|
521
551
|
return slashCount % 2 === 1;
|
|
522
552
|
}
|
|
523
553
|
|
|
524
|
-
function isInlineDelimiterCandidate(
|
|
554
|
+
function isInlineDelimiterCandidate(
|
|
555
|
+
text: string,
|
|
556
|
+
index: number,
|
|
557
|
+
length: number,
|
|
558
|
+
): boolean {
|
|
525
559
|
const previous = text[index - 1] ?? "";
|
|
526
560
|
const next = text[index + length] ?? "";
|
|
527
|
-
if (!next || /\s/.test(next))
|
|
561
|
+
if (!next || /\s/.test(next))
|
|
562
|
+
return previous.length > 0 && !/\s/.test(previous);
|
|
528
563
|
if (!previous || /\s/.test(previous)) return true;
|
|
529
564
|
return /[\p{P}\p{S}]/u.test(previous) || /[\p{P}\p{S}]/u.test(next);
|
|
530
565
|
}
|
|
@@ -580,12 +615,18 @@ function updateTelegramDraftInlineStateForLine(
|
|
|
580
615
|
state.linkDestination = false;
|
|
581
616
|
continue;
|
|
582
617
|
}
|
|
583
|
-
if (
|
|
618
|
+
if (
|
|
619
|
+
line.startsWith("~~", index) &&
|
|
620
|
+
isInlineDelimiterCandidate(line, index, 2)
|
|
621
|
+
) {
|
|
584
622
|
state.strike = !state.strike;
|
|
585
623
|
index += 1;
|
|
586
624
|
continue;
|
|
587
625
|
}
|
|
588
|
-
if (
|
|
626
|
+
if (
|
|
627
|
+
line.startsWith("**", index) &&
|
|
628
|
+
isInlineDelimiterCandidate(line, index, 2)
|
|
629
|
+
) {
|
|
589
630
|
state.strongAsterisk = !state.strongAsterisk;
|
|
590
631
|
index += 1;
|
|
591
632
|
continue;
|
|
@@ -594,7 +635,10 @@ function updateTelegramDraftInlineStateForLine(
|
|
|
594
635
|
state.emphasisAsterisk = !state.emphasisAsterisk;
|
|
595
636
|
continue;
|
|
596
637
|
}
|
|
597
|
-
if (
|
|
638
|
+
if (
|
|
639
|
+
line.startsWith("__", index) &&
|
|
640
|
+
isInlineDelimiterCandidate(line, index, 2)
|
|
641
|
+
) {
|
|
598
642
|
state.strongUnderscore = !state.strongUnderscore;
|
|
599
643
|
index += 1;
|
|
600
644
|
continue;
|
|
@@ -611,7 +655,11 @@ function updateTelegramDraftBlockStateForLine(
|
|
|
611
655
|
): boolean {
|
|
612
656
|
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
613
657
|
if (state.fence) {
|
|
614
|
-
if (
|
|
658
|
+
if (
|
|
659
|
+
new RegExp(
|
|
660
|
+
`^ {0,3}${state.fence.marker}{${state.fence.length},}\\s*$`,
|
|
661
|
+
).test(line)
|
|
662
|
+
) {
|
|
615
663
|
state.fence = undefined;
|
|
616
664
|
}
|
|
617
665
|
return true;
|
|
@@ -622,7 +670,10 @@ function updateTelegramDraftBlockStateForLine(
|
|
|
622
670
|
}
|
|
623
671
|
if (fenceMatch) {
|
|
624
672
|
const markerText = fenceMatch[1] ?? "```";
|
|
625
|
-
state.fence = {
|
|
673
|
+
state.fence = {
|
|
674
|
+
marker: markerText[0] as "`" | "~",
|
|
675
|
+
length: markerText.length,
|
|
676
|
+
};
|
|
626
677
|
return true;
|
|
627
678
|
}
|
|
628
679
|
if (line.trim() === "$$") {
|
|
@@ -654,9 +705,10 @@ export function getSafeTelegramRichMarkdownDraftPrefix(
|
|
|
654
705
|
): string | undefined {
|
|
655
706
|
const source = markdown.trim();
|
|
656
707
|
if (!source) return undefined;
|
|
657
|
-
const limited =
|
|
658
|
-
|
|
659
|
-
|
|
708
|
+
const limited =
|
|
709
|
+
source.length > maxMessageLength
|
|
710
|
+
? source.slice(0, maxMessageLength)
|
|
711
|
+
: source;
|
|
660
712
|
const safeEnd = findSafeTelegramRichMarkdownDraftEnd(limited);
|
|
661
713
|
if (safeEnd > 0) return limited.slice(0, safeEnd).trimEnd() || undefined;
|
|
662
714
|
let candidateEnd = limited.length;
|
|
@@ -683,15 +735,17 @@ function buildTelegramNativeMarkdownPreviewSnapshot(
|
|
|
683
735
|
return { text: safeText };
|
|
684
736
|
}
|
|
685
737
|
|
|
686
|
-
async function performTelegramPreviewFlush
|
|
687
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
688
|
-
>(
|
|
738
|
+
async function performTelegramPreviewFlush(
|
|
689
739
|
chatId: number,
|
|
690
740
|
state: TelegramPreviewRuntimeState,
|
|
691
|
-
deps: TelegramPreviewRuntimeDeps
|
|
741
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
742
|
+
options: { target?: TelegramTarget } = {},
|
|
692
743
|
): Promise<void> {
|
|
693
744
|
if (deps.canSend && !deps.canSend()) {
|
|
694
|
-
await clearTelegramPreview(chatId, deps, {
|
|
745
|
+
await clearTelegramPreview(chatId, deps, {
|
|
746
|
+
awaitFlush: false,
|
|
747
|
+
target: options.target,
|
|
748
|
+
});
|
|
695
749
|
return;
|
|
696
750
|
}
|
|
697
751
|
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
|
@@ -712,6 +766,7 @@ async function performTelegramPreviewFlush<
|
|
|
712
766
|
chatId,
|
|
713
767
|
draftId,
|
|
714
768
|
normalizeTelegramNativeMarkdown(snapshot.text),
|
|
769
|
+
{ ...getTelegramTargetThreadParams(options.target ?? { chatId }) },
|
|
715
770
|
);
|
|
716
771
|
deps.setDraftSupport("supported");
|
|
717
772
|
state.mode = "draft";
|
|
@@ -728,11 +783,10 @@ async function performTelegramPreviewFlush<
|
|
|
728
783
|
}
|
|
729
784
|
}
|
|
730
785
|
|
|
731
|
-
export async function flushTelegramPreview
|
|
732
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
733
|
-
>(
|
|
786
|
+
export async function flushTelegramPreview(
|
|
734
787
|
chatId: number,
|
|
735
|
-
deps: TelegramPreviewRuntimeDeps
|
|
788
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
789
|
+
options: { target?: TelegramTarget } = {},
|
|
736
790
|
): Promise<void> {
|
|
737
791
|
const state = deps.getState();
|
|
738
792
|
if (!state) return;
|
|
@@ -746,7 +800,7 @@ export async function flushTelegramPreview<
|
|
|
746
800
|
do {
|
|
747
801
|
state.flushRequested = false;
|
|
748
802
|
try {
|
|
749
|
-
await performTelegramPreviewFlush(chatId, state, deps);
|
|
803
|
+
await performTelegramPreviewFlush(chatId, state, deps, options);
|
|
750
804
|
} catch (error) {
|
|
751
805
|
deps.recordRuntimeEvent?.("preview", error, {
|
|
752
806
|
phase: "flush",
|
|
@@ -766,22 +820,21 @@ export async function flushTelegramPreview<
|
|
|
766
820
|
}
|
|
767
821
|
}
|
|
768
822
|
|
|
769
|
-
export async function finalizeTelegramPreview
|
|
770
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
771
|
-
>(
|
|
823
|
+
export async function finalizeTelegramPreview(
|
|
772
824
|
chatId: number,
|
|
773
|
-
deps: TelegramPreviewRuntimeDeps
|
|
825
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
826
|
+
options: { target?: TelegramTarget } = {},
|
|
774
827
|
): Promise<boolean> {
|
|
775
828
|
const state = deps.getState();
|
|
776
829
|
if (!state) return false;
|
|
777
830
|
if (deps.canSend && !deps.canSend()) {
|
|
778
|
-
await clearTelegramPreview(chatId, deps);
|
|
831
|
+
await clearTelegramPreview(chatId, deps, options);
|
|
779
832
|
return false;
|
|
780
833
|
}
|
|
781
|
-
await flushTelegramPreview(chatId, deps);
|
|
834
|
+
await flushTelegramPreview(chatId, deps, options);
|
|
782
835
|
const finalText = buildTelegramPreviewFinalText(state);
|
|
783
836
|
if (!finalText) {
|
|
784
|
-
await clearTelegramPreview(chatId, deps);
|
|
837
|
+
await clearTelegramPreview(chatId, deps, options);
|
|
785
838
|
return false;
|
|
786
839
|
}
|
|
787
840
|
deps.setState(undefined);
|
package/lib/prompt-templates.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pi prompt-template bridge helpers
|
|
3
3
|
* Zones: pi agent prompts, telegram controls, filesystem
|
|
4
|
-
* Discovers
|
|
4
|
+
* Discovers Pi prompt-template slash commands and expands them before Telegram queue dispatch
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
@@ -129,7 +129,7 @@ export function getTelegramPromptTemplateCommands(
|
|
|
129
129
|
export function createTelegramPromptTemplateCommandGetter(
|
|
130
130
|
deps: TelegramPromptTemplateCommandGetterDeps,
|
|
131
131
|
): () => TelegramPromptTemplateCommand[] {
|
|
132
|
-
return
|
|
132
|
+
return () => {
|
|
133
133
|
return getTelegramPromptTemplateCommands(
|
|
134
134
|
deps.getCommands(),
|
|
135
135
|
new Set([
|
package/lib/prompts.ts
CHANGED
|
@@ -4,41 +4,85 @@
|
|
|
4
4
|
* Owns Telegram-specific system prompt suffixes injected into pi agent turns
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
|
|
9
|
+
import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi.ts";
|
|
8
10
|
import { TELEGRAM_PREFIX } from "./turns.ts";
|
|
9
11
|
|
|
10
12
|
const LOCAL_SYSTEM_PROMPT_SUFFIX = `
|
|
11
13
|
|
|
12
|
-
Telegram bridge
|
|
13
|
-
|
|
14
|
-
Local/TUI Telegram delivery:
|
|
15
|
-
- Answer ordinary local prompts normally; do not add Telegram action comments unless the user explicitly asks for Telegram delivery.
|
|
16
|
-
- For explicit Telegram file delivery, call \`telegram_attach(local_path)\`. For explicit Telegram text delivery, call \`telegram_message(...)\`.
|
|
17
|
-
- 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.
|
|
18
|
-
`;
|
|
14
|
+
Telegram bridge available. Do not use it from local/TUI prompts unless explicitly asked.`;
|
|
19
15
|
|
|
20
16
|
const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
21
17
|
|
|
22
|
-
Telegram
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- \`[
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
- \`
|
|
40
|
-
-
|
|
41
|
-
|
|
18
|
+
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`.`;
|
|
19
|
+
|
|
20
|
+
const TELEGRAM_HELP_TEXT = `--- TELEGRAM BRIDGE HELP ---
|
|
21
|
+
|
|
22
|
+
How to understand Telegram turns:
|
|
23
|
+
- \`[telegram|thread:name|from:user|guest:group]\` marks Telegram origin and attributes.
|
|
24
|
+
- \`thread\` is the visible Thread identity in Threaded Mode; it is not a bus role.
|
|
25
|
+
- \`[reply]\` is quoted context only; act on the user's current instruction.
|
|
26
|
+
- \`[attachments]\` are local files; \`[outputs]\` are handler results/transcripts; \`[time]\` is wall-clock context; \`[voice]\` gives reply-mode policy.
|
|
27
|
+
|
|
28
|
+
How to answer Telegram turns:
|
|
29
|
+
- Reply in concise, scannable mobile Telegram Rich Markdown.
|
|
30
|
+
- Use \`$...$\` for inline math and \`$$...$$\` for block math.
|
|
31
|
+
- Real code blocks must stay literal.
|
|
32
|
+
- For generated/requested files, call \`telegram_attach(local_path)\`; do not only mention the path.
|
|
33
|
+
|
|
34
|
+
Assistant-authored Telegram actions:
|
|
35
|
+
- \`telegram_voice\` and \`telegram_button\` are hidden top-level HTML comments, not Pi tools.
|
|
36
|
+
- Put action comments at column zero, outside code, quotes, lists, and indented examples.
|
|
37
|
+
- Voice forms: \`<!-- telegram_voice text="Short summary" -->\` or \`<!-- telegram_voice: Short summary -->\`.
|
|
38
|
+
- Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
|
|
39
|
+
- Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
|
|
40
|
+
- Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\`.
|
|
41
|
+
- If hidden comments would be the whole reply, add visible text such as \`Choose one:\`.
|
|
42
|
+
|
|
43
|
+
Local/TUI direct delivery:
|
|
44
|
+
- Do not send Telegram actions from local/TUI prompts unless explicitly asked.
|
|
45
|
+
- Use \`telegram_attach\` for files and \`telegram_message\` for direct Markdown text.
|
|
46
|
+
- Direct delivery requires this Pi instance to own \`/telegram-connect\` or be registered with the Threaded Mode bus.
|
|
47
|
+
- For explicit targets, pass \`chat_id\` plus optional \`thread_id\`; registered followers default to their assigned Thread target.
|
|
48
|
+
- Do not use \`telegram_message\` for ordinary Telegram-originated replies; answer normally and let the bridge deliver the active turn reply.
|
|
49
|
+
|
|
50
|
+
Threaded Mode:
|
|
51
|
+
- pi-telegram supports private-chat Threaded Mode when BotFather Topics/Threaded Mode are enabled.
|
|
52
|
+
- Product/user language is Thread; Bot API primitive names may still say topic.
|
|
53
|
+
- Threaded Mode has one leader transport and visible follower Pi processes joined manually through \`/telegram-connect\`.
|
|
54
|
+
- Thread names are bridge-assigned or preserved identities; do not invent rename prompts or use a rename tool.
|
|
55
|
+
- The \`All\` surface is for routing/control, not hidden Pi process creation.
|
|
56
|
+
|
|
57
|
+
Configurable handlers:
|
|
58
|
+
- \`telegram.json\` can add no-code \`inboundHandlers\`/\`outboundHandlers\` using command templates before writing an extension.
|
|
59
|
+
- For speech-to-text, configure an \`inboundHandlers\` entry matching \`type: "voice"\` or \`mime: "audio/*"\`; stdout becomes \`[outputs]\` prompt context.
|
|
60
|
+
- If command-template config is not enough, build a companion extension through the public pi-telegram APIs; do not import package-private \`lib/*\` paths.
|
|
61
|
+
|
|
62
|
+
Debugging pi-telegram:
|
|
63
|
+
- Inspect \`~/.pi/agent/tmp/telegram/state.json\` for runtime state, roster, bindings, slots, reservations, and diagnostics.
|
|
64
|
+
- Inspect \`~/.pi/agent/tmp/telegram/logs.jsonl\` for redacted runtime event evidence.
|
|
65
|
+
- Use terminal \`telegram-status\` for compact human health; use \`telegram-status --debug\` for the full human-readable diagnostic dump.`;
|
|
66
|
+
|
|
67
|
+
export function getTelegramHelpText(): string {
|
|
68
|
+
return TELEGRAM_HELP_TEXT;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function registerTelegramHelpTool(pi: ExtensionAPI): void {
|
|
72
|
+
pi.registerTool({
|
|
73
|
+
name: "telegram_help",
|
|
74
|
+
label: "Telegram Help",
|
|
75
|
+
description:
|
|
76
|
+
"Read pi-telegram usage guidance for delivery actions, Threaded Mode, handlers, formatting, and debugging.",
|
|
77
|
+
parameters: Type.Object({}),
|
|
78
|
+
async execute() {
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: getTelegramHelpText() }],
|
|
81
|
+
details: {},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
42
86
|
|
|
43
87
|
export function buildTelegramBridgeSystemPrompt(options: {
|
|
44
88
|
prompt: string;
|
|
@@ -48,7 +92,13 @@ export function buildTelegramBridgeSystemPrompt(options: {
|
|
|
48
92
|
telegramTurnSystemPromptSuffix: string;
|
|
49
93
|
}): { systemPrompt: string } {
|
|
50
94
|
const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
|
|
51
|
-
const
|
|
95
|
+
const telegramHead = telegramPrefix.endsWith("]")
|
|
96
|
+
? telegramPrefix.slice(0, -1)
|
|
97
|
+
: telegramPrefix;
|
|
98
|
+
const trimmedPrompt = options.prompt.trimStart();
|
|
99
|
+
const telegramTurn =
|
|
100
|
+
trimmedPrompt.startsWith(`${telegramHead}]`) ||
|
|
101
|
+
trimmedPrompt.startsWith(`${telegramHead}|`);
|
|
52
102
|
const telegramSuffix = telegramTurn
|
|
53
103
|
? `${options.telegramTurnSystemPromptSuffix}\n- The current user message came from Telegram.`
|
|
54
104
|
: "";
|
|
@@ -92,7 +142,7 @@ export function createTelegramProactiveBeforeAgentStartHook<TContext>(
|
|
|
92
142
|
ctx: TContext,
|
|
93
143
|
) => Promise<{ systemPrompt: string }> {
|
|
94
144
|
const baseHook = deps.baseHook ?? createTelegramBeforeAgentStartHook();
|
|
95
|
-
return async
|
|
145
|
+
return async (event, ctx) => {
|
|
96
146
|
if (!deps.isConfigured()) return { systemPrompt: event.systemPrompt };
|
|
97
147
|
const result = baseHook(event);
|
|
98
148
|
if (!deps.isProactivePushEnabled()) return result;
|