@llblab/pi-actors 0.16.4 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -8
- package/BACKLOG.md +56 -21
- package/CHANGELOG.md +19 -8
- package/README.md +170 -274
- package/banner.jpg +0 -0
- package/docs/actor-messages.md +66 -3
- package/docs/async-runs.md +25 -3
- package/docs/recipe-library.md +4 -3
- package/docs/template-recipes.md +3 -4
- package/docs/tool-registry.md +7 -12
- package/index.ts +103 -3
- package/lib/actor-inspector-tui.ts +532 -0
- package/lib/actor-messages.ts +18 -0
- package/lib/actor-rooms.ts +373 -0
- package/lib/async-runs.ts +17 -1
- package/lib/config.ts +1 -1
- package/lib/paths.ts +1 -1
- package/lib/prompts.ts +3 -2
- package/lib/recipe-discovery.ts +83 -1
- package/lib/recipe-migration.ts +2 -2
- package/lib/recipe-references.ts +2 -0
- package/lib/tools.ts +292 -9
- package/package.json +1 -1
- package/recipes/lens-swarm.json +0 -1
- package/recipes/pipeline-room-swarm.json +49 -0
- package/scripts/room-swarm.mjs +244 -0
- package/skills/actors/SKILL.md +52 -9
- package/skills/swarm/SKILL.md +1 -1
package/docs/actor-messages.md
CHANGED
|
@@ -32,7 +32,19 @@ session:<id>
|
|
|
32
32
|
tool:<name>
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Cross-branch communication adds one organic endpoint kind:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
room:<run>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A room is the task's single shared discussion channel: an addressable mailbox with an append-only message log and a compact member roster stored under the owning run state. It is not a broker or coordinator: it accepts normal actor-message envelopes, records shared timeline entries, tracks join/leave presence, and lets actors discover peers for direct messages. `room:<run>` is the public address; named subrooms are intentionally not exposed in 0.17.
|
|
42
|
+
|
|
43
|
+
An alternate implementation shape is a dedicated non-LLM communication actor: a small script-backed service recipe, possibly singleton-scoped, that owns room timelines, rosters, subscriptions, and fanout. This is attractive when communication needs outgrow simple file-backed room state, but it should remain an implementation adapter behind the same `room:<run>` address and message envelope. The public model should not fork into a separate chat API.
|
|
44
|
+
|
|
45
|
+
That actor-backed shape can also reduce direct file storage. Instead of every protocol feature owning JSON files as primary state, a helper actor can keep live room/roster structures in memory or another local structure and write files only as snapshots, audit logs, artifacts, or recovery checkpoints. The decision boundary is practical: keep files when durability and inspectability are the main value; prefer actor-owned structures when live coordination, subscriptions, fanout, unread state, or mutation consistency becomes the main value.
|
|
46
|
+
|
|
47
|
+
Package-specific endpoints may still exist, but the envelope stays the same.
|
|
36
48
|
|
|
37
49
|
## Message Envelope
|
|
38
50
|
|
|
@@ -73,6 +85,8 @@ run -> coordinator
|
|
|
73
85
|
run -> run
|
|
74
86
|
parent -> branch
|
|
75
87
|
branch -> parent
|
|
88
|
+
branch -> room
|
|
89
|
+
room -> branch notification
|
|
76
90
|
coordinator -> tool
|
|
77
91
|
```
|
|
78
92
|
|
|
@@ -80,11 +94,59 @@ Transports differ, but the public contract does not:
|
|
|
80
94
|
|
|
81
95
|
- `to: run:<id>` routes through the run-local control channel selected by that recipe or runtime adapter.
|
|
82
96
|
- `to: coordinator` routes to the runtime attention path 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` messages and explicit coordinator/session-bound messages include the actor envelope fields alongside runtime metadata.
|
|
83
|
-
- `to: branch:<run>/<branch>` routes through the parent run mailbox with the full envelope preserved so the run can dispatch branch-local control.
|
|
97
|
+
- `to: branch:<run>/<branch>` routes through the parent run mailbox with the full envelope preserved so the run or recipe-specific worker protocol can dispatch branch-local control. It is not a broadcast room and it does not make an arbitrary prompt process consume the message automatically.
|
|
98
|
+
- `to: room:<run>` appends the full envelope to the room timeline and updates room state for room-control types such as `actor.join` and `actor.leave`.
|
|
84
99
|
- `to: tool:<name>` invokes an executable pi tool by name. Object bodies become tool parameters; primitive bodies are passed as `{ "input": body }`.
|
|
85
100
|
|
|
86
101
|
Transport is not public API unless a recipe explicitly documents a custom endpoint.
|
|
87
102
|
|
|
103
|
+
## Rooms and Rosters
|
|
104
|
+
|
|
105
|
+
The task room is the discovery and shared-context layer for actors whose spawn-tree positions do not give them each other's addresses. The spawn tree remains the lifecycle/provenance structure; the task room describes the group communication graph. Direct messages and room messages can share the same semantic `type` such as `chat.message`; the route (`to: branch:*` versus `to: room:*`) determines whether delivery is private or group-wide.
|
|
106
|
+
|
|
107
|
+
Use direct branch messages only when the receiving branch is backed by a worker or recipe that reads the parent run mailbox and dispatches branch-targeted envelopes. Room roster contacts are discovery hints, not a guarantee that an independent prompt process is subscribed to its branch address. For ad hoc or transcript-driven swarms, prefer room-visible replies and mentions so every participant can inspect the shared timeline.
|
|
108
|
+
|
|
109
|
+
A minimal join message:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"to": "room:review",
|
|
114
|
+
"from": "branch:review/security",
|
|
115
|
+
"type": "actor.join",
|
|
116
|
+
"summary": "Security reviewer joined",
|
|
117
|
+
"body": {
|
|
118
|
+
"role": "reviewer",
|
|
119
|
+
"caps": ["security-review", "risk-analysis"],
|
|
120
|
+
"claim": "Review auth boundary risks"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A leave message removes that actor from the roster while preserving the timeline entry:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"to": "room:review",
|
|
130
|
+
"from": "branch:review/security",
|
|
131
|
+
"type": "actor.leave"
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Room messages require `from` so roster presence and provenance stay explicit. The sender must belong to the room's run (`run:<run>` or `branch:<run>/<branch>`), which prevents accidental cross-run roster pollution. Other room posts also refresh sender presence, defaulting the role hint to `actor` when no richer role is known.
|
|
136
|
+
|
|
137
|
+
Roster entries should keep identity axes separate:
|
|
138
|
+
|
|
139
|
+
- `address`: Stable route for direct messages.
|
|
140
|
+
- `parent`: Spawn-tree parent for provenance and ownership checks.
|
|
141
|
+
- `role`: Current task function; dynamic and prompt-dependent.
|
|
142
|
+
- `caps`: Capabilities the actor can offer.
|
|
143
|
+
- `claim`: Current work claim or focus.
|
|
144
|
+
- `status` / `last_seen`: Presence and staleness hints.
|
|
145
|
+
|
|
146
|
+
Compact roster inspection includes these hints when present so agents can discover direct-message targets without verbose JSON.
|
|
147
|
+
|
|
148
|
+
Actors should receive a compact visible communication snapshot rather than a full global tree: self, parent/root, joined rooms, relevant sibling/member addresses, and role/capability hints. Current run actors get `communication.json` in their state dir, and async templates receive `{communication_file}`, `{actor_address}`, and `{default_room}` values. The snapshot includes `self`, `root`, optional `parent`, the default room, current default-room members, direct-message `contacts` derived from the room roster, and `updated_at`. Branch-local snapshots are refreshed when a branch joins or posts in the default room, so actors can discover peers without reading full timelines. Full timelines and rosters remain intentional inspection surfaces. For TUI and compact operator display, `view=previews` returns bounded message preview records with `timestamp`, `from`, `to`, `type`, optional `summary`, and optional `body_preview`.
|
|
149
|
+
|
|
88
150
|
## Mailbox Declaration
|
|
89
151
|
|
|
90
152
|
Recipes can declare their conversational surface:
|
|
@@ -131,7 +193,7 @@ Recipes can declare their conversational surface:
|
|
|
131
193
|
}
|
|
132
194
|
```
|
|
133
195
|
|
|
134
|
-
The implementation supports `status`, `tail`, `messages`, `artifacts`, `files`, and `
|
|
196
|
+
The implementation supports `status`, `tail`, `messages`, `artifacts`, `files`, `mailbox`, and `communication` for `run:<id>` actors, `status`, `messages`, `previews`, `roster`, and `contacts` for `room:<run>` actors, `status`/`runs` for `coordinator`, `session:<id>`, and `session:all` actors with optional status filtering, and `status`/`schema` for registered `tool:<name>` actors. Room `status` returns compact message/roster counts plus `last_message_at`, `last_message_from`, `last_message_type`, and `last_message_summary` when available. Use `messages` for actor-envelope inspection. `inspect target=coordinator` requires a current coordinator session; use `session:<id>` or `session:all` when the session is intentionally explicit. Direct `run:<id>` and `room:<run>` inspection respects coordinator-session ownership when the current session is known. `inspect` is for decision points and diagnosis only; examples must not teach sleep-then-inspect polling.
|
|
135
197
|
|
|
136
198
|
## Runtime Direction
|
|
137
199
|
|
|
@@ -152,4 +214,5 @@ intentional observe -> inspect
|
|
|
152
214
|
- No public transport-path vocabulary in recipe args.
|
|
153
215
|
- No polling-first examples.
|
|
154
216
|
- No separate upward and downward message schemas.
|
|
217
|
+
- No heavyweight chat/broker subsystem when an addressable room mailbox is enough.
|
|
155
218
|
- No broad facade that hides artifacts, logs, or ownership checks.
|
package/docs/async-runs.md
CHANGED
|
@@ -72,13 +72,20 @@ A caller can also start any recipe or inline template explicitly through `spawn`
|
|
|
72
72
|
|
|
73
73
|
`spawn` always starts a detached run actor. Registered recipe tools follow the recipe's `async` flag.
|
|
74
74
|
|
|
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
|
|
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:
|
|
76
|
+
|
|
77
|
+
- `{run_id}`: stable run id.
|
|
78
|
+
- `{state_dir}`: run-local state directory.
|
|
79
|
+
- `{actor_address}`: run actor address, e.g. `run:review`.
|
|
80
|
+
- `{default_room}`: default room address, e.g. `room:review`.
|
|
81
|
+
- `{communication_file}`: compact communication snapshot path.
|
|
76
82
|
|
|
77
83
|
## State Files
|
|
78
84
|
|
|
79
85
|
Use ordinary files under the extension temp directory so status tools stay simple and inspectable:
|
|
80
86
|
|
|
81
87
|
- `run.json`: pid, optional source metadata (`tool`, `recipe`, `recipe_file`), command-template config, cwd, coordinator owner id, values, named `artifacts`, mailbox metadata, created time, and state dir.
|
|
88
|
+
- `communication.json`: compact actor communication snapshot with self/root/parent, default-room, member, and contact hints for room-aware scripts and agents.
|
|
82
89
|
- `progress.json`: phase, active command count, completed count, failures, and updated time.
|
|
83
90
|
- `events.jsonl`: append-only implementation lifecycle log.
|
|
84
91
|
- `outbox.jsonl`: implementation storage for actor-message envelopes used by `inspect view=messages`, coordinator notifications, or follow-up context. Coordinator follow-ups preserve bounded `body` previews plus message metadata for decision points.
|
|
@@ -95,6 +102,7 @@ State files use this shape:
|
|
|
95
102
|
|
|
96
103
|
```text
|
|
97
104
|
~/.pi/agent/tmp/pi-actors/runs/<run>/run.json
|
|
105
|
+
~/.pi/agent/tmp/pi-actors/runs/<run>/communication.json
|
|
98
106
|
~/.pi/agent/tmp/pi-actors/runs/<run>/progress.json
|
|
99
107
|
~/.pi/agent/tmp/pi-actors/runs/<run>/events.jsonl
|
|
100
108
|
~/.pi/agent/tmp/pi-actors/runs/<run>/outbox.jsonl
|
|
@@ -129,13 +137,27 @@ The core loop is:
|
|
|
129
137
|
|
|
130
138
|
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
139
|
|
|
140
|
+
### Persistent backlog implementers
|
|
141
|
+
|
|
142
|
+
Backlog implementer actors should be long-lived workers, not one-shot prompts, when the coordinator wants continuous branch work. A typical run starts two actors, such as `branch:<run>/front` and `branch:<run>/back`, that claim from opposite ends of the canonical backlog and share `room:<run>` for visibility.
|
|
143
|
+
|
|
144
|
+
The stable loop is:
|
|
145
|
+
|
|
146
|
+
1. Coordinator sends `task.assign` to an idle branch actor with the exact backlog slice and validation boundary.
|
|
147
|
+
2. Actor posts `task.claim` to `room:<run>` before editing.
|
|
148
|
+
3. Actor completes the slice, validates, and posts `task.result` plus `awaiting_assignment`.
|
|
149
|
+
4. Actor remains alive and waits for the next coordinator message.
|
|
150
|
+
5. Coordinator either sends another `task.assign` or sends `control.stop` after confirming no actionable work remains.
|
|
151
|
+
|
|
152
|
+
Implementer recipes should declare this contract in `mailbox.accepts` and `mailbox.emits`. They should not self-terminate after a successful slice, and they should not silently self-select a new task unless the coordinator deliberately configured that policy for the run. This keeps task choice centralized while preserving actor-local execution autonomy.
|
|
153
|
+
|
|
132
154
|
## Tool Surface
|
|
133
155
|
|
|
134
156
|
The actor-level surface is:
|
|
135
157
|
|
|
136
158
|
- `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>`, `coordinator`, or `session:<id>`.
|
|
138
|
-
- `inspect`: intentionally read owned `run:<id>` status, tail, messages, artifacts, files, or
|
|
159
|
+
- `message`: send one typed envelope to `run:<id>`, `branch:<run>/<branch>`, `room:<run>`, `tool:<name>`, `coordinator`, or `session:<id>`.
|
|
160
|
+
- `inspect`: intentionally read owned `run:<id>` status, tail, messages, artifacts, files, mailbox metadata, or communication snapshot; read `room:<run>` status, messages, previews, roster, or contacts; 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
161
|
|
|
140
162
|
Low-level async actions map into the actor surface instead of forming a second public model:
|
|
141
163
|
|
package/docs/recipe-library.md
CHANGED
|
@@ -59,9 +59,9 @@ register_tool name=subagent_prompt \
|
|
|
59
59
|
Start it:
|
|
60
60
|
|
|
61
61
|
```text
|
|
62
|
-
subagent_prompt prompt="Review docs/async-runs.md for unclear wording." run_id=
|
|
63
|
-
inspect target=run:
|
|
64
|
-
inspect target=run:
|
|
62
|
+
subagent_prompt prompt="Review docs/async-runs.md for unclear wording." run_id=docs_review
|
|
63
|
+
inspect target=run:docs_review view=status
|
|
64
|
+
inspect target=run:docs_review view=tail
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
## Composed Pipelines
|
|
@@ -82,6 +82,7 @@ Pipeline recipes demonstrate second-order composition:
|
|
|
82
82
|
- `recipes/pipeline-development-tasking.json`: Plan → task card → critique → integrator handoff.
|
|
83
83
|
- `recipes/pipeline-docs-maintenance.json`: Docs index → documentation review → maintenance plan → artifact report.
|
|
84
84
|
- `recipes/pipeline-media-library.json`: Playlist build → media-library artifact report.
|
|
85
|
+
- `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", "glyph" }` objects; `name` stays ASCII-safe for `branch:<run>/<name>` addresses, while optional `glyph` is display-only for room summaries and operator scanning. 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
86
|
- `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
87
|
- `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
88
|
- `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.
|
package/docs/template-recipes.md
CHANGED
|
@@ -78,7 +78,7 @@ Recipe priority only matters when two discovered recipes have the same filename
|
|
|
78
78
|
3. Explicitly referenced ad hoc user recipe files located outside `~/.pi/agent/recipes`.
|
|
79
79
|
4. User recipe files under `~/.pi/agent/recipes/*.json`.
|
|
80
80
|
|
|
81
|
-
The high-priority user recipe directory is also the default tool set: recipes placed there are agent tools
|
|
81
|
+
The high-priority user recipe directory is also the default tool set: recipes placed there are agent tools by location. This preserves the old advantage of a tool-only registry because listing `~/.pi/agent/recipes` shows the operator-managed tool surface. Packaged and ad hoc recipes are recipe components by default; they become tools only when copied or registered into the agent recipe root.
|
|
82
82
|
|
|
83
83
|
Higher-priority files shadow lower-priority files with the same basename. A highest-priority invalid recipe is still visible and blocks fallback so operators do not accidentally run packaged behavior when a user override is broken. A highest-priority recipe with `disabled: true` also blocks fallback and intentionally disables that id.
|
|
84
84
|
|
|
@@ -97,7 +97,7 @@ User-owned recipes may accumulate extension-maintained usage metadata:
|
|
|
97
97
|
|
|
98
98
|
The extension increments `usage.calls` and updates `usage.last_called` when it starts that concrete recipe, either through a recipe-backed tool call or a direct async recipe-file run. It also stores a content `usage.fingerprint`; if the authored recipe content changes, the next launch resets `usage.calls` before counting the new launch and records `usage.reset_at`. Agents should treat these fields as cleanup evidence, not as authored recipe contract. Packaged standard-library recipes are not mutated for usage metadata.
|
|
99
99
|
|
|
100
|
-
There is intentionally no failure counter in the recipe contract. A failed launch can reflect caller misuse, missing runtime values, or an environmental problem rather than recipe uselessness. Cleanup decisions should be explicit operator work: keep as a tool,
|
|
100
|
+
There is intentionally no failure counter in the recipe contract. A failed launch can reflect caller misuse, missing runtime values, or an environmental problem rather than recipe uselessness. Cleanup decisions should be explicit operator work: keep as a tool, move out of the agent recipe root to retain recipe-only memory, merge, delete, or archive.
|
|
101
101
|
|
|
102
102
|
For object form, keep `template` last. Recipe metadata comes first; executable content stays last.
|
|
103
103
|
|
|
@@ -203,12 +203,11 @@ Call-time params override file params. `values` are merged with file values; cal
|
|
|
203
203
|
|
|
204
204
|
## Registered Recipe Tools
|
|
205
205
|
|
|
206
|
-
A registered tool is a recipe file exposed as an agent tool. User recipes under `~/.pi/agent/recipes/*.json` are tools by
|
|
206
|
+
A registered tool is a recipe file exposed as an agent tool. User recipes under `~/.pi/agent/recipes/*.json` are tools by location; packaged/ad hoc recipes are components unless copied or registered into that user recipe root:
|
|
207
207
|
|
|
208
208
|
```json
|
|
209
209
|
{
|
|
210
210
|
"description": "Start an async docs review actor",
|
|
211
|
-
"tool": true,
|
|
212
211
|
"async": true,
|
|
213
212
|
"args": ["scope:path", "model:string"],
|
|
214
213
|
"template": "review {scope} --model {model}"
|
package/docs/tool-registry.md
CHANGED
|
@@ -6,18 +6,16 @@ This document is the local adaptation of the portable [Command Template Standard
|
|
|
6
6
|
|
|
7
7
|
## Registry Model
|
|
8
8
|
|
|
9
|
-
The
|
|
9
|
+
The registry source is location-discovered recipes, not a live tool-only JSON file and not a recipe-owned boolean:
|
|
10
10
|
|
|
11
11
|
- `~/.pi/agent/recipes/*.json` is the highest-priority user recipe root and the operator-managed tool set.
|
|
12
|
-
- Recipes in that root are tools by
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Packaged or ad hoc recipes opt into tool exposure with `tool: true`.
|
|
12
|
+
- Recipes in that root are tools by location.
|
|
13
|
+
- Packaged pi-actors recipes are the lower-priority standard library of declarative actor config components, not automatically registered tools.
|
|
14
|
+
- Ad hoc recipe files outside the user recipe root are components unless explicitly registered/copied into `~/.pi/agent/recipes`.
|
|
16
15
|
- Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` has id/tool name `docs_review`.
|
|
17
16
|
|
|
18
|
-
`~/.pi/agent/actors-tools.json` is legacy compatibility input. On startup, pi-actors migrates it into recipe files when possible, writes a migration report, and archives the source only when migration has no conflicts or invalid generated recipes.
|
|
19
17
|
|
|
20
|
-
Because the user recipe directory is sticky agent muscle memory, runtime launches update `usage.calls`, `usage.last_called`, and a content `usage.fingerprint` on user-owned recipe files. If authored recipe content changes, the next launch resets `usage.calls` and records `usage.reset_at` before counting the launch, so usage evidence follows the current recipe meaning rather than an older file history.
|
|
18
|
+
Because the user recipe directory is sticky agent muscle memory, runtime launches update `usage.calls`, `usage.last_called`, and a content `usage.fingerprint` on user-owned recipe files. If authored recipe content changes, the next launch resets `usage.calls` and records `usage.reset_at` before counting the launch, so usage evidence follows the current recipe meaning rather than an older file history. `inspect target=recipes view=summary verbose=true` includes usage metadata and operator-gated cleanup recommendations for invalid, shadowed, disabled, component-only, unused, or overriding recipes. Recommended actions stay explicit: keep as a tool/component, enable, merge, fix, delete, or archive. The extension does not maintain a failure counter and agents should not silently clean tools during unrelated work.
|
|
21
19
|
|
|
22
20
|
`register_tool` is the preferred agent-facing mutation API. It creates, updates, and deletes recipe files in `~/.pi/agent/recipes`; agents do not need to edit the files directly for normal registration. Direct file edits are still valid for operators and advanced agents. Runtime behavior is reactive: file creation, deletion, or edits in the user recipe root trigger validation and tool-set refresh, with invalid recipes surfaced as diagnostics rather than silently ignored.
|
|
23
21
|
|
|
@@ -62,18 +60,17 @@ For reusable actor workflows, register a small tool whose `template` points to a
|
|
|
62
60
|
```text
|
|
63
61
|
register_tool name=docs_review \
|
|
64
62
|
description="Start an async docs review actor" \
|
|
65
|
-
template="
|
|
63
|
+
template="docs_review" \
|
|
66
64
|
args="scope:path,model:string"
|
|
67
65
|
```
|
|
68
66
|
|
|
69
|
-
This writes or updates `~/.pi/agent/recipes/docs_review.json` with
|
|
67
|
+
This writes or updates `~/.pi/agent/recipes/docs_review.json` with a recipe-reference template. Its location in the user recipe root makes it a tool. If the referenced recipe contains `async: true`, calling the tool starts a detached actor run and returns metadata immediately. If `async` is omitted or false, the same recipe runs foreground and returns normal tool output.
|
|
70
68
|
|
|
71
69
|
When co-location is clearer than a separate file, `register_tool` writes the recipe fields directly into the user recipe file:
|
|
72
70
|
|
|
73
71
|
```json
|
|
74
72
|
{
|
|
75
73
|
"description": "Start an async docs review",
|
|
76
|
-
"tool": true,
|
|
77
74
|
"async": true,
|
|
78
75
|
"args": ["scope:path", "model:string"],
|
|
79
76
|
"template": "pi -p --model {model} --tools read,bash \"Review {scope}\""
|
|
@@ -95,7 +92,6 @@ Tool names come from recipe filenames in `~/.pi/agent/recipes`. Recipe files def
|
|
|
95
92
|
```json
|
|
96
93
|
{
|
|
97
94
|
"description": "Transcribe an audio file",
|
|
98
|
-
"tool": true,
|
|
99
95
|
"template": "~/bin/transcribe {file:path} {lang=ru} {model:string}"
|
|
100
96
|
}
|
|
101
97
|
```
|
|
@@ -103,7 +99,6 @@ Tool names come from recipe filenames in `~/.pi/agent/recipes`. Recipe files def
|
|
|
103
99
|
```json
|
|
104
100
|
{
|
|
105
101
|
"description": "Run pi as a non-interactive sub-agent",
|
|
106
|
-
"tool": true,
|
|
107
102
|
"args": ["prompt:string", "model:string"],
|
|
108
103
|
"template": "pi -p --model {model} --no-tools {prompt}"
|
|
109
104
|
}
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* pi-actors — actor runtime and persistent local tool registry for pi.
|
|
3
3
|
* Zones: composition root, pi agent, actor runtime
|
|
4
4
|
*
|
|
5
|
-
* Wraps command templates as callable pi tools, stores
|
|
5
|
+
* Wraps command templates as callable pi tools, stores durable user tools as recipe files, and exposes actor orchestration across reloads and sessions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, watch, type FSWatcher } from "node:fs";
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ExtensionContext,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
|
|
15
|
+
import * as ActorInspectorTui from "./lib/actor-inspector-tui.ts";
|
|
15
16
|
import * as CommandTemplates from "./lib/command-templates.ts";
|
|
16
17
|
import * as Observability from "./lib/observability.ts";
|
|
17
18
|
import * as Paths from "./lib/paths.ts";
|
|
@@ -47,6 +48,9 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
|
|
|
47
48
|
const observedRuns = new Map<string, Observability.RunObservedStatus>();
|
|
48
49
|
const observedRunEventLines = new Map<string, number>();
|
|
49
50
|
let runStatusFrame = 0;
|
|
51
|
+
let communicationWidgetVisible = false;
|
|
52
|
+
let actorInspectorRows = 12;
|
|
53
|
+
let selectedInspectorSequence: number | undefined;
|
|
50
54
|
const getRunOwnerId = (ctx: ExtensionContext): string =>
|
|
51
55
|
ctx.sessionManager.getSessionId();
|
|
52
56
|
const updateRunUi = (ctx: ExtensionContext, notify = false): void => {
|
|
@@ -57,7 +61,46 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
|
|
|
57
61
|
"zz-pi-actors-runs",
|
|
58
62
|
status ? ctx.ui.theme.fg("dim", status) : undefined,
|
|
59
63
|
);
|
|
60
|
-
ctx.ui.setWidget(
|
|
64
|
+
ctx.ui.setWidget(
|
|
65
|
+
"zz-pi-actors-comms",
|
|
66
|
+
communicationWidgetVisible
|
|
67
|
+
? () => ({
|
|
68
|
+
invalidate() {},
|
|
69
|
+
render(width: number) {
|
|
70
|
+
const previews = ActorInspectorTui.readActorInspectorPreviews(
|
|
71
|
+
RUN_STATE_ROOT,
|
|
72
|
+
actorInspectorRows,
|
|
73
|
+
{ ownerId, currentRunOnly: true },
|
|
74
|
+
);
|
|
75
|
+
const style = {
|
|
76
|
+
actor: (text: string) => ctx.ui.theme.fg("accent", text),
|
|
77
|
+
muted: (text: string) => ctx.ui.theme.fg("dim", text),
|
|
78
|
+
preview: (text: string) => ctx.ui.theme.fg("text", text),
|
|
79
|
+
stripe: (text: string) => text,
|
|
80
|
+
stripeAlt: (text: string) =>
|
|
81
|
+
ctx.ui.theme.bg("customMessageBg", text),
|
|
82
|
+
target: (text: string) => ctx.ui.theme.fg("success", text),
|
|
83
|
+
type: (text: string) => ctx.ui.theme.fg("warning", text),
|
|
84
|
+
};
|
|
85
|
+
return (
|
|
86
|
+
(selectedInspectorSequence !== undefined
|
|
87
|
+
? ActorInspectorTui.renderInspectorItemView(
|
|
88
|
+
previews,
|
|
89
|
+
width,
|
|
90
|
+
style,
|
|
91
|
+
{ sequence: selectedInspectorSequence },
|
|
92
|
+
)
|
|
93
|
+
: ActorInspectorTui.renderInspectorWidget(
|
|
94
|
+
previews,
|
|
95
|
+
width,
|
|
96
|
+
style,
|
|
97
|
+
)) ?? []
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
: undefined,
|
|
102
|
+
{ placement: "belowEditor" },
|
|
103
|
+
);
|
|
61
104
|
const transitions = Observability.detectRunTransitions(
|
|
62
105
|
observedRuns,
|
|
63
106
|
summary,
|
|
@@ -135,7 +178,9 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
|
|
|
135
178
|
if (!existsSync(RUN_STATE_ROOT)) return;
|
|
136
179
|
if (!stateRootWatcher) {
|
|
137
180
|
try {
|
|
138
|
-
stateRootWatcher = watch(RUN_STATE_ROOT, () =>
|
|
181
|
+
stateRootWatcher = watch(RUN_STATE_ROOT, () =>
|
|
182
|
+
scheduleRunEventUpdate(ctx),
|
|
183
|
+
);
|
|
139
184
|
stateRootWatcher.on("error", () => {
|
|
140
185
|
stateRootWatcher?.close();
|
|
141
186
|
stateRootWatcher = undefined;
|
|
@@ -207,6 +252,61 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
|
|
|
207
252
|
closeRunWatchers();
|
|
208
253
|
closeRecipeWatcher();
|
|
209
254
|
});
|
|
255
|
+
pi.registerCommand("actors-inspector-toggle", {
|
|
256
|
+
description: "Toggle actor inspector widget; optional row count",
|
|
257
|
+
handler: async (args, ctx) => {
|
|
258
|
+
const raw = Array.isArray(args) ? args[0] : String(args ?? "");
|
|
259
|
+
if (String(raw).trim()) {
|
|
260
|
+
const rows = Number.parseInt(String(raw), 10);
|
|
261
|
+
if (!Number.isFinite(rows) || rows <= 0) {
|
|
262
|
+
ctx.ui.notify(
|
|
263
|
+
"Usage: /actors-inspector-toggle [rows] where rows > 0",
|
|
264
|
+
"warning",
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
actorInspectorRows = rows;
|
|
269
|
+
selectedInspectorSequence = undefined;
|
|
270
|
+
communicationWidgetVisible = true;
|
|
271
|
+
updateRunUi(ctx);
|
|
272
|
+
ctx.ui.notify(`Actor inspector rows ${rows}`, "info");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (selectedInspectorSequence !== undefined) {
|
|
276
|
+
selectedInspectorSequence = undefined;
|
|
277
|
+
communicationWidgetVisible = true;
|
|
278
|
+
updateRunUi(ctx);
|
|
279
|
+
ctx.ui.notify("Actor inspector table", "info");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (communicationWidgetVisible) {
|
|
283
|
+
communicationWidgetVisible = false;
|
|
284
|
+
} else {
|
|
285
|
+
actorInspectorRows = 12;
|
|
286
|
+
communicationWidgetVisible = true;
|
|
287
|
+
}
|
|
288
|
+
updateRunUi(ctx);
|
|
289
|
+
ctx.ui.notify(
|
|
290
|
+
`Actor inspector ${communicationWidgetVisible ? "shown" : "hidden"}`,
|
|
291
|
+
"info",
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
pi.registerCommand("actors-inspect", {
|
|
296
|
+
description: "Inspect actor message by visible number",
|
|
297
|
+
handler: async (args, ctx) => {
|
|
298
|
+
const raw = Array.isArray(args) ? args[0] : String(args ?? "");
|
|
299
|
+
const sequence = Number.parseInt(String(raw), 10);
|
|
300
|
+
if (!Number.isFinite(sequence) || sequence <= 0) {
|
|
301
|
+
ctx.ui.notify("Usage: /actors-inspect <number>", "warning");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
selectedInspectorSequence = sequence;
|
|
305
|
+
communicationWidgetVisible = true;
|
|
306
|
+
updateRunUi(ctx);
|
|
307
|
+
ctx.ui.notify(`Actor inspect item ${sequence}`, "info");
|
|
308
|
+
},
|
|
309
|
+
});
|
|
210
310
|
pi.on("before_agent_start", async (event) => ({
|
|
211
311
|
systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
|
|
212
312
|
}));
|