@llblab/pi-actors 0.12.9 → 0.12.10

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`, and run termination now accepts actor-native `control.stop`/`control.cancel`/`control.kill` aliases while preserving legacy `runtime.*` compatibility. 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,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.12.10: Actor Ownership and Recipe Operations
4
+
5
+ - `[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.
6
+ - `[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>`.
7
+ - `[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.
8
+ - `[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.
9
+ - `[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.
10
+ - `[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.
11
+ - `[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.
12
+ - `[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.
13
+ - `[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.
14
+ - `[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.
15
+ - `[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
16
 
5
17
  ## 0.12.9: Actor Runtime Hotfix
6
18
 
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/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>,
@@ -379,17 +388,44 @@ export function createSpawnToolDefinition<
379
388
  };
380
389
  }
381
390
 
382
- export function createInspectToolDefinition(): any {
391
+ export interface InspectToolDeps<TContext = unknown> {
392
+ getTool?: (name: string) => any | undefined;
393
+ }
394
+
395
+ function getContextSessionId(ctx: unknown): string | undefined {
396
+ return (ctx as AsyncRunToolContext | undefined)?.sessionManager?.getSessionId?.();
397
+ }
398
+
399
+ function requireContextSessionId(ctx: unknown, actor: string): string {
400
+ const sessionId = getContextSessionId(ctx);
401
+ if (!sessionId) {
402
+ throw new Error(`${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`);
403
+ }
404
+ return sessionId;
405
+ }
406
+
407
+ function assertRunAccessibleToContext(runId: string, ctx: unknown): Record<string, unknown> {
408
+ const status = AsyncRuns.getRunStatus(runId);
409
+ const sessionId = getContextSessionId(ctx);
410
+ if (sessionId && status.ownerId && status.ownerId !== sessionId) {
411
+ throw new Error(`run:${runId} is owned by session:${status.ownerId}; current session is ${sessionId}.`);
412
+ }
413
+ return status;
414
+ }
415
+
416
+ export function createInspectToolDefinition<TContext = unknown>(
417
+ deps: InspectToolDeps<TContext> = {},
418
+ ): any {
383
419
  return {
384
420
  name: "inspect",
385
421
  label: "Inspect",
386
422
  description:
387
- "Intentionally inspect an actor. Supports run:<id> views: status, tail, events, artifacts, files, mailbox; and session:<id> status.",
423
+ "Intentionally inspect an actor. Supports run:<id> views: status, tail, events, artifacts, files, mailbox; coordinator/session status; and tool:<name> status/schema.",
388
424
  parameters: objectSchema(
389
425
  {
390
426
  lines: stringSchema("Line count for tail/events views. Default 40."),
391
427
  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."),
428
+ target: stringSchema("Actor address to inspect, e.g. run:<id>, coordinator, session:<id>, session:all, or tool:<name>."),
393
429
  verbose: booleanSchema("Return full JSON instead of compact text where available."),
394
430
  view: stringSchema("Inspection view: status, tail, events, artifacts, files, or mailbox."),
395
431
  },
@@ -400,12 +436,30 @@ export function createInspectToolDefinition(): any {
400
436
  params: unknown,
401
437
  _signal: AbortSignal | undefined,
402
438
  _onUpdate: unknown,
403
- _ctx: unknown,
439
+ ctx: TContext,
404
440
  ) {
405
441
  const input = asRecord(params);
406
442
  const target = String(input.target ?? "");
407
443
  const address = ActorMessages.parseActorAddress(target);
408
444
  const view = String(input.view ?? "");
445
+ if (address.kind === "coordinator") {
446
+ if (view !== "status" && view !== "runs") {
447
+ throw new Error("inspect coordinator supports view=status or view=runs.");
448
+ }
449
+ const session = requireContextSessionId(ctx, "inspect coordinator");
450
+ const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
451
+ .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
452
+ .filter((run) => run.ownerId === session);
453
+ return {
454
+ content: [
455
+ {
456
+ type: "text" as const,
457
+ text: maybeJsonText({ session, runs }, input.verbose === true, compactSessionRuns(session, runs)),
458
+ },
459
+ ],
460
+ details: { session, runs },
461
+ };
462
+ }
409
463
  if (address.kind === "session") {
410
464
  if (view !== "status" && view !== "runs") {
411
465
  throw new Error("inspect session:<id> supports view=status or view=runs.");
@@ -423,11 +477,33 @@ export function createInspectToolDefinition(): any {
423
477
  details: { session: address.value, runs },
424
478
  };
425
479
  }
480
+ if (address.kind === "tool" && address.value) {
481
+ if (view !== "status" && view !== "schema") {
482
+ throw new Error("inspect tool:<name> supports view=status or view=schema.");
483
+ }
484
+ const tool = deps.getTool?.(address.value);
485
+ if (!tool) throw new Error(`tool actor not found: ${address.value}`);
486
+ const details = {
487
+ name: address.value,
488
+ description: tool.description,
489
+ parameters: tool.parameters,
490
+ promptSnippet: tool.promptSnippet,
491
+ };
492
+ return {
493
+ content: [
494
+ {
495
+ type: "text" as const,
496
+ text: maybeJsonText(details, input.verbose === true || view === "schema", compactToolActor(address.value, details)),
497
+ },
498
+ ],
499
+ details,
500
+ };
501
+ }
426
502
  const runId = address.kind === "run" ? address.value : undefined;
427
- if (!runId) throw new Error("inspect target must be run:<id> or session:<id>.");
503
+ if (!runId) throw new Error("inspect target must be run:<id>, coordinator, session:<id>, or tool:<name>.");
428
504
  switch (view) {
429
505
  case "status": {
430
- const status = AsyncRuns.getRunStatus(runId);
506
+ const status = assertRunAccessibleToContext(runId, ctx);
431
507
  return {
432
508
  content: [
433
509
  {
@@ -439,10 +515,12 @@ export function createInspectToolDefinition(): any {
439
515
  };
440
516
  }
441
517
  case "tail": {
518
+ assertRunAccessibleToContext(runId, ctx);
442
519
  const text = AsyncRuns.tailRun(runId, Number(input.lines || 40));
443
520
  return { content: [{ type: "text" as const, text: `\n${text}` }], details: {} };
444
521
  }
445
522
  case "events": {
523
+ assertRunAccessibleToContext(runId, ctx);
446
524
  const events = AsyncRuns.readRunEvents(runId, Number(input.lines || 40));
447
525
  return {
448
526
  content: [
@@ -456,7 +534,7 @@ export function createInspectToolDefinition(): any {
456
534
  }
457
535
  case "artifacts":
458
536
  case "files": {
459
- const status = AsyncRuns.getRunStatus(runId);
537
+ const status = assertRunAccessibleToContext(runId, ctx);
460
538
  return {
461
539
  content: [
462
540
  {
@@ -468,7 +546,7 @@ export function createInspectToolDefinition(): any {
468
546
  };
469
547
  }
470
548
  case "mailbox": {
471
- const status = AsyncRuns.getRunStatus(runId);
549
+ const status = assertRunAccessibleToContext(runId, ctx);
472
550
  const mailbox = asRecord(status.mailbox);
473
551
  return {
474
552
  content: [
@@ -498,7 +576,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
498
576
  name: "message",
499
577
  label: "Message",
500
578
  description:
501
- "Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, tool:<name> calls, and coordinator-bound run messages.",
579
+ "Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, tool:<name> calls, and coordinator/session-bound run messages.",
502
580
  parameters: objectSchema(
503
581
  {
504
582
  body: unionSchema([
@@ -529,9 +607,10 @@ export function createActorMessageToolDefinition<TContext = unknown>(
529
607
  const address = ActorMessages.parseActorAddress(message.to);
530
608
  let result: Record<string, unknown>;
531
609
  if (address.kind === "run" && address.value) {
532
- if (message.type === "runtime.cancel") {
610
+ assertRunAccessibleToContext(address.value, ctx);
611
+ if (message.type === "control.stop" || message.type === "control.cancel" || message.type === "runtime.cancel") {
533
612
  result = AsyncRuns.cancelRun(address.value);
534
- } else if (message.type === "runtime.kill") {
613
+ } else if (message.type === "control.kill" || message.type === "runtime.kill") {
535
614
  result = AsyncRuns.killRun(address.value);
536
615
  } else {
537
616
  result = AsyncRuns.sendRunMessage(
@@ -540,6 +619,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
540
619
  );
541
620
  }
542
621
  } else if (address.kind === "branch" && address.value) {
622
+ assertRunAccessibleToContext(address.value, ctx);
543
623
  result = AsyncRuns.sendRunMessage(
544
624
  address.value,
545
625
  JSON.stringify(message),
@@ -562,17 +642,27 @@ export function createActorMessageToolDefinition<TContext = unknown>(
562
642
  tool: address.value,
563
643
  tool_result: toolResult,
564
644
  };
565
- } else if (address.kind === "coordinator") {
645
+ } else if (address.kind === "coordinator" || address.kind === "session") {
566
646
  if (!message.from) {
567
- throw new Error("message to coordinator requires from=run:<id>.");
647
+ throw new Error(`message to ${address.kind} requires from=run:<id>.`);
568
648
  }
569
649
  const sender = ActorMessages.parseActorAddress(message.from);
570
650
  if (sender.kind !== "run" || !sender.value) {
571
- throw new Error("message to coordinator currently requires from=run:<id>.");
651
+ throw new Error(`message to ${address.kind} currently requires from=run:<id>.`);
652
+ }
653
+ const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
654
+ if (address.kind === "session") {
655
+ if (!senderStatus.ownerId) {
656
+ throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got no owner.`);
657
+ }
658
+ if (senderStatus.ownerId !== address.value) {
659
+ throw new Error(`message to session:${address.value} requires sender run owner ${address.value}; got ${senderStatus.ownerId}.`);
660
+ }
572
661
  }
573
662
  result = AsyncRuns.appendRunOutboxEvent(sender.value, {
574
663
  body: message.body,
575
664
  correlation_id: message.correlation_id,
665
+ delivery: address.kind === "session" ? "followup" : undefined,
576
666
  event: message.type,
577
667
  from: message.from,
578
668
  metadata: message.metadata,
@@ -583,7 +673,7 @@ export function createActorMessageToolDefinition<TContext = unknown>(
583
673
  });
584
674
  } else {
585
675
  throw new Error(
586
- `message currently supports run:<id>, branch:<run>/<branch>, tool:<name>, and coordinator destinations; unsupported destination: ${message.to}`,
676
+ `message currently supports run:<id>, branch:<run>/<branch>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
587
677
  );
588
678
  }
589
679
  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.10",
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
+ }
@@ -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,43 @@ 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 [{ run: run.run, reason: "running-stale", suggested_message: `message to=run:${run.run} type=control.stop body=stop` }];
139
+ }
140
+ if (["failed", "exited", "killed"].includes(run.status)) {
141
+ return [{ run: run.run, reason: `terminal-${run.status}`, suggested_inspect: `inspect target=run:${run.run} view=tail` }];
142
+ }
143
+ return [];
144
+ });
145
+ console.log(JSON.stringify({ runs, events: tailJsonl(eventFileValue, linesValue), recommendations }, null, 2));
109
146
  }
110
147
 
111
148
  function playlist(
@@ -255,6 +292,8 @@ if (!command) {
255
292
 
256
293
  if (command === "run-summary")
257
294
  runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
295
+ else if (command === "run-ops-snapshot")
296
+ 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
297
  else if (command === "playlist")
259
298
  playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
260
299
  else if (command === "changelog-section")