@llblab/pi-actors 0.22.5 → 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.
- package/AGENTS.md +1 -0
- package/BACKLOG.md +0 -220
- package/CHANGELOG.md +18 -1
- package/README.md +1 -1
- package/dist/index.js +15 -0
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +53 -12
- package/dist/lib/actor-rooms.d.ts +8 -0
- package/dist/lib/actor-rooms.js +61 -10
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +215 -22
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/observability.js +6 -4
- package/dist/lib/output.js +4 -5
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +4 -2
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/tools.js +197 -38
- package/docs/async-runs.md +2 -1
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +0 -1
- package/index.ts +18 -0
- package/lib/actor-inspector-tui.ts +131 -29
- package/lib/actor-rooms.ts +155 -44
- package/lib/async-runs.ts +290 -31
- package/lib/command-templates.ts +5 -5
- package/lib/limits.ts +12 -0
- package/lib/observability.ts +10 -4
- package/lib/output.ts +4 -6
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +20 -4
- package/lib/recipe-usage.ts +31 -4
- package/lib/runtime.ts +31 -7
- package/lib/tools.ts +296 -57
- package/package.json +2 -1
- package/scripts/async-runner.mjs +6 -0
- package/scripts/conformance.mjs +47 -0
- package/scripts/coordinator.mjs +13 -0
- package/scripts/locker.mjs +13 -0
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +13 -0
- package/scripts/validate-recipe.mjs +13 -0
- package/skills/actors/SKILL.md +3 -3
- package/skills/swarm/SKILL.md +1 -1
package/lib/actor-rooms.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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>>(
|
|
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 =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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)
|
|
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")
|
|
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)
|
|
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(
|
|
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)
|
|
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)
|
|
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 {
|
|
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) =>
|
|
533
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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)))
|
|
866
|
+
if (shouldDebounceSnapshot(branchSnapshotFile(stateDir, branch)))
|
|
867
|
+
return undefined;
|
|
757
868
|
return writeBranchCommunicationSnapshot(stateDir, run, self);
|
|
758
869
|
}
|