@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
package/lib/tools.ts CHANGED
@@ -10,6 +10,7 @@ import * as AsyncRuns from "./async-runs.ts";
10
10
  import * as CommandTemplates from "./command-templates.ts";
11
11
  import type { RegisteredTool } from "./config.ts";
12
12
  import * as Execution from "./execution.ts";
13
+ import * as Limits from "./limits.ts";
13
14
  import * as Paths from "./paths.ts";
14
15
  import * as Prompts from "./prompts.ts";
15
16
  import * as RecipeDiscovery from "./recipe-discovery.ts";
@@ -164,7 +165,8 @@ function compactAsyncRunStatus(value: unknown): string {
164
165
  ];
165
166
  if (status.tool) tokens.push(`tool=${String(status.tool)}`);
166
167
  if (status.recipe) tokens.push(`recipe=${String(status.recipe)}`);
167
- if (status.retire_when) tokens.push(`retire_when=${String(status.retire_when)}`);
168
+ if (status.retire_when)
169
+ tokens.push(`retire_when=${String(status.retire_when)}`);
168
170
  if (Number(status.pid) > 0) tokens.push(`pid=${Number(status.pid)}`);
169
171
  if (progress.phase && progress.phase !== status.status)
170
172
  tokens.push(`phase=${String(progress.phase)}`);
@@ -194,7 +196,10 @@ function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
194
196
  .join("\n")}`;
195
197
  }
196
198
 
197
- function compactPreview(value: unknown, maxLength = 80): string | undefined {
199
+ function compactPreview(
200
+ value: unknown,
201
+ maxLength = Limits.COMPACT_PREVIEW_CHARS,
202
+ ): string | undefined {
198
203
  if (value === undefined) return undefined;
199
204
  const text =
200
205
  typeof value === "string" ? value : JSON.stringify(value, undefined, 0);
@@ -204,7 +209,9 @@ function compactPreview(value: unknown, maxLength = 80): string | undefined {
204
209
  : compact;
205
210
  }
206
211
 
207
- function compactRoomPreviews(previews: ActorRooms.RoomMessagePreview[]): string {
212
+ function compactRoomPreviews(
213
+ previews: ActorRooms.RoomMessagePreview[],
214
+ ): string {
208
215
  if (previews.length === 0) return "\n(no room message previews)";
209
216
  return `\n${previews
210
217
  .map((preview) =>
@@ -214,7 +221,9 @@ function compactRoomPreviews(previews: ActorRooms.RoomMessagePreview[]): string
214
221
  `to=${preview.to}`,
215
222
  `type=${preview.type}`,
216
223
  preview.summary ? `summary=${compactPreview(preview.summary)}` : "",
217
- preview.body_preview ? `body=${compactPreview(preview.body_preview)}` : "",
224
+ preview.body_preview
225
+ ? `body=${compactPreview(preview.body_preview)}`
226
+ : "",
218
227
  ]
219
228
  .filter(Boolean)
220
229
  .join(" "),
@@ -232,7 +241,9 @@ function compactRoomMessages(messages: ActorRooms.RoomTimelineEntry[]): string {
232
241
  `to=${message.to}`,
233
242
  `type=${message.type}`,
234
243
  `summary=${String(message.summary ?? "").replaceAll(/\s+/g, "_")}`,
235
- compactPreview(message.body) ? `body=${compactPreview(message.body)}` : "",
244
+ compactPreview(message.body)
245
+ ? `body=${compactPreview(message.body)}`
246
+ : "",
236
247
  ]
237
248
  .filter(Boolean)
238
249
  .join(" "),
@@ -248,8 +259,12 @@ function compactRoomContacts(contacts: ActorRooms.RoomContact[]): string {
248
259
  `address=${contact.address}`,
249
260
  contact.role !== undefined ? `role=${String(contact.role)}` : "",
250
261
  contact.parent !== undefined ? `parent=${String(contact.parent)}` : "",
251
- contact.caps !== undefined ? `caps=${Array.isArray(contact.caps) ? contact.caps.join(",") : String(contact.caps)}` : "",
252
- contact.claim !== undefined ? `claim=${String(contact.claim).replaceAll(/\s+/g, "_")}` : "",
262
+ contact.caps !== undefined
263
+ ? `caps=${Array.isArray(contact.caps) ? contact.caps.join(",") : String(contact.caps)}`
264
+ : "",
265
+ contact.claim !== undefined
266
+ ? `claim=${String(contact.claim).replaceAll(/\s+/g, "_")}`
267
+ : "",
253
268
  contact.status !== undefined ? `status=${String(contact.status)}` : "",
254
269
  ]
255
270
  .filter(Boolean)
@@ -258,7 +273,9 @@ function compactRoomContacts(contacts: ActorRooms.RoomContact[]): string {
258
273
  .join("\n")}`;
259
274
  }
260
275
 
261
- function compactRoomRoster(roster: Record<string, ActorRooms.RoomMember>): string {
276
+ function compactRoomRoster(
277
+ roster: Record<string, ActorRooms.RoomMember>,
278
+ ): string {
262
279
  const members = Object.values(roster);
263
280
  if (members.length === 0) return "\n(no room members)";
264
281
  return `\n${members
@@ -267,8 +284,12 @@ function compactRoomRoster(roster: Record<string, ActorRooms.RoomMember>): strin
267
284
  `address=${member.address}`,
268
285
  `role=${String(member.role ?? "")}`,
269
286
  member.parent !== undefined ? `parent=${String(member.parent)}` : "",
270
- member.caps !== undefined ? `caps=${Array.isArray(member.caps) ? member.caps.join(",") : String(member.caps)}` : "",
271
- member.claim !== undefined ? `claim=${String(member.claim).replaceAll(/\s+/g, "_")}` : "",
287
+ member.caps !== undefined
288
+ ? `caps=${Array.isArray(member.caps) ? member.caps.join(",") : String(member.caps)}`
289
+ : "",
290
+ member.claim !== undefined
291
+ ? `claim=${String(member.claim).replaceAll(/\s+/g, "_")}`
292
+ : "",
272
293
  `status=${String(member.status ?? "")}`,
273
294
  `last_seen=${member.last_seen}`,
274
295
  ].join(" "),
@@ -277,7 +298,7 @@ function compactRoomRoster(roster: Record<string, ActorRooms.RoomMember>): strin
277
298
  }
278
299
 
279
300
  function compactRoomStatus(status: ActorRooms.RoomStatus): string {
280
- return `\nroom=${status.room} messages=${status.message_count} roster=${status.roster_count}${status.last_message_at ? ` last_message_at=${status.last_message_at}` : ""}${status.last_message_from ? ` last_from=${status.last_message_from}` : ""}${status.last_message_type ? ` last_type=${status.last_message_type}` : ""}${status.last_message_summary ? ` last_summary=${compactPreview(status.last_message_summary)}` : ""}`;
301
+ return `\nroom=${status.room} messages=${status.message_count} roster=${status.roster_count}${status.diagnostics_count ? ` diagnostics=${status.diagnostics_count}` : ""}${status.last_message_at ? ` last_message_at=${status.last_message_at}` : ""}${status.last_message_from ? ` last_from=${status.last_message_from}` : ""}${status.last_message_type ? ` last_type=${status.last_message_type}` : ""}${status.last_message_summary ? ` last_summary=${compactPreview(status.last_message_summary)}` : ""}`;
281
302
  }
282
303
 
283
304
  function compactCommunicationSnapshot(
@@ -300,11 +321,19 @@ function compactInboxMessages(
300
321
  `type=${String(message.type ?? "")}`,
301
322
  `from=${String(message.from ?? "")}`,
302
323
  `to=${String(message.to ?? "")}`,
303
- ...(message.queued_at ? [`queued_at=${String(message.queued_at)}`] : []),
324
+ ...(message.queued_at
325
+ ? [`queued_at=${String(message.queued_at)}`]
326
+ : []),
304
327
  ...(message.sent_at ? [`sent_at=${String(message.sent_at)}`] : []),
305
- ...(message.claimed_at ? [`claimed_at=${String(message.claimed_at)}`] : []),
306
- ...(message.handled_at ? [`handled_at=${String(message.handled_at)}`] : []),
307
- ...(message.failed_at ? [`failed_at=${String(message.failed_at)}`] : []),
328
+ ...(message.claimed_at
329
+ ? [`claimed_at=${String(message.claimed_at)}`]
330
+ : []),
331
+ ...(message.handled_at
332
+ ? [`handled_at=${String(message.handled_at)}`]
333
+ : []),
334
+ ...(message.failed_at
335
+ ? [`failed_at=${String(message.failed_at)}`]
336
+ : []),
308
337
  ].join(" "),
309
338
  )
310
339
  .join("\n")}`;
@@ -314,12 +343,64 @@ function compactBranchInbox(messages: Array<Record<string, unknown>>): string {
314
343
  return compactInboxMessages(messages, "branch inbox");
315
344
  }
316
345
 
346
+ function normalizeMailboxEntry(
347
+ value: unknown,
348
+ ): Record<string, unknown> | undefined {
349
+ if (typeof value === "string") return { type: value };
350
+ const record = asRecord(value);
351
+ if (typeof record.type !== "string" || !record.type.trim()) return undefined;
352
+ return {
353
+ type: record.type.trim(),
354
+ ...(record.body_schema !== undefined
355
+ ? { body_schema: record.body_schema }
356
+ : {}),
357
+ ...(record.ack !== undefined ? { ack: record.ack } : {}),
358
+ ...(typeof record.idempotency === "string"
359
+ ? { idempotency: record.idempotency }
360
+ : {}),
361
+ ...(typeof record.level === "string" ? { level: record.level } : {}),
362
+ ...(record.requires_response === true ? { requires_response: true } : {}),
363
+ ...(typeof record.summary === "string" ? { summary: record.summary } : {}),
364
+ };
365
+ }
366
+
367
+ function normalizeMailboxContracts(
368
+ mailbox: Record<string, unknown>,
369
+ ): Record<string, unknown[]> {
370
+ const accepts = Array.isArray(mailbox.accepts)
371
+ ? mailbox.accepts
372
+ .map(normalizeMailboxEntry)
373
+ .filter((entry): entry is Record<string, unknown> => Boolean(entry))
374
+ : [];
375
+ const emits = Array.isArray(mailbox.emits)
376
+ ? mailbox.emits
377
+ .map(normalizeMailboxEntry)
378
+ .filter((entry): entry is Record<string, unknown> => Boolean(entry))
379
+ : [];
380
+ return { accepts, emits };
381
+ }
382
+
383
+ function mailboxTypes(entries: unknown[]): string[] {
384
+ return entries.flatMap((entry) =>
385
+ typeof asRecord(entry).type === "string"
386
+ ? [String(asRecord(entry).type)]
387
+ : [],
388
+ );
389
+ }
390
+
317
391
  function compactRunMailbox(
318
392
  run: string,
319
393
  mailbox: Record<string, unknown>,
320
394
  messages: Array<Record<string, unknown>>,
321
395
  ): string {
322
- return `\nrun=${run} accepts=${Array.isArray(mailbox.accepts) ? mailbox.accepts.join(",") : ""} emits=${Array.isArray(mailbox.emits) ? mailbox.emits.join(",") : ""}${compactInboxMessages(messages, "run inbox")}`;
396
+ const normalized = normalizeMailboxContracts(mailbox);
397
+ return `\nrun=${run} accepts=${mailboxTypes(normalized.accepts).join(",")} emits=${mailboxTypes(normalized.emits).join(",")}${compactInboxMessages(messages, "run inbox")}`;
398
+ }
399
+
400
+ function compactArtifactPath(value: unknown): string {
401
+ if (typeof value === "string") return value;
402
+ const record = asRecord(value);
403
+ return String(record.path ?? "<missing>");
323
404
  }
324
405
 
325
406
  function compactActorFiles(status: Record<string, unknown>): string {
@@ -330,12 +411,14 @@ function compactActorFiles(status: Record<string, unknown>): string {
330
411
  status.stderrLog,
331
412
  status.eventsFile,
332
413
  status.outboxFile,
333
- status.state_dir ? `${String(status.state_dir)}/communication.json` : undefined,
414
+ status.state_dir
415
+ ? `${String(status.state_dir)}/communication.json`
416
+ : undefined,
334
417
  status.state_dir ? `${String(status.state_dir)}/result.json` : undefined,
335
418
  ].filter((file): file is string => typeof file === "string");
336
419
  const artifactText = Object.keys(artifacts).length
337
420
  ? ` artifacts=${Object.entries(artifacts)
338
- .map(([key, value]) => `${key}:${String(value)}`)
421
+ .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
339
422
  .join(",")}`
340
423
  : "";
341
424
  return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
@@ -353,7 +436,8 @@ function compactSessionRuns(
353
436
  `status=${String(run.status ?? "")}`,
354
437
  ];
355
438
  if (run.recipe) tokens.push(`recipe=${String(run.recipe)}`);
356
- if (run.retire_when) tokens.push(`retire_when=${String(run.retire_when)}`);
439
+ if (run.retire_when)
440
+ tokens.push(`retire_when=${String(run.retire_when)}`);
357
441
  return tokens.join(" ");
358
442
  })
359
443
  .join("\n")}`;
@@ -368,11 +452,53 @@ function compactToolActor(name: string, tool: Record<string, unknown>): string {
368
452
  return `\ntool=${name} description=${String(tool.description ?? "").replaceAll(/\s+/g, "_")} args=${Object.keys(properties).join(",")} required=${required}`;
369
453
  }
370
454
 
455
+ function compactRecipeImports(summary: Record<string, unknown>): string {
456
+ const active = Array.isArray(summary.active)
457
+ ? summary.active as Array<Record<string, unknown>>
458
+ : [];
459
+ const lines = active.flatMap((entry) => {
460
+ const imports = asRecord(entry.imports);
461
+ return Object.entries(imports).map(([alias, value]) => {
462
+ const binding = typeof value === "string" ? { from: value } : asRecord(value);
463
+ return `recipe=${String(entry.id ?? "<unknown>")} alias=${alias} from=${String(binding.from ?? value)}`;
464
+ });
465
+ });
466
+ return lines.length ? `\n${lines.join("\n")}` : "\n(no recipe imports)";
467
+ }
468
+
469
+ function compactRecipeDoctor(summary: Record<string, unknown>): string {
470
+ const details = Array.isArray(summary.diagnostic_details)
471
+ ? (summary.diagnostic_details as Array<Record<string, unknown>>)
472
+ : [];
473
+ const recommendations = Array.isArray(summary.recommendations)
474
+ ? (summary.recommendations as Array<Record<string, unknown>>)
475
+ : [];
476
+ const counts = { error: 0, info: 0, warning: 0 };
477
+ for (const detail of details) {
478
+ const severity = String(detail.severity ?? "info");
479
+ if (severity === "error" || severity === "warning" || severity === "info")
480
+ counts[severity] += 1;
481
+ }
482
+ const lines = [
483
+ `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} recommendations=${recommendations.length}`,
484
+ ];
485
+ for (const detail of details.slice(0, 8)) {
486
+ lines.push(
487
+ `${String(detail.severity ?? "info")} id=${String(detail.id ?? "root")} action=${compactPreview(detail.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS) ?? "inspect"}`,
488
+ );
489
+ }
490
+ return `\n${lines.join("\n")}`;
491
+ }
492
+
371
493
  function compactRecipeRegistry(summary: Record<string, unknown>): string {
372
494
  const active = Array.isArray(summary.active) ? summary.active.length : 0;
373
- const shadowed = Array.isArray(summary.shadowed) ? summary.shadowed.length : 0;
495
+ const shadowed = Array.isArray(summary.shadowed)
496
+ ? summary.shadowed.length
497
+ : 0;
374
498
  const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
375
- const disabled = Array.isArray(summary.disabled) ? summary.disabled.length : 0;
499
+ const disabled = Array.isArray(summary.disabled)
500
+ ? summary.disabled.length
501
+ : 0;
376
502
  const diagnostics = Array.isArray(summary.diagnostics)
377
503
  ? summary.diagnostics.length
378
504
  : 0;
@@ -537,6 +663,23 @@ function assertMessageSenderBelongsToRun(
537
663
  }
538
664
  }
539
665
 
666
+ async function routeBranchEnvelope(
667
+ stateDir: string,
668
+ runId: string,
669
+ recipient: string,
670
+ message: ActorMessages.ActorMessage,
671
+ _options: { source: "direct" | "room-multicast" },
672
+ ): Promise<Record<string, unknown>> {
673
+ const branchMessage = { ...message, to: recipient };
674
+ ActorRooms.appendBranchInboxMessage(
675
+ stateDir,
676
+ runId,
677
+ recipient,
678
+ branchMessage,
679
+ );
680
+ return AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
681
+ }
682
+
540
683
  function getRoomMulticastRecipients(
541
684
  message: ActorMessages.ActorMessage,
542
685
  run: string,
@@ -622,13 +765,21 @@ export function createSpawnToolDefinition<
622
765
  state_dir:
623
766
  typeof input.state_dir === "string" ? input.state_dir : undefined,
624
767
  ...(input.template !== undefined
625
- ? { template: input.template as AsyncRuns.AsyncRunStartParams["template"] }
768
+ ? {
769
+ template:
770
+ input.template as AsyncRuns.AsyncRunStartParams["template"],
771
+ }
626
772
  : {}),
627
773
  values: asRecord(input.values),
628
774
  ...(input.artifacts &&
629
775
  typeof input.artifacts === "object" &&
630
776
  !Array.isArray(input.artifacts)
631
- ? { artifacts: input.artifacts as Record<string, string> }
777
+ ? {
778
+ artifacts: input.artifacts as Record<
779
+ string,
780
+ AsyncRuns.RunArtifactDeclaration
781
+ >,
782
+ }
632
783
  : {}),
633
784
  },
634
785
  ctx.cwd,
@@ -688,7 +839,9 @@ function assertRunAccessibleToContext(
688
839
  return status;
689
840
  }
690
841
 
691
- function assertRunExistsForActorMessage(runId: string): Record<string, unknown> {
842
+ function assertRunExistsForActorMessage(
843
+ runId: string,
844
+ ): Record<string, unknown> {
692
845
  return AsyncRuns.getRunStatus(runId);
693
846
  }
694
847
 
@@ -729,11 +882,17 @@ export function createInspectToolDefinition<TContext = unknown>(
729
882
  const target = String(input.target ?? "");
730
883
  const view = String(input.view ?? "");
731
884
  if (target === "recipes" || target === "recipe-registry") {
732
- if (view !== "status" && view !== "summary") {
733
- throw new Error("inspect recipes supports view=status or view=summary.");
885
+ if (view !== "status" && view !== "summary" && view !== "doctor" && view !== "imports") {
886
+ throw new Error(
887
+ "inspect recipes supports view=status, view=summary, view=doctor, or view=imports.",
888
+ );
734
889
  }
735
890
  const discovered = RecipeDiscovery.discoverRecipeSources([
736
- { root: deps.recipeRoot ?? Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
891
+ {
892
+ root: deps.recipeRoot ?? Paths.getRecipeRoot(),
893
+ defaultTool: true,
894
+ mutableUsage: true,
895
+ },
737
896
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
738
897
  ]);
739
898
  const summary = RecipeDiscovery.summarizeDiscovery(discovered);
@@ -744,7 +903,11 @@ export function createInspectToolDefinition<TContext = unknown>(
744
903
  text: maybeJsonText(
745
904
  summary,
746
905
  input.verbose === true,
747
- compactRecipeRegistry(summary),
906
+ view === "doctor"
907
+ ? compactRecipeDoctor(summary)
908
+ : view === "imports"
909
+ ? compactRecipeImports(summary)
910
+ : compactRecipeRegistry(summary),
748
911
  ),
749
912
  },
750
913
  ],
@@ -838,7 +1001,8 @@ export function createInspectToolDefinition<TContext = unknown>(
838
1001
  if (address.kind === "room" && address.value && address.room) {
839
1002
  const status = assertRunAccessibleToContext(address.value, ctx);
840
1003
  const stateDir = String(status.state_dir ?? "");
841
- if (!stateDir) throw new Error(`room:${address.value} has no run state directory.`);
1004
+ if (!stateDir)
1005
+ throw new Error(`room:${address.value} has no run state directory.`);
842
1006
  if (view === "status") {
843
1007
  const status = ActorRooms.getRoomStatus(stateDir, address.room);
844
1008
  return {
@@ -859,7 +1023,7 @@ export function createInspectToolDefinition<TContext = unknown>(
859
1023
  const previews = ActorRooms.readRoomMessagePreviews(
860
1024
  stateDir,
861
1025
  address.room,
862
- Number(input.lines || 40),
1026
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
863
1027
  );
864
1028
  return {
865
1029
  content: [
@@ -879,7 +1043,7 @@ export function createInspectToolDefinition<TContext = unknown>(
879
1043
  const messages = ActorRooms.readRoomMessages(
880
1044
  stateDir,
881
1045
  address.room,
882
- Number(input.lines || 40),
1046
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
883
1047
  );
884
1048
  return {
885
1049
  content: [
@@ -927,21 +1091,29 @@ export function createInspectToolDefinition<TContext = unknown>(
927
1091
  details: { roster },
928
1092
  };
929
1093
  }
930
- throw new Error("inspect room:<run> supports view=status, view=messages, view=previews, view=roster, or view=contacts.");
1094
+ throw new Error(
1095
+ "inspect room:<run> supports view=status, view=messages, view=previews, view=roster, or view=contacts.",
1096
+ );
931
1097
  }
932
- const runId = address.kind === "run" || address.kind === "branch" ? address.value : undefined;
1098
+ const runId =
1099
+ address.kind === "run" || address.kind === "branch"
1100
+ ? address.value
1101
+ : undefined;
933
1102
  if (!runId)
934
1103
  throw new Error(
935
1104
  "inspect target must be run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>.",
936
1105
  );
937
1106
  if (address.kind === "branch") {
938
- if (view !== "mailbox") throw new Error("inspect branch:<run>/<branch> supports view=mailbox.");
1107
+ if (view !== "mailbox")
1108
+ throw new Error(
1109
+ "inspect branch:<run>/<branch> supports view=mailbox.",
1110
+ );
939
1111
  const status = assertRunAccessibleToContext(runId, ctx);
940
1112
  const branchInbox = ActorRooms.readBranchInboxDiagnostics(
941
1113
  String(status.state_dir ?? ""),
942
1114
  runId,
943
1115
  target,
944
- Number(input.lines || 40),
1116
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
945
1117
  );
946
1118
  const messages = branchInbox.messages;
947
1119
  return {
@@ -977,7 +1149,10 @@ export function createInspectToolDefinition<TContext = unknown>(
977
1149
  }
978
1150
  case "tail": {
979
1151
  assertRunAccessibleToContext(runId, ctx);
980
- const text = AsyncRuns.tailRun(runId, Number(input.lines || 40));
1152
+ const text = AsyncRuns.tailRun(
1153
+ runId,
1154
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1155
+ );
981
1156
  return {
982
1157
  content: [{ type: "text" as const, text: `\n${text}` }],
983
1158
  details: {},
@@ -987,7 +1162,7 @@ export function createInspectToolDefinition<TContext = unknown>(
987
1162
  assertRunAccessibleToContext(runId, ctx);
988
1163
  const messages = AsyncRuns.readRunEvents(
989
1164
  runId,
990
- Number(input.lines || 40),
1165
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
991
1166
  );
992
1167
  return {
993
1168
  content: [
@@ -1006,28 +1181,41 @@ export function createInspectToolDefinition<TContext = unknown>(
1006
1181
  case "artifacts":
1007
1182
  case "files": {
1008
1183
  const status = assertRunAccessibleToContext(runId, ctx);
1184
+ const artifactManifest = AsyncRuns.resolveArtifactManifest(
1185
+ status.artifacts as
1186
+ | Record<string, AsyncRuns.RunArtifactDeclaration>
1187
+ | undefined,
1188
+ );
1189
+ const details = artifactManifest
1190
+ ? { ...status, artifact_manifest: artifactManifest }
1191
+ : status;
1009
1192
  return {
1010
1193
  content: [
1011
1194
  {
1012
1195
  type: "text" as const,
1013
1196
  text: maybeJsonText(
1014
- status,
1197
+ details,
1015
1198
  input.verbose === true,
1016
1199
  compactActorFiles(status),
1017
1200
  ),
1018
1201
  },
1019
1202
  ],
1020
- details: status,
1203
+ details,
1021
1204
  };
1022
1205
  }
1023
1206
  case "mailbox": {
1024
1207
  const status = assertRunAccessibleToContext(runId, ctx);
1025
1208
  const mailbox = asRecord(status.mailbox);
1209
+ const normalizedMailbox = normalizeMailboxContracts(mailbox);
1026
1210
  const messages = AsyncRuns.readRunInboxMessages(
1027
1211
  runId,
1028
- Number(input.lines || 40),
1212
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1029
1213
  );
1030
- const details = { mailbox, messages };
1214
+ const details = {
1215
+ mailbox,
1216
+ normalized_mailbox: normalizedMailbox,
1217
+ messages,
1218
+ };
1031
1219
  return {
1032
1220
  content: [
1033
1221
  {
@@ -1035,7 +1223,11 @@ export function createInspectToolDefinition<TContext = unknown>(
1035
1223
  text: maybeJsonText(
1036
1224
  details,
1037
1225
  input.verbose === true,
1038
- compactRunMailbox(String(status.run ?? runId), mailbox, messages),
1226
+ compactRunMailbox(
1227
+ String(status.run ?? runId),
1228
+ mailbox,
1229
+ messages,
1230
+ ),
1039
1231
  ),
1040
1232
  },
1041
1233
  ],
@@ -1124,7 +1316,17 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1124
1316
  const address = ActorMessages.parseActorAddress(message.to);
1125
1317
  let result: Record<string, unknown>;
1126
1318
  if (address.kind === "run" && address.value) {
1127
- assertRunAccessibleToContext(address.value, ctx);
1319
+ const status = assertRunAccessibleToContext(address.value, ctx);
1320
+ const normalizedMailbox = normalizeMailboxContracts(
1321
+ asRecord(status.mailbox),
1322
+ );
1323
+ const acceptedTypes = new Set(mailboxTypes(normalizedMailbox.accepts));
1324
+ const advisoryWarnings =
1325
+ acceptedTypes.size > 0 && !acceptedTypes.has(message.type)
1326
+ ? [
1327
+ `Message type ${message.type} is not declared in mailbox.accepts for run:${address.value}.`,
1328
+ ]
1329
+ : [];
1128
1330
  if (
1129
1331
  message.type === "control.stop" ||
1130
1332
  message.type === "control.cancel"
@@ -1132,15 +1334,31 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1132
1334
  result = AsyncRuns.cancelRun(address.value);
1133
1335
  } else if (message.type === "control.kill") {
1134
1336
  result = AsyncRuns.killRun(address.value);
1337
+ } else if (message.type === "control.archive") {
1338
+ result = AsyncRuns.archiveRun(address.value);
1339
+ } else if (message.type === "control.prune") {
1340
+ const body = asRecord(message.body);
1341
+ result = AsyncRuns.pruneRun(address.value, {
1342
+ preserveArtifacts:
1343
+ body.preserve_artifacts === true ||
1344
+ body.preserveArtifacts === true,
1345
+ });
1135
1346
  } else {
1136
1347
  result = await AsyncRuns.sendRunMessage(
1137
1348
  address.value,
1138
1349
  messageBodyToRunLine(message),
1139
1350
  );
1140
1351
  }
1352
+ if (advisoryWarnings.length > 0)
1353
+ result = { ...result, warnings: advisoryWarnings };
1141
1354
  } else if (address.kind === "branch" && address.value) {
1142
1355
  const runId = address.value;
1143
- if (message.from) assertMessageSenderBelongsToRun(message, runId, `branch:${runId}/<branch>`);
1356
+ if (message.from)
1357
+ assertMessageSenderBelongsToRun(
1358
+ message,
1359
+ runId,
1360
+ `branch:${runId}/<branch>`,
1361
+ );
1144
1362
  const status = assertRunAccessibleToContext(runId, ctx);
1145
1363
  const stateDir = String(status.state_dir ?? "");
1146
1364
  if (stateDir && address.branch) {
@@ -1171,26 +1389,34 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1171
1389
  }
1172
1390
  }
1173
1391
  ActorRooms.writeCommunicationSnapshot(stateDir, runId);
1174
- ActorRooms.appendBranchInboxMessage(stateDir, runId, message.to, message);
1175
1392
  }
1176
- result = await AsyncRuns.sendRunMessage(
1177
- address.value,
1178
- JSON.stringify(message),
1393
+ result = await routeBranchEnvelope(
1394
+ stateDir,
1395
+ runId,
1396
+ message.to,
1397
+ message,
1398
+ {
1399
+ source: "direct",
1400
+ },
1179
1401
  );
1180
1402
  } else if (address.kind === "room" && address.value && address.room) {
1181
1403
  const runId = address.value;
1182
1404
  assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
1183
1405
  const status = assertRunAccessibleToContext(runId, ctx);
1184
1406
  const stateDir = String(status.state_dir ?? "");
1185
- if (!stateDir) throw new Error(`${message.to} has no run state directory.`);
1407
+ if (!stateDir)
1408
+ throw new Error(`${message.to} has no run state directory.`);
1186
1409
  const recipients = getRoomMulticastRecipients(message, runId);
1187
- const roomResult = ActorRooms.appendRoomMessage(stateDir, address.room, message);
1410
+ const roomResult = ActorRooms.appendRoomMessage(
1411
+ stateDir,
1412
+ address.room,
1413
+ message,
1414
+ );
1188
1415
  await Promise.all(
1189
1416
  recipients.map((recipient) =>
1190
- AsyncRuns.sendRunMessage(
1191
- runId,
1192
- JSON.stringify({ ...message, to: recipient }),
1193
- ),
1417
+ routeBranchEnvelope(stateDir, runId, recipient, message, {
1418
+ source: "room-multicast",
1419
+ }),
1194
1420
  ),
1195
1421
  );
1196
1422
  result = {
@@ -1217,7 +1443,12 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1217
1443
  ctx,
1218
1444
  );
1219
1445
  } catch (error) {
1220
- throw formatToolActorFailure(address.value, message, toolParams, error);
1446
+ throw formatToolActorFailure(
1447
+ address.value,
1448
+ message,
1449
+ toolParams,
1450
+ error,
1451
+ );
1221
1452
  }
1222
1453
  result = {
1223
1454
  invoked: true,
@@ -1251,7 +1482,11 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1251
1482
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
1252
1483
  body: message.body,
1253
1484
  correlation_id: message.correlation_id,
1254
- delivery: address.kind === "session" ? "followup" : undefined,
1485
+ delivery:
1486
+ message.metadata?.requires_response === true ||
1487
+ address.kind === "session"
1488
+ ? "followup"
1489
+ : undefined,
1255
1490
  event: message.type,
1256
1491
  from: message.from,
1257
1492
  metadata: message.metadata,
@@ -1340,7 +1575,8 @@ export function createRuntimeToolDefinition(
1340
1575
  ctx: AsyncRunToolContext,
1341
1576
  ) {
1342
1577
  try {
1343
- if (cfg.sourcePath) RecipeUsage.recordRecipeLaunch(cfg.sourcePath);
1578
+ if (cfg.sourcePath)
1579
+ RecipeUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
1344
1580
  if (isAsyncRecipe) {
1345
1581
  const input = params as Record<string, unknown>;
1346
1582
  const { run_id, ...values } = input;
@@ -1364,7 +1600,10 @@ export function createRuntimeToolDefinition(
1364
1600
  ctx.cwd,
1365
1601
  );
1366
1602
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
1367
- ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
1603
+ ActorRooms.writeCommunicationSnapshot(
1604
+ meta.state_dir,
1605
+ String(meta.run),
1606
+ );
1368
1607
  return {
1369
1608
  content: [
1370
1609
  { type: "text" as const, text: compactAsyncRunStatus(meta) },