@llblab/pi-actors 0.36.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/CHANGELOG.md +6 -0
- package/dist/index.js +0 -1
- package/dist/lib/async-runs.js +4 -3
- package/dist/lib/command-templates.js +19 -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.d.ts +2 -6
- package/dist/lib/runtime.js +10 -11
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/skills/actors/SKILL.md +30 -15
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/template-recipes.md +19 -2
- package/index.ts +0 -1
- package/lib/async-runs.ts +4 -7
- package/lib/command-templates.ts +22 -7
- package/lib/recipes-references.ts +201 -34
- package/lib/registry.ts +5 -5
- package/lib/runtime.ts +10 -16
- package/lib/tools.ts +2 -2
- package/package.json +1 -1
- package/skills/actors/SKILL.md +30 -15
- package/skills/swarm/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.37.0: Direct Recipe Delegation And Quiet Overrides
|
|
6
|
+
|
|
7
|
+
- `[Recipes]` Added direct recipe delegation from `template` strings so thin wrappers can point at ready recipe names/paths while preserving priority resolution, inherited metadata, and import-based composition for richer graphs.
|
|
8
|
+
- `[Command Templates]` Fixed inherited/default placeholder resolution for arrays and repeat-indexed values, unblocking lens-swarm recipes that pass `lenses` through wrapper defaults into `{lenses.length}` and `{lenses[index]}`.
|
|
9
|
+
- `[Registry]` Stopped treating same-name recipe/tool registration as a startup warning: higher-priority user recipes now quietly override lower-priority recipe/tool definitions as normal composition behavior, while reserved core tool names remain protected.
|
|
10
|
+
|
|
5
11
|
## 0.36.0: Recipe Diagnostics And Runtime Triage
|
|
6
12
|
|
|
7
13
|
- `[Recipe Doctor]` Added deterministic advisory risk labels for discovered recipes, including shell, eval, filesystem mutation, network, external side effect, long-running, platform-specific, and secret-touching signals in verbose recipe inspection plus compact doctor risk counts.
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,6 @@ export default function toolRegistryExtension(pi) {
|
|
|
88
88
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
89
89
|
exec: CommandTemplates.execCommandTemplate,
|
|
90
90
|
getActiveTools: () => pi.getActiveTools(),
|
|
91
|
-
getAllTools: () => pi.getAllTools(),
|
|
92
91
|
registerTool: (definition) => {
|
|
93
92
|
actorToolDefinitions.set(definition.name, definition);
|
|
94
93
|
pi.registerTool(definition);
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as RunsStart from "./runs-start.js";
|
|
|
20
20
|
import * as RunsIndex from "./runs-index.js";
|
|
21
21
|
import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
|
|
22
22
|
import { deliverRunMessage, } from "./runs-messages.js";
|
|
23
|
-
import { buildRunStatus, tailFile, tailLines
|
|
23
|
+
import { buildRunStatus, tailFile, tailLines } from "./runs-status.js";
|
|
24
24
|
import { readJsonFileResilient } from "./state-readers.js";
|
|
25
25
|
const RUNNER_IDENTITY_GRACE_MS = 5000;
|
|
26
26
|
const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
|
|
@@ -77,8 +77,9 @@ function assertNoActiveRunState(stateDir) {
|
|
|
77
77
|
RunsStart.assertNoActiveRunState(stateDir, readJson, RUNNER_PATH);
|
|
78
78
|
}
|
|
79
79
|
function resolveRecipeFile(file) {
|
|
80
|
-
return (RecipesReferences.
|
|
81
|
-
RecipesReferences.
|
|
80
|
+
return (RecipesReferences.resolveRecipeReferencePath(file, Paths.getRecipeRoot()) ??
|
|
81
|
+
RecipesReferences.getRecipePath(file, Paths.getRecipeRoot()) ??
|
|
82
|
+
RecipesReferences.resolveRecipePath(file, Paths.getRecipeRoot()));
|
|
82
83
|
}
|
|
83
84
|
function isMutableUsageRecipeFile(file) {
|
|
84
85
|
const userRoot = resolve(DEFAULT_RECIPE_ROOT);
|
|
@@ -44,18 +44,32 @@ function normalizeCommandTemplateDefaults(defaults) {
|
|
|
44
44
|
return normalized;
|
|
45
45
|
}
|
|
46
46
|
export function resolveInheritedDefaultReferences(ownDefaults, inheritedDefaults, runtimeValues = {}) {
|
|
47
|
-
if (!ownDefaults
|
|
47
|
+
if (!ownDefaults)
|
|
48
48
|
return ownDefaults;
|
|
49
49
|
const resolved = { ...ownDefaults };
|
|
50
|
+
const values = { ...(inheritedDefaults ?? {}), ...runtimeValues };
|
|
50
51
|
for (const [key, value] of Object.entries(ownDefaults)) {
|
|
51
52
|
if (typeof value !== "string")
|
|
52
53
|
continue;
|
|
53
54
|
const exact = /^\{([A-Za-z_][A-Za-z0-9_-]*)\}$/.exec(value);
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
!Object.hasOwn(inheritedDefaults, exact[1]))
|
|
55
|
+
if (exact && Object.hasOwn(values, exact[1])) {
|
|
56
|
+
resolved[key] = values[exact[1]];
|
|
57
57
|
continue;
|
|
58
|
-
|
|
58
|
+
}
|
|
59
|
+
const indexed = value.match(/^\{([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]\}$/);
|
|
60
|
+
if (!indexed)
|
|
61
|
+
continue;
|
|
62
|
+
const source = values[indexed[1]];
|
|
63
|
+
const indexValue = /^\d+$/.test(indexed[2])
|
|
64
|
+
? indexed[2]
|
|
65
|
+
: values[indexed[2]];
|
|
66
|
+
const index = Number(indexValue);
|
|
67
|
+
if (Array.isArray(source) &&
|
|
68
|
+
Number.isInteger(index) &&
|
|
69
|
+
index >= 0 &&
|
|
70
|
+
index < source.length) {
|
|
71
|
+
resolved[key] = source[index] ?? "";
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
74
|
return resolved;
|
|
61
75
|
}
|
|
@@ -65,6 +65,7 @@ export interface ReadResolvedRecipeConfigOptions {
|
|
|
65
65
|
includeActorRecipeContext?: boolean;
|
|
66
66
|
}
|
|
67
67
|
export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
|
|
68
|
+
export declare function resolveRecipeReferencePath(value: unknown, currentRecipeRoot?: string): string | undefined;
|
|
68
69
|
export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
|
|
69
70
|
export declare function diagnoseRawRecipeConfigFailure(path: string): string | undefined;
|
|
70
71
|
export declare function readRawRecipeConfig(path: string): Record<string, unknown> | undefined;
|
|
@@ -40,19 +40,43 @@ function recipeNameFiles(value) {
|
|
|
40
40
|
return [trimmed];
|
|
41
41
|
return [`${trimmed}.json`, `${trimmed}.md`];
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function recipeCandidatePaths(value, currentRecipeRoot) {
|
|
44
44
|
if (!isBareRecipeName(value))
|
|
45
|
-
return resolveRecipePath(value, currentRecipeRoot);
|
|
45
|
+
return [resolveRecipePath(value, currentRecipeRoot)];
|
|
46
46
|
const roots = [
|
|
47
47
|
Paths.getRecipeRoot(),
|
|
48
48
|
currentRecipeRoot,
|
|
49
49
|
Paths.getPackagedRecipeRoot(),
|
|
50
50
|
];
|
|
51
|
-
|
|
51
|
+
return [
|
|
52
52
|
...new Set(roots.flatMap((root) => recipeNameFiles(value).map((file) => resolve(root, file)))),
|
|
53
53
|
];
|
|
54
|
+
}
|
|
55
|
+
function resolveRecipeImportPath(value, currentRecipeRoot) {
|
|
56
|
+
const candidates = recipeCandidatePaths(value, currentRecipeRoot);
|
|
54
57
|
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
55
58
|
}
|
|
59
|
+
export function resolveRecipeReferencePath(value, currentRecipeRoot = Paths.getRecipeRoot()) {
|
|
60
|
+
if (typeof value !== "string")
|
|
61
|
+
return undefined;
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed || hasWhitespace(trimmed))
|
|
64
|
+
return undefined;
|
|
65
|
+
for (const path of recipeCandidatePaths(trimmed, currentRecipeRoot)) {
|
|
66
|
+
if (!existsSync(path))
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const raw = readRawRecipeConfig(path);
|
|
70
|
+
if (raw && typeof raw === "object" && Object.hasOwn(raw, "template"))
|
|
71
|
+
return path;
|
|
72
|
+
return path;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
56
80
|
export function getRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
|
|
57
81
|
if (typeof value !== "string")
|
|
58
82
|
return undefined;
|
|
@@ -61,20 +85,7 @@ export function getRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
|
|
|
61
85
|
return undefined;
|
|
62
86
|
if (trimmed.endsWith(".json") || trimmed.endsWith(".md"))
|
|
63
87
|
return resolveRecipePath(trimmed, recipeRoot);
|
|
64
|
-
|
|
65
|
-
const mdPath = resolveRecipePath(`${trimmed}.md`, recipeRoot);
|
|
66
|
-
const path = existsSync(jsonPath) ? jsonPath : mdPath;
|
|
67
|
-
if (!existsSync(path))
|
|
68
|
-
return undefined;
|
|
69
|
-
try {
|
|
70
|
-
const raw = readRawRecipeConfig(path);
|
|
71
|
-
return raw && typeof raw === "object" && Object.hasOwn(raw, "template")
|
|
72
|
-
? path
|
|
73
|
-
: undefined;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
88
|
+
return resolveRecipeReferencePath(trimmed, recipeRoot);
|
|
78
89
|
}
|
|
79
90
|
function isImportNode(value) {
|
|
80
91
|
if (!isRecord(value) || Object.hasOwn(value, "template"))
|
|
@@ -344,7 +355,7 @@ export function getRecipeIdFromPath(file) {
|
|
|
344
355
|
return basename(file, extname(file));
|
|
345
356
|
}
|
|
346
357
|
function readRecipeConfig(value) {
|
|
347
|
-
const path =
|
|
358
|
+
const path = resolveRecipeReferencePath(value);
|
|
348
359
|
return path ? readResolvedRecipeConfig(path) : undefined;
|
|
349
360
|
}
|
|
350
361
|
function isRecord(value) {
|
|
@@ -483,6 +494,7 @@ function mergeDefaults(...items) {
|
|
|
483
494
|
function applyDefaultsToTemplate(template, defaults, overrides) {
|
|
484
495
|
const cleanOverrides = { ...overrides };
|
|
485
496
|
delete cleanOverrides.name;
|
|
497
|
+
delete cleanOverrides.template;
|
|
486
498
|
delete cleanOverrides.values;
|
|
487
499
|
if (typeof template === "object" && !Array.isArray(template)) {
|
|
488
500
|
return {
|
|
@@ -509,6 +521,56 @@ function withActorRecipeContext(value, context) {
|
|
|
509
521
|
}
|
|
510
522
|
return { actorRecipeContext: context, template: value };
|
|
511
523
|
}
|
|
524
|
+
function loadDelegatedRecipe(value, currentRecipeFile, stack, options) {
|
|
525
|
+
const path = resolveRecipeReferencePath(value, dirname(currentRecipeFile));
|
|
526
|
+
if (!path)
|
|
527
|
+
return undefined;
|
|
528
|
+
const config = readResolvedRecipeConfig(path, [...stack, currentRecipeFile], options);
|
|
529
|
+
if (!config)
|
|
530
|
+
throw new Error(`Template recipe must define template: ${path}`);
|
|
531
|
+
if (config.disabled === true)
|
|
532
|
+
throw new Error(`Template recipe is disabled: ${path}`);
|
|
533
|
+
return config;
|
|
534
|
+
}
|
|
535
|
+
function applyDelegatedRecipeToNode(delegated, overrides = {}) {
|
|
536
|
+
return applyDefaultsToTemplate(delegated.template, delegated.values, overrides);
|
|
537
|
+
}
|
|
538
|
+
function expandRecipeDelegations(value, currentRecipeFile, stack, options = {}) {
|
|
539
|
+
if (typeof value === "string") {
|
|
540
|
+
const delegated = loadDelegatedRecipe(value, currentRecipeFile, stack, options);
|
|
541
|
+
return delegated ? applyDelegatedRecipeToNode(delegated) : value;
|
|
542
|
+
}
|
|
543
|
+
if (Array.isArray(value)) {
|
|
544
|
+
return value.map((item) => expandRecipeDelegations(item, currentRecipeFile, stack, options));
|
|
545
|
+
}
|
|
546
|
+
const record = value;
|
|
547
|
+
if (typeof record.template === "string") {
|
|
548
|
+
const delegated = loadDelegatedRecipe(record.template, currentRecipeFile, stack, options);
|
|
549
|
+
if (delegated)
|
|
550
|
+
return applyDelegatedRecipeToNode(delegated, record);
|
|
551
|
+
}
|
|
552
|
+
if (Array.isArray(record.template)) {
|
|
553
|
+
return {
|
|
554
|
+
...record,
|
|
555
|
+
template: record.template.map((item) => expandRecipeDelegations(item, currentRecipeFile, stack, options)),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (record.template && typeof record.template === "object") {
|
|
559
|
+
return {
|
|
560
|
+
...record,
|
|
561
|
+
template: expandRecipeDelegations(record.template, currentRecipeFile, stack, options),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
function getDirectDelegatedRecipe(value, currentRecipeFile, stack, options = {}) {
|
|
567
|
+
if (typeof value === "string")
|
|
568
|
+
return loadDelegatedRecipe(value, currentRecipeFile, stack, options);
|
|
569
|
+
if (!Array.isArray(value) && typeof value.template === "string") {
|
|
570
|
+
return loadDelegatedRecipe(value.template, currentRecipeFile, stack, options);
|
|
571
|
+
}
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
512
574
|
function expandImportNodes(value, imports, options = {}) {
|
|
513
575
|
if (typeof value === "string")
|
|
514
576
|
return value;
|
|
@@ -585,7 +647,14 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
585
647
|
const template = getRecipeCommandTemplate(substituted);
|
|
586
648
|
if (!template)
|
|
587
649
|
return undefined;
|
|
588
|
-
const
|
|
650
|
+
const expandedImportsTemplate = expandImportNodes(template, imports, options);
|
|
651
|
+
const delegated = getDirectDelegatedRecipe(expandedImportsTemplate, path, stack, options);
|
|
652
|
+
const expandedTemplate = delegated
|
|
653
|
+
? applyDelegatedRecipeToNode(delegated, typeof expandedImportsTemplate === "object" &&
|
|
654
|
+
!Array.isArray(expandedImportsTemplate)
|
|
655
|
+
? expandedImportsTemplate
|
|
656
|
+
: {})
|
|
657
|
+
: expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
|
|
589
658
|
const recipeName = getRecipeIdFromPath(path);
|
|
590
659
|
const templateWithContext = options.includeActorRecipeContext
|
|
591
660
|
? withActorRecipeContext(expandedTemplate, {
|
|
@@ -595,33 +664,50 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
595
664
|
role: stack.length > 0 ? "import" : "entry",
|
|
596
665
|
})
|
|
597
666
|
: expandedTemplate;
|
|
667
|
+
const mergedDefaults = mergeDefaults(delegated?.defaults, isRecord(substituted.defaults) ? substituted.defaults : undefined);
|
|
668
|
+
const artifactSource = isRecord(substituted.artifacts)
|
|
669
|
+
? substituted.artifacts
|
|
670
|
+
: delegated?.artifacts;
|
|
671
|
+
const mailboxSource = isRecord(substituted.mailbox)
|
|
672
|
+
? substituted.mailbox
|
|
673
|
+
: delegated?.mailbox;
|
|
598
674
|
return {
|
|
599
675
|
name: recipeName,
|
|
600
676
|
...(typeof substituted.description === "string" &&
|
|
601
677
|
substituted.description.trim()
|
|
602
678
|
? { description: substituted.description.trim() }
|
|
603
|
-
:
|
|
679
|
+
: typeof delegated?.description === "string"
|
|
680
|
+
? { description: delegated.description }
|
|
681
|
+
: {}),
|
|
604
682
|
...(typeof substituted.disabled === "boolean"
|
|
605
683
|
? { disabled: substituted.disabled }
|
|
606
|
-
:
|
|
684
|
+
: typeof delegated?.disabled === "boolean"
|
|
685
|
+
? { disabled: delegated.disabled }
|
|
686
|
+
: {}),
|
|
607
687
|
...(substituted.async === true
|
|
608
688
|
? { async: true }
|
|
609
689
|
: substituted.async === false
|
|
610
690
|
? { async: false }
|
|
611
|
-
:
|
|
691
|
+
: delegated?.async === true
|
|
692
|
+
? { async: true }
|
|
693
|
+
: delegated?.async === false
|
|
694
|
+
? { async: false }
|
|
695
|
+
: {}),
|
|
612
696
|
...(typeof substituted.state_dir === "string"
|
|
613
697
|
? { state_dir: substituted.state_dir }
|
|
614
|
-
:
|
|
698
|
+
: typeof delegated?.state_dir === "string"
|
|
699
|
+
? { state_dir: delegated.state_dir }
|
|
700
|
+
: {}),
|
|
615
701
|
...(Object.keys(imports).length > 0
|
|
616
702
|
? { imports: getRecipeImports(raw) }
|
|
617
703
|
: {}),
|
|
618
704
|
template: templateWithContext,
|
|
619
705
|
...(Array.isArray(substituted.args)
|
|
620
706
|
? { args: substituted.args }
|
|
621
|
-
:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
707
|
+
: Array.isArray(delegated?.args)
|
|
708
|
+
? { args: delegated.args }
|
|
709
|
+
: {}),
|
|
710
|
+
...(mergedDefaults ? { defaults: mergedDefaults } : {}),
|
|
625
711
|
...(typeof substituted.parallel === "boolean"
|
|
626
712
|
? { parallel: substituted.parallel }
|
|
627
713
|
: {}),
|
|
@@ -643,30 +729,31 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
643
729
|
...(typeof substituted.output === "string"
|
|
644
730
|
? { output: substituted.output }
|
|
645
731
|
: {}),
|
|
646
|
-
...(isRecord(
|
|
732
|
+
...(isRecord(artifactSource)
|
|
647
733
|
? {
|
|
648
|
-
artifacts: Object.fromEntries(Object.entries(
|
|
734
|
+
artifacts: Object.fromEntries(Object.entries(artifactSource).filter((entry) => typeof entry[1] === "string")),
|
|
649
735
|
}
|
|
650
736
|
: {}),
|
|
651
|
-
...(isRecord(
|
|
737
|
+
...(isRecord(mailboxSource)
|
|
652
738
|
? {
|
|
653
739
|
mailbox: {
|
|
654
|
-
...(Array.isArray(
|
|
740
|
+
...(Array.isArray(mailboxSource.accepts)
|
|
655
741
|
? {
|
|
656
|
-
accepts:
|
|
742
|
+
accepts: mailboxSource.accepts.filter((value) => typeof value === "string" ||
|
|
657
743
|
(isRecord(value) && typeof value.type === "string")),
|
|
658
744
|
}
|
|
659
745
|
: {}),
|
|
660
|
-
...(Array.isArray(
|
|
746
|
+
...(Array.isArray(mailboxSource.emits)
|
|
661
747
|
? {
|
|
662
|
-
emits:
|
|
748
|
+
emits: mailboxSource.emits.filter((value) => typeof value === "string" ||
|
|
663
749
|
(isRecord(value) && typeof value.type === "string")),
|
|
664
750
|
}
|
|
665
751
|
: {}),
|
|
666
752
|
},
|
|
667
753
|
}
|
|
668
754
|
: {}),
|
|
669
|
-
...(substituted.retire_when === "children_terminal"
|
|
755
|
+
...(substituted.retire_when === "children_terminal" ||
|
|
756
|
+
delegated?.retire_when === "children_terminal"
|
|
670
757
|
? { retire_when: "children_terminal" }
|
|
671
758
|
: {}),
|
|
672
759
|
...(typeof substituted.retry === "number" ||
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface RegisterToolResult {
|
|
|
39
39
|
export interface RegisterToolRuntimeDeps<TContext> {
|
|
40
40
|
configPath: string;
|
|
41
41
|
recipeRoot?: string;
|
|
42
|
-
|
|
42
|
+
getToolNameBlocker: (name: string) => string | undefined;
|
|
43
43
|
getTools: () => Map<string, Config.RegisteredTool>;
|
|
44
44
|
getActiveTools: () => string[];
|
|
45
45
|
notify: (ctx: TContext, message: string, type: "info" | "warning" | "error") => void;
|
package/dist/lib/registry.js
CHANGED
|
@@ -51,9 +51,9 @@ function promoteDraftRecipe(name, input, ctx, deps) {
|
|
|
51
51
|
const targetPath = getToolRecipePath(deps, name);
|
|
52
52
|
const tools = deps.getTools();
|
|
53
53
|
const existing = tools.get(name);
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
throw new Error(ExecutionOutput.formatToolText(
|
|
54
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
55
|
+
if (blocker)
|
|
56
|
+
throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
57
57
|
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
58
58
|
throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
|
|
59
59
|
}
|
|
@@ -238,9 +238,9 @@ export async function executeRegisterTool(params, ctx, deps) {
|
|
|
238
238
|
return deleteTool(name, ctx, deps);
|
|
239
239
|
const tools = deps.getTools();
|
|
240
240
|
const existing = tools.get(name);
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
throw new Error(ExecutionOutput.formatToolText(
|
|
241
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
242
|
+
if (blocker)
|
|
243
|
+
throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
244
244
|
if (existing && !input.update) {
|
|
245
245
|
throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
|
|
246
246
|
}
|
package/dist/lib/runtime.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool registry runtime coordinator
|
|
3
3
|
* Zones: runtime coordination, registry loading, pi tools
|
|
4
|
-
* Owns persisted tool loading,
|
|
4
|
+
* Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
|
|
5
5
|
*/
|
|
6
6
|
import * as Config from "./config.ts";
|
|
7
7
|
import type { RegisteredToolExec } from "./execution.ts";
|
|
@@ -12,22 +12,18 @@ export interface RuntimeContext {
|
|
|
12
12
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
export interface ToolInfoLike {
|
|
16
|
-
name: string;
|
|
17
|
-
}
|
|
18
15
|
export interface ToolRegistryRuntimeDeps {
|
|
19
16
|
configPath: string;
|
|
20
17
|
exec: RegisteredToolExec;
|
|
21
18
|
packagedRecipeRoot?: string;
|
|
22
19
|
recipeRoot?: string;
|
|
23
20
|
getActiveTools?: () => string[];
|
|
24
|
-
getAllTools: () => ToolInfoLike[];
|
|
25
21
|
registerTool: (definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>) => void;
|
|
26
22
|
reservedToolNames: Set<string>;
|
|
27
23
|
setActiveTools?: (toolNames: string[]) => void;
|
|
28
24
|
}
|
|
29
25
|
export interface ToolRegistryRuntime {
|
|
30
|
-
|
|
26
|
+
getToolNameBlocker(name: string): string | undefined;
|
|
31
27
|
getTools(): Map<string, Config.RegisteredTool>;
|
|
32
28
|
loadTools(ctx: RuntimeContext): void;
|
|
33
29
|
notify(ctx: RuntimeContext, message: string, type: "info" | "warning" | "error"): void;
|
package/dist/lib/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool registry runtime coordinator
|
|
3
3
|
* Zones: runtime coordination, registry loading, pi tools
|
|
4
|
-
* Owns persisted tool loading,
|
|
4
|
+
* Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, watch } from "node:fs";
|
|
7
7
|
import * as Paths from "./paths.js";
|
|
@@ -15,12 +15,9 @@ export function createAutoToolsRuntime(deps) {
|
|
|
15
15
|
if (ctx.hasUI)
|
|
16
16
|
ctx.ui.notify(message, type);
|
|
17
17
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const existing = deps.getAllTools().find((tool) => tool.name === name);
|
|
22
|
-
return existing
|
|
23
|
-
? `Tool "${name}" is already registered outside pi-actors.`
|
|
18
|
+
function getToolNameBlocker(name) {
|
|
19
|
+
return deps.reservedToolNames.has(name)
|
|
20
|
+
? `Reserved tool name: ${name}`
|
|
24
21
|
: undefined;
|
|
25
22
|
}
|
|
26
23
|
function getToolFingerprint(cfg) {
|
|
@@ -55,6 +52,8 @@ export function createAutoToolsRuntime(deps) {
|
|
|
55
52
|
runtimeToolFingerprints.set(cfg.name, fingerprint);
|
|
56
53
|
}
|
|
57
54
|
function isStartupActionableRegistryWarning(warning) {
|
|
55
|
+
if (warning.includes(" shadows "))
|
|
56
|
+
return false;
|
|
58
57
|
if (warning.includes("invokes bash;") &&
|
|
59
58
|
warning.includes("trusted executable content"))
|
|
60
59
|
return false;
|
|
@@ -104,9 +103,9 @@ export function createAutoToolsRuntime(deps) {
|
|
|
104
103
|
}
|
|
105
104
|
deactivateMissingRuntimeTools(new Set(tools.keys()));
|
|
106
105
|
for (const cfg of tools.values()) {
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
warnings.push(
|
|
106
|
+
const blocker = getToolNameBlocker(cfg.name);
|
|
107
|
+
if (blocker) {
|
|
108
|
+
warnings.push(blocker);
|
|
110
109
|
continue;
|
|
111
110
|
}
|
|
112
111
|
registerRuntimeTool(cfg);
|
|
@@ -117,7 +116,7 @@ export function createAutoToolsRuntime(deps) {
|
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
return {
|
|
120
|
-
|
|
119
|
+
getToolNameBlocker,
|
|
121
120
|
getTools: () => tools,
|
|
122
121
|
loadTools,
|
|
123
122
|
notify,
|
package/dist/lib/tools.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext
|
|
|
15
15
|
configPath: string;
|
|
16
16
|
getActiveTools: () => string[];
|
|
17
17
|
getRuntimeTool: (name: string) => unknown;
|
|
18
|
-
registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "
|
|
18
|
+
registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool">;
|
|
19
19
|
setActiveTools: (toolNames: string[]) => void;
|
|
20
20
|
}
|
|
21
21
|
export declare const RESERVED_TOOL_NAMES: Set<string>;
|
package/dist/lib/tools.js
CHANGED
|
@@ -25,7 +25,7 @@ export function createCoreActorToolDefinitions(deps) {
|
|
|
25
25
|
ToolsRegister.createRegisterToolDefinition({
|
|
26
26
|
configPath: deps.configPath,
|
|
27
27
|
getActiveTools: deps.getActiveTools,
|
|
28
|
-
|
|
28
|
+
getToolNameBlocker: deps.registryRuntime.getToolNameBlocker,
|
|
29
29
|
getTools: deps.registryRuntime.getTools,
|
|
30
30
|
notify: deps.registryRuntime.notify,
|
|
31
31
|
registerRuntimeTool: deps.registryRuntime.registerRuntimeTool,
|
|
@@ -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,13 +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. Declare `
|
|
200
|
-
8.
|
|
201
|
-
9. File-backed
|
|
202
|
-
10.
|
|
203
|
-
11.
|
|
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.
|
|
204
205
|
|
|
205
206
|
Priority for same-id recipes:
|
|
206
207
|
|
|
@@ -209,7 +210,7 @@ Priority for same-id recipes:
|
|
|
209
210
|
3. Explicit ad hoc user recipe file outside `~/.pi/agent/recipes`.
|
|
210
211
|
4. User recipe in `~/.pi/agent/recipes/*.json` or `*.md`: highest-priority operator tool surface.
|
|
211
212
|
|
|
212
|
-
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.
|
|
213
214
|
|
|
214
215
|
Muscle-memory lens: pi-actors has two durable executable-memory layers.
|
|
215
216
|
|
|
@@ -230,7 +231,21 @@ Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memo
|
|
|
230
231
|
|
|
231
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.
|
|
232
233
|
|
|
233
|
-
Ready-recipe registration
|
|
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:
|
|
234
249
|
|
|
235
250
|
```json
|
|
236
251
|
{
|
|
@@ -243,16 +258,16 @@ Ready-recipe registration pattern:
|
|
|
243
258
|
}
|
|
244
259
|
```
|
|
245
260
|
|
|
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.
|
|
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.
|
|
247
262
|
|
|
248
263
|
Tool-registration lenses are open-ended prompts for deciding what deserves durable tool status:
|
|
249
264
|
|
|
250
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.
|
|
251
266
|
2. **Safety lens**: prefer read-only diagnostics, dry-runs, preflights, confirmations, or bounded adapters around high-impact operations before registering direct action tools.
|
|
252
267
|
3. **Context-affordance lens**: register tools whose mere presence in the injected capability list should steer agents toward the right operational habit.
|
|
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.
|
|
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.
|
|
256
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.
|
|
257
272
|
|
|
258
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.
|
|
@@ -260,7 +275,7 @@ Default bias: register diagnostic/preflight tools before action tools, and promo
|
|
|
260
275
|
Tool templates may be:
|
|
261
276
|
|
|
262
277
|
- A foreground command template.
|
|
263
|
-
- A file-backed recipe name/path.
|
|
278
|
+
- A file-backed recipe name/path for thin delegation.
|
|
264
279
|
- A complete recipe body, optionally `async: true`.
|
|
265
280
|
|
|
266
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/docs/template-recipes.md
CHANGED
|
@@ -292,9 +292,26 @@ 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
|
|
295
|
+
Use imports as the default adapter for composed ready recipes. A user-root recipe such as `~/.pi/agent/recipes/repo_context_check.json` can import the maintained source recipe and call `{ "name": "alias" }` instead of duplicating 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 should delegate to or import the skill recipe instead of invoking the script path directly.
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
## Direct Recipe Delegation
|
|
298
|
+
|
|
299
|
+
A `template` string that resolves to a recipe name or recipe file path delegates to that recipe instead of executing the recipe file as a binary:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"description": "Play music through the packaged player recipe.",
|
|
304
|
+
"async": true,
|
|
305
|
+
"defaults": { "source": "~/Music", "volume": "70" },
|
|
306
|
+
"template": "music-player"
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Delegation is a one-to-one handoff for thin wrappers and simple reuse. It uses the same bare-name priority as imports: user-root recipes under `~/.pi/agent/recipes`, then the importing recipe's directory, then packaged standard-library recipes. A higher-priority invalid or disabled recipe still blocks lower-priority fallback so operator overrides fail closed.
|
|
311
|
+
|
|
312
|
+
Delegated recipes remain the source of truth for their command template, async setting, args/defaults, mailbox, artifacts, and future fixes. Wrapper recipe fields may narrow args/defaults or override lifecycle metadata. Use imports plus `{ "name": "alias" }` when you need rich composition, multiple recipe nodes, import-specific values/defaults, or a pipeline where the reusable recipe is only one step.
|
|
313
|
+
|
|
314
|
+
Async composition stays explicit: importing or delegating to 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; thin delegation may inherit `async: true` from the delegated recipe. 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.
|
|
298
315
|
|
|
299
316
|
```json
|
|
300
317
|
{
|