@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,6 +9,7 @@ import { randomUUID } from "node:crypto";
9
9
  import * as path from "node:path";
10
10
 
11
11
  import type { ActorMessage } from "./actor-messages.ts";
12
+ import * as Limits from "./limits.ts";
12
13
  import { notifyRuntimeWake } from "./runtime-notifier.ts";
13
14
 
14
15
  const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
@@ -49,10 +50,19 @@ export interface RoomMessagePreview {
49
50
  type: string;
50
51
  }
51
52
 
53
+ export interface RoomCompactionInfo {
54
+ compacted_at: string;
55
+ dropped_count: number;
56
+ first_kept_at?: string;
57
+ last_kept_at?: string;
58
+ max_messages: number;
59
+ }
60
+
52
61
  export interface RoomStatus {
53
62
  room: string;
54
63
  message_count: number;
55
64
  roster_count: number;
65
+ compaction?: RoomCompactionInfo;
56
66
  last_message_at?: string;
57
67
  last_message_from?: string;
58
68
  last_message_summary?: string;
@@ -105,7 +115,10 @@ function branchInboxFile(stateDir: string, branch: string): string {
105
115
  return path.join(stateDir, "branches", branch, "inbox.jsonl");
106
116
  }
107
117
 
108
- function branchIdFromAddress(address: string | undefined, run: string): string | undefined {
118
+ function branchIdFromAddress(
119
+ address: string | undefined,
120
+ run: string,
121
+ ): string | undefined {
109
122
  if (!address) return undefined;
110
123
  const match = new RegExp(`^branch:${run}/(.+)$`).exec(address);
111
124
  return match?.[1];
@@ -119,7 +132,11 @@ function sleepSync(ms: number): void {
119
132
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
120
133
  }
121
134
 
122
- function acquireStateLock(parentDir: string, name: string, label: string): () => void {
135
+ function acquireStateLock(
136
+ parentDir: string,
137
+ name: string,
138
+ label: string,
139
+ ): () => void {
123
140
  fs.mkdirSync(parentDir, { recursive: true });
124
141
  const lockDir = path.join(parentDir, name);
125
142
  const started = Date.now();
@@ -150,11 +167,19 @@ function acquireStateLock(parentDir: string, name: string, label: string): () =>
150
167
  }
151
168
 
152
169
  function acquireRoomLock(stateDir: string, room: string): () => void {
153
- return acquireStateLock(roomDir(stateDir, room), ".append.lock", `Room append ${room}`);
170
+ return acquireStateLock(
171
+ roomDir(stateDir, room),
172
+ ".append.lock",
173
+ `Room append ${room}`,
174
+ );
154
175
  }
155
176
 
156
177
  function acquireBranchInboxLock(stateDir: string, branch: string): () => void {
157
- return acquireStateLock(path.dirname(branchInboxFile(stateDir, branch)), ".inbox.lock", `Branch inbox ${branch}`);
178
+ return acquireStateLock(
179
+ path.dirname(branchInboxFile(stateDir, branch)),
180
+ ".inbox.lock",
181
+ `Branch inbox ${branch}`,
182
+ );
158
183
  }
159
184
 
160
185
  function asRecord(value: unknown): Record<string, unknown> {
@@ -201,7 +226,10 @@ function positiveEnvInt(name: string, fallback: number): number {
201
226
  }
202
227
 
203
228
  function roomMaxMessages(): number {
204
- return positiveEnvInt("PI_ACTORS_ROOM_MAX_MESSAGES", DEFAULT_ROOM_MAX_MESSAGES);
229
+ return positiveEnvInt(
230
+ "PI_ACTORS_ROOM_MAX_MESSAGES",
231
+ DEFAULT_ROOM_MAX_MESSAGES,
232
+ );
205
233
  }
206
234
 
207
235
  function snapshotMinIntervalMs(): number {
@@ -225,8 +253,22 @@ function compactRoomMessages(stateDir: string, room: string): void {
225
253
  if (lines.length <= maxMessages) return;
226
254
  const kept = lines.slice(-maxMessages);
227
255
  fs.writeFileSync(file, `${kept.join("\n")}\n`);
256
+ const keptMessages = kept.flatMap((line) => {
257
+ try {
258
+ return [JSON.parse(line) as RoomTimelineEntry];
259
+ } catch {
260
+ return [];
261
+ }
262
+ });
228
263
  writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
229
264
  compacted_at: new Date().toISOString(),
265
+ dropped_count: lines.length - kept.length,
266
+ ...(keptMessages[0]?.received_at
267
+ ? { first_kept_at: keptMessages[0].received_at }
268
+ : {}),
269
+ ...(keptMessages.at(-1)?.received_at
270
+ ? { last_kept_at: keptMessages.at(-1)?.received_at }
271
+ : {}),
230
272
  max_messages: maxMessages,
231
273
  });
232
274
  }
@@ -242,7 +284,13 @@ function readJsonlLineCount(file: string): number {
242
284
  let count = 0;
243
285
  let lastByte: number | undefined;
244
286
  while (position < stat.size) {
245
- const bytesRead = fs.readSync(fd, chunk, 0, Math.min(chunkSize, stat.size - position), position);
287
+ const bytesRead = fs.readSync(
288
+ fd,
289
+ chunk,
290
+ 0,
291
+ Math.min(chunkSize, stat.size - position),
292
+ position,
293
+ );
246
294
  if (bytesRead <= 0) break;
247
295
  position += bytesRead;
248
296
  for (let index = 0; index < bytesRead; index += 1) {
@@ -299,7 +347,10 @@ export function readRoomRoster(
299
347
  stateDir: string,
300
348
  room: string,
301
349
  ): Record<string, RoomMember> {
302
- return readJsonFile<Record<string, RoomMember>>(rosterFile(stateDir, room), {});
350
+ return readJsonFile<Record<string, RoomMember>>(
351
+ rosterFile(stateDir, room),
352
+ {},
353
+ );
303
354
  }
304
355
 
305
356
  function writeRoomRoster(
@@ -353,29 +404,54 @@ function updateRosterForMessage(
353
404
  if (!message.from) return roster;
354
405
  const body = asRecord(message.body);
355
406
  const current = roster[message.from];
356
- const next = message.type === "actor.leave"
357
- ? {
358
- address: message.from,
359
- joined_at: current?.joined_at ?? receivedAt,
360
- last_seen: receivedAt,
361
- ...(current?.caps !== undefined ? { caps: current.caps } : {}),
362
- ...(current?.claim !== undefined ? { claim: current.claim } : {}),
363
- ...(current?.display !== undefined ? { display: current.display } : {}),
364
- ...(current?.parent !== undefined ? { parent: current.parent } : {}),
365
- ...(current?.role !== undefined ? { role: current.role } : { role: "actor" }),
366
- status: String(body.status ?? "left"),
367
- }
368
- : {
369
- address: message.from,
370
- joined_at: current?.joined_at ?? receivedAt,
371
- last_seen: receivedAt,
372
- ...(body.caps !== undefined ? { caps: body.caps } : current?.caps !== undefined ? { caps: current.caps } : {}),
373
- ...(body.claim !== undefined ? { claim: body.claim } : current?.claim !== undefined ? { claim: current.claim } : {}),
374
- ...(body.display !== undefined ? { display: body.display } : current?.display !== undefined ? { display: current.display } : {}),
375
- ...(body.parent !== undefined ? { parent: body.parent } : current?.parent !== undefined ? { parent: current.parent } : {}),
376
- ...(body.role !== undefined ? { role: body.role } : current?.role !== undefined ? { role: current.role } : { role: "actor" }),
377
- status: String(body.status ?? current?.status ?? "present"),
378
- };
407
+ const next =
408
+ message.type === "actor.leave"
409
+ ? {
410
+ address: message.from,
411
+ joined_at: current?.joined_at ?? receivedAt,
412
+ last_seen: receivedAt,
413
+ ...(current?.caps !== undefined ? { caps: current.caps } : {}),
414
+ ...(current?.claim !== undefined ? { claim: current.claim } : {}),
415
+ ...(current?.display !== undefined
416
+ ? { display: current.display }
417
+ : {}),
418
+ ...(current?.parent !== undefined ? { parent: current.parent } : {}),
419
+ ...(current?.role !== undefined
420
+ ? { role: current.role }
421
+ : { role: "actor" }),
422
+ status: String(body.status ?? "left"),
423
+ }
424
+ : {
425
+ address: message.from,
426
+ joined_at: current?.joined_at ?? receivedAt,
427
+ last_seen: receivedAt,
428
+ ...(body.caps !== undefined
429
+ ? { caps: body.caps }
430
+ : current?.caps !== undefined
431
+ ? { caps: current.caps }
432
+ : {}),
433
+ ...(body.claim !== undefined
434
+ ? { claim: body.claim }
435
+ : current?.claim !== undefined
436
+ ? { claim: current.claim }
437
+ : {}),
438
+ ...(body.display !== undefined
439
+ ? { display: body.display }
440
+ : current?.display !== undefined
441
+ ? { display: current.display }
442
+ : {}),
443
+ ...(body.parent !== undefined
444
+ ? { parent: body.parent }
445
+ : current?.parent !== undefined
446
+ ? { parent: current.parent }
447
+ : {}),
448
+ ...(body.role !== undefined
449
+ ? { role: body.role }
450
+ : current?.role !== undefined
451
+ ? { role: current.role }
452
+ : { role: "actor" }),
453
+ status: String(body.status ?? current?.status ?? "present"),
454
+ };
379
455
  roster[message.from] = next;
380
456
  if (shouldWriteRoomRosterMember(stateDir, room, current, next)) {
381
457
  writeRoomRoster(stateDir, room, roster);
@@ -401,7 +477,8 @@ function readBranchInboxFile(
401
477
  limit = 40,
402
478
  ): BranchInboxReadResult {
403
479
  const branch = branchIdFromAddress(address, run);
404
- if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
480
+ if (!branch)
481
+ throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
405
482
  try {
406
483
  const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
407
484
  const messages: BranchInboxRecord[] = [];
@@ -415,7 +492,8 @@ function readBranchInboxFile(
415
492
  }
416
493
  return { corrupted, messages };
417
494
  } catch (error) {
418
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return { corrupted: 0, messages: [] };
495
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
496
+ return { corrupted: 0, messages: [] };
419
497
  throw error;
420
498
  }
421
499
  }
@@ -426,7 +504,8 @@ function readAllBranchInboxLines(
426
504
  address: string,
427
505
  ): Array<{ raw: string } | { message: BranchInboxRecord }> {
428
506
  const branch = branchIdFromAddress(address, run);
429
- if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
507
+ if (!branch)
508
+ throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
430
509
  try {
431
510
  const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
432
511
  return content
@@ -464,7 +543,9 @@ export function readBranchInboxDiagnostics(
464
543
  }
465
544
 
466
545
  export function getBranchInboxTerminalRetainLimit(): number {
467
- const value = Number(process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "");
546
+ const value = Number(
547
+ process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "",
548
+ );
468
549
  return Number.isInteger(value) && value >= 0
469
550
  ? value
470
551
  : DEFAULT_BRANCH_INBOX_TERMINAL_RETAINED;
@@ -490,7 +571,8 @@ export function appendBranchInboxMessage(
490
571
  message: ActorMessage,
491
572
  ): void {
492
573
  const branch = branchIdFromAddress(address, run);
493
- if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
574
+ if (!branch)
575
+ throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
494
576
  const releaseLock = acquireBranchInboxLock(stateDir, branch);
495
577
  try {
496
578
  fs.writeFileSync(
@@ -516,7 +598,8 @@ export function updateBranchInboxMessageStatus(
516
598
  metadata: Record<string, unknown> = {},
517
599
  ): boolean {
518
600
  const branch = branchIdFromAddress(address, run);
519
- if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
601
+ if (!branch)
602
+ throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
520
603
  const releaseLock = acquireBranchInboxLock(stateDir, branch);
521
604
  try {
522
605
  const file = branchInboxFile(stateDir, branch);
@@ -526,11 +609,22 @@ export function updateBranchInboxMessageStatus(
526
609
  const updated = records.map((record) => {
527
610
  if (!("message" in record) || record.message.id !== id) return record;
528
611
  changed = true;
529
- return { message: { ...record.message, ...metadata, [timestampKey]: new Date().toISOString(), status } };
612
+ return {
613
+ message: {
614
+ ...record.message,
615
+ ...metadata,
616
+ [timestampKey]: new Date().toISOString(),
617
+ status,
618
+ },
619
+ };
530
620
  });
531
621
  if (!changed) return false;
532
- const validMessages = updated.flatMap((record) => "message" in record ? [record.message] : []);
533
- const compactedIds = new Set(compactBranchInboxMessages(validMessages).map((message) => message.id));
622
+ const validMessages = updated.flatMap((record) =>
623
+ "message" in record ? [record.message] : [],
624
+ );
625
+ const compactedIds = new Set(
626
+ compactBranchInboxMessages(validMessages).map((message) => message.id),
627
+ );
534
628
  const lines = updated.flatMap((record) => {
535
629
  if ("raw" in record) return [record.raw];
536
630
  if (record.message.id && !compactedIds.has(record.message.id)) return [];
@@ -553,7 +647,10 @@ export function appendRoomMessage(
553
647
  try {
554
648
  const receivedAt = new Date().toISOString();
555
649
  const entry: RoomTimelineEntry = { ...message, received_at: receivedAt };
556
- fs.appendFileSync(messagesFile(stateDir, room), `${JSON.stringify(entry)}\n`);
650
+ fs.appendFileSync(
651
+ messagesFile(stateDir, room),
652
+ `${JSON.stringify(entry)}\n`,
653
+ );
557
654
  compactRoomMessages(stateDir, room);
558
655
  const roster = updateRosterForMessage(stateDir, room, message, receivedAt);
559
656
  const run = runFromRoomAddress(message.to);
@@ -592,7 +689,10 @@ export function readRoomMessages(
592
689
  }
593
690
  }
594
691
 
595
- function previewValue(value: unknown, maxLength = 120): string | undefined {
692
+ function previewValue(
693
+ value: unknown,
694
+ maxLength = Limits.ROOM_MESSAGE_PREVIEW_CHARS,
695
+ ): string | undefined {
596
696
  if (value === undefined) return undefined;
597
697
  const text = typeof value === "string" ? value : JSON.stringify(value);
598
698
  const compact = text.replaceAll(/\s+/g, " ").trim();
@@ -608,7 +708,9 @@ export function readRoomMessagePreviews(
608
708
  limit = 40,
609
709
  ): RoomMessagePreview[] {
610
710
  return readRoomMessages(stateDir, room, limit).map((message) => ({
611
- ...(previewValue(message.body) ? { body_preview: previewValue(message.body) } : {}),
711
+ ...(previewValue(message.body)
712
+ ? { body_preview: previewValue(message.body) }
713
+ : {}),
612
714
  ...(message.from ? { from: message.from } : {}),
613
715
  ...(message.summary ? { summary: message.summary } : {}),
614
716
  timestamp: message.received_at,
@@ -620,6 +722,10 @@ export function readRoomMessagePreviews(
620
722
  export function getRoomStatus(stateDir: string, room: string): RoomStatus {
621
723
  const messageCount = readRoomMessageCount(stateDir, room);
622
724
  const [last] = readRoomMessages(stateDir, room, 1);
725
+ const compaction = readJsonFile<RoomCompactionInfo | undefined>(
726
+ path.join(roomDir(stateDir, room), "compaction.json"),
727
+ undefined,
728
+ );
623
729
  return {
624
730
  ...(last
625
731
  ? {
@@ -629,6 +735,7 @@ export function getRoomStatus(stateDir: string, room: string): RoomStatus {
629
735
  last_message_type: last.type,
630
736
  }
631
737
  : {}),
738
+ ...(compaction ? { compaction } : {}),
632
739
  message_count: messageCount,
633
740
  room,
634
741
  roster_count: Object.keys(readRoomRoster(stateDir, room)).length,
@@ -661,7 +768,10 @@ export function ensureRoomMember(
661
768
  });
662
769
  }
663
770
 
664
- export function ensureDefaultRoom(stateDir: string, run: string): RoomAppendResult {
771
+ export function ensureDefaultRoom(
772
+ stateDir: string,
773
+ run: string,
774
+ ): RoomAppendResult {
665
775
  return ensureRoomMember(
666
776
  stateDir,
667
777
  run,
@@ -753,6 +863,7 @@ function writeBranchCommunicationSnapshotDebounced(
753
863
  ): ActorCommunicationSnapshot | undefined {
754
864
  const branch = branchIdFromAddress(self, run);
755
865
  if (!branch) throw new Error(`Expected branch:${run}/<branch>; got ${self}`);
756
- if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch))) return undefined;
866
+ if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
867
+ return undefined;
757
868
  return writeBranchCommunicationSnapshot(stateDir, run, self);
758
869
  }