@llblab/pi-actors 0.14.3 → 0.16.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 (76) hide show
  1. package/AGENTS.md +5 -1
  2. package/BACKLOG.md +54 -32
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +53 -61
  5. package/banner.jpg +0 -0
  6. package/docs/actor-messages.md +1 -1
  7. package/docs/async-runs.md +4 -4
  8. package/docs/command-templates.md +11 -11
  9. package/docs/recipe-library.md +7 -3
  10. package/docs/task-first-recipes.md +44 -43
  11. package/docs/template-recipes.md +45 -23
  12. package/docs/tool-registry.md +50 -42
  13. package/index.ts +34 -0
  14. package/lib/actor-messages.ts +20 -7
  15. package/lib/async-runs.ts +35 -12
  16. package/lib/command-templates.ts +6 -1
  17. package/lib/config.ts +3 -2
  18. package/lib/execution.ts +9 -5
  19. package/lib/observability.ts +20 -10
  20. package/lib/paths.ts +6 -1
  21. package/lib/prompts.ts +14 -21
  22. package/lib/recipe-discovery.ts +226 -0
  23. package/lib/recipe-migration.ts +123 -0
  24. package/lib/recipe-references.ts +45 -13
  25. package/lib/recipe-usage.ts +44 -0
  26. package/lib/registry.ts +48 -15
  27. package/lib/runtime.ts +59 -15
  28. package/lib/tools.ts +237 -65
  29. package/package.json +21 -11
  30. package/recipes/coordinator-locker.json +46 -0
  31. package/recipes/music-player.json +16 -2
  32. package/recipes/pipeline-architect-coordinator.json +11 -3
  33. package/recipes/pipeline-artifact-bundle.json +12 -3
  34. package/recipes/pipeline-artifact-report.json +9 -3
  35. package/recipes/pipeline-artifact-write.json +9 -3
  36. package/recipes/pipeline-async-run-ops.json +18 -9
  37. package/recipes/pipeline-checkpoint-continuation.json +14 -3
  38. package/recipes/pipeline-development-tasking.json +12 -3
  39. package/recipes/pipeline-docs-maintenance.json +12 -3
  40. package/recipes/pipeline-media-library.json +12 -3
  41. package/recipes/pipeline-quorum-review.json +12 -9
  42. package/recipes/pipeline-release-readiness.json +27 -9
  43. package/recipes/pipeline-release-summary.json +89 -0
  44. package/recipes/pipeline-repo-health.json +12 -3
  45. package/recipes/pipeline-research-synthesis.json +11 -3
  46. package/recipes/pipeline-review-readiness.json +12 -6
  47. package/recipes/subagent-artifact.json +9 -3
  48. package/recipes/subagent-checkpoint.json +10 -3
  49. package/recipes/subagent-conflict-report.json +11 -3
  50. package/recipes/subagent-contradiction-map.json +11 -3
  51. package/recipes/subagent-critic.json +11 -3
  52. package/recipes/subagent-evidence-map.json +11 -3
  53. package/recipes/subagent-followup.json +10 -3
  54. package/recipes/subagent-judge.json +11 -3
  55. package/recipes/subagent-merge.json +11 -3
  56. package/recipes/subagent-message.json +8 -3
  57. package/recipes/subagent-normalize.json +11 -3
  58. package/recipes/subagent-plan.json +11 -3
  59. package/recipes/subagent-prompt.json +10 -3
  60. package/recipes/subagent-quorum.json +10 -7
  61. package/recipes/subagent-review-coordinator.json +14 -6
  62. package/recipes/subagent-review.json +11 -3
  63. package/recipes/subagent-task-card.json +11 -3
  64. package/recipes/subagent-tools.json +10 -3
  65. package/recipes/subagent-verify.json +11 -3
  66. package/recipes/subagents-prompts.json +10 -3
  67. package/recipes/utility-coordinator-lock-snapshot.json +14 -0
  68. package/recipes/utility-run-ops-snapshot.json +3 -3
  69. package/recipes/utility-skill-summary.json +14 -0
  70. package/scripts/coordinator-locker.mjs +272 -0
  71. package/scripts/music-player.mjs +2 -1
  72. package/scripts/recipe-utils.mjs +239 -81
  73. package/scripts/validate-recipe.mjs +28 -10
  74. package/skills/actors/SKILL.md +301 -0
  75. package/skills/swarm/SKILL.md +451 -0
  76. package/skills/swarm/references/development-swarm.md +596 -0
package/lib/tools.ts CHANGED
@@ -4,13 +4,16 @@
4
4
  * Owns generated runtime tool schemas and the register_tool management tool schema
5
5
  */
6
6
 
7
- import type { RegisteredTool } from "./config.ts";
8
7
  import * as ActorMessages from "./actor-messages.ts";
9
8
  import * as AsyncRuns from "./async-runs.ts";
10
9
  import * as CommandTemplates from "./command-templates.ts";
10
+ import type { RegisteredTool } from "./config.ts";
11
11
  import * as Execution from "./execution.ts";
12
+ import * as Paths from "./paths.ts";
12
13
  import * as Prompts from "./prompts.ts";
14
+ import * as RecipeDiscovery from "./recipe-discovery.ts";
13
15
  import * as RecipeReferences from "./recipe-references.ts";
16
+ import * as RecipeUsage from "./recipe-usage.ts";
14
17
  import * as Registry from "./registry.ts";
15
18
  import * as Schema from "./schema.ts";
16
19
 
@@ -86,8 +89,7 @@ function sampleValueForArg(
86
89
  function shouldAddRuntimeToolUsageHint(error: unknown): boolean {
87
90
  const message = error instanceof Error ? error.message : String(error);
88
91
  return (
89
- /^Argument \S+ must /.test(message) ||
90
- /^Missing .* value: /.test(message)
92
+ /^Argument \S+ must /.test(message) || /^Missing .* value: /.test(message)
91
93
  );
92
94
  }
93
95
 
@@ -176,16 +178,15 @@ function compactAsyncRunStatus(value: unknown): string {
176
178
  return `\n${tokens.join(" ")}`;
177
179
  }
178
180
 
179
- function compactRunEvents(events: AsyncRuns.RunOutboxEvent[]): string {
180
- if (events.length === 0) return "\n(no run events)";
181
- return `\n${events
182
- .map((event) =>
181
+ function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
182
+ if (messages.length === 0) return "\n(no actor messages)";
183
+ return `\n${messages
184
+ .map((message) =>
183
185
  [
184
- `run=${event.run}`,
185
- `event=${event.event}`,
186
- `level=${event.level}`,
187
- `delivery=${event.delivery}`,
188
- `summary=${event.summary.replaceAll(/\s+/g, "_")}`,
186
+ `run=${message.run}`,
187
+ `type=${message.event}`,
188
+ `level=${message.level}`,
189
+ `summary=${message.summary.replaceAll(/\s+/g, "_")}`,
189
190
  ].join(" "),
190
191
  )
191
192
  .join("\n")}`;
@@ -209,10 +210,16 @@ function compactActorFiles(status: Record<string, unknown>): string {
209
210
  return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
210
211
  }
211
212
 
212
- function compactSessionRuns(session: string, runs: Array<Record<string, unknown>>): string {
213
+ function compactSessionRuns(
214
+ session: string,
215
+ runs: Array<Record<string, unknown>>,
216
+ ): string {
213
217
  if (runs.length === 0) return `\nsession=${session} runs=0`;
214
218
  return `\nsession=${session} runs=${runs.length}\n${runs
215
- .map((run) => `run=${String(run.run ?? "")} status=${String(run.status ?? "")}${run.recipe ? ` recipe=${String(run.recipe)}` : ""}`)
219
+ .map(
220
+ (run) =>
221
+ `run=${String(run.run ?? "")} status=${String(run.status ?? "")}${run.recipe ? ` recipe=${String(run.recipe)}` : ""}`,
222
+ )
216
223
  .join("\n")}`;
217
224
  }
218
225
 
@@ -225,6 +232,17 @@ function compactToolActor(name: string, tool: Record<string, unknown>): string {
225
232
  return `\ntool=${name} description=${String(tool.description ?? "").replaceAll(/\s+/g, "_")} args=${Object.keys(properties).join(",")} required=${required}`;
226
233
  }
227
234
 
235
+ function compactRecipeRegistry(summary: Record<string, unknown>): string {
236
+ const active = Array.isArray(summary.active) ? summary.active.length : 0;
237
+ const shadowed = Array.isArray(summary.shadowed) ? summary.shadowed.length : 0;
238
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
239
+ const disabled = Array.isArray(summary.disabled) ? summary.disabled.length : 0;
240
+ const diagnostics = Array.isArray(summary.diagnostics)
241
+ ? summary.diagnostics.length
242
+ : 0;
243
+ return `\nrecipes active=${active} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} diagnostics=${diagnostics}`;
244
+ }
245
+
228
246
  function compactActorMessageResult(
229
247
  message: ActorMessages.ActorMessage,
230
248
  result: Record<string, unknown>,
@@ -236,7 +254,7 @@ function compactActorMessageResult(
236
254
  ];
237
255
  if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
238
256
  if (result.control) tokens.push(`control=${String(result.control)}`);
239
- if (result.outbox) tokens.push(`outbox=${String(result.outbox)}`);
257
+ if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
240
258
  if (result.tool) tokens.push(`tool=${String(result.tool)}`);
241
259
  if (result.stopped === true) tokens.push("stopped=true");
242
260
  if (result.signal) tokens.push(`signal=${String(result.signal)}`);
@@ -309,15 +327,23 @@ function messageBodyToRunLine(message: ActorMessages.ActorMessage): string {
309
327
  return JSON.stringify(message.body);
310
328
  }
311
329
 
312
- function messageBodyToToolParams(message: ActorMessages.ActorMessage): Record<string, unknown> {
313
- if (message.body && typeof message.body === "object" && !Array.isArray(message.body)) {
330
+ function messageBodyToToolParams(
331
+ message: ActorMessages.ActorMessage,
332
+ ): Record<string, unknown> {
333
+ if (
334
+ message.body &&
335
+ typeof message.body === "object" &&
336
+ !Array.isArray(message.body)
337
+ ) {
314
338
  return message.body as Record<string, unknown>;
315
339
  }
316
340
  if (message.body === undefined) return {};
317
341
  return { input: message.body };
318
342
  }
319
343
 
320
- function runIdFromActorAddress(address: string | undefined): string | undefined {
344
+ function runIdFromActorAddress(
345
+ address: string | undefined,
346
+ ): string | undefined {
321
347
  if (!address) return undefined;
322
348
  const parsed = ActorMessages.parseActorAddress(address);
323
349
  if (parsed.kind !== "run" || !parsed.value) {
@@ -336,10 +362,18 @@ export function createSpawnToolDefinition<
336
362
  "Create an addressable actor from a recipe file or inline command template. Currently spawns run:<id> actors backed by async runs.",
337
363
  parameters: objectSchema(
338
364
  {
339
- artifacts: looseObjectSchema("Optional named artifact paths for the spawned actor."),
340
- as: stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
341
- file: stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
342
- recipe: stringSchema("Alias for file; template recipe JSON file/name to spawn."),
365
+ artifacts: looseObjectSchema(
366
+ "Optional named artifact paths for the spawned actor.",
367
+ ),
368
+ as: stringSchema(
369
+ "Optional actor address for the spawned run, e.g. run:<id>.",
370
+ ),
371
+ file: stringSchema(
372
+ "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
373
+ ),
374
+ recipe: stringSchema(
375
+ "Alias for file; template recipe JSON file/name to spawn.",
376
+ ),
343
377
  state_dir: stringSchema("Optional explicit run state directory."),
344
378
  template: unionSchema([
345
379
  stringSchema("Inline command template string"),
@@ -348,7 +382,9 @@ export function createSpawnToolDefinition<
348
382
  "Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template.",
349
383
  ),
350
384
  ]),
351
- values: looseObjectSchema("Runtime placeholder values passed to the actor."),
385
+ values: looseObjectSchema(
386
+ "Runtime placeholder values passed to the actor.",
387
+ ),
352
388
  verbose: booleanSchema("Return full JSON instead of compact text."),
353
389
  },
354
390
  [],
@@ -366,13 +402,21 @@ export function createSpawnToolDefinition<
366
402
  );
367
403
  const meta = AsyncRuns.startRun(
368
404
  {
369
- file: typeof input.file === "string" ? input.file : typeof input.recipe === "string" ? input.recipe : undefined,
405
+ file:
406
+ typeof input.file === "string"
407
+ ? input.file
408
+ : typeof input.recipe === "string"
409
+ ? input.recipe
410
+ : undefined,
370
411
  ownerId: getRunOwnerId(ctx),
371
412
  run_id: runId,
372
- state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
413
+ state_dir:
414
+ typeof input.state_dir === "string" ? input.state_dir : undefined,
373
415
  template: input.template as AsyncRuns.AsyncRunStartParams["template"],
374
416
  values: asRecord(input.values),
375
- ...(input.artifacts && typeof input.artifacts === "object" && !Array.isArray(input.artifacts)
417
+ ...(input.artifacts &&
418
+ typeof input.artifacts === "object" &&
419
+ !Array.isArray(input.artifacts)
376
420
  ? { artifacts: input.artifacts as Record<string, string> }
377
421
  : {}),
378
422
  },
@@ -382,7 +426,11 @@ export function createSpawnToolDefinition<
382
426
  content: [
383
427
  {
384
428
  type: "text" as const,
385
- text: maybeJsonText(meta, input.verbose === true, compactAsyncRunStatus(meta)),
429
+ text: maybeJsonText(
430
+ meta,
431
+ input.verbose === true,
432
+ compactAsyncRunStatus(meta),
433
+ ),
386
434
  },
387
435
  ],
388
436
  details: meta,
@@ -393,25 +441,36 @@ export function createSpawnToolDefinition<
393
441
 
394
442
  export interface InspectToolDeps<TContext = unknown> {
395
443
  getTool?: (name: string) => any | undefined;
444
+ packagedRecipeRoot?: string;
445
+ recipeRoot?: string;
396
446
  }
397
447
 
398
448
  function getContextSessionId(ctx: unknown): string | undefined {
399
- return (ctx as AsyncRunToolContext | undefined)?.sessionManager?.getSessionId?.();
449
+ return (
450
+ ctx as AsyncRunToolContext | undefined
451
+ )?.sessionManager?.getSessionId?.();
400
452
  }
401
453
 
402
454
  function requireContextSessionId(ctx: unknown, actor: string): string {
403
455
  const sessionId = getContextSessionId(ctx);
404
456
  if (!sessionId) {
405
- throw new Error(`${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`);
457
+ throw new Error(
458
+ `${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`,
459
+ );
406
460
  }
407
461
  return sessionId;
408
462
  }
409
463
 
410
- function assertRunAccessibleToContext(runId: string, ctx: unknown): Record<string, unknown> {
464
+ function assertRunAccessibleToContext(
465
+ runId: string,
466
+ ctx: unknown,
467
+ ): Record<string, unknown> {
411
468
  const status = AsyncRuns.getRunStatus(runId);
412
469
  const sessionId = getContextSessionId(ctx);
413
470
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
414
- throw new Error(`run:${runId} is owned by session:${status.ownerId}; current session is ${sessionId}.`);
471
+ throw new Error(
472
+ `run:${runId} is owned by session:${status.ownerId}; current session is ${sessionId}.`,
473
+ );
415
474
  }
416
475
  return status;
417
476
  }
@@ -427,10 +486,18 @@ export function createInspectToolDefinition<TContext = unknown>(
427
486
  parameters: objectSchema(
428
487
  {
429
488
  lines: stringSchema("Line count for tail/messages views. Default 40."),
430
- status: stringSchema("Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited."),
431
- target: stringSchema("Actor address to inspect, e.g. run:<id>, coordinator, session:<id>, session:all, or tool:<name>."),
432
- verbose: booleanSchema("Return full JSON instead of compact text where available."),
433
- view: stringSchema("Inspection view: status, tail, messages, artifacts, files, or mailbox."),
489
+ status: stringSchema(
490
+ "Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited.",
491
+ ),
492
+ target: stringSchema(
493
+ "Actor address to inspect, e.g. run:<id>, coordinator, session:<id>, session:all, or tool:<name>.",
494
+ ),
495
+ verbose: booleanSchema(
496
+ "Return full JSON instead of compact text where available.",
497
+ ),
498
+ view: stringSchema(
499
+ "Inspection view: status, tail, messages, artifacts, files, or mailbox.",
500
+ ),
434
501
  },
435
502
  ["target", "view"],
436
503
  ),
@@ -443,21 +510,53 @@ export function createInspectToolDefinition<TContext = unknown>(
443
510
  ) {
444
511
  const input = asRecord(params);
445
512
  const target = String(input.target ?? "");
446
- const address = ActorMessages.parseActorAddress(target);
447
513
  const view = String(input.view ?? "");
514
+ if (target === "recipes" || target === "recipe-registry") {
515
+ if (view !== "status" && view !== "summary") {
516
+ throw new Error("inspect recipes supports view=status or view=summary.");
517
+ }
518
+ const discovered = RecipeDiscovery.discoverRecipeSources([
519
+ { root: deps.recipeRoot ?? Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
520
+ { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
521
+ ]);
522
+ const summary = RecipeDiscovery.summarizeDiscovery(discovered);
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text" as const,
527
+ text: maybeJsonText(
528
+ summary,
529
+ input.verbose === true,
530
+ compactRecipeRegistry(summary),
531
+ ),
532
+ },
533
+ ],
534
+ details: summary,
535
+ };
536
+ }
537
+ const address = ActorMessages.parseActorAddress(target);
448
538
  if (address.kind === "coordinator") {
449
539
  if (view !== "status" && view !== "runs") {
450
- throw new Error("inspect coordinator supports view=status or view=runs.");
540
+ throw new Error(
541
+ "inspect coordinator supports view=status or view=runs.",
542
+ );
451
543
  }
452
544
  const session = requireContextSessionId(ctx, "inspect coordinator");
453
- const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
545
+ const runs = AsyncRuns.listRuns(
546
+ undefined,
547
+ typeof input.status === "string" ? input.status : undefined,
548
+ )
454
549
  .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
455
550
  .filter((run) => run.ownerId === session);
456
551
  return {
457
552
  content: [
458
553
  {
459
554
  type: "text" as const,
460
- text: maybeJsonText({ session, runs }, input.verbose === true, compactSessionRuns(session, runs)),
555
+ text: maybeJsonText(
556
+ { session, runs },
557
+ input.verbose === true,
558
+ compactSessionRuns(session, runs),
559
+ ),
461
560
  },
462
561
  ],
463
562
  details: { session, runs },
@@ -465,16 +564,27 @@ export function createInspectToolDefinition<TContext = unknown>(
465
564
  }
466
565
  if (address.kind === "session") {
467
566
  if (view !== "status" && view !== "runs") {
468
- throw new Error("inspect session:<id> supports view=status or view=runs.");
567
+ throw new Error(
568
+ "inspect session:<id> supports view=status or view=runs.",
569
+ );
469
570
  }
470
- const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
571
+ const runs = AsyncRuns.listRuns(
572
+ undefined,
573
+ typeof input.status === "string" ? input.status : undefined,
574
+ )
471
575
  .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
472
- .filter((run) => address.value === "all" || run.ownerId === address.value);
576
+ .filter(
577
+ (run) => address.value === "all" || run.ownerId === address.value,
578
+ );
473
579
  return {
474
580
  content: [
475
581
  {
476
582
  type: "text" as const,
477
- text: maybeJsonText({ session: address.value, runs }, input.verbose === true, compactSessionRuns(address.value || "", runs)),
583
+ text: maybeJsonText(
584
+ { session: address.value, runs },
585
+ input.verbose === true,
586
+ compactSessionRuns(address.value || "", runs),
587
+ ),
478
588
  },
479
589
  ],
480
590
  details: { session: address.value, runs },
@@ -482,7 +592,9 @@ export function createInspectToolDefinition<TContext = unknown>(
482
592
  }
483
593
  if (address.kind === "tool" && address.value) {
484
594
  if (view !== "status" && view !== "schema") {
485
- throw new Error("inspect tool:<name> supports view=status or view=schema.");
595
+ throw new Error(
596
+ "inspect tool:<name> supports view=status or view=schema.",
597
+ );
486
598
  }
487
599
  const tool = deps.getTool?.(address.value);
488
600
  if (!tool) throw new Error(`tool actor not found: ${address.value}`);
@@ -496,14 +608,21 @@ export function createInspectToolDefinition<TContext = unknown>(
496
608
  content: [
497
609
  {
498
610
  type: "text" as const,
499
- text: maybeJsonText(details, input.verbose === true || view === "schema", compactToolActor(address.value, details)),
611
+ text: maybeJsonText(
612
+ details,
613
+ input.verbose === true || view === "schema",
614
+ compactToolActor(address.value, details),
615
+ ),
500
616
  },
501
617
  ],
502
618
  details,
503
619
  };
504
620
  }
505
621
  const runId = address.kind === "run" ? address.value : undefined;
506
- if (!runId) throw new Error("inspect target must be run:<id>, coordinator, session:<id>, or tool:<name>.");
622
+ if (!runId)
623
+ throw new Error(
624
+ "inspect target must be run:<id>, coordinator, session:<id>, or tool:<name>.",
625
+ );
507
626
  switch (view) {
508
627
  case "status": {
509
628
  const status = assertRunAccessibleToContext(runId, ctx);
@@ -511,7 +630,11 @@ export function createInspectToolDefinition<TContext = unknown>(
511
630
  content: [
512
631
  {
513
632
  type: "text" as const,
514
- text: maybeJsonText(status, input.verbose === true, compactAsyncRunStatus(status)),
633
+ text: maybeJsonText(
634
+ status,
635
+ input.verbose === true,
636
+ compactAsyncRunStatus(status),
637
+ ),
515
638
  },
516
639
  ],
517
640
  details: status,
@@ -520,16 +643,26 @@ export function createInspectToolDefinition<TContext = unknown>(
520
643
  case "tail": {
521
644
  assertRunAccessibleToContext(runId, ctx);
522
645
  const text = AsyncRuns.tailRun(runId, Number(input.lines || 40));
523
- return { content: [{ type: "text" as const, text: `\n${text}` }], details: {} };
646
+ return {
647
+ content: [{ type: "text" as const, text: `\n${text}` }],
648
+ details: {},
649
+ };
524
650
  }
525
651
  case "messages": {
526
652
  assertRunAccessibleToContext(runId, ctx);
527
- const messages = AsyncRuns.readRunEvents(runId, Number(input.lines || 40));
653
+ const messages = AsyncRuns.readRunEvents(
654
+ runId,
655
+ Number(input.lines || 40),
656
+ );
528
657
  return {
529
658
  content: [
530
659
  {
531
660
  type: "text" as const,
532
- text: maybeJsonText(messages, input.verbose === true, compactRunEvents(messages)),
661
+ text: maybeJsonText(
662
+ messages,
663
+ input.verbose === true,
664
+ compactRunMessages(messages),
665
+ ),
533
666
  },
534
667
  ],
535
668
  details: { messages },
@@ -542,7 +675,11 @@ export function createInspectToolDefinition<TContext = unknown>(
542
675
  content: [
543
676
  {
544
677
  type: "text" as const,
545
- text: maybeJsonText(status, input.verbose === true, compactActorFiles(status)),
678
+ text: maybeJsonText(
679
+ status,
680
+ input.verbose === true,
681
+ compactActorFiles(status),
682
+ ),
546
683
  },
547
684
  ],
548
685
  details: status,
@@ -555,14 +692,20 @@ export function createInspectToolDefinition<TContext = unknown>(
555
692
  content: [
556
693
  {
557
694
  type: "text" as const,
558
- text: maybeJsonText(mailbox, input.verbose === true, `\nrun=${String(status.run ?? runId)} accepts=${Array.isArray(mailbox.accepts) ? mailbox.accepts.join(",") : ""} emits=${Array.isArray(mailbox.emits) ? mailbox.emits.join(",") : ""}`),
695
+ text: maybeJsonText(
696
+ mailbox,
697
+ input.verbose === true,
698
+ `\nrun=${String(status.run ?? runId)} accepts=${Array.isArray(mailbox.accepts) ? mailbox.accepts.join(",") : ""} emits=${Array.isArray(mailbox.emits) ? mailbox.emits.join(",") : ""}`,
699
+ ),
559
700
  },
560
701
  ],
561
702
  details: { mailbox },
562
703
  };
563
704
  }
564
705
  default:
565
- throw new Error("inspect view must be one of: status, tail, messages, artifacts, files, mailbox.");
706
+ throw new Error(
707
+ "inspect view must be one of: status, tail, messages, artifacts, files, mailbox.",
708
+ );
566
709
  }
567
710
  },
568
711
  };
@@ -583,17 +726,29 @@ export function createActorMessageToolDefinition<TContext = unknown>(
583
726
  parameters: objectSchema(
584
727
  {
585
728
  body: unionSchema([
586
- stringSchema("Message body. For run:<id>, this is the run-local command line."),
729
+ stringSchema(
730
+ "Message body. For run:<id>, this is the run-local command line.",
731
+ ),
587
732
  looseObjectSchema("Structured JSON message body."),
588
733
  arraySchema("Structured JSON message body array."),
589
734
  ]),
590
- correlation_id: stringSchema("Optional correlation id for workflow/task linkage."),
591
- from: stringSchema("Optional sender address, such as coordinator or run:<id>."),
592
- metadata: looseObjectSchema("Optional structured metadata for routing or domain hints."),
735
+ correlation_id: stringSchema(
736
+ "Optional correlation id for workflow/task linkage.",
737
+ ),
738
+ from: stringSchema(
739
+ "Optional sender address, such as coordinator or run:<id>.",
740
+ ),
741
+ metadata: looseObjectSchema(
742
+ "Optional structured metadata for routing or domain hints.",
743
+ ),
593
744
  reply_to: stringSchema("Optional message id this message replies to."),
594
745
  summary: stringSchema("Optional short human-facing summary."),
595
- to: stringSchema("Destination actor address, e.g. run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>."),
596
- type: stringSchema("Semantic message type, e.g. control.approve or checkpoint.needs_scope."),
746
+ to: stringSchema(
747
+ "Destination actor address, e.g. run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>.",
748
+ ),
749
+ type: stringSchema(
750
+ "Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
751
+ ),
597
752
  verbose: booleanSchema("Return full JSON instead of compact text."),
598
753
  },
599
754
  ["to", "type"],
@@ -611,7 +766,10 @@ export function createActorMessageToolDefinition<TContext = unknown>(
611
766
  let result: Record<string, unknown>;
612
767
  if (address.kind === "run" && address.value) {
613
768
  assertRunAccessibleToContext(address.value, ctx);
614
- if (message.type === "control.stop" || message.type === "control.cancel") {
769
+ if (
770
+ message.type === "control.stop" ||
771
+ message.type === "control.cancel"
772
+ ) {
615
773
  result = AsyncRuns.cancelRun(address.value);
616
774
  } else if (message.type === "control.kill") {
617
775
  result = AsyncRuns.killRun(address.value);
@@ -630,7 +788,9 @@ export function createActorMessageToolDefinition<TContext = unknown>(
630
788
  } else if (address.kind === "tool" && address.value) {
631
789
  const tool = deps.getTool?.(address.value);
632
790
  if (!tool || typeof tool.execute !== "function") {
633
- throw new Error(`tool actor not found or not executable: ${address.value}`);
791
+ throw new Error(
792
+ `tool actor not found or not executable: ${address.value}`,
793
+ );
634
794
  }
635
795
  const toolResult = await tool.execute(
636
796
  `message:${message.type}`,
@@ -651,15 +811,21 @@ export function createActorMessageToolDefinition<TContext = unknown>(
651
811
  }
652
812
  const sender = ActorMessages.parseActorAddress(message.from);
653
813
  if (sender.kind !== "run" || !sender.value) {
654
- throw new Error(`message to ${address.kind} currently requires from=run:<id>.`);
814
+ throw new Error(
815
+ `message to ${address.kind} currently requires from=run:<id>.`,
816
+ );
655
817
  }
656
818
  const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
657
819
  if (address.kind === "session") {
658
820
  if (!senderStatus.ownerId) {
659
- throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got no owner.`);
821
+ throw new Error(
822
+ `message to session:${address.value} requires sender run owner ${address.value}; got no owner.`,
823
+ );
660
824
  }
661
825
  if (senderStatus.ownerId !== address.value) {
662
- throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`);
826
+ throw new Error(
827
+ `message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`,
828
+ );
663
829
  }
664
830
  }
665
831
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
@@ -754,6 +920,7 @@ export function createRuntimeToolDefinition(
754
920
  ctx: AsyncRunToolContext,
755
921
  ) {
756
922
  try {
923
+ if (cfg.sourcePath) RecipeUsage.recordRecipeLaunch(cfg.sourcePath);
757
924
  if (isAsyncRecipe) {
758
925
  const input = params as Record<string, unknown>;
759
926
  const { run_id, ...values } = input;
@@ -807,7 +974,12 @@ export function createRuntimeToolDefinition(
807
974
  signal,
808
975
  );
809
976
  } catch (error) {
810
- throw formatRuntimeToolArgumentError(cfg, error, required, isAsyncRecipe);
977
+ throw formatRuntimeToolArgumentError(
978
+ cfg,
979
+ error,
980
+ required,
981
+ isAsyncRecipe,
982
+ );
811
983
  }
812
984
  },
813
985
  };
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.14.3",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "Actor runtime and orchestrator for agent-managed local processes",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "actors",
10
+ "orchestration",
11
+ "automation",
12
+ "tools"
13
+ ],
6
14
  "type": "module",
7
15
  "license": "MIT",
8
16
  "repository": {
@@ -13,14 +21,9 @@
13
21
  "bugs": {
14
22
  "url": "https://github.com/llblab/pi-actors/issues"
15
23
  },
16
- "keywords": [
17
- "pi-package",
18
- "pi-extension",
19
- "actors",
20
- "orchestration",
21
- "automation",
22
- "tools"
23
- ],
24
+ "engines": {
25
+ "node": ">=22.19.0"
26
+ },
24
27
  "scripts": {
25
28
  "check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-actors: extension import ok')\"",
26
29
  "test": "node --experimental-strip-types --test tests/*.test.ts",
@@ -32,16 +35,23 @@
32
35
  "lib",
33
36
  "scripts",
34
37
  "recipes",
38
+ "skills",
35
39
  "README.md",
36
40
  "AGENTS.md",
37
41
  "BACKLOG.md",
38
42
  "CHANGELOG.md",
39
- "docs"
43
+ "docs",
44
+ "banner.jpg"
40
45
  ],
41
46
  "pi": {
42
47
  "extensions": [
43
48
  "./index.ts"
44
- ]
49
+ ],
50
+ "skills": [
51
+ "./skills/actors/SKILL.md",
52
+ "./skills/swarm/SKILL.md"
53
+ ],
54
+ "image": "https://github.com/llblab/pi-actors/raw/main/banner.jpg"
45
55
  },
46
56
  "peerDependencies": {
47
57
  "@earendil-works/pi-coding-agent": "*"
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "coordinator-locker",
3
+ "async": true,
4
+ "args": [
5
+ "repo:path",
6
+ "lease_ms:int"
7
+ ],
8
+ "defaults": {
9
+ "repo": "~/.pi/agent/extensions/pi-actors",
10
+ "lease_ms": "600000"
11
+ },
12
+ "mailbox": {
13
+ "accepts": [
14
+ "coord.enqueue",
15
+ "coord.claim",
16
+ "coord.complete",
17
+ "coord.fail",
18
+ "lock.acquire",
19
+ "lock.renew",
20
+ "lock.release",
21
+ "control.stop",
22
+ "control.cancel",
23
+ "control.kill"
24
+ ],
25
+ "emits": [
26
+ "coord.started",
27
+ "coord.enqueued",
28
+ "coord.assigned",
29
+ "coord.empty",
30
+ "coord.completed",
31
+ "coord.failed",
32
+ "lock.granted",
33
+ "lock.denied",
34
+ "lock.renewed",
35
+ "lock.released",
36
+ "run.done",
37
+ "run.failed"
38
+ ]
39
+ },
40
+ "artifacts": {
41
+ "queue": "{state_dir}/queue.json",
42
+ "locks": "{state_dir}/locks.json",
43
+ "journal": "{state_dir}/journal.jsonl"
44
+ },
45
+ "template": "{repo}/scripts/coordinator-locker.mjs serve --state-dir {state_dir} --lease-ms {lease_ms}"
46
+ }