@llblab/pi-actors 0.31.0 → 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/AGENTS.md CHANGED
@@ -61,6 +61,7 @@ Pi host
61
61
  - Prefer explicit operator action over silent user-config rewrites.
62
62
  - Keep published documentation portable: use `~`, `<repo>`, or relative paths instead of machine-local absolute paths.
63
63
  - Preserve runtime output discipline because tool output flows directly into agent context.
64
+ - Optimize every actor-facing surface for signal over volume: prefer compact state-backed hints and fewer concepts over broad explanatory prose or speculative guidance.
64
65
  - Keep the project lens local-first and cybernetic: agents wrap durable local capabilities as actors, then use semantic tools and messages instead of repeatedly reconstructing shell commands.
65
66
  - Design recipes as agent-callable tools: make prompts, scopes, paths, models, and policy knobs public args/defaults when the caller should decide them at invocation time.
66
67
  - Decompose oversized bullets into sublists or hierarchy; long flat list items are a context-smell.
@@ -80,6 +81,7 @@ Pi host
80
81
  ## Public Actor Model
81
82
 
82
83
  - Preserve the public verbs: `spawn`, `message`, `inspect`.
84
+ - Keep the model-facing concept ladder minimal: core is run actors, typed messages, intentional inspection, artifacts, and recipe/tool memory; group messaging, roster, branches, sessions, and diagnostics are advanced surfaces.
83
85
  - Prefer one typed actor-message envelope for upward, downward, lateral, parent/branch, and branch/parent messages.
84
86
  - Prefer actor addresses and inspect views over exposing FIFO, outbox, or status mechanics as public concepts.
85
87
  - Keep route and semantic type separate: delivery behavior comes from `to`, while `type` describes intent.
@@ -116,6 +118,7 @@ Pi host
116
118
  ## State, IO, And Safety
117
119
 
118
120
  - Tool stdout and temp state must stay bounded and local.
121
+ - Feedback hints must be evidence-backed, bounded, and action-shaped; prefer `next_actions` pointing to existing verbs over prose, and avoid hints when no concrete next step is justified.
119
122
  - Keep tail truncation, full-output temp files, failure formatting, and centralized limits intact.
120
123
  - Published docs must not include machine-local absolute paths.
121
124
  - Any view scanning run directories must apply coordinator/session ownership filters before exposing summaries or previews.
package/BACKLOG.md CHANGED
@@ -47,25 +47,12 @@ No open hotfix items.
47
47
  - File length alone is not a domain-split trigger: ~1000-line cohesive domain files are acceptable when ownership is clear.
48
48
  - Consider splitting only when a file crosses roughly 2000 lines, mixes real ownership zones, or hides a clearer domain boundary.
49
49
  - Prefer semantic compression before file splitting: fewer public nouns, consistent outcomes, compact diagnostics, and domain-owned constants/helpers.
50
+ - Preserve signal/noise balance: feedback should be state-backed, compact, and action-shaped; do not add advisory prose just because a surface exists.
50
51
 
51
52
  ## Minor Backlog
52
53
 
53
54
  The backlog is intentionally pruned to the 20% of work most likely to deliver 80% of value for `pi-actors` as a local actor kernel. Bias toward consolidation, smaller public surface area, and reliability over new feature breadth.
54
55
 
55
- ### M-14 Session Mismatch Follow-through
56
-
57
- - Priority: Medium.
58
- - Status: Planned.
59
- - Goal: Extend 0.27 structured session diagnostics consistently across room, branch, run, coordinator, and session workflows.
60
- - Why now: M-12 established the shape; dogfood should now make every ownership denial equally actionable without relaxing ownership gates.
61
- - Direction:
62
- - Audit all session mismatch errors for consistent `reason`, owner/current session fields, and inspect-session hints.
63
- - Keep read/write ownership policy unchanged.
64
- - Update docs with session mismatch examples and recovery inspection paths.
65
- - Acceptance:
66
- - Room, branch, run, coordinator, and session denials share the same compact/verbose shape.
67
- - Tests cover representative inspect and message paths.
68
-
69
56
  ### M-15 Worker Stale-Claim Dogfood
70
57
 
71
58
  - Priority: Medium.
@@ -111,23 +98,23 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
111
98
  - Room messages distinguish timeline append success from forwarded branch-targeted copies.
112
99
  - Tests cover at least run, branch, room, coordinator, and ownership-denied outcomes.
113
100
 
114
- ### M-18 Candidate Recipe Promotion UX
101
+ ### M-18 Draft Recipe Promotion UX
115
102
 
116
103
  - Priority: High.
117
104
  - Status: Planned.
118
- - Goal: Make successful ad hoc actor patterns easy to promote manually from candidate memory into active user recipe memory.
119
- - Why now: Candidate recipes under `~/.pi/agent/recipes/candidates` are replayable but intentionally not active tools; the two-stage memory model now needs an explicit operator-gated promotion path.
105
+ - Goal: Make successful ad hoc actor patterns easy to promote manually from draft memory into active user recipe memory.
106
+ - Why now: Draft recipes under `~/.pi/agent/recipes/candidates` are replayable but intentionally not active tools; the directory name is retained for compatibility, and the two-stage memory model now needs an explicit operator-gated promotion path.
120
107
  - Direction:
121
- - List candidate recipes with source run, timestamp, fingerprint, description/template preview, and validation status.
122
- - Promote a selected candidate to `~/.pi/agent/recipes/<name>.json` only through an explicit action or explicit tool argument.
108
+ - List draft recipes with source run, timestamp, fingerprint, description/template preview, and validation status.
109
+ - Promote a selected draft to `~/.pi/agent/recipes/<name>.json` only through an explicit action or explicit tool argument.
123
110
  - Run recipe validation/doctor before writing and expose collision/shadowing diagnostics.
124
- - Preserve candidate files unless deletion is explicitly requested.
111
+ - Preserve draft files unless deletion is explicitly requested.
125
112
  - Prefer extending existing registry/tool surfaces over adding a new public noun.
126
113
  - Acceptance:
127
- - Candidate recipes remain non-tools until promotion.
114
+ - Draft recipes remain non-tools until promotion.
128
115
  - Promotion writes atomically and never auto-promotes.
129
- - Tests cover valid promotion, invalid candidate, name collision, and packaged-recipe shadowing.
130
- - Docs explain candidate memory vs active tool memory in one compact section.
116
+ - Tests cover valid promotion, invalid draft, name collision, and packaged-recipe shadowing.
117
+ - Docs explain draft memory vs active tool memory in one compact section.
131
118
 
132
119
  ### M-24 Registry Path Naming Cleanup
133
120
 
@@ -166,10 +153,10 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
166
153
  - Priority: Medium.
167
154
  - Status: Planned.
168
155
  - Goal: Add one compact operator triage view that answers what needs attention right now without performing repairs.
169
- - Why now: Runtime status, recipe doctor, candidates, stale claims, session mismatches, failed runs, and other-session counts are currently separate bounded surfaces.
156
+ - Why now: Runtime status, recipe doctor, drafts, stale claims, session mismatches, failed runs, and other-session counts are currently separate bounded surfaces.
170
157
  - Direction:
171
158
  - Add `inspect target=tool:pi-actors view=triage` or an equivalent existing inspect surface.
172
- - Summarize runtime version/mode, active runs, other-session runs, invalid or blocking recipes, high-risk recipes, candidate recipes, stale worker claims, recent failed runs, attention messages, and suggested next inspect actions.
159
+ - Summarize runtime version/mode, active runs, other-session runs, invalid or blocking recipes, high-risk recipes, draft recipes, stale worker claims, recent failed runs, attention messages, and suggested next inspect actions.
173
160
  - Keep every warning tied to a next inspect/action hint.
174
161
  - Do not auto-repair, auto-prune, relax ownership, or hide detailed source-of-truth views.
175
162
  - Acceptance:
@@ -226,7 +213,7 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
226
213
  ## Suggested Milestone Order
227
214
 
228
215
  ```text
229
- Next milestone: M-14 Session Mismatch Follow-through.
230
- Then: M-15 Worker Stale-Claim Dogfood → M-17 Message Delivery Outcome Contract → M-18 Candidate Recipe Promotion UX.
216
+ Next milestone: M-15 Worker Stale-Claim Dogfood.
217
+ Then: M-17 Message Delivery Outcome Contract → M-18 Draft Recipe Promotion UX.
231
218
  Small cleanup lane: M-23 Tool Boundary Type Tightening → M-24 Registry Path Naming Cleanup.
232
219
  ```
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.32.0: Actor Surface Minimization
6
+
7
+ - `[Context]` Added durable signal/noise guidance for actor-facing surfaces: keep the model-facing concept ladder minimal, make feedback hints state-backed and action-shaped, and avoid speculative advisory prose.
8
+ - `[Backlog]` Added critical concept-surface compression and actor feedback-loop strengthening tracks to guide the next minimization-focused development cycle.
9
+ - `[Concepts]` Compressed model-facing language by presenting captured inline-spawn recipes as drafts, treating `room:<run>` as advanced group messaging plus roster, demoting coordinator/session/debug views from golden-path guidance, and keeping compatibility names/paths as storage details rather than core onboarding nouns.
10
+ - `[Feedback]` Added bounded next-action hints to recipe registry/doctor inspection, artifact inspection, delivery-fallback message results, and terminal run follow-ups so state-backed surfaces point back to `inspect`, `message`, `spawn`, or draft promotion without polling or auto-repair.
11
+ - `[Sessions]` Normalized session-directed message ownership failures onto the same structured `reason=session_mismatch`, owner/current session, and inspect-session hint shape used by run, branch, room, and coordinator ownership denials.
12
+
5
13
  ## 0.31.0: Agent Adoption Ergonomics
6
14
 
7
15
  - `[Adoption]` Added a compact actor-mode trigger rule to the injected prompt, actors skill, README, and async-run docs so models prefer `spawn → message → inspect` for long-lived, stateful, follow-up, artifact, service, fanout, and resumable work while keeping short foreground checks as ordinary tools.
package/README.md CHANGED
@@ -66,16 +66,21 @@ The npm package is dist-first for JavaScript-only runtimes: default Pi metadata
66
66
 
67
67
  ## Address Surface
68
68
 
69
- Actors and coordination endpoints are addressed with compact route strings:
69
+ Core actor addresses stay small:
70
70
 
71
71
  ```text
72
- run:<id> one detached actor run
73
- branch:<run>/<branch> branch-local actor endpoint
74
- room:<run> shared run-local task room
75
- coordinator launching coordinator attention path
72
+ run:<id> one detached actor run
73
+ tool:<name> executable registered tool actor
74
+ ```
75
+
76
+ Advanced coordination/debug addresses are available when a recipe or workflow needs them:
77
+
78
+ ```text
79
+ branch:<run>/<branch> branch-local worker endpoint
80
+ room:<run> group-message timeline plus roster for one run
81
+ coordinator compatibility alias for the current session coordination path
76
82
  session: current session actor surface
77
- session:all cross-session inventory surface
78
- tool:<name> executable registered tool
83
+ session:all cross-session inventory surface for diagnostics
79
84
  ```
80
85
 
81
86
  Actor messages use one envelope shape:
@@ -140,9 +145,9 @@ message to=run:docs_review type=control.continue body=continue
140
145
  message to=run:docs_review type=control.kill body=stop
141
146
  ```
142
147
 
143
- ## Actor Rooms
148
+ ## Group Messaging And Roster
144
149
 
145
- Every spawned run can have a shared room at `room:<run>`. A room is not a broker and not a chat app. It is a run-local coordination surface: append-only timeline, compact roster, member discovery, and previews.
150
+ Every spawned run can have advanced group messaging at `room:<run>`. Treat this as a run-local timeline plus roster for coordinated actors, not as a core chat/broker concept.
146
151
 
147
152
  Actors can join, post, leave, and discover peers:
148
153
 
@@ -155,7 +160,7 @@ message \
155
160
  body='{"role":"reviewer","caps":["security-review"],"claim":"Review auth boundary risks"}'
156
161
  ```
157
162
 
158
- Inspect the room intentionally:
163
+ Inspect group messages and roster intentionally:
159
164
 
160
165
  ```text
161
166
  inspect target=room:review view=status
@@ -165,7 +170,7 @@ inspect target=room:review view=contacts
165
170
  inspect target=room:review view=messages
166
171
  ```
167
172
 
168
- Room posts require a same-run sender, so unrelated runs do not pollute the roster. Direct messages and room messages use the same envelope; only the address changes. Direct `branch:<run>/<branch>` messages are private: they are forwarded through the parent run mailbox and recorded in the recipient branch inbox for worker protocols that consume queued branch work. For selected-recipient multicast, send to `room:<run>` with `metadata.recipients` set to same-run `branch:<run>/<branch>` addresses; this keeps one room transcript entry while forwarding branch-targeted copies.
173
+ Group posts require a same-run sender, so unrelated runs do not pollute the roster. Direct messages and group messages use the same envelope; only the address changes. Direct `branch:<run>/<branch>` messages are private: they are forwarded through the parent run mailbox and recorded in the recipient branch inbox for worker protocols that consume queued branch work. For selected-recipient multicast, send to `room:<run>` with `metadata.recipients` set to same-run `branch:<run>/<branch>` addresses; this keeps one visible transcript entry while forwarding branch-targeted copies.
169
174
 
170
175
  ## Actor Inspector
171
176
 
@@ -721,19 +721,30 @@ function formatRecipePersistenceSuggestion(transition) {
721
721
  }
722
722
  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.`;
723
723
  }
724
+ function formatTransitionNextActions(transition) {
725
+ const actions = [
726
+ `inspect target=run:${transition.run} view=status`,
727
+ transition.to === "done" && Object.keys(transition.artifacts ?? {}).length > 0
728
+ ? `inspect target=run:${transition.run} view=artifacts`
729
+ : `inspect target=run:${transition.run} view=tail`,
730
+ `inspect target=run:${transition.run} view=messages`,
731
+ ].filter(Boolean);
732
+ return `\nNext actions: ${actions.join(" | ")}`;
733
+ }
724
734
  export function formatRunTransitionMessage(transition) {
725
735
  const artifacts = formatNamedArtifacts(transition.artifacts);
726
736
  const runFiles = formatRunFileList(getRunArtifacts(transition));
727
737
  const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
738
+ const nextActions = formatTransitionNextActions(transition);
728
739
  if (transition.to === "done")
729
- 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}`;
740
+ return `Run ${transition.run} completed successfully.${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
730
741
  if (transition.to === "failed")
731
- return `Run ${transition.run} failed.${artifacts}${runFiles}\nUse inspect target=run:${transition.run} view=status or view=tail for details.`;
742
+ return `Run ${transition.run} failed.${artifacts}${runFiles}${nextActions}`;
732
743
  if (transition.to === "cancelled")
733
- return `Run ${transition.run} was cancelled. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
744
+ return `Run ${transition.run} was cancelled.${nextActions}`;
734
745
  if (transition.to === "killed")
735
- return `Run ${transition.run} was force-killed. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
746
+ return `Run ${transition.run} was force-killed.${nextActions}`;
736
747
  if (transition.to === "exited")
737
- return `Run ${transition.run} exited before writing a result. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
738
- return `Run ${transition.run} finished with status ${transition.to}. Use inspect target=run:${transition.run} view=status or view=tail if analysis is needed.`;
748
+ return `Run ${transition.run} exited before writing a result.${nextActions}`;
749
+ return `Run ${transition.run} finished with status ${transition.to}.${nextActions}`;
739
750
  }
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 ?? status.candidate_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);
@@ -495,8 +509,42 @@ function compactRecipeDoctor(summary) {
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,41 @@ 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)
561
+ ? summary.drafts.length
562
+ : Array.isArray(summary.candidates)
563
+ ? summary.candidates.length
564
+ : 0;
515
565
  const recommendations = Array.isArray(summary.recommendations)
516
566
  ? summary.recommendations.length
517
567
  : 0;
518
- 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);
519
595
  }
520
596
  function compactActorMessageResult(message, result) {
521
597
  const tokens = [
@@ -548,6 +624,11 @@ function compactActorMessageResult(message, result) {
548
624
  if (result.delivery_error) {
549
625
  tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
550
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("|")}`);
551
632
  return `\n${tokens.join(" ")}`;
552
633
  }
553
634
  function maybeJsonText(value, verbose, compact) {
@@ -649,7 +730,7 @@ function writeSpawnCandidateRecipe(input, meta) {
649
730
  const defaults = candidateRecipeDefaults(meta.values);
650
731
  const recipe = {
651
732
  async: true,
652
- description: `Candidate recipe captured from spawn run ${String(meta.run)}`,
733
+ description: `Draft recipe captured from spawn run ${String(meta.run)}`,
653
734
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
654
735
  ...(defaults ? { defaults } : {}),
655
736
  template: input.template,
@@ -787,7 +868,9 @@ export function createSpawnToolDefinition() {
787
868
  const nextActions = actorRunNextActions(meta.run);
788
869
  const details = {
789
870
  ...meta,
790
- ...(candidateRecipe ? { candidate_recipe: candidateRecipe } : {}),
871
+ ...(candidateRecipe
872
+ ? { candidate_recipe: candidateRecipe, draft_recipe: candidateRecipe }
873
+ : {}),
791
874
  next_actions: nextActions,
792
875
  };
793
876
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
@@ -814,15 +897,29 @@ function requireContextSessionId(ctx, actor) {
814
897
  }
815
898
  return sessionId;
816
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
+ }
817
916
  function assertRunAccessibleToContext(runId, ctx) {
818
917
  const status = AsyncRuns.getRunStatus(runId);
819
918
  const sessionId = getContextSessionId(ctx);
820
919
  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",
920
+ throw sessionMismatchError({
921
+ currentSession: sessionId,
922
+ expectedSession: String(status.ownerId),
826
923
  run: runId,
827
924
  });
828
925
  }
@@ -835,13 +932,13 @@ export function createInspectToolDefinition(deps = {}) {
835
932
  return {
836
933
  name: "inspect",
837
934
  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.",
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.",
839
936
  parameters: objectSchema({
840
937
  lines: stringSchema("Line count for tail/messages views. Default 40."),
841
938
  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>."),
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."),
843
940
  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."),
941
+ view: stringSchema("Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts."),
845
942
  }, ["target", "view"]),
846
943
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
847
944
  const input = asRecord(params);
@@ -863,10 +960,15 @@ export function createInspectToolDefinition(deps = {}) {
863
960
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
864
961
  ]);
865
962
  const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
866
- const summary = {
963
+ const summaryBase = {
867
964
  ...RecipeDiscovery.summarizeDiscovery(discovered),
965
+ drafts: RecipeDiscovery.listCandidateRecipes(join(recipeRoot, "candidates")),
868
966
  candidates: RecipeDiscovery.listCandidateRecipes(join(recipeRoot, "candidates")),
869
967
  };
968
+ const summary = {
969
+ ...summaryBase,
970
+ next_actions: recipeRegistryNextActions(summaryBase, view),
971
+ };
870
972
  return {
871
973
  content: [
872
974
  {
@@ -1084,7 +1186,11 @@ export function createInspectToolDefinition(deps = {}) {
1084
1186
  const status = assertRunAccessibleToContext(runId, ctx);
1085
1187
  const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
1086
1188
  const details = artifactManifest
1087
- ? { ...status, artifact_manifest: artifactManifest }
1189
+ ? {
1190
+ ...status,
1191
+ artifact_manifest: artifactManifest,
1192
+ next_actions: artifactNextActions(status.run ?? runId, asRecord(status.artifacts)),
1193
+ }
1088
1194
  : status;
1089
1195
  return {
1090
1196
  content: [
@@ -1139,7 +1245,7 @@ export function createActorMessageToolDefinition(deps = {}) {
1139
1245
  return {
1140
1246
  name: "message",
1141
1247
  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.",
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>.",
1143
1249
  parameters: objectSchema({
1144
1250
  body: unionSchema([
1145
1251
  stringSchema("Message body. For run:<id>, this is the run-local command line."),
@@ -1151,7 +1257,7 @@ export function createActorMessageToolDefinition(deps = {}) {
1151
1257
  metadata: looseObjectSchema("Optional structured metadata for routing or domain hints."),
1152
1258
  reply_to: stringSchema("Optional message id this message replies to."),
1153
1259
  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>."),
1260
+ to: stringSchema("Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>."),
1155
1261
  type: stringSchema("Semantic message type, e.g. control.approve or checkpoint.needs_scope."),
1156
1262
  verbose: booleanSchema("Return full JSON instead of compact text."),
1157
1263
  }, ["to", "type"]),
@@ -1266,10 +1372,20 @@ export function createActorMessageToolDefinition(deps = {}) {
1266
1372
  const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
1267
1373
  if (address.kind === "session") {
1268
1374
  if (!senderStatus.ownerId) {
1269
- 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
+ });
1270
1381
  }
1271
1382
  if (senderStatus.ownerId !== address.value) {
1272
- 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
+ });
1273
1389
  }
1274
1390
  }
1275
1391
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
@@ -1291,14 +1407,18 @@ export function createActorMessageToolDefinition(deps = {}) {
1291
1407
  else {
1292
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}`);
1293
1409
  }
1410
+ const nextActions = actorMessageNextActions(message, result);
1411
+ const resultWithNext = nextActions.length
1412
+ ? { ...result, next_actions: nextActions }
1413
+ : result;
1294
1414
  return {
1295
1415
  content: [
1296
1416
  {
1297
1417
  type: "text",
1298
- text: maybeJsonText({ message, result }, input.verbose === true, compactActorMessageResult(message, result)),
1418
+ text: maybeJsonText({ message, result: resultWithNext }, input.verbose === true, compactActorMessageResult(message, resultWithNext)),
1299
1419
  },
1300
1420
  ],
1301
- details: { message, result },
1421
+ details: { message, result: resultWithNext },
1302
1422
  };
1303
1423
  },
1304
1424
  };
@@ -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.32.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/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`.
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.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
 
@@ -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/tools.ts CHANGED
@@ -222,8 +222,8 @@ function compactAsyncRunStatus(value: unknown): string {
222
222
  tokens.push(`failures=${failures}`);
223
223
  if (result.code !== undefined) tokens.push(`code=${String(result.code)}`);
224
224
  if (result.killed === true) tokens.push("killed=true");
225
- if (status.candidate_recipe)
226
- tokens.push(`candidate_recipe=${String(status.candidate_recipe)}`);
225
+ const draftRecipe = status.draft_recipe ?? status.candidate_recipe;
226
+ if (draftRecipe) tokens.push(`draft_recipe=${String(draftRecipe)}`);
227
227
  const nextActions = actorRunNextActions(run);
228
228
  if (nextActions.length > 0)
229
229
  tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
@@ -451,6 +451,15 @@ function compactArtifactPath(value: unknown): string {
451
451
  return String(record.path ?? "<missing>");
452
452
  }
453
453
 
454
+ function artifactNextActions(run: unknown, artifacts: Record<string, unknown>): string[] {
455
+ const id = String(run ?? "").trim();
456
+ if (!id || Object.keys(artifacts).length === 0) return [];
457
+ return [
458
+ `inspect target=run:${id} view=artifacts verbose=true`,
459
+ `inspect target=run:${id} view=messages`,
460
+ ];
461
+ }
462
+
454
463
  function compactActorFiles(status: Record<string, unknown>): string {
455
464
  const run = String(status.run ?? "<unknown>");
456
465
  const artifacts = asRecord(status.artifacts);
@@ -469,7 +478,11 @@ function compactActorFiles(status: Record<string, unknown>): string {
469
478
  .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
470
479
  .join(",")}`
471
480
  : "";
472
- return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
481
+ const nextActions = artifactNextActions(run, artifacts);
482
+ const nextText = nextActions.length
483
+ ? ` next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
484
+ : "";
485
+ return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}${nextText}`;
473
486
  }
474
487
 
475
488
  function summarizeOtherSessions(
@@ -621,9 +634,43 @@ function compactRecipeDoctor(summary: Record<string, unknown>): string {
621
634
  `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
622
635
  );
623
636
  }
637
+ const nextActions = Array.isArray(summary.next_actions)
638
+ ? (summary.next_actions as string[])
639
+ : [];
640
+ if (nextActions.length > 0) lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
624
641
  return `\n${lines.join("\n")}`;
625
642
  }
626
643
 
644
+ function recipeRegistryNextActions(summary: Record<string, unknown>, view: string): string[] {
645
+ const actions: string[] = [];
646
+ const drafts = Array.isArray(summary.drafts)
647
+ ? (summary.drafts as Array<Record<string, unknown>>)
648
+ : [];
649
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
650
+ const diagnostics = Array.isArray(summary.diagnostics)
651
+ ? summary.diagnostics.length
652
+ : 0;
653
+ const topAction = asRecord(summary.top_action);
654
+ if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
655
+ actions.push("inspect target=recipes view=doctor");
656
+ }
657
+ if (view === "doctor" && typeof topAction.action === "string") {
658
+ actions.push(String(topAction.action));
659
+ }
660
+ if (drafts.length > 0) {
661
+ actions.push("inspect target=recipes view=summary verbose=true");
662
+ const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
663
+ if (firstPath) actions.push(`spawn file=${firstPath}`);
664
+ }
665
+ return [...new Set(actions)].slice(0, 4);
666
+ }
667
+
668
+ function compactNextActions(actions: string[]): string {
669
+ return actions.length
670
+ ? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
671
+ : "";
672
+ }
673
+
627
674
  function compactRecipeRegistry(summary: Record<string, unknown>): string {
628
675
  const active = Array.isArray(summary.active) ? summary.active.length : 0;
629
676
  const shadowed = Array.isArray(summary.shadowed)
@@ -636,13 +683,43 @@ function compactRecipeRegistry(summary: Record<string, unknown>): string {
636
683
  const diagnostics = Array.isArray(summary.diagnostics)
637
684
  ? summary.diagnostics.length
638
685
  : 0;
639
- const candidates = Array.isArray(summary.candidates)
640
- ? summary.candidates.length
641
- : 0;
686
+ const drafts = Array.isArray(summary.drafts)
687
+ ? summary.drafts.length
688
+ : Array.isArray(summary.candidates)
689
+ ? summary.candidates.length
690
+ : 0;
642
691
  const recommendations = Array.isArray(summary.recommendations)
643
692
  ? summary.recommendations.length
644
693
  : 0;
645
- return `\nrecipes active=${active} candidates=${candidates} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}`;
694
+ const nextActions = Array.isArray(summary.next_actions)
695
+ ? (summary.next_actions as string[])
696
+ : [];
697
+ return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
698
+ }
699
+
700
+ function actorMessageNextActions(
701
+ message: ActorMessages.ActorMessage,
702
+ result: Record<string, unknown>,
703
+ ): string[] {
704
+ const actions: string[] = [];
705
+ const address = ActorMessages.parseActorAddress(message.to);
706
+ if (result.delivery_error || result.sent === false) {
707
+ if (address.kind === "run" && address.value) {
708
+ actions.push(`inspect target=run:${address.value} view=status`);
709
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
710
+ } else if (address.kind === "branch" && address.value) {
711
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
712
+ actions.push(`inspect target=run:${address.value} view=status`);
713
+ }
714
+ }
715
+ if (result.queued === true) {
716
+ if (address.kind === "branch" && address.value) {
717
+ actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
718
+ } else if (address.kind === "run" && address.value) {
719
+ actions.push(`inspect target=run:${address.value} view=mailbox`);
720
+ }
721
+ }
722
+ return [...new Set(actions)].slice(0, 3);
646
723
  }
647
724
 
648
725
  function compactActorMessageResult(
@@ -670,6 +747,11 @@ function compactActorMessageResult(
670
747
  if (result.delivery_error) {
671
748
  tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
672
749
  }
750
+ const nextActions = Array.isArray(result.next_actions)
751
+ ? (result.next_actions as string[])
752
+ : actorMessageNextActions(message, result);
753
+ if (nextActions.length > 0)
754
+ tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
673
755
  return `\n${tokens.join(" ")}`;
674
756
  }
675
757
 
@@ -832,7 +914,7 @@ function writeSpawnCandidateRecipe(
832
914
  const defaults = candidateRecipeDefaults(meta.values);
833
915
  const recipe = {
834
916
  async: true,
835
- description: `Candidate recipe captured from spawn run ${String(meta.run)}`,
917
+ description: `Draft recipe captured from spawn run ${String(meta.run)}`,
836
918
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
837
919
  ...(defaults ? { defaults } : {}),
838
920
  template: input.template,
@@ -1040,7 +1122,9 @@ export function createSpawnToolDefinition<
1040
1122
  const nextActions = actorRunNextActions(meta.run);
1041
1123
  const details = {
1042
1124
  ...meta,
1043
- ...(candidateRecipe ? { candidate_recipe: candidateRecipe } : {}),
1125
+ ...(candidateRecipe
1126
+ ? { candidate_recipe: candidateRecipe, draft_recipe: candidateRecipe }
1127
+ : {}),
1044
1128
  next_actions: nextActions,
1045
1129
  };
1046
1130
  ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
@@ -1084,6 +1168,33 @@ function requireContextSessionId(ctx: unknown, actor: string): string {
1084
1168
  return sessionId;
1085
1169
  }
1086
1170
 
1171
+ function sessionMismatchError(input: {
1172
+ currentSession?: string;
1173
+ expectedSession?: string;
1174
+ run?: string;
1175
+ target?: string;
1176
+ }): Error {
1177
+ const ownerSession = input.expectedSession ?? "none";
1178
+ const currentSession = input.currentSession ?? "none";
1179
+ const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
1180
+ const hintTarget = input.expectedSession
1181
+ ? `session:${input.expectedSession}`
1182
+ : "session:all";
1183
+ return Object.assign(
1184
+ new Error(
1185
+ `${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`,
1186
+ ),
1187
+ {
1188
+ current_session: input.currentSession,
1189
+ hint: `inspect target=${hintTarget} view=status`,
1190
+ owner_session: input.expectedSession,
1191
+ reason: "session_mismatch",
1192
+ run: input.run,
1193
+ target: input.target,
1194
+ },
1195
+ );
1196
+ }
1197
+
1087
1198
  function assertRunAccessibleToContext(
1088
1199
  runId: string,
1089
1200
  ctx: unknown,
@@ -1091,18 +1202,11 @@ function assertRunAccessibleToContext(
1091
1202
  const status = AsyncRuns.getRunStatus(runId);
1092
1203
  const sessionId = getContextSessionId(ctx);
1093
1204
  if (sessionId && status.ownerId && status.ownerId !== sessionId) {
1094
- throw Object.assign(
1095
- new Error(
1096
- `run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`,
1097
- ),
1098
- {
1099
- current_session: sessionId,
1100
- hint: `inspect target=session:${status.ownerId} view=status`,
1101
- owner_session: status.ownerId,
1102
- reason: "session_mismatch",
1103
- run: runId,
1104
- },
1105
- );
1205
+ throw sessionMismatchError({
1206
+ currentSession: sessionId,
1207
+ expectedSession: String(status.ownerId),
1208
+ run: runId,
1209
+ });
1106
1210
  }
1107
1211
  return status;
1108
1212
  }
@@ -1120,7 +1224,7 @@ export function createInspectToolDefinition<TContext = unknown>(
1120
1224
  name: "inspect",
1121
1225
  label: "Inspect",
1122
1226
  description:
1123
- "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.",
1227
+ "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.",
1124
1228
  parameters: objectSchema(
1125
1229
  {
1126
1230
  lines: stringSchema("Line count for tail/messages views. Default 40."),
@@ -1128,13 +1232,13 @@ export function createInspectToolDefinition<TContext = unknown>(
1128
1232
  "Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited.",
1129
1233
  ),
1130
1234
  target: stringSchema(
1131
- "Actor address to inspect, e.g. run:<id>, room:<run>, coordinator, session:<id>, session:all, or tool:<name>.",
1235
+ "Actor address to inspect, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>, session:all.",
1132
1236
  ),
1133
1237
  verbose: booleanSchema(
1134
1238
  "Return full JSON instead of compact text where available.",
1135
1239
  ),
1136
1240
  view: stringSchema(
1137
- "Inspection view: status, tail, messages, artifacts, files, mailbox, communication, roster, or contacts.",
1241
+ "Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts.",
1138
1242
  ),
1139
1243
  },
1140
1244
  ["target", "view"],
@@ -1169,12 +1273,19 @@ export function createInspectToolDefinition<TContext = unknown>(
1169
1273
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
1170
1274
  ]);
1171
1275
  const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
1172
- const summary = {
1276
+ const summaryBase = {
1173
1277
  ...RecipeDiscovery.summarizeDiscovery(discovered),
1278
+ drafts: RecipeDiscovery.listCandidateRecipes(
1279
+ join(recipeRoot, "candidates"),
1280
+ ),
1174
1281
  candidates: RecipeDiscovery.listCandidateRecipes(
1175
1282
  join(recipeRoot, "candidates"),
1176
1283
  ),
1177
1284
  };
1285
+ const summary = {
1286
+ ...summaryBase,
1287
+ next_actions: recipeRegistryNextActions(summaryBase, view),
1288
+ };
1178
1289
  return {
1179
1290
  content: [
1180
1291
  {
@@ -1488,7 +1599,14 @@ export function createInspectToolDefinition<TContext = unknown>(
1488
1599
  | undefined,
1489
1600
  );
1490
1601
  const details = artifactManifest
1491
- ? { ...status, artifact_manifest: artifactManifest }
1602
+ ? {
1603
+ ...status,
1604
+ artifact_manifest: artifactManifest,
1605
+ next_actions: artifactNextActions(
1606
+ status.run ?? runId,
1607
+ asRecord(status.artifacts),
1608
+ ),
1609
+ }
1492
1610
  : status;
1493
1611
  return {
1494
1612
  content: [
@@ -1574,7 +1692,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1574
1692
  name: "message",
1575
1693
  label: "Message",
1576
1694
  description:
1577
- "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.",
1695
+ "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>.",
1578
1696
  parameters: objectSchema(
1579
1697
  {
1580
1698
  body: unionSchema([
@@ -1596,7 +1714,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1596
1714
  reply_to: stringSchema("Optional message id this message replies to."),
1597
1715
  summary: stringSchema("Optional short human-facing summary."),
1598
1716
  to: stringSchema(
1599
- "Destination actor address, e.g. run:<id>, branch:<run>/<branch>, room:<run>, coordinator, session:<id>, or tool:<name>.",
1717
+ "Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>.",
1600
1718
  ),
1601
1719
  type: stringSchema(
1602
1720
  "Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
@@ -1765,14 +1883,20 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1765
1883
  const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
1766
1884
  if (address.kind === "session") {
1767
1885
  if (!senderStatus.ownerId) {
1768
- throw new Error(
1769
- `message to session:${address.value} requires sender run owner ${address.value}; got no owner.`,
1770
- );
1886
+ throw sessionMismatchError({
1887
+ currentSession: undefined,
1888
+ expectedSession: address.value,
1889
+ run: sender.value,
1890
+ target: `session:${address.value}`,
1891
+ });
1771
1892
  }
1772
1893
  if (senderStatus.ownerId !== address.value) {
1773
- throw new Error(
1774
- `message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`,
1775
- );
1894
+ throw sessionMismatchError({
1895
+ currentSession: String(senderStatus.ownerId),
1896
+ expectedSession: address.value,
1897
+ run: sender.value,
1898
+ target: `session:${address.value}`,
1899
+ });
1776
1900
  }
1777
1901
  }
1778
1902
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
@@ -1796,18 +1920,22 @@ export function createActorMessageToolDefinition<TContext = unknown>(
1796
1920
  `message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
1797
1921
  );
1798
1922
  }
1923
+ const nextActions = actorMessageNextActions(message, result);
1924
+ const resultWithNext = nextActions.length
1925
+ ? { ...result, next_actions: nextActions }
1926
+ : result;
1799
1927
  return {
1800
1928
  content: [
1801
1929
  {
1802
1930
  type: "text" as const,
1803
1931
  text: maybeJsonText(
1804
- { message, result },
1932
+ { message, result: resultWithNext },
1805
1933
  input.verbose === true,
1806
- compactActorMessageResult(message, result),
1934
+ compactActorMessageResult(message, resultWithNext),
1807
1935
  ),
1808
1936
  },
1809
1937
  ],
1810
- details: { message, result },
1938
+ details: { message, result: resultWithNext },
1811
1939
  };
1812
1940
  },
1813
1941
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -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.32.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/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`.
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.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.