@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/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
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
7
-
8
- import type { TelegramConfig } from "./api.ts";
6
+ export interface TelegramPollingConfig {
7
+ botToken?: string;
8
+ lastUpdateId?: number;
9
+ }
9
10
 
10
- export interface TelegramUpdateLike {
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: TelegramUpdateLike[],
51
+ updates: TelegramUpdate[],
51
52
  ): number | undefined {
52
53
  return updates.at(-1)?.update_id;
53
54
  }
@@ -62,30 +63,241 @@ export function shouldStopTelegramPolling(
62
63
  );
63
64
  }
64
65
 
65
- export interface TelegramPollLoopDeps<TUpdate extends TelegramUpdateLike> {
66
- ctx: ExtensionContext;
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: TelegramConfig;
69
- deleteWebhook: (signal: AbortSignal) => Promise<void>;
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: ExtensionContext) => Promise<void>;
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>;
79
240
  maxUpdateFailures?: number;
80
241
  }
81
242
 
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
+
82
293
  function getTelegramPollingErrorMessage(error: unknown): string {
83
294
  return error instanceof Error ? error.message : String(error);
84
295
  }
85
296
 
86
- export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
87
- deps: TelegramPollLoopDeps<TUpdate>,
88
- ): Promise<void> {
297
+ export async function runTelegramPollLoop<
298
+ TUpdate extends TelegramUpdate,
299
+ TContext = unknown,
300
+ >(deps: TelegramPollLoopDeps<TUpdate, TContext>): Promise<void> {
89
301
  if (!deps.config.botToken) return;
90
302
  try {
91
303
  await deps.deleteWebhook(deps.signal);
@@ -109,6 +321,7 @@ export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
109
321
  }
110
322
  const maxUpdateFailures = Math.max(1, deps.maxUpdateFailures ?? 3);
111
323
  const updateFailures = new Map<number, number>();
324
+ let handledUpdateFailureRethrown = false;
112
325
  while (!deps.signal.aborted) {
113
326
  try {
114
327
  const updates = await deps.getUpdates(
@@ -124,7 +337,15 @@ export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
124
337
  } catch (error) {
125
338
  const failureCount = (updateFailures.get(update.update_id) ?? 0) + 1;
126
339
  updateFailures.set(update.update_id, failureCount);
127
- if (failureCount < maxUpdateFailures) throw error;
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
+ }
128
349
  const message = getTelegramPollingErrorMessage(error);
129
350
  deps.onErrorStatus(
130
351
  `skipping Telegram update ${update.update_id} after ${failureCount} failures: ${message}`,
@@ -136,6 +357,11 @@ export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
136
357
  }
137
358
  } catch (error) {
138
359
  if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
360
+ if (handledUpdateFailureRethrown) {
361
+ handledUpdateFailureRethrown = false;
362
+ } else {
363
+ deps.recordRuntimeEvent?.("polling", error, { phase: "loop" });
364
+ }
139
365
  deps.onErrorStatus(getTelegramPollingErrorMessage(error));
140
366
  await deps.sleep(3000);
141
367
  deps.onStatusReset();