@llblab/pi-actors 0.30.2 → 0.32.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
@@ -128,12 +128,23 @@ function asRecord(value) {
128
128
  function formatFailureCount(value) {
129
129
  return Array.isArray(value) ? value.length : undefined;
130
130
  }
131
+ function actorRunNextActions(run) {
132
+ const id = String(run ?? "").trim();
133
+ if (!id)
134
+ return [];
135
+ return [
136
+ `inspect target=run:${id} view=status`,
137
+ `inspect target=run:${id} view=messages`,
138
+ `message to=run:${id} type=<actor.action>`,
139
+ ];
140
+ }
131
141
  function compactAsyncRunStatus(value) {
132
142
  const status = asRecord(value);
133
143
  const progress = asRecord(status.progress);
134
144
  const result = asRecord(status.result);
145
+ const run = String(status.run ?? "<unknown>");
135
146
  const tokens = [
136
- `run=${String(status.run ?? "<unknown>")}`,
147
+ `run=${run}`,
137
148
  `status=${String(status.status ?? "unknown")}`,
138
149
  ];
139
150
  if (status.tool)
@@ -157,8 +168,12 @@ function compactAsyncRunStatus(value) {
157
168
  tokens.push(`code=${String(result.code)}`);
158
169
  if (result.killed === true)
159
170
  tokens.push("killed=true");
160
- if (status.candidate_recipe)
161
- tokens.push(`candidate_recipe=${String(status.candidate_recipe)}`);
171
+ const draftRecipe = status.draft_recipe ?? status.candidate_recipe;
172
+ if (draftRecipe)
173
+ tokens.push(`draft_recipe=${String(draftRecipe)}`);
174
+ const nextActions = actorRunNextActions(run);
175
+ if (nextActions.length > 0)
176
+ tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
162
177
  return `\n${tokens.join(" ")}`;
163
178
  }
164
179
  function compactRunMessages(messages) {
@@ -343,6 +358,15 @@ function compactArtifactPath(value) {
343
358
  const record = asRecord(value);
344
359
  return String(record.path ?? "<missing>");
345
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
+ }
346
370
  function compactActorFiles(status) {
347
371
  const run = String(status.run ?? "<unknown>");
348
372
  const artifacts = asRecord(status.artifacts);
@@ -361,7 +385,11 @@ function compactActorFiles(status) {
361
385
  .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
362
386
  .join(",")}`
363
387
  : "";
364
- 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}`;
365
393
  }
366
394
  function summarizeOtherSessions(currentSession, allRuns) {
367
395
  const otherRuns = allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession);
@@ -481,8 +509,42 @@ function compactRecipeDoctor(summary) {
481
509
  : "";
482
510
  lines.push(`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`);
483
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)}`;
484
517
  return `\n${lines.join("\n")}`;
485
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
+ }
486
548
  function compactRecipeRegistry(summary) {
487
549
  const active = Array.isArray(summary.active) ? summary.active.length : 0;
488
550
  const shadowed = Array.isArray(summary.shadowed)
@@ -495,13 +557,41 @@ function compactRecipeRegistry(summary) {
495
557
  const diagnostics = Array.isArray(summary.diagnostics)
496
558
  ? summary.diagnostics.length
497
559
  : 0;
498
- const candidates = Array.isArray(summary.candidates)
499
- ? summary.candidates.length
500
- : 0;
560
+ const drafts = Array.isArray(summary.drafts)
561
+ ? summary.drafts.length
562
+ : Array.isArray(summary.candidates)
563
+ ? summary.candidates.length
564
+ : 0;
501
565
  const recommendations = Array.isArray(summary.recommendations)
502
566
  ? summary.recommendations.length
503
567
  : 0;
504
- return `\nrecipes active=${active} candidates=${candidates} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}`;
568
+ const nextActions = Array.isArray(summary.next_actions)
569
+ ? summary.next_actions
570
+ : [];
571
+ return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
572
+ }
573
+ function actorMessageNextActions(message, result) {
574
+ const actions = [];
575
+ const address = ActorMessages.parseActorAddress(message.to);
576
+ if (result.delivery_error || result.sent === false) {
577
+ if (address.kind === "run" && address.value) {
578
+ actions.push(`inspect target=run:${address.value} view=status`);
579
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
580
+ }
581
+ else if (address.kind === "branch" && address.value) {
582
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
583
+ actions.push(`inspect target=run:${address.value} view=status`);
584
+ }
585
+ }
586
+ if (result.queued === true) {
587
+ if (address.kind === "branch" && address.value) {
588
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
589
+ }
590
+ else if (address.kind === "run" && address.value) {
591
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
592
+ }
593
+ }
594
+ return [...new Set(actions)].slice(0, 3);
505
595
  }
506
596
  function compactActorMessageResult(message, result) {
507
597
  const tokens = [
@@ -534,6 +624,11 @@ function compactActorMessageResult(message, result) {
534
624
  if (result.delivery_error) {
535
625
  tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
536
626
  }
627
+ const nextActions = Array.isArray(result.next_actions)
628
+ ? result.next_actions
629
+ : actorMessageNextActions(message, result);
630
+ if (nextActions.length > 0)
631
+ tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
537
632
  return `\n${tokens.join(" ")}`;
538
633
  }
539
634
  function maybeJsonText(value, verbose, compact) {
@@ -635,7 +730,7 @@ function writeSpawnCandidateRecipe(input, meta) {
635
730
  const defaults = candidateRecipeDefaults(meta.values);
636
731
  const recipe = {
637
732
  async: true,
638
- description: `Candidate recipe captured from spawn run ${String(meta.run)}`,
733
+ description: `Draft recipe captured from spawn run ${String(meta.run)}`,
639
734
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
640
735
  ...(defaults ? { defaults } : {}),
641
736
  template: input.template,
@@ -720,7 +815,7 @@ export function createSpawnToolDefinition() {
720
815
  return {
721
816
  name: "spawn",
722
817
  label: "Spawn",
723
- description: "Create an addressable actor from a recipe file or inline command template. Currently spawns run:<id> actors backed by async runs.",
818
+ description: "Create an addressable actor from a recipe file or inline command template. Use instead of ad hoc shell backgrounding for work that may outlive this turn, needs steering/follow-up/artifacts, runs as a service, fans out, or should be inspected later. Currently spawns run:<id> actors backed by async runs.",
724
819
  parameters: objectSchema({
725
820
  artifacts: looseObjectSchema("Optional named artifact paths for the spawned actor."),
726
821
  as: stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
@@ -770,9 +865,14 @@ export function createSpawnToolDefinition() {
770
865
  throw enhanceSpawnRecipeError(error, recipe);
771
866
  }
772
867
  const candidateRecipe = writeSpawnCandidateRecipe(input, meta);
773
- const details = candidateRecipe
774
- ? { ...meta, candidate_recipe: candidateRecipe }
775
- : meta;
868
+ const nextActions = actorRunNextActions(meta.run);
869
+ const details = {
870
+ ...meta,
871
+ ...(candidateRecipe
872
+ ? { candidate_recipe: candidateRecipe, draft_recipe: candidateRecipe }
873
+ : {}),
874
+ next_actions: nextActions,
875
+ };
776
876
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
777
877
  ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
778
878
  return {
@@ -797,15 +897,29 @@ function requireContextSessionId(ctx, actor) {
797
897
  }
798
898
  return sessionId;
799
899
  }
900
+ function sessionMismatchError(input) {
901
+ const ownerSession = input.expectedSession ?? "none";
902
+ const currentSession = input.currentSession ?? "none";
903
+ const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
904
+ const hintTarget = input.expectedSession
905
+ ? `session:${input.expectedSession}`
906
+ : "session:all";
907
+ return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`), {
908
+ current_session: input.currentSession,
909
+ hint: `inspect target=${hintTarget} view=status`,
910
+ owner_session: input.expectedSession,
911
+ reason: "session_mismatch",
912
+ run: input.run,
913
+ target: input.target,
914
+ });
915
+ }
800
916
  function assertRunAccessibleToContext(runId, ctx) {
801
917
  const status = AsyncRuns.getRunStatus(runId);
802
918
  const sessionId = getContextSessionId(ctx);
803
919
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
804
- throw Object.assign(new Error(`run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`), {
805
- current_session: sessionId,
806
- hint: `inspect target=session:${status.ownerId} view=status`,
807
- owner_session: status.ownerId,
808
- reason: "session_mismatch",
920
+ throw sessionMismatchError({
921
+ currentSession: sessionId,
922
+ expectedSession: String(status.ownerId),
809
923
  run: runId,
810
924
  });
811
925
  }
@@ -818,13 +932,13 @@ export function createInspectToolDefinition(deps = {}) {
818
932
  return {
819
933
  name: "inspect",
820
934
  label: "Inspect",
821
- description: "Intentionally inspect an actor. 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.",
935
+ 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.",
822
936
  parameters: objectSchema({
823
937
  lines: stringSchema("Line count for tail/messages views. Default 40."),
824
938
  status: stringSchema("Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited."),
825
- target: stringSchema("Actor address to inspect, e.g. run:<id>, room:<run>, coordinator, session:<id>, session:all, or tool:<name>."),
939
+ target: stringSchema("Actor address to inspect, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>, session:all."),
826
940
  verbose: booleanSchema("Return full JSON instead of compact text where available."),
827
- view: stringSchema("Inspection view: status, tail, messages, artifacts, files, mailbox, communication, roster, or contacts."),
941
+ view: stringSchema("Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts."),
828
942
  }, ["target", "view"]),
829
943
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
830
944
  const input = asRecord(params);
@@ -846,10 +960,15 @@ export function createInspectToolDefinition(deps = {}) {
846
960
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
847
961
  ]);
848
962
  const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
849
- const summary = {
963
+ const summaryBase = {
850
964
  ...RecipeDiscovery.summarizeDiscovery(discovered),
965
+ drafts: RecipeDiscovery.listCandidateRecipes(join(recipeRoot, "candidates")),
851
966
  candidates: RecipeDiscovery.listCandidateRecipes(join(recipeRoot, "candidates")),
852
967
  };
968
+ const summary = {
969
+ ...summaryBase,
970
+ next_actions: recipeRegistryNextActions(summaryBase, view),
971
+ };
853
972
  return {
854
973
  content: [
855
974
  {
@@ -1067,7 +1186,11 @@ export function createInspectToolDefinition(deps = {}) {
1067
1186
  const status = assertRunAccessibleToContext(runId, ctx);
1068
1187
  const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
1069
1188
  const details = artifactManifest
1070
- ? { ...status, artifact_manifest: artifactManifest }
1189
+ ? {
1190
+ ...status,
1191
+ artifact_manifest: artifactManifest,
1192
+ next_actions: artifactNextActions(status.run ?? runId, asRecord(status.artifacts)),
1193
+ }
1071
1194
  : status;
1072
1195
  return {
1073
1196
  content: [
@@ -1122,7 +1245,7 @@ export function createActorMessageToolDefinition(deps = {}) {
1122
1245
  return {
1123
1246
  name: "message",
1124
1247
  label: "Message",
1125
- description: "Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, room:<run> timelines/rosters, tool:<name> calls, and coordinator/session-bound run messages.",
1248
+ 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>.",
1126
1249
  parameters: objectSchema({
1127
1250
  body: unionSchema([
1128
1251
  stringSchema("Message body. For run:<id>, this is the run-local command line."),
@@ -1134,7 +1257,7 @@ export function createActorMessageToolDefinition(deps = {}) {
1134
1257
  metadata: looseObjectSchema("Optional structured metadata for routing or domain hints."),
1135
1258
  reply_to: stringSchema("Optional message id this message replies to."),
1136
1259
  summary: stringSchema("Optional short human-facing summary."),
1137
- to: stringSchema("Destination actor address, e.g. run:<id>, branch:<run>/<branch>, room:<run>, coordinator, session:<id>, or tool:<name>."),
1260
+ to: stringSchema("Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>."),
1138
1261
  type: stringSchema("Semantic message type, e.g. control.approve or checkpoint.needs_scope."),
1139
1262
  verbose: booleanSchema("Return full JSON instead of compact text."),
1140
1263
  }, ["to", "type"]),
@@ -1249,10 +1372,20 @@ export function createActorMessageToolDefinition(deps = {}) {
1249
1372
  const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
1250
1373
  if (address.kind === "session") {
1251
1374
  if (!senderStatus.ownerId) {
1252
- throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got no owner.`);
1375
+ throw sessionMismatchError({
1376
+ currentSession: undefined,
1377
+ expectedSession: address.value,
1378
+ run: sender.value,
1379
+ target: `session:${address.value}`,
1380
+ });
1253
1381
  }
1254
1382
  if (senderStatus.ownerId !== address.value) {
1255
- throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`);
1383
+ throw sessionMismatchError({
1384
+ currentSession: String(senderStatus.ownerId),
1385
+ expectedSession: address.value,
1386
+ run: sender.value,
1387
+ target: `session:${address.value}`,
1388
+ });
1256
1389
  }
1257
1390
  }
1258
1391
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
@@ -1274,14 +1407,18 @@ export function createActorMessageToolDefinition(deps = {}) {
1274
1407
  else {
1275
1408
  throw new Error(`message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`);
1276
1409
  }
1410
+ const nextActions = actorMessageNextActions(message, result);
1411
+ const resultWithNext = nextActions.length
1412
+ ? { ...result, next_actions: nextActions }
1413
+ : result;
1277
1414
  return {
1278
1415
  content: [
1279
1416
  {
1280
1417
  type: "text",
1281
- text: maybeJsonText({ message, result }, input.verbose === true, compactActorMessageResult(message, result)),
1418
+ text: maybeJsonText({ message, result: resultWithNext }, input.verbose === true, compactActorMessageResult(message, resultWithNext)),
1282
1419
  },
1283
1420
  ],
1284
- details: { message, result },
1421
+ details: { message, result: resultWithNext },
1285
1422
  };
1286
1423
  },
1287
1424
  };
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: actors
3
- description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
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.30.2
5
+ version: 0.32.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
9
9
 
10
- `pi-actors` turns trusted local capabilities into addressable actors. This skill is the compact operator/reference layer for the extension itself: tools, nouns, lifecycle, message protocol, recipes, and common edge cases. It is not a multi-agent strategy guide; use a swarm skill for decomposition, quorum design, reviewer lenses, and consensus methodology.
10
+ `pi-actors` turns trusted local capabilities into addressable actors. This skill is the required practical layer for any non-trivial pi-actors use or repo change: tools, nouns, lifecycle, message protocol, recipes, and common edge cases. It is not a multi-agent strategy guide; use a swarm skill for decomposition, quorum design, reviewer lenses, and consensus methodology.
11
11
 
12
12
  Maintain this skill as the extension's agent-facing manual. When implementation changes reveal new durable mechanics, invariants, warnings, or safer operating patterns, update this skill alongside code/docs so future agents learn the current actor model instead of rediscovering it.
13
13
 
@@ -39,15 +39,16 @@ 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
 
50
+ Actor-mode trigger: if work may outlive this turn, needs steering/follow-up/artifacts, runs as a service, fans out, or should be resumed/inspected later, use `spawn → message → inspect` instead of ad hoc shell backgrounding. Keep short foreground checks as normal tools.
51
+
51
52
  ### `spawn` — create a run actor
52
53
 
53
54
  Use for long work, background services, subagents, fanout, pipelines, and reusable recipes.
@@ -84,8 +85,9 @@ Envelope fields:
84
85
 
85
86
  - Required: `to`, `type`.
86
87
  - Useful: `summary`, `body`, `from`, `reply_to`, `correlation_id`, `metadata`.
87
- - Addresses: `run:<id>`, `branch:<run>/<branch>`, `room:<run>`, `tool:<name>`, `coordinator`, `session:<id>`.
88
- - 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>`).
89
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`.
90
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`.
91
93
 
@@ -97,12 +99,7 @@ Check `inspect view=mailbox` before domain-specific messages.
97
99
  { "target": "run:repo-health", "view": "status" }
98
100
  { "target": "run:repo-health", "view": "tail", "lines": "80" }
99
101
  { "target": "run:repo-health", "view": "messages" }
100
- { "target": "run:repo-health", "view": "communication" }
101
102
  { "target": "run:repo-health", "view": "artifacts" }
102
- { "target": "room:repo-health", "view": "status" }
103
- { "target": "room:repo-health", "view": "roster" }
104
- { "target": "room:repo-health", "view": "contacts" }
105
- { "target": "room:repo-health", "view": "previews" }
106
103
  { "target": "tool:pi-actors", "view": "status" }
107
104
  { "target": "tool:music_player", "view": "status" }
108
105
  { "target": "recipes", "view": "status" }
@@ -114,10 +111,10 @@ Views:
114
111
  - `status`: lifecycle, pid, values, progress, result, compact summary.
115
112
  - `tail`: recent stdout/stderr/log tail.
116
113
  - `messages`: actor messages emitted by the run, or room timeline entries for `room:*`.
117
- - `communication`: run/branch communication snapshot with self/root/default-room/member/contact hints.
118
- - `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
119
- - `contacts`: roster-derived direct-message targets without full roster metadata.
120
- - `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.
121
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.
122
119
  - `files`: run state directory file list.
123
120
  - `artifacts`: declared artifact paths/status.
@@ -216,13 +213,13 @@ Only matching filename ids compete. Higher priority shadows lower priority; with
216
213
  Muscle-memory lens: pi-actors has two durable executable-memory layers.
217
214
 
218
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.
219
- 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/candidates/*.json` is draft memory captured from successful inline `spawn template=...` runs. The directory name is retained for compatibility; treat these as drafts, not active tools. Drafts 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`.
220
217
 
221
- 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.
222
219
 
223
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.
224
221
 
225
- 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.
226
223
 
227
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.
228
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.30.2
5
+ version: 0.32.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. It can be replayed by explicit file path and later promoted into the active tool recipe root after enough dogfood. Its compatibility storage path may still include `recipes/candidates`.
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
 
@@ -6,6 +6,8 @@ Async runs are detached executions of a template recipe or inline command templa
6
6
 
7
7
  **Scope:** run id, state path, runner pid, process-group cancellation, logs, status, tail, list, script-authored actor messages, run-local control messages, cancel, force-kill, terminal result state, ambient activity indicators, and extension-owned temp storage. No scheduler, queue daemon, workflow DSL, distributed worker, or second execution language.
8
8
 
9
+ Actor-mode trigger: choose an async run when work may outlive the current turn, needs later steering or inspection, produces artifacts/follow-ups, runs as a service, fans out, or should become repeatable recipe memory. Keep short foreground checks in ordinary tools/templates.
10
+
9
11
  Layer boundary: async-run configuration may inject lifecycle values such as `{run_id}` and `{state_dir}` and may choose detached execution through `async: true`, but it does not add command-template graph syntax. Recipe imports and recipe-local references belong to the template-recipe layer; status, control messages, actor messages, cancel, and kill belong to the async-run layer.
10
12
 
11
13
  ---
@@ -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/candidates/*.json` stores captured inline-spawn draft recipes, not registered tools. The directory name is retained for compatibility; model-facing output calls them drafts. 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`.
@@ -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
  }
package/lib/prompts.ts CHANGED
@@ -27,12 +27,12 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
27
27
  - ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
28
28
  - Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.
29
29
  - Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
30
- - Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.
30
+ - Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.
31
+ - Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.
31
32
  - Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
32
33
  - Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
33
- - Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.
34
34
  - Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.
35
- - For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.`;
35
+ - For any non-trivial actor use or pi-actors change, read the bundled actors skill first; for deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
36
36
 
37
37
  export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
38
38
  name: "Tool name in snake_case (e.g., 'transcribe')",