@llblab/pi-telegram 0.2.10 → 0.3.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/lib/status.ts CHANGED
@@ -3,8 +3,7 @@
3
3
  * Builds usage, cost, and context summaries for the interactive Telegram status view
4
4
  */
5
5
 
6
- import type { Model } from "@mariozechner/pi-ai";
7
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ export type TelegramStatusQueueLane = "control" | "priority" | "default";
8
7
 
9
8
  export interface TelegramUsageStats {
10
9
  totalInput: number;
@@ -14,6 +13,417 @@ export interface TelegramUsageStats {
14
13
  totalCost: number;
15
14
  }
16
15
 
16
+ interface TelegramUsageMessage {
17
+ role: string;
18
+ usage?: {
19
+ input: number;
20
+ output: number;
21
+ cacheRead: number;
22
+ cacheWrite: number;
23
+ cost: { total: number };
24
+ };
25
+ }
26
+
27
+ interface TelegramStatusSessionEntry {
28
+ type: string;
29
+ message?: TelegramUsageMessage;
30
+ }
31
+
32
+ interface TelegramContextUsage {
33
+ contextWindow?: number;
34
+ percent: number | null;
35
+ }
36
+
37
+ export interface TelegramStatusActiveModel {
38
+ contextWindow?: number;
39
+ }
40
+
41
+ export interface TelegramStatusContext {
42
+ sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
43
+ getContextUsage(): TelegramContextUsage | undefined;
44
+ modelRegistry: {
45
+ isUsingOAuth(model: TelegramStatusActiveModel): boolean;
46
+ };
47
+ }
48
+
49
+ export type TelegramRuntimeEventDetailValue = string | number | boolean | null;
50
+
51
+ const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
52
+
53
+ export interface TelegramRuntimeEvent {
54
+ at: number;
55
+ category: string;
56
+ message: string;
57
+ details?: Record<string, TelegramRuntimeEventDetailValue>;
58
+ }
59
+
60
+ export interface TelegramRuntimeEventInput {
61
+ category: string;
62
+ error?: unknown;
63
+ message?: string;
64
+ details?: Record<string, unknown>;
65
+ }
66
+
67
+ export interface TelegramRuntimeEventRecorder {
68
+ record: (
69
+ category: string,
70
+ error: unknown,
71
+ details?: Record<string, unknown>,
72
+ ) => void;
73
+ getEvents: () => TelegramRuntimeEvent[];
74
+ clear: () => void;
75
+ }
76
+
77
+ export interface TelegramRuntimeEventRecorderOptions {
78
+ getBotToken: () => string | undefined;
79
+ maxEvents?: number;
80
+ now?: () => number;
81
+ }
82
+
83
+ export interface TelegramBridgeStatusLineState {
84
+ botUsername?: string;
85
+ allowedUserId?: number;
86
+ pollingActive: boolean;
87
+ lastUpdateId?: number;
88
+ activeSourceMessageIds?: number[];
89
+ pendingDispatch: boolean;
90
+ compactionInProgress: boolean;
91
+ activeToolExecutions: number;
92
+ pendingModelSwitch: boolean;
93
+ queuedItems: Array<{ queueLane: TelegramStatusQueueLane }>;
94
+ recentRuntimeEvents: TelegramRuntimeEvent[];
95
+ }
96
+
97
+ export interface TelegramStatusBarTheme {
98
+ fg: (
99
+ token: "accent" | "error" | "muted" | "warning" | "success",
100
+ text: string,
101
+ ) => string;
102
+ }
103
+
104
+ export interface TelegramStatusBarState {
105
+ hasBotToken: boolean;
106
+ pollingActive: boolean;
107
+ paired: boolean;
108
+ compactionInProgress: boolean;
109
+ processing: boolean;
110
+ queuedStatus: string;
111
+ error?: string;
112
+ }
113
+
114
+ export interface TelegramStatusRuntimeContext {
115
+ ui: {
116
+ theme: TelegramStatusBarTheme;
117
+ setStatus: (key: string, text: string) => void;
118
+ };
119
+ }
120
+
121
+ export interface TelegramStatusRuntimeDeps<
122
+ TContext extends TelegramStatusRuntimeContext,
123
+ > {
124
+ statusKey?: string;
125
+ getStatusBarState: (ctx: TContext, error?: string) => TelegramStatusBarState;
126
+ getBridgeStatusLineState: () => TelegramBridgeStatusLineState;
127
+ }
128
+
129
+ export interface TelegramBridgeStatusConfig {
130
+ botToken?: string;
131
+ botUsername?: string;
132
+ allowedUserId?: number;
133
+ lastUpdateId?: number;
134
+ }
135
+
136
+ export interface TelegramBridgeStatusRuntimeDeps<
137
+ TQueueItem extends { queueLane: TelegramStatusQueueLane },
138
+ > {
139
+ statusKey?: string;
140
+ getConfig: () => TelegramBridgeStatusConfig;
141
+ isPollingActive: () => boolean;
142
+ getActiveSourceMessageIds: () => number[] | undefined;
143
+ hasActiveTurn: () => boolean;
144
+ hasDispatchPending: () => boolean;
145
+ isCompactionInProgress: () => boolean;
146
+ getActiveToolExecutions: () => number;
147
+ hasPendingModelSwitch: () => boolean;
148
+ getQueuedItems: () => TQueueItem[];
149
+ formatQueuedStatus: (items: TQueueItem[]) => string;
150
+ getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
151
+ }
152
+
153
+ export interface TelegramStatusRuntime<
154
+ TContext extends TelegramStatusRuntimeContext,
155
+ > {
156
+ updateStatus: (ctx: TContext, error?: string) => void;
157
+ getStatusLines: () => string[];
158
+ }
159
+
160
+ export function redactTelegramRuntimeMessage(
161
+ message: string,
162
+ botToken: string | undefined,
163
+ ): string {
164
+ if (!botToken) return message;
165
+ return message.split(botToken).join("<redacted-token>");
166
+ }
167
+
168
+ function normalizeTelegramRuntimeEventDetails(
169
+ details: Record<string, unknown> | undefined,
170
+ botToken: string | undefined,
171
+ ): Record<string, TelegramRuntimeEventDetailValue> | undefined {
172
+ if (!details) return undefined;
173
+ const normalized: Record<string, TelegramRuntimeEventDetailValue> = {};
174
+ for (const [key, value] of Object.entries(details)) {
175
+ if (value === undefined) continue;
176
+ if (typeof value === "string") {
177
+ normalized[key] = redactTelegramRuntimeMessage(value, botToken);
178
+ continue;
179
+ }
180
+ if (typeof value === "number" || typeof value === "boolean") {
181
+ normalized[key] = value;
182
+ continue;
183
+ }
184
+ if (value === null) {
185
+ normalized[key] = null;
186
+ continue;
187
+ }
188
+ normalized[key] = redactTelegramRuntimeMessage(String(value), botToken);
189
+ }
190
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
191
+ }
192
+
193
+ function getTelegramRuntimeEventMessage(
194
+ input: TelegramRuntimeEventInput,
195
+ ): string {
196
+ if (input.message !== undefined) return input.message;
197
+ if (input.error instanceof Error) return input.error.message;
198
+ return String(input.error);
199
+ }
200
+
201
+ export function recordStructuredTelegramRuntimeEvent(
202
+ events: TelegramRuntimeEvent[],
203
+ input: TelegramRuntimeEventInput,
204
+ options: { botToken?: string; maxEvents: number; now?: number },
205
+ ): void {
206
+ const details = normalizeTelegramRuntimeEventDetails(
207
+ input.details,
208
+ options.botToken,
209
+ );
210
+ events.push({
211
+ at: options.now ?? Date.now(),
212
+ category: input.category,
213
+ message: redactTelegramRuntimeMessage(
214
+ getTelegramRuntimeEventMessage(input),
215
+ options.botToken,
216
+ ),
217
+ ...(details ? { details } : {}),
218
+ });
219
+ while (events.length > options.maxEvents) {
220
+ events.shift();
221
+ }
222
+ }
223
+
224
+ export function recordTelegramRuntimeEvent(
225
+ events: TelegramRuntimeEvent[],
226
+ category: string,
227
+ error: unknown,
228
+ options: { botToken?: string; maxEvents: number; now?: number },
229
+ ): void {
230
+ recordStructuredTelegramRuntimeEvent(events, { category, error }, options);
231
+ }
232
+
233
+ export function createTelegramRuntimeEventRecorder(
234
+ options: TelegramRuntimeEventRecorderOptions,
235
+ ): TelegramRuntimeEventRecorder {
236
+ const events: TelegramRuntimeEvent[] = [];
237
+ return {
238
+ record: (category, error, details) => {
239
+ recordStructuredTelegramRuntimeEvent(
240
+ events,
241
+ { category, error, details },
242
+ {
243
+ botToken: options.getBotToken(),
244
+ maxEvents: options.maxEvents ?? MAX_RECENT_TELEGRAM_RUNTIME_EVENTS,
245
+ now: options.now?.(),
246
+ },
247
+ );
248
+ },
249
+ getEvents: () => events,
250
+ clear: () => {
251
+ events.length = 0;
252
+ },
253
+ };
254
+ }
255
+
256
+ function formatTelegramRuntimeEventCategory(
257
+ event: TelegramRuntimeEvent,
258
+ ): string {
259
+ const method = event.details?.method;
260
+ return typeof method === "string"
261
+ ? `${event.category}:${method}`
262
+ : event.category;
263
+ }
264
+
265
+ function formatTelegramRuntimeEventDetails(
266
+ event: TelegramRuntimeEvent,
267
+ ): string {
268
+ if (!event.details) return "";
269
+ const details = Object.entries(event.details)
270
+ .filter(([key]) => key !== "method")
271
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`);
272
+ return details.length > 0 ? ` (${details.join(", ")})` : "";
273
+ }
274
+
275
+ function formatTelegramRuntimeEventSummary(
276
+ event: TelegramRuntimeEvent,
277
+ ): string {
278
+ return `${formatTelegramRuntimeEventCategory(event)}: ${event.message}${formatTelegramRuntimeEventDetails(event)}`;
279
+ }
280
+
281
+ function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
282
+ return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
283
+ }
284
+
285
+ export function buildTelegramRuntimeEventLines(
286
+ events: TelegramRuntimeEvent[],
287
+ ): string[] {
288
+ if (events.length === 0) return ["recent runtime events: none"];
289
+ return [
290
+ "recent runtime events:",
291
+ ...events
292
+ .slice()
293
+ .reverse()
294
+ .map((event) => `- ${formatTelegramRuntimeEvent(event)}`),
295
+ ];
296
+ }
297
+
298
+ export function createTelegramStatusHtmlBuilder<TContext>(deps: {
299
+ getActiveModel: (ctx: TContext) => TelegramStatusActiveModel | undefined;
300
+ }): (ctx: TContext & TelegramStatusContext) => string {
301
+ return (ctx) => buildStatusHtml(ctx, deps.getActiveModel(ctx));
302
+ }
303
+
304
+ export function createTelegramStatusRuntime<
305
+ TContext extends TelegramStatusRuntimeContext,
306
+ >(deps: TelegramStatusRuntimeDeps<TContext>): TelegramStatusRuntime<TContext> {
307
+ const statusKey = deps.statusKey ?? "telegram";
308
+ return {
309
+ updateStatus: (ctx, error) => {
310
+ ctx.ui.setStatus(
311
+ statusKey,
312
+ buildTelegramStatusBarText(
313
+ ctx.ui.theme,
314
+ deps.getStatusBarState(ctx, error),
315
+ ),
316
+ );
317
+ },
318
+ getStatusLines: () =>
319
+ buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState()),
320
+ };
321
+ }
322
+
323
+ export function createTelegramBridgeStatusRuntime<
324
+ TContext extends TelegramStatusRuntimeContext,
325
+ TQueueItem extends { queueLane: TelegramStatusQueueLane },
326
+ >(
327
+ deps: TelegramBridgeStatusRuntimeDeps<TQueueItem>,
328
+ ): TelegramStatusRuntime<TContext> {
329
+ return createTelegramStatusRuntime({
330
+ statusKey: deps.statusKey,
331
+ getStatusBarState: (_ctx, error) => {
332
+ const config = deps.getConfig();
333
+ const queuedItems = deps.getQueuedItems();
334
+ const compactionInProgress = deps.isCompactionInProgress();
335
+ return {
336
+ hasBotToken: !!config.botToken,
337
+ pollingActive: deps.isPollingActive(),
338
+ paired: !!config.allowedUserId,
339
+ compactionInProgress,
340
+ processing:
341
+ deps.hasActiveTurn() ||
342
+ deps.hasDispatchPending() ||
343
+ queuedItems.length > 0,
344
+ queuedStatus: deps.formatQueuedStatus(queuedItems),
345
+ error,
346
+ };
347
+ },
348
+ getBridgeStatusLineState: () => {
349
+ const config = deps.getConfig();
350
+ return {
351
+ botUsername: config.botUsername,
352
+ allowedUserId: config.allowedUserId,
353
+ pollingActive: deps.isPollingActive(),
354
+ lastUpdateId: config.lastUpdateId,
355
+ activeSourceMessageIds: deps.getActiveSourceMessageIds(),
356
+ pendingDispatch: deps.hasDispatchPending(),
357
+ compactionInProgress: deps.isCompactionInProgress(),
358
+ activeToolExecutions: deps.getActiveToolExecutions(),
359
+ pendingModelSwitch: deps.hasPendingModelSwitch(),
360
+ queuedItems: deps.getQueuedItems(),
361
+ recentRuntimeEvents: deps.getRecentRuntimeEvents(),
362
+ };
363
+ },
364
+ });
365
+ }
366
+
367
+ export function buildTelegramStatusBarText(
368
+ theme: TelegramStatusBarTheme,
369
+ state: TelegramStatusBarState,
370
+ ): string {
371
+ const label = theme.fg("accent", "telegram");
372
+ if (state.error) {
373
+ return `${label} ${theme.fg("error", "error")} ${theme.fg("muted", state.error)}`;
374
+ }
375
+ if (!state.hasBotToken)
376
+ return `${label} ${theme.fg("muted", "not configured")}`;
377
+ if (!state.pollingActive)
378
+ return `${label} ${theme.fg("muted", "disconnected")}`;
379
+ if (!state.paired)
380
+ return `${label} ${theme.fg("warning", "awaiting pairing")}`;
381
+ const queued = theme.fg("muted", state.queuedStatus);
382
+ if (state.compactionInProgress) {
383
+ return `${label} ${theme.fg("accent", "compacting")}${queued}`;
384
+ }
385
+ if (state.processing) {
386
+ return `${label} ${theme.fg("accent", "processing")}${queued}`;
387
+ }
388
+ return `${label} ${theme.fg("success", "connected")}`;
389
+ }
390
+
391
+ export function buildTelegramBridgeStatusLines(
392
+ state: TelegramBridgeStatusLineState,
393
+ ): string[] {
394
+ const controlQueueCount = state.queuedItems.filter(
395
+ (item) => item.queueLane === "control",
396
+ ).length;
397
+ const priorityQueueCount = state.queuedItems.filter(
398
+ (item) => item.queueLane === "priority",
399
+ ).length;
400
+ const defaultQueueCount = state.queuedItems.filter(
401
+ (item) => item.queueLane === "default",
402
+ ).length;
403
+ return [
404
+ "connection:",
405
+ `- bot: ${state.botUsername ? `@${state.botUsername}` : "not configured"}`,
406
+ `- allowed user: ${state.allowedUserId ?? "not paired"}`,
407
+ "",
408
+ "polling:",
409
+ `- state: ${state.pollingActive ? "running" : "stopped"}`,
410
+ `- last update id: ${state.lastUpdateId ?? "none"}`,
411
+ "",
412
+ "execution:",
413
+ `- active turn: ${state.activeSourceMessageIds?.join(",") || "no"}`,
414
+ `- pending dispatch: ${state.pendingDispatch ? "yes" : "no"}`,
415
+ `- compaction: ${state.compactionInProgress ? "running" : "idle"}`,
416
+ `- active tools: ${state.activeToolExecutions}`,
417
+ `- pending model switch: ${state.pendingModelSwitch ? "yes" : "no"}`,
418
+ "",
419
+ "queue:",
420
+ `- queued turns: ${state.queuedItems.length}`,
421
+ `- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
422
+ "",
423
+ ...buildTelegramRuntimeEventLines(state.recentRuntimeEvents),
424
+ ];
425
+ }
426
+
17
427
  function escapeHtml(text: string): string {
18
428
  return text
19
429
  .replace(/&/g, "&amp;")
@@ -29,7 +439,7 @@ function formatTokens(count: number): string {
29
439
  return `${Math.round(count / 1000000)}M`;
30
440
  }
31
441
 
32
- export function collectUsageStats(ctx: ExtensionContext): TelegramUsageStats {
442
+ function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
33
443
  const stats: TelegramUsageStats = {
34
444
  totalInput: 0,
35
445
  totalOutput: 0,
@@ -38,14 +448,19 @@ export function collectUsageStats(ctx: ExtensionContext): TelegramUsageStats {
38
448
  totalCost: 0,
39
449
  };
40
450
  for (const entry of ctx.sessionManager.getEntries()) {
41
- if (entry.type !== "message" || entry.message.role !== "assistant") {
451
+ const usage = entry.message?.usage;
452
+ if (
453
+ entry.type !== "message" ||
454
+ entry.message?.role !== "assistant" ||
455
+ !usage
456
+ ) {
42
457
  continue;
43
458
  }
44
- stats.totalInput += entry.message.usage.input;
45
- stats.totalOutput += entry.message.usage.output;
46
- stats.totalCacheRead += entry.message.usage.cacheRead;
47
- stats.totalCacheWrite += entry.message.usage.cacheWrite;
48
- stats.totalCost += entry.message.usage.cost.total;
459
+ stats.totalInput += usage.input;
460
+ stats.totalOutput += usage.output;
461
+ stats.totalCacheRead += usage.cacheRead;
462
+ stats.totalCacheWrite += usage.cacheWrite;
463
+ stats.totalCost += usage.cost.total;
49
464
  }
50
465
  return stats;
51
466
  }
@@ -74,8 +489,8 @@ function buildCostSummary(
74
489
  }
75
490
 
76
491
  function buildContextSummary(
77
- ctx: ExtensionContext,
78
- activeModel: Model<any> | undefined,
492
+ ctx: TelegramStatusContext,
493
+ activeModel: TelegramStatusActiveModel | undefined,
79
494
  ): string {
80
495
  const usage = ctx.getContextUsage();
81
496
  if (!usage) return "unknown";
@@ -85,8 +500,8 @@ function buildContextSummary(
85
500
  }
86
501
 
87
502
  export function buildStatusHtml(
88
- ctx: ExtensionContext,
89
- activeModel: Model<any> | undefined,
503
+ ctx: TelegramStatusContext,
504
+ activeModel: TelegramStatusActiveModel | undefined,
90
505
  ): string {
91
506
  const stats = collectUsageStats(ctx);
92
507
  const usesSubscription = activeModel
package/lib/turns.ts CHANGED
@@ -3,27 +3,34 @@
3
3
  * Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
4
4
  */
5
5
 
6
+ import { readFile } from "node:fs/promises";
6
7
  import { basename } from "node:path";
7
8
 
8
- import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
9
-
10
9
  import {
11
10
  collectTelegramMessageIds,
11
+ type DownloadedTelegramMessageFile,
12
+ type DownloadTelegramMessageFilesDeps,
13
+ downloadTelegramMessageFiles,
14
+ extractTelegramMessagesText,
12
15
  formatTelegramHistoryText,
16
+ guessMediaType,
17
+ type TelegramMediaMessage,
13
18
  } from "./media.ts";
14
- import type { PendingTelegramTurn } from "./queue.ts";
19
+ import type {
20
+ PendingTelegramTurn,
21
+ TelegramPromptContent,
22
+ TelegramQueueItem,
23
+ TelegramQueueStore,
24
+ } from "./queue.ts";
25
+
26
+ export const TELEGRAM_PREFIX = "[telegram]";
15
27
 
16
- export interface TelegramTurnMessageLike {
28
+ export interface TelegramTurnMessage {
17
29
  message_id: number;
18
30
  chat: { id: number };
19
31
  }
20
32
 
21
- export interface DownloadedTelegramTurnFileLike {
22
- path: string;
23
- fileName: string;
24
- isImage: boolean;
25
- mimeType?: string;
26
- }
33
+ export type DownloadedTelegramTurnFile = DownloadedTelegramMessageFile;
27
34
 
28
35
  export function truncateTelegramQueueSummary(
29
36
  text: string,
@@ -45,7 +52,7 @@ export function truncateTelegramQueueSummary(
45
52
 
46
53
  export function formatTelegramTurnStatusSummary(
47
54
  rawText: string,
48
- files: DownloadedTelegramTurnFileLike[],
55
+ files: DownloadedTelegramTurnFile[],
49
56
  ): string {
50
57
  const textSummary = truncateTelegramQueueSummary(rawText);
51
58
  if (textSummary) return textSummary;
@@ -62,7 +69,7 @@ export function formatTelegramTurnStatusSummary(
62
69
  export function buildTelegramTurnPrompt(options: {
63
70
  telegramPrefix: string;
64
71
  rawText: string;
65
- files: DownloadedTelegramTurnFileLike[];
72
+ files: DownloadedTelegramTurnFile[];
66
73
  historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
67
74
  }): string {
68
75
  let prompt = options.telegramPrefix;
@@ -92,7 +99,7 @@ export function buildTelegramTurnPrompt(options: {
92
99
  function splitTelegramPromptAttachmentSuffix(prompt: string): {
93
100
  promptWithoutAttachments: string;
94
101
  attachmentSuffix: string;
95
- attachmentFiles: DownloadedTelegramTurnFileLike[];
102
+ attachmentFiles: DownloadedTelegramTurnFile[];
96
103
  } {
97
104
  const marker = "\n\nTelegram attachments were saved locally:";
98
105
  const markerIndex = prompt.indexOf(marker);
@@ -117,13 +124,12 @@ function buildEditedTelegramPromptText(options: {
117
124
  existingPrompt: string;
118
125
  telegramPrefix: string;
119
126
  rawText: string;
120
- }): { text: string; attachmentFiles: DownloadedTelegramTurnFileLike[] } {
127
+ }): { text: string; attachmentFiles: DownloadedTelegramTurnFile[] } {
121
128
  const { promptWithoutAttachments, attachmentSuffix, attachmentFiles } =
122
129
  splitTelegramPromptAttachmentSuffix(options.existingPrompt);
123
130
  const currentMessageMarker = "Current Telegram message:";
124
- const currentMessageIndex = promptWithoutAttachments.lastIndexOf(
125
- currentMessageMarker,
126
- );
131
+ const currentMessageIndex =
132
+ promptWithoutAttachments.lastIndexOf(currentMessageMarker);
127
133
  if (currentMessageIndex !== -1) {
128
134
  const prefix = promptWithoutAttachments.slice(
129
135
  0,
@@ -150,7 +156,7 @@ export function updateTelegramPromptTurnText(options: {
150
156
  telegramPrefix: string;
151
157
  rawText: string;
152
158
  }): PendingTelegramTurn {
153
- let attachmentFiles: DownloadedTelegramTurnFileLike[] = [];
159
+ let attachmentFiles: DownloadedTelegramTurnFile[] = [];
154
160
  const nextContent = options.turn.content.map((block, index) => {
155
161
  if (index !== 0 || block.type !== "text") return block;
156
162
  const updated = buildEditedTelegramPromptText({
@@ -175,21 +181,110 @@ export function updateTelegramPromptTurnText(options: {
175
181
  };
176
182
  }
177
183
 
178
- export async function buildTelegramPromptTurn(options: {
184
+ export function updateQueuedTelegramPromptTurnText<
185
+ TContext = unknown,
186
+ >(options: {
187
+ items: TelegramQueueItem<TContext>[];
188
+ sourceMessageId: number | undefined;
189
+ telegramPrefix: string;
190
+ rawText: string;
191
+ }): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
192
+ if (options.sourceMessageId === undefined) {
193
+ return { items: options.items, changed: false };
194
+ }
195
+ let changed = false;
196
+ const items = options.items.map((item) => {
197
+ if (
198
+ item.kind !== "prompt" ||
199
+ !item.sourceMessageIds.includes(options.sourceMessageId as number)
200
+ ) {
201
+ return item;
202
+ }
203
+ changed = true;
204
+ return updateTelegramPromptTurnText({
205
+ turn: item,
206
+ telegramPrefix: options.telegramPrefix,
207
+ rawText: options.rawText,
208
+ });
209
+ });
210
+ return { items, changed };
211
+ }
212
+
213
+ export interface TelegramQueuedPromptEditRuntimeDeps<
214
+ TContext = unknown,
215
+ > extends TelegramQueueStore<TContext> {
216
+ updateStatus: (ctx: TContext) => void;
217
+ }
218
+
219
+ export function createTelegramQueuedPromptEditRuntime<
220
+ TMessage extends TelegramMediaMessage,
221
+ TContext = unknown,
222
+ >(deps: TelegramQueuedPromptEditRuntimeDeps<TContext>) {
223
+ return {
224
+ updateFromEditedMessage: (message: TMessage, ctx: TContext): boolean => {
225
+ const { changed, items } = updateQueuedTelegramPromptTurnText({
226
+ items: deps.getQueuedItems(),
227
+ sourceMessageId: message.message_id,
228
+ telegramPrefix: TELEGRAM_PREFIX,
229
+ rawText: extractTelegramMessagesText([message]),
230
+ });
231
+ deps.setQueuedItems(items);
232
+ if (changed) deps.updateStatus(ctx);
233
+ return changed;
234
+ },
235
+ };
236
+ }
237
+
238
+ export interface BuildTelegramPromptTurnOptions {
179
239
  telegramPrefix: string;
180
- messages: TelegramTurnMessageLike[];
240
+ messages: TelegramTurnMessage[];
181
241
  historyTurns?: PendingTelegramTurn[];
182
242
  queueOrder: number;
183
243
  rawText: string;
184
- files: DownloadedTelegramTurnFileLike[];
244
+ files: DownloadedTelegramTurnFile[];
185
245
  readBinaryFile: (path: string) => Promise<Uint8Array>;
186
246
  inferImageMimeType: (path: string) => string | undefined;
187
- }): Promise<PendingTelegramTurn> {
247
+ }
248
+
249
+ export type BuildTelegramPromptTurnRuntimeOptions = Omit<
250
+ BuildTelegramPromptTurnOptions,
251
+ "readBinaryFile"
252
+ >;
253
+
254
+ export interface TelegramPromptTurnRuntimeBuilderDeps extends DownloadTelegramMessageFilesDeps {
255
+ allocateQueueOrder: () => number;
256
+ }
257
+
258
+ export function createTelegramPromptTurnRuntimeBuilder<
259
+ TMessage extends TelegramTurnMessage & TelegramMediaMessage,
260
+ >(
261
+ deps: TelegramPromptTurnRuntimeBuilderDeps,
262
+ ): (
263
+ messages: TMessage[],
264
+ historyTurns?: PendingTelegramTurn[],
265
+ ) => Promise<PendingTelegramTurn> {
266
+ return async (messages, historyTurns = []) =>
267
+ buildTelegramPromptTurnRuntime({
268
+ telegramPrefix: TELEGRAM_PREFIX,
269
+ messages,
270
+ historyTurns,
271
+ queueOrder: deps.allocateQueueOrder(),
272
+ rawText: extractTelegramMessagesText(messages),
273
+ files: await downloadTelegramMessageFiles(messages, {
274
+ downloadFile: deps.downloadFile,
275
+ }),
276
+ inferImageMimeType: guessMediaType,
277
+ });
278
+ }
279
+
280
+ export async function buildTelegramPromptTurn(
281
+ options: BuildTelegramPromptTurnOptions,
282
+ ): Promise<PendingTelegramTurn> {
188
283
  const firstMessage = options.messages[0];
189
284
  if (!firstMessage) {
190
285
  throw new Error("Missing Telegram message for turn creation");
191
286
  }
192
- const content: Array<TextContent | ImageContent> = [
287
+ const content: TelegramPromptContent[] = [
193
288
  {
194
289
  type: "text",
195
290
  text: buildTelegramTurnPrompt({
@@ -228,3 +323,12 @@ export async function buildTelegramPromptTurn(options: {
228
323
  ),
229
324
  };
230
325
  }
326
+
327
+ export async function buildTelegramPromptTurnRuntime(
328
+ options: BuildTelegramPromptTurnRuntimeOptions,
329
+ ): Promise<PendingTelegramTurn> {
330
+ return buildTelegramPromptTurn({
331
+ ...options,
332
+ readBinaryFile: readFile,
333
+ });
334
+ }