@llblab/pi-kit 0.5.1 → 0.6.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  6. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  7. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  8. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  9. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
  10. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  11. package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
  12. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +4 -0
  13. package/node_modules/@llblab/pi-telegram/docs/outbound.md +27 -5
  14. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -0
  15. package/node_modules/@llblab/pi-telegram/index.ts +24 -19
  16. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
  17. package/node_modules/@llblab/pi-telegram/lib/activity.ts +79 -6
  18. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +110 -91
  19. package/node_modules/@llblab/pi-telegram/lib/config.ts +1 -1
  20. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  21. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +14 -3
  22. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +2 -2
  23. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +41 -37
  24. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +39 -42
  25. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +28 -17
  26. package/node_modules/@llblab/pi-telegram/lib/preview.ts +134 -73
  27. package/node_modules/@llblab/pi-telegram/lib/queue.ts +113 -72
  28. package/node_modules/@llblab/pi-telegram/lib/replies.ts +46 -38
  29. package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
  30. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +36 -3
  31. package/node_modules/@llblab/pi-telegram/lib/updates.ts +27 -35
  32. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  33. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  34. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  35. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  36. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  37. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  38. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  39. package/node_modules/@llblab/skills/package.json +1 -1
  40. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  41. package/package.json +4 -4
@@ -8,12 +8,13 @@ import { unlink } from "node:fs/promises";
8
8
  import { basename, extname } from "node:path";
9
9
 
10
10
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
11
- import { buildTelegramMultipartReplyParameters } from "./replies.ts";
11
+ import { withTelegramReplyParameters } from "./replies.ts";
12
12
  import {
13
13
  getTelegramTargetThreadParams,
14
14
  type TelegramTarget,
15
15
  } from "./target.ts";
16
16
  import { getTelegramVoiceSynthesisProviders } from "./voice.ts";
17
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
17
18
 
18
19
  export interface TelegramVoiceReplyTurnView {
19
20
  chatId: number;
@@ -47,6 +48,7 @@ export interface TelegramVoiceReplySenderDeps {
47
48
  ) => Promise<unknown>;
48
49
  sendChatAction?: (chatId: number, action: string) => Promise<unknown>;
49
50
  sendRecordVoiceAction?: (chatId: number) => Promise<unknown>;
51
+ isDeliveryActive?: () => boolean;
50
52
  getHandlers?: () => unknown[] | undefined;
51
53
  cwd?: string;
52
54
  tempDir?: string;
@@ -78,17 +80,6 @@ export interface TelegramVoiceReplySenderPorts<THandler = unknown> {
78
80
  getProgrammaticVoiceHandlers?: () => TelegramOutboundProgrammaticVoiceHandler[];
79
81
  }
80
82
 
81
- function buildVoiceReplyParameters(
82
- chatId: number,
83
- replyToPrompt: boolean | undefined,
84
- replyToMessageId: number | undefined,
85
- target?: TelegramTarget,
86
- ): string | undefined {
87
- if (replyToPrompt === false || replyToMessageId === undefined)
88
- return undefined;
89
- return buildTelegramMultipartReplyParameters(chatId, replyToMessageId, target);
90
- }
91
-
92
83
  async function ensureTelegramVoiceFileFormat(
93
84
  filePath: string,
94
85
  ): Promise<string> {
@@ -123,39 +114,39 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
123
114
  replyMarkup?: unknown;
124
115
  },
125
116
  ): Promise<void> => {
117
+ if (deps.isDeliveryActive?.() === false) return;
126
118
  const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
127
119
  assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
120
+ if (deps.isDeliveryActive?.() === false) return;
128
121
  await sendVoiceChatAction(deps, turn.chatId);
129
- const replyParameters = buildVoiceReplyParameters(
130
- turn.chatId,
131
- options?.replyToPrompt,
132
- turn.replyToMessageId,
133
- turn.target,
134
- );
135
- await deps.sendMultipart(
136
- "sendVoice",
137
- {
138
- chat_id: String(turn.chatId),
139
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
140
- ...(turn.target
141
- ? Object.fromEntries(
142
- Object.entries(getTelegramTargetThreadParams(turn.target)).map(
143
- ([key, value]) => [key, String(value)],
144
- ),
145
- )
146
- : {}),
147
- ...(options?.replyMarkup !== undefined && options.replyMarkup !== null
148
- ? {
149
- reply_markup:
150
- typeof options.replyMarkup === "string"
151
- ? options.replyMarkup
152
- : JSON.stringify(options.replyMarkup),
153
- }
154
- : {}),
155
- },
156
- "voice",
157
- voiceFilePath,
158
- basename(voiceFilePath),
122
+ if (deps.isDeliveryActive?.() === false) return;
123
+ await withTelegramReplyParameters(
124
+ turn.chatId, options?.replyToPrompt === false ? undefined : turn.replyToMessageId, turn.target,
125
+ (replyParameters) => deps.sendMultipart(
126
+ "sendVoice",
127
+ {
128
+ chat_id: String(turn.chatId),
129
+ ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
130
+ ...(turn.target
131
+ ? Object.fromEntries(
132
+ Object.entries(getTelegramTargetThreadParams(turn.target)).map(
133
+ ([key, value]) => [key, String(value)],
134
+ ),
135
+ )
136
+ : {}),
137
+ ...(options?.replyMarkup !== undefined && options.replyMarkup !== null
138
+ ? {
139
+ reply_markup:
140
+ typeof options.replyMarkup === "string"
141
+ ? options.replyMarkup
142
+ : JSON.stringify(options.replyMarkup),
143
+ }
144
+ : {}),
145
+ },
146
+ "voice",
147
+ voiceFilePath,
148
+ basename(voiceFilePath),
149
+ ),
159
150
  );
160
151
  };
161
152
 
@@ -171,6 +162,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
171
162
  ): Promise<void> => {
172
163
  for (const handler of ports.findVoiceHandlers?.(deps.getHandlers?.()) ??
173
164
  []) {
165
+ if (deps.isDeliveryActive?.() === false) return;
174
166
  try {
175
167
  const filePath = await ports.generateVoiceFile?.(text, {
176
168
  lang: options?.lang,
@@ -187,6 +179,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
187
179
  });
188
180
  return;
189
181
  } catch (error) {
182
+ if (isTelegramApiCommitUnknownError(error)) throw error;
190
183
  deps.recordRuntimeEvent?.("voice", error, {
191
184
  phase: "template-handler-send",
192
185
  });
@@ -194,6 +187,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
194
187
  }
195
188
 
196
189
  for (const handler of ports.getProgrammaticVoiceHandlers?.() ?? []) {
190
+ if (deps.isDeliveryActive?.() === false) return;
197
191
  try {
198
192
  const filePath = await handler(text, {
199
193
  lang: options?.lang,
@@ -206,6 +200,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
206
200
  });
207
201
  return;
208
202
  } catch (error) {
203
+ if (isTelegramApiCommitUnknownError(error)) throw error;
209
204
  deps.recordRuntimeEvent?.("voice", error, {
210
205
  phase: "programmatic-handler-send",
211
206
  });
@@ -215,6 +210,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
215
210
  const providers = getTelegramVoiceSynthesisProviders();
216
211
 
217
212
  for (const provider of providers) {
213
+ if (deps.isDeliveryActive?.() === false) return;
218
214
  let voiceFilePath: string | undefined;
219
215
  let originalFilePath: string | undefined;
220
216
 
@@ -252,6 +248,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
252
248
  });
253
249
  return;
254
250
  } catch (error) {
251
+ if (isTelegramApiCommitUnknownError(error)) throw error;
255
252
  deps.recordRuntimeEvent?.("voice", error, { phase: "send" });
256
253
  } finally {
257
254
  if (voiceFilePath && voiceFilePath !== originalFilePath) {
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import type { TelegramAssistantSegmentEvent } from "./activity.ts";
12
12
  import { resolveTelegramTempDir } from "./paths.ts";
13
13
  import * as Replies from "./replies.ts";
14
+ import type { TelegramPreparedPreviewDelivery } from "./preview.ts";
15
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
14
16
  import type {
15
17
  TelegramEditMessageTextBody,
16
18
  TelegramSendMessageBody,
@@ -137,6 +139,7 @@ export interface TelegramVoiceReplySenderDeps {
137
139
  ) => Promise<unknown>;
138
140
  sendChatAction?: (chatId: number, action: string) => Promise<unknown>;
139
141
  sendRecordVoiceAction?: (chatId: number) => Promise<unknown>;
142
+ isDeliveryActive?: () => boolean;
140
143
  getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
141
144
  cwd?: string;
142
145
  tempDir?: string;
@@ -247,12 +250,8 @@ export interface TelegramOutboundTextPreviewRuntimeDeps<
247
250
  > {
248
251
  execCommand: TelegramVoiceReplySenderDeps["execCommand"];
249
252
  getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
250
- finalizeMarkdownPreview: (
251
- chatId: number,
252
- markdown: string,
253
- replyToMessageId: number,
254
- options?: { replyMarkup?: TReplyMarkup },
255
- ) => Promise<boolean>;
253
+ finalizeMarkdownPreview: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"];
254
+ preparePreviewDelivery?: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
256
255
  cwd?: string;
257
256
  recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
258
257
  }
@@ -727,12 +726,13 @@ export function createTelegramOutboundTextPreviewRuntime<
727
726
  TReplyMarkup = unknown,
728
727
  >(
729
728
  deps: TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
730
- ): Pick<
731
- TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
732
- "finalizeMarkdownPreview"
733
- > {
734
- return {
735
- finalizeMarkdownPreview: async (
729
+ ): {
730
+ finalizeMarkdownPreview: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"];
731
+ preparePreviewDelivery: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup> | undefined;
732
+ } {
733
+ const wrap = (
734
+ finalize: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"],
735
+ ): TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"] => async (
736
736
  chatId,
737
737
  markdown,
738
738
  replyToMessageId,
@@ -745,7 +745,7 @@ export function createTelegramOutboundTextPreviewRuntime<
745
745
  recordRuntimeEvent: deps.recordRuntimeEvent,
746
746
  replyMarkup: options?.replyMarkup,
747
747
  });
748
- return deps.finalizeMarkdownPreview(
748
+ return finalize(
749
749
  chatId,
750
750
  transformed.text,
751
751
  replyToMessageId,
@@ -756,6 +756,12 @@ export function createTelegramOutboundTextPreviewRuntime<
756
756
  : {}),
757
757
  },
758
758
  );
759
+ };
760
+ return {
761
+ finalizeMarkdownPreview: wrap(deps.finalizeMarkdownPreview),
762
+ preparePreviewDelivery(isDeliveryActive) {
763
+ const prepared = deps.preparePreviewDelivery?.(isDeliveryActive);
764
+ return prepared ? { ...prepared, finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview) } : undefined;
759
765
  },
760
766
  };
761
767
  }
@@ -877,15 +883,17 @@ export function createTelegramOutboundReplyPlanner(
877
883
  export function createTelegramOutboundReplyArtifactSender(
878
884
  deps: TelegramVoiceReplySenderDeps,
879
885
  ) {
880
- const sendVoiceReply = createTelegramVoiceReplySender(deps);
881
886
  return async (
882
887
  turn: TelegramVoiceReplyTurnView,
883
888
  plan: Pick<
884
889
  TelegramOutboundReplyPlan,
885
890
  "voiceText" | "voiceReplies" | "lang" | "rate" | "replyMarkup"
886
891
  >,
887
- options?: { replyToPrompt?: boolean },
892
+ options?: { replyToPrompt?: boolean; isDeliveryActive?: () => boolean },
888
893
  ): Promise<void> => {
894
+ const isDeliveryActive = () =>
895
+ deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false;
896
+ const sendVoiceReply = createTelegramVoiceReplySender({ ...deps, isDeliveryActive });
889
897
  // Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
890
898
  const voiceReplies = plan.voiceReplies?.length
891
899
  ? plan.voiceReplies
@@ -896,6 +904,7 @@ export function createTelegramOutboundReplyArtifactSender(
896
904
  let anyDelivered = false;
897
905
 
898
906
  for (const reply of voiceReplies) {
907
+ if (!isDeliveryActive()) return;
899
908
  try {
900
909
  await sendVoiceReply(turn, reply.text, {
901
910
  lang: reply.lang ?? plan.lang,
@@ -905,11 +914,13 @@ export function createTelegramOutboundReplyArtifactSender(
905
914
  replyMarkup: !anyDelivered ? plan.replyMarkup : undefined,
906
915
  });
907
916
  anyDelivered = true;
908
- } catch {
917
+ } catch (error) {
918
+ if (isTelegramApiCommitUnknownError(error)) throw error;
909
919
  // sendVoiceReply already recorded the error; continue to next reply
910
920
  }
911
921
  }
912
922
 
923
+ if (!isDeliveryActive()) return;
913
924
  if (!anyDelivered) {
914
925
  throw new Error(
915
926
  "Failed to send voice reply: every voice synthesis provider failed.",
@@ -1011,7 +1022,7 @@ export function createTelegramAssistantOutputSender<
1011
1022
  };
1012
1023
  await outboundRuntime.sendMarkdownReply(
1013
1024
  target.chatId,
1014
- undefined,
1025
+ event.source === "telegram" ? event.replyToMessageId : undefined,
1015
1026
  buttonReply.markdown,
1016
1027
  {
1017
1028
  target,