@llblab/pi-actors 0.22.4 → 0.23.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 (49) hide show
  1. package/AGENTS.md +1 -0
  2. package/BACKLOG.md +0 -220
  3. package/CHANGELOG.md +23 -1
  4. package/README.md +1 -1
  5. package/dist/index.js +15 -0
  6. package/dist/lib/actor-inspector-tui.d.ts +4 -0
  7. package/dist/lib/actor-inspector-tui.js +53 -12
  8. package/dist/lib/actor-rooms.d.ts +8 -0
  9. package/dist/lib/actor-rooms.js +61 -10
  10. package/dist/lib/async-runs.d.ts +34 -2
  11. package/dist/lib/async-runs.js +215 -22
  12. package/dist/lib/command-templates.js +5 -5
  13. package/dist/lib/limits.d.ts +11 -0
  14. package/dist/lib/limits.js +11 -0
  15. package/dist/lib/observability.js +6 -4
  16. package/dist/lib/output.js +4 -5
  17. package/dist/lib/recipe-discovery.js +1 -0
  18. package/dist/lib/recipe-references.d.ts +11 -2
  19. package/dist/lib/recipe-references.js +4 -2
  20. package/dist/lib/recipe-usage.d.ts +2 -1
  21. package/dist/lib/recipe-usage.js +15 -3
  22. package/dist/lib/runtime.js +12 -2
  23. package/dist/lib/tools.js +197 -38
  24. package/docs/async-runs.md +2 -1
  25. package/docs/template-recipes.md +1 -1
  26. package/docs/tool-registry.md +0 -1
  27. package/index.ts +18 -0
  28. package/lib/actor-inspector-tui.ts +131 -29
  29. package/lib/actor-rooms.ts +155 -44
  30. package/lib/async-runs.ts +290 -31
  31. package/lib/command-templates.ts +5 -5
  32. package/lib/limits.ts +12 -0
  33. package/lib/observability.ts +10 -4
  34. package/lib/output.ts +4 -6
  35. package/lib/recipe-discovery.ts +1 -0
  36. package/lib/recipe-references.ts +20 -4
  37. package/lib/recipe-usage.ts +31 -4
  38. package/lib/runtime.ts +31 -7
  39. package/lib/tools.ts +296 -57
  40. package/package.json +2 -1
  41. package/scripts/async-runner.mjs +6 -0
  42. package/scripts/conformance.mjs +47 -0
  43. package/scripts/coordinator.mjs +13 -0
  44. package/scripts/locker.mjs +13 -0
  45. package/scripts/music-player.mjs +21 -2
  46. package/scripts/recipe-utils.mjs +13 -0
  47. package/scripts/validate-recipe.mjs +13 -0
  48. package/skills/actors/SKILL.md +3 -3
  49. package/skills/swarm/SKILL.md +1 -1
@@ -9,16 +9,19 @@ import * as path from "node:path";
9
9
  import { visibleWidth } from "@earendil-works/pi-tui";
10
10
 
11
11
  import type { ActorMessage } from "./actor-messages.ts";
12
+ import * as Limits from "./limits.ts";
12
13
  import * as Paths from "./paths.ts";
13
14
 
14
15
  export interface ActorInspectorPreview {
15
16
  body_preview?: string;
16
17
  branch?: string;
17
18
  channel: "broadcast" | "direct" | "room";
19
+ event_id?: string;
18
20
  from?: string;
19
21
  from_display?: string;
20
22
  inbox_status?: string;
21
23
  message_id?: string;
24
+ needs_response?: boolean;
22
25
  run: string;
23
26
  sequence?: number;
24
27
  summary?: string;
@@ -57,6 +60,7 @@ export interface ActorInspectorPreviewReadOptions {
57
60
  currentRunOnly?: boolean;
58
61
  channels?: ActorInspectorPreview["channel"][];
59
62
  mention?: string;
63
+ readKeys?: Iterable<string>;
60
64
  roomLimitPerRun?: number;
61
65
  unreadOnly?: boolean;
62
66
  }
@@ -86,7 +90,10 @@ function readJsonLines(file: string): Record<string, unknown>[] {
86
90
  }
87
91
  }
88
92
 
89
- function previewValue(value: unknown, maxLength = 320): string | undefined {
93
+ function previewValue(
94
+ value: unknown,
95
+ maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS,
96
+ ): string | undefined {
90
97
  if (value === undefined) return undefined;
91
98
  const text = typeof value === "string" ? value : JSON.stringify(value);
92
99
  const compact = text.replaceAll(/\s+/g, " ").trim();
@@ -118,6 +125,11 @@ function previewFromMessage(
118
125
  const summary =
119
126
  typeof message.summary === "string" ? message.summary : undefined;
120
127
  const body = asRecord(message.body);
128
+ const metadata = asRecord(message.metadata);
129
+ const eventId =
130
+ typeof message.id === "string" && message.id.trim()
131
+ ? message.id.trim()
132
+ : undefined;
121
133
  const display = from
122
134
  ? typeof body.display === "string" && body.display.trim()
123
135
  ? body.display.trim()
@@ -128,8 +140,10 @@ function previewFromMessage(
128
140
  ? { body_preview: previewValue(message.body) }
129
141
  : {}),
130
142
  channel: channelFor({ to }),
143
+ ...(eventId ? { event_id: eventId } : {}),
131
144
  ...(from ? { from } : {}),
132
145
  ...(display ? { from_display: display } : {}),
146
+ ...(metadata.requires_response === true ? { needs_response: true } : {}),
133
147
  run,
134
148
  ...(summary ? { summary } : {}),
135
149
  timestamp,
@@ -144,19 +158,29 @@ function readRoomRosterRecords(
144
158
  ): Record<string, Record<string, unknown>> {
145
159
  try {
146
160
  return JSON.parse(
147
- fs.readFileSync(path.join(stateDir, "rooms", room, "roster.json"), "utf8"),
161
+ fs.readFileSync(
162
+ path.join(stateDir, "rooms", room, "roster.json"),
163
+ "utf8",
164
+ ),
148
165
  ) as Record<string, Record<string, unknown>>;
149
166
  } catch {
150
167
  return {};
151
168
  }
152
169
  }
153
170
 
154
- function memberDisplay(_address: string, member: Record<string, unknown>): string | undefined {
155
- const display = typeof member.display === "string" ? member.display.trim() : "";
171
+ function memberDisplay(
172
+ _address: string,
173
+ member: Record<string, unknown>,
174
+ ): string | undefined {
175
+ const display =
176
+ typeof member.display === "string" ? member.display.trim() : "";
156
177
  return display || undefined;
157
178
  }
158
179
 
159
- function readRoomDisplayNames(stateDir: string, room: string): Record<string, string> {
180
+ function readRoomDisplayNames(
181
+ stateDir: string,
182
+ room: string,
183
+ ): Record<string, string> {
160
184
  const roster = readRoomRosterRecords(stateDir, room);
161
185
  return Object.fromEntries(
162
186
  Object.entries(roster).flatMap(([address, member]) => {
@@ -227,17 +251,28 @@ function readBranchInboxPreviews(
227
251
  const preview = previewFromMessage(
228
252
  run,
229
253
  message,
230
- String(message.queued_at ?? message.received_at ?? message.timestamp ?? ""),
254
+ String(
255
+ message.queued_at ??
256
+ message.received_at ??
257
+ message.timestamp ??
258
+ "",
259
+ ),
231
260
  );
232
261
  if (!preview) return undefined;
233
262
  return {
234
263
  ...preview,
235
264
  branch: entry.name,
236
- ...(typeof message.id === "string" ? { message_id: message.id } : {}),
237
- ...(typeof message.status === "string" ? { inbox_status: message.status } : {}),
265
+ ...(typeof message.id === "string"
266
+ ? { message_id: message.id }
267
+ : {}),
268
+ ...(typeof message.status === "string"
269
+ ? { inbox_status: message.status }
270
+ : {}),
238
271
  };
239
272
  })
240
- .filter((preview): preview is ActorInspectorPreview => Boolean(preview)),
273
+ .filter((preview): preview is ActorInspectorPreview =>
274
+ Boolean(preview),
275
+ ),
241
276
  );
242
277
  } catch (error) {
243
278
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
@@ -276,8 +311,29 @@ function matchesOwner(stateDir: string, ownerId: string | undefined): boolean {
276
311
  return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
277
312
  }
278
313
 
279
- function isUnreadPreview(preview: ActorInspectorPreview): boolean {
280
- return preview.inbox_status === "queued" || preview.inbox_status === undefined && preview.branch !== undefined;
314
+ export function inspectorPreviewReadKey(
315
+ preview: ActorInspectorPreview,
316
+ ): string {
317
+ return [
318
+ preview.channel,
319
+ preview.run,
320
+ preview.branch ?? "",
321
+ preview.event_id ?? preview.message_id ?? preview.timestamp,
322
+ preview.from ?? "",
323
+ preview.to,
324
+ preview.type,
325
+ ].join("|");
326
+ }
327
+
328
+ function isUnreadPreview(
329
+ preview: ActorInspectorPreview,
330
+ readKeys?: Set<string>,
331
+ ): boolean {
332
+ if (readKeys?.has(inspectorPreviewReadKey(preview))) return false;
333
+ return (
334
+ preview.inbox_status === "queued" ||
335
+ (preview.inbox_status === undefined && preview.branch !== undefined)
336
+ );
281
337
  }
282
338
 
283
339
  function matchesBranchFilter(
@@ -298,7 +354,8 @@ function matchesPreviewFilter(
298
354
  if (options.channels?.length && !options.channels.includes(preview.channel)) {
299
355
  return false;
300
356
  }
301
- if (options.unreadOnly && !isUnreadPreview(preview)) return false;
357
+ const readKeys = options.readKeys ? new Set(options.readKeys) : undefined;
358
+ if (options.unreadOnly && !isUnreadPreview(preview, readKeys)) return false;
302
359
  if (!matchesBranchFilter(preview, options.branch)) return false;
303
360
  const mention = options.mention?.trim().toLowerCase();
304
361
  if (!mention) return true;
@@ -421,6 +478,10 @@ function routeText(preview: ActorInspectorPreview): string {
421
478
  return `${actor} → ${actorName(preview.to)}`;
422
479
  }
423
480
 
481
+ function attentionMarker(preview: ActorInspectorPreview): string {
482
+ return preview.needs_response ? "! " : "";
483
+ }
484
+
424
485
  function style(
425
486
  styleFn: ((text: string) => string) | undefined,
426
487
  text: string,
@@ -435,7 +496,8 @@ function previewText(preview: ActorInspectorPreview): string {
435
496
  function propertyValue(value: unknown): string {
436
497
  if (value === undefined) return "";
437
498
  if (typeof value === "string") return value;
438
- if (typeof value === "number" || typeof value === "boolean") return String(value);
499
+ if (typeof value === "number" || typeof value === "boolean")
500
+ return String(value);
439
501
  return JSON.stringify(value);
440
502
  }
441
503
 
@@ -491,7 +553,7 @@ function renderCompactInspectorEntry(
491
553
  const contentWidth = Math.max(8, width - prefix.length - suffix.length);
492
554
  const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
493
555
  const sequencePrefix = `${sequence}${separator}`;
494
- const route = routeText(preview);
556
+ const route = `${attentionMarker(preview)}${routeText(preview)}`;
495
557
  const routePadding = " ".repeat(
496
558
  Math.max(0, routeWidth - displayWidth(route)),
497
559
  );
@@ -537,14 +599,19 @@ function renderInspectorEntry(
537
599
  const contentWidth = Math.max(8, width - prefix.length - suffix.length);
538
600
  const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
539
601
  const sequencePrefix = `${sequence}${separator}`;
540
- const route = routeText(preview);
602
+ const route = `${attentionMarker(preview)}${routeText(preview)}`;
541
603
  const type = preview.type;
542
604
  const summary = preview.summary?.trim() ?? "";
543
- const body = preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
605
+ const body =
606
+ preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
544
607
  const visibleRoute = boundedLine(route, routeWidth);
545
608
  const visibleType = boundedLine(type, typeWidth);
546
- const boundedRoutePadding = " ".repeat(Math.max(0, routeWidth - displayWidth(visibleRoute)));
547
- const boundedTypePadding = " ".repeat(Math.max(0, typeWidth - displayWidth(visibleType)));
609
+ const boundedRoutePadding = " ".repeat(
610
+ Math.max(0, routeWidth - displayWidth(visibleRoute)),
611
+ );
612
+ const boundedTypePadding = " ".repeat(
613
+ Math.max(0, typeWidth - displayWidth(visibleType)),
614
+ );
548
615
  const leadParts = [
549
616
  style(styles.muted, sequencePrefix),
550
617
  style(styles.target, visibleRoute),
@@ -557,11 +624,20 @@ function renderInspectorEntry(
557
624
  let lead = `${sequencePrefix}${visibleRoute}${boundedRoutePadding}${separator}${visibleType}${boundedTypePadding}${separator}`;
558
625
  if (summary) {
559
626
  const visibleSummary = boundedLine(summary, summaryWidth);
560
- const summaryPadding = " ".repeat(Math.max(0, summaryWidth - displayWidth(visibleSummary)));
627
+ const summaryPadding = " ".repeat(
628
+ Math.max(0, summaryWidth - displayWidth(visibleSummary)),
629
+ );
561
630
  lead += `${visibleSummary}${summaryPadding}${separator}`;
562
- leadParts.push(style(styles.preview, visibleSummary), summaryPadding, separator);
631
+ leadParts.push(
632
+ style(styles.preview, visibleSummary),
633
+ summaryPadding,
634
+ separator,
635
+ );
563
636
  }
564
- const visibleBody = boundedLine(body, Math.max(0, contentWidth - displayWidth(lead)));
637
+ const visibleBody = boundedLine(
638
+ body,
639
+ Math.max(0, contentWidth - displayWidth(lead)),
640
+ );
565
641
  const plain = `${lead}${visibleBody}`;
566
642
  const rendered = `${leadParts.join("")}${style(styles.preview, visibleBody)}`;
567
643
  const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
@@ -589,7 +665,10 @@ export function readActorInspectorRoster(
589
665
 
590
666
  function rosterRoleText(role: string | undefined): string | undefined {
591
667
  const roleLabel = role?.split(";")[0] ?? "";
592
- const cleaned = roleLabel.replaceAll(/\s*\([^)]*\)\s*$/g, "").trim().toLowerCase();
668
+ const cleaned = roleLabel
669
+ .replaceAll(/\s*\([^)]*\)\s*$/g, "")
670
+ .trim()
671
+ .toLowerCase();
593
672
  if (!cleaned || cleaned === "actor") return undefined;
594
673
  return cleaned.replaceAll(/\s+/g, "-");
595
674
  }
@@ -603,7 +682,12 @@ function rosterMemberText(member: ActorInspectorRosterMember): string {
603
682
 
604
683
  function isRosterMemberActive(member: ActorInspectorRosterMember): boolean {
605
684
  const status = member.status?.trim().toLowerCase();
606
- return !status || status === "present" || status === "active" || status === "running";
685
+ return (
686
+ !status ||
687
+ status === "present" ||
688
+ status === "active" ||
689
+ status === "running"
690
+ );
607
691
  }
608
692
 
609
693
  export function renderInspectorRosterLine(
@@ -633,12 +717,17 @@ export function renderInspectorRosterPanel(
633
717
  const flush = () => {
634
718
  const visible = boundedLine(plain, innerWidth);
635
719
  const line = visible === plain ? rendered : style(styles.muted, visible);
636
- lines.push(` ${line}${" ".repeat(Math.max(0, innerWidth - displayWidth(visible)))} `);
720
+ lines.push(
721
+ ` ${line}${" ".repeat(Math.max(0, innerWidth - displayWidth(visible)))} `,
722
+ );
637
723
  };
638
724
  for (const token of tokens) {
639
725
  const separator = plain === prefix ? "" : ", ";
640
726
  const nextPlain = `${plain}${separator}${token.text}`;
641
- const renderedToken = style(token.active ? styles.target : styles.muted, token.text);
727
+ const renderedToken = style(
728
+ token.active ? styles.target : styles.muted,
729
+ token.text,
730
+ );
642
731
  if (plain !== prefix && displayWidth(nextPlain) > innerWidth) {
643
732
  flush();
644
733
  plain = ` ${token.text}`;
@@ -664,12 +753,15 @@ export function renderInspectorItemView(
664
753
  const orderedKeys = [
665
754
  "channel",
666
755
  "run",
756
+ "event_id",
667
757
  "from",
668
758
  "from_display",
669
759
  "to",
670
760
  "type",
671
761
  "summary",
672
762
  "body_preview",
763
+ "message_id",
764
+ "needs_response",
673
765
  "timestamp",
674
766
  "stripe",
675
767
  ] as const;
@@ -678,7 +770,9 @@ export function renderInspectorItemView(
678
770
  .map((key) => [key, propertyValue(preview[key])] as const);
679
771
  const keyWidth = Math.max(1, ...entries.map(([key]) => displayWidth(key)));
680
772
  const sequenceText = String(preview.sequence ?? options.sequence);
681
- const sequencePadding = " ".repeat(Math.max(0, keyWidth - displayWidth(sequenceText)));
773
+ const sequencePadding = " ".repeat(
774
+ Math.max(0, keyWidth - displayWidth(sequenceText)),
775
+ );
682
776
  const headerSeparator = " ";
683
777
  const route = routeText(preview);
684
778
  const visibleRoute = boundedLine(
@@ -728,7 +822,9 @@ export function renderInspectorWidget(
728
822
  safeWidth - 1 - sequencePrefixWidth - fixedSeparatorsWidth,
729
823
  );
730
824
  const naturalRouteWidth = Math.max(
731
- ...visible.map(({ preview }) => displayWidth(routeText(preview))),
825
+ ...visible.map(({ preview }) =>
826
+ displayWidth(`${attentionMarker(preview)}${routeText(preview)}`),
827
+ ),
732
828
  );
733
829
  const naturalTypeWidth = Math.max(
734
830
  ...visible.map(({ preview }) => displayWidth(preview.type)),
@@ -741,13 +837,19 @@ export function renderInspectorWidget(
741
837
  naturalTypeWidth,
742
838
  Math.max(4, Math.floor(availableForColumns * 0.25)),
743
839
  );
744
- const messageWidth = Math.max(0, availableForColumns - routeWidth - typeWidth);
840
+ const messageWidth = Math.max(
841
+ 0,
842
+ availableForColumns - routeWidth - typeWidth,
843
+ );
745
844
  const summaryWidths = visible
746
845
  .map(({ preview }) => preview.summary?.trim())
747
846
  .filter((summary): summary is string => Boolean(summary))
748
847
  .map((summary) => displayWidth(summary));
749
848
  const summaryWidth = summaryWidths.length
750
- ? Math.min(Math.max(...summaryWidths), Math.max(1, Math.floor(messageWidth * 0.5)))
849
+ ? Math.min(
850
+ Math.max(...summaryWidths),
851
+ Math.max(1, Math.floor(messageWidth * 0.5)),
852
+ )
751
853
  : 0;
752
854
  for (const { preview, stripe } of visible) {
753
855
  lines.push(