@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
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
function arg(name, fallback = "") {
|
|
7
|
+
const prefix = `--${name}=`;
|
|
8
|
+
const value = process.argv.find((item) => item.startsWith(prefix));
|
|
9
|
+
return value ? value.slice(prefix.length) : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function numberArg(name, fallback) {
|
|
13
|
+
const value = Number(arg(name, String(fallback)));
|
|
14
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function boolArg(name, fallback = false) {
|
|
18
|
+
const value = arg(name, String(fallback)).trim().toLowerCase();
|
|
19
|
+
return ["1", "true", "yes", "on"].includes(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeRoles(value) {
|
|
23
|
+
if (!Array.isArray(value)) throw new Error("roles must be an array");
|
|
24
|
+
return value.map((role, index) => ({
|
|
25
|
+
name: String(role.name ?? `actor-${index + 1}`),
|
|
26
|
+
persona: String(role.persona ?? role.role ?? "room participant"),
|
|
27
|
+
...(role.glyph ? { glyph: String(role.glyph) } : {}),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseRoles(raw) {
|
|
32
|
+
if (!raw.trim()) return defaultRoles;
|
|
33
|
+
try {
|
|
34
|
+
return normalizeRoles(JSON.parse(raw));
|
|
35
|
+
} catch {
|
|
36
|
+
return raw.split(",").map((item, index) => ({
|
|
37
|
+
name: item.trim() || `actor-${index + 1}`,
|
|
38
|
+
persona: "room participant",
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadRoles(raw, path) {
|
|
44
|
+
if (path.trim()) return normalizeRoles(JSON.parse(await readFile(path, "utf8")));
|
|
45
|
+
return parseRoles(raw);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shellQuote(value) {
|
|
49
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function agentDir() {
|
|
53
|
+
return process.env.PI_CODING_AGENT_DIR || `${process.env.HOME}/.pi/agent`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runStateDir(runId) {
|
|
57
|
+
return `${agentDir()}/tmp/pi-actors/runs/${runId}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function sleep(ms) {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function waitForPath(path, timeoutMs = 5000) {
|
|
65
|
+
const started = Date.now();
|
|
66
|
+
while (!existsSync(path)) {
|
|
67
|
+
if (Date.now() - started > timeoutMs) throw new Error(`timed out waiting for ${path}`);
|
|
68
|
+
await sleep(50);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function writeLockerMessage(locker, message) {
|
|
73
|
+
if (!locker) return;
|
|
74
|
+
await waitForPath(locker.controlPath);
|
|
75
|
+
await writeFile(locker.controlPath, `${JSON.stringify(message)}\n`, { flag: "a" });
|
|
76
|
+
await sleep(50);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function startLocker(config) {
|
|
80
|
+
if (!config.locker) return undefined;
|
|
81
|
+
const lockerStateDir = `${runStateDir(config.runId)}/locker`;
|
|
82
|
+
await mkdir(lockerStateDir, { recursive: true });
|
|
83
|
+
const child = spawn(new URL("./coordinator-locker.mjs", import.meta.url).pathname, [
|
|
84
|
+
"serve",
|
|
85
|
+
"--state-dir",
|
|
86
|
+
lockerStateDir,
|
|
87
|
+
"--lease-ms",
|
|
88
|
+
String(config.lockerLeaseMs),
|
|
89
|
+
], {
|
|
90
|
+
env: { ...process.env, run_id: config.runId },
|
|
91
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
92
|
+
});
|
|
93
|
+
child.stdout.on("data", (chunk) => process.stdout.write(`[locker] ${chunk}`));
|
|
94
|
+
child.stderr.on("data", (chunk) => process.stderr.write(`[locker] ${chunk}`));
|
|
95
|
+
const locker = { child, controlPath: `${lockerStateDir}/control.fifo`, stateDir: lockerStateDir };
|
|
96
|
+
await waitForPath(locker.controlPath);
|
|
97
|
+
await writeLockerMessage(locker, {
|
|
98
|
+
type: "coord.enqueue",
|
|
99
|
+
body: { id: "room-swarm-artifact", task: "Own final room-swarm artifact synthesis", resources: [config.artifactPath || "artifact"] },
|
|
100
|
+
});
|
|
101
|
+
return locker;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function stopLocker(locker) {
|
|
105
|
+
if (!locker) return;
|
|
106
|
+
try {
|
|
107
|
+
await writeLockerMessage(locker, { type: "control.stop", body: {} });
|
|
108
|
+
await sleep(100);
|
|
109
|
+
} finally {
|
|
110
|
+
if (!locker.child.killed) locker.child.kill("SIGTERM");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function runPi(prompt, model, thinking) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const child = spawn("pi", [
|
|
117
|
+
"--model", model,
|
|
118
|
+
"--thinking", thinking,
|
|
119
|
+
"--tools", "inspect,message",
|
|
120
|
+
"--no-context-files",
|
|
121
|
+
"--no-skills",
|
|
122
|
+
"--no-session",
|
|
123
|
+
"-p",
|
|
124
|
+
prompt,
|
|
125
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
126
|
+
let stdout = "";
|
|
127
|
+
let stderr = "";
|
|
128
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
129
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
130
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
131
|
+
child.on("error", (error) => resolve({ code: 1, stdout, stderr: String(error) }));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function participant(role, config) {
|
|
136
|
+
const displayName = role.glyph ? `${role.glyph} ${role.name}` : role.name;
|
|
137
|
+
const address = `branch:${config.runId}/${role.name}`;
|
|
138
|
+
const joinPrompt = `You are ${displayName}, ${role.persona}. Mission: ${config.mission}. Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.join', summary='${displayName} joined', body JSON {"role":${JSON.stringify(role.persona)},"display":${JSON.stringify(displayName)},"glyph":${JSON.stringify(role.glyph ?? "")},"caps":["coordination","synthesis"],"claim":"coordinate on mission"}. Then print one short line.`;
|
|
139
|
+
await runPi(joinPrompt, config.model, config.thinking);
|
|
140
|
+
for (let round = 1; round <= config.rounds; round += 1) {
|
|
141
|
+
const prompt = `You are ${displayName} (${address}), ${role.persona}. Mission: ${config.mission}. Round ${round}/${config.rounds}. First call inspect target=${config.room} view=previews lines=30 and inspect target=${config.room} view=contacts. Then call message once to ${config.room} from ${address} type=chat.message. Body: 2-4 sentences that react to a named participant, propose the next coordination step, and refine the shared artifact. Use contacts for peer names and addresses, but keep this packaged swarm's coordination room-visible; do not send direct branch messages unless a caller-specific worker protocol says recipients consume them. End stdout with summary <=160 chars.`;
|
|
142
|
+
const result = await runPi(prompt, config.model, config.thinking);
|
|
143
|
+
process.stdout.write(`[${role.name} round ${round}] code=${result.code}\n${result.stdout}\n`);
|
|
144
|
+
if (result.stderr.trim()) process.stderr.write(`[${role.name} round ${round}] ${result.stderr}\n`);
|
|
145
|
+
if (round < config.rounds && config.delay > 0) await new Promise((resolve) => setTimeout(resolve, config.delay * 1000));
|
|
146
|
+
}
|
|
147
|
+
const leavePrompt = `Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.leave', summary='${displayName} left', body='finished coordinated work'. Then print goodbye.`;
|
|
148
|
+
await runPi(leavePrompt, config.model, config.thinking);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readRoomTranscript(config) {
|
|
152
|
+
const messagesPath = `${agentDir()}/tmp/pi-actors/runs/${config.runId}/rooms/main/messages.jsonl`;
|
|
153
|
+
try {
|
|
154
|
+
const raw = await readFile(messagesPath, "utf8");
|
|
155
|
+
return raw
|
|
156
|
+
.split("\n")
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.slice(-160)
|
|
159
|
+
.map((line) => {
|
|
160
|
+
try {
|
|
161
|
+
const message = JSON.parse(line);
|
|
162
|
+
const from = String(message.from || "unknown").replace(/^branch:[^/]+\//, "");
|
|
163
|
+
const type = String(message.type || "message");
|
|
164
|
+
const body = typeof message.body === "string" ? message.body : JSON.stringify(message.body ?? "");
|
|
165
|
+
return `${from} [${type}]: ${body}`;
|
|
166
|
+
} catch {
|
|
167
|
+
return line;
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.join("\n");
|
|
171
|
+
} catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function synthesize(config, locker) {
|
|
177
|
+
if (!config.artifactPath) return;
|
|
178
|
+
await writeLockerMessage(locker, {
|
|
179
|
+
type: "coord.claim",
|
|
180
|
+
body: { owner: "room-swarm:synthesizer" },
|
|
181
|
+
});
|
|
182
|
+
const transcript = await readRoomTranscript(config);
|
|
183
|
+
const prompt = `Synthesize this room-swarm transcript into a concise Markdown artifact. Mission: ${config.mission}. Include: Title, Consensus, Roles, Protocol, Final Artifact Shape, Next Actions, Open Questions. Use only the transcript evidence below.\n\nTRANSCRIPT:\n${transcript.slice(-24000)}`;
|
|
184
|
+
const result = await runPi(prompt, config.model, config.thinking);
|
|
185
|
+
await writeFile(config.artifactPath, result.stdout.trim() ? result.stdout : "# Room Swarm Artifact\n\nNo synthesis output.\n", "utf8");
|
|
186
|
+
await writeLockerMessage(locker, {
|
|
187
|
+
type: "coord.complete",
|
|
188
|
+
body: { id: "room-swarm-artifact", artifact: config.artifactPath },
|
|
189
|
+
});
|
|
190
|
+
await writeLockerMessage(locker, {
|
|
191
|
+
type: "lock.release",
|
|
192
|
+
body: { resource: config.artifactPath },
|
|
193
|
+
});
|
|
194
|
+
process.stdout.write(`artifact=${config.artifactPath}\n`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const defaultRoles = [
|
|
198
|
+
{ name: "mapper", glyph: "🗺️", persona: "systems mapper; tracks shared structure and dependencies" },
|
|
199
|
+
{ name: "memory", glyph: "🌿", persona: "memory keeper; preserves decisions and unresolved questions" },
|
|
200
|
+
{ name: "risk", glyph: "🧨", persona: "risk scout; challenges weak coordination and missing owners" },
|
|
201
|
+
{ name: "flow", glyph: "🌊", persona: "flow designer; turns scattered ideas into process rhythm" },
|
|
202
|
+
{ name: "operator", glyph: "🔥", persona: "operator; converts ideas into concrete next actions" },
|
|
203
|
+
{ name: "narrative", glyph: "📖", persona: "narrative synthesizer; keeps the artifact coherent" },
|
|
204
|
+
{ name: "interface", glyph: "✨", persona: "interface designer; makes outputs visible and usable" },
|
|
205
|
+
{ name: "facilitator", glyph: "🤫", persona: "facilitator; asks for consensus and convergence" },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const config = {
|
|
209
|
+
runId: arg("run-id", process.env.run_id || process.env.RUN_ID || "room-swarm"),
|
|
210
|
+
mission: arg("mission", "Coordinate a shared artifact and converge on next actions"),
|
|
211
|
+
model: arg("model", ""),
|
|
212
|
+
thinking: arg("thinking", "off"),
|
|
213
|
+
roles: await loadRoles(arg("roles", ""), arg("roles-path", "")),
|
|
214
|
+
rounds: numberArg("rounds", 4),
|
|
215
|
+
delay: numberArg("delay", 10),
|
|
216
|
+
artifactPath: arg("artifact-path", ""),
|
|
217
|
+
locker: boolArg("locker", false),
|
|
218
|
+
lockerLeaseMs: numberArg("locker-lease-ms", 600000),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (!config.model) {
|
|
222
|
+
console.error("--model is required");
|
|
223
|
+
process.exit(2);
|
|
224
|
+
}
|
|
225
|
+
config.room = `room:${config.runId}`;
|
|
226
|
+
|
|
227
|
+
const failures = [];
|
|
228
|
+
const locker = await startLocker(config);
|
|
229
|
+
try {
|
|
230
|
+
await Promise.all(config.roles.map(async (role) => {
|
|
231
|
+
try {
|
|
232
|
+
await participant(role, config);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
failures.push(`${role.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
}
|
|
236
|
+
}));
|
|
237
|
+
await synthesize(config, locker);
|
|
238
|
+
} finally {
|
|
239
|
+
await stopLocker(locker);
|
|
240
|
+
}
|
|
241
|
+
if (failures.length > 0) {
|
|
242
|
+
console.error(failures.join("\n"));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.17.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -37,7 +37,8 @@ Trusted local capability
|
|
|
37
37
|
|
|
38
38
|
- **Command template**: portable execution graph. String leaf, sequence array, or object node with controls.
|
|
39
39
|
- **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
|
|
40
|
-
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, and artifacts.
|
|
40
|
+
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
|
|
41
|
+
- **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
|
|
41
42
|
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`.
|
|
42
43
|
- **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
|
|
43
44
|
- **Mailbox**: public interaction contract: message types the actor accepts/emits.
|
|
@@ -80,7 +81,8 @@ Envelope fields:
|
|
|
80
81
|
|
|
81
82
|
- Required: `to`, `type`.
|
|
82
83
|
- Useful: `summary`, `body`, `from`, `reply_to`, `correlation_id`, `metadata`.
|
|
83
|
-
- Addresses: `run:<id>`, `branch:<run>/<branch>`, `tool:<name>`, `coordinator`, `session:<id>`.
|
|
84
|
+
- Addresses: `run:<id>`, `branch:<run>/<branch>`, `room:<run>`, `tool:<name>`, `coordinator`, `session:<id>`.
|
|
85
|
+
- Room posts require `from` from the same run (`run:<run>` or `branch:<run>/<branch>`).
|
|
84
86
|
- Standard termination messages: `control.stop`, `control.cancel`, `control.kill`.
|
|
85
87
|
|
|
86
88
|
Check `inspect view=mailbox` before domain-specific messages.
|
|
@@ -91,7 +93,12 @@ Check `inspect view=mailbox` before domain-specific messages.
|
|
|
91
93
|
{ "target": "run:repo-health", "view": "status" }
|
|
92
94
|
{ "target": "run:repo-health", "view": "tail", "lines": "80" }
|
|
93
95
|
{ "target": "run:repo-health", "view": "messages" }
|
|
96
|
+
{ "target": "run:repo-health", "view": "communication" }
|
|
94
97
|
{ "target": "run:repo-health", "view": "artifacts" }
|
|
98
|
+
{ "target": "room:repo-health", "view": "status" }
|
|
99
|
+
{ "target": "room:repo-health", "view": "roster" }
|
|
100
|
+
{ "target": "room:repo-health", "view": "contacts" }
|
|
101
|
+
{ "target": "room:repo-health", "view": "previews" }
|
|
95
102
|
{ "target": "tool:music_player", "view": "status" }
|
|
96
103
|
{ "target": "recipes", "view": "status" }
|
|
97
104
|
{ "target": "coordinator", "view": "status" }
|
|
@@ -101,7 +108,11 @@ Views:
|
|
|
101
108
|
|
|
102
109
|
- `status`: lifecycle, pid, values, progress, result, compact summary.
|
|
103
110
|
- `tail`: recent stdout/stderr/log tail.
|
|
104
|
-
- `messages`: actor messages emitted by the run
|
|
111
|
+
- `messages`: actor messages emitted by the run, or room timeline entries for `room:*`.
|
|
112
|
+
- `communication`: run/branch communication snapshot with self/root/default-room/member/contact hints.
|
|
113
|
+
- `roster`: room member list with address, role, parent, caps, claim, status, and last seen.
|
|
114
|
+
- `contacts`: roster-derived direct-message targets without full roster metadata.
|
|
115
|
+
- `previews`: TUI-ready bounded room message previews with timestamp/from/to/type/summary/body_preview.
|
|
105
116
|
- `mailbox`: declared accepts/emits contract.
|
|
106
117
|
- `files`: run state directory file list.
|
|
107
118
|
- `artifacts`: declared artifact paths/status.
|
|
@@ -109,6 +120,38 @@ Views:
|
|
|
109
120
|
|
|
110
121
|
Let terminal notifications arrive; avoid sleep-poll loops except during diagnosis.
|
|
111
122
|
|
|
123
|
+
## Stable Multi-Actor Review Rules
|
|
124
|
+
|
|
125
|
+
- Prefer independent read-only reviewers for review swarms. Use shared room messages for coordination signals and observability, not for letting reviewers converge early, unless the task explicitly asks for collaborative discussion.
|
|
126
|
+
- Treat inspector-visible communication logs as recipe-quality evidence. Verbose room/direct timelines show whether recipes coordinate clearly, emit useful summaries, over-chat, miss handoffs, choose poor message types, or need better mailbox/artifact conventions. Use `inspect room:<run> view=messages|previews`, `inspect run:<id> view=communication`, and the actor inspector compact/verbose modes to improve recipes after real runs.
|
|
127
|
+
- Smoke-test provider/model availability before launching expensive fanout, or choose a provider known to be configured in this environment. A failed provider fanout creates noisy run transitions without useful review signal.
|
|
128
|
+
- Keep one public communication model: `spawn` creates actors, `message` sends typed envelopes, and `inspect` observes. Avoid adding public side channels or storage nouns when a normal actor address/view can express the operation.
|
|
129
|
+
- Keep route and semantic type separate. Direct, room, coordinator, and session messages may share `type`; delivery behavior comes from `to`.
|
|
130
|
+
- Any UI, summary, or aggregate view that scans run directories must apply coordinator/session ownership filters before exposing summaries or body previews.
|
|
131
|
+
- Treat `communication.json` as visible actor context, not a global mutable truth table. Run-level snapshots should identify the run actor; branch-local snapshots should identify the branch actor.
|
|
132
|
+
- Prefer same-run provenance checks on lateral actor routes. If `from` is accepted for room or branch routes, validate that it belongs to the addressed run.
|
|
133
|
+
|
|
134
|
+
## Persistent Backlog Implementers
|
|
135
|
+
|
|
136
|
+
When using actors as backlog implementers, avoid one-shot subagents that exit after one task. Use long-lived branch actors and keep task selection with the coordinator:
|
|
137
|
+
|
|
138
|
+
1. Coordinator assigns a concrete backlog slice with `task.assign`.
|
|
139
|
+
2. Actor posts `task.claim` to `room:<run>` before editing.
|
|
140
|
+
3. Actor executes and validates the slice.
|
|
141
|
+
4. Actor posts `task.result` and `awaiting_assignment`.
|
|
142
|
+
5. Actor stays alive until the coordinator sends another `task.assign` or an explicit `control.stop`.
|
|
143
|
+
|
|
144
|
+
Use `front`/`back` actors for opposite backlog ends when reducing overlap. Implementer workflows should be packaged as reusable recipe composition, not bespoke scripts: use `coordinator-locker` for queue/assignment/locking, subagent launcher recipes for execution cells, and actor-message utility recipes for structured handoffs. If the existing recipe library cannot express the scenario, add missing reusable component recipes first, then compose the higher-level workflow from them. Supervisors should route coordinator assignments by `body.actor`, preserve the assignment as an object rather than a JSON string, and keep stopped-worker summaries tied to the original actor list.
|
|
145
|
+
|
|
146
|
+
Current packaged building blocks:
|
|
147
|
+
|
|
148
|
+
- `coordinator-locker`: long-lived queue/lock coordinator for assignment and resource ownership.
|
|
149
|
+
- `subagent-prompt`, `subagent-tools`, `subagents-prompts`: execution launchers for one or many agent prompts.
|
|
150
|
+
- `utility-actor-message`: deterministic actor-message envelope construction for handoffs/results.
|
|
151
|
+
- `utility-run-ops-snapshot` and `pipeline-async-run-ops`: inspect live runs/messages before deciding the next assignment.
|
|
152
|
+
|
|
153
|
+
The missing higher-level persistent backlog-implementer workflow is intentionally future work until it can be expressed from reusable recipe cells.
|
|
154
|
+
|
|
112
155
|
## Command Template Standard
|
|
113
156
|
|
|
114
157
|
Forms:
|
|
@@ -180,11 +223,11 @@ Priority for same-name recipes:
|
|
|
180
223
|
|
|
181
224
|
Only matching filename ids compete. Higher priority shadows lower priority. An invalid or `disabled: true` higher-priority recipe blocks fallback so the agent does not silently run standard-library behavior when a user override is broken or intentionally disabled.
|
|
182
225
|
|
|
183
|
-
Muscle-memory lens:
|
|
226
|
+
Muscle-memory lens: `~/.pi/agent/recipes/*.json` is the agent's capability memory. Every recipe in that directory becomes an easy-to-call tool automatically and survives into later sessions. Agents grow this memory either by calling `register_tool`, which writes recipe files there under the hood, or by deliberately editing those recipe files. Treat this directory like `MEMORY.md` for executable habits: useful local patterns belong there; packaged recipes elsewhere are reusable components, not tools.
|
|
184
227
|
|
|
185
|
-
Usage lens: user recipes may carry extension-maintained launch metadata such as `usage.calls` and `usage.last_called`. The extension increments the counter when it starts that concrete recipe; agents should not hand-edit counters as part of normal recipe maintenance. Treat usage as evidence for usefulness analysis: heavily used recipes are good candidates for promotion, documentation, or stronger tests; unused recipes are cleanup
|
|
228
|
+
Usage lens: user recipes may carry extension-maintained launch metadata such as `usage.calls` and `usage.last_called`. The extension increments the counter when it starts that concrete recipe; agents should not hand-edit counters as part of normal recipe maintenance. Treat usage as evidence for usefulness analysis: heavily used recipes are good candidates for promotion, documentation, or stronger tests; unused recipes are cleanup candidates. Do not use failure counts as a primary usefulness signal because failures may reflect bad caller judgment rather than bad recipes. Do not delete or demote solely from counters without operator approval.
|
|
186
229
|
|
|
187
|
-
Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memory set. For each stale, duplicate, too-specific, or low-value recipe, choose one explicit action: keep as a tool,
|
|
230
|
+
Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memory set. For each stale, duplicate, too-specific, or low-value recipe, choose one explicit action: keep as a tool, move it out of the agent recipe root to retain recipe-only memory, merge into a better recipe, or delete/archive the file. Prefer moving over deletion when the recipe may still be useful as a component. Never silently remove tools during unrelated work.
|
|
188
231
|
|
|
189
232
|
## Registered Tools
|
|
190
233
|
|
|
@@ -198,7 +241,7 @@ Tool templates may be:
|
|
|
198
241
|
- A file-backed recipe name/path.
|
|
199
242
|
- A complete recipe body, optionally `async: true`.
|
|
200
243
|
|
|
201
|
-
The user recipe root is the default tool set; packaged recipes are
|
|
244
|
+
The user recipe root is the default tool set by location; packaged recipes are lower-priority standard-library components and are not tools unless copied or registered into the agent recipe root. Ideal runtime behavior is reactive: create/edit/delete recipe files, validate them, then connect valid tools or surface diagnostics without requiring agents to hand-maintain a separate registry.
|
|
202
245
|
|
|
203
246
|
## Recipe Navigator
|
|
204
247
|
|
|
@@ -227,7 +270,7 @@ Use packaged recipes by name with `spawn file=<name>` for async actors, or regis
|
|
|
227
270
|
- [`pipeline-docs-maintenance`](../../recipes/pipeline-docs-maintenance.json): docs index/review/planning → maintenance artifact.
|
|
228
271
|
- Artifacts: [`pipeline-artifact-report`](../../recipes/pipeline-artifact-report.json), [`pipeline-artifact-write`](../../recipes/pipeline-artifact-write.json), [`pipeline-artifact-bundle`](../../recipes/pipeline-artifact-bundle.json).
|
|
229
272
|
- Review gates: [`pipeline-quorum-review`](../../recipes/pipeline-quorum-review.json), [`pipeline-review-readiness`](../../recipes/pipeline-review-readiness.json).
|
|
230
|
-
- Task-first workflows: [`pipeline-architect-coordinator`](../../recipes/pipeline-architect-coordinator.json), [`pipeline-research-synthesis`](../../recipes/pipeline-research-synthesis.json), [`pipeline-development-tasking`](../../recipes/pipeline-development-tasking.json), [`pipeline-checkpoint-continuation`](../../recipes/pipeline-checkpoint-continuation.json), [`pipeline-media-library`](../../recipes/pipeline-media-library.json).
|
|
273
|
+
- Task-first workflows: [`pipeline-architect-coordinator`](../../recipes/pipeline-architect-coordinator.json), [`pipeline-research-synthesis`](../../recipes/pipeline-research-synthesis.json), [`pipeline-development-tasking`](../../recipes/pipeline-development-tasking.json), [`pipeline-checkpoint-continuation`](../../recipes/pipeline-checkpoint-continuation.json), [`pipeline-media-library`](../../recipes/pipeline-media-library.json), [`pipeline-room-swarm`](../../recipes/pipeline-room-swarm.json). For room swarms, prefer `roles_path` for custom role JSON; keep role `name` ASCII-safe for branch addresses and use optional `glyph` only as display identity. Use `locker=true` when the swarm needs a coordinator-locker-backed artifact lock and journal.
|
|
231
274
|
|
|
232
275
|
### Utilities
|
|
233
276
|
|
package/skills/swarm/SKILL.md
CHANGED