@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.
Files changed (180) hide show
  1. package/AGENTS.md +125 -59
  2. package/BACKLOG.md +88 -403
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +14 -1
  5. package/dist/fixtures/protocol/actor-message-branch.json +13 -0
  6. package/dist/fixtures/protocol/artifact-manifest.json +9 -0
  7. package/dist/fixtures/protocol/mailbox-contract.json +15 -0
  8. package/dist/fixtures/protocol/recipe-summary.json +16 -0
  9. package/dist/fixtures/protocol/room-message.json +11 -0
  10. package/dist/fixtures/protocol/room-roster.json +11 -0
  11. package/dist/fixtures/protocol/run-inbox-message.json +9 -0
  12. package/dist/fixtures/protocol/run-outbox-event.json +9 -0
  13. package/dist/fixtures/protocol/run-state.json +10 -0
  14. package/dist/index.js +18 -2
  15. package/dist/lib/actor-inspector-tui.d.ts +4 -0
  16. package/dist/lib/actor-inspector-tui.js +58 -44
  17. package/dist/lib/actor-rooms.d.ts +16 -0
  18. package/dist/lib/actor-rooms.js +123 -28
  19. package/dist/lib/actor-worker.d.ts +13 -0
  20. package/dist/lib/actor-worker.js +86 -0
  21. package/dist/lib/async-runner.d.ts +5 -0
  22. package/dist/lib/async-runner.js +134 -0
  23. package/dist/lib/async-runs.d.ts +34 -2
  24. package/dist/lib/async-runs.js +224 -50
  25. package/dist/lib/build-dist.d.ts +5 -0
  26. package/dist/lib/build-dist.js +24 -0
  27. package/dist/lib/command-templates.js +5 -5
  28. package/dist/lib/conformance.d.ts +12 -0
  29. package/dist/lib/conformance.js +28 -0
  30. package/dist/lib/coordinator.d.ts +5 -0
  31. package/dist/lib/coordinator.js +557 -0
  32. package/dist/lib/limits.d.ts +11 -0
  33. package/dist/lib/limits.js +11 -0
  34. package/dist/lib/locker.d.ts +5 -0
  35. package/dist/lib/locker.js +310 -0
  36. package/dist/lib/mailbox-loop.d.ts +41 -0
  37. package/dist/lib/mailbox-loop.js +62 -0
  38. package/dist/lib/observability.d.ts +2 -2
  39. package/dist/lib/observability.js +57 -57
  40. package/dist/lib/output.js +4 -5
  41. package/dist/lib/prompts.d.ts +1 -1
  42. package/dist/lib/prompts.js +1 -1
  43. package/dist/lib/recipe-discovery.js +1 -0
  44. package/dist/lib/recipe-references.d.ts +11 -2
  45. package/dist/lib/recipe-references.js +29 -4
  46. package/dist/lib/recipe-usage.d.ts +2 -1
  47. package/dist/lib/recipe-usage.js +15 -3
  48. package/dist/lib/recipe-utils.d.ts +5 -0
  49. package/dist/lib/recipe-utils.js +385 -0
  50. package/dist/lib/runtime-notifier.js +3 -7
  51. package/dist/lib/runtime.js +12 -2
  52. package/dist/lib/state-readers.d.ts +21 -0
  53. package/dist/lib/state-readers.js +74 -0
  54. package/dist/lib/tools.js +198 -39
  55. package/dist/lib/validate-recipe.d.ts +6 -0
  56. package/dist/lib/validate-recipe.js +104 -0
  57. package/dist/recipes/actor-worker.json +35 -0
  58. package/dist/recipes/coordinator-locker.json +45 -0
  59. package/dist/recipes/lens-swarm.json +66 -0
  60. package/dist/recipes/locker.json +45 -0
  61. package/dist/recipes/music-player.json +38 -0
  62. package/dist/recipes/pipeline-architect-coordinator.json +95 -0
  63. package/dist/recipes/pipeline-artifact-bundle.json +100 -0
  64. package/dist/recipes/pipeline-artifact-report.json +58 -0
  65. package/dist/recipes/pipeline-artifact-write.json +72 -0
  66. package/dist/recipes/pipeline-async-run-ops.json +70 -0
  67. package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
  68. package/dist/recipes/pipeline-development-tasking.json +81 -0
  69. package/dist/recipes/pipeline-docs-maintenance.json +80 -0
  70. package/dist/recipes/pipeline-media-library.json +59 -0
  71. package/dist/recipes/pipeline-quorum-review.json +79 -0
  72. package/dist/recipes/pipeline-release-readiness.json +110 -0
  73. package/dist/recipes/pipeline-release-summary.json +88 -0
  74. package/dist/recipes/pipeline-repo-health.json +89 -0
  75. package/dist/recipes/pipeline-research-synthesis.json +94 -0
  76. package/dist/recipes/pipeline-review-readiness.json +54 -0
  77. package/dist/recipes/pipeline-room-swarm.json +50 -0
  78. package/dist/recipes/subagent-artifact.json +32 -0
  79. package/dist/recipes/subagent-checkpoint.json +33 -0
  80. package/dist/recipes/subagent-conflict-report.json +32 -0
  81. package/dist/recipes/subagent-contradiction-map.json +33 -0
  82. package/dist/recipes/subagent-critic.json +35 -0
  83. package/dist/recipes/subagent-evidence-map.json +33 -0
  84. package/dist/recipes/subagent-followup.json +33 -0
  85. package/dist/recipes/subagent-judge.json +33 -0
  86. package/dist/recipes/subagent-merge.json +33 -0
  87. package/dist/recipes/subagent-message.json +34 -0
  88. package/dist/recipes/subagent-normalize.json +31 -0
  89. package/dist/recipes/subagent-plan.json +33 -0
  90. package/dist/recipes/subagent-prompt.json +28 -0
  91. package/dist/recipes/subagent-quorum.json +43 -0
  92. package/dist/recipes/subagent-review-coordinator.json +114 -0
  93. package/dist/recipes/subagent-review.json +37 -0
  94. package/dist/recipes/subagent-task-card.json +35 -0
  95. package/dist/recipes/subagent-tools.json +27 -0
  96. package/dist/recipes/subagent-verify.json +34 -0
  97. package/dist/recipes/subagents-prompts.json +51 -0
  98. package/dist/recipes/utility-actor-message.json +23 -0
  99. package/dist/recipes/utility-artifact-manifest.json +16 -0
  100. package/dist/recipes/utility-artifact-write.json +16 -0
  101. package/dist/recipes/utility-changelog-head.json +11 -0
  102. package/dist/recipes/utility-changelog-section.json +13 -0
  103. package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
  104. package/dist/recipes/utility-git-log.json +11 -0
  105. package/dist/recipes/utility-git-status.json +9 -0
  106. package/dist/recipes/utility-jsonl-tail.json +10 -0
  107. package/dist/recipes/utility-markdown-index.json +14 -0
  108. package/dist/recipes/utility-package-summary.json +11 -0
  109. package/dist/recipes/utility-playlist-build.json +17 -0
  110. package/dist/recipes/utility-playlist-scan.json +11 -0
  111. package/dist/recipes/utility-run-ops-snapshot.json +17 -0
  112. package/dist/recipes/utility-run-state-files.json +13 -0
  113. package/dist/recipes/utility-run-summary.json +11 -0
  114. package/dist/recipes/utility-skill-summary.json +13 -0
  115. package/dist/recipes/utility-validate-recipe.json +13 -0
  116. package/dist/recipes/utility-validation-wrapper.json +13 -0
  117. package/dist/scripts/actor-worker.mjs +31 -0
  118. package/dist/scripts/async-runner.mjs +31 -0
  119. package/dist/scripts/build-dist.mjs +25 -0
  120. package/dist/scripts/conformance.mjs +33 -0
  121. package/dist/scripts/coordinator.mjs +31 -0
  122. package/dist/scripts/locker.mjs +33 -0
  123. package/dist/scripts/music-player.mjs +964 -0
  124. package/dist/scripts/recipe-utils.mjs +31 -0
  125. package/dist/scripts/validate-recipe.mjs +34 -0
  126. package/dist/skills/actors/SKILL.md +375 -0
  127. package/dist/skills/swarm/SKILL.md +467 -0
  128. package/dist/skills/swarm/references/development-swarm.md +596 -0
  129. package/docs/actor-messages.md +2 -2
  130. package/docs/async-runs.md +13 -1
  131. package/docs/template-recipes.md +2 -2
  132. package/docs/tool-registry.md +0 -1
  133. package/fixtures/protocol/actor-message-branch.json +13 -0
  134. package/fixtures/protocol/artifact-manifest.json +9 -0
  135. package/fixtures/protocol/mailbox-contract.json +15 -0
  136. package/fixtures/protocol/recipe-summary.json +16 -0
  137. package/fixtures/protocol/room-message.json +11 -0
  138. package/fixtures/protocol/room-roster.json +11 -0
  139. package/fixtures/protocol/run-inbox-message.json +9 -0
  140. package/fixtures/protocol/run-outbox-event.json +9 -0
  141. package/fixtures/protocol/run-state.json +10 -0
  142. package/index.ts +21 -0
  143. package/lib/actor-inspector-tui.ts +138 -59
  144. package/lib/actor-rooms.ts +241 -60
  145. package/lib/actor-worker.ts +118 -0
  146. package/lib/async-runner.ts +173 -0
  147. package/lib/async-runs.ts +302 -53
  148. package/lib/build-dist.ts +30 -0
  149. package/lib/command-templates.ts +5 -5
  150. package/lib/conformance.ts +46 -0
  151. package/lib/coordinator.ts +649 -0
  152. package/lib/limits.ts +12 -0
  153. package/lib/locker.ts +340 -0
  154. package/lib/mailbox-loop.ts +148 -0
  155. package/lib/observability.ts +34 -23
  156. package/lib/output.ts +4 -6
  157. package/lib/prompts.ts +1 -1
  158. package/lib/recipe-discovery.ts +1 -0
  159. package/lib/recipe-references.ts +57 -6
  160. package/lib/recipe-usage.ts +31 -4
  161. package/lib/recipe-utils.ts +486 -0
  162. package/lib/runtime-notifier.ts +4 -6
  163. package/lib/runtime.ts +31 -7
  164. package/lib/state-readers.ts +93 -0
  165. package/lib/tools.ts +297 -58
  166. package/lib/validate-recipe.ts +110 -0
  167. package/package.json +11 -2
  168. package/recipes/actor-worker.json +35 -0
  169. package/recipes/pipeline-quorum-review.json +12 -7
  170. package/scripts/actor-worker.mjs +31 -0
  171. package/scripts/async-runner.mjs +11 -195
  172. package/scripts/build-dist.mjs +25 -0
  173. package/scripts/conformance.mjs +33 -0
  174. package/scripts/coordinator.mjs +19 -616
  175. package/scripts/locker.mjs +23 -322
  176. package/scripts/music-player.mjs +21 -2
  177. package/scripts/recipe-utils.mjs +20 -467
  178. package/scripts/validate-recipe.mjs +23 -113
  179. package/skills/actors/SKILL.md +7 -4
  180. package/skills/swarm/SKILL.md +3 -1
@@ -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
- try {
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 ? { display: current.display } : {}),
263
+ ...(current?.display !== undefined
264
+ ? { display: current.display }
265
+ : {}),
254
266
  ...(current?.parent !== undefined ? { parent: current.parent } : {}),
255
- ...(current?.role !== undefined ? { role: current.role } : { role: "actor" }),
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 ? { caps: body.caps } : current?.caps !== undefined ? { caps: current.caps } : {}),
263
- ...(body.claim !== undefined ? { claim: body.claim } : current?.claim !== undefined ? { claim: current.claim } : {}),
264
- ...(body.display !== undefined ? { display: body.display } : current?.display !== undefined ? { display: current.display } : {}),
265
- ...(body.parent !== undefined ? { parent: body.parent } : current?.parent !== undefined ? { parent: current.parent } : {}),
266
- ...(body.role !== undefined ? { role: body.role } : current?.role !== undefined ? { role: current.role } : { role: "actor" }),
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 { message: { ...record.message, ...metadata, [timestampKey]: new Date().toISOString(), status } };
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.map((line) => JSON.parse(line));
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 = 120) {
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) ? { body_preview: 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(readRoomRoster(stateDir, room)).length,
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
- try {
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,5 @@
1
+ /**
2
+ * Detached async-runner entrypoint logic.
3
+ * Zones: async run process, command-template execution telemetry
4
+ */
5
+ export declare function runAsyncRunner(stateDir?: string): Promise<void>;
@@ -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
+ }
@@ -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, 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, 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>;