@llblab/pi-actors 0.14.3 → 0.16.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 +5 -1
- package/BACKLOG.md +54 -32
- package/CHANGELOG.md +39 -0
- package/README.md +53 -61
- package/banner.jpg +0 -0
- package/docs/actor-messages.md +1 -1
- package/docs/async-runs.md +4 -4
- package/docs/command-templates.md +11 -11
- package/docs/recipe-library.md +7 -3
- package/docs/task-first-recipes.md +44 -43
- package/docs/template-recipes.md +45 -23
- package/docs/tool-registry.md +50 -42
- package/index.ts +34 -0
- package/lib/actor-messages.ts +20 -7
- package/lib/async-runs.ts +35 -12
- package/lib/command-templates.ts +6 -1
- package/lib/config.ts +3 -2
- package/lib/execution.ts +9 -5
- package/lib/observability.ts +20 -10
- package/lib/paths.ts +6 -1
- package/lib/prompts.ts +14 -21
- package/lib/recipe-discovery.ts +226 -0
- package/lib/recipe-migration.ts +123 -0
- package/lib/recipe-references.ts +45 -13
- package/lib/recipe-usage.ts +44 -0
- package/lib/registry.ts +48 -15
- package/lib/runtime.ts +59 -15
- package/lib/tools.ts +237 -65
- package/package.json +21 -11
- package/recipes/coordinator-locker.json +46 -0
- package/recipes/music-player.json +16 -2
- package/recipes/pipeline-architect-coordinator.json +11 -3
- package/recipes/pipeline-artifact-bundle.json +12 -3
- package/recipes/pipeline-artifact-report.json +9 -3
- package/recipes/pipeline-artifact-write.json +9 -3
- package/recipes/pipeline-async-run-ops.json +18 -9
- package/recipes/pipeline-checkpoint-continuation.json +14 -3
- package/recipes/pipeline-development-tasking.json +12 -3
- package/recipes/pipeline-docs-maintenance.json +12 -3
- package/recipes/pipeline-media-library.json +12 -3
- package/recipes/pipeline-quorum-review.json +12 -9
- package/recipes/pipeline-release-readiness.json +27 -9
- package/recipes/pipeline-release-summary.json +89 -0
- package/recipes/pipeline-repo-health.json +12 -3
- package/recipes/pipeline-research-synthesis.json +11 -3
- package/recipes/pipeline-review-readiness.json +12 -6
- package/recipes/subagent-artifact.json +9 -3
- package/recipes/subagent-checkpoint.json +10 -3
- package/recipes/subagent-conflict-report.json +11 -3
- package/recipes/subagent-contradiction-map.json +11 -3
- package/recipes/subagent-critic.json +11 -3
- package/recipes/subagent-evidence-map.json +11 -3
- package/recipes/subagent-followup.json +10 -3
- package/recipes/subagent-judge.json +11 -3
- package/recipes/subagent-merge.json +11 -3
- package/recipes/subagent-message.json +8 -3
- package/recipes/subagent-normalize.json +11 -3
- package/recipes/subagent-plan.json +11 -3
- package/recipes/subagent-prompt.json +10 -3
- package/recipes/subagent-quorum.json +10 -7
- package/recipes/subagent-review-coordinator.json +14 -6
- package/recipes/subagent-review.json +11 -3
- package/recipes/subagent-task-card.json +11 -3
- package/recipes/subagent-tools.json +10 -3
- package/recipes/subagent-verify.json +11 -3
- package/recipes/subagents-prompts.json +10 -3
- package/recipes/utility-coordinator-lock-snapshot.json +14 -0
- package/recipes/utility-run-ops-snapshot.json +3 -3
- package/recipes/utility-skill-summary.json +14 -0
- package/scripts/coordinator-locker.mjs +272 -0
- package/scripts/music-player.mjs +2 -1
- package/scripts/recipe-utils.mjs +239 -81
- package/scripts/validate-recipe.mjs +28 -10
- package/skills/actors/SKILL.md +301 -0
- package/skills/swarm/SKILL.md +451 -0
- package/skills/swarm/references/development-swarm.md +596 -0
package/lib/async-runs.ts
CHANGED
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
writeFileSync,
|
|
20
20
|
writeSync,
|
|
21
21
|
} from "node:fs";
|
|
22
|
-
import { basename, extname, join, resolve } from "node:path";
|
|
23
22
|
import { platform } from "node:os";
|
|
23
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
24
24
|
|
|
25
25
|
import type {
|
|
26
26
|
CommandTemplateFailureScope,
|
|
@@ -28,8 +28,9 @@ import type {
|
|
|
28
28
|
} from "./command-templates.ts";
|
|
29
29
|
import { substituteCommandTemplateToken } from "./command-templates.ts";
|
|
30
30
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
31
|
-
import * as RecipeReferences from "./recipe-references.ts";
|
|
32
31
|
import * as Paths from "./paths.ts";
|
|
32
|
+
import * as RecipeReferences from "./recipe-references.ts";
|
|
33
|
+
import * as RecipeUsage from "./recipe-usage.ts";
|
|
33
34
|
|
|
34
35
|
export interface AsyncRunStartParams {
|
|
35
36
|
async?: boolean;
|
|
@@ -139,8 +140,7 @@ function resolveArtifactPaths(
|
|
|
139
140
|
function resolveRunTemplate(params: AsyncRunStartParams): {
|
|
140
141
|
template: CommandTemplateValue;
|
|
141
142
|
} {
|
|
142
|
-
if (!params.template)
|
|
143
|
-
throw new Error("spawn requires file or template.");
|
|
143
|
+
if (!params.template) throw new Error("spawn requires file or template.");
|
|
144
144
|
const envelope: Record<string, unknown> = {};
|
|
145
145
|
for (const key of [
|
|
146
146
|
"args",
|
|
@@ -173,6 +173,12 @@ function resolveRecipeFile(file: string): string {
|
|
|
173
173
|
return RecipeReferences.resolveRecipePath(file, DEFAULT_RECIPE_ROOT);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function isMutableUsageRecipeFile(file: string): boolean {
|
|
177
|
+
const userRoot = resolve(DEFAULT_RECIPE_ROOT);
|
|
178
|
+
const resolved = resolve(file);
|
|
179
|
+
return resolved.startsWith(`${userRoot}/`);
|
|
180
|
+
}
|
|
181
|
+
|
|
176
182
|
function readRecipeFile(file: string): AsyncRunStartParams {
|
|
177
183
|
const path = resolveRecipeFile(file);
|
|
178
184
|
const raw = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
@@ -227,7 +233,8 @@ function isAlive(pid: number): boolean {
|
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
function pidMatchesRun(pid: number, cwd: string, stateDir: string): boolean {
|
|
230
|
-
if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
|
|
236
|
+
if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
|
|
237
|
+
return isAlive(pid);
|
|
231
238
|
try {
|
|
232
239
|
const procCwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
233
240
|
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
@@ -316,6 +323,9 @@ export function startRun(
|
|
|
316
323
|
? resolveRecipeFile(startParams.file)
|
|
317
324
|
: undefined;
|
|
318
325
|
const recipe = startParams.name || getRunIdFromFile(recipeFile);
|
|
326
|
+
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
327
|
+
RecipeUsage.recordRecipeLaunch(recipeFile);
|
|
328
|
+
}
|
|
319
329
|
const outFd = openSync(stdout, "a");
|
|
320
330
|
const errFd = openSync(stderr, "a");
|
|
321
331
|
const argv = ["--experimental-strip-types", RUNNER_PATH, stateDir];
|
|
@@ -405,15 +415,23 @@ function normalizeRunOutboxEvent(
|
|
|
405
415
|
: `${run}:${index}`;
|
|
406
416
|
return {
|
|
407
417
|
...(record.body !== undefined ? { body: record.body } : {}),
|
|
408
|
-
...(typeof record.correlation_id === "string"
|
|
418
|
+
...(typeof record.correlation_id === "string"
|
|
419
|
+
? { correlation_id: record.correlation_id }
|
|
420
|
+
: {}),
|
|
409
421
|
...(record.data !== undefined ? { data: record.data } : {}),
|
|
410
422
|
delivery: normalizeRunOutboxDelivery(record.delivery),
|
|
411
|
-
...(record.metadata &&
|
|
423
|
+
...(record.metadata &&
|
|
424
|
+
typeof record.metadata === "object" &&
|
|
425
|
+
!Array.isArray(record.metadata)
|
|
426
|
+
? { metadata: record.metadata as Record<string, unknown> }
|
|
427
|
+
: {}),
|
|
412
428
|
event,
|
|
413
429
|
...(typeof record.from === "string" ? { from: record.from } : {}),
|
|
414
430
|
id,
|
|
415
431
|
level: normalizeRunOutboxLevel(record.level),
|
|
416
|
-
...(typeof record.reply_to === "string"
|
|
432
|
+
...(typeof record.reply_to === "string"
|
|
433
|
+
? { reply_to: record.reply_to }
|
|
434
|
+
: {}),
|
|
417
435
|
run,
|
|
418
436
|
state_dir: stateDir,
|
|
419
437
|
summary,
|
|
@@ -448,8 +466,9 @@ export function getRunStatus(runOrDir: string): Record<string, unknown> {
|
|
|
448
466
|
const pid = Number(meta.pid || 0);
|
|
449
467
|
const aliveOwnedRunner = Boolean(
|
|
450
468
|
pid &&
|
|
451
|
-
|
|
452
|
-
|
|
469
|
+
isAlive(pid) &&
|
|
470
|
+
(!Array.isArray(meta.argv) ||
|
|
471
|
+
pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir)),
|
|
453
472
|
);
|
|
454
473
|
const status: AsyncRunStatus = result
|
|
455
474
|
? Number(result.code ?? 0) === 0
|
|
@@ -555,7 +574,9 @@ export function appendRunOutboxEvent(
|
|
|
555
574
|
...(event.body !== undefined ? { body: event.body } : {}),
|
|
556
575
|
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
557
576
|
...(event.data !== undefined ? { data: event.data } : {}),
|
|
558
|
-
delivery: normalizeRunOutboxDelivery(
|
|
577
|
+
delivery: normalizeRunOutboxDelivery(
|
|
578
|
+
event.delivery ?? (to === "coordinator" ? "followup" : "log"),
|
|
579
|
+
),
|
|
559
580
|
event: type,
|
|
560
581
|
from: event.from || `run:${run}`,
|
|
561
582
|
level: normalizeRunOutboxLevel(event.level),
|
|
@@ -608,7 +629,9 @@ export function sendRunMessage(
|
|
|
608
629
|
fd = openSync(controlPath, constants.O_WRONLY | constants.O_NONBLOCK);
|
|
609
630
|
const bytes = writeSync(fd, payload);
|
|
610
631
|
const trimmedMessage = message.trim().toLowerCase();
|
|
611
|
-
const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(
|
|
632
|
+
const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(
|
|
633
|
+
trimmedMessage,
|
|
634
|
+
);
|
|
612
635
|
writeFileSync(
|
|
613
636
|
join(stateDir, "events.jsonl"),
|
|
614
637
|
`${JSON.stringify({ bytes, event: "run.message", terminal: terminalMessage || undefined, ts: new Date().toISOString() })}\n`,
|
package/lib/command-templates.ts
CHANGED
|
@@ -541,7 +541,12 @@ function shouldResolveEmbeddedCommandTemplateToken(
|
|
|
541
541
|
function isFalsyCommandTemplateValue(value: unknown): boolean {
|
|
542
542
|
if (value === undefined || value === null || value === false) return true;
|
|
543
543
|
const normalized = String(value).trim().toLowerCase();
|
|
544
|
-
return
|
|
544
|
+
return (
|
|
545
|
+
normalized === "" ||
|
|
546
|
+
normalized === "0" ||
|
|
547
|
+
normalized === "false" ||
|
|
548
|
+
normalized === "no"
|
|
549
|
+
);
|
|
545
550
|
}
|
|
546
551
|
|
|
547
552
|
function resolveCommandTemplateCondition(
|
package/lib/config.ts
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
|
|
9
|
+
import type { CommandTemplateValue } from "./command-templates.ts";
|
|
10
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
9
11
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
10
12
|
import { normalizeToolName } from "./identity.ts";
|
|
11
|
-
import * as CommandTemplates from "./command-templates.ts";
|
|
12
13
|
import * as RecipeReferences from "./recipe-references.ts";
|
|
13
14
|
import * as Schema from "./schema.ts";
|
|
14
|
-
import type { CommandTemplateValue } from "./command-templates.ts";
|
|
15
15
|
|
|
16
16
|
export interface RegisteredTool {
|
|
17
17
|
name: string;
|
|
@@ -23,6 +23,7 @@ export interface RegisteredTool {
|
|
|
23
23
|
template?: CommandTemplateValue;
|
|
24
24
|
storedArgs?: string[];
|
|
25
25
|
storedDefaults?: Record<string, string>;
|
|
26
|
+
sourcePath?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface LoadConfigResult {
|
package/lib/execution.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Owns command-template invocation execution and pi tool-result payload formatting
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
7
8
|
import type { RegisteredTool } from "./config.ts";
|
|
8
9
|
import { formatFailureOutput, formatOutput, formatToolText } from "./output.ts";
|
|
9
|
-
import * as CommandTemplates from "./command-templates.ts";
|
|
10
10
|
import * as Schema from "./schema.ts";
|
|
11
11
|
|
|
12
12
|
export interface ToolExecOptions {
|
|
@@ -268,9 +268,10 @@ function resolveNumericControlField(
|
|
|
268
268
|
label: string,
|
|
269
269
|
): number | undefined {
|
|
270
270
|
if (value === undefined) return undefined;
|
|
271
|
-
const resolved =
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
const resolved =
|
|
272
|
+
typeof value === "string"
|
|
273
|
+
? CommandTemplates.substituteCommandTemplateToken(value, values, label)
|
|
274
|
+
: value;
|
|
274
275
|
if (resolved === "") return undefined;
|
|
275
276
|
const numeric = Number(resolved);
|
|
276
277
|
if (!Number.isFinite(numeric) || numeric < 0)
|
|
@@ -407,7 +408,10 @@ async function executeTemplateConfig(
|
|
|
407
408
|
const controlValues = { ...(context.defaults ?? {}), ...params };
|
|
408
409
|
await applyDelay(normalized.delay, controlValues, signal);
|
|
409
410
|
if (
|
|
410
|
-
!CommandTemplates.shouldRunCommandTemplateNode(
|
|
411
|
+
!CommandTemplates.shouldRunCommandTemplateNode(
|
|
412
|
+
normalized.when,
|
|
413
|
+
controlValues,
|
|
414
|
+
)
|
|
411
415
|
) {
|
|
412
416
|
return {
|
|
413
417
|
branches: [],
|
package/lib/observability.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import { basename, dirname,
|
|
8
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
9
9
|
|
|
10
10
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
11
|
import * as Paths from "./paths.ts";
|
|
@@ -114,7 +114,9 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
114
114
|
...(typeof status.ownerId === "string"
|
|
115
115
|
? { ownerId: status.ownerId }
|
|
116
116
|
: {}),
|
|
117
|
-
...(status.artifacts &&
|
|
117
|
+
...(status.artifacts &&
|
|
118
|
+
typeof status.artifacts === "object" &&
|
|
119
|
+
!Array.isArray(status.artifacts)
|
|
118
120
|
? { artifacts: status.artifacts as Record<string, string> }
|
|
119
121
|
: {}),
|
|
120
122
|
...(status.terminal_handled ? { terminalHandled: true } : {}),
|
|
@@ -359,7 +361,11 @@ function parseOutboxLine(
|
|
|
359
361
|
event,
|
|
360
362
|
id,
|
|
361
363
|
level: normalizeOutboxLevel(raw.level),
|
|
362
|
-
...(raw.metadata &&
|
|
364
|
+
...(raw.metadata &&
|
|
365
|
+
typeof raw.metadata === "object" &&
|
|
366
|
+
!Array.isArray(raw.metadata)
|
|
367
|
+
? { metadata: raw.metadata as Record<string, unknown> }
|
|
368
|
+
: {}),
|
|
363
369
|
run: run.run,
|
|
364
370
|
stateDir: run.stateDir,
|
|
365
371
|
summary,
|
|
@@ -413,7 +419,8 @@ export function shouldSendRunOutboxFollowUp(event: RunOutboxEvent): boolean {
|
|
|
413
419
|
|
|
414
420
|
function commonDirectory(paths: string[]): string | undefined {
|
|
415
421
|
if (paths.length === 0) return undefined;
|
|
416
|
-
const split = (path: string): string[] =>
|
|
422
|
+
const split = (path: string): string[] =>
|
|
423
|
+
dirname(path).split("/").filter(Boolean);
|
|
417
424
|
const first = split(paths[0]);
|
|
418
425
|
let length = first.length;
|
|
419
426
|
for (const path of paths.slice(1)) {
|
|
@@ -440,7 +447,9 @@ function formatPathGroup(label: string, paths: string[]): string {
|
|
|
440
447
|
const unique = [...new Set(paths.filter(Boolean))].slice(0, 8);
|
|
441
448
|
if (unique.length === 0) return "";
|
|
442
449
|
const base = commonDirectory(unique);
|
|
443
|
-
const names = unique
|
|
450
|
+
const names = unique
|
|
451
|
+
.map((path) => `\`${relativeName(base, path)}\``)
|
|
452
|
+
.join(", ");
|
|
444
453
|
return `\n${label}:\n- Base: ${base ? `\`${base}\`` : "current run"}\n- Files: ${names}`;
|
|
445
454
|
}
|
|
446
455
|
|
|
@@ -464,7 +473,9 @@ function formatNamedArtifacts(artifacts: unknown): string {
|
|
|
464
473
|
}
|
|
465
474
|
|
|
466
475
|
function getOutboxField(event: RunOutboxEvent, key: string): unknown {
|
|
467
|
-
return event.data &&
|
|
476
|
+
return event.data &&
|
|
477
|
+
typeof event.data === "object" &&
|
|
478
|
+
!Array.isArray(event.data)
|
|
468
479
|
? (event.data as Record<string, unknown>)[key]
|
|
469
480
|
: undefined;
|
|
470
481
|
}
|
|
@@ -478,7 +489,8 @@ function formatBodyPreview(body: unknown): string {
|
|
|
478
489
|
}
|
|
479
490
|
|
|
480
491
|
export function formatRunOutboxMessage(event: RunOutboxEvent): string {
|
|
481
|
-
if (event.event === "command.done")
|
|
492
|
+
if (event.event === "command.done")
|
|
493
|
+
return `Run ${event.run}: ${event.summary}`;
|
|
482
494
|
return `Run ${event.run}: ${event.summary}${formatBodyPreview(event.body)}${formatNamedArtifacts(getOutboxField(event, "artifacts"))}${formatRunFileList(getOutboxField(event, "run_files"))}`;
|
|
483
495
|
}
|
|
484
496
|
|
|
@@ -491,9 +503,7 @@ export function getRunTransitionNotificationType(
|
|
|
491
503
|
return "error";
|
|
492
504
|
}
|
|
493
505
|
|
|
494
|
-
export function shouldNotifyRunTransition(
|
|
495
|
-
transition: RunTransition,
|
|
496
|
-
): boolean {
|
|
506
|
+
export function shouldNotifyRunTransition(transition: RunTransition): boolean {
|
|
497
507
|
if (transition.terminalHandled) return false;
|
|
498
508
|
return (
|
|
499
509
|
transition.to === "done" ||
|
package/lib/paths.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { join, resolve } from "node:path";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
11
|
export function getAgentDir(
|
|
11
12
|
env: Record<string, string | undefined> = process.env,
|
|
@@ -33,3 +34,7 @@ export function getRunStateRoot(agentDir = getAgentDir()): string {
|
|
|
33
34
|
export function getRecipeRoot(agentDir = getAgentDir()): string {
|
|
34
35
|
return join(agentDir, "recipes");
|
|
35
36
|
}
|
|
37
|
+
|
|
38
|
+
export function getPackagedRecipeRoot(): string {
|
|
39
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "recipes");
|
|
40
|
+
}
|
package/lib/prompts.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export const REGISTER_TOOL_DESCRIPTION =
|
|
8
8
|
"Register a persistent custom tool from a command template, template recipe path, or co-located template recipe. " +
|
|
9
|
-
"Definitions are stored
|
|
9
|
+
"Definitions are stored as recipe files under ~/.pi/agent/recipes across reloads. " +
|
|
10
10
|
"Use update=true to overwrite an existing tool, template=null/empty to delete.";
|
|
11
11
|
|
|
12
12
|
export const REGISTER_TOOL_PROMPT_SNIPPET =
|
|
@@ -20,25 +20,18 @@ export const REGISTER_TOOL_GUIDELINES = [
|
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
23
|
-
- Local-first
|
|
24
|
-
-
|
|
25
|
-
- Command templates stay sync: string leaf, array sequence, object flags, parallel
|
|
26
|
-
-
|
|
27
|
-
- Recipes live in ~/.pi/agent/recipes/*.json and
|
|
28
|
-
- Recipe imports are local variables
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- register_tool makes compact persistent buttons; args may be typed or derived from placeholders.
|
|
36
|
-
- For single calls or short pipelines, use foreground templates/tools.
|
|
37
|
-
- For subagents, swarms, background music, or long fanout, prefer async recipes/runs.
|
|
38
|
-
- Long async fanout = parent async recipe wrapping template(parallel: true) and imports; packaged fanout recipes bubble branch completion follow-ups by default.
|
|
39
|
-
- If asked to explore pi-actors, read README.md, docs/README.md, docs/template-recipes.md, docs/async-runs.md, and recipes/.
|
|
40
|
-
- Ambient triangles show active async commands/subagents for the launching coordinator.
|
|
41
|
-
- After async run finish, inspect status/tail/messages before final artifacts.`;
|
|
23
|
+
- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
|
|
24
|
+
- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
|
|
25
|
+
- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.
|
|
26
|
+
- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
|
|
27
|
+
- Recipes live in ~/.pi/agent/recipes/*.json, own template directly, and may declare metadata/defaults/imports/mailbox/artifacts.
|
|
28
|
+
- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
|
|
29
|
+
- Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.
|
|
30
|
+
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
|
|
31
|
+
- Register_tool writes user recipe files in ~/.pi/agent/recipes; that root is the default tool set, while packaged recipes are the lower-priority standard library.
|
|
32
|
+
- Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.
|
|
33
|
+
- Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages.
|
|
34
|
+
- For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.`;
|
|
42
35
|
|
|
43
36
|
export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
44
37
|
name: "Tool name in snake_case (e.g., 'transcribe')",
|
|
@@ -53,7 +46,7 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
|
53
46
|
templateArray:
|
|
54
47
|
"Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.",
|
|
55
48
|
templateNull: "Delete the tool when template is null.",
|
|
56
|
-
args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,
|
|
49
|
+
args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast",
|
|
57
50
|
update: "Set to true to overwrite an existing tool registration.",
|
|
58
51
|
values:
|
|
59
52
|
"Optional default runtime placeholder values for a co-located template recipe.",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-discovered recipe registry helpers
|
|
3
|
+
* Zones: recipe discovery, tool exposure, migration diagnostics
|
|
4
|
+
* Owns filename identity discovery across prioritized recipe roots
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import type { RegisteredTool } from "./config.ts";
|
|
11
|
+
import type { TemplateRecipeConfig } from "./recipe-references.ts";
|
|
12
|
+
import * as RecipeReferences from "./recipe-references.ts";
|
|
13
|
+
import * as Schema from "./schema.ts";
|
|
14
|
+
|
|
15
|
+
export interface DiscoveredRecipe {
|
|
16
|
+
id: string;
|
|
17
|
+
path: string;
|
|
18
|
+
root: string;
|
|
19
|
+
priority: number;
|
|
20
|
+
config?: TemplateRecipeConfig;
|
|
21
|
+
active: boolean;
|
|
22
|
+
shadowed: boolean;
|
|
23
|
+
invalid: boolean;
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
tool: boolean;
|
|
26
|
+
mutableUsage: boolean;
|
|
27
|
+
diagnostics: string[];
|
|
28
|
+
shadows: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RecipeDiscoveryResult {
|
|
32
|
+
active: Map<string, DiscoveredRecipe>;
|
|
33
|
+
entries: DiscoveredRecipe[];
|
|
34
|
+
diagnostics: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RecipeDiscoverySource {
|
|
38
|
+
root?: string;
|
|
39
|
+
file?: string;
|
|
40
|
+
defaultTool?: boolean;
|
|
41
|
+
mutableUsage?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listRecipeFiles(root: string): string[] {
|
|
45
|
+
if (!existsSync(root)) return [];
|
|
46
|
+
return readdirSync(root, { withFileTypes: true })
|
|
47
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
48
|
+
.map((entry) => join(root, entry.name))
|
|
49
|
+
.sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readDiscoveredRecipe(
|
|
53
|
+
root: string,
|
|
54
|
+
file: string,
|
|
55
|
+
priority: number,
|
|
56
|
+
defaultTool = false,
|
|
57
|
+
mutableUsage = false,
|
|
58
|
+
): DiscoveredRecipe {
|
|
59
|
+
const id = RecipeReferences.getRecipeIdFromPath(file);
|
|
60
|
+
try {
|
|
61
|
+
const config = RecipeReferences.readResolvedRecipeConfig(file);
|
|
62
|
+
const invalid = !config;
|
|
63
|
+
const disabled = config?.disabled === true;
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
path: file,
|
|
67
|
+
root,
|
|
68
|
+
priority,
|
|
69
|
+
config,
|
|
70
|
+
active: false,
|
|
71
|
+
shadowed: false,
|
|
72
|
+
invalid,
|
|
73
|
+
disabled,
|
|
74
|
+
tool: (config?.tool ?? defaultTool) === true && !disabled && !invalid,
|
|
75
|
+
mutableUsage,
|
|
76
|
+
diagnostics: invalid ? [`Invalid recipe: ${file}`] : [],
|
|
77
|
+
shadows: [],
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
path: file,
|
|
83
|
+
root,
|
|
84
|
+
priority,
|
|
85
|
+
active: false,
|
|
86
|
+
shadowed: false,
|
|
87
|
+
invalid: true,
|
|
88
|
+
disabled: false,
|
|
89
|
+
tool: false,
|
|
90
|
+
mutableUsage,
|
|
91
|
+
diagnostics: [
|
|
92
|
+
`Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
93
|
+
],
|
|
94
|
+
shadows: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function filesForSource(
|
|
100
|
+
source: RecipeDiscoverySource,
|
|
101
|
+
): Array<{ root: string; file: string; defaultTool: boolean; mutableUsage: boolean }> {
|
|
102
|
+
const defaultTool = source.defaultTool === true;
|
|
103
|
+
const mutableUsage = source.mutableUsage === true;
|
|
104
|
+
if (source.file)
|
|
105
|
+
return [{ root: source.root ?? source.file, file: source.file, defaultTool, mutableUsage }];
|
|
106
|
+
return source.root
|
|
107
|
+
? listRecipeFiles(source.root).map((file) => ({
|
|
108
|
+
root: source.root!,
|
|
109
|
+
file,
|
|
110
|
+
defaultTool,
|
|
111
|
+
mutableUsage,
|
|
112
|
+
}))
|
|
113
|
+
: [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function discoverRecipeSources(
|
|
117
|
+
sources: RecipeDiscoverySource[],
|
|
118
|
+
): RecipeDiscoveryResult {
|
|
119
|
+
const entries = sources.flatMap((source, priority) =>
|
|
120
|
+
filesForSource(source).map(({ root, file, defaultTool, mutableUsage }) =>
|
|
121
|
+
readDiscoveredRecipe(root, file, priority, defaultTool, mutableUsage),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
const byId = new Map<string, DiscoveredRecipe[]>();
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const bucket = byId.get(entry.id) ?? [];
|
|
127
|
+
bucket.push(entry);
|
|
128
|
+
byId.set(entry.id, bucket);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const active = new Map<string, DiscoveredRecipe>();
|
|
132
|
+
const diagnostics: string[] = [];
|
|
133
|
+
for (const [id, bucket] of byId) {
|
|
134
|
+
bucket.sort(
|
|
135
|
+
(a, b) => a.priority - b.priority || a.path.localeCompare(b.path),
|
|
136
|
+
);
|
|
137
|
+
const winner = bucket[0];
|
|
138
|
+
winner.active = true;
|
|
139
|
+
winner.shadows = bucket.slice(1).map((entry) => entry.path);
|
|
140
|
+
active.set(id, winner);
|
|
141
|
+
for (const shadow of bucket.slice(1)) shadow.shadowed = true;
|
|
142
|
+
if (winner.invalid)
|
|
143
|
+
diagnostics.push(
|
|
144
|
+
`Recipe ${id} is invalid and blocks lower-priority recipes`,
|
|
145
|
+
);
|
|
146
|
+
if (winner.disabled)
|
|
147
|
+
diagnostics.push(
|
|
148
|
+
`Recipe ${id} is disabled and blocks lower-priority recipes`,
|
|
149
|
+
);
|
|
150
|
+
if (winner.shadows.length > 0)
|
|
151
|
+
diagnostics.push(
|
|
152
|
+
`Recipe ${id} shadows ${winner.shadows.length} lower-priority recipe(s)`,
|
|
153
|
+
);
|
|
154
|
+
diagnostics.push(...winner.diagnostics);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { active, entries, diagnostics };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function discoverRecipes(roots: string[]): RecipeDiscoveryResult {
|
|
161
|
+
return discoverRecipeSources(roots.map((root) => ({ root })));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function summarizeDiscovery(result: RecipeDiscoveryResult): Record<string, unknown> {
|
|
165
|
+
return {
|
|
166
|
+
active: [...result.active.values()].map((entry) => ({
|
|
167
|
+
id: entry.id,
|
|
168
|
+
path: entry.path,
|
|
169
|
+
description: entry.config?.description,
|
|
170
|
+
tool: entry.tool,
|
|
171
|
+
disabled: entry.disabled,
|
|
172
|
+
invalid: entry.invalid,
|
|
173
|
+
shadows: entry.shadows,
|
|
174
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
175
|
+
shadowed: result.entries
|
|
176
|
+
.filter((entry) => entry.shadowed)
|
|
177
|
+
.map((entry) => ({ id: entry.id, path: entry.path, shadowedBy: result.active.get(entry.id)?.path }))
|
|
178
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path)),
|
|
179
|
+
invalid: result.entries
|
|
180
|
+
.filter((entry) => entry.invalid)
|
|
181
|
+
.map((entry) => ({ id: entry.id, path: entry.path, diagnostics: entry.diagnostics }))
|
|
182
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
183
|
+
disabled: result.entries
|
|
184
|
+
.filter((entry) => entry.disabled)
|
|
185
|
+
.map((entry) => ({ id: entry.id, path: entry.path }))
|
|
186
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
187
|
+
diagnostics: result.diagnostics,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | undefined {
|
|
192
|
+
if (!entry.tool || entry.invalid || entry.disabled || !entry.config) return undefined;
|
|
193
|
+
const cfg = entry.config;
|
|
194
|
+
const template = entry.path;
|
|
195
|
+
const description = cfg.description ?? `Execute template recipe: ${entry.id}`;
|
|
196
|
+
const argTemplate = cfg.template;
|
|
197
|
+
const argTemplateConfig =
|
|
198
|
+
typeof argTemplate === "object" && !Array.isArray(argTemplate)
|
|
199
|
+
? {
|
|
200
|
+
...argTemplate,
|
|
201
|
+
...(cfg.args !== undefined ? { args: cfg.args } : {}),
|
|
202
|
+
defaults: { ...(argTemplate.defaults ?? {}), ...(cfg.defaults ?? {}) },
|
|
203
|
+
}
|
|
204
|
+
: { args: cfg.args, defaults: cfg.defaults ?? {}, template: argTemplate };
|
|
205
|
+
const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
206
|
+
return {
|
|
207
|
+
name: entry.id,
|
|
208
|
+
description,
|
|
209
|
+
template,
|
|
210
|
+
recipe: cfg,
|
|
211
|
+
args: Schema.getToolArgNames(argTemplateConfig),
|
|
212
|
+
defaults: Object.fromEntries(
|
|
213
|
+
Object.entries(cfg.defaults ?? {}).map(([key, value]) => [key, String(value)]),
|
|
214
|
+
),
|
|
215
|
+
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
216
|
+
...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
|
|
217
|
+
...(cfg.args ? { storedArgs: cfg.args } : {}),
|
|
218
|
+
...(cfg.defaults
|
|
219
|
+
? {
|
|
220
|
+
storedDefaults: Object.fromEntries(
|
|
221
|
+
Object.entries(cfg.defaults).map(([key, value]) => [key, String(value)]),
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
225
|
+
};
|
|
226
|
+
}
|