@llblab/pi-telegram 0.2.9 → 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 +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- 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 +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
package/lib/pi.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi SDK adapter boundary
|
|
3
|
+
* Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type AgentEndEvent,
|
|
8
|
+
type AgentStartEvent,
|
|
9
|
+
type BeforeAgentStartEvent,
|
|
10
|
+
type ExtensionAPI,
|
|
11
|
+
type ExtensionCommandContext,
|
|
12
|
+
type ExtensionContext,
|
|
13
|
+
type SessionShutdownEvent,
|
|
14
|
+
type SessionStartEvent,
|
|
15
|
+
SettingsManager,
|
|
16
|
+
} from "@mariozechner/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
AgentEndEvent,
|
|
20
|
+
AgentStartEvent,
|
|
21
|
+
BeforeAgentStartEvent,
|
|
22
|
+
ExtensionAPI,
|
|
23
|
+
ExtensionCommandContext,
|
|
24
|
+
ExtensionContext,
|
|
25
|
+
SessionShutdownEvent,
|
|
26
|
+
SessionStartEvent,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface PiSettingsManager {
|
|
30
|
+
reload: () => Promise<void>;
|
|
31
|
+
getEnabledModels: () => string[] | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PiExtensionApiRuntimePorts {
|
|
35
|
+
sendUserMessage: ExtensionAPI["sendUserMessage"];
|
|
36
|
+
getThinkingLevel: ExtensionAPI["getThinkingLevel"];
|
|
37
|
+
setThinkingLevel: ExtensionAPI["setThinkingLevel"];
|
|
38
|
+
setModel: ExtensionAPI["setModel"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createExtensionApiRuntimePorts(
|
|
42
|
+
api: Pick<
|
|
43
|
+
ExtensionAPI,
|
|
44
|
+
"sendUserMessage" | "getThinkingLevel" | "setThinkingLevel" | "setModel"
|
|
45
|
+
>,
|
|
46
|
+
): PiExtensionApiRuntimePorts {
|
|
47
|
+
return {
|
|
48
|
+
sendUserMessage: (content) => api.sendUserMessage(content),
|
|
49
|
+
getThinkingLevel: () => api.getThinkingLevel(),
|
|
50
|
+
setThinkingLevel: (level) => api.setThinkingLevel(level),
|
|
51
|
+
setModel: (model) => api.setModel(model),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createSettingsManager(cwd: string): PiSettingsManager {
|
|
56
|
+
return SettingsManager.create(cwd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getExtensionContextModel(
|
|
60
|
+
ctx: ExtensionContext,
|
|
61
|
+
): ExtensionContext["model"] {
|
|
62
|
+
return ctx.model;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
|
|
66
|
+
return ctx.isIdle();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function hasExtensionContextPendingMessages(
|
|
70
|
+
ctx: ExtensionContext,
|
|
71
|
+
): boolean {
|
|
72
|
+
return ctx.hasPendingMessages();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function compactExtensionContext(
|
|
76
|
+
ctx: ExtensionContext,
|
|
77
|
+
callbacks: Parameters<ExtensionContext["compact"]>[0],
|
|
78
|
+
): ReturnType<ExtensionContext["compact"]> {
|
|
79
|
+
return ctx.compact(callbacks);
|
|
80
|
+
}
|
package/lib/polling.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export interface TelegramPollingConfig {
|
|
7
|
+
botToken?: string;
|
|
8
|
+
lastUpdateId?: number;
|
|
9
|
+
}
|
|
9
10
|
|
|
10
|
-
export interface
|
|
11
|
+
export interface TelegramUpdate {
|
|
11
12
|
update_id: number;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -47,7 +48,7 @@ export function buildTelegramLongPollRequest(lastUpdateId?: number): {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export function getLatestTelegramUpdateId(
|
|
50
|
-
updates:
|
|
51
|
+
updates: TelegramUpdate[],
|
|
51
52
|
): number | undefined {
|
|
52
53
|
return updates.at(-1)?.update_id;
|
|
53
54
|
}
|
|
@@ -62,25 +63,241 @@ export function shouldStopTelegramPolling(
|
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export interface
|
|
66
|
-
|
|
66
|
+
export interface TelegramPollingStartState {
|
|
67
|
+
hasBotToken: boolean;
|
|
68
|
+
hasPollingPromise: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TelegramPollingControllerState {
|
|
72
|
+
pollingPromise?: Promise<void>;
|
|
73
|
+
pollingController?: AbortController;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createTelegramPollingControllerState(): TelegramPollingControllerState {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isTelegramPollingControllerActive(
|
|
81
|
+
state: TelegramPollingControllerState,
|
|
82
|
+
): boolean {
|
|
83
|
+
return !!state.pollingPromise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createTelegramPollingActivityReader(
|
|
87
|
+
state: TelegramPollingControllerState,
|
|
88
|
+
): () => boolean {
|
|
89
|
+
return () => isTelegramPollingControllerActive(state);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TelegramPollingRuntimeDeps<TContext> {
|
|
93
|
+
hasBotToken: () => boolean;
|
|
94
|
+
getPollingPromise: () => Promise<void> | undefined;
|
|
95
|
+
setPollingPromise: (promise: Promise<void> | undefined) => void;
|
|
96
|
+
getPollingController: () => AbortController | undefined;
|
|
97
|
+
setPollingController: (controller: AbortController | undefined) => void;
|
|
98
|
+
stopTypingLoop: () => unknown;
|
|
99
|
+
runPollLoop: (ctx: TContext, signal: AbortSignal) => Promise<void>;
|
|
100
|
+
updateStatus: (ctx: TContext) => void;
|
|
101
|
+
createAbortController?: () => AbortController;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type TelegramPollingControllerDeps<TContext> = Omit<
|
|
105
|
+
TelegramPollingRuntimeDeps<TContext>,
|
|
106
|
+
| "getPollingPromise"
|
|
107
|
+
| "setPollingPromise"
|
|
108
|
+
| "getPollingController"
|
|
109
|
+
| "setPollingController"
|
|
110
|
+
> & { state?: TelegramPollingControllerState };
|
|
111
|
+
|
|
112
|
+
export interface TelegramPollingController<TContext> {
|
|
113
|
+
isActive: () => boolean;
|
|
114
|
+
start: (ctx: TContext) => void;
|
|
115
|
+
stop: () => Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface TelegramPollingControllerRuntimeDeps<
|
|
119
|
+
TUpdate extends TelegramUpdate,
|
|
120
|
+
TContext = unknown,
|
|
121
|
+
> extends TelegramPollLoopRunnerDeps<TUpdate, TContext> {
|
|
122
|
+
state?: TelegramPollingControllerState;
|
|
123
|
+
hasBotToken: () => boolean;
|
|
124
|
+
stopTypingLoop: () => unknown;
|
|
125
|
+
createAbortController?: () => AbortController;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createTelegramPollingControllerRuntime<
|
|
129
|
+
TUpdate extends TelegramUpdate,
|
|
130
|
+
TContext = unknown,
|
|
131
|
+
>(
|
|
132
|
+
deps: TelegramPollingControllerRuntimeDeps<TUpdate, TContext>,
|
|
133
|
+
): TelegramPollingController<TContext> {
|
|
134
|
+
return createTelegramPollingController({
|
|
135
|
+
state: deps.state,
|
|
136
|
+
hasBotToken: deps.hasBotToken,
|
|
137
|
+
stopTypingLoop: deps.stopTypingLoop,
|
|
138
|
+
runPollLoop: createTelegramPollLoopRunner<TUpdate, TContext>({
|
|
139
|
+
getConfig: deps.getConfig,
|
|
140
|
+
deleteWebhook: deps.deleteWebhook,
|
|
141
|
+
getUpdates: deps.getUpdates,
|
|
142
|
+
persistConfig: deps.persistConfig,
|
|
143
|
+
handleUpdate: deps.handleUpdate,
|
|
144
|
+
updateStatus: deps.updateStatus,
|
|
145
|
+
sleep: deps.sleep,
|
|
146
|
+
maxUpdateFailures: deps.maxUpdateFailures,
|
|
147
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
148
|
+
}),
|
|
149
|
+
updateStatus: deps.updateStatus,
|
|
150
|
+
createAbortController: deps.createAbortController,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createTelegramPollingController<TContext>(
|
|
155
|
+
deps: TelegramPollingControllerDeps<TContext>,
|
|
156
|
+
): TelegramPollingController<TContext> {
|
|
157
|
+
const state = deps.state ?? createTelegramPollingControllerState();
|
|
158
|
+
const runtimeDeps: TelegramPollingRuntimeDeps<TContext> = {
|
|
159
|
+
...deps,
|
|
160
|
+
getPollingPromise: () => state.pollingPromise,
|
|
161
|
+
setPollingPromise: (promise) => {
|
|
162
|
+
state.pollingPromise = promise;
|
|
163
|
+
},
|
|
164
|
+
getPollingController: () => state.pollingController,
|
|
165
|
+
setPollingController: (controller) => {
|
|
166
|
+
state.pollingController = controller;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
isActive: () => isTelegramPollingControllerActive(state),
|
|
171
|
+
start: (ctx) => startTelegramPollingRuntime(ctx, runtimeDeps),
|
|
172
|
+
stop: () => stopTelegramPollingRuntime(runtimeDeps),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function shouldStartTelegramPolling(
|
|
177
|
+
state: TelegramPollingStartState,
|
|
178
|
+
): boolean {
|
|
179
|
+
return state.hasBotToken && !state.hasPollingPromise;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function stopTelegramPollingRuntime<TContext>(
|
|
183
|
+
deps: TelegramPollingRuntimeDeps<TContext>,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
deps.stopTypingLoop();
|
|
186
|
+
deps.getPollingController()?.abort();
|
|
187
|
+
deps.setPollingController(undefined);
|
|
188
|
+
await deps.getPollingPromise()?.catch(() => undefined);
|
|
189
|
+
deps.setPollingPromise(undefined);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function startTelegramPollingRuntime<TContext>(
|
|
193
|
+
ctx: TContext,
|
|
194
|
+
deps: TelegramPollingRuntimeDeps<TContext>,
|
|
195
|
+
): void {
|
|
196
|
+
if (
|
|
197
|
+
!shouldStartTelegramPolling({
|
|
198
|
+
hasBotToken: deps.hasBotToken(),
|
|
199
|
+
hasPollingPromise: !!deps.getPollingPromise(),
|
|
200
|
+
})
|
|
201
|
+
) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const controller = deps.createAbortController?.() ?? new AbortController();
|
|
205
|
+
deps.setPollingController(controller);
|
|
206
|
+
const promise = deps.runPollLoop(ctx, controller.signal).finally(() => {
|
|
207
|
+
deps.setPollingPromise(undefined);
|
|
208
|
+
deps.setPollingController(undefined);
|
|
209
|
+
deps.updateStatus(ctx);
|
|
210
|
+
});
|
|
211
|
+
deps.setPollingPromise(promise);
|
|
212
|
+
deps.updateStatus(ctx);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TelegramRuntimeEventRecorderPort {
|
|
216
|
+
recordRuntimeEvent?: (
|
|
217
|
+
category: string,
|
|
218
|
+
error: unknown,
|
|
219
|
+
details?: Record<string, unknown>,
|
|
220
|
+
) => void;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface TelegramPollLoopDeps<
|
|
224
|
+
TUpdate extends TelegramUpdate,
|
|
225
|
+
TContext = unknown,
|
|
226
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
227
|
+
ctx: TContext;
|
|
67
228
|
signal: AbortSignal;
|
|
68
|
-
config:
|
|
69
|
-
deleteWebhook: (signal: AbortSignal) => Promise<
|
|
229
|
+
config: TelegramPollingConfig;
|
|
230
|
+
deleteWebhook: (signal: AbortSignal) => Promise<unknown>;
|
|
70
231
|
getUpdates: (
|
|
71
232
|
body: Record<string, unknown>,
|
|
72
233
|
signal: AbortSignal,
|
|
73
234
|
) => Promise<TUpdate[]>;
|
|
74
235
|
persistConfig: () => Promise<void>;
|
|
75
|
-
handleUpdate: (update: TUpdate, ctx:
|
|
236
|
+
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
76
237
|
onErrorStatus: (message: string) => void;
|
|
77
238
|
onStatusReset: () => void;
|
|
78
239
|
sleep: (ms: number) => Promise<void>;
|
|
240
|
+
maxUpdateFailures?: number;
|
|
79
241
|
}
|
|
80
242
|
|
|
81
|
-
export
|
|
82
|
-
|
|
83
|
-
|
|
243
|
+
export interface TelegramPollLoopRunnerDeps<
|
|
244
|
+
TUpdate extends TelegramUpdate,
|
|
245
|
+
TContext = unknown,
|
|
246
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
247
|
+
getConfig: () => TelegramPollingConfig;
|
|
248
|
+
deleteWebhook: (signal: AbortSignal) => Promise<unknown>;
|
|
249
|
+
getUpdates: (
|
|
250
|
+
body: Record<string, unknown>,
|
|
251
|
+
signal: AbortSignal,
|
|
252
|
+
) => Promise<TUpdate[]>;
|
|
253
|
+
persistConfig: () => Promise<void>;
|
|
254
|
+
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
255
|
+
updateStatus: (ctx: TContext, message?: string) => void;
|
|
256
|
+
sleep?: (ms: number) => Promise<void>;
|
|
257
|
+
maxUpdateFailures?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function createTelegramPollLoopRunner<
|
|
261
|
+
TUpdate extends TelegramUpdate,
|
|
262
|
+
TContext = unknown,
|
|
263
|
+
>(
|
|
264
|
+
deps: TelegramPollLoopRunnerDeps<TUpdate, TContext>,
|
|
265
|
+
): (ctx: TContext, signal: AbortSignal) => Promise<void> {
|
|
266
|
+
const sleep =
|
|
267
|
+
deps.sleep ??
|
|
268
|
+
((ms: number) =>
|
|
269
|
+
new Promise<void>((resolve) => {
|
|
270
|
+
setTimeout(resolve, ms);
|
|
271
|
+
}));
|
|
272
|
+
return (ctx, signal) =>
|
|
273
|
+
runTelegramPollLoop({
|
|
274
|
+
ctx,
|
|
275
|
+
signal,
|
|
276
|
+
config: deps.getConfig(),
|
|
277
|
+
deleteWebhook: deps.deleteWebhook,
|
|
278
|
+
getUpdates: deps.getUpdates,
|
|
279
|
+
persistConfig: deps.persistConfig,
|
|
280
|
+
handleUpdate: deps.handleUpdate,
|
|
281
|
+
onErrorStatus: (message) => {
|
|
282
|
+
deps.updateStatus(ctx, message);
|
|
283
|
+
},
|
|
284
|
+
onStatusReset: () => {
|
|
285
|
+
deps.updateStatus(ctx);
|
|
286
|
+
},
|
|
287
|
+
sleep,
|
|
288
|
+
maxUpdateFailures: deps.maxUpdateFailures,
|
|
289
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function getTelegramPollingErrorMessage(error: unknown): string {
|
|
294
|
+
return error instanceof Error ? error.message : String(error);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function runTelegramPollLoop<
|
|
298
|
+
TUpdate extends TelegramUpdate,
|
|
299
|
+
TContext = unknown,
|
|
300
|
+
>(deps: TelegramPollLoopDeps<TUpdate, TContext>): Promise<void> {
|
|
84
301
|
if (!deps.config.botToken) return;
|
|
85
302
|
try {
|
|
86
303
|
await deps.deleteWebhook(deps.signal);
|
|
@@ -102,6 +319,9 @@ export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
|
|
|
102
319
|
// ignore
|
|
103
320
|
}
|
|
104
321
|
}
|
|
322
|
+
const maxUpdateFailures = Math.max(1, deps.maxUpdateFailures ?? 3);
|
|
323
|
+
const updateFailures = new Map<number, number>();
|
|
324
|
+
let handledUpdateFailureRethrown = false;
|
|
105
325
|
while (!deps.signal.aborted) {
|
|
106
326
|
try {
|
|
107
327
|
const updates = await deps.getUpdates(
|
|
@@ -109,14 +329,40 @@ export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
|
|
|
109
329
|
deps.signal,
|
|
110
330
|
);
|
|
111
331
|
for (const update of updates) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
332
|
+
try {
|
|
333
|
+
await deps.handleUpdate(update, deps.ctx);
|
|
334
|
+
deps.config.lastUpdateId = update.update_id;
|
|
335
|
+
updateFailures.delete(update.update_id);
|
|
336
|
+
await deps.persistConfig();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const failureCount = (updateFailures.get(update.update_id) ?? 0) + 1;
|
|
339
|
+
updateFailures.set(update.update_id, failureCount);
|
|
340
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
341
|
+
phase: "handleUpdate",
|
|
342
|
+
updateId: update.update_id,
|
|
343
|
+
failureCount,
|
|
344
|
+
});
|
|
345
|
+
if (failureCount < maxUpdateFailures) {
|
|
346
|
+
handledUpdateFailureRethrown = true;
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
const message = getTelegramPollingErrorMessage(error);
|
|
350
|
+
deps.onErrorStatus(
|
|
351
|
+
`skipping Telegram update ${update.update_id} after ${failureCount} failures: ${message}`,
|
|
352
|
+
);
|
|
353
|
+
deps.config.lastUpdateId = update.update_id;
|
|
354
|
+
updateFailures.delete(update.update_id);
|
|
355
|
+
await deps.persistConfig();
|
|
356
|
+
}
|
|
115
357
|
}
|
|
116
358
|
} catch (error) {
|
|
117
359
|
if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
|
|
118
|
-
|
|
119
|
-
|
|
360
|
+
if (handledUpdateFailureRethrown) {
|
|
361
|
+
handledUpdateFailureRethrown = false;
|
|
362
|
+
} else {
|
|
363
|
+
deps.recordRuntimeEvent?.("polling", error, { phase: "loop" });
|
|
364
|
+
}
|
|
365
|
+
deps.onErrorStatus(getTelegramPollingErrorMessage(error));
|
|
120
366
|
await deps.sleep(3000);
|
|
121
367
|
deps.onStatusReset();
|
|
122
368
|
}
|