@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
@@ -4,7 +4,7 @@
4
4
  * Owns persistent bounded thinking and tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
5
5
  */
6
6
 
7
- import type { TelegramActivityEvent } from "./activity.ts";
7
+ import type { TelegramActivityEvent, TelegramActivityPublicationRuntime } from "./activity.ts";
8
8
  import {
9
9
  escapeHtml,
10
10
  renderTelegramInlineMarkdownHtml,
@@ -321,6 +321,7 @@ export function createTelegramActivityVerbosityBinding(): TelegramActivityVerbos
321
321
  }
322
322
 
323
323
  export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
324
+ enqueue?: TelegramActivityPublicationRuntime["enqueue"];
324
325
  getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
325
326
  refreshActivityMode?: () => Promise<void>;
326
327
  getNowMs?: () => number;
@@ -380,15 +381,20 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
380
381
  };
381
382
  const hasAuthority = (): boolean =>
382
383
  authority !== undefined && deps.isAuthorityActive(authority);
383
- const ensureActivity = (event: TelegramActivityEvent): boolean => {
384
+ const isCurrent = (acceptedGeneration: number, admittedAuthority: TAuthority | undefined): boolean =>
385
+ active && generation === acceptedGeneration && admittedAuthority !== undefined && deps.isAuthorityActive(admittedAuthority);
386
+ const ensureActivity = (
387
+ event: TelegramActivityEvent,
388
+ admittedTarget: TelegramTarget | undefined,
389
+ admittedAuthority: TAuthority,
390
+ ): boolean => {
384
391
  if (deps.getActivityMode() === "quiet") return false;
385
392
  if (activityId === event.activityId) return hasAuthority();
386
393
  clearActivity();
387
- const resolvedTarget = deps.resolveTarget(event);
388
- if (!resolvedTarget) return false;
394
+ if (!admittedTarget) return false;
389
395
  activityId = event.activityId;
390
- target = { ...resolvedTarget };
391
- authority = deps.captureAuthority();
396
+ target = admittedTarget;
397
+ authority = admittedAuthority;
392
398
  return hasAuthority();
393
399
  };
394
400
  const closeToolBatch = () => {
@@ -398,10 +404,10 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
398
404
  event: TelegramActivityEvent,
399
405
  acceptedGeneration: number,
400
406
  ) => {
407
+ const admittedAuthority = authority;
401
408
  if (
402
- generation !== acceptedGeneration ||
409
+ !isCurrent(acceptedGeneration, admittedAuthority) ||
403
410
  !target ||
404
- !hasAuthority() ||
405
411
  reasoningBlocked
406
412
  ) {
407
413
  return;
@@ -440,22 +446,24 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
440
446
  parse_mode: "HTML",
441
447
  link_preview_options: { is_disabled: true },
442
448
  });
449
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
443
450
  reasoningMessage = {
444
451
  messageId: sent.message_id,
445
452
  target: { ...target },
446
453
  };
447
454
  }
448
- if (generation !== acceptedGeneration) return;
455
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
449
456
  reasoningMessageFrames += 1;
450
457
  lastReasoningMessageChars = reasoningChars;
451
458
  lastReasoningPublishMs = getNowMs();
452
459
  } catch (error) {
460
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
461
+ reasoningBlocked = true;
453
462
  deps.recordFailure?.(
454
463
  canEdit ? "reasoning-edit" : "reasoning-send",
455
464
  event,
456
465
  error,
457
466
  );
458
- reasoningBlocked = true;
459
467
  }
460
468
  };
461
469
  const publishTool = async (
@@ -463,11 +471,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
463
471
  tool: ToolActivity,
464
472
  acceptedGeneration: number,
465
473
  ) => {
466
- if (
467
- generation !== acceptedGeneration ||
468
- !target ||
469
- !hasAuthority()
470
- ) {
474
+ const admittedAuthority = authority;
475
+ if (!isCurrent(acceptedGeneration, admittedAuthority) || !target) {
471
476
  return;
472
477
  }
473
478
  const canAppend =
@@ -495,6 +500,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
495
500
  }),
496
501
  });
497
502
  } catch (error) {
503
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
498
504
  if (
499
505
  toolMessage.format !== "rich" ||
500
506
  !isKnownSafeRichActivityRejection(error)
@@ -508,9 +514,10 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
508
514
  parse_mode: "HTML",
509
515
  link_preview_options: { is_disabled: true },
510
516
  });
517
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
511
518
  toolMessage.format = "html";
512
519
  }
513
- if (generation !== acceptedGeneration) return;
520
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
514
521
  toolMessage.tools = nextTools;
515
522
  return;
516
523
  }
@@ -528,6 +535,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
528
535
  rich_message: renderTelegramToolActivityRichMessage([tool]),
529
536
  });
530
537
  } catch (error) {
538
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
531
539
  if (!isKnownSafeRichActivityRejection(error)) throw error;
532
540
  sent = await deps.sendMessage({
533
541
  ...body,
@@ -537,7 +545,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
537
545
  });
538
546
  format = "html";
539
547
  }
540
- if (generation !== acceptedGeneration) return;
548
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
541
549
  toolMessage = {
542
550
  messageId: sent.message_id,
543
551
  tools: [tool],
@@ -545,25 +553,30 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
545
553
  format,
546
554
  };
547
555
  } catch (error) {
548
- deps.recordFailure?.(canAppend ? "tool-edit" : "tool-send", event, error);
556
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
549
557
  closeToolBatch();
558
+ deps.recordFailure?.(canAppend ? "tool-edit" : "tool-send", event, error);
550
559
  }
551
560
  };
552
561
  const process = async (
553
562
  event: TelegramActivityEvent,
554
563
  acceptedGeneration: number,
564
+ admittedTarget: TelegramTarget | undefined,
565
+ admittedAuthority: TAuthority,
555
566
  ) => {
556
567
  if (event.type === "agent-start" && deps.refreshActivityMode) {
557
568
  try {
558
569
  await deps.refreshActivityMode();
559
570
  } catch (error) {
560
- deps.recordFailure?.("config-refresh", event, error);
571
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
561
572
  clearActivity();
562
573
  activityId = event.activityId;
574
+ deps.recordFailure?.("config-refresh", event, error);
563
575
  return;
564
576
  }
565
577
  }
566
- if (!ensureActivity(event)) {
578
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
579
+ if (!ensureActivity(event, admittedTarget, admittedAuthority)) {
567
580
  if (
568
581
  activityId === event.activityId &&
569
582
  deps.getActivityMode() === "quiet"
@@ -614,6 +627,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
614
627
  ) {
615
628
  await publishReasoning(event, acceptedGeneration);
616
629
  }
630
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
617
631
  reasoningBuffer = "";
618
632
  reasoningChars = 0;
619
633
  reasoningMessageFrames = 0;
@@ -668,7 +682,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
668
682
  toolOrder.shift();
669
683
  tools.delete(next.id);
670
684
  await publishTool(event, next, acceptedGeneration);
671
- if (generation !== acceptedGeneration) return;
685
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
672
686
  }
673
687
  return;
674
688
  }
@@ -680,6 +694,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
680
694
  ) {
681
695
  await publishReasoning(event, acceptedGeneration);
682
696
  }
697
+ if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
683
698
  clearActivity();
684
699
  }
685
700
  };
@@ -687,10 +702,13 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
687
702
  accept(event) {
688
703
  if (!active) return;
689
704
  const acceptedGeneration = generation;
690
- tail = tail
691
- .then(() => {
692
- if (!active || generation !== acceptedGeneration) return;
693
- return process(event, acceptedGeneration);
705
+ const resolvedTarget = deps.resolveTarget(event);
706
+ const admittedTarget = resolvedTarget ? { ...resolvedTarget } : undefined;
707
+ const admittedAuthority = deps.captureAuthority();
708
+ const enqueue = deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
709
+ tail = enqueue(async () => {
710
+ if (!active || generation !== acceptedGeneration || !deps.isAuthorityActive(admittedAuthority)) return;
711
+ await process(event, acceptedGeneration, admittedTarget, admittedAuthority);
694
712
  })
695
713
  .catch((error) => {
696
714
  deps.recordFailure?.("tool-send", event, error);
@@ -32,6 +32,7 @@ export interface TelegramActivityEnvelope {
32
32
  sequence: number;
33
33
  source: TelegramActivitySource;
34
34
  target?: TelegramActivityTarget;
35
+ replyToMessageId?: number;
35
36
  timestamp: number;
36
37
  }
37
38
 
@@ -422,8 +423,8 @@ export function createTelegramActivityBridgeRuntime(deps: {
422
423
  recordInputSource(source) {
423
424
  getRuntime()?.recordInputSource(source);
424
425
  },
425
- onAgentStart(target) {
426
- getRuntime()?.onAgentStart(target);
426
+ onAgentStart(target, replyToMessageId) {
427
+ getRuntime()?.onAgentStart(target, replyToMessageId);
427
428
  },
428
429
  onAssistantEvent(event) {
429
430
  getRuntime()?.onAssistantEvent(event);
@@ -492,7 +493,7 @@ export type TelegramAssistantStreamEvent =
492
493
  export interface TelegramActivityRuntime {
493
494
  onSessionStart?: () => void;
494
495
  recordInputSource: (source: TelegramActivityInputSource) => void;
495
- onAgentStart: (activeTelegramTarget?: TelegramActivityTarget) => void;
496
+ onAgentStart: (activeTelegramTarget?: TelegramActivityTarget, replyToMessageId?: number) => void;
496
497
  onAssistantEvent: (event: TelegramAssistantStreamEvent) => void;
497
498
  onAssistantMessageEnd: (stopReason?: string) => void;
498
499
  onToolStart: (event: {
@@ -549,6 +550,7 @@ export function createTelegramActivityRuntime(deps: {
549
550
  let activityId: string | undefined;
550
551
  let activitySource: TelegramActivitySource = "unknown";
551
552
  let activityTarget: TelegramActivityTarget | undefined;
553
+ let activityReplyToMessageId: number | undefined;
552
554
  let sequence = 0;
553
555
  let pendingInputSource: TelegramActivityInputSource = "unknown";
554
556
  let pendingAssistantSegment: PendingAssistantSegment | undefined;
@@ -584,6 +586,7 @@ export function createTelegramActivityRuntime(deps: {
584
586
  sequence,
585
587
  source: activitySource,
586
588
  ...(activityTarget ? { target: activityTarget } : {}),
589
+ ...(activityReplyToMessageId !== undefined ? { replyToMessageId: activityReplyToMessageId } : {}),
587
590
  timestamp: now(),
588
591
  } as TelegramActivityEvent;
589
592
  try {
@@ -610,6 +613,7 @@ export function createTelegramActivityRuntime(deps: {
610
613
  activityId = undefined;
611
614
  activitySource = "unknown";
612
615
  activityTarget = undefined;
616
+ activityReplyToMessageId = undefined;
613
617
  sequence = 0;
614
618
  pendingAssistantSegment = undefined;
615
619
  compactionInProgress = false;
@@ -627,9 +631,10 @@ export function createTelegramActivityRuntime(deps: {
627
631
  recordInputSource(source) {
628
632
  pendingInputSource = source;
629
633
  },
630
- onAgentStart(activeTelegramTarget) {
634
+ onAgentStart(activeTelegramTarget, replyToMessageId) {
631
635
  abandonCompaction();
632
636
  ensureActivity(activeTelegramTarget);
637
+ activityReplyToMessageId = activitySource === "telegram" ? replyToMessageId : undefined;
633
638
  emit({ type: "agent-start" });
634
639
  },
635
640
  onAssistantEvent(event) {
@@ -738,6 +743,63 @@ export function createTelegramActivityRuntime(deps: {
738
743
  };
739
744
  }
740
745
 
746
+ // --- Ordered Bridge-Owned Publication ---
747
+
748
+ export interface TelegramActivityPublicationReservation {
749
+ publish: (task: () => Promise<void>) => Promise<void>;
750
+ cancel: () => void;
751
+ }
752
+
753
+ export interface TelegramActivityPublicationRuntime {
754
+ enqueue: (task: () => Promise<void>) => Promise<void>;
755
+ reserve: () => TelegramActivityPublicationReservation;
756
+ reset: () => void;
757
+ }
758
+
759
+ export function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime {
760
+ let generation = 0;
761
+ let tail = Promise.resolve();
762
+ const pending = new Set<() => void>();
763
+ const reserve = (): TelegramActivityPublicationReservation => {
764
+ const admittedGeneration = generation;
765
+ let state: "pending" | "published" | "cancelled" = "pending";
766
+ let resolve!: (task: (() => Promise<void>) | undefined) => void;
767
+ const ready = new Promise<(() => Promise<void>) | undefined>((accept) => { resolve = accept; });
768
+ const cancel = () => {
769
+ if (state !== "pending") return;
770
+ state = "cancelled";
771
+ pending.delete(cancel);
772
+ resolve(undefined);
773
+ };
774
+ pending.add(cancel);
775
+ const result = tail.then(async () => {
776
+ const task = await ready;
777
+ if (admittedGeneration === generation && task) await task();
778
+ });
779
+ tail = result.catch(() => {});
780
+ return {
781
+ publish(task) {
782
+ if (state === "cancelled") return result;
783
+ if (state === "published") return Promise.reject(new Error("Publication reservation already published."));
784
+ state = "published";
785
+ pending.delete(cancel);
786
+ resolve(task);
787
+ return result;
788
+ },
789
+ cancel,
790
+ };
791
+ };
792
+ return {
793
+ reserve,
794
+ enqueue: (task) => reserve().publish(task),
795
+ reset() {
796
+ generation += 1;
797
+ for (const cancel of pending) cancel();
798
+ tail = Promise.resolve();
799
+ },
800
+ };
801
+ }
802
+
741
803
  // --- Public Assistant Output Projection ---
742
804
 
743
805
  export interface TelegramAssistantOutputRuntime {
@@ -747,7 +809,14 @@ export interface TelegramAssistantOutputRuntime {
747
809
  stop: () => void;
748
810
  }
749
811
 
812
+ export interface TelegramAssistantOutputPreparation {
813
+ wait: () => Promise<void>;
814
+ settle: () => void;
815
+ }
816
+
750
817
  export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(deps: {
818
+ prepareSend?: (event: TelegramAssistantSegmentEvent) => TelegramAssistantOutputPreparation | undefined;
819
+ enqueue?: TelegramActivityPublicationRuntime["enqueue"];
751
820
  captureAuthority?: () => TAuthority;
752
821
  isAuthorityActive?: (authority: TAuthority) => boolean;
753
822
  canDeliver: (event: TelegramAssistantSegmentEvent) => boolean;
@@ -785,7 +854,9 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
785
854
  admitted.add(key);
786
855
  const admittedGeneration = generation;
787
856
  const admittedAuthority = deps.captureAuthority?.();
788
- tail = tail.then(async () => {
857
+ const preparation = deps.prepareSend?.(event);
858
+ const enqueue = deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
859
+ tail = enqueue(async () => {
789
860
  const isAdmittedAuthorityActive = () =>
790
861
  running &&
791
862
  generation === admittedGeneration &&
@@ -794,6 +865,8 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
794
865
  deps.isAuthorityActive(admittedAuthority as TAuthority));
795
866
  if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
796
867
  try {
868
+ if (preparation) await preparation.wait();
869
+ if (!isAdmittedAuthorityActive() || !deps.canDeliver(event)) return;
797
870
  await deps.send(
798
871
  event,
799
872
  admittedAuthority as TAuthority,
@@ -802,7 +875,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
802
875
  } catch (error) {
803
876
  deps.recordFailure?.(event, error);
804
877
  }
805
- });
878
+ }).finally(() => preparation?.settle());
806
879
  },
807
880
  waitForIdle() {
808
881
  return tail;