@llblab/pi-actors 0.44.0 → 0.45.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 +7 -0
- package/README.md +3 -1
- package/dist/index.js +7 -3
- package/dist/lib/async-runs.d.ts +2 -0
- package/dist/lib/async-runs.js +33 -4
- package/dist/lib/config.js +1 -0
- package/dist/lib/inspector.js +8 -2
- package/dist/lib/recipes-discovery.js +20 -3
- package/dist/lib/recipes-references.d.ts +10 -0
- package/dist/lib/recipes-references.js +168 -5
- package/dist/lib/schema.d.ts +3 -0
- package/dist/lib/schema.js +34 -1
- package/dist/lib/tools-inspect.js +10 -0
- package/dist/lib/tools-local.js +27 -10
- package/dist/lib/tools-spawn.js +2 -2
- package/dist/scripts/release-gates.mjs +1 -1
- package/dist/skills/actors/SKILL.md +2 -2
- package/docs/command-templates.md +2 -2
- package/docs/recipe-library.md +2 -0
- package/docs/template-recipes.md +10 -4
- package/docs/tool-registry.md +1 -1
- package/index.ts +9 -3
- package/lib/async-runs.ts +43 -9
- package/lib/config.ts +4 -0
- package/lib/inspector.ts +16 -2
- package/lib/recipes-discovery.ts +23 -3
- package/lib/recipes-references.ts +227 -7
- package/lib/schema.ts +53 -1
- package/lib/tools-inspect.ts +11 -0
- package/lib/tools-local.ts +49 -19
- package/lib/tools-spawn.ts +2 -2
- package/package.json +1 -1
- package/scripts/release-gates.mjs +1 -1
- package/skills/actors/SKILL.md +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { basename, dirname, extname, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
CommandTemplateConfig,
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
import * as CommandTemplates from "./command-templates.ts";
|
|
16
16
|
import * as Paths from "./paths.ts";
|
|
17
17
|
import * as RecipeControl from "./recipe-control.ts";
|
|
18
|
+
import * as Schema from "./schema.ts";
|
|
18
19
|
|
|
19
20
|
const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
|
|
20
21
|
const MAX_RECIPE_IMPORT_DEPTH = 32;
|
|
@@ -57,6 +58,8 @@ export interface TemplateRecipeDefinition {
|
|
|
57
58
|
|
|
58
59
|
export interface TemplateRecipeConfig extends TemplateRecipeDefinition {
|
|
59
60
|
async?: boolean;
|
|
61
|
+
recipe_dir?: string;
|
|
62
|
+
skill_dir?: string;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
interface ImportedRecipe {
|
|
@@ -74,6 +77,7 @@ export interface TemplateRecipeContextRecord {
|
|
|
74
77
|
file: string;
|
|
75
78
|
import_path: string[];
|
|
76
79
|
name: string;
|
|
80
|
+
qualified_name?: string;
|
|
77
81
|
recipe: Record<string, unknown>;
|
|
78
82
|
role: "entry" | "import";
|
|
79
83
|
}
|
|
@@ -82,10 +86,120 @@ export interface ReadResolvedRecipeConfigOptions {
|
|
|
82
86
|
includeActorRecipeContext?: boolean;
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
const RUNTIME_RECIPE_VALUE_NAMES = new Set(["recipe_dir", "skill_dir"]);
|
|
90
|
+
const activeSkillRecipeRoots = new Map<string, Set<string>>();
|
|
91
|
+
|
|
92
|
+
export interface ActiveSkillRecipeSource {
|
|
93
|
+
name: string;
|
|
94
|
+
filePath?: string;
|
|
95
|
+
baseDir?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function setActiveSkillRecipeSources(
|
|
99
|
+
skills: ActiveSkillRecipeSource[],
|
|
100
|
+
): void {
|
|
101
|
+
activeSkillRecipeRoots.clear();
|
|
102
|
+
for (const skill of skills) {
|
|
103
|
+
const name = skill.name.trim();
|
|
104
|
+
const skillDir = skill.baseDir
|
|
105
|
+
? resolve(skill.baseDir)
|
|
106
|
+
: skill.filePath
|
|
107
|
+
? dirname(resolve(skill.filePath))
|
|
108
|
+
: undefined;
|
|
109
|
+
if (!name || !skillDir) continue;
|
|
110
|
+
const roots = activeSkillRecipeRoots.get(name) ?? new Set<string>();
|
|
111
|
+
roots.add(resolve(skillDir, "recipes"));
|
|
112
|
+
activeSkillRecipeRoots.set(name, roots);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getActiveSkillRecipeNamespaces(): Record<string, string[]> {
|
|
117
|
+
return Object.fromEntries(
|
|
118
|
+
[...activeSkillRecipeRoots.entries()]
|
|
119
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
120
|
+
.map(([name, roots]) => [name, [...roots].sort()]),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function qualifiedRecipeNameForFile(file: string): string | undefined {
|
|
125
|
+
const path = resolve(file);
|
|
126
|
+
const stdRelation = relative(Paths.getPackagedRecipeRoot(), path);
|
|
127
|
+
if (
|
|
128
|
+
stdRelation &&
|
|
129
|
+
!stdRelation.startsWith("..") &&
|
|
130
|
+
!isAbsolute(stdRelation)
|
|
131
|
+
) {
|
|
132
|
+
return `std:${stdRelation.replace(/\.(?:json|md)$/u, "")}`;
|
|
133
|
+
}
|
|
134
|
+
for (const [skillName, roots] of activeSkillRecipeRoots) {
|
|
135
|
+
if (roots.size !== 1) continue;
|
|
136
|
+
const root = [...roots][0];
|
|
137
|
+
const relation = relative(root, path);
|
|
138
|
+
if (relation && !relation.startsWith("..") && !isAbsolute(relation))
|
|
139
|
+
return `skill:${skillName}/${relation.replaceAll("\\", "/").replace(/\.(?:json|md)$/u, "")}`;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function qualifiedRecipeCandidates(value: string): string[] | undefined {
|
|
145
|
+
if (value.startsWith("std:")) {
|
|
146
|
+
return recipeNameFiles(value.slice(4)).map((file) =>
|
|
147
|
+
resolve(Paths.getPackagedRecipeRoot(), file),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!value.startsWith("skill:")) return undefined;
|
|
151
|
+
const qualified = value.slice(6);
|
|
152
|
+
const separator = qualified.indexOf("/");
|
|
153
|
+
if (separator <= 0 || separator === qualified.length - 1)
|
|
154
|
+
throw new Error(`Invalid Skill Recipe reference: ${value}`);
|
|
155
|
+
const skillName = qualified.slice(0, separator);
|
|
156
|
+
const recipeName = qualified.slice(separator + 1);
|
|
157
|
+
const roots = [...(activeSkillRecipeRoots.get(skillName) ?? [])];
|
|
158
|
+
if (roots.length === 0)
|
|
159
|
+
throw new Error(`Active Skill Recipe namespace not found: ${skillName}`);
|
|
160
|
+
if (roots.length > 1)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Ambiguous active Skill Recipe namespace ${skillName}: ${roots.join(", ")}`,
|
|
163
|
+
);
|
|
164
|
+
const root = roots[0];
|
|
165
|
+
return recipeNameFiles(recipeName).map((file) => {
|
|
166
|
+
const candidate = resolve(root, file);
|
|
167
|
+
const relation = relative(root, candidate);
|
|
168
|
+
if (!relation || relation.startsWith("..") || isAbsolute(relation))
|
|
169
|
+
throw new Error(`Skill Recipe reference escapes its recipes root: ${value}`);
|
|
170
|
+
return candidate;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
85
174
|
function hasWhitespace(value: string): boolean {
|
|
86
175
|
return /\s/.test(value);
|
|
87
176
|
}
|
|
88
177
|
|
|
178
|
+
function findOwningSkillDir(recipeFile: string): string | undefined {
|
|
179
|
+
let current = dirname(recipeFile);
|
|
180
|
+
while (true) {
|
|
181
|
+
if (existsSync(resolve(current, "SKILL.md"))) return current;
|
|
182
|
+
const parent = dirname(current);
|
|
183
|
+
if (parent === current) return undefined;
|
|
184
|
+
current = parent;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): void {
|
|
189
|
+
const args = Array.isArray(raw.args)
|
|
190
|
+
? Schema.getExplicitToolArgNames(raw.args.map(String))
|
|
191
|
+
: [];
|
|
192
|
+
for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
|
|
193
|
+
if (
|
|
194
|
+
args.includes(name) ||
|
|
195
|
+
(isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
|
|
196
|
+
(isRecord(raw.values) && Object.hasOwn(raw.values, name))
|
|
197
|
+
) {
|
|
198
|
+
throw new Error(`Recipe runtime value is reserved: ${name}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
89
203
|
export function resolveRecipePath(
|
|
90
204
|
value: string,
|
|
91
205
|
recipeRoot = Paths.getRecipeRoot(),
|
|
@@ -125,6 +239,8 @@ function recipeCandidatePaths(
|
|
|
125
239
|
value: string,
|
|
126
240
|
currentRecipeRoot: string,
|
|
127
241
|
): string[] {
|
|
242
|
+
const qualified = qualifiedRecipeCandidates(value.trim());
|
|
243
|
+
if (qualified) return qualified;
|
|
128
244
|
if (!isBareRecipeName(value))
|
|
129
245
|
return [resolveRecipePath(value, currentRecipeRoot)];
|
|
130
246
|
const roots = [
|
|
@@ -646,6 +762,63 @@ function mergeDefaults(
|
|
|
646
762
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
647
763
|
}
|
|
648
764
|
|
|
765
|
+
function assertRecipeDefaultsDeclared(
|
|
766
|
+
args: string[] | undefined,
|
|
767
|
+
defaults: Record<string, unknown> | undefined,
|
|
768
|
+
label: string,
|
|
769
|
+
): void {
|
|
770
|
+
const spec = Schema.parseToolArgDeclarationList(args ?? []);
|
|
771
|
+
if (spec.error) throw new Error(spec.error);
|
|
772
|
+
if (spec.args.length === 0) return;
|
|
773
|
+
const declared = new Set(spec.args);
|
|
774
|
+
for (const key of Object.keys(defaults ?? {})) {
|
|
775
|
+
if (!declared.has(key)) throw new Error(`Unknown ${label} argument: ${key}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function assertRecipeArgumentContract(
|
|
780
|
+
config: Pick<
|
|
781
|
+
TemplateRecipeDefinition,
|
|
782
|
+
"args" | "defaults" | "values" | "template"
|
|
783
|
+
>,
|
|
784
|
+
label = "Recipe default",
|
|
785
|
+
): void {
|
|
786
|
+
assertRecipeDefaultsDeclared(config.args, config.defaults, label);
|
|
787
|
+
const spec = Schema.parseToolArgDeclarationList(config.args ?? []);
|
|
788
|
+
if (spec.error) throw new Error(spec.error);
|
|
789
|
+
Schema.assertCompatibleToolArgTypes(
|
|
790
|
+
spec.argTypes,
|
|
791
|
+
Schema.getTemplateArgTypes({ template: config.template }),
|
|
792
|
+
);
|
|
793
|
+
const staticValues = Object.fromEntries(
|
|
794
|
+
Object.entries({ ...config.defaults, ...config.values }).filter(
|
|
795
|
+
([, value]) =>
|
|
796
|
+
typeof value !== "string" || !/\{[^{}]+\}/.test(value),
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
Schema.normalizeRuntimeValues(staticValues, spec.argTypes);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function resolveRecipeValueLayers(
|
|
803
|
+
args: string[] | undefined,
|
|
804
|
+
...layers: Array<Record<string, unknown> | undefined>
|
|
805
|
+
): Record<string, unknown> | undefined {
|
|
806
|
+
const spec = Schema.parseToolArgDeclarationList(args ?? []);
|
|
807
|
+
if (spec.error) throw new Error(spec.error);
|
|
808
|
+
const merged = mergeDefaults(spec.defaults, ...layers);
|
|
809
|
+
if (!merged) return undefined;
|
|
810
|
+
const staticValues = Object.fromEntries(
|
|
811
|
+
Object.entries(merged).filter(
|
|
812
|
+
([, value]) =>
|
|
813
|
+
typeof value !== "string" || !/\{[^{}]+\}/.test(value),
|
|
814
|
+
),
|
|
815
|
+
);
|
|
816
|
+
return {
|
|
817
|
+
...merged,
|
|
818
|
+
...Schema.normalizeRuntimeValues(staticValues, spec.argTypes),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
649
822
|
function applyDefaultsToTemplate(
|
|
650
823
|
template: CommandTemplateValue,
|
|
651
824
|
defaults: Record<string, unknown> | undefined,
|
|
@@ -655,6 +828,7 @@ function applyDefaultsToTemplate(
|
|
|
655
828
|
delete cleanOverrides.name;
|
|
656
829
|
delete cleanOverrides.template;
|
|
657
830
|
delete cleanOverrides.values;
|
|
831
|
+
delete cleanOverrides.defaults;
|
|
658
832
|
if (typeof template === "object" && !Array.isArray(template)) {
|
|
659
833
|
return {
|
|
660
834
|
...template,
|
|
@@ -716,11 +890,22 @@ function applyDelegatedRecipeToNode(
|
|
|
716
890
|
delegated: TemplateRecipeConfig,
|
|
717
891
|
overrides: Record<string, unknown> = {},
|
|
718
892
|
): CommandTemplateValue {
|
|
719
|
-
|
|
720
|
-
|
|
893
|
+
const nodeDefaults = isRecord(overrides.defaults)
|
|
894
|
+
? overrides.defaults
|
|
895
|
+
: undefined;
|
|
896
|
+
const nodeValues = isRecord(overrides.values) ? overrides.values : undefined;
|
|
897
|
+
const values = resolveRecipeValueLayers(
|
|
898
|
+
delegated.args,
|
|
899
|
+
delegated.defaults,
|
|
721
900
|
delegated.values,
|
|
722
|
-
|
|
901
|
+
nodeDefaults,
|
|
902
|
+
nodeValues,
|
|
903
|
+
{
|
|
904
|
+
...(delegated.recipe_dir ? { recipe_dir: delegated.recipe_dir } : {}),
|
|
905
|
+
...(delegated.skill_dir ? { skill_dir: delegated.skill_dir } : {}),
|
|
906
|
+
},
|
|
723
907
|
);
|
|
908
|
+
return applyDefaultsToTemplate(delegated.template, values, overrides);
|
|
724
909
|
}
|
|
725
910
|
|
|
726
911
|
function expandRecipeDelegations(
|
|
@@ -830,7 +1015,8 @@ function expandImportNodes(
|
|
|
830
1015
|
? record.defaults
|
|
831
1016
|
: undefined;
|
|
832
1017
|
const nodeValues = isRecord(record.values) ? record.values : undefined;
|
|
833
|
-
const defaults =
|
|
1018
|
+
const defaults = resolveRecipeValueLayers(
|
|
1019
|
+
imported.config.args,
|
|
834
1020
|
imported.defaults,
|
|
835
1021
|
imported.values,
|
|
836
1022
|
nodeDefaults,
|
|
@@ -897,6 +1083,8 @@ export function readResolvedRecipeConfig(
|
|
|
897
1083
|
const raw = readRawRecipeConfig(path);
|
|
898
1084
|
if (!raw || !Object.hasOwn(raw, "template")) return undefined;
|
|
899
1085
|
RecipeControl.assertRecipeHasNoMailbox(raw);
|
|
1086
|
+
assertRuntimeRecipeValuesNotDeclared(raw);
|
|
1087
|
+
assertRecipeArgumentContract(raw as unknown as TemplateRecipeDefinition);
|
|
900
1088
|
const imports: Record<string, ImportedRecipe> = {};
|
|
901
1089
|
for (const [alias, binding] of Object.entries(getRecipeImports(raw))) {
|
|
902
1090
|
const importPath = resolveRecipeImportPath(
|
|
@@ -913,13 +1101,22 @@ export function readResolvedRecipeConfig(
|
|
|
913
1101
|
typeof binding === "string" ? undefined : binding.defaults;
|
|
914
1102
|
const bindingValues =
|
|
915
1103
|
typeof binding === "string" ? undefined : binding.values;
|
|
1104
|
+
assertRecipeDefaultsDeclared(
|
|
1105
|
+
config.args,
|
|
1106
|
+
bindingDefaults,
|
|
1107
|
+
`Recipe import ${alias} default`,
|
|
1108
|
+
);
|
|
916
1109
|
imports[alias] = {
|
|
917
1110
|
alias,
|
|
918
1111
|
file: importPath,
|
|
919
1112
|
name: config.name ?? alias,
|
|
920
1113
|
config,
|
|
921
1114
|
defaults: { ...(config.defaults ?? {}), ...(bindingDefaults ?? {}) },
|
|
922
|
-
values: {
|
|
1115
|
+
values: {
|
|
1116
|
+
...(bindingValues ?? {}),
|
|
1117
|
+
...(config.recipe_dir ? { recipe_dir: config.recipe_dir } : {}),
|
|
1118
|
+
...(config.skill_dir ? { skill_dir: config.skill_dir } : {}),
|
|
1119
|
+
},
|
|
923
1120
|
};
|
|
924
1121
|
}
|
|
925
1122
|
const substituted = substituteImportRefs(raw, imports) as Record<
|
|
@@ -945,6 +1142,21 @@ export function readResolvedRecipeConfig(
|
|
|
945
1142
|
)
|
|
946
1143
|
: expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
|
|
947
1144
|
const recipeName = getRecipeIdFromPath(path);
|
|
1145
|
+
const recipeDir = dirname(path);
|
|
1146
|
+
const skillDir = findOwningSkillDir(path);
|
|
1147
|
+
if (
|
|
1148
|
+
!skillDir &&
|
|
1149
|
+
JSON.stringify({
|
|
1150
|
+
artifacts: substituted.artifacts,
|
|
1151
|
+
defaults: substituted.defaults,
|
|
1152
|
+
template: substituted.template,
|
|
1153
|
+
values: substituted.values,
|
|
1154
|
+
}).includes("{skill_dir}")
|
|
1155
|
+
) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`Recipe uses {skill_dir} outside an owning Skill directory: ${path}`,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
948
1160
|
const templateWithContext = options.includeActorRecipeContext
|
|
949
1161
|
? withActorRecipeContext(expandedTemplate, {
|
|
950
1162
|
file: path,
|
|
@@ -967,6 +1179,8 @@ export function readResolvedRecipeConfig(
|
|
|
967
1179
|
);
|
|
968
1180
|
return {
|
|
969
1181
|
name: recipeName,
|
|
1182
|
+
recipe_dir: recipeDir,
|
|
1183
|
+
...(skillDir ? { skill_dir: skillDir } : {}),
|
|
970
1184
|
...(typeof substituted.description === "string" &&
|
|
971
1185
|
substituted.description.trim()
|
|
972
1186
|
? { description: substituted.description.trim() }
|
|
@@ -1059,7 +1273,11 @@ export function readResolvedRecipeConfig(
|
|
|
1059
1273
|
...(typeof substituted.repeat === "number"
|
|
1060
1274
|
? { repeat: substituted.repeat }
|
|
1061
1275
|
: {}),
|
|
1062
|
-
|
|
1276
|
+
values: {
|
|
1277
|
+
...(isRecord(substituted.values) ? substituted.values : {}),
|
|
1278
|
+
recipe_dir: recipeDir,
|
|
1279
|
+
...(skillDir ? { skill_dir: skillDir } : {}),
|
|
1280
|
+
},
|
|
1063
1281
|
...(isRecord(substituted.usage) ? { usage: substituted.usage } : {}),
|
|
1064
1282
|
};
|
|
1065
1283
|
}
|
|
@@ -1084,12 +1302,14 @@ function collectRecipeContextRecords(
|
|
|
1084
1302
|
}
|
|
1085
1303
|
const raw = readRawRecipeConfig(path);
|
|
1086
1304
|
if (!raw || !Object.hasOwn(raw, "template")) return [];
|
|
1305
|
+
const qualifiedName = qualifiedRecipeNameForFile(path);
|
|
1087
1306
|
const record: TemplateRecipeContextRecord = {
|
|
1088
1307
|
...(alias ? { alias } : {}),
|
|
1089
1308
|
depth: stack.length,
|
|
1090
1309
|
file: path,
|
|
1091
1310
|
import_path: importPath,
|
|
1092
1311
|
name: getRecipeIdFromPath(path),
|
|
1312
|
+
...(qualifiedName ? { qualified_name: qualifiedName } : {}),
|
|
1093
1313
|
recipe: raw,
|
|
1094
1314
|
role: stack.length === 0 ? "entry" : "import",
|
|
1095
1315
|
};
|
package/lib/schema.ts
CHANGED
|
@@ -185,7 +185,11 @@ function isValidDefault(type: ToolArgType, value: string): boolean {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
export function parseToolArgDeclarations(value: string): ToolArgSpec {
|
|
188
|
-
|
|
188
|
+
return parseToolArgDeclarationList(splitArgDeclarations(value));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function parseToolArgDeclarationList(value: unknown[]): ToolArgSpec {
|
|
192
|
+
const source = value.map(String);
|
|
189
193
|
const seen = new Set<string>();
|
|
190
194
|
const duplicates = new Set<string>();
|
|
191
195
|
const args: string[] = [];
|
|
@@ -324,6 +328,7 @@ function collectCommandTemplateConfigDeclarations(
|
|
|
324
328
|
collectTemplatePlaceholderDeclarations(config, declarations);
|
|
325
329
|
return;
|
|
326
330
|
}
|
|
331
|
+
if (!config || typeof config !== "object") return;
|
|
327
332
|
if (Array.isArray(config)) {
|
|
328
333
|
for (const step of config)
|
|
329
334
|
collectCommandTemplateConfigDeclarations(step, declarations);
|
|
@@ -356,11 +361,42 @@ export function getTemplatePlaceholderNames(
|
|
|
356
361
|
);
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
function toolArgTypesEqual(left: ToolArgType, right: ToolArgType): boolean {
|
|
365
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function assertCompatibleToolArgTypes(
|
|
369
|
+
...sources: Array<Record<string, ToolArgType> | undefined>
|
|
370
|
+
): void {
|
|
371
|
+
const seen: Record<string, ToolArgType> = {};
|
|
372
|
+
for (const source of sources) {
|
|
373
|
+
for (const [name, type] of Object.entries(source ?? {})) {
|
|
374
|
+
const previous = seen[name];
|
|
375
|
+
if (previous && !toolArgTypesEqual(previous, type)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Conflicting argument type for ${name}: ${canonicalArgDeclaration(name, previous)} vs ${canonicalArgDeclaration(name, type)}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
seen[name] = type;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
359
385
|
export function getTemplateArgTypes(
|
|
360
386
|
config: CommandTemplates.CommandTemplateConfig,
|
|
361
387
|
): Record<string, ToolArgType> {
|
|
362
388
|
const argTypes: Record<string, ToolArgType> = {};
|
|
363
389
|
for (const declaration of getTemplatePlaceholderDeclarations(config)) {
|
|
390
|
+
const previous = argTypes[declaration.arg];
|
|
391
|
+
if (
|
|
392
|
+
previous &&
|
|
393
|
+
declaration.type.kind !== "string" &&
|
|
394
|
+
!toolArgTypesEqual(previous, declaration.type)
|
|
395
|
+
) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Conflicting argument type for ${declaration.arg}: ${canonicalArgDeclaration(declaration.arg, previous)} vs ${declaration.declaration}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
364
400
|
if (declaration.type.kind !== "string")
|
|
365
401
|
argTypes[declaration.arg] = declaration.type;
|
|
366
402
|
}
|
|
@@ -373,6 +409,22 @@ export function getExplicitToolArgNames(args: string[] | undefined): string[] {
|
|
|
373
409
|
);
|
|
374
410
|
}
|
|
375
411
|
|
|
412
|
+
export function getExplicitToolArgDefaults(
|
|
413
|
+
args: string[] | string | undefined,
|
|
414
|
+
): Record<string, string> {
|
|
415
|
+
const declarations =
|
|
416
|
+
typeof args === "string" ? splitArgDeclarations(args) : (args ?? []);
|
|
417
|
+
return Object.fromEntries(
|
|
418
|
+
declarations
|
|
419
|
+
.map((item) => parseToolArgToken(String(item)))
|
|
420
|
+
.filter(
|
|
421
|
+
(item): item is ParsedToolArgToken & { defaultValue: string } =>
|
|
422
|
+
item.defaultValue !== undefined,
|
|
423
|
+
)
|
|
424
|
+
.map((item) => [item.arg, item.defaultValue]),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
376
428
|
export function getToolArgNames(
|
|
377
429
|
config: CommandTemplates.CommandTemplateConfig,
|
|
378
430
|
): string[] {
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as Limits from "./limits.ts";
|
|
|
14
14
|
import { computeRunControlCapacity } from "./run-evidence-policy.ts";
|
|
15
15
|
import * as Paths from "./paths.ts";
|
|
16
16
|
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
17
|
+
import * as RecipesReferences from "./recipes-references.ts";
|
|
17
18
|
import * as ReviewDiagnostics from "./review-diagnostics.ts";
|
|
18
19
|
import * as RunsControls from "./runs-controls.ts";
|
|
19
20
|
import * as RunControlDelivery from "./runs-control-delivery.ts";
|
|
@@ -214,8 +215,18 @@ function inspectRecipes(
|
|
|
214
215
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
215
216
|
{ root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
|
|
216
217
|
]);
|
|
218
|
+
const skillRecipeNamespaces =
|
|
219
|
+
RecipesReferences.getActiveSkillRecipeNamespaces();
|
|
217
220
|
const summary = {
|
|
218
221
|
...RecipesDiscovery.summarizeDiscovery(discovered),
|
|
222
|
+
skill_recipe_namespaces: skillRecipeNamespaces,
|
|
223
|
+
skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
|
|
224
|
+
.filter(([, roots]) => roots.length > 1)
|
|
225
|
+
.map(([name, roots]) => ({
|
|
226
|
+
name,
|
|
227
|
+
roots,
|
|
228
|
+
error: `Ambiguous active Skill Recipe namespace ${name}`,
|
|
229
|
+
})),
|
|
219
230
|
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
220
231
|
};
|
|
221
232
|
return {
|
package/lib/tools-local.ts
CHANGED
|
@@ -67,11 +67,16 @@ function formatRuntimeToolUsageHint(
|
|
|
67
67
|
includeRunId: boolean,
|
|
68
68
|
): string {
|
|
69
69
|
const optional = cfg.args.filter((arg) => !required.includes(arg));
|
|
70
|
+
const exampleDefaults = {
|
|
71
|
+
...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
|
|
72
|
+
...cfg.defaults,
|
|
73
|
+
...(cfg.recipe?.values ?? {}),
|
|
74
|
+
};
|
|
70
75
|
const example: Record<string, unknown> = {};
|
|
71
76
|
for (const arg of required)
|
|
72
|
-
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg],
|
|
77
|
+
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
|
|
73
78
|
for (const arg of optional)
|
|
74
|
-
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg],
|
|
79
|
+
example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
|
|
75
80
|
if (includeRunId) example.run_id = `${cfg.name}-1`;
|
|
76
81
|
const lines = [
|
|
77
82
|
`Expected call shape for ${cfg.name}:`,
|
|
@@ -85,6 +90,32 @@ function formatRuntimeToolUsageHint(
|
|
|
85
90
|
return lines.join("\n");
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function resolveRecipeToolValues(
|
|
94
|
+
cfg: RegisteredTool,
|
|
95
|
+
callerValues: Record<string, unknown>,
|
|
96
|
+
ctx: RuntimeToolContext,
|
|
97
|
+
): Record<string, unknown> {
|
|
98
|
+
const recipeRuntimeValues = {
|
|
99
|
+
...(cfg.recipe?.recipe_dir
|
|
100
|
+
? { recipe_dir: cfg.recipe.recipe_dir }
|
|
101
|
+
: {}),
|
|
102
|
+
...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
|
|
103
|
+
};
|
|
104
|
+
return Schema.normalizeRuntimeValues(
|
|
105
|
+
ModelContext.withCurrentModelValues(
|
|
106
|
+
{
|
|
107
|
+
...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
|
|
108
|
+
...cfg.defaults,
|
|
109
|
+
...(cfg.recipe?.values ?? {}),
|
|
110
|
+
...callerValues,
|
|
111
|
+
...recipeRuntimeValues,
|
|
112
|
+
},
|
|
113
|
+
ctx,
|
|
114
|
+
),
|
|
115
|
+
cfg.argTypes,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
function formatRuntimeToolArgumentError(
|
|
89
120
|
cfg: RegisteredTool,
|
|
90
121
|
error: unknown,
|
|
@@ -128,9 +159,18 @@ export function createRuntimeToolDefinition(
|
|
|
128
159
|
defaults: cfg.defaults,
|
|
129
160
|
template: requiredTemplate,
|
|
130
161
|
};
|
|
162
|
+
const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(
|
|
163
|
+
cfg.recipe?.args,
|
|
164
|
+
);
|
|
131
165
|
const requiredArgs =
|
|
132
166
|
isRecipe && cfg.storedArgs !== undefined
|
|
133
|
-
? new Set(
|
|
167
|
+
? new Set(
|
|
168
|
+
cfg.args.filter(
|
|
169
|
+
(arg) =>
|
|
170
|
+
!Object.hasOwn(cfg.defaults, arg) &&
|
|
171
|
+
!Object.hasOwn(recipeInlineDefaults, arg),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
134
174
|
: RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
|
|
135
175
|
? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
|
|
136
176
|
: Schema.getRequiredToolArgNames(requiredTemplateConfig);
|
|
@@ -200,13 +240,7 @@ export function createRuntimeToolDefinition(
|
|
|
200
240
|
{ ...(cfg.recipe?.values ?? {}), ...values },
|
|
201
241
|
ctx,
|
|
202
242
|
),
|
|
203
|
-
values:
|
|
204
|
-
ModelContext.withCurrentModelValues(
|
|
205
|
-
{ ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values },
|
|
206
|
-
ctx,
|
|
207
|
-
),
|
|
208
|
-
cfg.argTypes,
|
|
209
|
-
),
|
|
243
|
+
values: resolveRecipeToolValues(cfg, values, ctx),
|
|
210
244
|
},
|
|
211
245
|
ctx.cwd,
|
|
212
246
|
);
|
|
@@ -221,17 +255,13 @@ export function createRuntimeToolDefinition(
|
|
|
221
255
|
};
|
|
222
256
|
}
|
|
223
257
|
if (isRecipe && recipeTemplate) {
|
|
224
|
-
const paramsWithDefaults = ModelContext.withCurrentModelValues(
|
|
225
|
-
{
|
|
226
|
-
...(cfg.recipe?.values ?? {}),
|
|
227
|
-
...cfg.defaults,
|
|
228
|
-
...(params as Record<string, unknown>),
|
|
229
|
-
},
|
|
230
|
-
ctx,
|
|
231
|
-
);
|
|
232
258
|
return await Execution.executeRegisteredTool(
|
|
233
259
|
{ ...cfg, template: recipeTemplate },
|
|
234
|
-
|
|
260
|
+
resolveRecipeToolValues(
|
|
261
|
+
cfg,
|
|
262
|
+
params as Record<string, unknown>,
|
|
263
|
+
ctx,
|
|
264
|
+
),
|
|
235
265
|
exec,
|
|
236
266
|
ctx.cwd,
|
|
237
267
|
signal,
|
package/lib/tools-spawn.ts
CHANGED
|
@@ -138,10 +138,10 @@ export function createSpawnToolDefinition<
|
|
|
138
138
|
"Optional Run identity in exact run:<id> form.",
|
|
139
139
|
),
|
|
140
140
|
file: Schema.stringSchema(
|
|
141
|
-
"Optional template
|
|
141
|
+
"Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>.",
|
|
142
142
|
),
|
|
143
143
|
recipe: Schema.stringSchema(
|
|
144
|
-
"Alias for file;
|
|
144
|
+
"Alias for file; qualified Skill/std or ordinary Recipe reference to spawn.",
|
|
145
145
|
),
|
|
146
146
|
template: Schema.unionSchema([
|
|
147
147
|
Schema.stringSchema("Inline command template string"),
|
package/package.json
CHANGED
|
@@ -198,7 +198,7 @@ try {
|
|
|
198
198
|
);
|
|
199
199
|
console.log("[release] zero-warning Recipe QA gate checked");
|
|
200
200
|
|
|
201
|
-
const maximumShippedLines =
|
|
201
|
+
const maximumShippedLines = 29_767;
|
|
202
202
|
const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
|
|
203
203
|
const shippedLines = files
|
|
204
204
|
.filter((path) => shippedPath.test(path))
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -25,11 +25,11 @@ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `contro
|
|
|
25
25
|
|
|
26
26
|
## Recipe
|
|
27
27
|
|
|
28
|
-
A Recipe defines execution. It may declare args,
|
|
28
|
+
A Recipe defines execution. It may declare named typed args, inline fallbacks, configuration `defaults`, composition `values`, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
|
|
29
29
|
|
|
30
30
|
Do not declare Control for ordinary one-shot work. Runtime lifecycle actions such as `kill` stay runtime-owned and must not appear in Recipe Control declarations. Imported Recipes act as local definitions inside one Run; they do not create nested Runs unless execution explicitly spawns them.
|
|
31
31
|
|
|
32
|
-
Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
|
|
32
|
+
Prefer maintained packaged Recipes over ad hoc wrappers. Use `std:<recipe>` for exact packaged lookup and `skill:<skill>/<recipe-path>` for a component bundled with a Pi-active Skill. File-backed Recipes own `{recipe_dir}` and Skill Recipes own `{skill_dir}`; callers never pass or override these origins. Skill Recipes are components, not automatic tools. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
|
|
33
33
|
|
|
34
34
|
## Trace
|
|
35
35
|
|