@llblab/pi-actors 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/AGENTS.md +5 -3
  2. package/BACKLOG.md +54 -29
  3. package/CHANGELOG.md +18 -2
  4. package/README.md +184 -300
  5. package/docs/actor-messages.md +6 -2
  6. package/docs/async-runs.md +3 -5
  7. package/docs/command-templates.md +2 -0
  8. package/docs/recipe-library.md +3 -0
  9. package/docs/task-first-recipes.md +29 -0
  10. package/docs/template-recipes.md +9 -14
  11. package/index.ts +158 -34
  12. package/lib/actor-inspector-tui.ts +374 -118
  13. package/lib/actor-rooms.ts +222 -24
  14. package/lib/async-runs.ts +59 -1
  15. package/lib/execution.ts +17 -0
  16. package/lib/file-state.ts +2 -1
  17. package/lib/observability.ts +82 -2
  18. package/lib/prompts.ts +2 -2
  19. package/lib/recipe-discovery.ts +86 -6
  20. package/lib/recipe-migration.ts +0 -2
  21. package/lib/recipe-references.ts +43 -10
  22. package/lib/temp.ts +55 -2
  23. package/lib/tools.ts +99 -11
  24. package/package.json +1 -1
  25. package/recipes/coordinator-locker.json +0 -1
  26. package/recipes/lens-swarm.json +0 -1
  27. package/recipes/music-player.json +0 -1
  28. package/recipes/pipeline-architect-coordinator.json +0 -1
  29. package/recipes/pipeline-artifact-bundle.json +0 -1
  30. package/recipes/pipeline-artifact-report.json +0 -1
  31. package/recipes/pipeline-artifact-write.json +0 -1
  32. package/recipes/pipeline-async-run-ops.json +0 -1
  33. package/recipes/pipeline-checkpoint-continuation.json +0 -1
  34. package/recipes/pipeline-development-tasking.json +0 -1
  35. package/recipes/pipeline-docs-maintenance.json +0 -1
  36. package/recipes/pipeline-media-library.json +0 -1
  37. package/recipes/pipeline-quorum-review.json +0 -1
  38. package/recipes/pipeline-release-readiness.json +0 -1
  39. package/recipes/pipeline-release-summary.json +0 -1
  40. package/recipes/pipeline-repo-health.json +0 -1
  41. package/recipes/pipeline-research-synthesis.json +0 -1
  42. package/recipes/pipeline-review-readiness.json +0 -1
  43. package/recipes/pipeline-room-swarm.json +48 -0
  44. package/recipes/subagent-artifact.json +0 -1
  45. package/recipes/subagent-checkpoint.json +0 -1
  46. package/recipes/subagent-conflict-report.json +0 -1
  47. package/recipes/subagent-contradiction-map.json +0 -1
  48. package/recipes/subagent-critic.json +0 -1
  49. package/recipes/subagent-evidence-map.json +0 -1
  50. package/recipes/subagent-followup.json +0 -1
  51. package/recipes/subagent-judge.json +0 -1
  52. package/recipes/subagent-merge.json +0 -1
  53. package/recipes/subagent-message.json +0 -1
  54. package/recipes/subagent-normalize.json +0 -1
  55. package/recipes/subagent-plan.json +0 -1
  56. package/recipes/subagent-prompt.json +0 -1
  57. package/recipes/subagent-quorum.json +0 -1
  58. package/recipes/subagent-review-coordinator.json +0 -1
  59. package/recipes/subagent-review.json +0 -1
  60. package/recipes/subagent-task-card.json +0 -1
  61. package/recipes/subagent-tools.json +0 -1
  62. package/recipes/subagent-verify.json +0 -1
  63. package/recipes/subagents-prompts.json +0 -1
  64. package/recipes/utility-actor-message.json +0 -1
  65. package/recipes/utility-artifact-manifest.json +0 -1
  66. package/recipes/utility-artifact-write.json +0 -1
  67. package/recipes/utility-changelog-head.json +0 -1
  68. package/recipes/utility-changelog-section.json +0 -1
  69. package/recipes/utility-coordinator-lock-snapshot.json +0 -1
  70. package/recipes/utility-git-log.json +0 -1
  71. package/recipes/utility-git-status.json +0 -1
  72. package/recipes/utility-jsonl-tail.json +0 -1
  73. package/recipes/utility-markdown-index.json +0 -1
  74. package/recipes/utility-package-summary.json +0 -1
  75. package/recipes/utility-playlist-build.json +0 -1
  76. package/recipes/utility-playlist-scan.json +0 -1
  77. package/recipes/utility-run-ops-snapshot.json +0 -1
  78. package/recipes/utility-run-state-files.json +0 -1
  79. package/recipes/utility-run-summary.json +0 -1
  80. package/recipes/utility-skill-summary.json +0 -1
  81. package/recipes/utility-validate-recipe.json +0 -1
  82. package/recipes/utility-validation-wrapper.json +0 -1
  83. package/scripts/room-swarm.mjs +243 -0
  84. package/skills/actors/SKILL.md +25 -12
  85. package/skills/swarm/SKILL.md +15 -1
@@ -22,7 +22,7 @@ Async-run standard owns:
22
22
  Async-run standard does not own:
23
23
 
24
24
  - Command-template syntax, placeholders, graph semantics, or branch policy.
25
- - Recipe import resolution, recipe names, or recipe storage format.
25
+ - Recipe import resolution, filename-derived recipe identity, or recipe storage format.
26
26
  - Domain semantics for subagents, swarms, release readiness, media playback, or project policy.
27
27
  - Scheduling, queue daemons, distributed workers, or workflow DSLs.
28
28
 
@@ -52,7 +52,6 @@ A recipe with `async: true` starts detached when invoked through its registered
52
52
 
53
53
  ```json
54
54
  {
55
- "name": "music-player",
56
55
  "async": true,
57
56
  "template": "play-audio {source}"
58
57
  }
@@ -72,7 +71,7 @@ A caller can also start any recipe or inline template explicitly through `spawn`
72
71
 
73
72
  `spawn` always starts a detached run actor. Registered recipe tools follow the recipe's `async` flag.
74
73
 
75
- Use `run_id` on async recipe tools or `as: "run:<id>"` on `spawn` when the caller wants a stable id for later inspection or control. Recipe `name` identifies the saved definition; the run id identifies one execution instance of that recipe. Async runs inject lifecycle and communication values into template values so scripts can write run-local status files, control endpoints, or room-aware coordination messages:
74
+ Use `run_id` on async recipe tools or `as: "run:<id>"` on `spawn` when the caller wants a stable id for later inspection or control. The recipe filename identifies the saved definition; the run id identifies one execution instance of that recipe. Async runs inject lifecycle and communication values into template values so scripts can write run-local status files, control endpoints, or room-aware coordination messages:
76
75
 
77
76
  - `{run_id}`: stable run id.
78
77
  - `{state_dir}`: run-local state directory.
@@ -182,7 +181,7 @@ Some recipes expose a run-local control channel. When present, a caller can send
182
181
  }
183
182
  ```
184
183
 
185
- For `run:<id>`, `message` adapts the body to the recipe's run-local control channel. For `branch:<run>/<branch>`, it sends the full envelope through the parent run mailbox so the run can dispatch branch-local control. For `tool:<name>`, object bodies become the target tool parameters and primitive bodies are passed as `{ "input": body }`. The generic runtime records control messages but does not interpret arbitrary run mailbox content. For example, a music player may accept `play`, `pause`, `next`, and `stop`, while a collaborative agent recipe may accept `continue`, `revise:<note>`, `approve`, or `abort`. Recipes may treat terminal control messages such as `stop` as synchronously handled so the later process exit does not generate a duplicate async follow-up.
184
+ For `run:<id>`, `message` adapts the body to the recipe's run-local control channel. For `branch:<run>/<branch>`, it sends the full envelope through the parent run mailbox and records a queued branch-local inbox entry at `branches/<branch>/inbox.jsonl` so the run can dispatch branch-local control. Current consumers are recipe-specific worker protocols that read the parent run mailbox or branch inbox; independent one-shot prompt processes do not automatically consume branch inbox entries. For `tool:<name>`, object bodies become the target tool parameters and primitive bodies are passed as `{ "input": body }`. The generic runtime records control messages but does not interpret arbitrary run mailbox content. For example, a music player may accept `play`, `pause`, `next`, and `stop`, while a collaborative agent recipe may accept `continue`, `revise:<note>`, `approve`, or `abort`. Recipes may treat terminal control messages such as `stop` as synchronously handled so the later process exit does not generate a duplicate async follow-up.
186
185
 
187
186
  The standard run-local transport is Unix-oriented. Use WSL/Linux/macOS for packaged message-controlled recipes, or let a Windows-specific recipe expose its own transport such as a Windows named pipe or localhost socket.
188
187
 
@@ -294,7 +293,6 @@ Example recipe:
294
293
 
295
294
  ```json
296
295
  {
297
- "name": "collab-{run}",
298
296
  "async": true,
299
297
  "parallel": true,
300
298
  "timeout": 1800000,
@@ -159,6 +159,8 @@ template="echo 'literal words' {text}"
159
159
 
160
160
  ## Composition
161
161
 
162
+ Shell syntax warning: command-template placeholder parsing still sees braces inside shell snippets. Avoid inline Bash parameter expansion such as `${file%.md}` or `${name}` in recipe/template strings because `{file...}` can be parsed as a pi-actors placeholder. For non-trivial shell loops or parameter expansion, put the shell logic in a small trusted script and call that script from the template; keep the command template as the launch boundary.
163
+
162
164
  `template: [...]` means sequential composition by default; each leaf is a command template executed with one shared runtime value map:
163
165
 
164
166
  ```json
@@ -48,6 +48,8 @@ Core subagent recipes:
48
48
 
49
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. Packaged recipes intentionally do not ship concrete model-version defaults: callers must pass current model policy at launch, which keeps reusable recipe components from aging around old provider aliases. 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
+ For build-oriented swarms, prefer a consensus-first shape over parallel writers: proposer roles coordinate in a room with message/inspect tools, a named implementer owns the first artifact write, a QA reviewer inspects the result, and a finalizer applies review-grounded fixes before `run.done`. This pattern keeps creative/lens diversity while preserving one coherent artifact and gives recipes concrete artifact assertions instead of treating room discussion as success.
52
+
51
53
  Register one atom:
52
54
 
53
55
  ```text
@@ -82,6 +84,7 @@ Pipeline recipes demonstrate second-order composition:
82
84
  - `recipes/pipeline-development-tasking.json`: Plan → task card → critique → integrator handoff.
83
85
  - `recipes/pipeline-docs-maintenance.json`: Docs index → documentation review → maintenance plan → artifact report.
84
86
  - `recipes/pipeline-media-library.json`: Playlist build → media-library artifact report.
87
+ - `recipes/pipeline-room-swarm.json`: Room participants join `room:<run>`, coordinate over repeated room-visible rounds, leave cleanly, and synthesize the room transcript into a caller-provided artifact path. Keep model/thinking/mission policy caller-owned. Custom roles can be supplied with `roles_path` as a JSON array of `{ "name", "persona" }` objects; `name` stays ASCII-safe for `branch:<run>/<name>` addresses and debugger output remains plain and name-driven. The packaged swarm uses contacts for peer awareness but does not rely on direct branch delivery unless a caller-specific worker protocol consumes branch envelopes. Set `locker=true` to compose a local `coordinator-locker` cell under `{state_dir}/locker` for artifact ownership, resource lease locks, and a decision journal without merging locker policy into the room-participant script.
85
88
  - `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.
86
89
  - `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`.
87
90
  - `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.
@@ -137,6 +137,35 @@ Existing seeds:
137
137
  - `subagent-contradiction-map`
138
138
  - `subagent-verify`
139
139
 
140
+ ### Consensus-First Build Cell
141
+
142
+ Purpose: turn several expert proposals into one coherent artifact without parallel writers fragmenting the result.
143
+
144
+ Pipeline:
145
+
146
+ ```text
147
+ mission → lens proposers in room → consensus transcript → named implementer writes artifact → QA reviewer checks artifact + transcript → finalizer applies fixes → artifact assertions
148
+ ```
149
+
150
+ Use this for creative demos, single-file artifacts, specs, docs, prompt packs, and product/UX deliverables where broad input matters but one owner should shape the final file. Proposers should have message/inspect tools only. The implementer should be the first role with write tools. QA should inspect/read but not mutate. The finalizer may write only after reading QA evidence.
151
+
152
+ Required gates:
153
+
154
+ - Artifact path, report path, and minimum acceptance checks are explicit inputs.
155
+ - The workflow fails if the requested artifact is missing, too small, or not self-contained enough for the task.
156
+ - `run.done` is emitted only after QA/finalizer passes, not merely after room discussion.
157
+
158
+ Existing seeds:
159
+
160
+ - `pipeline-room-swarm` for room-visible discussion and rosters.
161
+ - `subagent-message` for explicit room handoffs.
162
+ - `subagent-review` / `subagent-verify` for QA roles.
163
+ - `pipeline-artifact-write` or a small helper script for deterministic artifact assertion.
164
+
165
+ Next recipe direction:
166
+
167
+ - Add a generic packaged consensus-build pipeline once the interface stabilizes around proposer roles, implementer prompt, QA prompt, artifact assertions, and public model/tool knobs.
168
+
140
169
  ### Implementation Tasking Cell
141
170
 
142
171
  Purpose: prepare bounded work for one or more implementation agents.
@@ -19,7 +19,7 @@ async: true = run through detached lifecycle
19
19
 
20
20
  A recipe wraps one command-template tree. The wrapped `template` keeps the normal command-template semantics: argv splitting, placeholders, defaults, typed args, sequence, `parallel: true`, `when`, delay, retry, failure propagation, recover cleanup, and output selection.
21
21
 
22
- Layer boundary: `imports`, `{ "name": "alias" }` imported-recipe nodes, `{alias.defaults.key}` references, fallback expressions, and recipe-local ternaries are recipe-loading features. They resolve before the command-template graph runs and do not extend the portable Command Template Standard. Typed imports are recipe definitions: they expose the imported recipe's command-template-shaped metadata (`template`, `args`, `defaults`, flags, and `values`), while async-run launch fields such as `async` and `state_dir` remain lifecycle configuration for starting a run, not part of the imported execution graph.
22
+ Layer boundary: `imports`, `{ "name": "alias" }` imported-recipe nodes, `{alias.defaults.key}` references, fallback expressions, and recipe-local ternaries are recipe-loading features. They resolve before the command-template graph runs and do not extend the portable Command Template Standard. Typed imports are recipe definitions: they expose the imported recipe's command-template-shaped metadata (`template`, `args`, `defaults`, flags, and `values`), while async-run launch fields such as `async`, `state_dir`, and `retire_when` remain lifecycle configuration for starting a run, not part of the imported execution graph.
23
23
 
24
24
  Packaged recipes are the pi-actors recipe standard library: declarative actor config components that can be imported, launched, inspected, overridden, or composed by user recipes. Treat them as stable building blocks rather than user-local policy.
25
25
 
@@ -29,7 +29,7 @@ Template-recipe standard owns:
29
29
 
30
30
  - Saved JSON definitions around one command-template graph.
31
31
  - File-backed and co-located recipe shapes.
32
- - Recipe identity through `name` or filename.
32
+ - Recipe identity through file-backed filename or co-located tool id.
33
33
  - Recipe defaults, values, imports, import references, and import-node expansion.
34
34
  - Ordered named artifact declarations through `artifacts`.
35
35
  - Foreground-vs-detached selection through `async: true` when invoked by a recipe-aware host.
@@ -52,7 +52,6 @@ Synchronous recipe:
52
52
 
53
53
  ```json
54
54
  {
55
- "name": "check-docs",
56
55
  "template": "npm run check:docs"
57
56
  }
58
57
  ```
@@ -61,13 +60,12 @@ Async recipe:
61
60
 
62
61
  ```json
63
62
  {
64
- "name": "review-docs",
65
63
  "async": true,
66
64
  "template": "review docs/spec.md"
67
65
  }
68
66
  ```
69
67
 
70
- `name` names the saved definition when an explicit name is needed. File-backed recipes usually omit it because the filename is the canonical recipe id. `template` is the command-template tree. `async: true` selects detached run mode when the recipe is invoked through a registered tool.
68
+ A file-backed recipe's id comes from its filename, not a JSON `name` field. Legacy files may still contain `name`, but loaders ignore it for identity. `template` is the command-template tree. `async: true` selects detached run mode when the recipe is invoked through a registered tool.
71
69
 
72
70
  ## Discovery Priority
73
71
 
@@ -107,7 +105,6 @@ Use recipe-level `artifacts` to declare stable artifact names and paths for the
107
105
 
108
106
  ```json
109
107
  {
110
- "name": "report-task",
111
108
  "args": ["report_path:path"],
112
109
  "defaults": { "report_path": "artifacts/report.md" },
113
110
  "artifacts": {
@@ -156,11 +153,10 @@ Recipes do not declare a second event-delivery policy. A running actor emits add
156
153
 
157
154
  ## Command-Template Flags At Recipe Top Level
158
155
 
159
- Top-level command-template flags may sit beside `name` and `async`:
156
+ Top-level command-template flags may sit beside recipe metadata such as `async`:
160
157
 
161
158
  ```json
162
159
  {
163
- "name": "review-docs",
164
160
  "async": true,
165
161
  "parallel": true,
166
162
  "timeout": 300000,
@@ -199,7 +195,7 @@ Bare recipe names resolve under that directory, so `file: "review-docs"` loads:
199
195
  ~/.pi/agent/recipes/review-docs.json
200
196
  ```
201
197
 
202
- Call-time params override file params. `values` are merged with file values; call-time values win. If a run id is omitted for an explicit async start, the explicit recipe `name` or file basename becomes the default run id.
198
+ Call-time params override file params. `values` are merged with file values; call-time values win. If a run id is omitted for an explicit async start, the file basename becomes the default run id.
203
199
 
204
200
  ## Registered Recipe Tools
205
201
 
@@ -226,7 +222,7 @@ Example: a recipe may expose a private `repo` default for an example script, whi
226
222
 
227
223
  ## Recipe Imports
228
224
 
229
- File-backed recipes may import other file-backed recipes at the recipe layer. Imports are resolved before the command-template graph is executed, so command-template core stays registry-free and synchronous.
225
+ File-backed recipes may import other file-backed recipes at the recipe layer. Imports are resolved before the command-template graph is executed, so command-template core stays registry-free and synchronous. Recipe loading is intentionally bounded: a single recipe file larger than 1 MiB is rejected before JSON parsing, and import chains deeper than 32 are rejected before further resolution. Split very large prompts/data into explicit files or artifacts and keep recipe graphs shallow enough for operator review.
230
226
 
231
227
  ```json
232
228
  {
@@ -244,17 +240,16 @@ File-backed recipes may import other file-backed recipes at the recipe layer. Im
244
240
 
245
241
  An import binding may be either a string recipe path/name or an object with:
246
242
 
247
- - `from`: recipe path or bare name. Import paths support static load-time placeholders: `{repo}` expands to the directory above the active recipe root, and `{agent}` expands to the pi agent directory. For a packaged recipe in `<repo>/recipes/name.json`, `{repo}/recipes/other.json` resolves to a sibling packaged recipe. For a user recipe in `~/.pi/agent/recipes/name.json`, `{repo}` and `{agent}` both resolve to `~/.pi/agent`.
243
+ - `from`: recipe path or bare recipe name. Import paths support static load-time placeholders: `{repo}` expands to the directory above the active recipe root, and `{agent}` expands to the pi agent directory. For a packaged recipe in `<repo>/recipes/name.json`, `{repo}/recipes/other.json` resolves to a sibling packaged recipe. For a user recipe in `~/.pi/agent/recipes/name.json`, `{repo}` and `{agent}` both resolve to `~/.pi/agent`. Bare names such as `utility-package-summary` resolve by recipe priority: first `~/.pi/agent/recipes`, then the importing recipe's directory, then the packaged standard library. This makes packaged recipes easy to reuse while preserving user/ad hoc overrides by filename identity.
248
244
  - `defaults`: extra default values exposed through the import.
249
245
  - `values`: explicit values for embedding that imported recipe.
250
246
 
251
247
  A template node of `{ "name": "alias" }` is replaced with the imported recipe's command-template graph. Imported recipe defaults are merged with import `defaults`, import `values`, node `defaults`, and node `values`; later layers win. This lets a parent recipe embed a reusable recipe in a sequence or `parallel: true` branch without inventing a workflow language.
252
248
 
253
- Async composition stays explicit: importing a recipe reuses its command-template-shaped definition. It does not start a nested async run. Put `async: true` on the parent recipe when the combined imported graph should run detached as one run with one state dir. For agent-callable fanout, prefer public inputs such as `prompts:array` plus `repeat: "{prompts.length}"`, then select each branch value with `{prompts[index]}` instead of baking concrete prompts or file names into the reusable recipe.
249
+ Async composition stays explicit: importing a recipe reuses its command-template-shaped definition. It does not start a nested async run. Put `async: true` on the parent recipe when the combined imported graph should run detached as one run with one state dir. Ephemeral coordinator recipes may declare `retire_when: "children_terminal"` as an opt-in lifecycle hint for future graceful retirement handling; persistent services and implementer loops should omit it. For agent-callable fanout, prefer public inputs such as `prompts:array` plus `repeat: "{prompts.length}"`, then select each branch value with `{prompts[index]}` instead of baking concrete prompts or file names into the reusable recipe.
254
250
 
255
251
  ```json
256
252
  {
257
- "name": "parallel-review",
258
253
  "async": true,
259
254
  "imports": {
260
255
  "review": "review-one.json"
@@ -301,6 +296,6 @@ Nested object keys are dot-separated. Import references are resolved before norm
301
296
 
302
297
  ## Recipe Shape
303
298
 
304
- Use `name` for an explicit recipe id, rely on the filename for file-backed recipe ids, and use `async: true` for detached runs. Use `parallel: true` for fanout, `when` for node guards, and semantic public args such as `tools`, `all`, or `timeout_ms` instead of leaking CLI fragments or reusing node-control names. Local files belong under `~/.pi/agent/recipes/*.json` before relying on recipe launchers.
299
+ Use the filename for file-backed recipe ids, and use `async: true` for detached runs. Use `parallel: true` for fanout, `when` for node guards, and semantic public args such as `tools`, `all`, or `timeout_ms` instead of leaking CLI fragments or reusing node-control names. Local files belong under `~/.pi/agent/recipes/*.json` before relying on recipe launchers.
305
300
 
306
301
  If a proposed recipe needs a scheduler, queue daemon, `goto`, or custom workflow syntax, stop. Keep the recipe as saved command-template JSON and put policy in the registered tool, script, or caller.
package/index.ts CHANGED
@@ -49,7 +49,14 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
49
49
  const observedRunEventLines = new Map<string, number>();
50
50
  let runStatusFrame = 0;
51
51
  let communicationWidgetVisible = false;
52
- let inspectorVerbosity: ActorInspectorTui.ActorInspectorVerbosity = "verbose";
52
+ let actorInspectorRows = 12;
53
+ let actorInspectorChannels:
54
+ | ActorInspectorTui.ActorInspectorPreview["channel"][]
55
+ | undefined;
56
+ let actorInspectorMention: string | undefined;
57
+ const actorInspectorRoomLimitPerRun = 6;
58
+ let selectedInspectorSequence: number | undefined;
59
+ let recipeWatcherFailureNotified = false;
53
60
  const getRunOwnerId = (ctx: ExtensionContext): string =>
54
61
  ctx.sessionManager.getSessionId();
55
62
  const updateRunUi = (ctx: ExtensionContext, notify = false): void => {
@@ -63,31 +70,59 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
63
70
  ctx.ui.setWidget(
64
71
  "zz-pi-actors-comms",
65
72
  communicationWidgetVisible
66
- ? () => ({
67
- invalidate() {},
68
- render(width: number) {
69
- return (
70
- ActorInspectorTui.renderInspectorWidget(
71
- ActorInspectorTui.readActorInspectorPreviews(
72
- RUN_STATE_ROOT,
73
- 14,
74
- { ownerId, currentRunOnly: true },
75
- ),
76
- width,
73
+ ? () => {
74
+ const style = {
75
+ actor: (text: string) => ctx.ui.theme.fg("accent", text),
76
+ muted: (text: string) => ctx.ui.theme.fg("dim", text),
77
+ preview: (text: string) => ctx.ui.theme.fg("text", text),
78
+ stripe: (text: string) => text,
79
+ stripeAlt: (text: string) =>
80
+ ctx.ui.theme.bg("customMessageBg", text),
81
+ target: (text: string) => ctx.ui.theme.fg("success", text),
82
+ type: (text: string) => ctx.ui.theme.fg("warning", text),
83
+ };
84
+ return {
85
+ invalidate() {},
86
+ render(width: number) {
87
+ const previews = ActorInspectorTui.readActorInspectorPreviews(
88
+ RUN_STATE_ROOT,
89
+ actorInspectorRows,
77
90
  {
78
- actor: (text) => ctx.ui.theme.fg("accent", text),
79
- muted: (text) => ctx.ui.theme.fg("dim", text),
80
- preview: (text) => ctx.ui.theme.fg("text", text),
81
- stripe: (text) => text,
82
- stripeAlt: (text) => ctx.ui.theme.bg("customMessageBg", text),
83
- target: (text) => ctx.ui.theme.fg("success", text),
84
- type: (text) => ctx.ui.theme.fg("warning", text),
91
+ channels: actorInspectorChannels,
92
+ currentRunOnly: true,
93
+ mention: actorInspectorMention,
94
+ ownerId,
95
+ roomLimitPerRun: actorInspectorRoomLimitPerRun,
85
96
  },
86
- { verbosity: inspectorVerbosity },
87
- ) ?? []
88
- );
89
- },
90
- })
97
+ );
98
+ const rows =
99
+ (selectedInspectorSequence !== undefined
100
+ ? ActorInspectorTui.renderInspectorItemView(
101
+ previews,
102
+ width,
103
+ style,
104
+ { sequence: selectedInspectorSequence },
105
+ )
106
+ : ActorInspectorTui.renderInspectorWidget(
107
+ previews,
108
+ width,
109
+ style,
110
+ )) ?? [];
111
+ const run = previews[0]?.run;
112
+ const roster = run
113
+ ? ActorInspectorTui.renderInspectorRosterPanel(
114
+ ActorInspectorTui.readActorInspectorRoster(
115
+ RUN_STATE_ROOT,
116
+ run,
117
+ ),
118
+ width,
119
+ style,
120
+ )
121
+ : undefined;
122
+ return roster ? [...roster, ...rows] : rows;
123
+ },
124
+ };
125
+ }
91
126
  : undefined,
92
127
  { placement: "belowEditor" },
93
128
  );
@@ -117,6 +152,12 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
117
152
  { deliverAs: "followUp", triggerTurn: true },
118
153
  );
119
154
  }
155
+ Observability.pruneRunObservationState(
156
+ observedRuns,
157
+ observedRunEventLines,
158
+ summary,
159
+ transitions.map((transition) => transition.run),
160
+ );
120
161
  for (const event of outboxEvents) {
121
162
  if (!Observability.shouldNotifyRunOutboxEvent(event)) continue;
122
163
  const text = Observability.formatRunOutboxMessage(event);
@@ -190,7 +231,16 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
190
231
  if (recipeReloadTimeout) clearTimeout(recipeReloadTimeout);
191
232
  recipeReloadTimeout = undefined;
192
233
  };
234
+ const notifyRecipeWatcherFailure = (ctx: ExtensionContext): void => {
235
+ if (recipeWatcherFailureNotified) return;
236
+ recipeWatcherFailureNotified = true;
237
+ ctx.ui.notify(
238
+ "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
239
+ "warning",
240
+ );
241
+ };
193
242
  const scheduleRecipeReload = (ctx: ExtensionContext): void => {
243
+ recipeWatcherFailureNotified = false;
194
244
  if (recipeReloadTimeout) clearTimeout(recipeReloadTimeout);
195
245
  recipeReloadTimeout = setTimeout(() => {
196
246
  runtime.loadTools(ctx);
@@ -206,9 +256,10 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
206
256
  recipeRootWatcher.on("error", () => {
207
257
  recipeRootWatcher?.close();
208
258
  recipeRootWatcher = undefined;
259
+ notifyRecipeWatcherFailure(ctx);
209
260
  });
210
261
  } catch {
211
- // Watching is best-effort; restarting the session reloads recipe tools.
262
+ notifyRecipeWatcherFailure(ctx);
212
263
  }
213
264
  };
214
265
  const actorToolDefinitions = new Map<string, any>();
@@ -243,9 +294,38 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
243
294
  closeRecipeWatcher();
244
295
  });
245
296
  pi.registerCommand("actors-inspector-toggle", {
246
- description: "Toggle actor inspector widget",
247
- handler: async (_args, ctx) => {
248
- communicationWidgetVisible = !communicationWidgetVisible;
297
+ description: "Toggle actor inspector widget; optional row count",
298
+ handler: async (args, ctx) => {
299
+ const raw = Array.isArray(args) ? args[0] : String(args ?? "");
300
+ if (String(raw).trim()) {
301
+ const rows = Number.parseInt(String(raw), 10);
302
+ if (!Number.isFinite(rows) || rows <= 0) {
303
+ ctx.ui.notify(
304
+ "Usage: /actors-inspector-toggle [rows] where rows > 0",
305
+ "warning",
306
+ );
307
+ return;
308
+ }
309
+ actorInspectorRows = rows;
310
+ selectedInspectorSequence = undefined;
311
+ communicationWidgetVisible = true;
312
+ updateRunUi(ctx);
313
+ ctx.ui.notify(`Actor inspector rows ${rows}`, "info");
314
+ return;
315
+ }
316
+ if (selectedInspectorSequence !== undefined) {
317
+ selectedInspectorSequence = undefined;
318
+ communicationWidgetVisible = true;
319
+ updateRunUi(ctx);
320
+ ctx.ui.notify("Actor inspector table", "info");
321
+ return;
322
+ }
323
+ if (communicationWidgetVisible) {
324
+ communicationWidgetVisible = false;
325
+ } else {
326
+ actorInspectorRows = 12;
327
+ communicationWidgetVisible = true;
328
+ }
249
329
  updateRunUi(ctx);
250
330
  ctx.ui.notify(
251
331
  `Actor inspector ${communicationWidgetVisible ? "shown" : "hidden"}`,
@@ -253,13 +333,57 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
253
333
  );
254
334
  },
255
335
  });
256
- pi.registerCommand("actors-inspector-verbosity", {
257
- description: "Toggle actor inspector verbosity",
258
- handler: async (_args, ctx) => {
259
- inspectorVerbosity =
260
- inspectorVerbosity === "verbose" ? "compact" : "verbose";
336
+ pi.registerCommand("actors-inspector-filter", {
337
+ description:
338
+ "Filter actor inspector rows: all, room, direct, broadcast, mention <text>",
339
+ handler: async (args, ctx) => {
340
+ const parts = Array.isArray(args)
341
+ ? args.map(String)
342
+ : String(args ?? "").split(/\s+/);
343
+ const mode = (parts[0] ?? "").trim().toLowerCase();
344
+ if (!mode || mode === "all" || mode === "clear") {
345
+ actorInspectorChannels = undefined;
346
+ actorInspectorMention = undefined;
347
+ } else if (mode === "room" || mode === "direct" || mode === "broadcast") {
348
+ actorInspectorChannels = [mode];
349
+ actorInspectorMention = undefined;
350
+ } else if (mode === "mention") {
351
+ const mention = parts.slice(1).join(" ").trim();
352
+ if (!mention) {
353
+ ctx.ui.notify(
354
+ "Usage: /actors-inspector-filter mention <text>",
355
+ "warning",
356
+ );
357
+ return;
358
+ }
359
+ actorInspectorChannels = undefined;
360
+ actorInspectorMention = mention;
361
+ } else {
362
+ ctx.ui.notify(
363
+ "Usage: /actors-inspector-filter all|room|direct|broadcast|mention <text>",
364
+ "warning",
365
+ );
366
+ return;
367
+ }
368
+ selectedInspectorSequence = undefined;
369
+ communicationWidgetVisible = true;
370
+ updateRunUi(ctx);
371
+ ctx.ui.notify(`Actor inspector filter ${mode || "all"}`, "info");
372
+ },
373
+ });
374
+ pi.registerCommand("actors-inspect", {
375
+ description: "Inspect actor message by visible number",
376
+ handler: async (args, ctx) => {
377
+ const raw = Array.isArray(args) ? args[0] : String(args ?? "");
378
+ const sequence = Number.parseInt(String(raw), 10);
379
+ if (!Number.isFinite(sequence) || sequence <= 0) {
380
+ ctx.ui.notify("Usage: /actors-inspect <number>", "warning");
381
+ return;
382
+ }
383
+ selectedInspectorSequence = sequence;
384
+ communicationWidgetVisible = true;
261
385
  updateRunUi(ctx);
262
- ctx.ui.notify(`Actor inspector ${inspectorVerbosity} mode`, "info");
386
+ ctx.ui.notify(`Actor inspect item ${sequence}`, "info");
263
387
  },
264
388
  });
265
389
  pi.on("before_agent_start", async (event) => ({