@llblab/pi-actors 0.22.5 → 0.24.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 +125 -59
- package/BACKLOG.md +88 -403
- package/CHANGELOG.md +39 -0
- package/README.md +14 -1
- package/dist/fixtures/protocol/actor-message-branch.json +13 -0
- package/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/dist/fixtures/protocol/mailbox-contract.json +15 -0
- package/dist/fixtures/protocol/recipe-summary.json +16 -0
- package/dist/fixtures/protocol/room-message.json +11 -0
- package/dist/fixtures/protocol/room-roster.json +11 -0
- package/dist/fixtures/protocol/run-inbox-message.json +9 -0
- package/dist/fixtures/protocol/run-outbox-event.json +9 -0
- package/dist/fixtures/protocol/run-state.json +10 -0
- package/dist/index.js +18 -2
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +58 -44
- package/dist/lib/actor-rooms.d.ts +16 -0
- package/dist/lib/actor-rooms.js +123 -28
- package/dist/lib/actor-worker.d.ts +13 -0
- package/dist/lib/actor-worker.js +86 -0
- package/dist/lib/async-runner.d.ts +5 -0
- package/dist/lib/async-runner.js +134 -0
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +224 -50
- package/dist/lib/build-dist.d.ts +5 -0
- package/dist/lib/build-dist.js +24 -0
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/conformance.d.ts +12 -0
- package/dist/lib/conformance.js +28 -0
- package/dist/lib/coordinator.d.ts +5 -0
- package/dist/lib/coordinator.js +557 -0
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/locker.d.ts +5 -0
- package/dist/lib/locker.js +310 -0
- package/dist/lib/mailbox-loop.d.ts +41 -0
- package/dist/lib/mailbox-loop.js +62 -0
- package/dist/lib/observability.d.ts +2 -2
- package/dist/lib/observability.js +57 -57
- package/dist/lib/output.js +4 -5
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +29 -4
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/recipe-utils.d.ts +5 -0
- package/dist/lib/recipe-utils.js +385 -0
- package/dist/lib/runtime-notifier.js +3 -7
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +198 -39
- package/dist/lib/validate-recipe.d.ts +6 -0
- package/dist/lib/validate-recipe.js +104 -0
- package/dist/recipes/actor-worker.json +35 -0
- package/dist/recipes/coordinator-locker.json +45 -0
- package/dist/recipes/lens-swarm.json +66 -0
- package/dist/recipes/locker.json +45 -0
- package/dist/recipes/music-player.json +38 -0
- package/dist/recipes/pipeline-architect-coordinator.json +95 -0
- package/dist/recipes/pipeline-artifact-bundle.json +100 -0
- package/dist/recipes/pipeline-artifact-report.json +58 -0
- package/dist/recipes/pipeline-artifact-write.json +72 -0
- package/dist/recipes/pipeline-async-run-ops.json +70 -0
- package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
- package/dist/recipes/pipeline-development-tasking.json +81 -0
- package/dist/recipes/pipeline-docs-maintenance.json +80 -0
- package/dist/recipes/pipeline-media-library.json +59 -0
- package/dist/recipes/pipeline-quorum-review.json +79 -0
- package/dist/recipes/pipeline-release-readiness.json +110 -0
- package/dist/recipes/pipeline-release-summary.json +88 -0
- package/dist/recipes/pipeline-repo-health.json +89 -0
- package/dist/recipes/pipeline-research-synthesis.json +94 -0
- package/dist/recipes/pipeline-review-readiness.json +54 -0
- package/dist/recipes/pipeline-room-swarm.json +50 -0
- package/dist/recipes/subagent-artifact.json +32 -0
- package/dist/recipes/subagent-checkpoint.json +33 -0
- package/dist/recipes/subagent-conflict-report.json +32 -0
- package/dist/recipes/subagent-contradiction-map.json +33 -0
- package/dist/recipes/subagent-critic.json +35 -0
- package/dist/recipes/subagent-evidence-map.json +33 -0
- package/dist/recipes/subagent-followup.json +33 -0
- package/dist/recipes/subagent-judge.json +33 -0
- package/dist/recipes/subagent-merge.json +33 -0
- package/dist/recipes/subagent-message.json +34 -0
- package/dist/recipes/subagent-normalize.json +31 -0
- package/dist/recipes/subagent-plan.json +33 -0
- package/dist/recipes/subagent-prompt.json +28 -0
- package/dist/recipes/subagent-quorum.json +43 -0
- package/dist/recipes/subagent-review-coordinator.json +114 -0
- package/dist/recipes/subagent-review.json +37 -0
- package/dist/recipes/subagent-task-card.json +35 -0
- package/dist/recipes/subagent-tools.json +27 -0
- package/dist/recipes/subagent-verify.json +34 -0
- package/dist/recipes/subagents-prompts.json +51 -0
- package/dist/recipes/utility-actor-message.json +23 -0
- package/dist/recipes/utility-artifact-manifest.json +16 -0
- package/dist/recipes/utility-artifact-write.json +16 -0
- package/dist/recipes/utility-changelog-head.json +11 -0
- package/dist/recipes/utility-changelog-section.json +13 -0
- package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
- package/dist/recipes/utility-git-log.json +11 -0
- package/dist/recipes/utility-git-status.json +9 -0
- package/dist/recipes/utility-jsonl-tail.json +10 -0
- package/dist/recipes/utility-markdown-index.json +14 -0
- package/dist/recipes/utility-package-summary.json +11 -0
- package/dist/recipes/utility-playlist-build.json +17 -0
- package/dist/recipes/utility-playlist-scan.json +11 -0
- package/dist/recipes/utility-run-ops-snapshot.json +17 -0
- package/dist/recipes/utility-run-state-files.json +13 -0
- package/dist/recipes/utility-run-summary.json +11 -0
- package/dist/recipes/utility-skill-summary.json +13 -0
- package/dist/recipes/utility-validate-recipe.json +13 -0
- package/dist/recipes/utility-validation-wrapper.json +13 -0
- package/dist/scripts/actor-worker.mjs +31 -0
- package/dist/scripts/async-runner.mjs +31 -0
- package/dist/scripts/build-dist.mjs +25 -0
- package/dist/scripts/conformance.mjs +33 -0
- package/dist/scripts/coordinator.mjs +31 -0
- package/dist/scripts/locker.mjs +33 -0
- package/dist/scripts/music-player.mjs +964 -0
- package/dist/scripts/recipe-utils.mjs +31 -0
- package/dist/scripts/validate-recipe.mjs +34 -0
- package/dist/skills/actors/SKILL.md +375 -0
- package/dist/skills/swarm/SKILL.md +467 -0
- package/dist/skills/swarm/references/development-swarm.md +596 -0
- package/docs/actor-messages.md +2 -2
- package/docs/async-runs.md +13 -1
- package/docs/template-recipes.md +2 -2
- package/docs/tool-registry.md +0 -1
- package/fixtures/protocol/actor-message-branch.json +13 -0
- package/fixtures/protocol/artifact-manifest.json +9 -0
- package/fixtures/protocol/mailbox-contract.json +15 -0
- package/fixtures/protocol/recipe-summary.json +16 -0
- package/fixtures/protocol/room-message.json +11 -0
- package/fixtures/protocol/room-roster.json +11 -0
- package/fixtures/protocol/run-inbox-message.json +9 -0
- package/fixtures/protocol/run-outbox-event.json +9 -0
- package/fixtures/protocol/run-state.json +10 -0
- package/index.ts +21 -0
- package/lib/actor-inspector-tui.ts +138 -59
- package/lib/actor-rooms.ts +241 -60
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +302 -53
- package/lib/build-dist.ts +30 -0
- package/lib/command-templates.ts +5 -5
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +649 -0
- package/lib/limits.ts +12 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +34 -23
- package/lib/output.ts +4 -6
- package/lib/prompts.ts +1 -1
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +57 -6
- package/lib/recipe-usage.ts +31 -4
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/runtime.ts +31 -7
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +297 -58
- package/lib/validate-recipe.ts +110 -0
- package/package.json +11 -2
- package/recipes/actor-worker.json +35 -0
- package/recipes/pipeline-quorum-review.json +12 -7
- package/scripts/actor-worker.mjs +31 -0
- package/scripts/async-runner.mjs +11 -195
- package/scripts/build-dist.mjs +25 -0
- package/scripts/conformance.mjs +33 -0
- package/scripts/coordinator.mjs +19 -616
- package/scripts/locker.mjs +23 -322
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +20 -467
- package/scripts/validate-recipe.mjs +23 -113
- package/skills/actors/SKILL.md +7 -4
- package/skills/swarm/SKILL.md +3 -1
package/dist/lib/actor-rooms.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import * as Limits from "./limits.js";
|
|
9
10
|
import { notifyRuntimeWake } from "./runtime-notifier.js";
|
|
11
|
+
import { formatStateReadDiagnostics, readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
|
|
10
12
|
const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
11
13
|
const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
12
14
|
const DEFAULT_ROOM_MAX_MESSAGES = 10000;
|
|
@@ -86,14 +88,7 @@ function runFromRoomAddress(address) {
|
|
|
86
88
|
return match?.[1];
|
|
87
89
|
}
|
|
88
90
|
function readJsonFile(file, fallback) {
|
|
89
|
-
|
|
90
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
if (error.code === "ENOENT")
|
|
94
|
-
return fallback;
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
91
|
+
return readJsonFileResilient(file, fallback).value;
|
|
97
92
|
}
|
|
98
93
|
function writeJsonFile(file, value) {
|
|
99
94
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -128,8 +123,23 @@ function compactRoomMessages(stateDir, room) {
|
|
|
128
123
|
return;
|
|
129
124
|
const kept = lines.slice(-maxMessages);
|
|
130
125
|
fs.writeFileSync(file, `${kept.join("\n")}\n`);
|
|
126
|
+
const keptMessages = kept.flatMap((line) => {
|
|
127
|
+
try {
|
|
128
|
+
return [JSON.parse(line)];
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
});
|
|
131
134
|
writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
|
|
132
135
|
compacted_at: new Date().toISOString(),
|
|
136
|
+
dropped_count: lines.length - kept.length,
|
|
137
|
+
...(keptMessages[0]?.received_at
|
|
138
|
+
? { first_kept_at: keptMessages[0].received_at }
|
|
139
|
+
: {}),
|
|
140
|
+
...(keptMessages.at(-1)?.received_at
|
|
141
|
+
? { last_kept_at: keptMessages.at(-1)?.received_at }
|
|
142
|
+
: {}),
|
|
133
143
|
max_messages: maxMessages,
|
|
134
144
|
});
|
|
135
145
|
}
|
|
@@ -250,20 +260,44 @@ function updateRosterForMessage(stateDir, room, message, receivedAt) {
|
|
|
250
260
|
last_seen: receivedAt,
|
|
251
261
|
...(current?.caps !== undefined ? { caps: current.caps } : {}),
|
|
252
262
|
...(current?.claim !== undefined ? { claim: current.claim } : {}),
|
|
253
|
-
...(current?.display !== undefined
|
|
263
|
+
...(current?.display !== undefined
|
|
264
|
+
? { display: current.display }
|
|
265
|
+
: {}),
|
|
254
266
|
...(current?.parent !== undefined ? { parent: current.parent } : {}),
|
|
255
|
-
...(current?.role !== undefined
|
|
267
|
+
...(current?.role !== undefined
|
|
268
|
+
? { role: current.role }
|
|
269
|
+
: { role: "actor" }),
|
|
256
270
|
status: String(body.status ?? "left"),
|
|
257
271
|
}
|
|
258
272
|
: {
|
|
259
273
|
address: message.from,
|
|
260
274
|
joined_at: current?.joined_at ?? receivedAt,
|
|
261
275
|
last_seen: receivedAt,
|
|
262
|
-
...(body.caps !== undefined
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
...(body.caps !== undefined
|
|
277
|
+
? { caps: body.caps }
|
|
278
|
+
: current?.caps !== undefined
|
|
279
|
+
? { caps: current.caps }
|
|
280
|
+
: {}),
|
|
281
|
+
...(body.claim !== undefined
|
|
282
|
+
? { claim: body.claim }
|
|
283
|
+
: current?.claim !== undefined
|
|
284
|
+
? { claim: current.claim }
|
|
285
|
+
: {}),
|
|
286
|
+
...(body.display !== undefined
|
|
287
|
+
? { display: body.display }
|
|
288
|
+
: current?.display !== undefined
|
|
289
|
+
? { display: current.display }
|
|
290
|
+
: {}),
|
|
291
|
+
...(body.parent !== undefined
|
|
292
|
+
? { parent: body.parent }
|
|
293
|
+
: current?.parent !== undefined
|
|
294
|
+
? { parent: current.parent }
|
|
295
|
+
: {}),
|
|
296
|
+
...(body.role !== undefined
|
|
297
|
+
? { role: body.role }
|
|
298
|
+
: current?.role !== undefined
|
|
299
|
+
? { role: current.role }
|
|
300
|
+
: { role: "actor" }),
|
|
267
301
|
status: String(body.status ?? current?.status ?? "present"),
|
|
268
302
|
};
|
|
269
303
|
roster[message.from] = next;
|
|
@@ -354,6 +388,43 @@ export function appendBranchInboxMessage(stateDir, run, address, message) {
|
|
|
354
388
|
releaseLock();
|
|
355
389
|
}
|
|
356
390
|
}
|
|
391
|
+
export function claimBranchInboxMessage(stateDir, run, address, owner = "runtime", statuses = ["queued"]) {
|
|
392
|
+
const branch = branchIdFromAddress(address, run);
|
|
393
|
+
if (!branch)
|
|
394
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
395
|
+
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
396
|
+
try {
|
|
397
|
+
const file = branchInboxFile(stateDir, branch);
|
|
398
|
+
const records = readAllBranchInboxLines(stateDir, run, address);
|
|
399
|
+
let claimed;
|
|
400
|
+
const now = new Date().toISOString();
|
|
401
|
+
const updated = records.map((record) => {
|
|
402
|
+
if (claimed || !("message" in record))
|
|
403
|
+
return record;
|
|
404
|
+
const status = record.message.status ?? "queued";
|
|
405
|
+
if (!statuses.includes(status))
|
|
406
|
+
return record;
|
|
407
|
+
claimed = {
|
|
408
|
+
...record.message,
|
|
409
|
+
claimed_at: now,
|
|
410
|
+
claimed_by: owner,
|
|
411
|
+
status: "claimed",
|
|
412
|
+
};
|
|
413
|
+
return { message: claimed };
|
|
414
|
+
});
|
|
415
|
+
if (!claimed)
|
|
416
|
+
return undefined;
|
|
417
|
+
fs.writeFileSync(file, `${updated.map((record) => ("raw" in record ? record.raw : JSON.stringify(record.message))).join("\n")}\n`);
|
|
418
|
+
notifyActorWake(stateDir, address, "branch.inbox.claim", {
|
|
419
|
+
id: claimed.id,
|
|
420
|
+
owner,
|
|
421
|
+
});
|
|
422
|
+
return claimed;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
releaseLock();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
357
428
|
export function updateBranchInboxMessageStatus(stateDir, run, address, id, status, metadata = {}) {
|
|
358
429
|
const branch = branchIdFromAddress(address, run);
|
|
359
430
|
if (!branch)
|
|
@@ -368,7 +439,14 @@ export function updateBranchInboxMessageStatus(stateDir, run, address, id, statu
|
|
|
368
439
|
if (!("message" in record) || record.message.id !== id)
|
|
369
440
|
return record;
|
|
370
441
|
changed = true;
|
|
371
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
message: {
|
|
444
|
+
...record.message,
|
|
445
|
+
...metadata,
|
|
446
|
+
[timestampKey]: new Date().toISOString(),
|
|
447
|
+
status,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
372
450
|
});
|
|
373
451
|
if (!changed)
|
|
374
452
|
return false;
|
|
@@ -422,7 +500,14 @@ export function appendRoomMessage(stateDir, room, message) {
|
|
|
422
500
|
export function readRoomMessages(stateDir, room, limit = 40) {
|
|
423
501
|
try {
|
|
424
502
|
const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
|
|
425
|
-
return lines.
|
|
503
|
+
return lines.flatMap((line) => {
|
|
504
|
+
try {
|
|
505
|
+
return [JSON.parse(line)];
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
});
|
|
426
511
|
}
|
|
427
512
|
catch (error) {
|
|
428
513
|
if (error.code === "ENOENT")
|
|
@@ -430,7 +515,7 @@ export function readRoomMessages(stateDir, room, limit = 40) {
|
|
|
430
515
|
throw error;
|
|
431
516
|
}
|
|
432
517
|
}
|
|
433
|
-
function previewValue(value, maxLength =
|
|
518
|
+
function previewValue(value, maxLength = Limits.ROOM_MESSAGE_PREVIEW_CHARS) {
|
|
434
519
|
if (value === undefined)
|
|
435
520
|
return undefined;
|
|
436
521
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
@@ -443,7 +528,9 @@ function previewValue(value, maxLength = 120) {
|
|
|
443
528
|
}
|
|
444
529
|
export function readRoomMessagePreviews(stateDir, room, limit = 40) {
|
|
445
530
|
return readRoomMessages(stateDir, room, limit).map((message) => ({
|
|
446
|
-
...(previewValue(message.body)
|
|
531
|
+
...(previewValue(message.body)
|
|
532
|
+
? { body_preview: previewValue(message.body) }
|
|
533
|
+
: {}),
|
|
447
534
|
...(message.from ? { from: message.from } : {}),
|
|
448
535
|
...(message.summary ? { summary: message.summary } : {}),
|
|
449
536
|
timestamp: message.received_at,
|
|
@@ -454,6 +541,14 @@ export function readRoomMessagePreviews(stateDir, room, limit = 40) {
|
|
|
454
541
|
export function getRoomStatus(stateDir, room) {
|
|
455
542
|
const messageCount = readRoomMessageCount(stateDir, room);
|
|
456
543
|
const [last] = readRoomMessages(stateDir, room, 1);
|
|
544
|
+
const compactionRead = readJsonFileResilient(path.join(roomDir(stateDir, room), "compaction.json"), undefined);
|
|
545
|
+
const rosterRead = readJsonFileResilient(rosterFile(stateDir, room), {});
|
|
546
|
+
const messageRead = readJsonlFileResilient(messagesFile(stateDir, room));
|
|
547
|
+
const diagnostics = [
|
|
548
|
+
...messageRead.diagnostics,
|
|
549
|
+
...rosterRead.diagnostics,
|
|
550
|
+
...compactionRead.diagnostics,
|
|
551
|
+
];
|
|
457
552
|
return {
|
|
458
553
|
...(last
|
|
459
554
|
? {
|
|
@@ -463,9 +558,16 @@ export function getRoomStatus(stateDir, room) {
|
|
|
463
558
|
last_message_type: last.type,
|
|
464
559
|
}
|
|
465
560
|
: {}),
|
|
561
|
+
...(compactionRead.value ? { compaction: compactionRead.value } : {}),
|
|
562
|
+
...(diagnostics.length
|
|
563
|
+
? {
|
|
564
|
+
diagnostics: formatStateReadDiagnostics(diagnostics),
|
|
565
|
+
diagnostics_count: diagnostics.length,
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
466
568
|
message_count: messageCount,
|
|
467
569
|
room,
|
|
468
|
-
roster_count: Object.keys(
|
|
570
|
+
roster_count: Object.keys(rosterRead.value).length,
|
|
469
571
|
};
|
|
470
572
|
}
|
|
471
573
|
export function ensureRoomMember(stateDir, run, room, address, body, summary) {
|
|
@@ -490,14 +592,7 @@ export function ensureDefaultRoom(stateDir, run) {
|
|
|
490
592
|
return ensureRoomMember(stateDir, run, "main", `run:${run}`, { role: "run", status: "present" }, "Run joined default room");
|
|
491
593
|
}
|
|
492
594
|
export function readCommunicationSnapshot(stateDir) {
|
|
493
|
-
|
|
494
|
-
return JSON.parse(fs.readFileSync(snapshotFile(stateDir), "utf8"));
|
|
495
|
-
}
|
|
496
|
-
catch (error) {
|
|
497
|
-
if (error.code === "ENOENT")
|
|
498
|
-
return undefined;
|
|
499
|
-
throw error;
|
|
500
|
-
}
|
|
595
|
+
return readJsonFileResilient(snapshotFile(stateDir), undefined).value;
|
|
501
596
|
}
|
|
502
597
|
export function readRoomContacts(stateDir, room, self) {
|
|
503
598
|
return Object.values(readRoomRoster(stateDir, room))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mailbox-backed actor worker entrypoint logic.
|
|
3
|
+
* Zones: worker demo, mailbox loop recipe runtime
|
|
4
|
+
*/
|
|
5
|
+
interface ActorWorkerArgs {
|
|
6
|
+
branch?: string;
|
|
7
|
+
poll_ms?: string | number;
|
|
8
|
+
run?: string;
|
|
9
|
+
state_dir?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseActorWorkerArgs(argv: string[]): ActorWorkerArgs;
|
|
12
|
+
export declare function runActorWorker(argv?: string[]): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mailbox-backed actor worker entrypoint logic.
|
|
3
|
+
* Zones: worker demo, mailbox loop recipe runtime
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { appendRoomMessage, ensureRoomMember } from "./actor-rooms.js";
|
|
8
|
+
import { handleMailboxLoopOnce, isMailboxLoopStopMessage } from "./mailbox-loop.js";
|
|
9
|
+
export function parseActorWorkerArgs(argv) {
|
|
10
|
+
const args = {};
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const token = argv[index];
|
|
13
|
+
if (!token.startsWith("--"))
|
|
14
|
+
continue;
|
|
15
|
+
const key = token.slice(2).replaceAll("-", "_");
|
|
16
|
+
const next = argv[index + 1];
|
|
17
|
+
if (!next || next.startsWith("--")) {
|
|
18
|
+
args[key] = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
args[key] = next;
|
|
22
|
+
index += 1;
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
function text(value) {
|
|
30
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
export async function runActorWorker(argv = process.argv.slice(2)) {
|
|
33
|
+
const args = parseActorWorkerArgs(argv);
|
|
34
|
+
const stateDir = String(args.state_dir ?? "");
|
|
35
|
+
const run = String(args.run ?? "");
|
|
36
|
+
const branch = String(args.branch ?? "worker");
|
|
37
|
+
const pollMs = Math.max(50, Number(args.poll_ms ?? 1000));
|
|
38
|
+
if (!stateDir || !run) {
|
|
39
|
+
throw new Error("usage: actor-worker.mjs --state-dir <dir> --run <id> [--branch worker] [--poll-ms 1000]");
|
|
40
|
+
}
|
|
41
|
+
const branchAddress = `branch:${run}/${branch}`;
|
|
42
|
+
const roomAddress = `room:${run}`;
|
|
43
|
+
const journalPath = join(stateDir, "worker-events.jsonl");
|
|
44
|
+
function journal(event, data = {}) {
|
|
45
|
+
appendFileSync(journalPath, `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`);
|
|
46
|
+
}
|
|
47
|
+
function room(type, summary, body = {}) {
|
|
48
|
+
appendRoomMessage(stateDir, "main", {
|
|
49
|
+
body,
|
|
50
|
+
from: branchAddress,
|
|
51
|
+
summary,
|
|
52
|
+
to: roomAddress,
|
|
53
|
+
type,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
ensureRoomMember(stateDir, run, "main", branchAddress, { display: branch, role: "worker", status: "present" }, `${branch} joined as mailbox worker`);
|
|
57
|
+
room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
|
|
58
|
+
journal("worker.started", { branch, run });
|
|
59
|
+
let stopping = false;
|
|
60
|
+
while (!stopping) {
|
|
61
|
+
const result = await handleMailboxLoopOnce({ address: branchAddress, kind: "branch", run, stateDir }, (message) => {
|
|
62
|
+
if (isMailboxLoopStopMessage(message)) {
|
|
63
|
+
stopping = true;
|
|
64
|
+
room("actor.leave", `${branch} stopping`, { branch, reason: message.type });
|
|
65
|
+
journal("worker.stopping", { id: message.id, type: message.type });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
room("task.claim", `${branch} claimed ${message.type}`, {
|
|
69
|
+
branch,
|
|
70
|
+
id: message.id,
|
|
71
|
+
type: message.type,
|
|
72
|
+
});
|
|
73
|
+
room("task.result", `${branch} handled ${message.type}`, {
|
|
74
|
+
branch,
|
|
75
|
+
id: message.id,
|
|
76
|
+
result: text(message.body),
|
|
77
|
+
type: message.type,
|
|
78
|
+
});
|
|
79
|
+
room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
|
|
80
|
+
journal("task.handled", { id: message.id, type: message.type });
|
|
81
|
+
}, { owner: branchAddress });
|
|
82
|
+
if (!result.handled)
|
|
83
|
+
await sleep(pollMs);
|
|
84
|
+
}
|
|
85
|
+
journal("worker.done", { branch, run });
|
|
86
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached async-runner entrypoint logic.
|
|
3
|
+
* Zones: async run process, command-template execution telemetry
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { appendRecipeContextToPiArgs } from "./actor-recipe-context.js";
|
|
8
|
+
import { execCommandTemplate } from "./command-templates.js";
|
|
9
|
+
import { executeRegisteredTool } from "./execution.js";
|
|
10
|
+
import { writeJsonAtomic } from "./file-state.js";
|
|
11
|
+
function quoteCommandDetailPart(value) {
|
|
12
|
+
if (value === "")
|
|
13
|
+
return "''";
|
|
14
|
+
if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value))
|
|
15
|
+
return value;
|
|
16
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
17
|
+
}
|
|
18
|
+
function formatCommandDetail(command, args) {
|
|
19
|
+
return [command, ...args].map(quoteCommandDetailPart).join(" ");
|
|
20
|
+
}
|
|
21
|
+
function summarizeCommandDetail(commandDetail) {
|
|
22
|
+
return commandDetail.length > 160
|
|
23
|
+
? `${commandDetail.slice(0, 157)}...`
|
|
24
|
+
: commandDetail;
|
|
25
|
+
}
|
|
26
|
+
export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
27
|
+
if (!stateDir)
|
|
28
|
+
throw new Error("missing state dir");
|
|
29
|
+
const runPath = join(stateDir, "run.json");
|
|
30
|
+
const progressPath = join(stateDir, "progress.json");
|
|
31
|
+
const resultPath = join(stateDir, "result.json");
|
|
32
|
+
const eventsPath = join(stateDir, "events.jsonl");
|
|
33
|
+
const outboxPath = join(stateDir, "outbox.jsonl");
|
|
34
|
+
const stdoutPath = join(stateDir, "stdout.log");
|
|
35
|
+
const stderrPath = join(stateDir, "stderr.log");
|
|
36
|
+
const meta = JSON.parse(readFileSync(runPath, "utf8"));
|
|
37
|
+
function event(name, data = {}) {
|
|
38
|
+
appendFileSync(eventsPath, `${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`);
|
|
39
|
+
}
|
|
40
|
+
function outbox(name, summary, data = {}, delivery = "log", level = "info") {
|
|
41
|
+
appendFileSync(outboxPath, `${JSON.stringify({ body: data, data, delivery, event: name, from: `run:${meta.run}`, level, summary, to: "coordinator", ts: new Date().toISOString(), type: name })}\n`);
|
|
42
|
+
}
|
|
43
|
+
function progress(phase, extra = {}) {
|
|
44
|
+
writeJsonAtomic(progressPath, {
|
|
45
|
+
phase,
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
...extra,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
let activeSubagents = 0;
|
|
51
|
+
let completedSubagents = 0;
|
|
52
|
+
const subagentFailures = [];
|
|
53
|
+
function getCommandDoneDelivery(result) {
|
|
54
|
+
return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
|
|
55
|
+
}
|
|
56
|
+
function progressRunning() {
|
|
57
|
+
progress("running", {
|
|
58
|
+
activeSubagents,
|
|
59
|
+
completed: completedSubagents,
|
|
60
|
+
failures: subagentFailures,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function observedExec(command, args, options) {
|
|
64
|
+
const commandDetail = formatCommandDetail(command, args);
|
|
65
|
+
const execArgs = appendRecipeContextToPiArgs(command, args, meta.recipe_context_records, options?.actorRecipeContext);
|
|
66
|
+
activeSubagents += 1;
|
|
67
|
+
event("command.start", { activeSubagents, command: commandDetail });
|
|
68
|
+
progressRunning();
|
|
69
|
+
const result = await execCommandTemplate(command, execArgs, options);
|
|
70
|
+
activeSubagents = Math.max(0, activeSubagents - 1);
|
|
71
|
+
completedSubagents += 1;
|
|
72
|
+
if (result.code !== 0) {
|
|
73
|
+
subagentFailures.push({
|
|
74
|
+
code: result.code,
|
|
75
|
+
command: commandDetail,
|
|
76
|
+
killed: result.killed,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
event("command.done", {
|
|
80
|
+
activeSubagents,
|
|
81
|
+
code: result.code,
|
|
82
|
+
command: commandDetail,
|
|
83
|
+
killed: result.killed,
|
|
84
|
+
});
|
|
85
|
+
outbox("command.done", `Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`, {
|
|
86
|
+
activeSubagents,
|
|
87
|
+
...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
|
|
88
|
+
run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath],
|
|
89
|
+
code: result.code,
|
|
90
|
+
command: commandDetail,
|
|
91
|
+
killed: result.killed,
|
|
92
|
+
}, getCommandDoneDelivery(result), result.code === 0 ? "info" : "error");
|
|
93
|
+
progressRunning();
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
event("run.runner.start", { pid: process.pid });
|
|
98
|
+
progressRunning();
|
|
99
|
+
const result = await executeRegisteredTool({
|
|
100
|
+
name: "run_actor",
|
|
101
|
+
description: "Detached command-template run actor",
|
|
102
|
+
template: meta.template,
|
|
103
|
+
args: [],
|
|
104
|
+
defaults: {},
|
|
105
|
+
}, meta.values || {}, observedExec, meta.cwd);
|
|
106
|
+
const text = result.content?.[0]?.text || "";
|
|
107
|
+
appendFileSync(stdoutPath, text);
|
|
108
|
+
writeJsonAtomic(resultPath, {
|
|
109
|
+
code: result.details.code,
|
|
110
|
+
command: result.details.command,
|
|
111
|
+
killed: result.details.killed,
|
|
112
|
+
truncated: result.details.truncated,
|
|
113
|
+
completedAt: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
progress("done", {
|
|
116
|
+
completed: 1,
|
|
117
|
+
failures: result.details.nonCriticalFailures || [],
|
|
118
|
+
});
|
|
119
|
+
event("run.done", { code: result.details.code });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
appendFileSync(stderrPath, `${message}\n`);
|
|
124
|
+
writeJsonAtomic(resultPath, {
|
|
125
|
+
code: 1,
|
|
126
|
+
error: message,
|
|
127
|
+
killed: false,
|
|
128
|
+
completedAt: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
progress("failed", { completed: 0, failures: [{ message }] });
|
|
131
|
+
event("run.failed", { error: message });
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface AsyncRunStartParams {
|
|
|
29
29
|
timeout?: number | string;
|
|
30
30
|
delay?: number | string;
|
|
31
31
|
output?: string;
|
|
32
|
-
artifacts?: Record<string,
|
|
32
|
+
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
33
33
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
34
34
|
retire_when?: "children_terminal";
|
|
35
35
|
retry?: number | string;
|
|
@@ -76,15 +76,43 @@ export interface AsyncRunMeta {
|
|
|
76
76
|
tool?: string;
|
|
77
77
|
template: CommandTemplateValue;
|
|
78
78
|
values: Record<string, unknown>;
|
|
79
|
-
artifacts?: Record<string,
|
|
79
|
+
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
80
80
|
control?: AsyncRunControlEndpoint;
|
|
81
81
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
82
82
|
recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
|
|
83
83
|
retire_when?: "children_terminal";
|
|
84
84
|
}
|
|
85
|
+
export type RunArtifactDeclaration = string | {
|
|
86
|
+
path: string;
|
|
87
|
+
kind?: string;
|
|
88
|
+
media_type?: string;
|
|
89
|
+
required?: boolean;
|
|
90
|
+
};
|
|
91
|
+
export interface RunArtifactManifestEntry {
|
|
92
|
+
exists: boolean;
|
|
93
|
+
kind?: string;
|
|
94
|
+
media_type?: string;
|
|
95
|
+
path: string;
|
|
96
|
+
required?: boolean;
|
|
97
|
+
sha256?: string;
|
|
98
|
+
size?: number;
|
|
99
|
+
}
|
|
100
|
+
export declare function resolveArtifactManifest(artifacts: Record<string, RunArtifactDeclaration> | undefined): Record<string, RunArtifactManifestEntry> | undefined;
|
|
85
101
|
export declare function startRun(params: AsyncRunStartParams, cwd: string): AsyncRunMeta;
|
|
86
102
|
export declare function parseRunOutboxEventLine(line: string, run: string, stateDir: string, index: number): RunOutboxEvent | undefined;
|
|
87
103
|
export declare function getRunStatus(runOrDir: string): Record<string, unknown>;
|
|
104
|
+
export interface RunStateIndexEntry {
|
|
105
|
+
ownerId?: string;
|
|
106
|
+
recipe?: string;
|
|
107
|
+
run: string;
|
|
108
|
+
state_dir: string;
|
|
109
|
+
status: AsyncRunStatus;
|
|
110
|
+
tool?: string;
|
|
111
|
+
updated_at?: string;
|
|
112
|
+
}
|
|
113
|
+
export declare function listRunStateDirs(stateRoot?: string, depth?: number, seen?: Set<string>): string[];
|
|
114
|
+
export declare function rebuildRunStateIndex(stateRoot?: string): RunStateIndexEntry[];
|
|
115
|
+
export declare function readRunStateIndex(stateRoot?: string): RunStateIndexEntry[] | undefined;
|
|
88
116
|
export declare function listRuns(stateRoot?: string, statusFilter?: string): Array<Record<string, unknown>>;
|
|
89
117
|
export declare function tailRun(runOrDir: string, lines?: number): string;
|
|
90
118
|
export declare function readRunEvents(runOrDir: string, lines?: number): RunOutboxEvent[];
|
|
@@ -132,4 +160,8 @@ export interface RunProcessSignalPlan {
|
|
|
132
160
|
}
|
|
133
161
|
export declare function getRunProcessSignalPlan(pid: number, signal: NodeJS.Signals, runtimePlatform?: NodeJS.Platform): RunProcessSignalPlan;
|
|
134
162
|
export declare function cancelRun(runOrDir: string): Record<string, unknown>;
|
|
163
|
+
export declare function archiveRun(runOrDir: string): Record<string, unknown>;
|
|
164
|
+
export declare function pruneRun(runOrDir: string, options?: {
|
|
165
|
+
preserveArtifacts?: boolean;
|
|
166
|
+
}): Record<string, unknown>;
|
|
135
167
|
export declare function killRun(runOrDir: string): Record<string, unknown>;
|