@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/index.ts
CHANGED
|
@@ -106,7 +106,6 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
106
106
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
107
107
|
exec: CommandTemplates.execCommandTemplate,
|
|
108
108
|
getActiveTools: () => pi.getActiveTools(),
|
|
109
|
-
getAllTools: () => pi.getAllTools(),
|
|
110
109
|
registerTool: (definition) => {
|
|
111
110
|
actorToolDefinitions.set(definition.name, definition);
|
|
112
111
|
pi.registerTool(definition);
|
package/lib/async-runs.ts
CHANGED
|
@@ -67,11 +67,7 @@ import {
|
|
|
67
67
|
deliverRunMessage,
|
|
68
68
|
type SendRunMessageOptions,
|
|
69
69
|
} from "./runs-messages.ts";
|
|
70
|
-
import {
|
|
71
|
-
buildRunStatus,
|
|
72
|
-
tailFile,
|
|
73
|
-
tailLines,
|
|
74
|
-
} from "./runs-status.ts";
|
|
70
|
+
import { buildRunStatus, tailFile, tailLines } from "./runs-status.ts";
|
|
75
71
|
import { readJsonFileResilient } from "./state-readers.ts";
|
|
76
72
|
|
|
77
73
|
const RUNNER_IDENTITY_GRACE_MS = 5000;
|
|
@@ -213,8 +209,9 @@ function assertNoActiveRunState(stateDir: string): void {
|
|
|
213
209
|
|
|
214
210
|
function resolveRecipeFile(file: string): string {
|
|
215
211
|
return (
|
|
216
|
-
RecipesReferences.
|
|
217
|
-
RecipesReferences.
|
|
212
|
+
RecipesReferences.resolveRecipeReferencePath(file, Paths.getRecipeRoot()) ??
|
|
213
|
+
RecipesReferences.getRecipePath(file, Paths.getRecipeRoot()) ??
|
|
214
|
+
RecipesReferences.resolveRecipePath(file, Paths.getRecipeRoot())
|
|
218
215
|
);
|
|
219
216
|
}
|
|
220
217
|
|
package/lib/command-templates.ts
CHANGED
|
@@ -134,18 +134,33 @@ export function resolveInheritedDefaultReferences(
|
|
|
134
134
|
inheritedDefaults: Record<string, unknown> | undefined,
|
|
135
135
|
runtimeValues: Record<string, unknown> = {},
|
|
136
136
|
): Record<string, unknown> | undefined {
|
|
137
|
-
if (!ownDefaults
|
|
137
|
+
if (!ownDefaults) return ownDefaults;
|
|
138
138
|
const resolved = { ...ownDefaults };
|
|
139
|
+
const values = { ...(inheritedDefaults ?? {}), ...runtimeValues };
|
|
139
140
|
for (const [key, value] of Object.entries(ownDefaults)) {
|
|
140
141
|
if (typeof value !== "string") continue;
|
|
141
142
|
const exact = /^\{([A-Za-z_][A-Za-z0-9_-]*)\}$/.exec(value);
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
Object.hasOwn(runtimeValues, exact[1]) ||
|
|
145
|
-
!Object.hasOwn(inheritedDefaults, exact[1])
|
|
146
|
-
)
|
|
143
|
+
if (exact && Object.hasOwn(values, exact[1])) {
|
|
144
|
+
resolved[key] = values[exact[1]];
|
|
147
145
|
continue;
|
|
148
|
-
|
|
146
|
+
}
|
|
147
|
+
const indexed = value.match(
|
|
148
|
+
/^\{([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]\}$/,
|
|
149
|
+
);
|
|
150
|
+
if (!indexed) continue;
|
|
151
|
+
const source = values[indexed[1]];
|
|
152
|
+
const indexValue = /^\d+$/.test(indexed[2])
|
|
153
|
+
? indexed[2]
|
|
154
|
+
: values[indexed[2]];
|
|
155
|
+
const index = Number(indexValue);
|
|
156
|
+
if (
|
|
157
|
+
Array.isArray(source) &&
|
|
158
|
+
Number.isInteger(index) &&
|
|
159
|
+
index >= 0 &&
|
|
160
|
+
index < source.length
|
|
161
|
+
) {
|
|
162
|
+
resolved[key] = source[index] ?? "";
|
|
163
|
+
}
|
|
149
164
|
}
|
|
150
165
|
return resolved;
|
|
151
166
|
}
|
|
@@ -135,27 +135,55 @@ function recipeNameFiles(value: string): string[] {
|
|
|
135
135
|
return [`${trimmed}.json`, `${trimmed}.md`];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function
|
|
138
|
+
function recipeCandidatePaths(
|
|
139
139
|
value: string,
|
|
140
140
|
currentRecipeRoot: string,
|
|
141
|
-
): string {
|
|
141
|
+
): string[] {
|
|
142
142
|
if (!isBareRecipeName(value))
|
|
143
|
-
return resolveRecipePath(value, currentRecipeRoot);
|
|
143
|
+
return [resolveRecipePath(value, currentRecipeRoot)];
|
|
144
144
|
const roots = [
|
|
145
145
|
Paths.getRecipeRoot(),
|
|
146
146
|
currentRecipeRoot,
|
|
147
147
|
Paths.getPackagedRecipeRoot(),
|
|
148
148
|
];
|
|
149
|
-
|
|
149
|
+
return [
|
|
150
150
|
...new Set(
|
|
151
151
|
roots.flatMap((root) =>
|
|
152
152
|
recipeNameFiles(value).map((file) => resolve(root, file)),
|
|
153
153
|
),
|
|
154
154
|
),
|
|
155
155
|
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveRecipeImportPath(
|
|
159
|
+
value: string,
|
|
160
|
+
currentRecipeRoot: string,
|
|
161
|
+
): string {
|
|
162
|
+
const candidates = recipeCandidatePaths(value, currentRecipeRoot);
|
|
156
163
|
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
export function resolveRecipeReferencePath(
|
|
167
|
+
value: unknown,
|
|
168
|
+
currentRecipeRoot = Paths.getRecipeRoot(),
|
|
169
|
+
): string | undefined {
|
|
170
|
+
if (typeof value !== "string") return undefined;
|
|
171
|
+
const trimmed = value.trim();
|
|
172
|
+
if (!trimmed || hasWhitespace(trimmed)) return undefined;
|
|
173
|
+
for (const path of recipeCandidatePaths(trimmed, currentRecipeRoot)) {
|
|
174
|
+
if (!existsSync(path)) continue;
|
|
175
|
+
try {
|
|
176
|
+
const raw = readRawRecipeConfig(path);
|
|
177
|
+
if (raw && typeof raw === "object" && Object.hasOwn(raw, "template"))
|
|
178
|
+
return path;
|
|
179
|
+
return path;
|
|
180
|
+
} catch {
|
|
181
|
+
return path;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
export function getRecipePath(
|
|
160
188
|
value: unknown,
|
|
161
189
|
recipeRoot = Paths.getRecipeRoot(),
|
|
@@ -165,18 +193,7 @@ export function getRecipePath(
|
|
|
165
193
|
if (!trimmed || hasWhitespace(trimmed)) return undefined;
|
|
166
194
|
if (trimmed.endsWith(".json") || trimmed.endsWith(".md"))
|
|
167
195
|
return resolveRecipePath(trimmed, recipeRoot);
|
|
168
|
-
|
|
169
|
-
const mdPath = resolveRecipePath(`${trimmed}.md`, recipeRoot);
|
|
170
|
-
const path = existsSync(jsonPath) ? jsonPath : mdPath;
|
|
171
|
-
if (!existsSync(path)) return undefined;
|
|
172
|
-
try {
|
|
173
|
-
const raw = readRawRecipeConfig(path);
|
|
174
|
-
return raw && typeof raw === "object" && Object.hasOwn(raw, "template")
|
|
175
|
-
? path
|
|
176
|
-
: undefined;
|
|
177
|
-
} catch {
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
196
|
+
return resolveRecipeReferencePath(trimmed, recipeRoot);
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
function isImportNode(value: unknown): boolean {
|
|
@@ -483,7 +500,7 @@ export function getRecipeIdFromPath(file: string): string {
|
|
|
483
500
|
}
|
|
484
501
|
|
|
485
502
|
function readRecipeConfig(value: unknown): TemplateRecipeConfig | undefined {
|
|
486
|
-
const path =
|
|
503
|
+
const path = resolveRecipeReferencePath(value);
|
|
487
504
|
return path ? readResolvedRecipeConfig(path) : undefined;
|
|
488
505
|
}
|
|
489
506
|
|
|
@@ -647,6 +664,7 @@ function applyDefaultsToTemplate(
|
|
|
647
664
|
): CommandTemplateValue {
|
|
648
665
|
const cleanOverrides = { ...overrides };
|
|
649
666
|
delete cleanOverrides.name;
|
|
667
|
+
delete cleanOverrides.template;
|
|
650
668
|
delete cleanOverrides.values;
|
|
651
669
|
if (typeof template === "object" && !Array.isArray(template)) {
|
|
652
670
|
return {
|
|
@@ -686,6 +704,119 @@ function withActorRecipeContext(
|
|
|
686
704
|
return { actorRecipeContext: context, template: value };
|
|
687
705
|
}
|
|
688
706
|
|
|
707
|
+
function loadDelegatedRecipe(
|
|
708
|
+
value: unknown,
|
|
709
|
+
currentRecipeFile: string,
|
|
710
|
+
stack: string[],
|
|
711
|
+
options: ReadResolvedRecipeConfigOptions,
|
|
712
|
+
): TemplateRecipeConfig | undefined {
|
|
713
|
+
const path = resolveRecipeReferencePath(value, dirname(currentRecipeFile));
|
|
714
|
+
if (!path) return undefined;
|
|
715
|
+
const config = readResolvedRecipeConfig(
|
|
716
|
+
path,
|
|
717
|
+
[...stack, currentRecipeFile],
|
|
718
|
+
options,
|
|
719
|
+
);
|
|
720
|
+
if (!config) throw new Error(`Template recipe must define template: ${path}`);
|
|
721
|
+
if (config.disabled === true)
|
|
722
|
+
throw new Error(`Template recipe is disabled: ${path}`);
|
|
723
|
+
return config;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function applyDelegatedRecipeToNode(
|
|
727
|
+
delegated: TemplateRecipeConfig,
|
|
728
|
+
overrides: Record<string, unknown> = {},
|
|
729
|
+
): CommandTemplateValue {
|
|
730
|
+
return applyDefaultsToTemplate(
|
|
731
|
+
delegated.template,
|
|
732
|
+
delegated.values,
|
|
733
|
+
overrides,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function expandRecipeDelegations(
|
|
738
|
+
value: CommandTemplateValue,
|
|
739
|
+
currentRecipeFile: string,
|
|
740
|
+
stack: string[],
|
|
741
|
+
options: ReadResolvedRecipeConfigOptions = {},
|
|
742
|
+
): CommandTemplateValue {
|
|
743
|
+
if (typeof value === "string") {
|
|
744
|
+
const delegated = loadDelegatedRecipe(
|
|
745
|
+
value,
|
|
746
|
+
currentRecipeFile,
|
|
747
|
+
stack,
|
|
748
|
+
options,
|
|
749
|
+
);
|
|
750
|
+
return delegated ? applyDelegatedRecipeToNode(delegated) : value;
|
|
751
|
+
}
|
|
752
|
+
if (Array.isArray(value)) {
|
|
753
|
+
return value.map(
|
|
754
|
+
(item) =>
|
|
755
|
+
expandRecipeDelegations(
|
|
756
|
+
item as CommandTemplateValue,
|
|
757
|
+
currentRecipeFile,
|
|
758
|
+
stack,
|
|
759
|
+
options,
|
|
760
|
+
) as CommandTemplateConfig,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
const record = value as Record<string, unknown>;
|
|
764
|
+
if (typeof record.template === "string") {
|
|
765
|
+
const delegated = loadDelegatedRecipe(
|
|
766
|
+
record.template,
|
|
767
|
+
currentRecipeFile,
|
|
768
|
+
stack,
|
|
769
|
+
options,
|
|
770
|
+
);
|
|
771
|
+
if (delegated) return applyDelegatedRecipeToNode(delegated, record);
|
|
772
|
+
}
|
|
773
|
+
if (Array.isArray(record.template)) {
|
|
774
|
+
return {
|
|
775
|
+
...record,
|
|
776
|
+
template: record.template.map(
|
|
777
|
+
(item) =>
|
|
778
|
+
expandRecipeDelegations(
|
|
779
|
+
item as CommandTemplateValue,
|
|
780
|
+
currentRecipeFile,
|
|
781
|
+
stack,
|
|
782
|
+
options,
|
|
783
|
+
) as CommandTemplateConfig,
|
|
784
|
+
),
|
|
785
|
+
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
786
|
+
}
|
|
787
|
+
if (record.template && typeof record.template === "object") {
|
|
788
|
+
return {
|
|
789
|
+
...record,
|
|
790
|
+
template: expandRecipeDelegations(
|
|
791
|
+
record.template as CommandTemplateValue,
|
|
792
|
+
currentRecipeFile,
|
|
793
|
+
stack,
|
|
794
|
+
options,
|
|
795
|
+
),
|
|
796
|
+
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
797
|
+
}
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function getDirectDelegatedRecipe(
|
|
802
|
+
value: CommandTemplateValue,
|
|
803
|
+
currentRecipeFile: string,
|
|
804
|
+
stack: string[],
|
|
805
|
+
options: ReadResolvedRecipeConfigOptions = {},
|
|
806
|
+
): TemplateRecipeConfig | undefined {
|
|
807
|
+
if (typeof value === "string")
|
|
808
|
+
return loadDelegatedRecipe(value, currentRecipeFile, stack, options);
|
|
809
|
+
if (!Array.isArray(value) && typeof value.template === "string") {
|
|
810
|
+
return loadDelegatedRecipe(
|
|
811
|
+
value.template,
|
|
812
|
+
currentRecipeFile,
|
|
813
|
+
stack,
|
|
814
|
+
options,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
|
|
689
820
|
function expandImportNodes(
|
|
690
821
|
value: CommandTemplateValue,
|
|
691
822
|
imports: Record<string, ImportedRecipe>,
|
|
@@ -807,7 +938,22 @@ export function readResolvedRecipeConfig(
|
|
|
807
938
|
>;
|
|
808
939
|
const template = getRecipeCommandTemplate(substituted);
|
|
809
940
|
if (!template) return undefined;
|
|
810
|
-
const
|
|
941
|
+
const expandedImportsTemplate = expandImportNodes(template, imports, options);
|
|
942
|
+
const delegated = getDirectDelegatedRecipe(
|
|
943
|
+
expandedImportsTemplate,
|
|
944
|
+
path,
|
|
945
|
+
stack,
|
|
946
|
+
options,
|
|
947
|
+
);
|
|
948
|
+
const expandedTemplate = delegated
|
|
949
|
+
? applyDelegatedRecipeToNode(
|
|
950
|
+
delegated,
|
|
951
|
+
typeof expandedImportsTemplate === "object" &&
|
|
952
|
+
!Array.isArray(expandedImportsTemplate)
|
|
953
|
+
? (expandedImportsTemplate as Record<string, unknown>)
|
|
954
|
+
: {},
|
|
955
|
+
)
|
|
956
|
+
: expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
|
|
811
957
|
const recipeName = getRecipeIdFromPath(path);
|
|
812
958
|
const templateWithContext = options.includeActorRecipeContext
|
|
813
959
|
? withActorRecipeContext(expandedTemplate, {
|
|
@@ -817,33 +963,53 @@ export function readResolvedRecipeConfig(
|
|
|
817
963
|
role: stack.length > 0 ? "import" : "entry",
|
|
818
964
|
})
|
|
819
965
|
: expandedTemplate;
|
|
966
|
+
const mergedDefaults = mergeDefaults(
|
|
967
|
+
delegated?.defaults,
|
|
968
|
+
isRecord(substituted.defaults) ? substituted.defaults : undefined,
|
|
969
|
+
);
|
|
970
|
+
const artifactSource = isRecord(substituted.artifacts)
|
|
971
|
+
? substituted.artifacts
|
|
972
|
+
: delegated?.artifacts;
|
|
973
|
+
const mailboxSource = isRecord(substituted.mailbox)
|
|
974
|
+
? substituted.mailbox
|
|
975
|
+
: delegated?.mailbox;
|
|
820
976
|
return {
|
|
821
977
|
name: recipeName,
|
|
822
978
|
...(typeof substituted.description === "string" &&
|
|
823
979
|
substituted.description.trim()
|
|
824
980
|
? { description: substituted.description.trim() }
|
|
825
|
-
:
|
|
981
|
+
: typeof delegated?.description === "string"
|
|
982
|
+
? { description: delegated.description }
|
|
983
|
+
: {}),
|
|
826
984
|
...(typeof substituted.disabled === "boolean"
|
|
827
985
|
? { disabled: substituted.disabled }
|
|
828
|
-
:
|
|
986
|
+
: typeof delegated?.disabled === "boolean"
|
|
987
|
+
? { disabled: delegated.disabled }
|
|
988
|
+
: {}),
|
|
829
989
|
...(substituted.async === true
|
|
830
990
|
? { async: true }
|
|
831
991
|
: substituted.async === false
|
|
832
992
|
? { async: false }
|
|
833
|
-
:
|
|
993
|
+
: delegated?.async === true
|
|
994
|
+
? { async: true }
|
|
995
|
+
: delegated?.async === false
|
|
996
|
+
? { async: false }
|
|
997
|
+
: {}),
|
|
834
998
|
...(typeof substituted.state_dir === "string"
|
|
835
999
|
? { state_dir: substituted.state_dir }
|
|
836
|
-
:
|
|
1000
|
+
: typeof delegated?.state_dir === "string"
|
|
1001
|
+
? { state_dir: delegated.state_dir }
|
|
1002
|
+
: {}),
|
|
837
1003
|
...(Object.keys(imports).length > 0
|
|
838
1004
|
? { imports: getRecipeImports(raw) }
|
|
839
1005
|
: {}),
|
|
840
1006
|
template: templateWithContext,
|
|
841
1007
|
...(Array.isArray(substituted.args)
|
|
842
1008
|
? { args: substituted.args as string[] }
|
|
843
|
-
:
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1009
|
+
: Array.isArray(delegated?.args)
|
|
1010
|
+
? { args: delegated.args }
|
|
1011
|
+
: {}),
|
|
1012
|
+
...(mergedDefaults ? { defaults: mergedDefaults } : {}),
|
|
847
1013
|
...(typeof substituted.parallel === "boolean"
|
|
848
1014
|
? { parallel: substituted.parallel }
|
|
849
1015
|
: {}),
|
|
@@ -865,31 +1031,31 @@ export function readResolvedRecipeConfig(
|
|
|
865
1031
|
...(typeof substituted.output === "string"
|
|
866
1032
|
? { output: substituted.output }
|
|
867
1033
|
: {}),
|
|
868
|
-
...(isRecord(
|
|
1034
|
+
...(isRecord(artifactSource)
|
|
869
1035
|
? {
|
|
870
1036
|
artifacts: Object.fromEntries(
|
|
871
|
-
Object.entries(
|
|
1037
|
+
Object.entries(artifactSource).filter(
|
|
872
1038
|
(entry): entry is [string, string] =>
|
|
873
1039
|
typeof entry[1] === "string",
|
|
874
1040
|
),
|
|
875
1041
|
),
|
|
876
1042
|
}
|
|
877
1043
|
: {}),
|
|
878
|
-
...(isRecord(
|
|
1044
|
+
...(isRecord(mailboxSource)
|
|
879
1045
|
? {
|
|
880
1046
|
mailbox: {
|
|
881
|
-
...(Array.isArray(
|
|
1047
|
+
...(Array.isArray(mailboxSource.accepts)
|
|
882
1048
|
? {
|
|
883
|
-
accepts:
|
|
1049
|
+
accepts: mailboxSource.accepts.filter(
|
|
884
1050
|
(value): value is TemplateRecipeMailboxEntry =>
|
|
885
1051
|
typeof value === "string" ||
|
|
886
1052
|
(isRecord(value) && typeof value.type === "string"),
|
|
887
1053
|
),
|
|
888
1054
|
}
|
|
889
1055
|
: {}),
|
|
890
|
-
...(Array.isArray(
|
|
1056
|
+
...(Array.isArray(mailboxSource.emits)
|
|
891
1057
|
? {
|
|
892
|
-
emits:
|
|
1058
|
+
emits: mailboxSource.emits.filter(
|
|
893
1059
|
(value): value is TemplateRecipeMailboxEntry =>
|
|
894
1060
|
typeof value === "string" ||
|
|
895
1061
|
(isRecord(value) && typeof value.type === "string"),
|
|
@@ -899,7 +1065,8 @@ export function readResolvedRecipeConfig(
|
|
|
899
1065
|
},
|
|
900
1066
|
}
|
|
901
1067
|
: {}),
|
|
902
|
-
...(substituted.retire_when === "children_terminal"
|
|
1068
|
+
...(substituted.retire_when === "children_terminal" ||
|
|
1069
|
+
delegated?.retire_when === "children_terminal"
|
|
903
1070
|
? { retire_when: "children_terminal" as const }
|
|
904
1071
|
: {}),
|
|
905
1072
|
...(typeof substituted.retry === "number" ||
|
package/lib/registry.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface RegisterToolResult {
|
|
|
50
50
|
export interface RegisterToolRuntimeDeps<TContext> {
|
|
51
51
|
configPath: string;
|
|
52
52
|
recipeRoot?: string;
|
|
53
|
-
|
|
53
|
+
getToolNameBlocker: (name: string) => string | undefined;
|
|
54
54
|
getTools: () => Map<string, Config.RegisteredTool>;
|
|
55
55
|
getActiveTools: () => string[];
|
|
56
56
|
notify: (
|
|
@@ -132,8 +132,8 @@ function promoteDraftRecipe<TContext>(
|
|
|
132
132
|
const targetPath = getToolRecipePath(deps, name);
|
|
133
133
|
const tools = deps.getTools();
|
|
134
134
|
const existing = tools.get(name);
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
135
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
136
|
+
if (blocker) throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
137
137
|
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
138
138
|
throw new Error(
|
|
139
139
|
ExecutionOutput.formatToolText(
|
|
@@ -383,8 +383,8 @@ export async function executeRegisterTool<TContext>(
|
|
|
383
383
|
return deleteTool(name, ctx, deps);
|
|
384
384
|
const tools = deps.getTools();
|
|
385
385
|
const existing = tools.get(name);
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
386
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
387
|
+
if (blocker) throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
388
388
|
if (existing && !input.update) {
|
|
389
389
|
throw new Error(
|
|
390
390
|
ExecutionOutput.formatToolText(
|
package/lib/runtime.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
|
|
|
7
7
|
import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
@@ -19,17 +19,12 @@ export interface RuntimeContext {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export interface ToolInfoLike {
|
|
23
|
-
name: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
export interface ToolRegistryRuntimeDeps {
|
|
27
23
|
configPath: string;
|
|
28
24
|
exec: RegisteredToolExec;
|
|
29
25
|
packagedRecipeRoot?: string;
|
|
30
26
|
recipeRoot?: string;
|
|
31
27
|
getActiveTools?: () => string[];
|
|
32
|
-
getAllTools: () => ToolInfoLike[];
|
|
33
28
|
registerTool: (
|
|
34
29
|
definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
|
|
35
30
|
) => void;
|
|
@@ -38,7 +33,7 @@ export interface ToolRegistryRuntimeDeps {
|
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
export interface ToolRegistryRuntime {
|
|
41
|
-
|
|
36
|
+
getToolNameBlocker(name: string): string | undefined;
|
|
42
37
|
getTools(): Map<string, Config.RegisteredTool>;
|
|
43
38
|
loadTools(ctx: RuntimeContext): void;
|
|
44
39
|
notify(
|
|
@@ -67,11 +62,9 @@ export function createAutoToolsRuntime(
|
|
|
67
62
|
) {
|
|
68
63
|
if (ctx.hasUI) ctx.ui.notify(message, type);
|
|
69
64
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return existing
|
|
74
|
-
? `Tool "${name}" is already registered outside pi-actors.`
|
|
65
|
+
function getToolNameBlocker(name: string): string | undefined {
|
|
66
|
+
return deps.reservedToolNames.has(name)
|
|
67
|
+
? `Reserved tool name: ${name}`
|
|
75
68
|
: undefined;
|
|
76
69
|
}
|
|
77
70
|
function getToolFingerprint(cfg: Config.RegisteredTool): string {
|
|
@@ -105,6 +98,7 @@ export function createAutoToolsRuntime(
|
|
|
105
98
|
runtimeToolFingerprints.set(cfg.name, fingerprint);
|
|
106
99
|
}
|
|
107
100
|
function isStartupActionableRegistryWarning(warning: string): boolean {
|
|
101
|
+
if (warning.includes(" shadows ")) return false;
|
|
108
102
|
if (
|
|
109
103
|
warning.includes("invokes bash;") &&
|
|
110
104
|
warning.includes("trusted executable content")
|
|
@@ -166,9 +160,9 @@ export function createAutoToolsRuntime(
|
|
|
166
160
|
}
|
|
167
161
|
deactivateMissingRuntimeTools(new Set(tools.keys()));
|
|
168
162
|
for (const cfg of tools.values()) {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
warnings.push(
|
|
163
|
+
const blocker = getToolNameBlocker(cfg.name);
|
|
164
|
+
if (blocker) {
|
|
165
|
+
warnings.push(blocker);
|
|
172
166
|
continue;
|
|
173
167
|
}
|
|
174
168
|
registerRuntimeTool(cfg);
|
|
@@ -179,7 +173,7 @@ export function createAutoToolsRuntime(
|
|
|
179
173
|
}
|
|
180
174
|
}
|
|
181
175
|
return {
|
|
182
|
-
|
|
176
|
+
getToolNameBlocker,
|
|
183
177
|
getTools: () => tools,
|
|
184
178
|
loadTools,
|
|
185
179
|
notify,
|
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
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,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/skills/swarm/SKILL.md
CHANGED