@llblab/pi-actors 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/tools.js CHANGED
@@ -168,8 +168,9 @@ function compactAsyncRunStatus(value) {
168
168
  tokens.push(`code=${String(result.code)}`);
169
169
  if (result.killed === true)
170
170
  tokens.push("killed=true");
171
- if (status.candidate_recipe)
172
- tokens.push(`candidate_recipe=${String(status.candidate_recipe)}`);
171
+ const draftRecipe = status.draft_recipe;
172
+ if (draftRecipe)
173
+ tokens.push(`draft_recipe=${String(draftRecipe)}`);
173
174
  const nextActions = actorRunNextActions(run);
174
175
  if (nextActions.length > 0)
175
176
  tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
@@ -357,6 +358,15 @@ function compactArtifactPath(value) {
357
358
  const record = asRecord(value);
358
359
  return String(record.path ?? "<missing>");
359
360
  }
361
+ function artifactNextActions(run, artifacts) {
362
+ const id = String(run ?? "").trim();
363
+ if (!id || Object.keys(artifacts).length === 0)
364
+ return [];
365
+ return [
366
+ `inspect target=run:${id} view=artifacts verbose=true`,
367
+ `inspect target=run:${id} view=messages`,
368
+ ];
369
+ }
360
370
  function compactActorFiles(status) {
361
371
  const run = String(status.run ?? "<unknown>");
362
372
  const artifacts = asRecord(status.artifacts);
@@ -375,7 +385,11 @@ function compactActorFiles(status) {
375
385
  .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
376
386
  .join(",")}`
377
387
  : "";
378
- return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
388
+ const nextActions = artifactNextActions(run, artifacts);
389
+ const nextText = nextActions.length
390
+ ? ` next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
391
+ : "";
392
+ return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}${nextText}`;
379
393
  }
380
394
  function summarizeOtherSessions(currentSession, allRuns) {
381
395
  const otherRuns = allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession);
@@ -490,13 +504,47 @@ function compactRecipeDoctor(summary) {
490
504
  }
491
505
  for (const item of remediations.slice(0, 8)) {
492
506
  const action = compactPreview(item.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS);
493
- const blocked = item.blocked_candidate
494
- ? ` blocked=${compactPreview(item.blocked_candidate, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
507
+ const blocked = item.blocked_fallback
508
+ ? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
495
509
  : "";
496
510
  lines.push(`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`);
497
511
  }
512
+ const nextActions = Array.isArray(summary.next_actions)
513
+ ? summary.next_actions
514
+ : [];
515
+ if (nextActions.length > 0)
516
+ lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
498
517
  return `\n${lines.join("\n")}`;
499
518
  }
519
+ function recipeRegistryNextActions(summary, view) {
520
+ const actions = [];
521
+ const drafts = Array.isArray(summary.drafts)
522
+ ? summary.drafts
523
+ : [];
524
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
525
+ const diagnostics = Array.isArray(summary.diagnostics)
526
+ ? summary.diagnostics.length
527
+ : 0;
528
+ const topAction = asRecord(summary.top_action);
529
+ if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
530
+ actions.push("inspect target=recipes view=doctor");
531
+ }
532
+ if (view === "doctor" && typeof topAction.action === "string") {
533
+ actions.push(String(topAction.action));
534
+ }
535
+ if (drafts.length > 0) {
536
+ actions.push("inspect target=recipes view=summary verbose=true");
537
+ const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
538
+ if (firstPath)
539
+ actions.push(`spawn file=${firstPath}`);
540
+ }
541
+ return [...new Set(actions)].slice(0, 4);
542
+ }
543
+ function compactNextActions(actions) {
544
+ return actions.length
545
+ ? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
546
+ : "";
547
+ }
500
548
  function compactRecipeRegistry(summary) {
501
549
  const active = Array.isArray(summary.active) ? summary.active.length : 0;
502
550
  const shadowed = Array.isArray(summary.shadowed)
@@ -509,13 +557,37 @@ function compactRecipeRegistry(summary) {
509
557
  const diagnostics = Array.isArray(summary.diagnostics)
510
558
  ? summary.diagnostics.length
511
559
  : 0;
512
- const candidates = Array.isArray(summary.candidates)
513
- ? summary.candidates.length
514
- : 0;
560
+ const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
515
561
  const recommendations = Array.isArray(summary.recommendations)
516
562
  ? summary.recommendations.length
517
563
  : 0;
518
- return `\nrecipes active=${active} candidates=${candidates} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}`;
564
+ const nextActions = Array.isArray(summary.next_actions)
565
+ ? summary.next_actions
566
+ : [];
567
+ return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
568
+ }
569
+ function actorMessageNextActions(message, result) {
570
+ const actions = [];
571
+ const address = ActorMessages.parseActorAddress(message.to);
572
+ if (result.delivery_error || result.sent === false) {
573
+ if (address.kind === "run" && address.value) {
574
+ actions.push(`inspect target=run:${address.value} view=status`);
575
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
576
+ }
577
+ else if (address.kind === "branch" && address.value) {
578
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
579
+ actions.push(`inspect target=run:${address.value} view=status`);
580
+ }
581
+ }
582
+ if (result.queued === true) {
583
+ if (address.kind === "branch" && address.value) {
584
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
585
+ }
586
+ else if (address.kind === "run" && address.value) {
587
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
588
+ }
589
+ }
590
+ return [...new Set(actions)].slice(0, 3);
519
591
  }
520
592
  function compactActorMessageResult(message, result) {
521
593
  const tokens = [
@@ -548,6 +620,11 @@ function compactActorMessageResult(message, result) {
548
620
  if (result.delivery_error) {
549
621
  tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
550
622
  }
623
+ const nextActions = Array.isArray(result.next_actions)
624
+ ? result.next_actions
625
+ : actorMessageNextActions(message, result);
626
+ if (nextActions.length > 0)
627
+ tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
551
628
  return `\n${tokens.join(" ")}`;
552
629
  }
553
630
  function maybeJsonText(value, verbose, compact) {
@@ -621,10 +698,10 @@ function shadowedRecipeLaunchDiagnostic(recipe) {
621
698
  ]);
622
699
  return RecipeDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
623
700
  }
624
- function candidateRecipeName(run) {
701
+ function draftRecipeName(run) {
625
702
  return `${run.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "spawn"}.json`;
626
703
  }
627
- function candidateRecipeDefaults(values) {
704
+ function draftRecipeDefaults(values) {
628
705
  const ignored = new Set([
629
706
  "actor_address",
630
707
  "communication_file",
@@ -635,21 +712,21 @@ function candidateRecipeDefaults(values) {
635
712
  const defaults = Object.fromEntries(Object.entries(values).filter(([key]) => !ignored.has(key)));
636
713
  return Object.keys(defaults).length > 0 ? defaults : undefined;
637
714
  }
638
- function writeSpawnCandidateRecipe(input, meta) {
715
+ function writeSpawnDraftRecipe(input, meta) {
639
716
  if (process.env.NODE_TEST_CONTEXT &&
640
- process.env.PI_ACTORS_ENABLE_SPAWN_CANDIDATES_IN_TEST !== "1")
717
+ process.env.PI_ACTORS_ENABLE_SPAWN_DRAFTS_IN_TEST !== "1")
641
718
  return undefined;
642
719
  if (input.template === undefined ||
643
720
  input.file !== undefined ||
644
721
  input.recipe !== undefined)
645
722
  return undefined;
646
- const root = Paths.getRecipeCandidateRoot();
723
+ const root = Paths.getRecipeDraftRoot();
647
724
  mkdirSync(root, { recursive: true });
648
- const path = join(root, candidateRecipeName(String(meta.run)));
649
- const defaults = candidateRecipeDefaults(meta.values);
725
+ const path = join(root, draftRecipeName(String(meta.run)));
726
+ const defaults = draftRecipeDefaults(meta.values);
650
727
  const recipe = {
651
728
  async: true,
652
- description: `Candidate recipe captured from spawn run ${String(meta.run)}`,
729
+ description: `Draft recipe captured from spawn run ${String(meta.run)}`,
653
730
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
654
731
  ...(defaults ? { defaults } : {}),
655
732
  template: input.template,
@@ -662,7 +739,7 @@ function enhanceSpawnRecipeError(error, recipe) {
662
739
  if (!diagnostic)
663
740
  return error instanceof Error ? error : new Error(String(error));
664
741
  const original = error instanceof Error ? error.message : String(error);
665
- return Object.assign(new Error(`${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_candidate=${diagnostic.blocked_candidate} hint=${diagnostic.hint}`), {
742
+ return Object.assign(new Error(`${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_fallback=${diagnostic.blocked_fallback} hint=${diagnostic.hint}`), {
666
743
  ...diagnostic,
667
744
  original_error: original,
668
745
  });
@@ -783,11 +860,11 @@ export function createSpawnToolDefinition() {
783
860
  catch (error) {
784
861
  throw enhanceSpawnRecipeError(error, recipe);
785
862
  }
786
- const candidateRecipe = writeSpawnCandidateRecipe(input, meta);
863
+ const draftRecipe = writeSpawnDraftRecipe(input, meta);
787
864
  const nextActions = actorRunNextActions(meta.run);
788
865
  const details = {
789
866
  ...meta,
790
- ...(candidateRecipe ? { candidate_recipe: candidateRecipe } : {}),
867
+ ...(draftRecipe ? { draft_recipe: draftRecipe } : {}),
791
868
  next_actions: nextActions,
792
869
  };
793
870
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
@@ -814,15 +891,29 @@ function requireContextSessionId(ctx, actor) {
814
891
  }
815
892
  return sessionId;
816
893
  }
894
+ function sessionMismatchError(input) {
895
+ const ownerSession = input.expectedSession ?? "none";
896
+ const currentSession = input.currentSession ?? "none";
897
+ const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
898
+ const hintTarget = input.expectedSession
899
+ ? `session:${input.expectedSession}`
900
+ : "session:all";
901
+ return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`), {
902
+ current_session: input.currentSession,
903
+ hint: `inspect target=${hintTarget} view=status`,
904
+ owner_session: input.expectedSession,
905
+ reason: "session_mismatch",
906
+ run: input.run,
907
+ target: input.target,
908
+ });
909
+ }
817
910
  function assertRunAccessibleToContext(runId, ctx) {
818
911
  const status = AsyncRuns.getRunStatus(runId);
819
912
  const sessionId = getContextSessionId(ctx);
820
913
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
821
- throw Object.assign(new Error(`run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`), {
822
- current_session: sessionId,
823
- hint: `inspect target=session:${status.ownerId} view=status`,
824
- owner_session: status.ownerId,
825
- reason: "session_mismatch",
914
+ throw sessionMismatchError({
915
+ currentSession: sessionId,
916
+ expectedSession: String(status.ownerId),
826
917
  run: runId,
827
918
  });
828
919
  }
@@ -835,13 +926,13 @@ export function createInspectToolDefinition(deps = {}) {
835
926
  return {
836
927
  name: "inspect",
837
928
  label: "Inspect",
838
- description: "Intentionally inspect an actor at decision points, after follow-ups, or during diagnosis instead of polling. Supports run:<id> views: status, tail, messages, artifacts, files, mailbox, communication; room:<run> status/messages/previews/roster/contacts; coordinator/session status; and tool:<name> status/schema.",
929
+ description: "Intentionally inspect actors at decision points, after follow-ups, or during diagnosis instead of polling. Core targets are run:<id> and tool:<name>; advanced targets include branch:<run>/<branch>, room:<run>, coordinator, session:<id>, and session:all.",
839
930
  parameters: objectSchema({
840
931
  lines: stringSchema("Line count for tail/messages views. Default 40."),
841
932
  status: stringSchema("Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited."),
842
- target: stringSchema("Actor address to inspect, e.g. run:<id>, room:<run>, coordinator, session:<id>, session:all, or tool:<name>."),
933
+ target: stringSchema("Actor address to inspect, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>, session:all."),
843
934
  verbose: booleanSchema("Return full JSON instead of compact text where available."),
844
- view: stringSchema("Inspection view: status, tail, messages, artifacts, files, mailbox, communication, roster, or contacts."),
935
+ view: stringSchema("Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts."),
845
936
  }, ["target", "view"]),
846
937
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
847
938
  const input = asRecord(params);
@@ -863,9 +954,13 @@ export function createInspectToolDefinition(deps = {}) {
863
954
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
864
955
  ]);
865
956
  const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
866
- const summary = {
957
+ const summaryBase = {
867
958
  ...RecipeDiscovery.summarizeDiscovery(discovered),
868
- candidates: RecipeDiscovery.listCandidateRecipes(join(recipeRoot, "candidates")),
959
+ drafts: RecipeDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
960
+ };
961
+ const summary = {
962
+ ...summaryBase,
963
+ next_actions: recipeRegistryNextActions(summaryBase, view),
869
964
  };
870
965
  return {
871
966
  content: [
@@ -1084,7 +1179,11 @@ export function createInspectToolDefinition(deps = {}) {
1084
1179
  const status = assertRunAccessibleToContext(runId, ctx);
1085
1180
  const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
1086
1181
  const details = artifactManifest
1087
- ? { ...status, artifact_manifest: artifactManifest }
1182
+ ? {
1183
+ ...status,
1184
+ artifact_manifest: artifactManifest,
1185
+ next_actions: artifactNextActions(status.run ?? runId, asRecord(status.artifacts)),
1186
+ }
1088
1187
  : status;
1089
1188
  return {
1090
1189
  content: [
@@ -1139,7 +1238,7 @@ export function createActorMessageToolDefinition(deps = {}) {
1139
1238
  return {
1140
1239
  name: "message",
1141
1240
  label: "Message",
1142
- description: "Send one typed addressed message to steer an existing actor instead of restarting it. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, room:<run> timelines/rosters, tool:<name> calls, and coordinator/session-bound run messages.",
1241
+ description: "Send one typed addressed message to steer an existing actor instead of restarting it. Core routes are run:<id> and tool:<name>; advanced routes include branch:<run>/<branch>, room:<run> group timelines, coordinator, and session:<id>.",
1143
1242
  parameters: objectSchema({
1144
1243
  body: unionSchema([
1145
1244
  stringSchema("Message body. For run:<id>, this is the run-local command line."),
@@ -1151,7 +1250,7 @@ export function createActorMessageToolDefinition(deps = {}) {
1151
1250
  metadata: looseObjectSchema("Optional structured metadata for routing or domain hints."),
1152
1251
  reply_to: stringSchema("Optional message id this message replies to."),
1153
1252
  summary: stringSchema("Optional short human-facing summary."),
1154
- to: stringSchema("Destination actor address, e.g. run:<id>, branch:<run>/<branch>, room:<run>, coordinator, session:<id>, or tool:<name>."),
1253
+ to: stringSchema("Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>."),
1155
1254
  type: stringSchema("Semantic message type, e.g. control.approve or checkpoint.needs_scope."),
1156
1255
  verbose: booleanSchema("Return full JSON instead of compact text."),
1157
1256
  }, ["to", "type"]),
@@ -1266,10 +1365,20 @@ export function createActorMessageToolDefinition(deps = {}) {
1266
1365
  const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
1267
1366
  if (address.kind === "session") {
1268
1367
  if (!senderStatus.ownerId) {
1269
- throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got no owner.`);
1368
+ throw sessionMismatchError({
1369
+ currentSession: undefined,
1370
+ expectedSession: address.value,
1371
+ run: sender.value,
1372
+ target: `session:${address.value}`,
1373
+ });
1270
1374
  }
1271
1375
  if (senderStatus.ownerId !== address.value) {
1272
- throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`);
1376
+ throw sessionMismatchError({
1377
+ currentSession: String(senderStatus.ownerId),
1378
+ expectedSession: address.value,
1379
+ run: sender.value,
1380
+ target: `session:${address.value}`,
1381
+ });
1273
1382
  }
1274
1383
  }
1275
1384
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
@@ -1291,14 +1400,18 @@ export function createActorMessageToolDefinition(deps = {}) {
1291
1400
  else {
1292
1401
  throw new Error(`message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`);
1293
1402
  }
1403
+ const nextActions = actorMessageNextActions(message, result);
1404
+ const resultWithNext = nextActions.length
1405
+ ? { ...result, next_actions: nextActions }
1406
+ : result;
1294
1407
  return {
1295
1408
  content: [
1296
1409
  {
1297
1410
  type: "text",
1298
- text: maybeJsonText({ message, result }, input.verbose === true, compactActorMessageResult(message, result)),
1411
+ text: maybeJsonText({ message, result: resultWithNext }, input.verbose === true, compactActorMessageResult(message, resultWithNext)),
1299
1412
  },
1300
1413
  ],
1301
- details: { message, result },
1414
+ details: { message, result: resultWithNext },
1302
1415
  };
1303
1416
  },
1304
1417
  };
@@ -1,33 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Internal conformance runner shim.
4
+ * Internal conformance runner.
5
5
  *
6
- * Runtime logic lives in lib/conformance.ts and is compiled to
7
- * dist/lib/conformance.js for installed JS-only packages.
6
+ * This script is intentionally standalone package/release glue rather than a
7
+ * lib domain: it only selects regression suites and formats their summary.
8
8
  */
9
9
 
10
- import { existsSync } from "node:fs";
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { spawnSync } from "node:child_process";
11
+ import { dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const conformanceSuites = [
15
+ "tests/protocol-examples.test.ts",
16
+ "tests/recipe-discovery.test.ts",
17
+ "tests/registry.test.ts",
18
+ "tests/runtime-registry.test.ts",
19
+ "tests/async-runs.test.ts",
20
+ "tests/actor-rooms.test.ts",
21
+ "tests/tools.test.ts",
22
+ ];
13
23
 
14
24
  function packageRoot() {
15
25
  return dirname(dirname(fileURLToPath(import.meta.url)));
16
26
  }
17
27
 
18
- function mainModulePath() {
19
- const root = packageRoot();
20
- const compiled = join(root, "dist", "lib", "conformance.js");
21
- return existsSync(compiled) ? compiled : join(root, "lib", "conformance.ts");
22
- }
28
+ const result = spawnSync(
29
+ process.execPath,
30
+ ["--experimental-strip-types", "--test", ...conformanceSuites],
31
+ { cwd: packageRoot(), encoding: "utf8", stdio: "pipe" },
32
+ );
23
33
 
24
- const { runConformance } = await import(pathToFileURL(mainModulePath()).href);
25
- const report = runConformance(packageRoot());
34
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
35
+ const summary = output
36
+ .split("\n")
37
+ .filter((line) =>
38
+ /^ℹ (tests|pass|fail|cancelled|skipped|todo|duration_ms) /.test(line),
39
+ )
40
+ .join("\n");
26
41
 
27
42
  console.log("pi-actors conformance");
28
- console.log(`suites ${report.suites}`);
29
- if (report.summary) console.log(report.summary);
30
- if (report.code !== 0) {
31
- console.error(report.output.trimEnd());
32
- process.exit(report.code);
43
+ console.log(`suites ${conformanceSuites.length}`);
44
+
45
+ if (summary) console.log(summary);
46
+ if ((result.status ?? 1) !== 0) {
47
+ console.error(output.trimEnd());
48
+ process.exit(result.status ?? 1);
33
49
  }
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.31.0
5
+ version: 0.33.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -39,12 +39,11 @@ Trusted local capability
39
39
 
40
40
  - **Command template**: portable execution graph. String leaf, sequence array, or object node with controls.
41
41
  - **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
42
- - **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
43
- - **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
42
+ - **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, and artifacts.
44
43
  - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
45
- - **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
46
- - **Mailbox**: public interaction contract: message types the actor accepts/emits.
47
44
  - **Artifact**: named durable output path declared by a recipe/run.
45
+ - **Mailbox**: interaction contract: message types the actor accepts/emits.
46
+ - **Advanced group/coordination surfaces**: `branch:<run>/<branch>`, `room:<run>`, `coordinator`, `session:<id>`, and communication snapshots exist for multi-actor workflows and diagnostics; do not make them the default mental model.
48
47
 
49
48
  ## Three Verbs
50
49
 
@@ -86,8 +85,9 @@ Envelope fields:
86
85
 
87
86
  - Required: `to`, `type`.
88
87
  - Useful: `summary`, `body`, `from`, `reply_to`, `correlation_id`, `metadata`.
89
- - Addresses: `run:<id>`, `branch:<run>/<branch>`, `room:<run>`, `tool:<name>`, `coordinator`, `session:<id>`.
90
- - Room posts require `from` from the same run (`run:<run>` or `branch:<run>/<branch>`).
88
+ - Core addresses: `run:<id>`, `tool:<name>`.
89
+ - Advanced addresses: `branch:<run>/<branch>`, `room:<run>` for group timeline/roster, `coordinator`, `session:<id>`.
90
+ - Group posts to `room:<run>` require `from` from the same run (`run:<run>` or `branch:<run>/<branch>`).
91
91
  - Runtime termination message: `control.kill` is the only documented actor message that kills a run. `control.stop` and `control.cancel` are actor-local mailbox vocabulary only when a recipe declares and handles them. Terminal retention messages: `control.archive`, `control.prune`.
92
92
  - Long-lived child processes should remain in the run-owned process group unless the recipe implements an explicit daemon termination bridge; do not leave unowned detached services behind `control.kill`.
93
93
 
@@ -99,12 +99,7 @@ Check `inspect view=mailbox` before domain-specific messages.
99
99
  { "target": "run:repo-health", "view": "status" }
100
100
  { "target": "run:repo-health", "view": "tail", "lines": "80" }
101
101
  { "target": "run:repo-health", "view": "messages" }
102
- { "target": "run:repo-health", "view": "communication" }
103
102
  { "target": "run:repo-health", "view": "artifacts" }
104
- { "target": "room:repo-health", "view": "status" }
105
- { "target": "room:repo-health", "view": "roster" }
106
- { "target": "room:repo-health", "view": "contacts" }
107
- { "target": "room:repo-health", "view": "previews" }
108
103
  { "target": "tool:pi-actors", "view": "status" }
109
104
  { "target": "tool:music_player", "view": "status" }
110
105
  { "target": "recipes", "view": "status" }
@@ -116,10 +111,10 @@ Views:
116
111
  - `status`: lifecycle, pid, values, progress, result, compact summary.
117
112
  - `tail`: recent stdout/stderr/log tail.
118
113
  - `messages`: actor messages emitted by the run, or room timeline entries for `room:*`.
119
- - `communication`: run/branch communication snapshot with self/root/default-room/member/contact hints.
120
- - `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
121
- - `contacts`: roster-derived direct-message targets without full roster metadata.
122
- - `previews`: TUI-ready bounded room message previews with timestamp/from/to/type/summary/body_preview.
114
+ - Advanced `communication`: run/branch group-coordination snapshot with self/root/default-room/member/contact hints.
115
+ - Advanced `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
116
+ - Advanced `contacts`: roster-derived direct-message targets without full roster metadata.
117
+ - Advanced `previews`: TUI-ready bounded group-message previews with timestamp/from/to/type/summary/body_preview.
123
118
  - `mailbox`: declared accepts/emits contract for runs; queued direct branch inbox messages for `branch:<run>/<branch>` with `id`, status, route/type, and queue/handling timestamps.
124
119
  - `files`: run state directory file list.
125
120
  - `artifacts`: declared artifact paths/status.
@@ -218,13 +213,13 @@ Only matching filename ids compete. Higher priority shadows lower priority; with
218
213
  Muscle-memory lens: pi-actors has two durable executable-memory layers.
219
214
 
220
215
  1. `~/.pi/agent/recipes/*.json` and `*.md` are the agent's active capability memory. Every recipe in that directory becomes an easy-to-call tool automatically and survives into later sessions. Descriptions matter here because they become the tool's operator-facing title/context.
221
- 2. `~/.pi/agent/recipes/candidates/*.json` is candidate memory captured from successful inline `spawn template=...` runs. Candidates are not registered tools and do not enter the injected tool surface. They remain reusable by explicit path, e.g. `spawn file="~/.pi/agent/recipes/candidates/<name>.json"`, and can be promoted by moving or copying one level up into `~/.pi/agent/recipes`.
216
+ 2. `~/.pi/agent/recipes/drafts/*.json` is draft memory captured from successful inline `spawn template=...` runs. Drafts do not enter the injected tool surface. They remain reusable by explicit path, e.g. `spawn file="~/.pi/agent/recipes/drafts/<name>.json"`, and can be promoted by moving or copying one level up into `~/.pi/agent/recipes`.
222
217
 
223
- Agents grow active memory by calling `register_tool` or by deliberate recipe-file edits. They grow candidate memory by trying ad hoc actors successfully. Treat both as executable habits: candidates are the workbench/proving ground; root recipes are promoted muscle memory.
218
+ Agents grow active memory by calling `register_tool` or by deliberate recipe-file edits. They grow draft memory by trying ad hoc actors successfully. Treat both as executable habits: drafts are the workbench/proving ground; root recipes are promoted muscle memory.
224
219
 
225
220
  Usage lens: user recipes may carry extension-maintained launch metadata such as `usage.calls` and `usage.last_called`. The extension increments the counter when it starts that concrete recipe; agents should not hand-edit counters as part of normal recipe maintenance. Treat usage as evidence for usefulness analysis: heavily used recipes are good candidates for promotion, documentation, or stronger tests; unused recipes are cleanup candidates. Do not use failure counts as a primary usefulness signal because failures may reflect bad caller judgment rather than bad recipes. Do not delete or demote solely from counters without operator approval.
226
221
 
227
- Promotion lens: successful transient/ad hoc actor runs are evidence, not commands. Inline spawns leave candidate recipes as replayable evidence, not active tools. If a candidate is repeatable, parameterized, safe enough, and likely useful later, the agent may promote it by moving/copying it into `~/.pi/agent/recipes` or by calling `register_tool` with a concise name, typed args/defaults, and a reviewed template or recipe path. Do not auto-register every success; do not promote temp paths, secrets, one-off prompts, or project-private assumptions without normalization and approval.
222
+ Promotion lens: successful transient/ad hoc actor runs are evidence, not commands. Inline spawns leave draft recipes as replayable evidence, not active tools. If a draft is repeatable, parameterized, safe enough, and likely useful later, the agent may promote it by moving/copying it into `~/.pi/agent/recipes` or by calling `register_tool` with a concise name, typed args/defaults, and a reviewed template or recipe path. Do not auto-register every success; do not promote temp paths, secrets, one-off prompts, or project-private assumptions without normalization and approval.
228
223
 
229
224
  Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memory set. For each stale, duplicate, too-specific, or low-value recipe, choose one explicit action: keep as a tool, move it out of the agent recipe root to retain recipe-only memory, merge into a better recipe, or delete/archive the file. Prefer moving over deletion when the recipe may still be useful as a component. Never silently remove tools during unrelated work.
230
225
 
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.31.0
5
+ version: 0.33.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -30,8 +30,8 @@ Maintain this skill as a living orchestration standard. When real swarm work exp
30
30
  - `Async Run`: A local lifecycle envelope around a command-template swarm composer or utility. It owns state, logs, status, cancellation, and observability, not swarm semantics.
31
31
  - `Lens`: A deliberately narrow cognitive role assigned to one subagent, such as security, tests, architecture, economics, or operator UX.
32
32
  - `Task Card`: A bounded implementation assignment with goal, allowed files, avoided files, expected output, and validation gates.
33
- - `Component Capability`: An abstract adapter operation such as launcher, reviewer, verifier, merger, quorum, checkpoint, follow-up, judge, or normalizer. Swarm may target these capabilities, but local adapters bind them to concrete tools, recipes, candidate recipes, command templates, async runs, or services.
34
- - `Candidate Recipe`: A reusable but non-registered recipe captured from a successful inline actor spawn. It can be replayed by explicit file path and later promoted into the active tool recipe root after enough dogfood.
33
+ - `Component Capability`: An abstract adapter operation such as launcher, reviewer, verifier, merger, quorum, checkpoint, follow-up, judge, or normalizer. Swarm may target these capabilities, but local adapters bind them to concrete tools, recipes, draft recipes, command templates, async runs, or services.
34
+ - `Draft Recipe`: A reusable but non-registered recipe captured from a successful inline actor spawn under `~/.pi/agent/recipes/drafts`. It can be replayed by explicit file path and later promoted into the active tool recipe root after enough dogfood.
35
35
  - `Coordinator Checkpoint`: A deliberate subagent pause where the subagent preserves its working context, sends a bounded question or status to the orchestrator, receives a coordinator reply, and continues in the same subagent context.
36
36
  - `Evidence Checkpoint`: A deliberate stop where a subagent records sources, assumptions, confidence, contradictions, or blocking evidence gaps before synthesis.
37
37
  - `Integrator`: The human or agent that merges isolated branches/worktrees into the shared target and owns conflict resolution.
@@ -199,7 +199,7 @@ Recipes can declare their conversational surface:
199
199
  }
200
200
  ```
201
201
 
202
- The implementation supports `status`, `tail`, `messages`, `artifacts`, `files`, `mailbox`, and `communication` for `run:<id>` actors, `status`, `messages`, `previews`, `roster`, and `contacts` for `room:<run>` actors, `status`/`runs` for `coordinator`, `session:<id>`, and `session:all` actors with optional status filtering, and `status`/`schema` for registered `tool:<name>` actors. Run mailbox inspection shows recipe-declared mailbox metadata plus recent durable run inbox entries; branch mailbox inspection shows branch-local queued/claimed/handled records. Room `status` returns compact message/roster counts plus `last_message_at`, `last_message_from`, `last_message_type`, and `last_message_summary` when available, without parsing the full timeline into actor envelopes. Use `messages` for actor-envelope inspection. `inspect target=coordinator` requires a current coordinator session; use `session:<id>` or `session:all` when the session is intentionally explicit. Direct `run:<id>` and `room:<run>` inspection respects coordinator-session ownership when the current session is known. `inspect` is for decision points and diagnosis only; examples must not teach sleep-then-inspect polling.
202
+ The implementation supports `status`, `tail`, `messages`, `artifacts`, `files`, `mailbox`, and `communication` for `run:<id>` actors, `status`, `messages`, `previews`, `roster`, and `contacts` for `room:<run>` actors, `status`/`runs` for `coordinator`, `session:<id>`, and `session:all` actors with optional status filtering, and `status`/`schema` for registered `tool:<name>` actors. Run mailbox inspection shows recipe-declared mailbox metadata plus recent durable run inbox entries; branch mailbox inspection shows branch-local queued/claimed/handled records. Room `status` returns compact message/roster counts plus `last_message_at`, `last_message_from`, `last_message_type`, and `last_message_summary` when available, without parsing the full timeline into actor envelopes. Use `messages` for actor-envelope inspection. `inspect target=coordinator` requires a current coordinator session; use `session:<id>` or `session:all` when the session is intentionally explicit. Direct `run:<id>` and `room:<run>` inspection respects coordinator-session ownership when the current session is known. Ownership denials use `reason=session_mismatch owner_session=<id> current_session=<id> hint=inspect_session:<id>`; recover by inspecting the hinted `session:<id>` instead of forcing cross-session control. `inspect` is for decision points and diagnosis only; examples must not teach sleep-then-inspect polling.
203
203
 
204
204
  ## Runtime Direction
205
205
 
@@ -10,7 +10,7 @@ The registry source is location-discovered recipes, not a live tool-only JSON fi
10
10
 
11
11
  - `~/.pi/agent/recipes/*.json` and `*.md` are the highest-priority user recipe root and the operator-managed tool set.
12
12
  - Recipes in that root are tools by location.
13
- - `~/.pi/agent/recipes/candidates/*.json` are captured inline-spawn candidates, not registered tools; promote one by moving or copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths/descriptions for explicit replay by file path.
13
+ - `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one by moving or copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths/descriptions for explicit replay by file path.
14
14
  - Packaged pi-actors recipes are the lower-priority standard library of declarative actor config components, not automatically registered tools.
15
15
  - Ad hoc recipe files outside the user recipe root are components unless explicitly registered/copied into `~/.pi/agent/recipes`.
16
16
  - Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` and `docs_review.md` both have id/tool name `docs_review`.
@@ -31,9 +31,9 @@ inspect target=recipes view=summary verbose=true
31
31
 
32
32
  `tool:pi-actors` is a reserved runtime-status actor: it reports the loaded package version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available. Use it after reloads to confirm which extension code is actually live.
33
33
 
34
- The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority candidate paths when a broken or disabled higher-priority recipe masks a fallback.
34
+ The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority fallback paths when a broken or disabled higher-priority recipe masks a fallback.
35
35
 
36
- Routine shadowing is quiet. If a bare `spawn` recipe launch already fails because an invalid or `disabled: true` user recipe blocks a lower-priority candidate, the launch error adds compact tokens such as `reason=shadowed_invalid` or `reason=shadowed_disabled`, `active_path`, `blocked_candidate`, and `hint=inspect_recipes_doctor`.
36
+ Routine shadowing is quiet. If a bare `spawn` recipe launch already fails because an invalid or `disabled: true` user recipe blocks a lower-priority fallback, the launch error adds compact tokens such as `reason=shadowed_invalid` or `reason=shadowed_disabled`, `active_path`, `blocked_fallback`, and `hint=inspect_recipes_doctor`.
37
37
 
38
38
  ## Registering Tools
39
39
 
package/index.ts CHANGED
@@ -102,7 +102,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
102
102
  onChange: () =>
103
103
  activeRunContext && scheduleRunEventUpdate(activeRunContext),
104
104
  });
105
- const actorToolDefinitions = new Map<string, any>();
105
+ const actorToolDefinitions = new Map<string, Tools.ActorToolDefinition>();
106
106
  const runtime = Runtime.createAutoToolsRuntime({
107
107
  configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
108
108
  exec: CommandTemplates.execCommandTemplate,
@@ -1021,19 +1021,31 @@ function formatRecipePersistenceSuggestion(transition: RunTransition): string {
1021
1021
  return `\nAgent note: this actor was spawned directly and completed successfully. If this pattern fits this machine's recurring workflow, ask the operator whether to save it as a durable recipe/tool under ~/.pi/agent/recipes with register_tool. Do not auto-save without confirmation.`;
1022
1022
  }
1023
1023
 
1024
+ function formatTransitionNextActions(transition: RunTransition): string {
1025
+ const actions = [
1026
+ `inspect target=run:${transition.run} view=status`,
1027
+ transition.to === "done" && Object.keys(transition.artifacts ?? {}).length > 0
1028
+ ? `inspect target=run:${transition.run} view=artifacts`
1029
+ : `inspect target=run:${transition.run} view=tail`,
1030
+ `inspect target=run:${transition.run} view=messages`,
1031
+ ].filter(Boolean);
1032
+ return `\nNext actions: ${actions.join(" | ")}`;
1033
+ }
1034
+
1024
1035
  export function formatRunTransitionMessage(transition: RunTransition): string {
1025
1036
  const artifacts = formatNamedArtifacts(transition.artifacts);
1026
1037
  const runFiles = formatRunFileList(getRunArtifacts(transition));
1027
1038
  const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
1039
+ const nextActions = formatTransitionNextActions(transition);
1028
1040
  if (transition.to === "done")
1029
- return `Run ${transition.run} completed successfully.${artifacts}${runFiles}\nUse inspect target=run:${transition.run} view=status or view=tail if the result needs inspection.${persistenceSuggestion}`;
1041
+ return `Run ${transition.run} completed successfully.${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
1030
1042
  if (transition.to === "failed")
1031
- return `Run ${transition.run} failed.${artifacts}${runFiles}\nUse inspect target=run:${transition.run} view=status or view=tail for details.`;
1043
+ return `Run ${transition.run} failed.${artifacts}${runFiles}${nextActions}`;
1032
1044
  if (transition.to === "cancelled")
1033
- return `Run ${transition.run} was cancelled. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
1045
+ return `Run ${transition.run} was cancelled.${nextActions}`;
1034
1046
  if (transition.to === "killed")
1035
- return `Run ${transition.run} was force-killed. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
1047
+ return `Run ${transition.run} was force-killed.${nextActions}`;
1036
1048
  if (transition.to === "exited")
1037
- return `Run ${transition.run} exited before writing a result. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
1038
- return `Run ${transition.run} finished with status ${transition.to}. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
1049
+ return `Run ${transition.run} exited before writing a result.${nextActions}`;
1050
+ return `Run ${transition.run} finished with status ${transition.to}.${nextActions}`;
1039
1051
  }