@llblab/pi-actors 0.35.0 → 0.37.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 -72
- package/CHANGELOG.md +17 -3
- package/README.md +2 -1
- package/dist/index.js +0 -1
- package/dist/lib/async-runs.js +4 -3
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +95 -5
- package/dist/lib/recipes-discovery.d.ts +2 -0
- package/dist/lib/recipes-discovery.js +52 -5
- package/dist/lib/recipes-references.d.ts +1 -0
- package/dist/lib/recipes-references.js +122 -35
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +6 -6
- package/dist/lib/runtime-notifier.js +8 -3
- package/dist/lib/runtime.d.ts +2 -6
- package/dist/lib/runtime.js +10 -11
- package/dist/lib/tools-inspect.js +172 -4
- package/dist/lib/tools-response.js +13 -2
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/scripts/validate-recipe.mjs +164 -7
- package/dist/skills/actors/SKILL.md +45 -13
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +2 -0
- package/docs/template-recipes.md +20 -1
- package/docs/tool-registry.md +15 -10
- package/index.ts +0 -1
- package/lib/async-runs.ts +4 -7
- package/lib/command-templates.ts +146 -7
- package/lib/recipes-discovery.ts +69 -6
- package/lib/recipes-references.ts +201 -34
- package/lib/registry.ts +5 -5
- package/lib/runtime-notifier.ts +9 -3
- package/lib/runtime.ts +10 -16
- package/lib/tools-inspect.ts +186 -4
- package/lib/tools-response.ts +16 -2
- package/lib/tools.ts +2 -2
- package/package.json +3 -2
- package/scripts/validate-recipe.mjs +164 -7
- package/skills/actors/SKILL.md +45 -13
- package/skills/swarm/SKILL.md +1 -1
package/lib/tools-inspect.ts
CHANGED
|
@@ -248,6 +248,181 @@ function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
|
|
|
248
248
|
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)}` : ""}`;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
function isStaleClaim(message: AsyncRuns.RunInboxMessage, now: number): boolean {
|
|
252
|
+
if (message.status !== "claimed") return false;
|
|
253
|
+
const claimedAt = Date.parse(String(message.claimed_at ?? ""));
|
|
254
|
+
return Number.isFinite(claimedAt) && now - claimedAt > 5 * 60 * 1000;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getRunTriageSignals(
|
|
258
|
+
runs: Array<Record<string, unknown>>,
|
|
259
|
+
): Record<string, unknown> {
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
const staleClaims: Array<Record<string, unknown>> = [];
|
|
262
|
+
const attentionMessages: Array<Record<string, unknown>> = [];
|
|
263
|
+
for (const run of runs) {
|
|
264
|
+
const stateDir = String(run.state_dir ?? "");
|
|
265
|
+
const runId = String(run.run ?? "");
|
|
266
|
+
if (!stateDir) continue;
|
|
267
|
+
try {
|
|
268
|
+
for (const message of AsyncRuns.readRunInboxMessages(stateDir, 200)) {
|
|
269
|
+
if (!isStaleClaim(message, now)) continue;
|
|
270
|
+
staleClaims.push({
|
|
271
|
+
run: runId,
|
|
272
|
+
id: message.id,
|
|
273
|
+
claimed_at: message.claimed_at,
|
|
274
|
+
claimed_by: message.claimed_by,
|
|
275
|
+
type: message.type,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
} catch {}
|
|
279
|
+
try {
|
|
280
|
+
for (const event of AsyncRuns.readRunEvents(stateDir, 80)) {
|
|
281
|
+
if (event.metadata?.requires_response !== true) continue;
|
|
282
|
+
attentionMessages.push({
|
|
283
|
+
run: runId,
|
|
284
|
+
id: event.id,
|
|
285
|
+
summary: event.summary,
|
|
286
|
+
type: event.type ?? event.event,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
return { attention_messages: attentionMessages, stale_claims: staleClaims };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isTriageHighRiskRecipe(recipe: Record<string, unknown>): boolean {
|
|
295
|
+
if (recipe.tool !== true) return false;
|
|
296
|
+
const labels = Array.isArray(recipe.risk_labels)
|
|
297
|
+
? recipe.risk_labels.map((label) => String(label))
|
|
298
|
+
: [];
|
|
299
|
+
return labels.some((label) => label !== "risk.long_running");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getPiActorsTriage(
|
|
303
|
+
ctx: unknown,
|
|
304
|
+
deps: InspectToolDeps,
|
|
305
|
+
): Record<string, unknown> {
|
|
306
|
+
const runtime = getPiActorsRuntimeStatus();
|
|
307
|
+
const currentSession = ToolsAccess.getContextSessionId(ctx);
|
|
308
|
+
const allRuns = AsyncRuns.listRuns().map((run) =>
|
|
309
|
+
AsyncRuns.getRunStatus(String(run.state_dir)),
|
|
310
|
+
);
|
|
311
|
+
const visibleRuns = currentSession
|
|
312
|
+
? allRuns.filter(
|
|
313
|
+
(run) => !run.ownerId || run.ownerId === currentSession,
|
|
314
|
+
)
|
|
315
|
+
: allRuns;
|
|
316
|
+
const activeRuns = visibleRuns.filter((run) => run.status === "running");
|
|
317
|
+
const failedRuns = visibleRuns.filter((run) => run.status === "failed");
|
|
318
|
+
const otherRuns = currentSession
|
|
319
|
+
? allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession)
|
|
320
|
+
: [];
|
|
321
|
+
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
322
|
+
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
323
|
+
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
324
|
+
{ root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
|
|
325
|
+
]);
|
|
326
|
+
const recipeSummary: Record<string, unknown> = {
|
|
327
|
+
...RecipesDiscovery.summarizeDiscovery(discovered),
|
|
328
|
+
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
329
|
+
};
|
|
330
|
+
const activeRecipes = Array.isArray(recipeSummary.active)
|
|
331
|
+
? (recipeSummary.active as Array<Record<string, unknown>>)
|
|
332
|
+
: [];
|
|
333
|
+
const highRiskRecipes = activeRecipes.filter(isTriageHighRiskRecipe);
|
|
334
|
+
const signals = getRunTriageSignals(visibleRuns);
|
|
335
|
+
const attentionMessages = signals.attention_messages as Array<
|
|
336
|
+
Record<string, unknown>
|
|
337
|
+
>;
|
|
338
|
+
const staleClaims = signals.stale_claims as Array<Record<string, unknown>>;
|
|
339
|
+
const invalidRecipes = Array.isArray(recipeSummary.invalid)
|
|
340
|
+
? (recipeSummary.invalid as Array<Record<string, unknown>>)
|
|
341
|
+
: [];
|
|
342
|
+
const remediations = Array.isArray(recipeSummary.remediations)
|
|
343
|
+
? (recipeSummary.remediations as Array<Record<string, unknown>>)
|
|
344
|
+
: [];
|
|
345
|
+
const drafts = Array.isArray(recipeSummary.drafts)
|
|
346
|
+
? (recipeSummary.drafts as Array<Record<string, unknown>>)
|
|
347
|
+
: [];
|
|
348
|
+
const nextActions = [
|
|
349
|
+
invalidRecipes.length || remediations.length
|
|
350
|
+
? "inspect target=recipes view=doctor"
|
|
351
|
+
: "",
|
|
352
|
+
drafts.length ? "inspect target=recipes view=summary verbose=true" : "",
|
|
353
|
+
failedRuns[0]?.run
|
|
354
|
+
? `inspect target=run:${String(failedRuns[0].run)} view=tail lines=80`
|
|
355
|
+
: "",
|
|
356
|
+
attentionMessages[0]?.run
|
|
357
|
+
? `inspect target=run:${String(attentionMessages[0].run)} view=messages`
|
|
358
|
+
: "",
|
|
359
|
+
activeRuns.length
|
|
360
|
+
? "inspect target=session:all view=runs status=active"
|
|
361
|
+
: "",
|
|
362
|
+
].filter(Boolean);
|
|
363
|
+
return {
|
|
364
|
+
runtime,
|
|
365
|
+
current_session: currentSession ?? null,
|
|
366
|
+
active_runs: activeRuns.map((run) => ({
|
|
367
|
+
run: run.run,
|
|
368
|
+
ownerId: run.ownerId,
|
|
369
|
+
recipe: run.recipe,
|
|
370
|
+
status: run.status,
|
|
371
|
+
})),
|
|
372
|
+
other_session_runs: otherRuns.length,
|
|
373
|
+
invalid_recipes: invalidRecipes,
|
|
374
|
+
blocking_recipes: remediations.filter((item) =>
|
|
375
|
+
String(item.kind ?? "").startsWith("blocking_"),
|
|
376
|
+
),
|
|
377
|
+
high_risk_recipes: highRiskRecipes.map((recipe) => ({
|
|
378
|
+
id: recipe.id,
|
|
379
|
+
path: recipe.path,
|
|
380
|
+
risk_labels: recipe.risk_labels,
|
|
381
|
+
})),
|
|
382
|
+
draft_recipes: drafts,
|
|
383
|
+
stale_claims: staleClaims,
|
|
384
|
+
recent_failed_runs: failedRuns.slice(0, 5).map((run) => ({
|
|
385
|
+
run: run.run,
|
|
386
|
+
recipe: run.recipe,
|
|
387
|
+
status: run.status,
|
|
388
|
+
})),
|
|
389
|
+
attention_messages: attentionMessages.slice(-10),
|
|
390
|
+
next_actions: [...new Set(nextActions)].slice(0, 5),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function compactPiActorsTriage(summary: Record<string, unknown>): string {
|
|
395
|
+
const runtime = asRecord(summary.runtime);
|
|
396
|
+
const activeRuns = Array.isArray(summary.active_runs)
|
|
397
|
+
? summary.active_runs.length
|
|
398
|
+
: 0;
|
|
399
|
+
const invalidRecipes = Array.isArray(summary.invalid_recipes)
|
|
400
|
+
? summary.invalid_recipes.length
|
|
401
|
+
: 0;
|
|
402
|
+
const blockingRecipes = Array.isArray(summary.blocking_recipes)
|
|
403
|
+
? summary.blocking_recipes.length
|
|
404
|
+
: 0;
|
|
405
|
+
const highRiskRecipes = Array.isArray(summary.high_risk_recipes)
|
|
406
|
+
? summary.high_risk_recipes.length
|
|
407
|
+
: 0;
|
|
408
|
+
const drafts = Array.isArray(summary.draft_recipes)
|
|
409
|
+
? summary.draft_recipes.length
|
|
410
|
+
: 0;
|
|
411
|
+
const staleClaims = Array.isArray(summary.stale_claims)
|
|
412
|
+
? summary.stale_claims.length
|
|
413
|
+
: 0;
|
|
414
|
+
const failedRuns = Array.isArray(summary.recent_failed_runs)
|
|
415
|
+
? summary.recent_failed_runs.length
|
|
416
|
+
: 0;
|
|
417
|
+
const attention = Array.isArray(summary.attention_messages)
|
|
418
|
+
? summary.attention_messages.length
|
|
419
|
+
: 0;
|
|
420
|
+
const nextActions = Array.isArray(summary.next_actions)
|
|
421
|
+
? (summary.next_actions as string[])
|
|
422
|
+
: [];
|
|
423
|
+
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)}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
251
426
|
function compactToolActor(name: string, tool: Record<string, unknown>): string {
|
|
252
427
|
const parameters = asRecord(tool.parameters);
|
|
253
428
|
const required = Array.isArray(parameters.required)
|
|
@@ -414,10 +589,15 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
414
589
|
}
|
|
415
590
|
if (address.kind === "tool" && address.value) {
|
|
416
591
|
if (address.value === "pi-actors") {
|
|
417
|
-
if (view !== "status") {
|
|
418
|
-
throw new Error(
|
|
592
|
+
if (view !== "status" && view !== "triage") {
|
|
593
|
+
throw new Error(
|
|
594
|
+
"inspect tool:pi-actors supports view=status or view=triage.",
|
|
595
|
+
);
|
|
419
596
|
}
|
|
420
|
-
const details =
|
|
597
|
+
const details =
|
|
598
|
+
view === "triage"
|
|
599
|
+
? getPiActorsTriage(ctx, deps)
|
|
600
|
+
: getPiActorsRuntimeStatus();
|
|
421
601
|
return {
|
|
422
602
|
content: [
|
|
423
603
|
{
|
|
@@ -425,7 +605,9 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
425
605
|
text: maybeJsonText(
|
|
426
606
|
details,
|
|
427
607
|
input.verbose === true,
|
|
428
|
-
|
|
608
|
+
view === "triage"
|
|
609
|
+
? compactPiActorsTriage(details)
|
|
610
|
+
: compactPiActorsRuntimeStatus(details),
|
|
429
611
|
),
|
|
430
612
|
},
|
|
431
613
|
],
|
package/lib/tools-response.ts
CHANGED
|
@@ -173,15 +173,26 @@ export function compactRecipeDoctor(summary: Record<string, unknown>): string {
|
|
|
173
173
|
const recommendations = Array.isArray(summary.recommendations)
|
|
174
174
|
? (summary.recommendations as Array<Record<string, unknown>>)
|
|
175
175
|
: [];
|
|
176
|
+
const riskSummary = Array.isArray(summary.risk_summary)
|
|
177
|
+
? (summary.risk_summary as Array<Record<string, unknown>>)
|
|
178
|
+
: [];
|
|
176
179
|
const counts = { error: 0, info: 0, warning: 0 };
|
|
177
180
|
for (const detail of details) {
|
|
178
181
|
const severity = String(detail.severity ?? "info");
|
|
179
182
|
if (severity === "error" || severity === "warning" || severity === "info")
|
|
180
183
|
counts[severity] += 1;
|
|
181
184
|
}
|
|
185
|
+
const riskCount = riskSummary.reduce(
|
|
186
|
+
(total, item) => total + Number(item.count ?? 0),
|
|
187
|
+
0,
|
|
188
|
+
);
|
|
189
|
+
const topRisks = riskSummary
|
|
190
|
+
.slice(0, 4)
|
|
191
|
+
.map((item) => `${String(item.label)}:${String(item.count ?? 0)}`)
|
|
192
|
+
.join(",");
|
|
182
193
|
const topAction = asRecord(summary.top_action);
|
|
183
194
|
const lines = [
|
|
184
|
-
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
|
|
195
|
+
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length} risks=${riskCount}${topRisks ? ` top_risks=${topRisks}` : ""}`,
|
|
185
196
|
];
|
|
186
197
|
if (Object.keys(topAction).length > 0) {
|
|
187
198
|
const action = compactPreview(
|
|
@@ -200,8 +211,11 @@ export function compactRecipeDoctor(summary: Record<string, unknown>): string {
|
|
|
200
211
|
const blocked = item.blocked_fallback
|
|
201
212
|
? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
|
|
202
213
|
: "";
|
|
214
|
+
const labels = Array.isArray(item.risk_labels)
|
|
215
|
+
? ` labels=${(item.risk_labels as unknown[]).map(String).slice(0, 4).join(",")}`
|
|
216
|
+
: "";
|
|
203
217
|
lines.push(
|
|
204
|
-
`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
|
|
218
|
+
`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked}${labels} action=${action ?? "inspect"}`,
|
|
205
219
|
);
|
|
206
220
|
}
|
|
207
221
|
const nextActions = Array.isArray(summary.next_actions)
|
package/lib/tools.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface CoreActorToolDefinitionDeps<
|
|
|
27
27
|
getRuntimeTool: (name: string) => unknown;
|
|
28
28
|
registryRuntime: Pick<
|
|
29
29
|
RegisterToolRuntimeDeps<TContext>,
|
|
30
|
-
"
|
|
30
|
+
"getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool"
|
|
31
31
|
>;
|
|
32
32
|
setActiveTools: (toolNames: string[]) => void;
|
|
33
33
|
}
|
|
@@ -53,7 +53,7 @@ export function createCoreActorToolDefinitions<
|
|
|
53
53
|
ToolsRegister.createRegisterToolDefinition<TContext>({
|
|
54
54
|
configPath: deps.configPath,
|
|
55
55
|
getActiveTools: deps.getActiveTools,
|
|
56
|
-
|
|
56
|
+
getToolNameBlocker: deps.registryRuntime.getToolNameBlocker,
|
|
57
57
|
getTools: deps.registryRuntime.getTools,
|
|
58
58
|
notify: deps.registryRuntime.notify,
|
|
59
59
|
registerRuntimeTool: deps.registryRuntime.registerRuntimeTool,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-actors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local Actor Kernel for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"conformance": "node scripts/conformance.mjs",
|
|
30
30
|
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
31
31
|
"pack:dry": "npm pack --dry-run",
|
|
32
|
-
"
|
|
32
|
+
"recipes:qa": "node scripts/validate-recipe.mjs recipes --all --qa --summary",
|
|
33
|
+
"validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run pack:dry",
|
|
33
34
|
"build": "node scripts/build-dist.mjs",
|
|
34
35
|
"prepack": "npm run build"
|
|
35
36
|
},
|
|
@@ -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));
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -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.37.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -194,12 +194,14 @@ 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.
|
|
199
|
-
7.
|
|
200
|
-
8.
|
|
201
|
-
9.
|
|
202
|
-
10.
|
|
197
|
+
5. Direct recipe delegation is the thin-wrapper case: when a `template` value is just a ready recipe name/path, the intended behavior is to delegate to that recipe rather than execute the recipe file as a program. Use this for simple handoffs and wrapper tools; use `imports` + `{ "name": "alias" }` when you need rich composition, multiple nodes, or import-specific values/defaults.
|
|
198
|
+
6. When exposing an already-authored recipe as a user tool before direct delegation is available or when composition is needed, 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.
|
|
199
|
+
7. Declare `mailbox` for actors that accept or emit meaningful messages.
|
|
200
|
+
8. Declare `artifacts` for durable outputs the coordinator should inspect.
|
|
201
|
+
9. File-backed recipe identity comes from the filename basename; legacy top-level `name` fields are ignored by loaders.
|
|
202
|
+
10. 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.
|
|
203
|
+
11. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
|
|
204
|
+
12. 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
205
|
|
|
204
206
|
Priority for same-id recipes:
|
|
205
207
|
|
|
@@ -208,7 +210,7 @@ Priority for same-id recipes:
|
|
|
208
210
|
3. Explicit ad hoc user recipe file outside `~/.pi/agent/recipes`.
|
|
209
211
|
4. User recipe in `~/.pi/agent/recipes/*.json` or `*.md`: highest-priority operator tool surface.
|
|
210
212
|
|
|
211
|
-
Only matching filename ids compete. Higher priority shadows lower priority; within one priority layer, same-id JSON shadows Markdown. 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.
|
|
213
|
+
Only matching filename ids compete. Higher priority shadows lower priority; within one priority layer, same-id JSON shadows Markdown. Same-id overrides are normal composition/delegation behavior, not startup-warning material. 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.
|
|
212
214
|
|
|
213
215
|
Muscle-memory lens: pi-actors has two durable executable-memory layers.
|
|
214
216
|
|
|
@@ -227,23 +229,53 @@ Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memo
|
|
|
227
229
|
|
|
228
230
|
`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
231
|
|
|
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
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
Ready-recipe registration patterns:
|
|
235
|
+
|
|
236
|
+
Thin delegation target shape:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"description": "Run a ready recipe through a local tool name.",
|
|
241
|
+
"args": ["source:path", "volume:int=70"],
|
|
242
|
+
"template": "/path/to/ready-recipe.json"
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Delegation is for one-to-one handoff: expose or call a maintained recipe directly, preserving that recipe as the source of truth. If the runtime does not yet support direct recipe references in `template`, or if you need composition, use the import-node wrapper below.
|
|
247
|
+
|
|
248
|
+
Composition/import wrapper:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"description": "Run the ABCd context validator through its skill recipe.",
|
|
253
|
+
"imports": {
|
|
254
|
+
"validate_context": "{agent}/skills/abcd-context/recipes/validate-context.json"
|
|
255
|
+
},
|
|
256
|
+
"args": ["path:path=."],
|
|
257
|
+
"template": { "name": "validate_context" }
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Use delegation or this import 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 delegated/imported recipe remains the source of truth for the script path, default values, mailbox contract, artifacts, and future fixes.
|
|
231
262
|
|
|
232
263
|
Tool-registration lenses are open-ended prompts for deciding what deserves durable tool status:
|
|
233
264
|
|
|
234
265
|
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
266
|
2. **Safety lens**: prefer read-only diagnostics, dry-runs, preflights, confirmations, or bounded adapters around high-impact operations before registering direct action tools.
|
|
236
267
|
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. **
|
|
268
|
+
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 delegate to or import from a user-root wrapper when they match a recurring local workflow.
|
|
269
|
+
5. **Skill-recipe lens**: when a skill ships a recipe for its script, local tools must delegate to or import that recipe instead of calling the skill script directly. This preserves the skill's maintained interface and keeps future script/default changes centralized.
|
|
270
|
+
6. **Composition lens**: register small semantic entrypoints over reusable recipe components instead of baking one large scenario-specific shell command into a tool; prefer direct delegation for one recipe, imports for composed graphs.
|
|
271
|
+
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
272
|
|
|
241
273
|
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
274
|
|
|
243
275
|
Tool templates may be:
|
|
244
276
|
|
|
245
277
|
- A foreground command template.
|
|
246
|
-
- A file-backed recipe name/path.
|
|
278
|
+
- A file-backed recipe name/path for thin delegation.
|
|
247
279
|
- A complete recipe body, optionally `async: true`.
|
|
248
280
|
|
|
249
281
|
The user recipe root is the default tool set by location. It accepts canonical JSON recipes and literate Markdown recipes with frontmatter plus fenced `template`/`json recipe` blocks; same-id JSON shadows Markdown in the same priority layer. 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.
|
package/skills/swarm/SKILL.md
CHANGED