@llblab/pi-telegram 0.2.10 → 0.4.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.
Files changed (47) hide show
  1. package/README.md +52 -19
  2. package/docs/README.md +2 -3
  3. package/docs/architecture.md +62 -31
  4. package/docs/locks.md +136 -0
  5. package/index.ts +323 -1880
  6. package/lib/api.ts +396 -60
  7. package/lib/attachments.ts +128 -16
  8. package/lib/commands.ts +648 -14
  9. package/lib/config.ts +169 -0
  10. package/lib/handlers.ts +474 -0
  11. package/lib/locks.ts +306 -0
  12. package/lib/media.ts +196 -46
  13. package/lib/menu.ts +920 -338
  14. package/lib/model.ts +647 -0
  15. package/lib/pi.ts +90 -0
  16. package/lib/polling.ts +240 -14
  17. package/lib/preview.ts +420 -25
  18. package/lib/queue.ts +1137 -110
  19. package/lib/registration.ts +214 -31
  20. package/lib/rendering.ts +560 -366
  21. package/lib/replies.ts +198 -8
  22. package/lib/routing.ts +217 -0
  23. package/lib/runtime.ts +475 -0
  24. package/lib/setup.ts +143 -1
  25. package/lib/status.ts +432 -13
  26. package/lib/turns.ts +217 -36
  27. package/lib/updates.ts +340 -109
  28. package/package.json +18 -3
  29. package/AGENTS.md +0 -91
  30. package/BACKLOG.md +0 -5
  31. package/CHANGELOG.md +0 -34
  32. package/lib/model-switch.ts +0 -62
  33. package/lib/types.ts +0 -137
  34. package/tests/api.test.ts +0 -331
  35. package/tests/attachments.test.ts +0 -132
  36. package/tests/commands.test.ts +0 -85
  37. package/tests/config.test.ts +0 -80
  38. package/tests/media.test.ts +0 -166
  39. package/tests/menu.test.ts +0 -676
  40. package/tests/polling.test.ts +0 -202
  41. package/tests/preview.test.ts +0 -480
  42. package/tests/queue.test.ts +0 -3245
  43. package/tests/registration.test.ts +0 -268
  44. package/tests/rendering.test.ts +0 -526
  45. package/tests/replies.test.ts +0 -142
  46. package/tests/turns.test.ts +0 -247
  47. package/tests/updates.test.ts +0 -416
package/lib/commands.ts CHANGED
@@ -1,20 +1,73 @@
1
1
  /**
2
- * Telegram command parsing helpers
3
- * Owns slash-command normalization so command routing stays separate from transport update handling
2
+ * Telegram command routing helpers
3
+ * Owns slash-command normalization and command side-effect branching behind runtime ports
4
4
  */
5
5
 
6
+ import { pairTelegramUserIfNeeded } from "./config.ts";
7
+ import {
8
+ createTelegramControlItemBuilder,
9
+ createTelegramControlQueueController,
10
+ type PendingTelegramControlItem,
11
+ } from "./queue.ts";
12
+
6
13
  export interface ParsedTelegramCommand {
7
14
  name: string;
8
15
  args: string;
9
16
  }
10
17
 
18
+ export interface TelegramBotCommandDefinition {
19
+ command: string;
20
+ description: string;
21
+ }
22
+
23
+ export const TELEGRAM_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [
24
+ {
25
+ command: "start",
26
+ description: "Show help and pair the Telegram bridge",
27
+ },
28
+ {
29
+ command: "status",
30
+ description: "Show model, usage, cost, and context status",
31
+ },
32
+ { command: "model", description: "Open the interactive model selector" },
33
+ { command: "compact", description: "Compact the current pi session" },
34
+ { command: "stop", description: "Abort the current pi task" },
35
+ ];
36
+
37
+ export interface TelegramBotCommandRegistrationDeps {
38
+ setMyCommands: (
39
+ commands: readonly TelegramBotCommandDefinition[],
40
+ ) => Promise<unknown>;
41
+ }
42
+
43
+ export async function registerTelegramBotCommands(
44
+ deps: TelegramBotCommandRegistrationDeps,
45
+ ): Promise<void> {
46
+ await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
47
+ }
48
+
49
+ export function createTelegramBotCommandRegistrar(
50
+ deps: TelegramBotCommandRegistrationDeps,
51
+ ): () => Promise<void> {
52
+ return () => registerTelegramBotCommands(deps);
53
+ }
54
+
11
55
  export type TelegramCommandAction =
12
- | { kind: "ignore" }
13
- | { kind: "stop" }
14
- | { kind: "compact" }
15
- | { kind: "status" }
16
- | { kind: "model" }
17
- | { kind: "help"; commandName: "help" | "start" };
56
+ | { kind: "ignore"; executionMode: "ignored" }
57
+ | { kind: "stop"; executionMode: "immediate" }
58
+ | { kind: "compact"; executionMode: "immediate" }
59
+ | { kind: "status"; executionMode: "control-queue" }
60
+ | { kind: "model"; executionMode: "control-queue" }
61
+ | {
62
+ kind: "help";
63
+ commandName: "help" | "start";
64
+ executionMode: "immediate";
65
+ };
66
+
67
+ export type TelegramCommandExecutionMode =
68
+ | "ignored"
69
+ | "immediate"
70
+ | "control-queue";
18
71
 
19
72
  export interface TelegramCommandActionDeps<TMessage, TContext> {
20
73
  handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
@@ -28,6 +81,285 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
28
81
  ) => Promise<void>;
29
82
  }
30
83
 
84
+ export interface TelegramStopCommandDeps {
85
+ hasAbortHandler: () => boolean;
86
+ clearPendingModelSwitch: () => void;
87
+ hasQueuedTelegramItems: () => boolean;
88
+ setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
89
+ abortCurrentTurn: () => void;
90
+ updateStatus: () => void;
91
+ sendTextReply: (text: string) => Promise<void>;
92
+ }
93
+
94
+ export interface TelegramRuntimeEventRecorderPort {
95
+ recordRuntimeEvent?: (
96
+ category: string,
97
+ error: unknown,
98
+ details?: Record<string, unknown>,
99
+ ) => void;
100
+ }
101
+
102
+ export interface TelegramCompactCommandDeps
103
+ extends TelegramRuntimeEventRecorderPort {
104
+ isIdle: () => boolean;
105
+ hasPendingMessages: () => boolean;
106
+ hasActiveTelegramTurn: () => boolean;
107
+ hasDispatchPending: () => boolean;
108
+ hasQueuedTelegramItems: () => boolean;
109
+ isCompactionInProgress: () => boolean;
110
+ setCompactionInProgress: (inProgress: boolean) => void;
111
+ updateStatus: () => void;
112
+ dispatchNextQueuedTelegramTurn: () => void;
113
+ compact: (callbacks: {
114
+ onComplete: () => void;
115
+ onError: (error: unknown) => void;
116
+ }) => void;
117
+ sendTextReply: (text: string) => Promise<void>;
118
+ }
119
+
120
+ export interface TelegramHelpCommandDeps {
121
+ senderUserId?: number;
122
+ getAllowedUserId: () => number | undefined;
123
+ setAllowedUserId: (userId: number) => void;
124
+ registerBotCommands: () => Promise<void>;
125
+ persistConfig: () => Promise<void>;
126
+ updateStatus: () => void;
127
+ sendTextReply: (text: string) => Promise<void>;
128
+ }
129
+
130
+ export type TelegramControlCommandType =
131
+ PendingTelegramControlItem<unknown>["controlType"];
132
+
133
+ export interface TelegramQueuedControlCommandDeps<TContext> {
134
+ enqueueControlItem: (
135
+ controlType: TelegramControlCommandType,
136
+ statusSummary: string,
137
+ execute: (ctx: TContext) => Promise<void>,
138
+ ) => void;
139
+ }
140
+
141
+ export interface TelegramCommandRuntimeMessage {
142
+ chat: { id: number };
143
+ message_id: number;
144
+ from?: { id?: number };
145
+ }
146
+
147
+ export interface TelegramCommandMessageTarget {
148
+ chatId: number;
149
+ replyToMessageId: number;
150
+ }
151
+
152
+ export interface TelegramCommandTargetRuntimeDeps<TContext> {
153
+ enqueueControlItem: (
154
+ target: TelegramCommandMessageTarget,
155
+ ctx: TContext,
156
+ controlType: TelegramControlCommandType,
157
+ statusSummary: string,
158
+ execute: (ctx: TContext) => Promise<void>,
159
+ ) => void;
160
+ showStatus: (
161
+ chatId: number,
162
+ replyToMessageId: number,
163
+ ctx: TContext,
164
+ ) => Promise<void>;
165
+ openModelMenu: (
166
+ chatId: number,
167
+ replyToMessageId: number,
168
+ ctx: TContext,
169
+ ) => Promise<void>;
170
+ sendTextReply: (
171
+ chatId: number,
172
+ replyToMessageId: number,
173
+ text: string,
174
+ ) => Promise<unknown>;
175
+ }
176
+
177
+ export interface TelegramCommandTargetRuntime<
178
+ TMessage extends TelegramCommandRuntimeMessage,
179
+ TContext,
180
+ > {
181
+ enqueueControlItem: (
182
+ message: TMessage,
183
+ ctx: TContext,
184
+ controlType: TelegramControlCommandType,
185
+ statusSummary: string,
186
+ execute: (ctx: TContext) => Promise<void>,
187
+ ) => void;
188
+ showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
189
+ openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
190
+ sendTextReply: (message: TMessage, text: string) => Promise<void>;
191
+ }
192
+
193
+ export function getTelegramCommandMessageTarget(
194
+ message: TelegramCommandRuntimeMessage,
195
+ ): TelegramCommandMessageTarget {
196
+ return {
197
+ chatId: message.chat.id,
198
+ replyToMessageId: message.message_id,
199
+ };
200
+ }
201
+
202
+ export interface TelegramCommandControlQueueRuntimeDeps<TContext> {
203
+ createControlItem: (options: {
204
+ chatId: number;
205
+ replyToMessageId: number;
206
+ controlType: TelegramControlCommandType;
207
+ statusSummary: string;
208
+ execute: (ctx: TContext) => Promise<void>;
209
+ }) => PendingTelegramControlItem<TContext>;
210
+ appendControlItem: (
211
+ item: PendingTelegramControlItem<TContext>,
212
+ ctx: TContext,
213
+ ) => void;
214
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
215
+ }
216
+
217
+ export function createTelegramCommandControlQueueRuntime<TContext>(
218
+ deps: TelegramCommandControlQueueRuntimeDeps<TContext>,
219
+ ): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
220
+ const controlQueueController = createTelegramControlQueueController({
221
+ appendControlItem: deps.appendControlItem,
222
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
223
+ });
224
+ return createTelegramCommandControlEnqueueAdapter({
225
+ createControlItem: deps.createControlItem,
226
+ enqueueControlItem: controlQueueController.enqueue,
227
+ });
228
+ }
229
+
230
+ export function createTelegramCommandControlEnqueueAdapter<TContext>(deps: {
231
+ createControlItem: (options: {
232
+ chatId: number;
233
+ replyToMessageId: number;
234
+ controlType: TelegramControlCommandType;
235
+ statusSummary: string;
236
+ execute: (ctx: TContext) => Promise<void>;
237
+ }) => PendingTelegramControlItem<TContext>;
238
+ enqueueControlItem: (
239
+ item: PendingTelegramControlItem<TContext>,
240
+ ctx: TContext,
241
+ ) => void;
242
+ }): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
243
+ return (target, ctx, controlType, statusSummary, execute) => {
244
+ deps.enqueueControlItem(
245
+ deps.createControlItem({
246
+ ...target,
247
+ controlType,
248
+ statusSummary,
249
+ execute,
250
+ }),
251
+ ctx,
252
+ );
253
+ };
254
+ }
255
+
256
+ export type TelegramCommandTargetQueueRuntimeDeps<TContext> =
257
+ TelegramCommandControlQueueRuntimeDeps<TContext> &
258
+ Omit<TelegramCommandTargetRuntimeDeps<TContext>, "enqueueControlItem">;
259
+
260
+ export function createTelegramCommandTargetQueueRuntime<
261
+ TMessage extends TelegramCommandRuntimeMessage,
262
+ TContext,
263
+ >(
264
+ deps: TelegramCommandTargetQueueRuntimeDeps<TContext>,
265
+ ): TelegramCommandTargetRuntime<TMessage, TContext> {
266
+ return createTelegramCommandTargetRuntime({
267
+ enqueueControlItem: createTelegramCommandControlQueueRuntime({
268
+ createControlItem: deps.createControlItem,
269
+ appendControlItem: deps.appendControlItem,
270
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
271
+ }),
272
+ showStatus: deps.showStatus,
273
+ openModelMenu: deps.openModelMenu,
274
+ sendTextReply: deps.sendTextReply,
275
+ });
276
+ }
277
+
278
+ export function createTelegramCommandTargetRuntime<
279
+ TMessage extends TelegramCommandRuntimeMessage,
280
+ TContext,
281
+ >(
282
+ deps: TelegramCommandTargetRuntimeDeps<TContext>,
283
+ ): TelegramCommandTargetRuntime<TMessage, TContext> {
284
+ return {
285
+ enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
286
+ deps.enqueueControlItem(
287
+ getTelegramCommandMessageTarget(message),
288
+ ctx,
289
+ controlType,
290
+ statusSummary,
291
+ execute,
292
+ );
293
+ },
294
+ showStatus: (message, ctx) => {
295
+ const target = getTelegramCommandMessageTarget(message);
296
+ return deps.showStatus(target.chatId, target.replyToMessageId, ctx);
297
+ },
298
+ openModelMenu: (message, ctx) => {
299
+ const target = getTelegramCommandMessageTarget(message);
300
+ return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx);
301
+ },
302
+ sendTextReply: async (message, text) => {
303
+ const target = getTelegramCommandMessageTarget(message);
304
+ await deps.sendTextReply(target.chatId, target.replyToMessageId, text);
305
+ },
306
+ };
307
+ }
308
+
309
+ export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
310
+ extractRawText: (messages: TMessage[]) => string;
311
+ handleCommand: (
312
+ commandName: string | undefined,
313
+ message: TMessage,
314
+ ctx: TContext,
315
+ ) => Promise<boolean>;
316
+ enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise<void>;
317
+ }
318
+
319
+ export interface TelegramCommandRuntimeDeps<
320
+ TMessage extends TelegramCommandRuntimeMessage,
321
+ TContext,
322
+ > extends TelegramRuntimeEventRecorderPort {
323
+ hasAbortHandler: () => boolean;
324
+ clearPendingModelSwitch: () => void;
325
+ hasQueuedTelegramItems: () => boolean;
326
+ setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
327
+ abortCurrentTurn: () => void;
328
+ isIdle: (ctx: TContext) => boolean;
329
+ hasPendingMessages: (ctx: TContext) => boolean;
330
+ hasActiveTelegramTurn: () => boolean;
331
+ hasDispatchPending: () => boolean;
332
+ isCompactionInProgress: () => boolean;
333
+ setCompactionInProgress: (inProgress: boolean) => void;
334
+ updateStatus: (ctx: TContext) => void;
335
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
336
+ compact: (
337
+ ctx: TContext,
338
+ callbacks: { onComplete: () => void; onError: (error: unknown) => void },
339
+ ) => void;
340
+ enqueueControlItem: (
341
+ message: TMessage,
342
+ ctx: TContext,
343
+ controlType: TelegramControlCommandType,
344
+ statusSummary: string,
345
+ execute: (ctx: TContext) => Promise<void>,
346
+ ) => void;
347
+ showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
348
+ openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
349
+ getAllowedUserId: () => number | undefined;
350
+ setAllowedUserId: (userId: number) => void;
351
+ registerBotCommands: () => Promise<void>;
352
+ persistConfig: () => Promise<void>;
353
+ sendTextReply: (message: TMessage, text: string) => Promise<void>;
354
+ }
355
+
356
+ export const TELEGRAM_HELP_TEXT =
357
+ "Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop.";
358
+
359
+ function getTelegramCommandErrorMessage(error: unknown): string {
360
+ return error instanceof Error ? error.message : String(error);
361
+ }
362
+
31
363
  export function parseTelegramCommand(
32
364
  text: string,
33
365
  ): ParsedTelegramCommand | undefined {
@@ -44,19 +376,127 @@ export function buildTelegramCommandAction(
44
376
  ): TelegramCommandAction {
45
377
  switch (commandName) {
46
378
  case "stop":
47
- return { kind: "stop" };
379
+ return { kind: "stop", executionMode: "immediate" };
48
380
  case "compact":
49
- return { kind: "compact" };
381
+ return { kind: "compact", executionMode: "immediate" };
50
382
  case "status":
51
- return { kind: "status" };
383
+ return { kind: "status", executionMode: "control-queue" };
52
384
  case "model":
53
- return { kind: "model" };
385
+ return { kind: "model", executionMode: "control-queue" };
54
386
  case "help":
55
387
  case "start":
56
- return { kind: "help", commandName };
388
+ return { kind: "help", commandName, executionMode: "immediate" };
57
389
  default:
58
- return { kind: "ignore" };
390
+ return { kind: "ignore", executionMode: "ignored" };
391
+ }
392
+ }
393
+
394
+ export function getTelegramCommandExecutionMode(
395
+ action: TelegramCommandAction,
396
+ ): TelegramCommandExecutionMode {
397
+ return action.executionMode;
398
+ }
399
+
400
+ export async function handleTelegramStopCommand(
401
+ deps: TelegramStopCommandDeps,
402
+ ): Promise<void> {
403
+ if (!deps.hasAbortHandler()) {
404
+ await deps.sendTextReply("No active turn.");
405
+ return;
406
+ }
407
+ deps.clearPendingModelSwitch();
408
+ if (deps.hasQueuedTelegramItems()) {
409
+ deps.setPreserveQueuedTurnsAsHistory(true);
410
+ }
411
+ deps.abortCurrentTurn();
412
+ deps.updateStatus();
413
+ await deps.sendTextReply("Aborted current turn.");
414
+ }
415
+
416
+ export async function handleTelegramCompactCommand(
417
+ deps: TelegramCompactCommandDeps,
418
+ ): Promise<void> {
419
+ if (
420
+ !deps.isIdle() ||
421
+ deps.hasPendingMessages() ||
422
+ deps.hasActiveTelegramTurn() ||
423
+ deps.hasDispatchPending() ||
424
+ deps.hasQueuedTelegramItems() ||
425
+ deps.isCompactionInProgress()
426
+ ) {
427
+ await deps.sendTextReply(
428
+ "Cannot compact while pi or the Telegram queue is busy. Wait for queued turns to finish or send /stop first.",
429
+ );
430
+ return;
431
+ }
432
+ deps.setCompactionInProgress(true);
433
+ deps.updateStatus();
434
+ try {
435
+ deps.compact({
436
+ onComplete: () => {
437
+ deps.setCompactionInProgress(false);
438
+ deps.updateStatus();
439
+ deps.dispatchNextQueuedTelegramTurn();
440
+ void deps.sendTextReply("Compaction completed.");
441
+ },
442
+ onError: (error) => {
443
+ deps.setCompactionInProgress(false);
444
+ deps.updateStatus();
445
+ deps.dispatchNextQueuedTelegramTurn();
446
+ deps.recordRuntimeEvent?.("compact", error);
447
+ const errorMessage = getTelegramCommandErrorMessage(error);
448
+ void deps.sendTextReply(`Compaction failed: ${errorMessage}`);
449
+ },
450
+ });
451
+ } catch (error) {
452
+ deps.setCompactionInProgress(false);
453
+ deps.updateStatus();
454
+ deps.recordRuntimeEvent?.("compact", error);
455
+ const errorMessage = getTelegramCommandErrorMessage(error);
456
+ await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
457
+ return;
458
+ }
459
+ await deps.sendTextReply("Compaction started.");
460
+ }
461
+
462
+ export async function handleTelegramHelpCommand(
463
+ commandName: "help" | "start",
464
+ deps: TelegramHelpCommandDeps,
465
+ ): Promise<void> {
466
+ let helpText = TELEGRAM_HELP_TEXT;
467
+ if (commandName === "start") {
468
+ try {
469
+ await deps.registerBotCommands();
470
+ } catch (error) {
471
+ const errorMessage = getTelegramCommandErrorMessage(error);
472
+ helpText += `\n\nWarning: failed to register bot commands menu: ${errorMessage}`;
473
+ }
59
474
  }
475
+ await deps.sendTextReply(helpText);
476
+ if (deps.senderUserId === undefined) return;
477
+ await pairTelegramUserIfNeeded(deps.senderUserId, {
478
+ allowedUserId: deps.getAllowedUserId(),
479
+ ctx: undefined,
480
+ setAllowedUserId: deps.setAllowedUserId,
481
+ persistConfig: deps.persistConfig,
482
+ updateStatus: deps.updateStatus,
483
+ });
484
+ }
485
+
486
+ export async function handleTelegramStatusCommand<TContext>(
487
+ deps: TelegramQueuedControlCommandDeps<TContext> & {
488
+ showStatus: (ctx: TContext) => Promise<void>;
489
+ },
490
+ ): Promise<void> {
491
+ deps.enqueueControlItem("status", "⚡ status", deps.showStatus);
492
+ }
493
+
494
+ export async function handleTelegramModelCommand<TContext>(
495
+ deps: TelegramQueuedControlCommandDeps<TContext> & {
496
+ openModelMenu: (ctx: TContext) => Promise<void>;
497
+ },
498
+ ): Promise<void> {
499
+ deps.enqueueControlItem("model", "⚡ model", deps.openModelMenu);
60
500
  }
61
501
 
62
502
  export async function executeTelegramCommandAction<TMessage, TContext>(
@@ -85,3 +525,197 @@ export async function executeTelegramCommandAction<TMessage, TContext>(
85
525
  return true;
86
526
  }
87
527
  }
528
+
529
+ export interface TelegramCommandHandlerTargetRuntimeDeps<
530
+ TMessage extends TelegramCommandRuntimeMessage,
531
+ TContext,
532
+ >
533
+ extends
534
+ Omit<
535
+ TelegramCommandRuntimeDeps<TMessage, TContext>,
536
+ | "enqueueControlItem"
537
+ | "showStatus"
538
+ | "openModelMenu"
539
+ | "sendTextReply"
540
+ | "registerBotCommands"
541
+ >,
542
+ Omit<TelegramCommandTargetQueueRuntimeDeps<TContext>, "createControlItem">,
543
+ TelegramBotCommandRegistrationDeps {
544
+ allocateItemOrder: () => number;
545
+ allocateControlOrder: () => number;
546
+ }
547
+
548
+ export function createTelegramCommandHandlerTargetRuntime<
549
+ TMessage extends TelegramCommandRuntimeMessage,
550
+ TContext,
551
+ >(
552
+ deps: TelegramCommandHandlerTargetRuntimeDeps<TMessage, TContext>,
553
+ ): (
554
+ commandName: string | undefined,
555
+ message: TMessage,
556
+ ctx: TContext,
557
+ ) => Promise<boolean> {
558
+ const commandTargetRuntime = createTelegramCommandTargetQueueRuntime<
559
+ TMessage,
560
+ TContext
561
+ >({
562
+ createControlItem: createTelegramControlItemBuilder<TContext>({
563
+ allocateItemOrder: deps.allocateItemOrder,
564
+ allocateControlOrder: deps.allocateControlOrder,
565
+ }),
566
+ appendControlItem: deps.appendControlItem,
567
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
568
+ showStatus: deps.showStatus,
569
+ openModelMenu: deps.openModelMenu,
570
+ sendTextReply: deps.sendTextReply,
571
+ });
572
+ return createTelegramCommandHandler({
573
+ hasAbortHandler: deps.hasAbortHandler,
574
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
575
+ hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
576
+ setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
577
+ abortCurrentTurn: deps.abortCurrentTurn,
578
+ isIdle: deps.isIdle,
579
+ hasPendingMessages: deps.hasPendingMessages,
580
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
581
+ hasDispatchPending: deps.hasDispatchPending,
582
+ isCompactionInProgress: deps.isCompactionInProgress,
583
+ setCompactionInProgress: deps.setCompactionInProgress,
584
+ updateStatus: deps.updateStatus,
585
+ dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
586
+ compact: deps.compact,
587
+ enqueueControlItem: commandTargetRuntime.enqueueControlItem,
588
+ showStatus: commandTargetRuntime.showStatus,
589
+ openModelMenu: commandTargetRuntime.openModelMenu,
590
+ getAllowedUserId: deps.getAllowedUserId,
591
+ setAllowedUserId: deps.setAllowedUserId,
592
+ registerBotCommands: createTelegramBotCommandRegistrar({
593
+ setMyCommands: deps.setMyCommands,
594
+ }),
595
+ persistConfig: deps.persistConfig,
596
+ sendTextReply: commandTargetRuntime.sendTextReply,
597
+ recordRuntimeEvent: deps.recordRuntimeEvent,
598
+ });
599
+ }
600
+
601
+ export function createTelegramCommandHandler<
602
+ TMessage extends TelegramCommandRuntimeMessage,
603
+ TContext,
604
+ >(deps: TelegramCommandRuntimeDeps<TMessage, TContext>) {
605
+ return async function handleTelegramCommand(
606
+ commandName: string | undefined,
607
+ message: TMessage,
608
+ ctx: TContext,
609
+ ): Promise<boolean> {
610
+ return handleTelegramCommandRuntime(commandName, message, ctx, deps);
611
+ };
612
+ }
613
+
614
+ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
615
+ deps: TelegramCommandOrPromptRuntimeDeps<TMessage, TContext>,
616
+ ) {
617
+ return {
618
+ dispatchMessages: async (
619
+ messages: TMessage[],
620
+ ctx: TContext,
621
+ ): Promise<void> => {
622
+ const firstMessage = messages[0];
623
+ if (!firstMessage) return;
624
+ const commandName = parseTelegramCommand(
625
+ deps.extractRawText(messages),
626
+ )?.name;
627
+ const handled = await deps.handleCommand(commandName, firstMessage, ctx);
628
+ if (handled) return;
629
+ await deps.enqueueTurn(messages, ctx);
630
+ },
631
+ };
632
+ }
633
+
634
+ async function handleTelegramCommandRuntime<
635
+ TMessage extends TelegramCommandRuntimeMessage,
636
+ TContext,
637
+ >(
638
+ commandName: string | undefined,
639
+ message: TMessage,
640
+ ctx: TContext,
641
+ deps: TelegramCommandRuntimeDeps<TMessage, TContext>,
642
+ ): Promise<boolean> {
643
+ const sendReplyFor = (nextMessage: TMessage) => (text: string) =>
644
+ deps.sendTextReply(nextMessage, text);
645
+ const updateStatusFor = (commandCtx: TContext) => () =>
646
+ deps.updateStatus(commandCtx);
647
+ const enqueueControlFor =
648
+ (nextMessage: TMessage, commandCtx: TContext) =>
649
+ (
650
+ controlType: TelegramControlCommandType,
651
+ statusSummary: string,
652
+ execute: (ctx: TContext) => Promise<void>,
653
+ ) => {
654
+ deps.enqueueControlItem(
655
+ nextMessage,
656
+ commandCtx,
657
+ controlType,
658
+ statusSummary,
659
+ execute,
660
+ );
661
+ };
662
+ return executeTelegramCommandAction(
663
+ buildTelegramCommandAction(commandName),
664
+ message,
665
+ ctx,
666
+ {
667
+ handleStop: async (nextMessage, commandCtx) => {
668
+ await handleTelegramStopCommand({
669
+ hasAbortHandler: deps.hasAbortHandler,
670
+ clearPendingModelSwitch: deps.clearPendingModelSwitch,
671
+ hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
672
+ setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
673
+ abortCurrentTurn: deps.abortCurrentTurn,
674
+ updateStatus: updateStatusFor(commandCtx),
675
+ sendTextReply: sendReplyFor(nextMessage),
676
+ });
677
+ },
678
+ handleCompact: async (nextMessage, commandCtx) => {
679
+ await handleTelegramCompactCommand({
680
+ isIdle: () => deps.isIdle(commandCtx),
681
+ hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
682
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
683
+ hasDispatchPending: deps.hasDispatchPending,
684
+ hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
685
+ isCompactionInProgress: deps.isCompactionInProgress,
686
+ setCompactionInProgress: deps.setCompactionInProgress,
687
+ updateStatus: updateStatusFor(commandCtx),
688
+ dispatchNextQueuedTelegramTurn: () =>
689
+ deps.dispatchNextQueuedTelegramTurn(commandCtx),
690
+ compact: (callbacks) => deps.compact(commandCtx, callbacks),
691
+ sendTextReply: sendReplyFor(nextMessage),
692
+ recordRuntimeEvent: deps.recordRuntimeEvent,
693
+ });
694
+ },
695
+ handleStatus: async (nextMessage, commandCtx) => {
696
+ await handleTelegramStatusCommand<TContext>({
697
+ enqueueControlItem: enqueueControlFor(nextMessage, commandCtx),
698
+ showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
699
+ });
700
+ },
701
+ handleModel: async (nextMessage, commandCtx) => {
702
+ await handleTelegramModelCommand<TContext>({
703
+ enqueueControlItem: enqueueControlFor(nextMessage, commandCtx),
704
+ openModelMenu: (controlCtx) =>
705
+ deps.openModelMenu(nextMessage, controlCtx),
706
+ });
707
+ },
708
+ handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
709
+ await handleTelegramHelpCommand(nextCommandName, {
710
+ senderUserId: nextMessage.from?.id,
711
+ getAllowedUserId: deps.getAllowedUserId,
712
+ setAllowedUserId: deps.setAllowedUserId,
713
+ registerBotCommands: deps.registerBotCommands,
714
+ persistConfig: deps.persistConfig,
715
+ updateStatus: updateStatusFor(commandCtx),
716
+ sendTextReply: sendReplyFor(nextMessage),
717
+ });
718
+ },
719
+ },
720
+ );
721
+ }