@llblab/pi-actors 0.12.9 → 0.12.13

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/BACKLOG.md CHANGED
@@ -6,7 +6,7 @@ Continue progressive component/pipeline expansion in small validated slices; rea
6
6
 
7
7
  - Plan organic universal communication primitives.
8
8
  - Priority: High.
9
- - Status: The actor-like model is empirically useful: async runs can emit follow-up messages upward, coordinators can send run-local commands downward, multiple parallel runs can progress independently, and recipes no longer need sleep-poll coordination. The design is captured in `docs/actor-messages.md`: `spawn`, `message`, and `inspect` as concentrated verbs; addressed actors; one symmetric message envelope; mailbox `accepts`/`emits`; and adapter mappings from runtime async actions. Initial implementation landed pure actor address/message normalization plus public `spawn`, `message`, and `inspect` tools for `run:<id>` actors; `spawn` accepts state/artifact metadata, `message` routes `run:<id>` → `coordinator` envelopes into the runtime attention path, `message` can invoke `tool:<name>` actors, all packaged async recipes declare mailbox metadata, `inspect view=mailbox` exposes recipe mailbox contracts from run metadata, and recipe-authored messages now use envelope-aligned `type` fields with deterministic validated wrapping available through `utility-actor-message`. Remaining work is to absorb remaining runtime async action surfaces into the actor vocabulary instead of preserving a parallel public API.
9
+ - Status: The actor-like model is empirically useful: async runs can emit follow-up messages upward, coordinators can send run-local commands downward, multiple parallel runs can progress independently, and recipes no longer need sleep-poll coordination. The design is captured in `docs/actor-messages.md`: `spawn`, `message`, and `inspect` as concentrated verbs; addressed actors; one symmetric message envelope; mailbox `accepts`/`emits`; and adapter mappings from runtime async actions. Initial implementation landed pure actor address/message normalization plus public `spawn`, `message`, and `inspect` tools for `run:<id>` actors; `spawn` accepts state/artifact metadata, `message` routes `run:<id>` → `coordinator` and `run:<id>` → `session:<id>` envelopes into the runtime attention path, `message` can invoke `tool:<name>` actors, `inspect target=tool:<name>` exposes registered tool actor contracts, `inspect target=coordinator` exposes current-session run inventory, all packaged async recipes declare mailbox metadata, `inspect view=mailbox` exposes recipe mailbox contracts from run metadata, recipe-authored messages now use envelope-aligned `type` fields with deterministic validated wrapping available through `utility-actor-message`, run termination now accepts actor-native `control.stop`/`control.cancel`/`control.kill` aliases while preserving legacy `runtime.*` compatibility, async command events preserve full argv details while keeping coordinator summaries bounded, and async-run operations recommendations now emit structured `message`/`inspect` objects instead of shell-like suggestion strings. Remaining work is to absorb remaining runtime async action surfaces into the actor vocabulary instead of preserving a parallel public API.
10
10
  - Scope: Design and implement a small semantic layer around addressed messages and actors while preserving low-level primitives as adapters where useful. Candidate top-level concepts are `spawn` for creating an actor/run from a recipe or template, `message` for sending typed messages to any address, and `inspect` for intentional observation/debugging. Candidate addresses include `run:<id>`, `branch:<run>/<branch>`, `coordinator`, `session:<id>`, `tool:<name>`, and future chat/session endpoints. Candidate message fields include `to`, `from`, `type`, `summary`, `body`, `reply_to`, `correlation_id`, and `metadata`.
11
11
  - Contract direction: Unify “send down” and “messages up” as one message model. `to: run:<id>` routes to a run mailbox, `to: coordinator` routes to the coordinator attention path, and branch/tool/session addresses can be layered over the same semantic envelope. Recipes should declare mailbox capability (`accepts`, `emits`) without exposing FIFO/outbox mechanics or delivery policy as their public interface.
12
12
  - Design gates: Breaking changes are allowed in this phase, so compress concepts instead of preserving accidental surfaces. Consolidate duplicated lifecycle/message/event APIs into a concentrated protocol with the fewest durable nouns and verbs that still explain the system. Duplex communication should be symmetric where the domain is symmetric: the same message envelope should represent run→coordinator, coordinator→run, run→run, branch→parent, and parent→branch traffic, with routing/transport hidden below it. Keep command templates as the portable synchronous execution graph; keep recipe files as semantic definitions; avoid leaking transports into public args; make polling an explicit diagnostic operation, not an example path; replace runtime action names with the actor API rather than preserving parallel concepts.
@@ -14,18 +14,19 @@ Continue progressive component/pipeline expansion in small validated slices; rea
14
14
 
15
15
  - Progressively increase component parameterization and higher-level recipe composition.
16
16
  - Priority: High.
17
+ - Status: `subagent-tools` and `subagents-prompts` now align with the common subagent policy knobs for thinking, tools, model, and output format.
17
18
  - Scope: Iteratively strengthen atom/component recipes with public policy knobs such as model pools, stage-specific models, thinking, tool policy, output format, evidence policy, risk policy, source policy, artifact paths, mailbox contract, handoff format, resume/continuity policy, and validation gates; add higher-level component recipes that compose existing atoms into reusable coordinator patterns.
18
19
  - Exit: Each iteration adds or refines at least one atom-level parameterization surface and at least one composed recipe/pipeline, with packaged recipe import validation passing and docs/changelog updated.
19
20
 
20
21
  - Add another task-first high-level pipeline candidate from the design map.
21
22
  - Priority: Medium.
22
- - Status: `pipeline-release-readiness`, `pipeline-repo-health`, `pipeline-async-run-ops`, `pipeline-docs-maintenance`, and `pipeline-media-library` landed. `pipeline-media-library` required playlist output-mode parameterization, then reused playlist and artifact-report cells.
23
+ - Status: `pipeline-release-readiness`, `pipeline-repo-health`, `pipeline-async-run-ops`, `pipeline-docs-maintenance`, `pipeline-media-library`, and `pipeline-artifact-bundle` landed. `pipeline-media-library` required playlist output-mode parameterization, then reused playlist and artifact-report cells. `pipeline-artifact-bundle` reused artifact-write, artifact-manifest, validation, manifest-write, and actor-message cells.
23
24
  - Scope: Reassess `docs/task-first-recipes.md` for the next high-value task cell, then implement only the minimum missing cells needed for that task.
24
25
  - Exit: Another task-first pipeline lands with docs, package validation, and a note about which missing atoms/utilities it required.
25
26
 
26
27
  - Grow the standard recipe library with safer structured utility transforms.
27
28
  - Priority: Low.
28
- - Status: `utility-artifact-manifest` landed for machine-readable artifact metadata; `utility-artifact-write` landed for deterministic writes of accepted prepared artifacts; `utility-package-summary` landed for bounded package metadata used by release/repo-health flows; `utility-validate-recipe` landed with a dedicated recipe validator script.
29
+ - Status: `utility-artifact-manifest` landed for machine-readable artifact metadata; `utility-artifact-write` landed for deterministic writes of accepted prepared artifacts; `utility-package-summary` landed for bounded package metadata used by release/repo-health flows; `utility-validate-recipe` landed with a dedicated recipe validator script; `utility-run-ops-snapshot` landed for async-run summaries, event tails, and operator-gated cleanup recommendations.
29
30
  - Scope: Continue beyond listing/extraction utilities toward structured transforms for artifact packaging, report normalization, release prep, and machine-readable summaries. Keep helpers generic, parameterized, and justified by repeated recipe needs.
30
31
  - Exit: A future utility slice adds another structured transform only when a repeated recipe need appears; otherwise treat the current helper-backed utility surface as sufficient.
31
32
 
package/CHANGELOG.md CHANGED
@@ -1,6 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.12.13: Structured Run Operations Recommendations
4
+
5
+ - `[Recipe Utilities]` Changed `utility-run-ops-snapshot` recommendations from shell-like suggestion strings to structured `message` and `inspect` call objects. Impact: async-run operations reports now preserve the actor API shape directly and avoid reintroducing command-string parsing into coordinator handoffs.
6
+
7
+ ## 0.12.12: Async Command Summary Hygiene
8
+
9
+ - `[Observability]` Kept async `command.done` summaries bounded while preserving full argv-shaped command details in event payloads. Impact: long prompted fanouts keep diagnostic fidelity without flooding coordinator follow-ups with huge command lines.
10
+
11
+ ## 0.12.11: Recipe Import Diagnostics Hotfix
12
+
13
+ - `[Template Recipes]` Added regression coverage proving imported recipe nodes execute correctly under a repeated parallel parent (`imports` + `repeat` + object `template`). Impact: the suspected composition blocker is now guarded as supported behavior instead of relying on manual smoke interpretation.
14
+ - `[Observability]` Expanded command details in foreground execution results and async `command.start`/`command.done` events from executable-only labels to full argv-shaped launch strings. Impact: failed fanout branches no longer appear as misleading `pi && pi && ...` summaries when the real command was `pi -p --model ...` with a long prompt.
15
+ - `[Spawn]` Allowed the public `spawn` schema to accept inline object command-template configs, not only strings and arrays. Impact: agents can launch object-form templates with `parallel`, `repeat`, `failure`, and nested `template` directly through `spawn` as documented.
16
+
17
+ ## 0.12.10: Actor Ownership and Recipe Operations
18
+
19
+ - `[Actor Messages]` Added actor-native `control.stop`, `control.cancel`, and `control.kill` handling for run termination while retaining `runtime.cancel` and `runtime.kill` as compatibility aliases. Impact: public examples can use the same control-message vocabulary declared by recipe mailboxes instead of preserving runtime action names.
20
+ - `[Actor Messages]` Added `inspect target=tool:<name>` support for registered tool actor status/schema contracts. Impact: tool actors can now be intentionally observed through the same actor vocabulary used to invoke them with `message to=tool:<name>`.
21
+ - `[Recipe Library]` Added `pipeline-artifact-bundle`, a task-first handoff pipeline that composes optional validation, deterministic artifact writing, machine-readable manifest generation, deterministic manifest writing, and an actor-message handoff. Impact: callers who explicitly want filesystem writes can produce paired artifact and manifest paths as one reusable bundle workflow.
22
+ - `[Component Recipes]` Aligned `subagent-tools` and `subagents-prompts` with the common subagent policy knobs for `model`, `thinking`, `tools`, and `output_format`. Impact: prompt launchers and prompt fanout can be tuned through the same public controls as the richer subagent atoms.
23
+ - `[Recipe Library]` Added `utility-run-ops-snapshot` and routed `pipeline-async-run-ops` through it so run summaries, event tails, and stale/terminal recommendations stay in one structured input. Impact: async-run operations reports no longer lose summary context before normalization and can suggest `inspect` or `control.stop` messages without executing them.
24
+ - `[Actor Messages]` Added `inspect target=coordinator` support for current-session run inventory. Impact: the coordinator actor can now be intentionally observed without spelling out the session id.
25
+ - `[Actor Messages]` Added `message to=session:<id>` support for run-owned session-directed follow-ups. Impact: explicit session checkpoints now use the same actor envelope as coordinator follow-ups while preserving run-owner checks.
26
+ - `[Actor Messages]` Hardened `message to=session:<id>` routing to require an owned sender run. Impact: unowned or cross-session runs cannot synthesize session-directed follow-ups.
27
+ - `[Actor Messages]` Applied coordinator-session ownership checks to addressed run, branch, and coordinator message routes when a session context is available. Impact: actor messages now fail closed before controlling or emitting from runs owned by another coordinator session.
28
+ - `[Actor Messages]` Applied the same coordinator-session ownership gate to direct `inspect target=run:<id>` views. Impact: explicit run inspection no longer leaks cross-session run details when the current session is known.
29
+ - `[Actor Messages]` Tightened `inspect target=coordinator` to require a current coordinator session instead of falling back to all runs. Impact: callers without session context must use explicit `session:<id>` or `session:all` inventory.
4
30
 
5
31
  ## 0.12.9: Actor Runtime Hotfix
6
32
 
package/README.md CHANGED
@@ -307,7 +307,7 @@ Read recent events or logs only after a follow-up asks for inspection, at a real
307
307
  { "target": "run:docs-review", "view": "tail", "lines": "80" }
308
308
  ```
309
309
 
310
- Reusable local recipes live in `~/.pi/agent/recipes/*.json`; recipe tools honor each file's `async` flag. Use `spawn` for explicit detached starts from a file or inline template, and `inspect target=session:<id> view=runs status=running` or `inspect target=session:all view=runs` for explicit inventory/diagnosis. List output includes `tool` and `recipe` when the launcher recorded that source context.
310
+ Reusable local recipes live in `~/.pi/agent/recipes/*.json`; recipe tools honor each file's `async` flag. Use `spawn` for explicit detached starts from a file or inline template, and `inspect target=coordinator view=runs status=running`, `inspect target=session:<id> view=runs status=running`, or `inspect target=session:all view=runs` for explicit inventory/diagnosis. List output includes `tool` and `recipe` when the launcher recorded that source context.
311
311
 
312
312
  ## Recipe Library
313
313
 
@@ -376,8 +376,8 @@ See [`docs/recipe-library.md`](./docs/recipe-library.md) for install notes and r
376
376
  - Obvious high-risk templates such as shells, interpreter eval modes, and broad filesystem mutation surface lightweight warnings without blocking existing tools.
377
377
  - `async: true` on a recipe selects detached run lifecycle; omitted or false async runs the recipe foreground through registered tools.
378
378
  - Layer boundaries stay explicit: command templates define synchronous execution graphs; template recipes add saved JSON metadata/import resolution and named `artifacts`; async runs add detached lifecycle, state, IPC, and observability.
379
- - `spawn`, `message`, and `inspect` are high-level actor adapters. `spawn` creates `run:<id>` actors from recipes or inline templates with optional state/artifact metadata, `message` sends one typed envelope to `run:<id>` mailboxes, `branch:<run>/<branch>` mailboxes, `tool:<name>` calls, or the coordinator attention path, and `inspect` intentionally reads `run:<id>` status/tail/events/mailbox metadata or `session:<id>` run status while the broader actor/message protocol is refined.
380
- - `spawn`, `message`, and `inspect` are the public async coordination vocabulary. Low-level async actions map to this actor API: start belongs to `spawn`; send/control belongs to `message`; status/tail/events/list belong to `inspect`; stop/kill are runtime control messages with synchronous results.
379
+ - `spawn`, `message`, and `inspect` are high-level actor adapters. `spawn` creates `run:<id>` actors from recipes or inline templates with optional state/artifact metadata, `message` sends one typed envelope to `run:<id>` mailboxes, `branch:<run>/<branch>` mailboxes, `tool:<name>` calls, or coordinator/session attention paths, and `inspect` intentionally reads `run:<id>` status/tail/events/mailbox metadata, coordinator/session run status, or registered `tool:<name>` contracts while the broader actor/message protocol is refined.
380
+ - `spawn`, `message`, and `inspect` are the public async coordination vocabulary. Low-level async actions map to this actor API: start belongs to `spawn`; send/control/stop/kill belongs to `message`; status/tail/events/list belongs to `inspect`. Prefer `control.stop` and `control.kill` for run termination; legacy `runtime.cancel` and `runtime.kill` remain compatibility aliases.
381
381
  - Actor management returns compact text by default; pass `verbose: true` to `inspect` when full JSON state is needed.
382
382
  - Detached runs inject `{run_id}` and `{state_dir}` into template values for run-local artifacts or recipe-specific control endpoints.
383
383
  - Runtime actor messages are stored in `<state_dir>/outbox.jsonl`; coordinator attention is inferred by the runtime, not exposed as recipe or message-envelope input. Follow-ups preserve bounded body previews and metadata for decision messages.
@@ -58,7 +58,7 @@ Field rules:
58
58
  - `type`: required semantic message type.
59
59
  - `summary`: short human-facing line for notifications/follow-ups.
60
60
  - `body`: string or JSON payload.
61
- - routing/delivery is inferred from `to`, actor ownership, and coordinator runtime policy; recipes should not expose delivery knobs.
61
+ - routing/delivery is inferred from `to`, actor ownership, and coordinator runtime policy; recipes should not expose delivery knobs. When a coordinator session is known, addressed run/branch/control messages fail closed before controlling or emitting from runs owned by another session.
62
62
  - `reply_to`: optional message id for conversational checkpoints.
63
63
  - `correlation_id`: optional task/run/workflow id.
64
64
  - `metadata`: optional structured routing or domain hints.
@@ -79,7 +79,7 @@ coordinator -> tool
79
79
  Transports differ, but the public contract does not:
80
80
 
81
81
  - `to: run:<id>` may route to FIFO, mailbox file, socket, or process stdin.
82
- - `to: coordinator` routes to outbox/watch/follow-up delivery when `from` names a run actor. Generic async-runner `command.done` events and explicit coordinator-bound messages include the actor envelope fields alongside the runtime event fields.
82
+ - `to: coordinator` routes to outbox/watch/follow-up delivery when `from` names a run actor. `to: session:<id>` uses the same actor-message path only when the sender run is owned by that session, making explicit session-directed checkpoints possible without exposing runtime delivery knobs. Generic async-runner `command.done` events and explicit coordinator/session-bound messages include the actor envelope fields alongside the runtime event fields.
83
83
  - `to: branch:<run>/<branch>` routes through the parent run mailbox with the full envelope preserved so the run can dispatch branch-local control.
84
84
  - `to: tool:<name>` invokes an executable pi tool by name. Object bodies become tool parameters; primitive bodies are passed as `{ "input": body }`.
85
85
 
@@ -126,7 +126,7 @@ Recipes can declare their conversational surface:
126
126
  }
127
127
  ```
128
128
 
129
- The implementation supports `status`, `tail`, `events`, `artifacts`, `files`, and `mailbox` for `run:<id>` actors, plus `status`/`runs` for `session:<id>` and `session:all` actors with optional status filtering. `inspect` is for decision points and diagnosis only; examples must not teach sleep-then-inspect polling.
129
+ The implementation supports `status`, `tail`, `events`, `artifacts`, `files`, and `mailbox` for `run:<id>` actors, `status`/`runs` for `coordinator`, `session:<id>`, and `session:all` actors with optional status filtering, and `status`/`schema` for registered `tool:<name>` actors. `inspect target=coordinator` requires a current coordinator session; use `session:<id>` or `session:all` when the session is intentionally explicit. Direct `run:<id>` 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.
130
130
 
131
131
  ## Runtime Direction
132
132
 
@@ -135,7 +135,8 @@ Runtime operations use the actor/message vocabulary:
135
135
  ```text
136
136
  create detached work -> spawn
137
137
  run-local control -> message to run:<id>
138
- coordinator signal -> message to coordinator
138
+ run stop/kill -> message type control.stop/control.kill
139
+ coordinator signal -> message to coordinator/session
139
140
  tool execution -> message to tool:<name>
140
141
  intentional observe -> inspect
141
142
  ```
@@ -127,22 +127,22 @@ The core loop is:
127
127
 
128
128
  4. Do not inspect just because time passed. Inspect `status`, `tail`, or `events` only when a follow-up asks for inspection, a real decision depends on it, or a suspected stuck run needs diagnosis.
129
129
 
130
- Addressed `message` calls and coordinator follow-ups are the paired control plane: run-to-coordinator actor messages flow upward, while coordinator-to-run actor messages flow downward. Recipe scripts own the message vocabulary (`next`, `pause`, `approve`, `revise`, `continue`, and so on); pi-actors owns the safe run-local transport, ownership checks, and coordinator attention policy.
130
+ Addressed `message` calls and coordinator follow-ups are the paired control plane: run-to-coordinator actor messages flow upward, while coordinator-to-run actor messages flow downward. Recipe scripts own the message vocabulary (`next`, `pause`, `approve`, `revise`, `continue`, and so on); pi-actors owns the safe run-local transport, coordinator-session ownership checks, and coordinator attention policy.
131
131
 
132
132
  ## Tool Surface
133
133
 
134
134
  The actor-level surface is:
135
135
 
136
136
  - `spawn`: start a detached `run:<id>` actor from `file`, `recipe`, or inline `template`.
137
- - `message`: send one typed envelope to `run:<id>`, `branch:<run>/<branch>`, `tool:<name>`, or `coordinator`.
138
- - `inspect`: intentionally read `run:<id>` status, tail, events, artifacts, files, or mailbox metadata; read `session:<id>` or `session:all` run inventory with optional status filtering.
137
+ - `message`: send one typed envelope to `run:<id>`, `branch:<run>/<branch>`, `tool:<name>`, `coordinator`, or `session:<id>`.
138
+ - `inspect`: intentionally read owned `run:<id>` status, tail, events, artifacts, files, or mailbox metadata; read current `coordinator` run inventory only when a coordinator session is known; read `session:<id>` or `session:all` run inventory with optional status filtering when the session is explicit; read `tool:<name>` status or schema for registered tool actors.
139
139
 
140
140
  Low-level async actions map into the actor surface instead of forming a second public model:
141
141
 
142
142
  - start → `spawn`
143
143
  - send/control → `message`
144
144
  - status/tail/events/list → `inspect`
145
- - stop/kill → runtime control messages with synchronous results
145
+ - stop/kill → `message` with `control.stop` or `control.kill`, with synchronous results
146
146
 
147
147
  Compact text is returned by default so async management does not flood agent context; use verbose inspection when the full state object is needed. List output intentionally shares one state root across music, subagents, timers, and other async work; source fields such as `tool` and `recipe` distinguish run purpose when the launcher recorded them. Registered tools are the preferred user-facing surface for reusable recipes.
148
148
 
@@ -172,13 +172,13 @@ Native Windows does not support this Unix FIFO contract. Use WSL/Linux/macOS for
172
172
 
173
173
  ## Coordinator Notifications
174
174
 
175
- The launching coordinator should not busy-poll long-running async runs. The extension watches run state directories and delivers terminal `done`/`failed`/unhandled `killed`/`exited` transitions plus script-authored `notify`/`followup` actor messages back to the owning session. This gives the top-level async task a completion signal on the happy path while still letting recipe-local messages bubble up when scripts need finer-grained notifications. Terminal follow-ups include recipe-level named `artifacts` when declared. The generic runner also emits compact `command.done` actor messages for completed leaf commands; recipe authors declare that capability in `mailbox.emits` rather than configuring a separate delivery policy. Failures and in-flight parallel branch completions can bubble as follow-ups, while successful final leaf completions stay diagnostic to avoid flooding long sequential pipelines. Branch-level `command.done` follow-ups omit artifact manifests because the top-level terminal follow-up carries them once. Intentional `runtime.cancel`, `runtime.kill`, and control messages such as `stop` stay out of follow-up context because the initiating message already returns synchronously. If a follow-up asks for direction, answer with `message` rather than starting a polling loop. Use explicit `inspect` only when a delivered follow-up requests inspection, a real decision depends on state, or a suspected stuck run needs diagnosis — never merely because a timeout elapsed.
175
+ The launching coordinator should not busy-poll long-running async runs. The extension watches run state directories and delivers terminal `done`/`failed`/unhandled `killed`/`exited` transitions plus script-authored `notify`/`followup` actor messages back to the owning session. This gives the top-level async task a completion signal on the happy path while still letting recipe-local messages bubble up when scripts need finer-grained notifications. Terminal follow-ups include recipe-level named `artifacts` when declared. The generic runner also emits compact `command.done` actor messages for completed leaf commands; recipe authors declare that capability in `mailbox.emits` rather than configuring a separate delivery policy. Failures and in-flight parallel branch completions can bubble as follow-ups, while successful final leaf completions stay diagnostic to avoid flooding long sequential pipelines. Branch-level `command.done` follow-ups omit artifact manifests because the top-level terminal follow-up carries them once. Intentional `control.stop`, `control.kill`, and recipe-local stop commands stay out of follow-up context because the initiating message already returns synchronously. If a follow-up asks for direction, answer with `message` rather than starting a polling loop. Use explicit `inspect` only when a delivered follow-up requests inspection, a real decision depends on state, or a suspected stuck run needs diagnosis — never merely because a timeout elapsed.
176
176
 
177
177
  Ambient status indicators may refresh while work is active, but coordinator attention is event-driven from state-file changes rather than a coordinator agent loop. This lets the coordinator continue other work after `spawn`; the run signals back through `events.jsonl`, `result.json`, and `outbox.jsonl`. The ambient triangle count represents active async work units: each running async run contributes at least one triangle, and a run with multiple active parallel command/subagent branches contributes the reported active branch count. If a coordinator starts one parent run with four active parallel branches, four triangles are shown; if the same coordinator starts five independent single-branch runs, five triangles are shown.
178
178
 
179
179
  ## Run Actor Messages
180
180
 
181
- A recipe or script may append coordinator-bound actor message records to:
181
+ A recipe or script may append coordinator-bound or session-bound actor message records to:
182
182
 
183
183
  ```text
184
184
  <state_dir>/outbox.jsonl
@@ -200,7 +200,7 @@ Shape:
200
200
 
201
201
  `level` is `info`, `warning`, or `error`. The public message describes sender, receiver, type, summary, and body; it does not choose notification mechanics. Runtime attention policy infers whether a coordinator-bound message stays available for explicit `inspect`, becomes a UI notification, or re-enters the launching coordinator as compact follow-up context.
202
202
 
203
- Use coordinator-bound messages for completion and decision points, not for every progress tick. Packaged multi-agent branch completion is a completion message and should bubble by default. Follow-up path lists use Markdown hierarchy: a section heading, `- Base: ...`, and `- Files: ...`, so repeated run-state prefixes do not flood agent context.
203
+ Use coordinator/session-bound messages for completion and decision points, not for every progress tick. Packaged multi-agent branch completion is a completion message and should bubble by default. Follow-up path lists use Markdown hierarchy: a section heading, `- Base: ...`, and `- Files: ...`, so repeated run-state prefixes do not flood agent context.
204
204
 
205
205
  ## Cancellation And Ownership
206
206
 
@@ -32,7 +32,7 @@ Keep components narrow. Higher-level recipes should own composition, not hidden
32
32
 
33
33
  Start one subagent or branch with a caller-provided prompt and model. Launchers do not judge or merge output.
34
34
 
35
- Example: `recipes/subagent-prompt.json`.
35
+ Examples: `recipes/subagent-prompt.json`, `recipes/subagent-tools.json`, and `recipes/subagents-prompts.json`.
36
36
 
37
37
  ### Reviewers
38
38
 
@@ -46,7 +46,7 @@ Core subagent recipes:
46
46
  - `recipes/subagent-followup.json`: Same-context or degraded continuation.
47
47
  - `recipes/subagent-judge.json`: Post-merge/report quality judge.
48
48
 
49
- Most atoms expose policy knobs such as `model`, `thinking`, `tools`, `output_format`, `evidence_policy`, `risk_policy`, source policy, continuity policy, handoff format, or model pools. Interactive async atoms also declare mailbox metadata for their basic control, completion, and domain-result message surface. Higher-level recipes pass these knobs through instead of hard-coding local policy.
49
+ Most atoms expose policy knobs such as `model`, `thinking`, `tools`, `output_format`, `evidence_policy`, `risk_policy`, source policy, continuity policy, handoff format, or model pools. The generic prompt launchers, including `subagent-tools` and `subagents-prompts`, expose the same core model/thinking/tool/output knobs so callers do not need separate recipe families for policy tuning. Interactive async atoms also declare mailbox metadata for their basic control, completion, and domain-result message surface. Higher-level recipes pass these knobs through instead of hard-coding local policy.
50
50
 
51
51
  Register one atom:
52
52
 
@@ -82,6 +82,7 @@ Pipeline recipes demonstrate second-order composition:
82
82
  - `recipes/pipeline-media-library.json`: Playlist build → media-library artifact report.
83
83
  - `recipes/pipeline-artifact-report.json`: Normalize → artifact-shaped output → actor-message-shaped record. This pipeline prepares a candidate artifact and emits `artifact.prepared`/`artifact.blocked`; the `artifact_path` is a target path, not a guarantee that the file was written.
84
84
  - `recipes/pipeline-artifact-write.json`: Normalize → artifact-shaped output → deterministic artifact write → actor-message-shaped record. Use only when the caller explicitly wants filesystem writes; `write_mode` is `create`, `overwrite`, or `append`.
85
+ - `recipes/pipeline-artifact-bundle.json`: Optional validation → deterministic artifact write → machine-readable manifest generation → deterministic manifest write → actor-message-shaped record. Use when the caller explicitly wants a filesystem handoff bundle with both artifact and manifest paths.
85
86
 
86
87
  These are examples of library composition, not a workflow DSL. Pipeline recipes declare mailbox metadata for their high-level completion, artifact, and control message surface. The recipe layer owns imports and saved defaults; command templates own execution shape; async runs own lifecycle.
87
88
 
@@ -98,6 +99,7 @@ Utility recipes cover local operator workflows that do not need subagents:
98
99
  - `recipes/utility-changelog-head.json`: Read the top slice of a changelog for release summary prep.
99
100
  - `recipes/utility-playlist-scan.json`: List local media files as playlist-building input.
100
101
  - `recipes/utility-run-summary.json`: Use `scripts/recipe-utils.mjs` to summarize async run state files as JSON.
102
+ - `recipes/utility-run-ops-snapshot.json`: Combine async run summaries, event-tail JSONL, and stale/terminal recommendations into one structured operations snapshot.
101
103
  - `recipes/utility-playlist-build.json`: Use `scripts/recipe-utils.mjs` to build a filtered playlist listing as newline paths, M3U, or inline `|`-separated source.
102
104
  - `recipes/utility-changelog-section.json`: Use `scripts/recipe-utils.mjs` to extract one changelog release section.
103
105
  - `recipes/utility-artifact-manifest.json`: Use `scripts/recipe-utils.mjs` to emit a machine-readable JSON manifest for an artifact path.
@@ -173,4 +175,4 @@ Message body is currently adapted to one newline-delimited command written to `<
173
175
  - Only play trusted local files or URLs.
174
176
  - Volume is clamped to `0..100` by the wrapper.
175
177
  - Prefer a stable `run_id` such as `music` when the operator expects to control the run by name.
176
- - Use `message type=runtime.kill` only when graceful cancellation fails.
178
+ - Use `message type=control.kill` only when graceful `control.stop` cancellation fails.
@@ -108,7 +108,7 @@ Existing seeds:
108
108
 
109
109
  Implemented seed:
110
110
 
111
- - `pipeline-async-run-ops`: run summary event tail → normalized operations report → artifact report.
111
+ - `pipeline-async-run-ops`: structured run operations snapshot → normalized operations report → artifact report. The snapshot combines run summary, event tail, and recommended inspect/control messages before the LLM normalization step.
112
112
 
113
113
  ### Research Brief Cell
114
114
 
@@ -227,7 +227,7 @@ Prefer adding a high-level recipe when at least three cells already exist and th
227
227
  Good next candidates for the standard library after the first task-first wave:
228
228
 
229
229
  1. Package/release metadata enrichment: use `utility-package-summary` with changelog and validation cells to make release-readiness reports more evidence-rich without adding publish automation.
230
- 2. Artifact packaging and manifesting: compose `pipeline-artifact-write`, `utility-artifact-manifest`, artifact reports, and validation summaries into a machine-readable handoff bundle when the caller explicitly requests filesystem writes.
230
+ 2. Artifact packaging and manifesting: implemented as `pipeline-artifact-bundle`, which composes optional validation, `pipeline-artifact-write`, `utility-artifact-manifest`, deterministic manifest writing, and an actor-message handoff when the caller explicitly requests filesystem writes.
231
231
  3. Async run cleanup planning: extend async-run operations with stale-run classification and recommended `message`, `cancel`, or `kill` controls, keeping actual control execution operator-gated.
232
232
 
233
- Each candidate should land with the minimum missing cells rather than a broad one-shot framework. Already implemented task-first seeds include `pipeline-release-readiness`, `pipeline-repo-health`, `pipeline-async-run-ops`, `pipeline-docs-maintenance`, and `pipeline-media-library`.
233
+ Each candidate should land with the minimum missing cells rather than a broad one-shot framework. Already implemented task-first seeds include `pipeline-release-readiness`, `pipeline-repo-health`, `pipeline-async-run-ops`, `pipeline-docs-maintenance`, `pipeline-media-library`, and `pipeline-artifact-bundle`.
package/index.ts CHANGED
@@ -194,5 +194,9 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
194
194
  getTool: (name) => actorToolDefinitions.get(name),
195
195
  }),
196
196
  );
197
- pi.registerTool(Tools.createInspectToolDefinition());
197
+ pi.registerTool(
198
+ Tools.createInspectToolDefinition<ExtensionContext>({
199
+ getTool: (name) => actorToolDefinitions.get(name),
200
+ }),
201
+ );
198
202
  }
package/lib/execution.ts CHANGED
@@ -98,6 +98,20 @@ function createTemplateConfig(
98
98
  return { args: cfg.args, defaults: cfg.defaults, template: cfg.template };
99
99
  }
100
100
 
101
+ function quoteCommandDetailPart(value: string): string {
102
+ if (value === "") return "''";
103
+ if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value)) return value;
104
+ return `'${value.replaceAll("'", "'\\''")}'`;
105
+ }
106
+
107
+ function formatInvocationDetail(
108
+ invocation: CommandTemplates.CommandTemplateInvocation,
109
+ ): string {
110
+ return [invocation.command, ...invocation.args]
111
+ .map(quoteCommandDetailPart)
112
+ .join(" ");
113
+ }
114
+
101
115
  function formatCommandDetail(commands: string[]): string {
102
116
  return commands.length === 1 ? commands[0] : commands.join(" && ");
103
117
  }
@@ -494,7 +508,7 @@ async function executeTemplateConfig(
494
508
  });
495
509
  return {
496
510
  branches: [],
497
- commands: [invocation.command],
511
+ commands: [formatInvocationDetail(invocation)],
498
512
  failures: [],
499
513
  result,
500
514
  };
package/lib/tools.ts CHANGED
@@ -216,6 +216,15 @@ function compactSessionRuns(session: string, runs: Array<Record<string, unknown>
216
216
  .join("\n")}`;
217
217
  }
218
218
 
219
+ function compactToolActor(name: string, tool: Record<string, unknown>): string {
220
+ const parameters = asRecord(tool.parameters);
221
+ const required = Array.isArray(parameters.required)
222
+ ? parameters.required.join(",")
223
+ : "";
224
+ const properties = asRecord(parameters.properties);
225
+ return `\ntool=${name} description=${String(tool.description ?? "").replaceAll(/\s+/g, "_")} args=${Object.keys(properties).join(",")} required=${required}`;
226
+ }
227
+
219
228
  function compactActorMessageResult(
220
229
  message: ActorMessages.ActorMessage,
221
230
  result: Record<string, unknown>,
@@ -335,6 +344,9 @@ export function createSpawnToolDefinition<
335
344
  template: unionSchema([
336
345
  stringSchema("Inline command template string"),
337
346
  arraySchema("Inline command-template sequence or parallel tree"),
347
+ looseObjectSchema(
348
+ "Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template.",
349
+ ),
338
350
  ]),
339
351
  values: looseObjectSchema("Runtime placeholder values passed to the actor."),
340
352
  verbose: booleanSchema("Return full JSON instead of compact text."),
@@ -379,17 +391,44 @@ export function createSpawnToolDefinition<
379
391
  };
380
392
  }
381
393
 
382
- export function createInspectToolDefinition(): any {
394
+ export interface InspectToolDeps<TContext = unknown> {
395
+ getTool?: (name: string) => any | undefined;
396
+ }
397
+
398
+ function getContextSessionId(ctx: unknown): string | undefined {
399
+ return (ctx as AsyncRunToolContext | undefined)?.sessionManager?.getSessionId?.();
400
+ }
401
+
402
+ function requireContextSessionId(ctx: unknown, actor: string): string {
403
+ const sessionId = getContextSessionId(ctx);
404
+ if (!sessionId) {
405
+ throw new Error(`${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`);
406
+ }
407
+ return sessionId;
408
+ }
409
+
410
+ function assertRunAccessibleToContext(runId: string, ctx: unknown): Record<string, unknown> {
411
+ const status = AsyncRuns.getRunStatus(runId);
412
+ const sessionId = getContextSessionId(ctx);
413
+ if (sessionId && status.ownerId && status.ownerId !== sessionId) {
414
+ throw new Error(`run:${runId} is owned by session:${status.ownerId}; current session is ${sessionId}.`);
415
+ }
416
+ return status;
417
+ }
418
+
419
+ export function createInspectToolDefinition<TContext = unknown>(
420
+ deps: InspectToolDeps<TContext> = {},
421
+ ): any {
383
422
  return {
384
423
  name: "inspect",
385
424
  label: "Inspect",
386
425
  description:
387
- "Intentionally inspect an actor. Supports run:<id> views: status, tail, events, artifacts, files, mailbox; and session:<id> status.",
426
+ "Intentionally inspect an actor. Supports run:<id> views: status, tail, events, artifacts, files, mailbox; coordinator/session status; and tool:<name> status/schema.",
388
427
  parameters: objectSchema(
389
428
  {
390
429
  lines: stringSchema("Line count for tail/events views. Default 40."),
391
430
  status: stringSchema("Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited."),
392
- target: stringSchema("Actor address to inspect, e.g. run:<id>, session:<id>, or session:all."),
431
+ target: stringSchema("Actor address to inspect, e.g. run:<id>, coordinator, session:<id>, session:all, or tool:<name>."),
393
432
  verbose: booleanSchema("Return full JSON instead of compact text where available."),
394
433
  view: stringSchema("Inspection view: status, tail, events, artifacts, files, or mailbox."),
395
434
  },
@@ -400,12 +439,30 @@ export function createInspectToolDefinition(): any {
400
439
  params: unknown,
401
440
  _signal: AbortSignal | undefined,
402
441
  _onUpdate: unknown,
403
- _ctx: unknown,
442
+ ctx: TContext,
404
443
  ) {
405
444
  const input = asRecord(params);
406
445
  const target = String(input.target ?? "");
407
446
  const address = ActorMessages.parseActorAddress(target);
408
447
  const view = String(input.view ?? "");
448
+ if (address.kind === "coordinator") {
449
+ if (view !== "status" && view !== "runs") {
450
+ throw new Error("inspect coordinator supports view=status or view=runs.");
451
+ }
452
+ const session = requireContextSessionId(ctx, "inspect coordinator");
453
+ const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
454
+ .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
455
+ .filter((run) => run.ownerId === session);
456
+ return {
457
+ content: [
458
+ {
459
+ type: "text" as const,
460
+ text: maybeJsonText({ session, runs }, input.verbose === true, compactSessionRuns(session, runs)),
461
+ },
462
+ ],
463
+ details: { session, runs },
464
+ };
465
+ }
409
466
  if (address.kind === "session") {
410
467
  if (view !== "status" && view !== "runs") {
411
468
  throw new Error("inspect session:<id> supports view=status or view=runs.");
@@ -423,11 +480,33 @@ export function createInspectToolDefinition(): any {
423
480
  details: { session: address.value, runs },
424
481
  };
425
482
  }
483
+ if (address.kind === "tool" && address.value) {
484
+ if (view !== "status" && view !== "schema") {
485
+ throw new Error("inspect tool:<name> supports view=status or view=schema.");
486
+ }
487
+ const tool = deps.getTool?.(address.value);
488
+ if (!tool) throw new Error(`tool actor not found: ${address.value}`);
489
+ const details = {
490
+ name: address.value,
491
+ description: tool.description,
492
+ parameters: tool.parameters,
493
+ promptSnippet: tool.promptSnippet,
494
+ };
495
+ return {
496
+ content: [
497
+ {
498
+ type: "text" as const,
499
+ text: maybeJsonText(details, input.verbose === true || view === "schema", compactToolActor(address.value, details)),
500
+ },
501
+ ],
502
+ details,
503
+ };
504
+ }
426
505
  const runId = address.kind === "run" ? address.value : undefined;
427
- if (!runId) throw new Error("inspect target must be run:<id> or session:<id>.");
506
+ if (!runId) throw new Error("inspect target must be run:<id>, coordinator, session:<id>, or tool:<name>.");
428
507
  switch (view) {
429
508
  case "status": {
430
- const status = AsyncRuns.getRunStatus(runId);
509
+ const status = assertRunAccessibleToContext(runId, ctx);
431
510
  return {
432
511
  content: [
433
512
  {
@@ -439,10 +518,12 @@ export function createInspectToolDefinition(): any {
439
518
  };
440
519
  }
441
520
  case "tail": {
521
+ assertRunAccessibleToContext(runId, ctx);
442
522
  const text = AsyncRuns.tailRun(runId, Number(input.lines || 40));
443
523
  return { content: [{ type: "text" as const, text: `\n${text}` }], details: {} };
444
524
  }
445
525
  case "events": {
526
+ assertRunAccessibleToContext(runId, ctx);
446
527
  const events = AsyncRuns.readRunEvents(runId, Number(input.lines || 40));
447
528
  return {
448
529
  content: [
@@ -456,7 +537,7 @@ export function createInspectToolDefinition(): any {
456
537
  }
457
538
  case "artifacts":
458
539
  case "files": {
459
- const status = AsyncRuns.getRunStatus(runId);
540
+ const status = assertRunAccessibleToContext(runId, ctx);
460
541
  return {
461
542
  content: [
462
543
  {
@@ -468,7 +549,7 @@ export function createInspectToolDefinition(): any {
468
549
  };
469
550
  }
470
551
  case "mailbox": {
471
- const status = AsyncRuns.getRunStatus(runId);
552
+ const status = assertRunAccessibleToContext(runId, ctx);
472
553
  const mailbox = asRecord(status.mailbox);
473
554
  return {
474
555
  content: [
@@ -498,7 +579,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
498
579
  name: "message",
499
580
  label: "Message",
500
581
  description:
501
- "Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, tool:<name> calls, and coordinator-bound run messages.",
582
+ "Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, tool:<name> calls, and coordinator/session-bound run messages.",
502
583
  parameters: objectSchema(
503
584
  {
504
585
  body: unionSchema([
@@ -529,9 +610,10 @@ export function createActorMessageToolDefinition<TContext = unknown>(
529
610
  const address = ActorMessages.parseActorAddress(message.to);
530
611
  let result: Record<string, unknown>;
531
612
  if (address.kind === "run" && address.value) {
532
- if (message.type === "runtime.cancel") {
613
+ assertRunAccessibleToContext(address.value, ctx);
614
+ if (message.type === "control.stop" || message.type === "control.cancel" || message.type === "runtime.cancel") {
533
615
  result = AsyncRuns.cancelRun(address.value);
534
- } else if (message.type === "runtime.kill") {
616
+ } else if (message.type === "control.kill" || message.type === "runtime.kill") {
535
617
  result = AsyncRuns.killRun(address.value);
536
618
  } else {
537
619
  result = AsyncRuns.sendRunMessage(
@@ -540,6 +622,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
540
622
  );
541
623
  }
542
624
  } else if (address.kind === "branch" && address.value) {
625
+ assertRunAccessibleToContext(address.value, ctx);
543
626
  result = AsyncRuns.sendRunMessage(
544
627
  address.value,
545
628
  JSON.stringify(message),
@@ -562,17 +645,27 @@ export function createActorMessageToolDefinition<TContext = unknown>(
562
645
  tool: address.value,
563
646
  tool_result: toolResult,
564
647
  };
565
- } else if (address.kind === "coordinator") {
648
+ } else if (address.kind === "coordinator" || address.kind === "session") {
566
649
  if (!message.from) {
567
- throw new Error("message to coordinator requires from=run:<id>.");
650
+ throw new Error(`message to ${address.kind} requires from=run:<id>.`);
568
651
  }
569
652
  const sender = ActorMessages.parseActorAddress(message.from);
570
653
  if (sender.kind !== "run" || !sender.value) {
571
- throw new Error("message to coordinator currently requires from=run:<id>.");
654
+ throw new Error(`message to ${address.kind} currently requires from=run:<id>.`);
655
+ }
656
+ const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
657
+ if (address.kind === "session") {
658
+ if (!senderStatus.ownerId) {
659
+ throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got no owner.`);
660
+ }
661
+ if (senderStatus.ownerId !== address.value) {
662
+ throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`);
663
+ }
572
664
  }
573
665
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
574
666
  body: message.body,
575
667
  correlation_id: message.correlation_id,
668
+ delivery: address.kind === "session" ? "followup" : undefined,
576
669
  event: message.type,
577
670
  from: message.from,
578
671
  metadata: message.metadata,
@@ -583,7 +676,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
583
676
  });
584
677
  } else {
585
678
  throw new Error(
586
- `message currently supports run:<id>, branch:<run>/<branch>, tool:<name>, and coordinator destinations; unsupported destination: ${message.to}`,
679
+ `message currently supports run:<id>, branch:<run>/<branch>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
587
680
  );
588
681
  }
589
682
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.12.9",
3
+ "version": "0.12.13",
4
4
  "private": false,
5
5
  "description": "Actor runtime and orchestrator for agent-managed local processes",
6
6
  "type": "module",
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "pipeline-artifact-bundle",
3
+ "async": true,
4
+ "imports": {
5
+ "artifactWrite": "pipeline-artifact-write.json",
6
+ "manifest": "utility-artifact-manifest.json",
7
+ "manifestWrite": "utility-artifact-write.json",
8
+ "validation": "utility-validation-wrapper.json",
9
+ "message": "utility-actor-message.json"
10
+ },
11
+ "args": [
12
+ "input:string",
13
+ "artifact_path:path",
14
+ "manifest_path:path",
15
+ "artifact_title:string",
16
+ "artifact_format:string",
17
+ "artifact_status:enum(draft,ready,blocked,accepted)",
18
+ "write_mode:enum(create,overwrite,append)",
19
+ "run_validation:bool",
20
+ "validation_command:string",
21
+ "validation_scope:string",
22
+ "validation_timeout_ms:int",
23
+ "model:string",
24
+ "tools:string"
25
+ ],
26
+ "defaults": {
27
+ "artifact_title": "Artifact Bundle",
28
+ "artifact_format": "Markdown sections: Summary, Findings, Evidence, Risks, Next Actions.",
29
+ "artifact_status": "ready",
30
+ "write_mode": "create",
31
+ "run_validation": "false",
32
+ "validation_command": "true",
33
+ "validation_scope": ".",
34
+ "validation_timeout_ms": "300000",
35
+ "model": "openai-codex/gpt-5.5",
36
+ "tools": ""
37
+ },
38
+ "mailbox": {
39
+ "accepts": ["control.stop"],
40
+ "emits": ["artifact.bundle_ready", "artifact.written", "artifact.blocked", "run.done", "run.failed"]
41
+ },
42
+ "artifacts": {
43
+ "artifact": "{artifact_path}",
44
+ "manifest": "{manifest_path}"
45
+ },
46
+ "template": [
47
+ {
48
+ "when": "run_validation",
49
+ "name": "validation",
50
+ "values": {
51
+ "command": "{validation_command}",
52
+ "scope": "{validation_scope}",
53
+ "timeout_ms": "{validation_timeout_ms}"
54
+ }
55
+ },
56
+ {
57
+ "name": "artifactWrite",
58
+ "values": {
59
+ "input": "{input}",
60
+ "artifact_path": "{artifact_path}",
61
+ "artifact_format": "{artifact_format}",
62
+ "write_mode": "{write_mode}",
63
+ "model": "{model}",
64
+ "tools": "{tools}"
65
+ }
66
+ },
67
+ {
68
+ "name": "manifest",
69
+ "values": {
70
+ "artifact_path": "{artifact_path}",
71
+ "title": "{artifact_title}",
72
+ "status": "{artifact_status}",
73
+ "summary": "Artifact bundle manifest for {artifact_path}."
74
+ }
75
+ },
76
+ {
77
+ "name": "manifestWrite",
78
+ "values": {
79
+ "artifact_path": "{manifest_path}",
80
+ "mode": "{write_mode}"
81
+ }
82
+ },
83
+ {
84
+ "name": "message",
85
+ "values": {
86
+ "type": "artifact.bundle_ready",
87
+ "summary": "Artifact bundle ready: {artifact_path} with manifest {manifest_path}.",
88
+ "metadata": "{\"artifact\":\"{artifact_path}\",\"manifest\":\"{manifest_path}\"}"
89
+ }
90
+ }
91
+ ]
92
+ }
@@ -2,8 +2,7 @@
2
2
  "name": "pipeline-async-run-ops",
3
3
  "async": true,
4
4
  "imports": {
5
- "summary": "utility-run-summary.json",
6
- "events": "utility-jsonl-tail.json",
5
+ "snapshot": "utility-run-ops-snapshot.json",
7
6
  "normalizer": "subagent-normalize.json",
8
7
  "artifact": "pipeline-artifact-report.json"
9
8
  },
@@ -11,6 +10,7 @@
11
10
  "state_root:path",
12
11
  "event_file:path",
13
12
  "lines:int",
13
+ "stale_minutes:int",
14
14
  "artifact_path:path",
15
15
  "model:string",
16
16
  "tools:string"
@@ -19,6 +19,7 @@
19
19
  "state_root": "~/.pi/agent/tmp/pi-actors/runs",
20
20
  "event_file": "~/.pi/agent/tmp/pi-actors/runs/music/outbox.jsonl",
21
21
  "lines": "80",
22
+ "stale_minutes": "60",
22
23
  "artifact_path": "./async-run-ops.md",
23
24
  "model": "openai-codex/gpt-5.5",
24
25
  "tools": ""
@@ -29,24 +30,18 @@
29
30
  },
30
31
  "template": [
31
32
  {
32
- "name": "summary",
33
+ "name": "snapshot",
33
34
  "values": {
34
- "state_root": "{state_root}"
35
- }
36
- },
37
- {
38
- "name": "events",
39
- "failure": "continue",
40
- "values": {
41
- "file": "{event_file}",
35
+ "state_root": "{state_root}",
36
+ "event_file": "{event_file}",
42
37
  "lines": "{lines}",
43
- "mode": "raw"
38
+ "stale_minutes": "{stale_minutes}"
44
39
  }
45
40
  },
46
41
  {
47
42
  "name": "normalizer",
48
43
  "values": {
49
- "input": "Use async run summary and event tail from stdin. State root: {state_root}. Event file: {event_file}.",
44
+ "input": "Use async run operations snapshot JSON from stdin. State root: {state_root}. Event file: {event_file}.",
50
45
  "format": "Markdown sections: Active Runs, Terminal Runs, Recent Events, Stale/Risky Runs, Recommended Controls, Follow-up Needed.",
51
46
  "model": "{model}",
52
47
  "thinking": "off",
@@ -4,14 +4,18 @@
4
4
  "args": [
5
5
  "prompt:string",
6
6
  "tools:string",
7
- "model:string"
7
+ "model:string",
8
+ "thinking:string",
9
+ "output_format:string"
8
10
  ],
9
11
  "defaults": {
10
- "model": "openai-codex/gpt-5.5"
12
+ "model": "openai-codex/gpt-5.5",
13
+ "thinking": "off",
14
+ "output_format": "Concise Markdown with assumptions and next actions."
11
15
  },
12
16
  "mailbox": {
13
17
  "accepts": ["control.stop"],
14
18
  "emits": ["command.done", "run.done", "run.failed"]
15
19
  },
16
- "template": "pi -p --model {model} --tools {tools} {prompt}"
20
+ "template": "pi -p --model {model} --thinking {thinking} --tools {tools} {prompt}. Output format: {output_format}"
17
21
  }
@@ -6,10 +6,18 @@
6
6
  },
7
7
  "args": [
8
8
  "prompts:array",
9
+ "model:string",
10
+ "thinking:string",
11
+ "tools:string",
12
+ "output_format:string",
9
13
  "report_path:path",
10
14
  "summary_path:path"
11
15
  ],
12
16
  "defaults": {
17
+ "model": "openai-codex/gpt-5.5",
18
+ "thinking": "off",
19
+ "tools": "",
20
+ "output_format": "Concise Markdown with assumptions and next actions.",
13
21
  "report_path": "{state_dir}/stdout.log",
14
22
  "summary_path": "{state_dir}/result.json"
15
23
  },
@@ -26,7 +34,11 @@
26
34
  "template": {
27
35
  "name": "subagent",
28
36
  "values": {
29
- "prompt": "{prompts[index]}"
37
+ "prompt": "{prompts[index]}",
38
+ "model": "{model}",
39
+ "thinking": "{thinking}",
40
+ "tools": "{tools}",
41
+ "output_format": "{output_format}"
30
42
  }
31
43
  }
32
44
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "utility-run-ops-snapshot",
3
+ "args": [
4
+ "repo:path",
5
+ "state_root:path",
6
+ "event_file:path",
7
+ "lines:int",
8
+ "stale_minutes:int"
9
+ ],
10
+ "defaults": {
11
+ "repo": "~/.pi/agent/extensions/pi-actors",
12
+ "state_root": "~/.pi/agent/tmp/pi-actors/runs",
13
+ "event_file": "~/.pi/agent/tmp/pi-actors/runs/music/outbox.jsonl",
14
+ "lines": "80",
15
+ "stale_minutes": "60"
16
+ },
17
+ "template": "{repo}/scripts/recipe-utils.mjs run-ops-snapshot {state_root} {event_file} {lines} {stale_minutes}"
18
+ }
@@ -43,6 +43,19 @@ function event(name, data = {}) {
43
43
  `${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`,
44
44
  );
45
45
  }
46
+ function quoteCommandDetailPart(value) {
47
+ if (value === "") return "''";
48
+ if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value)) return value;
49
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
50
+ }
51
+ function formatCommandDetail(command, args) {
52
+ return [command, ...args].map(quoteCommandDetailPart).join(" ");
53
+ }
54
+ function summarizeCommandDetail(commandDetail) {
55
+ return commandDetail.length > 160
56
+ ? `${commandDetail.slice(0, 157)}...`
57
+ : commandDetail;
58
+ }
46
59
  function getCommandDoneDelivery(result) {
47
60
  return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
48
61
  }
@@ -76,30 +89,35 @@ function progressRunning() {
76
89
  });
77
90
  }
78
91
  async function observedExec(command, args, options) {
92
+ const commandDetail = formatCommandDetail(command, args);
79
93
  activeSubagents += 1;
80
- event("command.start", { activeSubagents, command });
94
+ event("command.start", { activeSubagents, command: commandDetail });
81
95
  progressRunning();
82
96
  const result = await execCommandTemplate(command, args, options);
83
97
  activeSubagents = Math.max(0, activeSubagents - 1);
84
98
  completedSubagents += 1;
85
99
  if (result.code !== 0) {
86
- subagentFailures.push({ code: result.code, command, killed: result.killed });
100
+ subagentFailures.push({
101
+ code: result.code,
102
+ command: commandDetail,
103
+ killed: result.killed,
104
+ });
87
105
  }
88
106
  event("command.done", {
89
107
  activeSubagents,
90
108
  code: result.code,
91
- command,
109
+ command: commandDetail,
92
110
  killed: result.killed,
93
111
  });
94
112
  outbox(
95
113
  "command.done",
96
- `Command ${command} completed with code ${result.code}`,
114
+ `Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`,
97
115
  {
98
116
  activeSubagents,
99
117
  ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
100
118
  run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath],
101
119
  code: result.code,
102
- command,
120
+ command: commandDetail,
103
121
  killed: result.killed,
104
122
  },
105
123
  getCommandDoneDelivery(result),
@@ -5,6 +5,7 @@ import { dirname, extname, join, relative, resolve } from "node:path";
5
5
  function usage() {
6
6
  console.error(`Usage:
7
7
  recipe-utils.mjs run-summary <state-root>
8
+ recipe-utils.mjs run-ops-snapshot <state-root> <event-file> [lines] [stale-minutes]
8
9
  recipe-utils.mjs playlist <source-dir> [extensions] [max-depth] [paths|m3u|inline]
9
10
  recipe-utils.mjs changelog-section <file> <version>
10
11
  recipe-utils.mjs artifact-manifest <artifact-path> <title> <status> [summary]
@@ -75,7 +76,7 @@ function getRunStatus(run, progress, result) {
75
76
  return run.status ?? "unknown";
76
77
  }
77
78
 
78
- function runSummary(rootValue) {
79
+ function collectRunSummary(rootValue) {
79
80
  const root = resolve(
80
81
  rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
81
82
  );
@@ -105,7 +106,51 @@ function runSummary(rootValue) {
105
106
  rows.sort((a, b) =>
106
107
  `${a.status}:${a.run}`.localeCompare(`${b.status}:${b.run}`),
107
108
  );
108
- console.log(JSON.stringify(rows, null, 2));
109
+ return rows;
110
+ }
111
+
112
+ function runSummary(rootValue) {
113
+ console.log(JSON.stringify(collectRunSummary(rootValue), null, 2));
114
+ }
115
+
116
+ function tailJsonl(fileValue, linesValue = "80") {
117
+ const file = resolve(fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
118
+ if (!existsSync(file)) return [];
119
+ const lines = Number.parseInt(linesValue, 10);
120
+ const count = Number.isFinite(lines) && lines > 0 ? lines : 80;
121
+ return readFileSync(file, "utf8").trimEnd().split("\n").filter(Boolean).slice(-count).map((line) => {
122
+ try {
123
+ return JSON.parse(line);
124
+ } catch {
125
+ return { raw: line };
126
+ }
127
+ });
128
+ }
129
+
130
+ function runOpsSnapshot(rootValue, eventFileValue, linesValue = "80", staleMinutesValue = "60") {
131
+ const runs = collectRunSummary(rootValue);
132
+ const staleMs = Number(staleMinutesValue) * 60 * 1000;
133
+ const now = Date.now();
134
+ const recommendations = runs.flatMap((run) => {
135
+ const updatedMs = Date.parse(run.updated || "");
136
+ const stale = Number.isFinite(updatedMs) && Number.isFinite(staleMs) && now - updatedMs > staleMs;
137
+ if (run.status === "running" && stale) {
138
+ return [{
139
+ run: run.run,
140
+ reason: "running-stale",
141
+ suggested_message: { to: `run:${run.run}`, type: "control.stop", body: "stop" },
142
+ }];
143
+ }
144
+ if (["failed", "exited", "killed"].includes(run.status)) {
145
+ return [{
146
+ run: run.run,
147
+ reason: `terminal-${run.status}`,
148
+ suggested_inspect: { target: `run:${run.run}`, view: "tail" },
149
+ }];
150
+ }
151
+ return [];
152
+ });
153
+ console.log(JSON.stringify({ runs, events: tailJsonl(eventFileValue, linesValue), recommendations }, null, 2));
109
154
  }
110
155
 
111
156
  function playlist(
@@ -255,6 +300,8 @@ if (!command) {
255
300
 
256
301
  if (command === "run-summary")
257
302
  runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
303
+ else if (command === "run-ops-snapshot")
304
+ runOpsSnapshot(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs", args[1] ?? "~/.pi/agent/tmp/pi-actors/runs/music/outbox.jsonl", args[2], args[3]);
258
305
  else if (command === "playlist")
259
306
  playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
260
307
  else if (command === "changelog-section")