@llblab/pi-kit 0.5.0 → 0.5.2

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 (27) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  4. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +21 -0
  5. package/node_modules/@llblab/pi-telegram/README.md +2 -0
  6. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -4
  7. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +5 -1
  8. package/node_modules/@llblab/pi-telegram/docs/outbound.md +19 -3
  9. package/node_modules/@llblab/pi-telegram/index.ts +10 -0
  10. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
  11. package/node_modules/@llblab/pi-telegram/lib/activity.ts +60 -1
  12. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +101 -90
  13. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -2
  14. package/node_modules/@llblab/pi-telegram/lib/locks.ts +99 -16
  15. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +18 -7
  16. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +11 -0
  17. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +10 -3
  18. package/node_modules/@llblab/pi-telegram/lib/polling.ts +142 -30
  19. package/node_modules/@llblab/pi-telegram/lib/preview.ts +19 -3
  20. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +8 -6
  21. package/node_modules/@llblab/pi-telegram/lib/queue.ts +101 -66
  22. package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
  23. package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -0
  24. package/node_modules/@llblab/pi-telegram/lib/updates.ts +33 -35
  25. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  26. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +2 -0
  27. package/package.json +2 -2
@@ -353,6 +353,7 @@ export function createTelegramAssistantOutputBindingRuntime<
353
353
  typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
354
354
  >[0];
355
355
  waitForActivityIdle?: () => Promise<void>;
356
+ enqueue?: Activity.TelegramActivityPublicationRuntime["enqueue"];
356
357
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
357
358
  }): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
358
359
  const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
@@ -364,6 +365,7 @@ export function createTelegramAssistantOutputBindingRuntime<
364
365
  );
365
366
  const runtime = Activity.createTelegramAssistantOutputRuntime({
366
367
  ...authority,
368
+ enqueue: deps.enqueue,
367
369
  async send(event, authority, isAuthorityActive) {
368
370
  await deps.waitForActivityIdle?.();
369
371
  if (!isAuthorityActive()) return;
@@ -390,7 +392,14 @@ type TelegramAssistantOutputAuthority<TTransportStamp> = ReturnType<
390
392
  Routing.TelegramAssistantOutputAuthorityRuntime<TTransportStamp>["captureAuthority"]
391
393
  >;
392
394
 
395
+ export interface TelegramBridgePublicationRuntime {
396
+ enqueue: Activity.TelegramActivityPublicationRuntime["enqueue"];
397
+ reserve: Activity.TelegramActivityPublicationRuntime["reserve"];
398
+ capture: () => { target?: Queue.TelegramQueueTarget; isCurrent: () => boolean };
399
+ }
400
+
393
401
  export interface TelegramActivityBindingRuntime {
402
+ publicationRuntime: TelegramBridgePublicationRuntime;
394
403
  activityRuntime: Activity.TelegramActivityRuntime;
395
404
  activityVerbosityRuntime: ActivityVerbosity.TelegramActivityVerbosityRuntime;
396
405
  assistantOutputRuntime: Activity.TelegramAssistantOutputRuntime;
@@ -403,7 +412,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
403
412
  Parameters<
404
413
  typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>
405
414
  >[0],
406
- "waitForActivityIdle"
415
+ "waitForActivityIdle" | "enqueue"
407
416
  >;
408
417
  activityVerbosity: Omit<
409
418
  Parameters<
@@ -411,19 +420,19 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
411
420
  TelegramAssistantOutputAuthority<TTransportStamp>
412
421
  >
413
422
  >[0],
414
- "captureAuthority" | "isAuthorityActive" | "recordFailure"
423
+ "captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue"
415
424
  >;
416
425
  }): TelegramActivityBindingRuntime {
417
- const activityVerbosityBinding =
418
- ActivityVerbosity.createTelegramActivityVerbosityBinding();
426
+ const publication = Activity.createTelegramActivityPublicationRuntime();
419
427
  const assistantOutputBinding =
420
428
  createTelegramAssistantOutputBindingRuntime({
421
429
  ...deps.assistantOutput,
422
- waitForActivityIdle: activityVerbosityBinding.waitForIdle,
430
+ enqueue: publication.enqueue,
423
431
  });
424
432
  const activityVerbosityRuntime =
425
433
  ActivityVerbosity.createTelegramActivityVerbosityRuntime({
426
434
  ...deps.activityVerbosity,
435
+ enqueue: publication.enqueue,
427
436
  captureAuthority: assistantOutputBinding.authority.captureAuthority,
428
437
  isAuthorityActive: assistantOutputBinding.authority.isAuthorityActive,
429
438
  recordFailure(operation, event, error) {
@@ -434,7 +443,6 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
434
443
  });
435
444
  },
436
445
  });
437
- activityVerbosityBinding.bind(activityVerbosityRuntime);
438
446
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
439
447
  generation: deps.generation,
440
448
  observeEvent(event) {
@@ -450,9 +458,30 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
450
458
  },
451
459
  });
452
460
  return {
453
- activityRuntime,
461
+ activityRuntime: {
462
+ ...activityRuntime,
463
+ onSessionStart() {
464
+ publication.reset();
465
+ activityRuntime.onSessionStart?.();
466
+ },
467
+ onSessionShutdown() {
468
+ publication.reset();
469
+ activityRuntime.onSessionShutdown();
470
+ },
471
+ },
454
472
  activityVerbosityRuntime,
455
473
  assistantOutputRuntime: assistantOutputBinding.runtime,
474
+ publicationRuntime: {
475
+ enqueue: publication.enqueue,
476
+ reserve: publication.reserve,
477
+ capture() {
478
+ const authority = assistantOutputBinding.authority.captureAuthority();
479
+ return {
480
+ target: authority.target ? { ...authority.target } : undefined,
481
+ isCurrent: () => assistantOutputBinding.authority.isAuthorityActive(authority),
482
+ };
483
+ },
484
+ },
456
485
  };
457
486
  }
458
487
 
@@ -680,6 +709,7 @@ export function registerTelegramCommandsAndTools({
680
709
 
681
710
  interface TelegramLifecycleBindingDeps {
682
711
  pi: Pi.ExtensionAPI;
712
+ publicationRuntime: TelegramBridgePublicationRuntime;
683
713
  activityRuntime: Activity.TelegramActivityRuntime;
684
714
  activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
685
715
  assistantOutputRuntime: Pick<
@@ -770,6 +800,7 @@ interface TelegramLifecycleBindingDeps {
770
800
 
771
801
  export function registerTelegramLifecycleRuntimeHooks({
772
802
  pi,
803
+ publicationRuntime,
773
804
  activityRuntime,
774
805
  activityVerbosityRuntime,
775
806
  assistantOutputRuntime,
@@ -936,24 +967,19 @@ export function registerTelegramLifecycleRuntimeHooks({
936
967
  { replyToPrompt: false },
937
968
  );
938
969
  };
939
- let activeTurnDeliveryTail = Promise.resolve();
940
- const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
941
- const previous = activeTurnDeliveryTail;
942
- activeTurnDeliveryTail = (async () => {
943
- await previous;
944
- await new Promise<void>((resolve) => {
945
- const timer = setTimeout(resolve, 0);
946
- timer.unref?.();
947
- });
948
- await task();
949
- })().catch((error) => {
950
- recordRuntimeEvent("delivery", error, {
951
- phase: "agent-end-background-delivery",
952
- });
953
- });
970
+ let pendingFinalPublication: {
971
+ turn: Queue.PendingTelegramTurn;
972
+ reservation: Activity.TelegramActivityPublicationReservation;
973
+ } | undefined;
974
+ const cancelPendingFinalPublication = (): void => {
975
+ pendingFinalPublication?.reservation.cancel();
976
+ pendingFinalPublication = undefined;
954
977
  };
955
- const waitForActiveTurnDelivery = async (): Promise<void> => {
956
- await activeTurnDeliveryTail;
978
+ const recordPublicationFailure = (error: unknown): void => {
979
+ recordRuntimeEvent("delivery", error, { phase: "agent-end-background-delivery" });
980
+ };
981
+ const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
982
+ void publicationRuntime.enqueue(task).catch(recordPublicationFailure);
957
983
  };
958
984
  const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
959
985
  Queue.PendingTelegramTurn,
@@ -989,14 +1015,22 @@ export function registerTelegramLifecycleRuntimeHooks({
989
1015
  isSessionActive: isSessionContextActive,
990
1016
  isTurnTransportActive,
991
1017
  waitForTypingIdle: typing.waitForIdle,
992
- async waitForActivityIdle() {
993
- await activityVerbosityRuntime?.waitForIdle();
994
- await assistantOutputRuntime.waitForIdle();
995
- },
996
1018
  dispatchNextQueuedTelegramTurn,
997
1019
  requestDeferredDispatchNextQueuedTelegramTurn:
998
1020
  deferredQueueDispatchRuntime.request,
999
1021
  scheduleActiveTurnDelivery,
1022
+ reserveActiveTurnDelivery() {
1023
+ const pending = pendingFinalPublication;
1024
+ pendingFinalPublication = undefined;
1025
+ const matches = pending?.turn === activeTurnRuntime.get();
1026
+ if (!matches) pending?.reservation.cancel();
1027
+ const reservation = matches && pending ? pending.reservation : publicationRuntime.reserve();
1028
+ return {
1029
+ schedule: (task) => { void reservation.publish(task).catch(recordPublicationFailure); },
1030
+ cancel: reservation.cancel,
1031
+ };
1032
+ },
1033
+ preparePreviewClear: previewRuntime.prepareClear,
1000
1034
  clearPreview: previewRuntime.clear,
1001
1035
  setPreviewPendingText: previewRuntime.setPendingText,
1002
1036
  finalizeMarkdownPreview,
@@ -1038,25 +1072,24 @@ export function registerTelegramLifecycleRuntimeHooks({
1038
1072
  };
1039
1073
  let observedAutomaticCompaction = false;
1040
1074
  let agentWorkActive = false;
1041
- let terminalAssistantMessagePendingDelivery = false;
1042
- const deferredAutomaticCompactionNotices: string[] = [];
1043
- const sendCompactionNotice = async (text: string): Promise<void> => {
1075
+ const prepareCompactionNotice = (text: string, ctx: Pi.ExtensionContext): (() => Promise<void>) => {
1076
+ const authority = publicationRuntime.capture();
1044
1077
  const turn = activeTurnRuntime.get();
1045
- const target = turn?.target ?? proactivePushTargetGetter?.();
1046
- if (!target) return;
1047
- try {
1048
- await sendMarkdownReply(target.chatId, turn?.replyToMessageId, text, {
1049
- target,
1050
- });
1051
- } catch (error) {
1052
- recordRuntimeEvent("delivery", error, {
1053
- phase: "compaction-notice",
1054
- });
1055
- }
1078
+ const selectedTarget = turn?.target ?? authority.target;
1079
+ const target = selectedTarget ? { ...selectedTarget } : undefined;
1080
+ const replyToMessageId = turn?.replyToMessageId;
1081
+ return async () => {
1082
+ if (!target || !isSessionContextActive(ctx) || !authority.isCurrent()) return;
1083
+ if (turn && isTurnTransportActive?.(turn) === false) return;
1084
+ try {
1085
+ await sendMarkdownReply(target.chatId, replyToMessageId, text, { target });
1086
+ } catch (error) {
1087
+ recordRuntimeEvent("delivery", error, { phase: "compaction-notice" });
1088
+ }
1089
+ };
1056
1090
  };
1057
- const flushDeferredAutomaticCompactionNotices = async (): Promise<void> => {
1058
- const notices = deferredAutomaticCompactionNotices.splice(0);
1059
- for (const notice of notices) await sendCompactionNotice(notice);
1091
+ const sendCompactionNotice = (text: string, ctx: Pi.ExtensionContext): void => {
1092
+ scheduleActiveTurnDelivery(prepareCompactionNotice(text, ctx));
1060
1093
  };
1061
1094
  const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
1062
1095
  isContextActive: isSessionContextActive,
@@ -1070,7 +1103,6 @@ export function registerTelegramLifecycleRuntimeHooks({
1070
1103
  recordRuntimeEvent,
1071
1104
  onCompactionAbandoned: () => {
1072
1105
  observedAutomaticCompaction = false;
1073
- deferredAutomaticCompactionNotices.length = 0;
1074
1106
  activityRuntime.onCompactionAbandoned();
1075
1107
  },
1076
1108
  });
@@ -1091,6 +1123,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1091
1123
  activityRuntime.recordInputSource(event.source ?? "unknown");
1092
1124
  },
1093
1125
  async onSessionStart(event, ctx) {
1126
+ cancelPendingFinalPublication();
1094
1127
  previewRuntime.invalidate();
1095
1128
  assistantOutputRuntime.start();
1096
1129
  activityRuntime.onSessionStart?.();
@@ -1106,9 +1139,8 @@ export function registerTelegramLifecycleRuntimeHooks({
1106
1139
  assistantOutputRuntime.stop();
1107
1140
  observedAutomaticCompaction = false;
1108
1141
  agentWorkActive = false;
1109
- terminalAssistantMessagePendingDelivery = false;
1142
+ cancelPendingFinalPublication();
1110
1143
  uiPromptActive = false;
1111
- deferredAutomaticCompactionNotices.length = 0;
1112
1144
  compactionObserver.onSessionShutdown();
1113
1145
  if (event.reason === "quit" && disconnectOnQuit) {
1114
1146
  try {
@@ -1129,19 +1161,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1129
1161
  if (shouldNotify) observedAutomaticCompaction = true;
1130
1162
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
1131
1163
  compactionObserver.onSessionBeforeCompact(event, ctx);
1132
- if (shouldNotify) {
1133
- if (terminalAssistantMessagePendingDelivery) {
1134
- deferredAutomaticCompactionNotices.push(
1135
- Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1136
- );
1137
- } else {
1138
- await waitForActiveTurnDelivery();
1139
- if (!isSessionContextActive(ctx)) return;
1140
- await sendCompactionNotice(
1141
- Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
1142
- );
1143
- }
1144
- }
1164
+ if (shouldNotify) sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN, ctx);
1145
1165
  },
1146
1166
  async onSessionCompact(event, ctx) {
1147
1167
  if (!isSessionContextActive(ctx)) return;
@@ -1149,38 +1169,23 @@ export function registerTelegramLifecycleRuntimeHooks({
1149
1169
  compactionObserver.onSessionCompact(event, ctx);
1150
1170
  if (observedAutomaticCompaction) {
1151
1171
  observedAutomaticCompaction = false;
1152
- if (deferredAutomaticCompactionNotices.length > 0) {
1153
- deferredAutomaticCompactionNotices.push(
1154
- Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1155
- );
1156
- } else {
1157
- await sendCompactionNotice(
1158
- Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
1159
- );
1160
- }
1172
+ sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN, ctx);
1161
1173
  }
1162
1174
  },
1163
1175
  async onSessionCompactFailed(event, ctx) {
1164
1176
  if (!isSessionContextActive(ctx)) return;
1165
1177
  const shouldNotify = observedAutomaticCompaction;
1166
- const deferredNotices = deferredAutomaticCompactionNotices.splice(0);
1167
- const shouldDefer =
1168
- deferredNotices.length > 0 || terminalAssistantMessagePendingDelivery;
1169
1178
  compactionObserver.onSessionCompactFailed(event, ctx);
1170
1179
  if (!shouldNotify) return;
1171
1180
  const notice = event.aborted
1172
1181
  ? "**⚠️ Compaction cancelled.**"
1173
1182
  : "**⚠️ Compaction failed.**";
1174
- if (shouldDefer) {
1175
- deferredAutomaticCompactionNotices.push(...deferredNotices, notice);
1176
- } else {
1177
- await sendCompactionNotice(notice);
1178
- }
1183
+ sendCompactionNotice(notice, ctx);
1179
1184
  },
1180
1185
  async onAgentStart(event, ctx) {
1181
1186
  if (!isSessionContextActive(ctx)) return;
1182
1187
  agentWorkActive = true;
1183
- terminalAssistantMessagePendingDelivery = false;
1188
+ cancelPendingFinalPublication();
1184
1189
  await agentStartWithDedupReset(event, ctx);
1185
1190
  activityRuntime.onAgentStart(activeTurnRuntime.get()?.target);
1186
1191
  startAgentActivityTypingLoop(ctx);
@@ -1230,11 +1235,13 @@ export function registerTelegramLifecycleRuntimeHooks({
1230
1235
  if (event.message.role === "assistant") {
1231
1236
  activityRuntime.onAssistantMessageEnd(event.message.stopReason);
1232
1237
  }
1233
- terminalAssistantMessagePendingDelivery =
1234
- event.message.role === "assistant" &&
1235
- event.message.stopReason !== "toolUse" &&
1236
- event.message.stopReason !== "error" &&
1237
- event.message.stopReason !== "aborted";
1238
+ if (event.message.role !== "assistant" || event.message.stopReason === "toolUse" || event.message.stopReason === "aborted") return;
1239
+ const turn = activeTurnRuntime.get();
1240
+ if (!turn || turn.guestQueryId || pendingFinalPublication?.turn === turn) return;
1241
+ cancelPendingFinalPublication();
1242
+ const assistant = Replies.extractLatestAssistantMessageText([event.message]);
1243
+ if (!assistant.text && assistant.stopReason !== "error" && turn.queuedAttachments.length === 0) return;
1244
+ pendingFinalPublication = { turn, reservation: publicationRuntime.reserve() };
1238
1245
  },
1239
1246
  onUiPromptStart(event, ctx) {
1240
1247
  if (!isSessionContextActive(ctx)) return;
@@ -1254,18 +1261,22 @@ export function registerTelegramLifecycleRuntimeHooks({
1254
1261
  },
1255
1262
  async onAgentEnd(event, ctx) {
1256
1263
  if (!isSessionContextActive(ctx)) return;
1264
+ if (pendingFinalPublication && pendingFinalPublication.turn !== activeTurnRuntime.get()) {
1265
+ cancelPendingFinalPublication();
1266
+ return;
1267
+ }
1257
1268
  activityRuntime.onAgentEnd();
1258
1269
  await agentLifecycleHooks.onAgentEnd(event, ctx);
1259
1270
  },
1260
1271
  async onAgentSettled(event, ctx) {
1261
1272
  if (!isSessionContextActive(ctx)) return;
1262
- await agentLifecycleHooks.onAgentSettled(event, ctx);
1263
- if (deferredAutomaticCompactionNotices.length > 0) {
1264
- await waitForActiveTurnDelivery();
1265
- if (!isSessionContextActive(ctx)) return;
1266
- await flushDeferredAutomaticCompactionNotices();
1273
+ const pending = pendingFinalPublication;
1274
+ try {
1275
+ await agentLifecycleHooks.onAgentSettled(event, ctx);
1276
+ } finally {
1277
+ if (pendingFinalPublication === pending) cancelPendingFinalPublication();
1267
1278
  }
1268
- terminalAssistantMessagePendingDelivery = false;
1279
+ if (!isSessionContextActive(ctx)) return;
1269
1280
  agentWorkActive = false;
1270
1281
  activityRuntime.onAgentSettled();
1271
1282
  modelContextAvailabilityRuntime.reconcile();
@@ -450,8 +450,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
450
450
  const setTimer = deps.setTimer ?? setTimeout;
451
451
  const clearTimer = deps.clearTimer ?? clearTimeout;
452
452
  let fallbackTimer: TelegramLifecycleTimer | undefined;
453
+ let observationGeneration = 0;
453
454
  let typingStartedByObserver = false;
454
455
  const clearFallbackTimer = (): void => {
456
+ observationGeneration += 1;
455
457
  if (!fallbackTimer) return;
456
458
  clearTimer(fallbackTimer);
457
459
  fallbackTimer = undefined;
@@ -470,7 +472,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
470
472
  !!deps.startTypingLoop && typingStartResult !== false;
471
473
  deps.updateStatus(ctx);
472
474
  clearFallbackTimer();
475
+ const admittedGeneration = observationGeneration;
473
476
  fallbackTimer = setTimer(() => {
477
+ if (observationGeneration !== admittedGeneration) return;
478
+ observationGeneration += 1;
474
479
  fallbackTimer = undefined;
475
480
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
476
481
  deps.setCompactionInProgress(false);
@@ -487,8 +492,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
487
492
  unrefTelegramLifecycleTimer(fallbackTimer);
488
493
  },
489
494
  onSessionCompact: (_event, ctx) => {
490
- clearFallbackTimer();
491
495
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
496
+ clearFallbackTimer();
492
497
  deps.setCompactionInProgress(false);
493
498
  if (typingStartedByObserver) deps.stopTypingLoop?.();
494
499
  typingStartedByObserver = false;
@@ -496,8 +501,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
496
501
  requestDispatch();
497
502
  },
498
503
  onSessionCompactFailed: (_event, ctx) => {
499
- clearFallbackTimer();
500
504
  if (deps.isContextActive && !deps.isContextActive(ctx)) return;
505
+ clearFallbackTimer();
501
506
  deps.setCompactionInProgress(false);
502
507
  if (typingStartedByObserver) deps.stopTypingLoop?.();
503
508
  typingStartedByObserver = false;