@llblab/pi-actors 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +72 -0
- package/BACKLOG.md +38 -0
- package/CHANGELOG.md +179 -0
- package/README.md +338 -0
- package/docs/README.md +21 -0
- package/docs/actor-messages.md +149 -0
- package/docs/async-runs.md +335 -0
- package/docs/command-templates.md +424 -0
- package/docs/component-recipes.md +148 -0
- package/docs/recipe-library.md +176 -0
- package/docs/task-first-recipes.md +233 -0
- package/docs/template-recipes.md +285 -0
- package/docs/tool-registry.md +142 -0
- package/index.ts +198 -0
- package/lib/actor-messages.ts +120 -0
- package/lib/async-runs.ts +688 -0
- package/lib/command-templates.ts +795 -0
- package/lib/config.ts +266 -0
- package/lib/execution.ts +720 -0
- package/lib/file-state.ts +24 -0
- package/lib/identity.ts +29 -0
- package/lib/observability.ts +525 -0
- package/lib/output.ts +123 -0
- package/lib/paths.ts +35 -0
- package/lib/prompts.ts +75 -0
- package/lib/recipe-references.ts +586 -0
- package/lib/registry.ts +302 -0
- package/lib/runtime.ts +101 -0
- package/lib/schema.ts +402 -0
- package/lib/temp.ts +44 -0
- package/lib/tools.ts +651 -0
- package/package.json +52 -0
- package/recipes/music-player.json +25 -0
- package/recipes/pipeline-architect-coordinator.json +88 -0
- package/recipes/pipeline-artifact-report.json +52 -0
- package/recipes/pipeline-artifact-write.json +66 -0
- package/recipes/pipeline-async-run-ops.json +67 -0
- package/recipes/pipeline-checkpoint-continuation.json +57 -0
- package/recipes/pipeline-development-tasking.json +73 -0
- package/recipes/pipeline-docs-maintenance.json +72 -0
- package/recipes/pipeline-media-library.json +51 -0
- package/recipes/pipeline-quorum-review.json +72 -0
- package/recipes/pipeline-release-readiness.json +83 -0
- package/recipes/pipeline-repo-health.json +81 -0
- package/recipes/pipeline-research-synthesis.json +87 -0
- package/recipes/pipeline-review-readiness.json +49 -0
- package/recipes/subagent-artifact.json +26 -0
- package/recipes/subagent-checkpoint.json +27 -0
- package/recipes/subagent-conflict-report.json +25 -0
- package/recipes/subagent-contradiction-map.json +26 -0
- package/recipes/subagent-critic.json +28 -0
- package/recipes/subagent-evidence-map.json +26 -0
- package/recipes/subagent-followup.json +27 -0
- package/recipes/subagent-judge.json +26 -0
- package/recipes/subagent-merge.json +26 -0
- package/recipes/subagent-message.json +29 -0
- package/recipes/subagent-normalize.json +24 -0
- package/recipes/subagent-plan.json +26 -0
- package/recipes/subagent-prompt.json +22 -0
- package/recipes/subagent-quorum.json +41 -0
- package/recipes/subagent-review-coordinator.json +107 -0
- package/recipes/subagent-review.json +30 -0
- package/recipes/subagent-task-card.json +28 -0
- package/recipes/subagent-tools.json +17 -0
- package/recipes/subagent-verify.json +27 -0
- package/recipes/subagents-prompts.json +32 -0
- package/recipes/utility-actor-message.json +24 -0
- package/recipes/utility-artifact-manifest.json +17 -0
- package/recipes/utility-artifact-write.json +17 -0
- package/recipes/utility-changelog-head.json +12 -0
- package/recipes/utility-changelog-section.json +14 -0
- package/recipes/utility-git-log.json +12 -0
- package/recipes/utility-git-status.json +10 -0
- package/recipes/utility-jsonl-tail.json +11 -0
- package/recipes/utility-markdown-index.json +15 -0
- package/recipes/utility-package-summary.json +12 -0
- package/recipes/utility-playlist-build.json +18 -0
- package/recipes/utility-playlist-scan.json +12 -0
- package/recipes/utility-run-state-files.json +14 -0
- package/recipes/utility-run-summary.json +12 -0
- package/recipes/utility-validate-recipe.json +14 -0
- package/recipes/utility-validation-wrapper.json +14 -0
- package/scripts/async-runner.mjs +170 -0
- package/scripts/music-player.mjs +637 -0
- package/scripts/recipe-utils.mjs +273 -0
- package/scripts/validate-recipe.mjs +89 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Template Recipe Standard
|
|
2
|
+
|
|
3
|
+
Template recipes are saved JSON definitions around the synchronous [Command Template Standard](./command-templates.md).
|
|
4
|
+
|
|
5
|
+
**Meta-contract:** a recipe stores a command-template graph plus defaults and run mode. It does not create a second execution language.
|
|
6
|
+
|
|
7
|
+
**Scope:** reusable JSON shape, recipe naming, file-backed recipes, co-located recipes, recipe-layer imports/references, call-time values, foreground execution, and the `async: true` handoff to the [Async Run Standard](./async-runs.md).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Reading Model
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
command template = execution graph
|
|
15
|
+
recipe = saved JSON definition
|
|
16
|
+
run = one execution instance
|
|
17
|
+
async: true = run through detached lifecycle
|
|
18
|
+
```
|
|
19
|
+
|
|
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
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Layer Ownership
|
|
25
|
+
|
|
26
|
+
Template-recipe standard owns:
|
|
27
|
+
|
|
28
|
+
- Saved JSON definitions around one command-template graph.
|
|
29
|
+
- File-backed and co-located recipe shapes.
|
|
30
|
+
- Recipe identity through `name` or filename.
|
|
31
|
+
- Recipe defaults, values, imports, import references, and import-node expansion.
|
|
32
|
+
- Ordered named artifact declarations through `artifacts`.
|
|
33
|
+
- Foreground-vs-detached selection through `async: true` when invoked by a recipe-aware host.
|
|
34
|
+
|
|
35
|
+
Template-recipe standard does not own:
|
|
36
|
+
|
|
37
|
+
- How command-template nodes execute internally.
|
|
38
|
+
- Async state files, logs, FIFO, status, cancellation, or observability.
|
|
39
|
+
- Tool registry naming, button UX, package installation, or operator-specific policy.
|
|
40
|
+
- Domain workflows such as swarm quorum, release policy, backlog parsing, or merge policy.
|
|
41
|
+
|
|
42
|
+
A recipe can be synchronous or asynchronous:
|
|
43
|
+
|
|
44
|
+
- Omitted or false `async`: a registered tool executes the recipe in the foreground and returns normal tool output.
|
|
45
|
+
- `async: true`: a registered tool starts a detached async run and returns run metadata immediately.
|
|
46
|
+
|
|
47
|
+
## Minimal Shape
|
|
48
|
+
|
|
49
|
+
Synchronous recipe:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"name": "check-docs",
|
|
54
|
+
"template": "npm run check:docs"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Async recipe:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"name": "review-docs",
|
|
63
|
+
"async": true,
|
|
64
|
+
"template": "review docs/spec.md"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`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.
|
|
69
|
+
|
|
70
|
+
For object form, keep `template` last. Recipe metadata comes first; executable content stays last.
|
|
71
|
+
|
|
72
|
+
## Named Artifacts
|
|
73
|
+
|
|
74
|
+
Use recipe-level `artifacts` to declare stable artifact names and paths for the whole recipe, ordered from most important to least important:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"name": "report-task",
|
|
79
|
+
"args": ["report_path:path"],
|
|
80
|
+
"defaults": { "report_path": "artifacts/report.md" },
|
|
81
|
+
"artifacts": {
|
|
82
|
+
"report": "{report_path}",
|
|
83
|
+
"summary": "artifacts/summary.json"
|
|
84
|
+
},
|
|
85
|
+
"template": "generate-report --out {report_path}"
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`output` and `artifacts` are intentionally different. `output` is the command-template primary result selector and defaults to stdout; it participates in sequence/stdin flow. `artifacts` is recipe metadata: an ordered named artifact manifest for humans, async completion events, and downstream tooling. `stdout` remains the default command result channel and is not renamed by `artifacts`.
|
|
90
|
+
|
|
91
|
+
## Mailbox
|
|
92
|
+
|
|
93
|
+
Use recipe-level `mailbox` to document the semantic messages a recipe actor accepts and emits:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mailbox": {
|
|
98
|
+
"accepts": ["control.continue", "control.revise", "control.approve", "control.stop"],
|
|
99
|
+
"emits": ["checkpoint.needs_scope", "branch.done", "run.done"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`mailbox` is contract metadata, not transport configuration. It should name semantic message types, not FIFO commands, file paths, or CLI fragments.
|
|
105
|
+
|
|
106
|
+
## Actor Message Delivery
|
|
107
|
+
|
|
108
|
+
Recipes do not declare a second event-delivery policy. A running actor emits addressed messages such as `command.done`, `run.done`, or `checkpoint.needs_input`; the coordinator/runtime decides whether a message stays diagnostic, becomes a notification, or re-enters the agent context. This keeps recipe metadata focused on the actor contract:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mailbox": {
|
|
113
|
+
"accepts": ["control.stop"],
|
|
114
|
+
"emits": ["command.done", "run.done", "run.failed"]
|
|
115
|
+
},
|
|
116
|
+
"template": "run-subtask {prompt}"
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Command-Template Flags At Recipe Top Level
|
|
121
|
+
|
|
122
|
+
Top-level command-template flags may sit beside `name` and `async`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"name": "review-docs",
|
|
127
|
+
"async": true,
|
|
128
|
+
"parallel": true,
|
|
129
|
+
"timeout": 300000,
|
|
130
|
+
"failure": "branch",
|
|
131
|
+
"template": ["review-a docs/spec.md", "review-b docs/spec.md"]
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Valid command-template flags include `args`, `defaults`, `parallel`, `when`, `label`, `timeout`, `delay`, `output`, `retry`, `failure`, `recover`, and `repeat`.
|
|
136
|
+
|
|
137
|
+
Timeout is disabled by default. Set a positive `timeout` when a recipe should fail closed after a bounded runtime; omit it, or set `0`, for intentionally open-ended runs that will be stopped by async cancellation, such as background audio playback.
|
|
138
|
+
|
|
139
|
+
## Valid Graph
|
|
140
|
+
|
|
141
|
+
The valid chain is:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
tool → template reference → recipe → run → template
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A recipe must define `template` directly. A recipe must not define `tool`, because recipes are saved command-template definitions, not tool indirection layers.
|
|
148
|
+
|
|
149
|
+
A recipe may live in a file or be co-located inside a registered tool entry. Both are storage variants of the same graph.
|
|
150
|
+
|
|
151
|
+
## File-Backed Recipes
|
|
152
|
+
|
|
153
|
+
Reusable local recipes live in:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
~/.pi/agent/recipes/*.json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Bare recipe names resolve under that directory, so `file: "review-docs"` loads:
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
~/.pi/agent/recipes/review-docs.json
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
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.
|
|
166
|
+
|
|
167
|
+
## Registered Recipe Tools
|
|
168
|
+
|
|
169
|
+
A registered tool can point at an actor recipe by storing the recipe path or name in `template`:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"shader_ring": {
|
|
174
|
+
"description": "Start the shader ring recipe",
|
|
175
|
+
"args": ["theme", "out_dir"],
|
|
176
|
+
"template": "shader-ring-8-parallel.json"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
If `shader-ring-8-parallel.json` contains `async: true`, calling `shader_ring` starts a detached run and returns metadata. If `async` is omitted or false, calling `shader_ring` executes the recipe foreground and returns normal tool output.
|
|
182
|
+
|
|
183
|
+
A registered tool may also co-locate an actor recipe directly in `tools.json`:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"review_docs": {
|
|
188
|
+
"description": "Start an async docs review",
|
|
189
|
+
"name": "review-docs",
|
|
190
|
+
"async": true,
|
|
191
|
+
"template": "review {scope}"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The co-located entry must still own `template` directly and must not define `tool`.
|
|
197
|
+
|
|
198
|
+
## Values And Public Args
|
|
199
|
+
|
|
200
|
+
Recipe placeholders come from runtime values, recipe `defaults`, inline placeholder defaults, and registered-tool defaults.
|
|
201
|
+
|
|
202
|
+
Recipe tools derive public arguments from the referenced or co-located command template when the recipe is available locally. Explicit `args` is still available when the public tool surface should be narrower than the recipe internals.
|
|
203
|
+
|
|
204
|
+
Example: a recipe may expose a private `repo` default for an example script, while the registered public tool only asks for `file`, `volume`, and `player`.
|
|
205
|
+
|
|
206
|
+
## Recipe Imports
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"name": "parent",
|
|
213
|
+
"imports": {
|
|
214
|
+
"prepare": "prepare-worktree.json",
|
|
215
|
+
"test": {
|
|
216
|
+
"from": "run-tests.json",
|
|
217
|
+
"values": { "suite": "unit" }
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"template": [{ "name": "prepare" }, { "name": "test" }]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
An import binding may be either a string recipe path/name or an object with:
|
|
225
|
+
|
|
226
|
+
- `from`: recipe path or bare name.
|
|
227
|
+
- `defaults`: extra default values exposed through the import.
|
|
228
|
+
- `values`: explicit values for embedding that imported recipe.
|
|
229
|
+
|
|
230
|
+
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.
|
|
231
|
+
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"name": "parallel-review",
|
|
237
|
+
"async": true,
|
|
238
|
+
"imports": {
|
|
239
|
+
"review": "review-one.json"
|
|
240
|
+
},
|
|
241
|
+
"parallel": true,
|
|
242
|
+
"failure": "branch",
|
|
243
|
+
"template": [
|
|
244
|
+
{ "name": "review", "values": { "scope": "README.md" } },
|
|
245
|
+
{ "name": "review", "values": { "scope": "docs/template-recipes.md" } }
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Recipes can also read imported metadata and value containers before command-template placeholder expansion. Each import alias acts like a recipe-local variable:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"imports": {
|
|
255
|
+
"base": {
|
|
256
|
+
"from": "base.json",
|
|
257
|
+
"values": { "target": "docs" }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"defaults": {
|
|
261
|
+
"profile": "{base.defaults.profile=safe}",
|
|
262
|
+
"target": "{base.values.target}",
|
|
263
|
+
"label": "{base.name}:{base.values.target}",
|
|
264
|
+
"enabled_label": "{base.defaults.enabled?enabled:disabled}"
|
|
265
|
+
},
|
|
266
|
+
"template": "run {base.defaults.profile=safe} {base.values.target} {label}"
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Supported references are:
|
|
271
|
+
|
|
272
|
+
- `{alias.name}`
|
|
273
|
+
- `{alias.file}`
|
|
274
|
+
- `{alias.defaults.key}`
|
|
275
|
+
- `{alias.values.key}`
|
|
276
|
+
- `{alias.defaults.key=fallback}` for a missing/null import value fallback.
|
|
277
|
+
- `{alias.values.key?truthy:falsy}` for a small recipe-layer ternary.
|
|
278
|
+
|
|
279
|
+
Nested object keys are dot-separated. Import references are resolved before normal command-template placeholders, so ordinary values such as `{label}` still flow through command-template defaults and call-time values. Ternaries use simple falsy checks for missing, null, false, zero, and empty string. Missing imports, missing values without fallback, and import cycles fail during recipe loading.
|
|
280
|
+
|
|
281
|
+
## Recipe Shape
|
|
282
|
+
|
|
283
|
+
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.
|
|
284
|
+
|
|
285
|
+
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.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Tool Registry
|
|
2
|
+
|
|
3
|
+
`pi-actors` stores registered command-template tools and template-recipe launchers in `~/.pi/agent/tools.json` and registers them automatically on session start.
|
|
4
|
+
|
|
5
|
+
This document is the local adaptation of the portable [Command Template Standard](./command-templates.md).
|
|
6
|
+
|
|
7
|
+
## Registering Tools
|
|
8
|
+
|
|
9
|
+
`register_tool` is the interactive API for listing, creating, updating, or deleting persistent tools. Call it without arguments to list registered tools.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
register_tool name=transcribe_groq \
|
|
13
|
+
description="Transcribe audio files using Groq Whisper API" \
|
|
14
|
+
template="~/.pi/agent/skills/groq-stt/scripts/transcribe.mjs {file} {lang=ru} {model=whisper-large-v3-turbo}"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
register_tool name=call_subagent \
|
|
19
|
+
description="Run pi as a non-interactive sub-agent" \
|
|
20
|
+
template="pi -p --model {model=openai-codex/gpt-5.5} --no-tools {prompt}"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding.
|
|
24
|
+
|
|
25
|
+
`template` may also be a standard command-template sequence for multi-step tools. Timeout is disabled by default; add explicit positive `timeout` values when individual steps should fail closed:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
[
|
|
29
|
+
"~/bin/tts --text {text} --out {mp3}",
|
|
30
|
+
{ "timeout": 300000, "template": "ffmpeg -y -i {mp3} -c:a libopus {ogg}" }
|
|
31
|
+
]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For reusable workflows, register a small tool whose `template` points to a template recipe instead of embedding a large parallel template in the tool itself:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
register_tool name=shader_ring \
|
|
38
|
+
description="Start the shader ring recipe" \
|
|
39
|
+
template="shader-ring-8-parallel.json" \
|
|
40
|
+
args="theme,out_dir"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This stores the recipe path in the registry as `template`. If `~/.pi/agent/recipes/shader-ring-8-parallel.json` contains `async: true`, calling the tool starts a detached run and returns metadata immediately. If `async` is omitted or false, the same recipe runs foreground and returns normal tool output.
|
|
44
|
+
|
|
45
|
+
When co-location is clearer than a separate file, the registry entry may include recipe fields directly beside tool metadata:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"review_docs": {
|
|
50
|
+
"description": "Start an async docs review",
|
|
51
|
+
"name": "review-docs",
|
|
52
|
+
"async": true,
|
|
53
|
+
"template": "pi -p --model openai-codex/gpt-5.5 --tools read,bash \"Review {scope}\""
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This is still not a cycle: `name` names the saved definition when it differs from the tool key, `async: true` selects detached run mode, and `template` remains the executable body. Co-located recipe entries must not define `tool`.
|
|
59
|
+
|
|
60
|
+
Delete a tool with `template=null`:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
register_tool name=call_subagent template=null
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Stored Shape
|
|
67
|
+
|
|
68
|
+
Tool names come from the top-level registry keys. Tool entries define `template`; it may be an inline command template, a template recipe JSON path/name, or the body of a co-located template recipe when `async` or entry-local `name` is present. Template entries keep `template` last, matching the command-template readability rule. The commands above persist entries like this:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"transcribe_groq": {
|
|
73
|
+
"description": "Transcribe audio files using Groq Whisper API",
|
|
74
|
+
"template": "~/.pi/agent/skills/groq-stt/scripts/transcribe.mjs {file} {lang=ru} {model=whisper-large-v3-turbo}"
|
|
75
|
+
},
|
|
76
|
+
"call_subagent": {
|
|
77
|
+
"description": "Run pi as a non-interactive sub-agent",
|
|
78
|
+
"template": "pi -p --model {model=openai-codex/gpt-5.5} --no-tools {prompt}"
|
|
79
|
+
},
|
|
80
|
+
"shader_ring": {
|
|
81
|
+
"description": "Start the shader ring recipe",
|
|
82
|
+
"args": ["theme", "out_dir"],
|
|
83
|
+
"template": "shader-ring-8-parallel.json"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Args and Defaults
|
|
89
|
+
|
|
90
|
+
When `args` is omitted, `pi-actors` derives tool parameters from placeholders in `template`:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
template="~/bin/transcribe {file} {lang=ru} {model=whisper-large-v3-turbo}"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The optional `args` field is an explicit placeholder declaration, matching the command-template standard. Untyped declarations remain valid:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{ "args": ["file", "lang"] }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Typed declarations are progressive and compact; they improve generated tool schemas and runtime validation without requiring authors to write JSON Schema. Types can be declared either in `args` or directly on template placeholders.
|
|
103
|
+
|
|
104
|
+
Use the metadata-first style when the command line is long and readability benefits from keeping the executable string short:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"args": [
|
|
109
|
+
"file:path",
|
|
110
|
+
"out_dir:path",
|
|
111
|
+
"request_timeout:int",
|
|
112
|
+
"speed:number",
|
|
113
|
+
"dry_run:bool",
|
|
114
|
+
"mode:enum(check,fix)"
|
|
115
|
+
],
|
|
116
|
+
"defaults": {
|
|
117
|
+
"timeout": "60000",
|
|
118
|
+
"speed": "1.5",
|
|
119
|
+
"dry_run": "true",
|
|
120
|
+
"mode": "check"
|
|
121
|
+
},
|
|
122
|
+
"template": "tool --file {file} --out {out_dir} --timeout {request_timeout} --speed {speed} --dry-run {dry_run} --mode {mode}"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Use the inline-first style when a compact tool is clearer as one self-contained template:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
template="tool --file {file:path} --out {out_dir:path} --timeout {request_timeout:int=60000} --speed {speed:number=1.5} --dry-run {dry_run:bool=true} --mode {mode:enum(check,fix)=check}"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Supported compact types are `string` (implicit), `path`, `int`, `number`, `bool`, and `enum(a,b)`. Defaults should be stored in `defaults`, written inline as `{name=default}`, or supplied through interactive shorthand. Shorthand such as `args="file,lang=ru"` and typed shorthand such as `request_timeout:int=60000` are normalized before persistence. When both `args` and template placeholders provide a type for the same name, explicit `args` wins.
|
|
133
|
+
|
|
134
|
+
Defaults are applied before substitution, with resolution order runtime values → stored `defaults` → inline default → error. Missing required values are rejected before or during execution. Typed runtime values are normalized before substitution: `int` and `number` values become numeric strings, booleans become `true`/`false`, and enums must match one of the declared values.
|
|
135
|
+
|
|
136
|
+
Template recipe tools derive public arguments from the referenced or co-located command template when the recipe is available locally. Explicit `args` is still available when the public tool surface should be narrower or defaulted differently, or when a file-backed recipe is not available during registration. Runtime values are passed as `values`; async recipe tools also accept optional `run_id` to override the generated run id.
|
|
137
|
+
|
|
138
|
+
## File Argument Naming
|
|
139
|
+
|
|
140
|
+
For tools that accept a local file path, use `file` as the canonical argument name.
|
|
141
|
+
|
|
142
|
+
Avoid using `filename` for full paths. `filename` usually means a basename/display name, while `file` can represent a concrete local file path.
|
package/index.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-actors — actor runtime and persistent local tool registry for pi.
|
|
3
|
+
* Zones: composition root, pi agent, actor runtime
|
|
4
|
+
*
|
|
5
|
+
* Wraps command templates as callable pi tools, stores their definitions in tools.json, and exposes actor orchestration across reloads and sessions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, watch, type FSWatcher } from "node:fs";
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
ExtensionContext,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
import * as CommandTemplates from "./lib/command-templates.ts";
|
|
16
|
+
import * as Observability from "./lib/observability.ts";
|
|
17
|
+
import * as Paths from "./lib/paths.ts";
|
|
18
|
+
import * as Prompts from "./lib/prompts.ts";
|
|
19
|
+
import * as Runtime from "./lib/runtime.ts";
|
|
20
|
+
import * as Temp from "./lib/temp.ts";
|
|
21
|
+
import * as Tools from "./lib/tools.ts";
|
|
22
|
+
|
|
23
|
+
const CONFIG_PATH = Paths.getConfigPath();
|
|
24
|
+
const TEMP_DIR = Paths.getExtensionTmpDir();
|
|
25
|
+
const RUN_STATE_ROOT = Paths.getRunStateRoot();
|
|
26
|
+
const RESERVED_TOOL_NAMES = new Set([
|
|
27
|
+
"read",
|
|
28
|
+
"write",
|
|
29
|
+
"edit",
|
|
30
|
+
"bash",
|
|
31
|
+
"find",
|
|
32
|
+
"grep",
|
|
33
|
+
"ls",
|
|
34
|
+
"register_tool",
|
|
35
|
+
"message",
|
|
36
|
+
"spawn",
|
|
37
|
+
"inspect",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export default function toolRegistryExtension(pi: ExtensionAPI) {
|
|
41
|
+
let runsAnimationInterval: NodeJS.Timeout | undefined;
|
|
42
|
+
let runsNotifyTimeout: NodeJS.Timeout | undefined;
|
|
43
|
+
let stateRootWatcher: FSWatcher | undefined;
|
|
44
|
+
const runDirWatchers = new Map<string, FSWatcher>();
|
|
45
|
+
const observedRuns = new Map<string, Observability.RunObservedStatus>();
|
|
46
|
+
const observedRunEventLines = new Map<string, number>();
|
|
47
|
+
let runStatusFrame = 0;
|
|
48
|
+
const getRunOwnerId = (ctx: ExtensionContext): string =>
|
|
49
|
+
ctx.sessionManager.getSessionId();
|
|
50
|
+
const updateRunUi = (ctx: ExtensionContext, notify = false): void => {
|
|
51
|
+
const ownerId = getRunOwnerId(ctx);
|
|
52
|
+
const summary = Observability.summarizeRuns(undefined, ownerId);
|
|
53
|
+
const status = Observability.renderRunStatus(summary, runStatusFrame++);
|
|
54
|
+
ctx.ui.setStatus(
|
|
55
|
+
"zz-pi-actors-runs",
|
|
56
|
+
status ? ctx.ui.theme.fg("dim", status) : undefined,
|
|
57
|
+
);
|
|
58
|
+
ctx.ui.setWidget("zz-pi-actors-runs", undefined);
|
|
59
|
+
const transitions = Observability.detectRunTransitions(
|
|
60
|
+
observedRuns,
|
|
61
|
+
summary,
|
|
62
|
+
);
|
|
63
|
+
const outboxEvents = Observability.detectRunOutboxEvents(
|
|
64
|
+
observedRunEventLines,
|
|
65
|
+
summary,
|
|
66
|
+
);
|
|
67
|
+
if (!notify) return;
|
|
68
|
+
for (const transition of transitions) {
|
|
69
|
+
if (!Observability.shouldNotifyRunTransition(transition)) continue;
|
|
70
|
+
const text = Observability.formatRunTransitionMessage(transition);
|
|
71
|
+
const notificationType =
|
|
72
|
+
Observability.getRunTransitionNotificationType(transition);
|
|
73
|
+
ctx.ui.notify(text, notificationType);
|
|
74
|
+
if (!Observability.shouldSendRunTransitionFollowUp(transition)) continue;
|
|
75
|
+
pi.sendMessage(
|
|
76
|
+
{
|
|
77
|
+
customType: "pi-actors-run",
|
|
78
|
+
content: text,
|
|
79
|
+
display: true,
|
|
80
|
+
details: transition,
|
|
81
|
+
},
|
|
82
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
for (const event of outboxEvents) {
|
|
86
|
+
if (!Observability.shouldNotifyRunOutboxEvent(event)) continue;
|
|
87
|
+
const text = Observability.formatRunOutboxMessage(event);
|
|
88
|
+
const notificationType =
|
|
89
|
+
Observability.getRunOutboxNotificationType(event);
|
|
90
|
+
ctx.ui.notify(text, notificationType);
|
|
91
|
+
if (!Observability.shouldSendRunOutboxFollowUp(event)) continue;
|
|
92
|
+
pi.sendMessage(
|
|
93
|
+
{
|
|
94
|
+
customType: "pi-actors-run-message",
|
|
95
|
+
content: text,
|
|
96
|
+
display: true,
|
|
97
|
+
details: event,
|
|
98
|
+
},
|
|
99
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const closeRunWatchers = (): void => {
|
|
104
|
+
stateRootWatcher?.close();
|
|
105
|
+
stateRootWatcher = undefined;
|
|
106
|
+
for (const watcher of runDirWatchers.values()) watcher.close();
|
|
107
|
+
runDirWatchers.clear();
|
|
108
|
+
if (runsNotifyTimeout) clearTimeout(runsNotifyTimeout);
|
|
109
|
+
runsNotifyTimeout = undefined;
|
|
110
|
+
};
|
|
111
|
+
const scheduleRunEventUpdate = (ctx: ExtensionContext): void => {
|
|
112
|
+
if (runsNotifyTimeout) clearTimeout(runsNotifyTimeout);
|
|
113
|
+
runsNotifyTimeout = setTimeout(() => {
|
|
114
|
+
refreshRunWatchers(ctx);
|
|
115
|
+
updateRunUi(ctx, true);
|
|
116
|
+
}, 50);
|
|
117
|
+
runsNotifyTimeout.unref?.();
|
|
118
|
+
};
|
|
119
|
+
const watchRunDir = (ctx: ExtensionContext, stateDir: string): void => {
|
|
120
|
+
if (runDirWatchers.has(stateDir) || !existsSync(stateDir)) return;
|
|
121
|
+
try {
|
|
122
|
+
const watcher = watch(stateDir, () => scheduleRunEventUpdate(ctx));
|
|
123
|
+
watcher.on("error", () => {
|
|
124
|
+
watcher.close();
|
|
125
|
+
runDirWatchers.delete(stateDir);
|
|
126
|
+
});
|
|
127
|
+
runDirWatchers.set(stateDir, watcher);
|
|
128
|
+
} catch {
|
|
129
|
+
// Watching is best-effort; explicit inspect remains available.
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function refreshRunWatchers(ctx: ExtensionContext): void {
|
|
133
|
+
if (!existsSync(RUN_STATE_ROOT)) return;
|
|
134
|
+
if (!stateRootWatcher) {
|
|
135
|
+
try {
|
|
136
|
+
stateRootWatcher = watch(RUN_STATE_ROOT, () => scheduleRunEventUpdate(ctx));
|
|
137
|
+
stateRootWatcher.on("error", () => {
|
|
138
|
+
stateRootWatcher?.close();
|
|
139
|
+
stateRootWatcher = undefined;
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
// Watching is best-effort; explicit inspect remains available.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const entry of readdirSync(RUN_STATE_ROOT, { withFileTypes: true })) {
|
|
146
|
+
if (!entry.isDirectory()) continue;
|
|
147
|
+
watchRunDir(ctx, `${RUN_STATE_ROOT}/${entry.name}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const actorToolDefinitions = new Map<string, any>();
|
|
151
|
+
const runtime = Runtime.createAutoToolsRuntime({
|
|
152
|
+
configPath: CONFIG_PATH,
|
|
153
|
+
exec: CommandTemplates.execCommandTemplate,
|
|
154
|
+
getAllTools: () => pi.getAllTools(),
|
|
155
|
+
registerTool: (definition) => {
|
|
156
|
+
actorToolDefinitions.set(definition.name, definition);
|
|
157
|
+
pi.registerTool(definition);
|
|
158
|
+
},
|
|
159
|
+
reservedToolNames: RESERVED_TOOL_NAMES,
|
|
160
|
+
});
|
|
161
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
162
|
+
await Temp.prepareExtensionTempDir(TEMP_DIR);
|
|
163
|
+
runtime.loadTools(ctx);
|
|
164
|
+
updateRunUi(ctx);
|
|
165
|
+
closeRunWatchers();
|
|
166
|
+
refreshRunWatchers(ctx);
|
|
167
|
+
if (runsAnimationInterval) clearInterval(runsAnimationInterval);
|
|
168
|
+
runsAnimationInterval = setInterval(() => updateRunUi(ctx, false), 1000);
|
|
169
|
+
runsAnimationInterval.unref?.();
|
|
170
|
+
});
|
|
171
|
+
pi.on("session_shutdown", async () => {
|
|
172
|
+
if (runsAnimationInterval) clearInterval(runsAnimationInterval);
|
|
173
|
+
runsAnimationInterval = undefined;
|
|
174
|
+
closeRunWatchers();
|
|
175
|
+
});
|
|
176
|
+
pi.on("before_agent_start", async (event) => ({
|
|
177
|
+
systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
|
|
178
|
+
}));
|
|
179
|
+
pi.registerTool(
|
|
180
|
+
Tools.createRegisterToolDefinition<ExtensionContext>({
|
|
181
|
+
configPath: CONFIG_PATH,
|
|
182
|
+
getActiveTools: () => pi.getActiveTools(),
|
|
183
|
+
getExternalToolConflict: runtime.getExternalToolConflict,
|
|
184
|
+
getTools: runtime.getTools,
|
|
185
|
+
notify: runtime.notify,
|
|
186
|
+
registerRuntimeTool: runtime.registerRuntimeTool,
|
|
187
|
+
reservedToolNames: RESERVED_TOOL_NAMES,
|
|
188
|
+
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
pi.registerTool(Tools.createSpawnToolDefinition<ExtensionContext>());
|
|
192
|
+
pi.registerTool(
|
|
193
|
+
Tools.createActorMessageToolDefinition<ExtensionContext>({
|
|
194
|
+
getTool: (name) => actorToolDefinitions.get(name),
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
pi.registerTool(Tools.createInspectToolDefinition());
|
|
198
|
+
}
|