@llblab/pi-actors 0.22.5 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +1 -0
  2. package/BACKLOG.md +0 -220
  3. package/CHANGELOG.md +18 -1
  4. package/README.md +1 -1
  5. package/dist/index.js +15 -0
  6. package/dist/lib/actor-inspector-tui.d.ts +4 -0
  7. package/dist/lib/actor-inspector-tui.js +53 -12
  8. package/dist/lib/actor-rooms.d.ts +8 -0
  9. package/dist/lib/actor-rooms.js +61 -10
  10. package/dist/lib/async-runs.d.ts +34 -2
  11. package/dist/lib/async-runs.js +215 -22
  12. package/dist/lib/command-templates.js +5 -5
  13. package/dist/lib/limits.d.ts +11 -0
  14. package/dist/lib/limits.js +11 -0
  15. package/dist/lib/observability.js +6 -4
  16. package/dist/lib/output.js +4 -5
  17. package/dist/lib/recipe-discovery.js +1 -0
  18. package/dist/lib/recipe-references.d.ts +11 -2
  19. package/dist/lib/recipe-references.js +4 -2
  20. package/dist/lib/recipe-usage.d.ts +2 -1
  21. package/dist/lib/recipe-usage.js +15 -3
  22. package/dist/lib/runtime.js +12 -2
  23. package/dist/lib/tools.js +197 -38
  24. package/docs/async-runs.md +2 -1
  25. package/docs/template-recipes.md +1 -1
  26. package/docs/tool-registry.md +0 -1
  27. package/index.ts +18 -0
  28. package/lib/actor-inspector-tui.ts +131 -29
  29. package/lib/actor-rooms.ts +155 -44
  30. package/lib/async-runs.ts +290 -31
  31. package/lib/command-templates.ts +5 -5
  32. package/lib/limits.ts +12 -0
  33. package/lib/observability.ts +10 -4
  34. package/lib/output.ts +4 -6
  35. package/lib/recipe-discovery.ts +1 -0
  36. package/lib/recipe-references.ts +20 -4
  37. package/lib/recipe-usage.ts +31 -4
  38. package/lib/runtime.ts +31 -7
  39. package/lib/tools.ts +296 -57
  40. package/package.json +2 -1
  41. package/scripts/async-runner.mjs +6 -0
  42. package/scripts/conformance.mjs +47 -0
  43. package/scripts/coordinator.mjs +13 -0
  44. package/scripts/locker.mjs +13 -0
  45. package/scripts/music-player.mjs +21 -2
  46. package/scripts/recipe-utils.mjs +13 -0
  47. package/scripts/validate-recipe.mjs +13 -0
  48. package/skills/actors/SKILL.md +3 -3
  49. package/skills/swarm/SKILL.md +1 -1
@@ -28,7 +28,10 @@ function getRecipeFingerprint(raw) {
28
28
  const { usage: _usage, ...content } = raw;
29
29
  return createHash("sha256").update(stableStringify(content)).digest("hex");
30
30
  }
31
- export function recordRecipeLaunch(path, now = new Date()) {
31
+ function launchCounterKey(kind) {
32
+ return `${kind}_calls`;
33
+ }
34
+ export function recordRecipeLaunch(path, now = new Date(), kind = "direct") {
32
35
  if (!existsSync(path))
33
36
  return false;
34
37
  try {
@@ -37,16 +40,25 @@ export function recordRecipeLaunch(path, now = new Date()) {
37
40
  return false;
38
41
  const usage = isRecord(raw.usage) ? raw.usage : {};
39
42
  const fingerprint = getRecipeFingerprint(raw);
40
- const changed = typeof usage.fingerprint === "string" && usage.fingerprint !== fingerprint;
43
+ const changed = typeof usage.fingerprint === "string" &&
44
+ usage.fingerprint !== fingerprint;
41
45
  const nowIso = now.toISOString();
46
+ const counterKey = launchCounterKey(kind);
42
47
  writeJsonAtomic(path, {
43
48
  ...raw,
44
49
  usage: {
45
50
  ...usage,
46
51
  calls: (changed ? 0 : normalizeCalls(usage.calls)) + 1,
52
+ [counterKey]: (changed ? 0 : normalizeCalls(usage[counterKey])) + 1,
47
53
  last_called: nowIso,
54
+ launch_kind: kind,
48
55
  fingerprint,
49
- ...(changed ? { reset_at: nowIso } : {}),
56
+ ...(changed
57
+ ? {
58
+ reset_at: nowIso,
59
+ reset_reason: "recipe content fingerprint changed",
60
+ }
61
+ : {}),
50
62
  },
51
63
  });
52
64
  return true;
@@ -54,6 +54,15 @@ export function createAutoToolsRuntime(deps) {
54
54
  runtimeTools.add(cfg.name);
55
55
  runtimeToolFingerprints.set(cfg.name, fingerprint);
56
56
  }
57
+ function isStartupActionableRegistryWarning(warning) {
58
+ if (warning.includes("invokes bash;") &&
59
+ warning.includes("trusted executable content"))
60
+ return false;
61
+ if (warning.includes("invokes bash;") &&
62
+ warning.includes("shell scripts are trusted executable content"))
63
+ return false;
64
+ return true;
65
+ }
57
66
  function formatRecipeToolWarnings(warnings) {
58
67
  const shadowed = warnings.filter((warning) => warning.includes(" shadows "));
59
68
  const skipped = warnings.filter((warning) => warning.includes(" could not be exposed as a tool:"));
@@ -112,8 +121,9 @@ export function createAutoToolsRuntime(deps) {
112
121
  }
113
122
  registerRuntimeTool(cfg);
114
123
  }
115
- if (warnings.length > 0) {
116
- notify(ctx, formatRecipeToolWarnings(warnings), "warning");
124
+ const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
125
+ if (startupWarnings.length > 0) {
126
+ notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
117
127
  }
118
128
  }
119
129
  return {
package/dist/lib/tools.js CHANGED
@@ -7,6 +7,7 @@ import * as ActorMessages from "./actor-messages.js";
7
7
  import * as ActorRooms from "./actor-rooms.js";
8
8
  import * as AsyncRuns from "./async-runs.js";
9
9
  import * as Execution from "./execution.js";
10
+ import * as Limits from "./limits.js";
10
11
  import * as Paths from "./paths.js";
11
12
  import * as Prompts from "./prompts.js";
12
13
  import * as RecipeDiscovery from "./recipe-discovery.js";
@@ -154,7 +155,7 @@ function compactRunMessages(messages) {
154
155
  ].join(" "))
155
156
  .join("\n")}`;
156
157
  }
157
- function compactPreview(value, maxLength = 80) {
158
+ function compactPreview(value, maxLength = Limits.COMPACT_PREVIEW_CHARS) {
158
159
  if (value === undefined)
159
160
  return undefined;
160
161
  const text = typeof value === "string" ? value : JSON.stringify(value, undefined, 0);
@@ -173,7 +174,9 @@ function compactRoomPreviews(previews) {
173
174
  `to=${preview.to}`,
174
175
  `type=${preview.type}`,
175
176
  preview.summary ? `summary=${compactPreview(preview.summary)}` : "",
176
- preview.body_preview ? `body=${compactPreview(preview.body_preview)}` : "",
177
+ preview.body_preview
178
+ ? `body=${compactPreview(preview.body_preview)}`
179
+ : "",
177
180
  ]
178
181
  .filter(Boolean)
179
182
  .join(" "))
@@ -189,7 +192,9 @@ function compactRoomMessages(messages) {
189
192
  `to=${message.to}`,
190
193
  `type=${message.type}`,
191
194
  `summary=${String(message.summary ?? "").replaceAll(/\s+/g, "_")}`,
192
- compactPreview(message.body) ? `body=${compactPreview(message.body)}` : "",
195
+ compactPreview(message.body)
196
+ ? `body=${compactPreview(message.body)}`
197
+ : "",
193
198
  ]
194
199
  .filter(Boolean)
195
200
  .join(" "))
@@ -203,8 +208,12 @@ function compactRoomContacts(contacts) {
203
208
  `address=${contact.address}`,
204
209
  contact.role !== undefined ? `role=${String(contact.role)}` : "",
205
210
  contact.parent !== undefined ? `parent=${String(contact.parent)}` : "",
206
- contact.caps !== undefined ? `caps=${Array.isArray(contact.caps) ? contact.caps.join(",") : String(contact.caps)}` : "",
207
- contact.claim !== undefined ? `claim=${String(contact.claim).replaceAll(/\s+/g, "_")}` : "",
211
+ contact.caps !== undefined
212
+ ? `caps=${Array.isArray(contact.caps) ? contact.caps.join(",") : String(contact.caps)}`
213
+ : "",
214
+ contact.claim !== undefined
215
+ ? `claim=${String(contact.claim).replaceAll(/\s+/g, "_")}`
216
+ : "",
208
217
  contact.status !== undefined ? `status=${String(contact.status)}` : "",
209
218
  ]
210
219
  .filter(Boolean)
@@ -220,8 +229,12 @@ function compactRoomRoster(roster) {
220
229
  `address=${member.address}`,
221
230
  `role=${String(member.role ?? "")}`,
222
231
  member.parent !== undefined ? `parent=${String(member.parent)}` : "",
223
- member.caps !== undefined ? `caps=${Array.isArray(member.caps) ? member.caps.join(",") : String(member.caps)}` : "",
224
- member.claim !== undefined ? `claim=${String(member.claim).replaceAll(/\s+/g, "_")}` : "",
232
+ member.caps !== undefined
233
+ ? `caps=${Array.isArray(member.caps) ? member.caps.join(",") : String(member.caps)}`
234
+ : "",
235
+ member.claim !== undefined
236
+ ? `claim=${String(member.claim).replaceAll(/\s+/g, "_")}`
237
+ : "",
225
238
  `status=${String(member.status ?? "")}`,
226
239
  `last_seen=${member.last_seen}`,
227
240
  ].join(" "))
@@ -245,19 +258,72 @@ function compactInboxMessages(messages, emptyLabel) {
245
258
  `type=${String(message.type ?? "")}`,
246
259
  `from=${String(message.from ?? "")}`,
247
260
  `to=${String(message.to ?? "")}`,
248
- ...(message.queued_at ? [`queued_at=${String(message.queued_at)}`] : []),
261
+ ...(message.queued_at
262
+ ? [`queued_at=${String(message.queued_at)}`]
263
+ : []),
249
264
  ...(message.sent_at ? [`sent_at=${String(message.sent_at)}`] : []),
250
- ...(message.claimed_at ? [`claimed_at=${String(message.claimed_at)}`] : []),
251
- ...(message.handled_at ? [`handled_at=${String(message.handled_at)}`] : []),
252
- ...(message.failed_at ? [`failed_at=${String(message.failed_at)}`] : []),
265
+ ...(message.claimed_at
266
+ ? [`claimed_at=${String(message.claimed_at)}`]
267
+ : []),
268
+ ...(message.handled_at
269
+ ? [`handled_at=${String(message.handled_at)}`]
270
+ : []),
271
+ ...(message.failed_at
272
+ ? [`failed_at=${String(message.failed_at)}`]
273
+ : []),
253
274
  ].join(" "))
254
275
  .join("\n")}`;
255
276
  }
256
277
  function compactBranchInbox(messages) {
257
278
  return compactInboxMessages(messages, "branch inbox");
258
279
  }
280
+ function normalizeMailboxEntry(value) {
281
+ if (typeof value === "string")
282
+ return { type: value };
283
+ const record = asRecord(value);
284
+ if (typeof record.type !== "string" || !record.type.trim())
285
+ return undefined;
286
+ return {
287
+ type: record.type.trim(),
288
+ ...(record.body_schema !== undefined
289
+ ? { body_schema: record.body_schema }
290
+ : {}),
291
+ ...(record.ack !== undefined ? { ack: record.ack } : {}),
292
+ ...(typeof record.idempotency === "string"
293
+ ? { idempotency: record.idempotency }
294
+ : {}),
295
+ ...(typeof record.level === "string" ? { level: record.level } : {}),
296
+ ...(record.requires_response === true ? { requires_response: true } : {}),
297
+ ...(typeof record.summary === "string" ? { summary: record.summary } : {}),
298
+ };
299
+ }
300
+ function normalizeMailboxContracts(mailbox) {
301
+ const accepts = Array.isArray(mailbox.accepts)
302
+ ? mailbox.accepts
303
+ .map(normalizeMailboxEntry)
304
+ .filter((entry) => Boolean(entry))
305
+ : [];
306
+ const emits = Array.isArray(mailbox.emits)
307
+ ? mailbox.emits
308
+ .map(normalizeMailboxEntry)
309
+ .filter((entry) => Boolean(entry))
310
+ : [];
311
+ return { accepts, emits };
312
+ }
313
+ function mailboxTypes(entries) {
314
+ return entries.flatMap((entry) => typeof asRecord(entry).type === "string"
315
+ ? [String(asRecord(entry).type)]
316
+ : []);
317
+ }
259
318
  function compactRunMailbox(run, mailbox, messages) {
260
- return `\nrun=${run} accepts=${Array.isArray(mailbox.accepts) ? mailbox.accepts.join(",") : ""} emits=${Array.isArray(mailbox.emits) ? mailbox.emits.join(",") : ""}${compactInboxMessages(messages, "run inbox")}`;
319
+ const normalized = normalizeMailboxContracts(mailbox);
320
+ return `\nrun=${run} accepts=${mailboxTypes(normalized.accepts).join(",")} emits=${mailboxTypes(normalized.emits).join(",")}${compactInboxMessages(messages, "run inbox")}`;
321
+ }
322
+ function compactArtifactPath(value) {
323
+ if (typeof value === "string")
324
+ return value;
325
+ const record = asRecord(value);
326
+ return String(record.path ?? "<missing>");
261
327
  }
262
328
  function compactActorFiles(status) {
263
329
  const run = String(status.run ?? "<unknown>");
@@ -267,12 +333,14 @@ function compactActorFiles(status) {
267
333
  status.stderrLog,
268
334
  status.eventsFile,
269
335
  status.outboxFile,
270
- status.state_dir ? `${String(status.state_dir)}/communication.json` : undefined,
336
+ status.state_dir
337
+ ? `${String(status.state_dir)}/communication.json`
338
+ : undefined,
271
339
  status.state_dir ? `${String(status.state_dir)}/result.json` : undefined,
272
340
  ].filter((file) => typeof file === "string");
273
341
  const artifactText = Object.keys(artifacts).length
274
342
  ? ` artifacts=${Object.entries(artifacts)
275
- .map(([key, value]) => `${key}:${String(value)}`)
343
+ .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
276
344
  .join(",")}`
277
345
  : "";
278
346
  return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
@@ -302,11 +370,49 @@ function compactToolActor(name, tool) {
302
370
  const properties = asRecord(parameters.properties);
303
371
  return `\ntool=${name} description=${String(tool.description ?? "").replaceAll(/\s+/g, "_")} args=${Object.keys(properties).join(",")} required=${required}`;
304
372
  }
373
+ function compactRecipeImports(summary) {
374
+ const active = Array.isArray(summary.active)
375
+ ? summary.active
376
+ : [];
377
+ const lines = active.flatMap((entry) => {
378
+ const imports = asRecord(entry.imports);
379
+ return Object.entries(imports).map(([alias, value]) => {
380
+ const binding = typeof value === "string" ? { from: value } : asRecord(value);
381
+ return `recipe=${String(entry.id ?? "<unknown>")} alias=${alias} from=${String(binding.from ?? value)}`;
382
+ });
383
+ });
384
+ return lines.length ? `\n${lines.join("\n")}` : "\n(no recipe imports)";
385
+ }
386
+ function compactRecipeDoctor(summary) {
387
+ const details = Array.isArray(summary.diagnostic_details)
388
+ ? summary.diagnostic_details
389
+ : [];
390
+ const recommendations = Array.isArray(summary.recommendations)
391
+ ? summary.recommendations
392
+ : [];
393
+ const counts = { error: 0, info: 0, warning: 0 };
394
+ for (const detail of details) {
395
+ const severity = String(detail.severity ?? "info");
396
+ if (severity === "error" || severity === "warning" || severity === "info")
397
+ counts[severity] += 1;
398
+ }
399
+ const lines = [
400
+ `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} recommendations=${recommendations.length}`,
401
+ ];
402
+ for (const detail of details.slice(0, 8)) {
403
+ lines.push(`${String(detail.severity ?? "info")} id=${String(detail.id ?? "root")} action=${compactPreview(detail.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS) ?? "inspect"}`);
404
+ }
405
+ return `\n${lines.join("\n")}`;
406
+ }
305
407
  function compactRecipeRegistry(summary) {
306
408
  const active = Array.isArray(summary.active) ? summary.active.length : 0;
307
- const shadowed = Array.isArray(summary.shadowed) ? summary.shadowed.length : 0;
409
+ const shadowed = Array.isArray(summary.shadowed)
410
+ ? summary.shadowed.length
411
+ : 0;
308
412
  const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
309
- const disabled = Array.isArray(summary.disabled) ? summary.disabled.length : 0;
413
+ const disabled = Array.isArray(summary.disabled)
414
+ ? summary.disabled.length
415
+ : 0;
310
416
  const diagnostics = Array.isArray(summary.diagnostics)
311
417
  ? summary.diagnostics.length
312
418
  : 0;
@@ -420,6 +526,11 @@ function assertMessageSenderBelongsToRun(message, run, routeLabel) {
420
526
  throw new Error(`message to ${routeLabel} requires from=run:${run} or branch:${run}/<branch>; got ${message.from}.`);
421
527
  }
422
528
  }
529
+ async function routeBranchEnvelope(stateDir, runId, recipient, message, _options) {
530
+ const branchMessage = { ...message, to: recipient };
531
+ ActorRooms.appendBranchInboxMessage(stateDir, runId, recipient, branchMessage);
532
+ return AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
533
+ }
423
534
  function getRoomMulticastRecipients(message, run) {
424
535
  const raw = message.metadata?.recipients;
425
536
  if (raw === undefined)
@@ -471,13 +582,17 @@ export function createSpawnToolDefinition() {
471
582
  run_id: runId,
472
583
  state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
473
584
  ...(input.template !== undefined
474
- ? { template: input.template }
585
+ ? {
586
+ template: input.template,
587
+ }
475
588
  : {}),
476
589
  values: asRecord(input.values),
477
590
  ...(input.artifacts &&
478
591
  typeof input.artifacts === "object" &&
479
592
  !Array.isArray(input.artifacts)
480
- ? { artifacts: input.artifacts }
593
+ ? {
594
+ artifacts: input.artifacts,
595
+ }
481
596
  : {}),
482
597
  }, ctx.cwd);
483
598
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
@@ -532,11 +647,15 @@ export function createInspectToolDefinition(deps = {}) {
532
647
  const target = String(input.target ?? "");
533
648
  const view = String(input.view ?? "");
534
649
  if (target === "recipes" || target === "recipe-registry") {
535
- if (view !== "status" && view !== "summary") {
536
- throw new Error("inspect recipes supports view=status or view=summary.");
650
+ if (view !== "status" && view !== "summary" && view !== "doctor" && view !== "imports") {
651
+ throw new Error("inspect recipes supports view=status, view=summary, view=doctor, or view=imports.");
537
652
  }
538
653
  const discovered = RecipeDiscovery.discoverRecipeSources([
539
- { root: deps.recipeRoot ?? Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
654
+ {
655
+ root: deps.recipeRoot ?? Paths.getRecipeRoot(),
656
+ defaultTool: true,
657
+ mutableUsage: true,
658
+ },
540
659
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
541
660
  ]);
542
661
  const summary = RecipeDiscovery.summarizeDiscovery(discovered);
@@ -544,7 +663,11 @@ export function createInspectToolDefinition(deps = {}) {
544
663
  content: [
545
664
  {
546
665
  type: "text",
547
- text: maybeJsonText(summary, input.verbose === true, compactRecipeRegistry(summary)),
666
+ text: maybeJsonText(summary, input.verbose === true, view === "doctor"
667
+ ? compactRecipeDoctor(summary)
668
+ : view === "imports"
669
+ ? compactRecipeImports(summary)
670
+ : compactRecipeRegistry(summary)),
548
671
  },
549
672
  ],
550
673
  details: summary,
@@ -627,7 +750,7 @@ export function createInspectToolDefinition(deps = {}) {
627
750
  };
628
751
  }
629
752
  if (view === "previews") {
630
- const previews = ActorRooms.readRoomMessagePreviews(stateDir, address.room, Number(input.lines || 40));
753
+ const previews = ActorRooms.readRoomMessagePreviews(stateDir, address.room, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
631
754
  return {
632
755
  content: [
633
756
  {
@@ -639,7 +762,7 @@ export function createInspectToolDefinition(deps = {}) {
639
762
  };
640
763
  }
641
764
  if (view === "messages") {
642
- const messages = ActorRooms.readRoomMessages(stateDir, address.room, Number(input.lines || 40));
765
+ const messages = ActorRooms.readRoomMessages(stateDir, address.room, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
643
766
  return {
644
767
  content: [
645
768
  {
@@ -676,14 +799,16 @@ export function createInspectToolDefinition(deps = {}) {
676
799
  }
677
800
  throw new Error("inspect room:<run> supports view=status, view=messages, view=previews, view=roster, or view=contacts.");
678
801
  }
679
- const runId = address.kind === "run" || address.kind === "branch" ? address.value : undefined;
802
+ const runId = address.kind === "run" || address.kind === "branch"
803
+ ? address.value
804
+ : undefined;
680
805
  if (!runId)
681
806
  throw new Error("inspect target must be run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>.");
682
807
  if (address.kind === "branch") {
683
808
  if (view !== "mailbox")
684
809
  throw new Error("inspect branch:<run>/<branch> supports view=mailbox.");
685
810
  const status = assertRunAccessibleToContext(runId, ctx);
686
- const branchInbox = ActorRooms.readBranchInboxDiagnostics(String(status.state_dir ?? ""), runId, target, Number(input.lines || 40));
811
+ const branchInbox = ActorRooms.readBranchInboxDiagnostics(String(status.state_dir ?? ""), runId, target, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
687
812
  const messages = branchInbox.messages;
688
813
  return {
689
814
  content: [
@@ -710,7 +835,7 @@ export function createInspectToolDefinition(deps = {}) {
710
835
  }
711
836
  case "tail": {
712
837
  assertRunAccessibleToContext(runId, ctx);
713
- const text = AsyncRuns.tailRun(runId, Number(input.lines || 40));
838
+ const text = AsyncRuns.tailRun(runId, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
714
839
  return {
715
840
  content: [{ type: "text", text: `\n${text}` }],
716
841
  details: {},
@@ -718,7 +843,7 @@ export function createInspectToolDefinition(deps = {}) {
718
843
  }
719
844
  case "messages": {
720
845
  assertRunAccessibleToContext(runId, ctx);
721
- const messages = AsyncRuns.readRunEvents(runId, Number(input.lines || 40));
846
+ const messages = AsyncRuns.readRunEvents(runId, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
722
847
  return {
723
848
  content: [
724
849
  {
@@ -732,21 +857,30 @@ export function createInspectToolDefinition(deps = {}) {
732
857
  case "artifacts":
733
858
  case "files": {
734
859
  const status = assertRunAccessibleToContext(runId, ctx);
860
+ const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
861
+ const details = artifactManifest
862
+ ? { ...status, artifact_manifest: artifactManifest }
863
+ : status;
735
864
  return {
736
865
  content: [
737
866
  {
738
867
  type: "text",
739
- text: maybeJsonText(status, input.verbose === true, compactActorFiles(status)),
868
+ text: maybeJsonText(details, input.verbose === true, compactActorFiles(status)),
740
869
  },
741
870
  ],
742
- details: status,
871
+ details,
743
872
  };
744
873
  }
745
874
  case "mailbox": {
746
875
  const status = assertRunAccessibleToContext(runId, ctx);
747
876
  const mailbox = asRecord(status.mailbox);
748
- const messages = AsyncRuns.readRunInboxMessages(runId, Number(input.lines || 40));
749
- const details = { mailbox, messages };
877
+ const normalizedMailbox = normalizeMailboxContracts(mailbox);
878
+ const messages = AsyncRuns.readRunInboxMessages(runId, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
879
+ const details = {
880
+ mailbox,
881
+ normalized_mailbox: normalizedMailbox,
882
+ messages,
883
+ };
750
884
  return {
751
885
  content: [
752
886
  {
@@ -802,7 +936,14 @@ export function createActorMessageToolDefinition(deps = {}) {
802
936
  const address = ActorMessages.parseActorAddress(message.to);
803
937
  let result;
804
938
  if (address.kind === "run" && address.value) {
805
- assertRunAccessibleToContext(address.value, ctx);
939
+ const status = assertRunAccessibleToContext(address.value, ctx);
940
+ const normalizedMailbox = normalizeMailboxContracts(asRecord(status.mailbox));
941
+ const acceptedTypes = new Set(mailboxTypes(normalizedMailbox.accepts));
942
+ const advisoryWarnings = acceptedTypes.size > 0 && !acceptedTypes.has(message.type)
943
+ ? [
944
+ `Message type ${message.type} is not declared in mailbox.accepts for run:${address.value}.`,
945
+ ]
946
+ : [];
806
947
  if (message.type === "control.stop" ||
807
948
  message.type === "control.cancel") {
808
949
  result = AsyncRuns.cancelRun(address.value);
@@ -810,9 +951,21 @@ export function createActorMessageToolDefinition(deps = {}) {
810
951
  else if (message.type === "control.kill") {
811
952
  result = AsyncRuns.killRun(address.value);
812
953
  }
954
+ else if (message.type === "control.archive") {
955
+ result = AsyncRuns.archiveRun(address.value);
956
+ }
957
+ else if (message.type === "control.prune") {
958
+ const body = asRecord(message.body);
959
+ result = AsyncRuns.pruneRun(address.value, {
960
+ preserveArtifacts: body.preserve_artifacts === true ||
961
+ body.preserveArtifacts === true,
962
+ });
963
+ }
813
964
  else {
814
965
  result = await AsyncRuns.sendRunMessage(address.value, messageBodyToRunLine(message));
815
966
  }
967
+ if (advisoryWarnings.length > 0)
968
+ result = { ...result, warnings: advisoryWarnings };
816
969
  }
817
970
  else if (address.kind === "branch" && address.value) {
818
971
  const runId = address.value;
@@ -837,9 +990,10 @@ export function createActorMessageToolDefinition(deps = {}) {
837
990
  }
838
991
  }
839
992
  ActorRooms.writeCommunicationSnapshot(stateDir, runId);
840
- ActorRooms.appendBranchInboxMessage(stateDir, runId, message.to, message);
841
993
  }
842
- result = await AsyncRuns.sendRunMessage(address.value, JSON.stringify(message));
994
+ result = await routeBranchEnvelope(stateDir, runId, message.to, message, {
995
+ source: "direct",
996
+ });
843
997
  }
844
998
  else if (address.kind === "room" && address.value && address.room) {
845
999
  const runId = address.value;
@@ -850,7 +1004,9 @@ export function createActorMessageToolDefinition(deps = {}) {
850
1004
  throw new Error(`${message.to} has no run state directory.`);
851
1005
  const recipients = getRoomMulticastRecipients(message, runId);
852
1006
  const roomResult = ActorRooms.appendRoomMessage(stateDir, address.room, message);
853
- await Promise.all(recipients.map((recipient) => AsyncRuns.sendRunMessage(runId, JSON.stringify({ ...message, to: recipient }))));
1007
+ await Promise.all(recipients.map((recipient) => routeBranchEnvelope(stateDir, runId, recipient, message, {
1008
+ source: "room-multicast",
1009
+ })));
854
1010
  result = {
855
1011
  ...roomResult,
856
1012
  ...(recipients.length > 0
@@ -898,7 +1054,10 @@ export function createActorMessageToolDefinition(deps = {}) {
898
1054
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
899
1055
  body: message.body,
900
1056
  correlation_id: message.correlation_id,
901
- delivery: address.kind === "session" ? "followup" : undefined,
1057
+ delivery: message.metadata?.requires_response === true ||
1058
+ address.kind === "session"
1059
+ ? "followup"
1060
+ : undefined,
902
1061
  event: message.type,
903
1062
  from: message.from,
904
1063
  metadata: message.metadata,
@@ -965,7 +1124,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
965
1124
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
966
1125
  try {
967
1126
  if (cfg.sourcePath)
968
- RecipeUsage.recordRecipeLaunch(cfg.sourcePath);
1127
+ RecipeUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
969
1128
  if (isAsyncRecipe) {
970
1129
  const input = params;
971
1130
  const { run_id, ...values } = input;
@@ -166,8 +166,9 @@ Low-level async actions map into the actor surface instead of forming a second p
166
166
  - Send/control → `message`
167
167
  - Status/tail/messages/list → `inspect`
168
168
  - Stop/kill → `message` with `control.stop` or `control.kill`, with synchronous results
169
+ - Archive/prune terminal state → `message` with `control.archive` or `control.prune`, with active runs rejected fail-closed
169
170
 
170
- Compact text is returned by default so async management does not flood agent context; use verbose inspection when the full state object is needed. List output intentionally shares one state root across music, subagents, timers, and other async work; source fields such as `tool` and `recipe` distinguish run purpose when the launcher recorded them. Registered tools are the preferred user-facing surface for reusable recipes.
171
+ Compact text is returned by default so async management does not flood agent context; use verbose inspection when the full state object is needed. List output intentionally shares one state root across music, subagents, timers, and other async work; source fields such as `tool` and `recipe` distinguish run purpose when the launcher recorded them. The run root may contain a rebuildable `index.json` with run id, state directory, owner, status, update time, and recipe/tool hints; corrupt indexes fall back to recursive scan. Registered tools are the preferred user-facing surface for reusable recipes. `control.prune` accepts `body.preserve_artifacts=true` to copy existing named artifacts beside the run root before deleting terminal state.
171
172
 
172
173
  ## Run-Local Messages
173
174
 
@@ -162,7 +162,7 @@ Use recipe-level `mailbox` to document the semantic messages a recipe actor acce
162
162
  }
163
163
  ```
164
164
 
165
- `mailbox` is contract metadata, not transport configuration. It should name semantic message types, not transport commands, file paths, or CLI fragments.
165
+ `mailbox` is contract metadata, not transport configuration. It should name semantic message types, not transport commands, file paths, or CLI fragments. Entries may be strings or typed objects such as `{ "type": "task.assign", "requires_response": true, "summary": "Assign work" }`; inspection normalizes both forms. Acceptance contracts are advisory by default: messages outside `mailbox.accepts` produce warnings rather than hard routing failures.
166
166
 
167
167
  ## Actor Recipe Context
168
168
 
@@ -15,7 +15,6 @@ The registry source is location-discovered recipes, not a live tool-only JSON fi
15
15
  - Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` and `docs_review.md` both have id/tool name `docs_review`.
16
16
  - Same-id JSON shadows Markdown in the same priority layer.
17
17
 
18
-
19
18
  Because the user recipe directory is sticky agent muscle memory, runtime launches update `usage.calls`, `usage.last_called`, and a content `usage.fingerprint` on user-owned recipe files. If authored recipe content changes, the next launch resets `usage.calls` and records `usage.reset_at` before counting the launch, so usage evidence follows the current recipe meaning rather than an older file history. `inspect target=recipes view=summary verbose=true` includes usage metadata and operator-gated cleanup recommendations for invalid, shadowed, disabled, component-only, unused, or overriding recipes. Recommended actions stay explicit: keep as a tool/component, enable, merge, fix, delete, or archive. The extension does not maintain a failure counter and agents should not silently clean tools during unrelated work.
20
19
 
21
20
  `register_tool` is the preferred agent-facing mutation API. It creates, updates, and deletes recipe files in `~/.pi/agent/recipes`; agents do not need to edit the files directly for normal registration. Direct file edits are still valid for operators and advanced agents. Runtime behavior is reactive: file creation, deletion, or edits in the user recipe root trigger validation and tool-set refresh, with invalid recipes surfaced as diagnostics rather than silently ignored.
package/index.ts CHANGED
@@ -60,6 +60,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
60
60
  let actorInspectorUnreadOnly = false;
61
61
  let actorInspectorRoomLimitPerRun = 12;
62
62
  let selectedInspectorSequence: number | undefined;
63
+ const actorInspectorReadKeys = new Set<string>();
63
64
  let recipeWatcherFailureNotified = false;
64
65
  const getRunOwnerId = (ctx: ExtensionContext): string =>
65
66
  ctx.sessionManager.getSessionId();
@@ -108,6 +109,7 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
108
109
  branch: actorInspectorBranch,
109
110
  mention: actorInspectorMention,
110
111
  ownerId,
112
+ readKeys: actorInspectorReadKeys,
111
113
  roomLimitPerRun: actorInspectorRoomLimitPerRun,
112
114
  unreadOnly: actorInspectorUnreadOnly,
113
115
  },
@@ -414,6 +416,22 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
414
416
  ctx.ui.notify("Usage: /actors-inspect <number>", "warning");
415
417
  return;
416
418
  }
419
+ const previews = ActorInspectorTui.readActorInspectorPreviews(
420
+ RUN_STATE_ROOT,
421
+ actorInspectorRows,
422
+ {
423
+ channels: actorInspectorChannels,
424
+ currentRunOnly: true,
425
+ branch: actorInspectorBranch,
426
+ mention: actorInspectorMention,
427
+ ownerId: getRunOwnerId(ctx),
428
+ readKeys: actorInspectorReadKeys,
429
+ roomLimitPerRun: actorInspectorRoomLimitPerRun,
430
+ unreadOnly: actorInspectorUnreadOnly,
431
+ },
432
+ );
433
+ const preview = previews.find((item) => item.sequence === sequence);
434
+ if (preview) actorInspectorReadKeys.add(ActorInspectorTui.inspectorPreviewReadKey(preview));
417
435
  selectedInspectorSequence = sequence;
418
436
  communicationWidgetVisible = true;
419
437
  updateRunUi(ctx);