@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/README.md +30 -19
- package/docs/architecture.md +51 -28
- package/index.ts +388 -1881
- package/lib/api.ts +396 -60
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +648 -14
- package/lib/config.ts +157 -0
- package/lib/media.ts +147 -41
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +240 -14
- package/lib/preview.ts +420 -25
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +560 -366
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +127 -23
- package/lib/updates.ts +340 -109
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -34
- package/lib/model-switch.ts +0 -62
- package/lib/types.ts +0 -137
- package/tests/api.test.ts +0 -331
- package/tests/attachments.test.ts +0 -132
- package/tests/commands.test.ts +0 -85
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -166
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -202
- package/tests/preview.test.ts +0 -480
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -526
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -247
- package/tests/updates.test.ts +0 -416
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
|
-
|
|
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, "&")
|
|
@@ -29,7 +439,7 @@ function formatTokens(count: number): string {
|
|
|
29
439
|
return `${Math.round(count / 1000000)}M`;
|
|
30
440
|
}
|
|
31
441
|
|
|
32
|
-
|
|
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
|
-
|
|
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 +=
|
|
45
|
-
stats.totalOutput +=
|
|
46
|
-
stats.totalCacheRead +=
|
|
47
|
-
stats.totalCacheWrite +=
|
|
48
|
-
stats.totalCost +=
|
|
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:
|
|
78
|
-
activeModel:
|
|
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:
|
|
89
|
-
activeModel:
|
|
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 {
|
|
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
|
|
28
|
+
export interface TelegramTurnMessage {
|
|
17
29
|
message_id: number;
|
|
18
30
|
chat: { id: number };
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
export
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
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:
|
|
240
|
+
messages: TelegramTurnMessage[];
|
|
181
241
|
historyTurns?: PendingTelegramTurn[];
|
|
182
242
|
queueOrder: number;
|
|
183
243
|
rawText: string;
|
|
184
|
-
files:
|
|
244
|
+
files: DownloadedTelegramTurnFile[];
|
|
185
245
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
186
246
|
inferImageMimeType: (path: string) => string | undefined;
|
|
187
|
-
}
|
|
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:
|
|
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
|
+
}
|