@llblab/pi-telegram 0.2.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/AGENTS.md +90 -0
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +17 -0
- package/README.md +202 -0
- package/docs/README.md +9 -0
- package/docs/architecture.md +148 -0
- package/index.ts +1968 -0
- package/lib/api.ts +222 -0
- package/lib/attachments.ts +98 -0
- package/lib/media.ts +234 -0
- package/lib/menu.ts +951 -0
- package/lib/model-switch.ts +62 -0
- package/lib/polling.ts +122 -0
- package/lib/queue.ts +534 -0
- package/lib/registration.ts +163 -0
- package/lib/rendering.ts +697 -0
- package/lib/replies.ts +313 -0
- package/lib/setup.ts +41 -0
- package/lib/status.ts +109 -0
- package/lib/turns.ts +144 -0
- package/lib/updates.ts +397 -0
- package/package.json +40 -0
- package/screenshot.png +0 -0
- package/tests/api.test.ts +89 -0
- package/tests/attachments.test.ts +132 -0
- package/tests/config.test.ts +80 -0
- package/tests/media.test.ts +77 -0
- package/tests/menu.test.ts +645 -0
- package/tests/polling.test.ts +129 -0
- package/tests/queue.test.ts +2982 -0
- package/tests/registration.test.ts +268 -0
- package/tests/rendering.test.ts +308 -0
- package/tests/replies.test.ts +362 -0
- package/tests/turns.test.ts +132 -0
- package/tests/updates.test.ts +366 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight Telegram model-switch helpers
|
|
3
|
+
* Encodes the safe restart and continuation rules for switching models during active Telegram-owned runs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
7
|
+
|
|
8
|
+
import type { TelegramInFlightModelSwitchState } from "./queue.ts";
|
|
9
|
+
|
|
10
|
+
export type TelegramThinkingLevel =
|
|
11
|
+
| "off"
|
|
12
|
+
| "minimal"
|
|
13
|
+
| "low"
|
|
14
|
+
| "medium"
|
|
15
|
+
| "high"
|
|
16
|
+
| "xhigh";
|
|
17
|
+
|
|
18
|
+
export function canRestartTelegramTurnForModelSwitch(
|
|
19
|
+
state: TelegramInFlightModelSwitchState,
|
|
20
|
+
): boolean {
|
|
21
|
+
return !state.isIdle && state.hasActiveTelegramTurn && state.hasAbortHandler;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldTriggerPendingTelegramModelSwitchAbort(state: {
|
|
25
|
+
hasPendingModelSwitch: boolean;
|
|
26
|
+
hasActiveTelegramTurn: boolean;
|
|
27
|
+
hasAbortHandler: boolean;
|
|
28
|
+
activeToolExecutions: number;
|
|
29
|
+
}): boolean {
|
|
30
|
+
return (
|
|
31
|
+
state.hasPendingModelSwitch &&
|
|
32
|
+
state.hasActiveTelegramTurn &&
|
|
33
|
+
state.hasAbortHandler &&
|
|
34
|
+
state.activeToolExecutions === 0
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function restartTelegramModelSwitchContinuation<TTurn, TSelection>(state: {
|
|
39
|
+
activeTurn: TTurn | undefined;
|
|
40
|
+
abort: (() => void) | undefined;
|
|
41
|
+
selection: TSelection;
|
|
42
|
+
queueContinuation: (turn: TTurn, selection: TSelection) => void;
|
|
43
|
+
}): boolean {
|
|
44
|
+
if (!state.activeTurn || !state.abort) return false;
|
|
45
|
+
state.queueContinuation(state.activeTurn, state.selection);
|
|
46
|
+
state.abort();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildTelegramModelSwitchContinuationText<
|
|
51
|
+
TModel extends Pick<Model<any>, "provider" | "id">,
|
|
52
|
+
>(
|
|
53
|
+
telegramPrefix: string,
|
|
54
|
+
model: TModel,
|
|
55
|
+
thinkingLevel?: TelegramThinkingLevel,
|
|
56
|
+
): string {
|
|
57
|
+
const modelLabel = `${model.provider}/${model.id}`;
|
|
58
|
+
const thinkingSuffix = thinkingLevel
|
|
59
|
+
? ` Keep the selected thinking level (${thinkingLevel}) if it still applies.`
|
|
60
|
+
: "";
|
|
61
|
+
return `${telegramPrefix} Continue the interrupted previous Telegram request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
|
|
62
|
+
}
|
package/lib/polling.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram polling domain helpers
|
|
3
|
+
* Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
import type { TelegramConfig } from "./api.ts";
|
|
9
|
+
|
|
10
|
+
export interface TelegramUpdateLike {
|
|
11
|
+
update_id: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
15
|
+
"message",
|
|
16
|
+
"edited_message",
|
|
17
|
+
"callback_query",
|
|
18
|
+
"message_reaction",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export function buildTelegramInitialSyncRequest(): {
|
|
22
|
+
offset: number;
|
|
23
|
+
limit: number;
|
|
24
|
+
timeout: number;
|
|
25
|
+
} {
|
|
26
|
+
return {
|
|
27
|
+
offset: -1,
|
|
28
|
+
limit: 1,
|
|
29
|
+
timeout: 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildTelegramLongPollRequest(lastUpdateId?: number): {
|
|
34
|
+
offset?: number;
|
|
35
|
+
limit: number;
|
|
36
|
+
timeout: number;
|
|
37
|
+
allowed_updates: readonly string[];
|
|
38
|
+
} {
|
|
39
|
+
return {
|
|
40
|
+
offset: lastUpdateId !== undefined ? lastUpdateId + 1 : undefined,
|
|
41
|
+
limit: 10,
|
|
42
|
+
timeout: 30,
|
|
43
|
+
allowed_updates: TELEGRAM_ALLOWED_UPDATES,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getLatestTelegramUpdateId(
|
|
48
|
+
updates: TelegramUpdateLike[],
|
|
49
|
+
): number | undefined {
|
|
50
|
+
return updates.at(-1)?.update_id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function shouldStopTelegramPolling(
|
|
54
|
+
signalAborted: boolean,
|
|
55
|
+
error: unknown,
|
|
56
|
+
): boolean {
|
|
57
|
+
return (
|
|
58
|
+
signalAborted ||
|
|
59
|
+
(error instanceof DOMException && error.name === "AbortError")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TelegramPollLoopDeps<TUpdate extends TelegramUpdateLike> {
|
|
64
|
+
ctx: ExtensionContext;
|
|
65
|
+
signal: AbortSignal;
|
|
66
|
+
config: TelegramConfig;
|
|
67
|
+
deleteWebhook: (signal: AbortSignal) => Promise<void>;
|
|
68
|
+
getUpdates: (
|
|
69
|
+
body: Record<string, unknown>,
|
|
70
|
+
signal: AbortSignal,
|
|
71
|
+
) => Promise<TUpdate[]>;
|
|
72
|
+
persistConfig: () => Promise<void>;
|
|
73
|
+
handleUpdate: (update: TUpdate, ctx: ExtensionContext) => Promise<void>;
|
|
74
|
+
onErrorStatus: (message: string) => void;
|
|
75
|
+
onStatusReset: () => void;
|
|
76
|
+
sleep: (ms: number) => Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runTelegramPollLoop<TUpdate extends TelegramUpdateLike>(
|
|
80
|
+
deps: TelegramPollLoopDeps<TUpdate>,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (!deps.config.botToken) return;
|
|
83
|
+
try {
|
|
84
|
+
await deps.deleteWebhook(deps.signal);
|
|
85
|
+
} catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
if (deps.config.lastUpdateId === undefined) {
|
|
89
|
+
try {
|
|
90
|
+
const updates = await deps.getUpdates(
|
|
91
|
+
buildTelegramInitialSyncRequest(),
|
|
92
|
+
deps.signal,
|
|
93
|
+
);
|
|
94
|
+
const lastUpdateId = getLatestTelegramUpdateId(updates);
|
|
95
|
+
if (lastUpdateId !== undefined) {
|
|
96
|
+
deps.config.lastUpdateId = lastUpdateId;
|
|
97
|
+
await deps.persistConfig();
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
while (!deps.signal.aborted) {
|
|
104
|
+
try {
|
|
105
|
+
const updates = await deps.getUpdates(
|
|
106
|
+
buildTelegramLongPollRequest(deps.config.lastUpdateId),
|
|
107
|
+
deps.signal,
|
|
108
|
+
);
|
|
109
|
+
for (const update of updates) {
|
|
110
|
+
deps.config.lastUpdateId = update.update_id;
|
|
111
|
+
await deps.persistConfig();
|
|
112
|
+
await deps.handleUpdate(update, deps.ctx);
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
deps.onErrorStatus(message);
|
|
118
|
+
await deps.sleep(3000);
|
|
119
|
+
deps.onStatusReset();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
package/lib/queue.ts
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram queue and queue-runtime domain helpers
|
|
3
|
+
* Owns queue items, queue mutations, dispatch and lifecycle planning, session resets, and queue-adjacent runtime helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
|
|
7
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
// --- Queue Items ---
|
|
10
|
+
|
|
11
|
+
export interface QueuedAttachment {
|
|
12
|
+
path: string;
|
|
13
|
+
fileName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TelegramQueueItemKind = "prompt" | "control";
|
|
17
|
+
export type TelegramQueueLane = "control" | "priority" | "default";
|
|
18
|
+
|
|
19
|
+
export interface TelegramQueueItemBase {
|
|
20
|
+
kind: TelegramQueueItemKind;
|
|
21
|
+
chatId: number;
|
|
22
|
+
replyToMessageId: number;
|
|
23
|
+
queueOrder: number;
|
|
24
|
+
queueLane: TelegramQueueLane;
|
|
25
|
+
laneOrder: number;
|
|
26
|
+
statusSummary: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
30
|
+
kind: "prompt";
|
|
31
|
+
sourceMessageIds: number[];
|
|
32
|
+
queuedAttachments: QueuedAttachment[];
|
|
33
|
+
content: Array<TextContent | ImageContent>;
|
|
34
|
+
historyText: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PendingTelegramControlItem extends TelegramQueueItemBase {
|
|
38
|
+
kind: "control";
|
|
39
|
+
controlType: "status" | "model";
|
|
40
|
+
execute: (ctx: ExtensionContext) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type TelegramQueueItem =
|
|
44
|
+
| PendingTelegramTurn
|
|
45
|
+
| PendingTelegramControlItem;
|
|
46
|
+
|
|
47
|
+
export interface TelegramDispatchGuardState {
|
|
48
|
+
compactionInProgress: boolean;
|
|
49
|
+
hasActiveTelegramTurn: boolean;
|
|
50
|
+
hasPendingTelegramDispatch: boolean;
|
|
51
|
+
isIdle: boolean;
|
|
52
|
+
hasPendingMessages: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TelegramInFlightModelSwitchState {
|
|
56
|
+
isIdle: boolean;
|
|
57
|
+
hasActiveTelegramTurn: boolean;
|
|
58
|
+
hasAbortHandler: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getTelegramQueueLaneRank(lane: TelegramQueueLane): number {
|
|
62
|
+
switch (lane) {
|
|
63
|
+
case "control":
|
|
64
|
+
return 0;
|
|
65
|
+
case "priority":
|
|
66
|
+
return 1;
|
|
67
|
+
default:
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isPendingTelegramTurn(
|
|
73
|
+
item: TelegramQueueItem,
|
|
74
|
+
): item is PendingTelegramTurn {
|
|
75
|
+
return item.kind === "prompt";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Queue Mutations ---
|
|
79
|
+
|
|
80
|
+
export function partitionTelegramQueueItemsForHistory(
|
|
81
|
+
items: TelegramQueueItem[],
|
|
82
|
+
): {
|
|
83
|
+
historyTurns: PendingTelegramTurn[];
|
|
84
|
+
remainingItems: TelegramQueueItem[];
|
|
85
|
+
} {
|
|
86
|
+
const historyTurns: PendingTelegramTurn[] = [];
|
|
87
|
+
const remainingItems: TelegramQueueItem[] = [];
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
if (isPendingTelegramTurn(item)) {
|
|
90
|
+
historyTurns.push(item);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
remainingItems.push(item);
|
|
94
|
+
}
|
|
95
|
+
return { historyTurns, remainingItems };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function compareTelegramQueueItems(
|
|
99
|
+
left: TelegramQueueItem,
|
|
100
|
+
right: TelegramQueueItem,
|
|
101
|
+
): number {
|
|
102
|
+
const laneRankDelta =
|
|
103
|
+
getTelegramQueueLaneRank(left.queueLane) -
|
|
104
|
+
getTelegramQueueLaneRank(right.queueLane);
|
|
105
|
+
if (laneRankDelta !== 0) return laneRankDelta;
|
|
106
|
+
if (left.laneOrder !== right.laneOrder) {
|
|
107
|
+
return left.laneOrder - right.laneOrder;
|
|
108
|
+
}
|
|
109
|
+
return left.queueOrder - right.queueOrder;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function removeTelegramQueueItemsByMessageIds(
|
|
113
|
+
items: TelegramQueueItem[],
|
|
114
|
+
messageIds: number[],
|
|
115
|
+
): { items: TelegramQueueItem[]; removedCount: number } {
|
|
116
|
+
if (messageIds.length === 0 || items.length === 0) {
|
|
117
|
+
return { items, removedCount: 0 };
|
|
118
|
+
}
|
|
119
|
+
const deletedMessageIds = new Set(messageIds);
|
|
120
|
+
const nextItems = items.filter((item) => {
|
|
121
|
+
if (!isPendingTelegramTurn(item)) return true;
|
|
122
|
+
return !item.sourceMessageIds.some((messageId) =>
|
|
123
|
+
deletedMessageIds.has(messageId),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
items: nextItems,
|
|
128
|
+
removedCount: items.length - nextItems.length,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function clearTelegramQueuePromptPriority(
|
|
133
|
+
items: TelegramQueueItem[],
|
|
134
|
+
messageId: number,
|
|
135
|
+
): { items: TelegramQueueItem[]; changed: boolean } {
|
|
136
|
+
let changed = false;
|
|
137
|
+
const nextItems = items.map((item) => {
|
|
138
|
+
if (
|
|
139
|
+
!isPendingTelegramTurn(item) ||
|
|
140
|
+
!item.sourceMessageIds.includes(messageId) ||
|
|
141
|
+
item.queueLane !== "priority"
|
|
142
|
+
) {
|
|
143
|
+
return item;
|
|
144
|
+
}
|
|
145
|
+
changed = true;
|
|
146
|
+
return {
|
|
147
|
+
...item,
|
|
148
|
+
queueLane: "default" as const,
|
|
149
|
+
laneOrder: item.queueOrder,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
return { items: nextItems, changed };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function prioritizeTelegramQueuePrompt(
|
|
156
|
+
items: TelegramQueueItem[],
|
|
157
|
+
messageId: number,
|
|
158
|
+
laneOrder: number,
|
|
159
|
+
): { items: TelegramQueueItem[]; changed: boolean } {
|
|
160
|
+
let changed = false;
|
|
161
|
+
const nextItems = items.map((item) => {
|
|
162
|
+
if (
|
|
163
|
+
!isPendingTelegramTurn(item) ||
|
|
164
|
+
!item.sourceMessageIds.includes(messageId)
|
|
165
|
+
) {
|
|
166
|
+
return item;
|
|
167
|
+
}
|
|
168
|
+
changed = true;
|
|
169
|
+
return {
|
|
170
|
+
...item,
|
|
171
|
+
queueLane: "priority" as const,
|
|
172
|
+
laneOrder,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
return { items: nextItems, changed };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function consumeDispatchedTelegramPrompt(
|
|
179
|
+
items: TelegramQueueItem[],
|
|
180
|
+
hasPendingDispatch: boolean,
|
|
181
|
+
): { activeTurn?: PendingTelegramTurn; remainingItems: TelegramQueueItem[] } {
|
|
182
|
+
if (!hasPendingDispatch) {
|
|
183
|
+
return { activeTurn: undefined, remainingItems: items };
|
|
184
|
+
}
|
|
185
|
+
const nextItem = items[0];
|
|
186
|
+
if (!nextItem || !isPendingTelegramTurn(nextItem)) {
|
|
187
|
+
return { activeTurn: undefined, remainingItems: items };
|
|
188
|
+
}
|
|
189
|
+
return { activeTurn: nextItem, remainingItems: items.slice(1) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatQueuedTelegramItemsStatus(
|
|
193
|
+
items: TelegramQueueItem[],
|
|
194
|
+
): string {
|
|
195
|
+
if (items.length === 0) return "";
|
|
196
|
+
const previewCount = 4;
|
|
197
|
+
const summaries = items
|
|
198
|
+
.slice(0, previewCount)
|
|
199
|
+
.map((item) => item.statusSummary)
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
if (summaries.length === 0) return ` +${items.length}`;
|
|
202
|
+
const suffix = items.length > summaries.length ? ", …" : "";
|
|
203
|
+
return ` +${items.length}: [${summaries.join(", ")}${suffix}]`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function canDispatchTelegramTurnState(
|
|
207
|
+
state: TelegramDispatchGuardState,
|
|
208
|
+
): boolean {
|
|
209
|
+
return (
|
|
210
|
+
!state.compactionInProgress &&
|
|
211
|
+
!state.hasActiveTelegramTurn &&
|
|
212
|
+
!state.hasPendingTelegramDispatch &&
|
|
213
|
+
state.isIdle &&
|
|
214
|
+
!state.hasPendingMessages
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function canRestartTelegramTurnForModelSwitch(
|
|
219
|
+
state: TelegramInFlightModelSwitchState,
|
|
220
|
+
): boolean {
|
|
221
|
+
return !state.isIdle && state.hasActiveTelegramTurn && state.hasAbortHandler;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function shouldTriggerPendingTelegramModelSwitchAbort(state: {
|
|
225
|
+
hasPendingModelSwitch: boolean;
|
|
226
|
+
hasActiveTelegramTurn: boolean;
|
|
227
|
+
hasAbortHandler: boolean;
|
|
228
|
+
activeToolExecutions: number;
|
|
229
|
+
}): boolean {
|
|
230
|
+
return (
|
|
231
|
+
state.hasPendingModelSwitch &&
|
|
232
|
+
state.hasActiveTelegramTurn &&
|
|
233
|
+
state.hasAbortHandler &&
|
|
234
|
+
state.activeToolExecutions === 0
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Dispatch Planning ---
|
|
239
|
+
|
|
240
|
+
export type TelegramQueueDispatchAction =
|
|
241
|
+
| { kind: "none"; remainingItems: TelegramQueueItem[] }
|
|
242
|
+
| {
|
|
243
|
+
kind: "control";
|
|
244
|
+
item: PendingTelegramControlItem;
|
|
245
|
+
remainingItems: TelegramQueueItem[];
|
|
246
|
+
}
|
|
247
|
+
| {
|
|
248
|
+
kind: "prompt";
|
|
249
|
+
item: PendingTelegramTurn;
|
|
250
|
+
remainingItems: TelegramQueueItem[];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export function planNextTelegramQueueAction(
|
|
254
|
+
items: TelegramQueueItem[],
|
|
255
|
+
canDispatch: boolean,
|
|
256
|
+
): TelegramQueueDispatchAction {
|
|
257
|
+
if (!canDispatch || items.length === 0) {
|
|
258
|
+
return { kind: "none", remainingItems: items };
|
|
259
|
+
}
|
|
260
|
+
const [firstItem, ...remainingItems] = items;
|
|
261
|
+
if (!firstItem) {
|
|
262
|
+
return { kind: "none", remainingItems: items };
|
|
263
|
+
}
|
|
264
|
+
if (isPendingTelegramTurn(firstItem)) {
|
|
265
|
+
return { kind: "prompt", item: firstItem, remainingItems: items };
|
|
266
|
+
}
|
|
267
|
+
return { kind: "control", item: firstItem, remainingItems };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function shouldDispatchAfterTelegramAgentEnd(options: {
|
|
271
|
+
hasTurn: boolean;
|
|
272
|
+
stopReason?: string;
|
|
273
|
+
preserveQueuedTurnsAsHistory: boolean;
|
|
274
|
+
}): boolean {
|
|
275
|
+
if (!options.hasTurn) return true;
|
|
276
|
+
if (options.stopReason === "aborted") {
|
|
277
|
+
return !options.preserveQueuedTurnsAsHistory;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Agent Runtime ---
|
|
283
|
+
|
|
284
|
+
export interface TelegramAgentStartPlan {
|
|
285
|
+
activeTurn?: PendingTelegramTurn;
|
|
286
|
+
remainingItems: TelegramQueueItem[];
|
|
287
|
+
shouldResetPendingModelSwitch: boolean;
|
|
288
|
+
shouldResetToolExecutions: boolean;
|
|
289
|
+
shouldClearDispatchPending: boolean;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function buildTelegramAgentStartPlan(options: {
|
|
293
|
+
queuedItems: TelegramQueueItem[];
|
|
294
|
+
hasPendingDispatch: boolean;
|
|
295
|
+
hasActiveTurn: boolean;
|
|
296
|
+
}): TelegramAgentStartPlan {
|
|
297
|
+
if (options.hasActiveTurn || !options.hasPendingDispatch) {
|
|
298
|
+
return {
|
|
299
|
+
activeTurn: undefined,
|
|
300
|
+
remainingItems: options.queuedItems,
|
|
301
|
+
shouldResetPendingModelSwitch: true,
|
|
302
|
+
shouldResetToolExecutions: true,
|
|
303
|
+
shouldClearDispatchPending: options.hasPendingDispatch,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const nextDispatch = consumeDispatchedTelegramPrompt(
|
|
307
|
+
options.queuedItems,
|
|
308
|
+
options.hasPendingDispatch,
|
|
309
|
+
);
|
|
310
|
+
return {
|
|
311
|
+
activeTurn: nextDispatch.activeTurn,
|
|
312
|
+
remainingItems: nextDispatch.remainingItems,
|
|
313
|
+
shouldResetPendingModelSwitch: true,
|
|
314
|
+
shouldResetToolExecutions: true,
|
|
315
|
+
shouldClearDispatchPending: options.hasPendingDispatch,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function getNextTelegramToolExecutionCount(options: {
|
|
320
|
+
hasActiveTurn: boolean;
|
|
321
|
+
currentCount: number;
|
|
322
|
+
event: "start" | "end";
|
|
323
|
+
}): number {
|
|
324
|
+
if (!options.hasActiveTurn) return options.currentCount;
|
|
325
|
+
if (options.event === "start") {
|
|
326
|
+
return options.currentCount + 1;
|
|
327
|
+
}
|
|
328
|
+
return Math.max(0, options.currentCount - 1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- Agent End Lifecycle ---
|
|
332
|
+
|
|
333
|
+
export interface TelegramAgentEndPlan {
|
|
334
|
+
kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
|
|
335
|
+
shouldClearPreview: boolean;
|
|
336
|
+
shouldDispatchNext: boolean;
|
|
337
|
+
shouldSendErrorMessage: boolean;
|
|
338
|
+
shouldSendAttachmentNotice: boolean;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function buildTelegramAgentEndPlan(options: {
|
|
342
|
+
hasTurn: boolean;
|
|
343
|
+
stopReason?: string;
|
|
344
|
+
hasFinalText: boolean;
|
|
345
|
+
hasQueuedAttachments: boolean;
|
|
346
|
+
preserveQueuedTurnsAsHistory: boolean;
|
|
347
|
+
}): TelegramAgentEndPlan {
|
|
348
|
+
const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
|
|
349
|
+
hasTurn: options.hasTurn,
|
|
350
|
+
stopReason: options.stopReason,
|
|
351
|
+
preserveQueuedTurnsAsHistory: options.preserveQueuedTurnsAsHistory,
|
|
352
|
+
});
|
|
353
|
+
if (!options.hasTurn) {
|
|
354
|
+
return {
|
|
355
|
+
kind: "no-turn",
|
|
356
|
+
shouldClearPreview: false,
|
|
357
|
+
shouldDispatchNext,
|
|
358
|
+
shouldSendErrorMessage: false,
|
|
359
|
+
shouldSendAttachmentNotice: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (options.stopReason === "aborted") {
|
|
363
|
+
return {
|
|
364
|
+
kind: "aborted",
|
|
365
|
+
shouldClearPreview: true,
|
|
366
|
+
shouldDispatchNext,
|
|
367
|
+
shouldSendErrorMessage: false,
|
|
368
|
+
shouldSendAttachmentNotice: false,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (options.stopReason === "error") {
|
|
372
|
+
return {
|
|
373
|
+
kind: "error",
|
|
374
|
+
shouldClearPreview: true,
|
|
375
|
+
shouldDispatchNext,
|
|
376
|
+
shouldSendErrorMessage: true,
|
|
377
|
+
shouldSendAttachmentNotice: false,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (options.hasFinalText) {
|
|
381
|
+
return {
|
|
382
|
+
kind: "text",
|
|
383
|
+
shouldClearPreview: false,
|
|
384
|
+
shouldDispatchNext,
|
|
385
|
+
shouldSendErrorMessage: false,
|
|
386
|
+
shouldSendAttachmentNotice: false,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (options.hasQueuedAttachments) {
|
|
390
|
+
return {
|
|
391
|
+
kind: "attachments-only",
|
|
392
|
+
shouldClearPreview: true,
|
|
393
|
+
shouldDispatchNext,
|
|
394
|
+
shouldSendErrorMessage: false,
|
|
395
|
+
shouldSendAttachmentNotice: true,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
kind: "empty",
|
|
400
|
+
shouldClearPreview: true,
|
|
401
|
+
shouldDispatchNext,
|
|
402
|
+
shouldSendErrorMessage: false,
|
|
403
|
+
shouldSendAttachmentNotice: false,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- Session Runtime ---
|
|
408
|
+
|
|
409
|
+
export interface TelegramPollingStartState {
|
|
410
|
+
hasBotToken: boolean;
|
|
411
|
+
hasPollingPromise: boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function shouldStartTelegramPolling(
|
|
415
|
+
state: TelegramPollingStartState,
|
|
416
|
+
): boolean {
|
|
417
|
+
return state.hasBotToken && !state.hasPollingPromise;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function buildTelegramSessionStartState(
|
|
421
|
+
currentModel: Model<any> | undefined,
|
|
422
|
+
): {
|
|
423
|
+
currentTelegramModel: Model<any> | undefined;
|
|
424
|
+
activeTelegramToolExecutions: number;
|
|
425
|
+
pendingTelegramModelSwitch: undefined;
|
|
426
|
+
nextQueuedTelegramItemOrder: number;
|
|
427
|
+
nextQueuedTelegramControlOrder: number;
|
|
428
|
+
telegramTurnDispatchPending: boolean;
|
|
429
|
+
compactionInProgress: boolean;
|
|
430
|
+
} {
|
|
431
|
+
return {
|
|
432
|
+
currentTelegramModel: currentModel,
|
|
433
|
+
activeTelegramToolExecutions: 0,
|
|
434
|
+
pendingTelegramModelSwitch: undefined,
|
|
435
|
+
nextQueuedTelegramItemOrder: 0,
|
|
436
|
+
nextQueuedTelegramControlOrder: 0,
|
|
437
|
+
telegramTurnDispatchPending: false,
|
|
438
|
+
compactionInProgress: false,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function buildTelegramSessionShutdownState<TQueueItem>(): {
|
|
443
|
+
queuedTelegramItems: TQueueItem[];
|
|
444
|
+
nextQueuedTelegramItemOrder: number;
|
|
445
|
+
nextQueuedTelegramControlOrder: number;
|
|
446
|
+
nextPriorityReactionOrder: number;
|
|
447
|
+
currentTelegramModel: undefined;
|
|
448
|
+
activeTelegramToolExecutions: number;
|
|
449
|
+
pendingTelegramModelSwitch: undefined;
|
|
450
|
+
telegramTurnDispatchPending: boolean;
|
|
451
|
+
compactionInProgress: boolean;
|
|
452
|
+
preserveQueuedTurnsAsHistory: boolean;
|
|
453
|
+
} {
|
|
454
|
+
return {
|
|
455
|
+
queuedTelegramItems: [],
|
|
456
|
+
nextQueuedTelegramItemOrder: 0,
|
|
457
|
+
nextQueuedTelegramControlOrder: 0,
|
|
458
|
+
nextPriorityReactionOrder: 0,
|
|
459
|
+
currentTelegramModel: undefined,
|
|
460
|
+
activeTelegramToolExecutions: 0,
|
|
461
|
+
pendingTelegramModelSwitch: undefined,
|
|
462
|
+
telegramTurnDispatchPending: false,
|
|
463
|
+
compactionInProgress: false,
|
|
464
|
+
preserveQueuedTurnsAsHistory: false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// --- Control Runtime ---
|
|
469
|
+
|
|
470
|
+
export interface TelegramControlRuntimeDeps {
|
|
471
|
+
ctx: ExtensionContext;
|
|
472
|
+
sendTextReply: (
|
|
473
|
+
chatId: number,
|
|
474
|
+
replyToMessageId: number,
|
|
475
|
+
text: string,
|
|
476
|
+
) => Promise<number | undefined>;
|
|
477
|
+
onSettled: () => void;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function executeTelegramControlItemRuntime(
|
|
481
|
+
item: PendingTelegramControlItem,
|
|
482
|
+
deps: TelegramControlRuntimeDeps,
|
|
483
|
+
): Promise<void> {
|
|
484
|
+
try {
|
|
485
|
+
await item.execute(deps.ctx);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
await deps.sendTextReply(
|
|
489
|
+
item.chatId,
|
|
490
|
+
item.replyToMessageId,
|
|
491
|
+
`Telegram control action failed: ${message}`,
|
|
492
|
+
);
|
|
493
|
+
} finally {
|
|
494
|
+
deps.onSettled();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// --- Dispatch Runtime ---
|
|
499
|
+
|
|
500
|
+
export interface TelegramDispatchRuntimeDeps {
|
|
501
|
+
executeControlItem: (
|
|
502
|
+
item: Extract<TelegramQueueDispatchAction, { kind: "control" }>["item"],
|
|
503
|
+
) => void;
|
|
504
|
+
onPromptDispatchStart: (chatId: number) => void;
|
|
505
|
+
sendUserMessage: (
|
|
506
|
+
content: Extract<
|
|
507
|
+
TelegramQueueDispatchAction,
|
|
508
|
+
{ kind: "prompt" }
|
|
509
|
+
>["item"]["content"],
|
|
510
|
+
) => void;
|
|
511
|
+
onPromptDispatchFailure: (message: string) => void;
|
|
512
|
+
onIdle: () => void;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function executeTelegramQueueDispatchPlan(
|
|
516
|
+
plan: TelegramQueueDispatchAction,
|
|
517
|
+
deps: TelegramDispatchRuntimeDeps,
|
|
518
|
+
): void {
|
|
519
|
+
if (plan.kind === "none") {
|
|
520
|
+
deps.onIdle();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (plan.kind === "control") {
|
|
524
|
+
deps.executeControlItem(plan.item);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
deps.onPromptDispatchStart(plan.item.chatId);
|
|
528
|
+
try {
|
|
529
|
+
deps.sendUserMessage(plan.item.content);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
532
|
+
deps.onPromptDispatchFailure(message);
|
|
533
|
+
}
|
|
534
|
+
}
|