@llblab/pi-actors 0.34.1 → 0.36.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 +3 -1
- package/BACKLOG.md +4 -90
- package/CHANGELOG.md +14 -2
- package/README.md +12 -2
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +76 -0
- package/dist/lib/prompts.d.ts +1 -0
- package/dist/lib/prompts.js +1 -0
- package/dist/lib/recipes-discovery.d.ts +2 -0
- package/dist/lib/recipes-discovery.js +77 -5
- package/dist/lib/registry.d.ts +3 -0
- package/dist/lib/registry.js +60 -1
- package/dist/lib/runtime-notifier.js +8 -3
- package/dist/lib/tools-inspect.js +172 -4
- package/dist/lib/tools-register.js +1 -0
- package/dist/lib/tools-response.js +13 -2
- package/dist/scripts/validate-recipe.mjs +164 -7
- package/dist/skills/actors/SKILL.md +28 -11
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +2 -0
- package/docs/template-recipes.md +2 -0
- package/docs/tool-registry.md +17 -12
- package/lib/command-templates.ts +124 -0
- package/lib/prompts.ts +2 -0
- package/lib/recipes-discovery.ts +98 -6
- package/lib/registry.ts +94 -1
- package/lib/runtime-notifier.ts +9 -3
- package/lib/tools-inspect.ts +186 -4
- package/lib/tools-register.ts +1 -0
- package/lib/tools-response.ts +16 -2
- package/package.json +3 -2
- package/scripts/validate-recipe.mjs +164 -7
- package/skills/actors/SKILL.md +28 -11
- package/skills/swarm/SKILL.md +1 -1
|
@@ -212,6 +212,170 @@ function getPiActorsRuntimeStatus() {
|
|
|
212
212
|
function compactPiActorsRuntimeStatus(status) {
|
|
213
213
|
return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
|
|
214
214
|
}
|
|
215
|
+
function isStaleClaim(message, now) {
|
|
216
|
+
if (message.status !== "claimed")
|
|
217
|
+
return false;
|
|
218
|
+
const claimedAt = Date.parse(String(message.claimed_at ?? ""));
|
|
219
|
+
return Number.isFinite(claimedAt) && now - claimedAt > 5 * 60 * 1000;
|
|
220
|
+
}
|
|
221
|
+
function getRunTriageSignals(runs) {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const staleClaims = [];
|
|
224
|
+
const attentionMessages = [];
|
|
225
|
+
for (const run of runs) {
|
|
226
|
+
const stateDir = String(run.state_dir ?? "");
|
|
227
|
+
const runId = String(run.run ?? "");
|
|
228
|
+
if (!stateDir)
|
|
229
|
+
continue;
|
|
230
|
+
try {
|
|
231
|
+
for (const message of AsyncRuns.readRunInboxMessages(stateDir, 200)) {
|
|
232
|
+
if (!isStaleClaim(message, now))
|
|
233
|
+
continue;
|
|
234
|
+
staleClaims.push({
|
|
235
|
+
run: runId,
|
|
236
|
+
id: message.id,
|
|
237
|
+
claimed_at: message.claimed_at,
|
|
238
|
+
claimed_by: message.claimed_by,
|
|
239
|
+
type: message.type,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
244
|
+
try {
|
|
245
|
+
for (const event of AsyncRuns.readRunEvents(stateDir, 80)) {
|
|
246
|
+
if (event.metadata?.requires_response !== true)
|
|
247
|
+
continue;
|
|
248
|
+
attentionMessages.push({
|
|
249
|
+
run: runId,
|
|
250
|
+
id: event.id,
|
|
251
|
+
summary: event.summary,
|
|
252
|
+
type: event.type ?? event.event,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
257
|
+
}
|
|
258
|
+
return { attention_messages: attentionMessages, stale_claims: staleClaims };
|
|
259
|
+
}
|
|
260
|
+
function isTriageHighRiskRecipe(recipe) {
|
|
261
|
+
if (recipe.tool !== true)
|
|
262
|
+
return false;
|
|
263
|
+
const labels = Array.isArray(recipe.risk_labels)
|
|
264
|
+
? recipe.risk_labels.map((label) => String(label))
|
|
265
|
+
: [];
|
|
266
|
+
return labels.some((label) => label !== "risk.long_running");
|
|
267
|
+
}
|
|
268
|
+
function getPiActorsTriage(ctx, deps) {
|
|
269
|
+
const runtime = getPiActorsRuntimeStatus();
|
|
270
|
+
const currentSession = ToolsAccess.getContextSessionId(ctx);
|
|
271
|
+
const allRuns = AsyncRuns.listRuns().map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
|
|
272
|
+
const visibleRuns = currentSession
|
|
273
|
+
? allRuns.filter((run) => !run.ownerId || run.ownerId === currentSession)
|
|
274
|
+
: allRuns;
|
|
275
|
+
const activeRuns = visibleRuns.filter((run) => run.status === "running");
|
|
276
|
+
const failedRuns = visibleRuns.filter((run) => run.status === "failed");
|
|
277
|
+
const otherRuns = currentSession
|
|
278
|
+
? allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession)
|
|
279
|
+
: [];
|
|
280
|
+
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
281
|
+
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
282
|
+
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
283
|
+
{ root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
|
|
284
|
+
]);
|
|
285
|
+
const recipeSummary = {
|
|
286
|
+
...RecipesDiscovery.summarizeDiscovery(discovered),
|
|
287
|
+
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
288
|
+
};
|
|
289
|
+
const activeRecipes = Array.isArray(recipeSummary.active)
|
|
290
|
+
? recipeSummary.active
|
|
291
|
+
: [];
|
|
292
|
+
const highRiskRecipes = activeRecipes.filter(isTriageHighRiskRecipe);
|
|
293
|
+
const signals = getRunTriageSignals(visibleRuns);
|
|
294
|
+
const attentionMessages = signals.attention_messages;
|
|
295
|
+
const staleClaims = signals.stale_claims;
|
|
296
|
+
const invalidRecipes = Array.isArray(recipeSummary.invalid)
|
|
297
|
+
? recipeSummary.invalid
|
|
298
|
+
: [];
|
|
299
|
+
const remediations = Array.isArray(recipeSummary.remediations)
|
|
300
|
+
? recipeSummary.remediations
|
|
301
|
+
: [];
|
|
302
|
+
const drafts = Array.isArray(recipeSummary.drafts)
|
|
303
|
+
? recipeSummary.drafts
|
|
304
|
+
: [];
|
|
305
|
+
const nextActions = [
|
|
306
|
+
invalidRecipes.length || remediations.length
|
|
307
|
+
? "inspect target=recipes view=doctor"
|
|
308
|
+
: "",
|
|
309
|
+
drafts.length ? "inspect target=recipes view=summary verbose=true" : "",
|
|
310
|
+
failedRuns[0]?.run
|
|
311
|
+
? `inspect target=run:${String(failedRuns[0].run)} view=tail lines=80`
|
|
312
|
+
: "",
|
|
313
|
+
attentionMessages[0]?.run
|
|
314
|
+
? `inspect target=run:${String(attentionMessages[0].run)} view=messages`
|
|
315
|
+
: "",
|
|
316
|
+
activeRuns.length
|
|
317
|
+
? "inspect target=session:all view=runs status=active"
|
|
318
|
+
: "",
|
|
319
|
+
].filter(Boolean);
|
|
320
|
+
return {
|
|
321
|
+
runtime,
|
|
322
|
+
current_session: currentSession ?? null,
|
|
323
|
+
active_runs: activeRuns.map((run) => ({
|
|
324
|
+
run: run.run,
|
|
325
|
+
ownerId: run.ownerId,
|
|
326
|
+
recipe: run.recipe,
|
|
327
|
+
status: run.status,
|
|
328
|
+
})),
|
|
329
|
+
other_session_runs: otherRuns.length,
|
|
330
|
+
invalid_recipes: invalidRecipes,
|
|
331
|
+
blocking_recipes: remediations.filter((item) => String(item.kind ?? "").startsWith("blocking_")),
|
|
332
|
+
high_risk_recipes: highRiskRecipes.map((recipe) => ({
|
|
333
|
+
id: recipe.id,
|
|
334
|
+
path: recipe.path,
|
|
335
|
+
risk_labels: recipe.risk_labels,
|
|
336
|
+
})),
|
|
337
|
+
draft_recipes: drafts,
|
|
338
|
+
stale_claims: staleClaims,
|
|
339
|
+
recent_failed_runs: failedRuns.slice(0, 5).map((run) => ({
|
|
340
|
+
run: run.run,
|
|
341
|
+
recipe: run.recipe,
|
|
342
|
+
status: run.status,
|
|
343
|
+
})),
|
|
344
|
+
attention_messages: attentionMessages.slice(-10),
|
|
345
|
+
next_actions: [...new Set(nextActions)].slice(0, 5),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function compactPiActorsTriage(summary) {
|
|
349
|
+
const runtime = asRecord(summary.runtime);
|
|
350
|
+
const activeRuns = Array.isArray(summary.active_runs)
|
|
351
|
+
? summary.active_runs.length
|
|
352
|
+
: 0;
|
|
353
|
+
const invalidRecipes = Array.isArray(summary.invalid_recipes)
|
|
354
|
+
? summary.invalid_recipes.length
|
|
355
|
+
: 0;
|
|
356
|
+
const blockingRecipes = Array.isArray(summary.blocking_recipes)
|
|
357
|
+
? summary.blocking_recipes.length
|
|
358
|
+
: 0;
|
|
359
|
+
const highRiskRecipes = Array.isArray(summary.high_risk_recipes)
|
|
360
|
+
? summary.high_risk_recipes.length
|
|
361
|
+
: 0;
|
|
362
|
+
const drafts = Array.isArray(summary.draft_recipes)
|
|
363
|
+
? summary.draft_recipes.length
|
|
364
|
+
: 0;
|
|
365
|
+
const staleClaims = Array.isArray(summary.stale_claims)
|
|
366
|
+
? summary.stale_claims.length
|
|
367
|
+
: 0;
|
|
368
|
+
const failedRuns = Array.isArray(summary.recent_failed_runs)
|
|
369
|
+
? summary.recent_failed_runs.length
|
|
370
|
+
: 0;
|
|
371
|
+
const attention = Array.isArray(summary.attention_messages)
|
|
372
|
+
? summary.attention_messages.length
|
|
373
|
+
: 0;
|
|
374
|
+
const nextActions = Array.isArray(summary.next_actions)
|
|
375
|
+
? summary.next_actions
|
|
376
|
+
: [];
|
|
377
|
+
return `\ntriage version=${String(runtime.version ?? "unknown")} mode=${String(runtime.mode ?? "unknown")} active_runs=${activeRuns} other_runs=${String(summary.other_session_runs ?? 0)} invalid_recipes=${invalidRecipes} blocking_recipes=${blockingRecipes} high_risk_recipes=${highRiskRecipes} drafts=${drafts} stale_claims=${staleClaims} failed_runs=${failedRuns} attention=${attention}${ToolsResponse.compactNextActions(nextActions)}`;
|
|
378
|
+
}
|
|
215
379
|
function compactToolActor(name, tool) {
|
|
216
380
|
const parameters = asRecord(tool.parameters);
|
|
217
381
|
const required = Array.isArray(parameters.required)
|
|
@@ -314,15 +478,19 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
314
478
|
}
|
|
315
479
|
if (address.kind === "tool" && address.value) {
|
|
316
480
|
if (address.value === "pi-actors") {
|
|
317
|
-
if (view !== "status") {
|
|
318
|
-
throw new Error("inspect tool:pi-actors supports view=status.");
|
|
481
|
+
if (view !== "status" && view !== "triage") {
|
|
482
|
+
throw new Error("inspect tool:pi-actors supports view=status or view=triage.");
|
|
319
483
|
}
|
|
320
|
-
const details =
|
|
484
|
+
const details = view === "triage"
|
|
485
|
+
? getPiActorsTriage(ctx, deps)
|
|
486
|
+
: getPiActorsRuntimeStatus();
|
|
321
487
|
return {
|
|
322
488
|
content: [
|
|
323
489
|
{
|
|
324
490
|
type: "text",
|
|
325
|
-
text: maybeJsonText(details, input.verbose === true,
|
|
491
|
+
text: maybeJsonText(details, input.verbose === true, view === "triage"
|
|
492
|
+
? compactPiActorsTriage(details)
|
|
493
|
+
: compactPiActorsRuntimeStatus(details)),
|
|
326
494
|
},
|
|
327
495
|
],
|
|
328
496
|
details,
|
|
@@ -24,6 +24,7 @@ export function createRegisterToolDefinition(deps) {
|
|
|
24
24
|
args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
|
|
25
25
|
async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
|
|
26
26
|
description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
|
|
27
|
+
draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
|
|
27
28
|
name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
|
|
28
29
|
state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
|
|
29
30
|
template: unionSchema([
|
|
@@ -143,15 +143,23 @@ export function compactRecipeDoctor(summary) {
|
|
|
143
143
|
const recommendations = Array.isArray(summary.recommendations)
|
|
144
144
|
? summary.recommendations
|
|
145
145
|
: [];
|
|
146
|
+
const riskSummary = Array.isArray(summary.risk_summary)
|
|
147
|
+
? summary.risk_summary
|
|
148
|
+
: [];
|
|
146
149
|
const counts = { error: 0, info: 0, warning: 0 };
|
|
147
150
|
for (const detail of details) {
|
|
148
151
|
const severity = String(detail.severity ?? "info");
|
|
149
152
|
if (severity === "error" || severity === "warning" || severity === "info")
|
|
150
153
|
counts[severity] += 1;
|
|
151
154
|
}
|
|
155
|
+
const riskCount = riskSummary.reduce((total, item) => total + Number(item.count ?? 0), 0);
|
|
156
|
+
const topRisks = riskSummary
|
|
157
|
+
.slice(0, 4)
|
|
158
|
+
.map((item) => `${String(item.label)}:${String(item.count ?? 0)}`)
|
|
159
|
+
.join(",");
|
|
152
160
|
const topAction = asRecord(summary.top_action);
|
|
153
161
|
const lines = [
|
|
154
|
-
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
|
|
162
|
+
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length} risks=${riskCount}${topRisks ? ` top_risks=${topRisks}` : ""}`,
|
|
155
163
|
];
|
|
156
164
|
if (Object.keys(topAction).length > 0) {
|
|
157
165
|
const action = compactPreview(topAction.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS);
|
|
@@ -162,7 +170,10 @@ export function compactRecipeDoctor(summary) {
|
|
|
162
170
|
const blocked = item.blocked_fallback
|
|
163
171
|
? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
|
|
164
172
|
: "";
|
|
165
|
-
|
|
173
|
+
const labels = Array.isArray(item.risk_labels)
|
|
174
|
+
? ` labels=${item.risk_labels.map(String).slice(0, 4).join(",")}`
|
|
175
|
+
: "";
|
|
176
|
+
lines.push(`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked}${labels} action=${action ?? "inspect"}`);
|
|
166
177
|
}
|
|
167
178
|
const nextActions = Array.isArray(summary.next_actions)
|
|
168
179
|
? summary.next_actions
|
|
@@ -27,9 +27,9 @@ const { readResolvedRecipeConfig } = await importRuntimeModule("recipes-referenc
|
|
|
27
27
|
|
|
28
28
|
export function validateRecipeUsage() {
|
|
29
29
|
return `Usage:
|
|
30
|
-
validate-recipe.mjs <recipe-file-or-dir> [--all]
|
|
30
|
+
validate-recipe.mjs <recipe-file-or-dir> [--all] [--qa] [--summary]
|
|
31
31
|
|
|
32
|
-
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set.`;
|
|
32
|
+
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks. Add --summary for compact CLI output.`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function expandPath(value) {
|
|
@@ -63,14 +63,133 @@ function recipeFiles(target, all) {
|
|
|
63
63
|
.map((file) => resolve(target, file));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function
|
|
66
|
+
function mailboxType(value) {
|
|
67
|
+
if (typeof value === "string") return value;
|
|
68
|
+
if (value && typeof value === "object" && typeof value.type === "string")
|
|
69
|
+
return value.type;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectTemplateStrings(value, out = []) {
|
|
74
|
+
if (typeof value === "string") out.push(value);
|
|
75
|
+
else if (Array.isArray(value)) value.forEach((item) => collectTemplateStrings(item, out));
|
|
76
|
+
else if (value && typeof value === "object") {
|
|
77
|
+
collectTemplateStrings(value.template, out);
|
|
78
|
+
collectTemplateStrings(value.recover, out);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasPlatformNote(config) {
|
|
84
|
+
const text = [
|
|
85
|
+
config.description,
|
|
86
|
+
config.platforms,
|
|
87
|
+
config.platform_notes,
|
|
88
|
+
config.requirements,
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" ");
|
|
92
|
+
return /linux|macos|darwin|windows|win32|unix|wsl|cross-platform|portable/i.test(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateArtifactDeclarations(config) {
|
|
96
|
+
const diagnostics = [];
|
|
97
|
+
const artifacts = config.artifacts;
|
|
98
|
+
if (artifacts === undefined) return diagnostics;
|
|
99
|
+
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts)) {
|
|
100
|
+
diagnostics.push("artifacts: must be an object of named artifact paths");
|
|
101
|
+
return diagnostics;
|
|
102
|
+
}
|
|
103
|
+
for (const [name, value] of Object.entries(artifacts)) {
|
|
104
|
+
const path = typeof value === "string" ? value : value?.path;
|
|
105
|
+
if (typeof path !== "string" || !path.trim())
|
|
106
|
+
diagnostics.push(`artifacts.${name}: must declare a non-empty path`);
|
|
107
|
+
if (typeof path === "string" && /^\/home\//.test(path))
|
|
108
|
+
diagnostics.push(`artifacts.${name}: must not use a machine-local absolute path`);
|
|
109
|
+
}
|
|
110
|
+
return diagnostics;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateHelperPaths(file, config) {
|
|
114
|
+
const diagnostics = [];
|
|
115
|
+
for (const template of collectTemplateStrings(config.template)) {
|
|
116
|
+
if (/(^|\s)(?:node\s+)?scripts\/[\w.-]+\.mjs/.test(template))
|
|
117
|
+
diagnostics.push("template: helper scripts must be referenced through {repo}/scripts for installed packages");
|
|
118
|
+
for (const match of template.matchAll(/\{repo\}\/scripts\/([^\s"']+\.mjs)/g)) {
|
|
119
|
+
const scriptPath = join(packageRoot(), "scripts", match[1]);
|
|
120
|
+
if (!existsSync(scriptPath))
|
|
121
|
+
diagnostics.push(`template: referenced helper script not found: scripts/${match[1]}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return diagnostics;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validateMailboxContract(file, config) {
|
|
128
|
+
const diagnostics = [];
|
|
129
|
+
const mailbox = config.mailbox;
|
|
130
|
+
const accepts = Array.isArray(mailbox?.accepts) ? mailbox.accepts : [];
|
|
131
|
+
const emits = Array.isArray(mailbox?.emits) ? mailbox.emits : [];
|
|
132
|
+
if (config.async === true && !mailbox)
|
|
133
|
+
diagnostics.push("mailbox: async recipes must declare mailbox metadata");
|
|
134
|
+
if (config.async === true && !accepts.map(mailboxType).includes("control.kill"))
|
|
135
|
+
diagnostics.push("mailbox.accepts: async recipes must include control.kill");
|
|
136
|
+
const allowedLegacyTypes = new Set(["awaiting_assignment"]);
|
|
137
|
+
for (const [key, entries] of Object.entries({ accepts, emits })) {
|
|
138
|
+
entries.forEach((entry, index) => {
|
|
139
|
+
const type = mailboxType(entry);
|
|
140
|
+
if (!type) diagnostics.push(`mailbox.${key}[${index}]: must be a string or object with type`);
|
|
141
|
+
else if (
|
|
142
|
+
!allowedLegacyTypes.has(type) &&
|
|
143
|
+
!/^[a-z][a-z0-9_-]*\.(?:[a-z][a-z0-9_-]*|\*)$/.test(type)
|
|
144
|
+
)
|
|
145
|
+
diagnostics.push(`mailbox.${key}[${index}]: message type must use channel.action form`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const allowedDomainTermination = new Set([
|
|
149
|
+
"coordinator-locker.json",
|
|
150
|
+
"locker.json",
|
|
151
|
+
"music-player.json",
|
|
152
|
+
]);
|
|
153
|
+
const hasDomainTermination = accepts
|
|
154
|
+
.map(mailboxType)
|
|
155
|
+
.some((type) => type === "control.stop" || type === "control.cancel");
|
|
156
|
+
if (hasDomainTermination && !allowedDomainTermination.has(file.split(/[\\/]/).pop()))
|
|
157
|
+
diagnostics.push("mailbox.accepts: control.stop/control.cancel are reserved for actor-domain handlers");
|
|
158
|
+
return diagnostics;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function qaDiagnostics(file, config) {
|
|
162
|
+
const diagnostics = [];
|
|
163
|
+
const warnings = [];
|
|
164
|
+
if (typeof config.description !== "string" || !config.description.trim())
|
|
165
|
+
warnings.push("description: missing or empty");
|
|
166
|
+
diagnostics.push(...validateMailboxContract(file, config));
|
|
167
|
+
diagnostics.push(...validateArtifactDeclarations(config));
|
|
168
|
+
diagnostics.push(...validateHelperPaths(file, config));
|
|
169
|
+
const platformSpecificTemplate = collectTemplateStrings(config.template).some(
|
|
170
|
+
(template) =>
|
|
171
|
+
/(^|\s)(?:systemctl|launchctl|osascript|powershell|pwsh|cmd\.exe|apt|apt-get|dnf|yum|brew|pacman|apk)(\s|$)/i.test(
|
|
172
|
+
template,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
if (platformSpecificTemplate && !hasPlatformNote(config))
|
|
176
|
+
diagnostics.push("platform: platform-specific templates must document platform scope");
|
|
177
|
+
return { diagnostics, warnings };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function qaOk(qaReport) {
|
|
181
|
+
return qaReport.diagnostics.length === 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateFile(file, qa = false) {
|
|
67
185
|
try {
|
|
68
186
|
const config = readResolvedRecipeConfig(file);
|
|
69
187
|
if (!config?.template)
|
|
70
188
|
throw new Error("Recipe must define a non-empty template.");
|
|
189
|
+
const qaReport = qa ? qaDiagnostics(file, config) : { diagnostics: [], warnings: [] };
|
|
71
190
|
return {
|
|
72
191
|
file,
|
|
73
|
-
ok:
|
|
192
|
+
ok: qaOk(qaReport),
|
|
74
193
|
name: config.name ?? "",
|
|
75
194
|
async: Boolean(config.async),
|
|
76
195
|
args: Array.isArray(config.args) ? config.args : [],
|
|
@@ -93,6 +212,15 @@ function validateFile(file) {
|
|
|
93
212
|
: [],
|
|
94
213
|
}
|
|
95
214
|
: undefined,
|
|
215
|
+
...(qa
|
|
216
|
+
? {
|
|
217
|
+
qa: {
|
|
218
|
+
ok: qaOk(qaReport),
|
|
219
|
+
diagnostics: qaReport.diagnostics,
|
|
220
|
+
warnings: qaReport.warnings,
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
96
224
|
template: templateKind(config.template),
|
|
97
225
|
};
|
|
98
226
|
} catch (error) {
|
|
@@ -107,12 +235,13 @@ function validateFile(file) {
|
|
|
107
235
|
export function validateRecipes(argv) {
|
|
108
236
|
const targetArg = argv.find((arg) => !arg.startsWith("-"));
|
|
109
237
|
const all = argv.includes("--all");
|
|
238
|
+
const qa = argv.includes("--qa");
|
|
110
239
|
if (!targetArg || argv.includes("--help") || argv.includes("-h")) {
|
|
111
240
|
return { help: true, ok: Boolean(targetArg), usage: validateRecipeUsage() };
|
|
112
241
|
}
|
|
113
242
|
|
|
114
243
|
const files = recipeFiles(expandPath(targetArg), all);
|
|
115
|
-
const results = files.map(validateFile);
|
|
244
|
+
const results = files.map((file) => validateFile(file, qa));
|
|
116
245
|
const failed = results.filter((result) => !result.ok).length;
|
|
117
246
|
return {
|
|
118
247
|
ok: failed === 0,
|
|
@@ -123,10 +252,38 @@ export function validateRecipes(argv) {
|
|
|
123
252
|
};
|
|
124
253
|
}
|
|
125
254
|
|
|
255
|
+
function summarizeReport(report) {
|
|
256
|
+
const results = Array.isArray(report.results) ? report.results : [];
|
|
257
|
+
return {
|
|
258
|
+
ok: report.ok,
|
|
259
|
+
total: report.total,
|
|
260
|
+
passed: report.passed,
|
|
261
|
+
failed: report.failed,
|
|
262
|
+
diagnostics: results.reduce(
|
|
263
|
+
(total, result) => total + (result.qa?.diagnostics?.length ?? 0),
|
|
264
|
+
0,
|
|
265
|
+
),
|
|
266
|
+
warnings: results.reduce(
|
|
267
|
+
(total, result) => total + (result.qa?.warnings?.length ?? 0),
|
|
268
|
+
0,
|
|
269
|
+
),
|
|
270
|
+
failed_files: results
|
|
271
|
+
.filter((result) => !result.ok)
|
|
272
|
+
.map((result) => ({
|
|
273
|
+
file: result.file,
|
|
274
|
+
...(result.error ? { error: result.error } : {}),
|
|
275
|
+
...(result.qa?.diagnostics?.length
|
|
276
|
+
? { diagnostics: result.qa.diagnostics }
|
|
277
|
+
: {}),
|
|
278
|
+
})),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
126
282
|
try {
|
|
127
|
-
const
|
|
283
|
+
const argv = process.argv.slice(2);
|
|
284
|
+
const report = validateRecipes(argv);
|
|
128
285
|
if (report.help) console.error(report.usage);
|
|
129
|
-
else console.log(JSON.stringify(report, null, 2));
|
|
286
|
+
else console.log(JSON.stringify(argv.includes("--summary") ? summarizeReport(report) : report, null, 2));
|
|
130
287
|
process.exit(report.ok ? 0 : 1);
|
|
131
288
|
} catch (error) {
|
|
132
289
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use. Read before using or changing 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.36.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -194,12 +194,13 @@ Rules:
|
|
|
194
194
|
2. `async: true` makes spawned work a detached actor run.
|
|
195
195
|
3. Public knobs belong in `args`/`defaults`; hidden launch mechanics stay inside `template`.
|
|
196
196
|
4. Use `imports` to compose recipes; imported recipes are definitions, not nested async runs.
|
|
197
|
-
5.
|
|
198
|
-
6. Declare `
|
|
199
|
-
7.
|
|
200
|
-
8. File-backed
|
|
201
|
-
9.
|
|
202
|
-
10.
|
|
197
|
+
5. When exposing an already-authored recipe as a user tool, make a small wrapper recipe in `~/.pi/agent/recipes` that imports the source recipe and uses a `{ "name": "alias" }` node. Do not copy the ready recipe's script command, defaults, mailbox, or artifacts into a second template.
|
|
198
|
+
6. Declare `mailbox` for actors that accept or emit meaningful messages.
|
|
199
|
+
7. Declare `artifacts` for durable outputs the coordinator should inspect.
|
|
200
|
+
8. File-backed recipe identity comes from the filename basename; legacy top-level `name` fields are ignored by loaders.
|
|
201
|
+
9. File-backed async recipes pass child `pi -p` actors a bounded JSONL recipe context bundle by default: raw entry/import recipe records, derived `name`, import path/alias, and `"you_are_here": true` on the launching recipe node. Set `"actor_context": false` or `"off"` to suppress it for minimal prompts.
|
|
202
|
+
10. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
|
|
203
|
+
11. Do not ship concrete model-version defaults in packaged recipes; expose `model`, `models`, and stage-specific model args so the caller must choose current policy at launch.
|
|
203
204
|
|
|
204
205
|
Priority for same-id recipes:
|
|
205
206
|
|
|
@@ -227,16 +228,32 @@ Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memo
|
|
|
227
228
|
|
|
228
229
|
`register_tool` persists trusted local capabilities as recipe files in `~/.pi/agent/recipes/*.json`; hand-authored Markdown recipes in the same directory are also discovered as tools.
|
|
229
230
|
|
|
230
|
-
Use it when a command/template/recipe should become durable agent muscle memory. Prefer typed args or placeholder-derived args; use `update=true` for replacement and `template=null` or `template=""` for deletion. `register_tool` should create/update/delete recipe files in the user recipe root; direct file editing is
|
|
231
|
+
Use it when a command/template/recipe should become durable agent muscle memory. Prefer typed args or placeholder-derived args; use `update=true` for replacement and `template=null` or `template=""` for deletion. `register_tool` should create/update/delete simple recipe files in the user recipe root; direct recipe-file editing is the right path when the wrapper needs `imports` or other top-level recipe metadata not exposed by the interactive mutation API.
|
|
232
|
+
|
|
233
|
+
Ready-recipe registration pattern:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"description": "Run the ABCd context validator through its skill recipe.",
|
|
238
|
+
"imports": {
|
|
239
|
+
"validate_context": "{agent}/skills/abcd-context/recipes/validate-context.json"
|
|
240
|
+
},
|
|
241
|
+
"args": ["path:path=."],
|
|
242
|
+
"template": { "name": "validate_context" }
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Use this pattern whenever a reusable recipe already exists: packaged pi-actors components, project-local recipes, ad hoc reviewed recipe files, and especially skill-owned recipes that wrap skill scripts. The wrapper owns only the public tool name, description, optional narrowed args/defaults, and local usage metadata. The imported recipe remains the source of truth for the script path, default values, mailbox contract, artifacts, and future fixes.
|
|
231
247
|
|
|
232
248
|
Tool-registration lenses are open-ended prompts for deciding what deserves durable tool status:
|
|
233
249
|
|
|
234
250
|
1. **Reliability lens**: register wrappers for operations where agents commonly omit checks, run steps out of order, pass ambiguous inputs, or recover poorly from partial failure.
|
|
235
251
|
2. **Safety lens**: prefer read-only diagnostics, dry-runs, preflights, confirmations, or bounded adapters around high-impact operations before registering direct action tools.
|
|
236
252
|
3. **Context-affordance lens**: register tools whose mere presence in the injected capability list should steer agents toward the right operational habit.
|
|
237
|
-
4. **Existing-recipe lens**: scan already-authored recipes before inventing a new tool. Packaged recipes, ad hoc project recipes, and recipes co-located under skill directories are
|
|
238
|
-
5. **
|
|
239
|
-
6. **
|
|
253
|
+
4. **Existing-recipe lens**: scan already-authored recipes before inventing a new tool. Packaged recipes, ad hoc project recipes, and recipes co-located under skill directories are the first candidates to import from a user-root wrapper when they match a recurring local workflow.
|
|
254
|
+
5. **Skill-recipe lens**: when a skill ships a recipe for its script, local tools must import that recipe instead of calling the skill script directly. This preserves the skill's maintained interface and keeps future script/default changes centralized.
|
|
255
|
+
6. **Composition lens**: register small semantic entrypoints over reusable recipe components instead of baking one large scenario-specific shell command into a tool.
|
|
256
|
+
7. **Portability lens**: keep recipe files transportable; make tool exposure a consequence of placement in `~/.pi/agent/recipes`, not recipe-owned markers or machine-local assumptions.
|
|
240
257
|
|
|
241
258
|
Default bias: register diagnostic/preflight tools before action tools, and promote existing recipes before writing new orchestration. A good persistent tool shrinks the chance of a subtle operational mistake, not just the number of keystrokes.
|
|
242
259
|
|
package/docs/recipe-library.md
CHANGED
|
@@ -115,6 +115,8 @@ Utility recipes cover local operator workflows that do not need subagents:
|
|
|
115
115
|
- `recipes/utility-skill-summary.json`: Use `scripts/recipe-utils.mjs` to summarize packaged skill frontmatter, body shape, formatter-safe scalar lines, and package-version alignment.
|
|
116
116
|
- `recipes/utility-validate-recipe.json`: Use `scripts/validate-recipe.mjs` to validate one template recipe file, or all packaged recipes in a directory with `all: true`.
|
|
117
117
|
|
|
118
|
+
Packaged QA is available through the `recipes:qa` npm script. It reports description warnings and fails exact diagnostics for async mailbox contracts, termination vocabulary, artifact paths, platform scope, helper script paths, and missing helper scripts.
|
|
119
|
+
|
|
118
120
|
These recipes are intentionally small. Register them only for trusted local commands and prefer narrow scopes. Discovery diagnostics flag obvious trust-boundary shapes such as shell/eval/destructive commands; those warnings are operator review aids, not a sandbox. The helper-backed utilities share `scripts/recipe-utils.mjs` so repeated parsing/listing logic stays out of recipe strings.
|
|
119
121
|
|
|
120
122
|
## Actor OS Smoke Matrix
|
package/docs/template-recipes.md
CHANGED
|
@@ -292,6 +292,8 @@ An import binding may be either a string recipe path/name or an object with:
|
|
|
292
292
|
|
|
293
293
|
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.
|
|
294
294
|
|
|
295
|
+
Use imports as the default adapter for exposing ready recipes as local tools. A user-root recipe such as `~/.pi/agent/recipes/repo_context_check.json` should import the maintained source recipe and call `{ "name": "alias" }`, not duplicate the source recipe's script command. Skill scripts are the strongest version of this rule: when a skill provides `recipes/<name>.json` for its `scripts/*` entrypoint, local tools must import the skill recipe via `{agent}/skills/<skill>/recipes/<name>.json` instead of invoking the script path directly.
|
|
296
|
+
|
|
295
297
|
Async composition stays explicit: importing a recipe reuses its command-template-shaped definition. It does not start a nested async run. Put `async: true` on the parent recipe when the combined imported graph should run detached as one run with one state dir. Ephemeral coordinator recipes may declare `retire_when: "children_terminal"` as an opt-in lifecycle hint for future graceful retirement handling; persistent services and implementer loops should omit it. For agent-callable fanout, prefer public inputs such as `prompts:array` plus `repeat: "{prompts.length}"`, then select each branch value with `{prompts[index]}` instead of baking concrete prompts or file names into the reusable recipe.
|
|
296
298
|
|
|
297
299
|
```json
|
package/docs/tool-registry.md
CHANGED
|
@@ -10,7 +10,7 @@ The registry source is location-discovered recipes, not a live tool-only JSON fi
|
|
|
10
10
|
|
|
11
11
|
- `~/.pi/agent/recipes/*.json` and `*.md` are the highest-priority user recipe root and the operator-managed tool set.
|
|
12
12
|
- Recipes in that root are tools by location.
|
|
13
|
-
- `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one
|
|
13
|
+
- `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one with `register_tool name=<tool_name> draft=<draft_path>` or by manually moving/copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths, timestamps, fingerprints, validation state, source run when known, descriptions, and template previews for explicit replay or promotion.
|
|
14
14
|
- Packaged pi-actors recipes are the lower-priority standard library of declarative actor config components, not automatically registered tools.
|
|
15
15
|
- Ad hoc recipe files outside the user recipe root are components unless explicitly registered/copied into `~/.pi/agent/recipes`.
|
|
16
16
|
- Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` and `docs_review.md` both have id/tool name `docs_review`.
|
|
@@ -24,14 +24,15 @@ Inspect the loaded pi-actors runtime and discovered registry with:
|
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
26
|
inspect target=tool:pi-actors view=status
|
|
27
|
+
inspect target=tool:pi-actors view=triage
|
|
27
28
|
inspect target=recipes view=status
|
|
28
29
|
inspect target=recipes view=doctor
|
|
29
30
|
inspect target=recipes view=summary verbose=true
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
`tool:pi-actors` is a reserved runtime-status actor
|
|
33
|
+
`tool:pi-actors` is a reserved runtime-status actor. `view=status` reports the loaded package version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available. Use it after reloads to confirm which extension code is actually live. `view=triage` adds a compact attention surface for active runs, other-session runs, invalid or blocking recipes, exposed tool recipes with non-lifecycle risk labels, drafts, stale claims, failed runs, attention messages, and next inspect actions without repairing anything. Packaged components and recipes whose only label is `risk.long_running` stay in recipe doctor/summary evidence rather than triage attention.
|
|
33
34
|
|
|
34
|
-
The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation
|
|
35
|
+
The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation, risk-label counts, and ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps per-recipe `risk_labels`, the structured `risk_summary`, `remediations`, `top_action`, diagnostic details, and blocked lower-priority fallback paths when a broken or disabled higher-priority recipe masks a fallback. Risk labels are deterministic review aids, not execution blockers or sandbox claims.
|
|
35
36
|
|
|
36
37
|
Routine shadowing is quiet. If a bare `spawn` recipe launch already fails because an invalid or `disabled: true` user recipe blocks a lower-priority fallback, the launch error adds compact tokens such as `reason=shadowed_invalid` or `reason=shadowed_disabled`, `active_path`, `blocked_fallback`, and `hint=inspect_recipes_doctor`.
|
|
37
38
|
|
|
@@ -51,7 +52,7 @@ register_tool name=call_subagent \
|
|
|
51
52
|
template="pi -p --model {model} --no-tools {prompt}" args="prompt:string,model:string"
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding.
|
|
55
|
+
Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding. To promote a captured draft, pass `name` plus `draft` with a path under `~/.pi/agent/recipes/drafts`; promotion validates the draft before writing `~/.pi/agent/recipes/<name>.json`, preserves the draft file, rejects name collisions unless `update=true`, and leaves shadowing evidence visible through `inspect target=recipes view=summary` or `view=doctor`.
|
|
55
56
|
|
|
56
57
|
`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:
|
|
57
58
|
|
|
@@ -62,18 +63,22 @@ Use `update=true` to overwrite an existing tool. Omit `template` and co-located
|
|
|
62
63
|
]
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
For reusable actor workflows,
|
|
66
|
+
For reusable actor workflows, expose an existing recipe by writing a small wrapper recipe in `~/.pi/agent/recipes` that imports the ready recipe and calls it by alias. This keeps the imported recipe as the source of truth for its script path, defaults, mailbox, artifacts, and future fixes:
|
|
66
67
|
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
description
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"description": "Run the ABCd context validator through its skill recipe.",
|
|
71
|
+
"imports": {
|
|
72
|
+
"validate_context": "{agent}/skills/abcd-context/recipes/validate-context.json"
|
|
73
|
+
},
|
|
74
|
+
"args": ["path:path=."],
|
|
75
|
+
"template": { "name": "validate_context" }
|
|
76
|
+
}
|
|
72
77
|
```
|
|
73
78
|
|
|
74
|
-
|
|
79
|
+
Use the same pattern for packaged pi-actors components, reviewed ad hoc recipes, project-local recipe files, and especially skill recipes that wrap skill scripts. Do not register a local tool that calls `~/.pi/agent/skills/<skill>/scripts/*` directly when the skill already ships a recipe. The wrapper's location in the user recipe root makes it a tool; the import preserves the ready recipe's maintained interface.
|
|
75
80
|
|
|
76
|
-
When co-location is clearer than a separate file, `register_tool` writes the recipe fields directly into the user recipe file:
|
|
81
|
+
When no ready recipe exists and co-location is clearer than a separate file, `register_tool` writes the recipe fields directly into the user recipe file:
|
|
77
82
|
|
|
78
83
|
```json
|
|
79
84
|
{
|