@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.45.0: Skill Recipe Standard Library
|
|
6
|
+
|
|
7
|
+
- `Recipe Values`: Makes inline argument defaults optional, applies caller > values > defaults > inline precedence across tools, Runs, imports, and delegation, and validates final typed/enum values plus duplicate, unknown-default, and conflicting-type declarations.
|
|
8
|
+
- `Recipe Origins`: Gives file-backed Recipes immutable `{recipe_dir}` and active-Skill Recipes `{skill_dir}` across templates, defaults, artifacts, tools, and Runs while retaining local provenance and hiding machine-local origins from model-facing launch values.
|
|
9
|
+
- `Skill Recipe Library`: Adds exact `std:<name>` and Pi-active `skill:<skill>/<path>` component lookup for imports, delegation, spawn, validation, Inspect diagnostics, captured qualified identity, and installed dist without automatically registering Skill Recipes as tools.
|
|
10
|
+
- `Validation`: Covers source, composed async execution, installed dist, namespace collisions/reload, path redaction, and migrated Skill wrappers while resetting the shipped-line ceiling to the validated 29,767-line release tree.
|
|
11
|
+
|
|
5
12
|
## 0.44.0: Bounded Run Evidence
|
|
6
13
|
|
|
7
14
|
- `Trace`: Bounds each Run to 2,048 events and 4 MiB, atomically retaining the newest suffix with one cumulative `runtime.trace_compacted` marker so discarded history is explicit while terminal, result, execution, and artifact evidence remains authoritative.
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Persist a trusted command template or Recipe-backed capability under `~/.pi/agen
|
|
|
84
84
|
|
|
85
85
|
Recipes can declare:
|
|
86
86
|
|
|
87
|
-
- args and
|
|
87
|
+
- named typed args, inline fallbacks, configuration `defaults`, and composition `values`;
|
|
88
88
|
- imports and command-template composition;
|
|
89
89
|
- retry, failure, recovery, repeat, concurrency, and timeout policy;
|
|
90
90
|
- artifact paths;
|
|
@@ -103,6 +103,8 @@ Example controlled Recipe:
|
|
|
103
103
|
|
|
104
104
|
Ordinary one-shot Recipes should omit `control`. Recipe imports compose definitions inside one Run; they do not create peer actors.
|
|
105
105
|
|
|
106
|
+
File-backed Recipes receive immutable `{recipe_dir}`; Recipes under a Pi-active Skill also receive `{skill_dir}`. Use `std:<recipe>` for an exact packaged component and `skill:<skill>/<recipe-path>` for an exact active-Skill component. Skill Recipes are namespaced reusable components and do not become tools automatically.
|
|
107
|
+
|
|
106
108
|
String command-template leaves execute directly without shell interpretation. Use template arrays for sequencing or an explicit trusted shell/script when shell semantics matter.
|
|
107
109
|
|
|
108
110
|
## Trace
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as InspectorCommand from "./lib/inspector-command.js";
|
|
|
10
10
|
import * as Paths from "./lib/paths.js";
|
|
11
11
|
import * as Pi from "./lib/pi.js";
|
|
12
12
|
import * as Prompts from "./lib/prompts.js";
|
|
13
|
+
import * as RecipesReferences from "./lib/recipes-references.js";
|
|
13
14
|
import * as RunUiRuntime from "./lib/run-ui-runtime.js";
|
|
14
15
|
import * as Runtime from "./lib/runtime.js";
|
|
15
16
|
import * as Temp from "./lib/temp.js";
|
|
@@ -99,9 +100,12 @@ export default function toolRegistryExtension(pi) {
|
|
|
99
100
|
runUiRuntime.shutdown(event.reason, ctx);
|
|
100
101
|
});
|
|
101
102
|
InspectorCommand.registerActorInspectorCommand(pi, getRunOwnerId);
|
|
102
|
-
pi.on("before_agent_start", async (event) =>
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
pi.on("before_agent_start", async (event) => {
|
|
104
|
+
RecipesReferences.setActiveSkillRecipeSources(event.systemPromptOptions.skills ?? []);
|
|
105
|
+
return {
|
|
106
|
+
systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
105
109
|
Pi.registerToolDefinitions(pi, Tools.createCoreActorToolDefinitions({
|
|
106
110
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
107
111
|
getActiveTools: () => pi.getActiveTools(),
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface AsyncRunStartParams {
|
|
|
38
38
|
template?: CommandTemplateValue;
|
|
39
39
|
args?: string[];
|
|
40
40
|
defaults?: Record<string, unknown>;
|
|
41
|
+
recipe_dir?: string;
|
|
42
|
+
skill_dir?: string;
|
|
41
43
|
parallel?: boolean;
|
|
42
44
|
concurrency?: number | string;
|
|
43
45
|
min_successful?: number | string;
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPol
|
|
|
12
12
|
import * as Paths from "./paths.js";
|
|
13
13
|
import * as RecipesReferences from "./recipes-references.js";
|
|
14
14
|
import * as RecipesUsage from "./recipes-usage.js";
|
|
15
|
+
import * as Schema from "./schema.js";
|
|
15
16
|
import { resolveArtifactPaths, } from "./runs-artifacts.js";
|
|
16
17
|
import { safeRunId } from "./runs-identity.js";
|
|
17
18
|
import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
|
|
@@ -151,7 +152,14 @@ function resolveStartParams(params) {
|
|
|
151
152
|
fileParams.run_id ||
|
|
152
153
|
fileParams.name ||
|
|
153
154
|
getRunIdFromFile(fileParams.file),
|
|
154
|
-
values: {
|
|
155
|
+
values: {
|
|
156
|
+
...(fileParams.values ?? {}),
|
|
157
|
+
...(params.values ?? {}),
|
|
158
|
+
...(fileParams.recipe_dir
|
|
159
|
+
? { recipe_dir: fileParams.recipe_dir }
|
|
160
|
+
: {}),
|
|
161
|
+
...(fileParams.skill_dir ? { skill_dir: fileParams.skill_dir } : {}),
|
|
162
|
+
},
|
|
155
163
|
};
|
|
156
164
|
}
|
|
157
165
|
function readJson(path) {
|
|
@@ -249,13 +257,34 @@ export function startRun(params, cwd) {
|
|
|
249
257
|
!isAbsolute(recipeRelation)
|
|
250
258
|
? dirname(packagedRecipeRoot)
|
|
251
259
|
: undefined;
|
|
252
|
-
const
|
|
260
|
+
const argSpec = Schema.parseToolArgDeclarationList(startParams.args ?? []);
|
|
261
|
+
if (argSpec.error)
|
|
262
|
+
throw new Error(argSpec.error);
|
|
263
|
+
const declaredArgs = new Set(argSpec.args);
|
|
264
|
+
for (const key of Object.keys(startParams.defaults ?? {})) {
|
|
265
|
+
if (declaredArgs.size > 0 && !declaredArgs.has(key))
|
|
266
|
+
throw new Error(`Unknown Recipe default argument: ${key}`);
|
|
267
|
+
}
|
|
268
|
+
const values = Schema.normalizeRuntimeValues({
|
|
269
|
+
...argSpec.defaults,
|
|
270
|
+
...(startParams.defaults || {}),
|
|
253
271
|
...(packagedRepo ? { repo: packagedRepo } : {}),
|
|
254
272
|
...(startParams.values || {}),
|
|
255
273
|
run_id: run,
|
|
256
274
|
state_dir: stateDir,
|
|
257
275
|
trace_file: join(stateDir, "trace.jsonl"),
|
|
258
|
-
};
|
|
276
|
+
}, argSpec.argTypes);
|
|
277
|
+
const runtimeTemplate = resolved.template &&
|
|
278
|
+
typeof resolved.template === "object" &&
|
|
279
|
+
!Array.isArray(resolved.template)
|
|
280
|
+
? {
|
|
281
|
+
...resolved.template,
|
|
282
|
+
defaults: {
|
|
283
|
+
...(resolved.template.defaults ?? {}),
|
|
284
|
+
...values,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
: resolved.template;
|
|
259
288
|
const modelPolicy = describeCurrentPolicyProvenance({
|
|
260
289
|
defaults: startParams.defaults,
|
|
261
290
|
template: resolved.template,
|
|
@@ -312,7 +341,7 @@ export function startRun(params, cwd) {
|
|
|
312
341
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
313
342
|
status: "running",
|
|
314
343
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
315
|
-
template:
|
|
344
|
+
template: runtimeTemplate,
|
|
316
345
|
values,
|
|
317
346
|
model_policy: modelPolicy,
|
|
318
347
|
...(artifacts ? { artifacts } : {}),
|
package/dist/lib/config.js
CHANGED
|
@@ -149,6 +149,7 @@ export function normalizeStoredTool(key, value, reservedToolNames) {
|
|
|
149
149
|
template: argTemplate,
|
|
150
150
|
};
|
|
151
151
|
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
152
|
+
Schema.assertCompatibleToolArgTypes(inferredArgTypes, declarations.argTypes);
|
|
152
153
|
const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
|
|
153
154
|
const cfg = {
|
|
154
155
|
name,
|
package/dist/lib/inspector.js
CHANGED
|
@@ -56,7 +56,12 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
56
56
|
!Array.isArray(primary.recipe)
|
|
57
57
|
? primary.recipe
|
|
58
58
|
: undefined;
|
|
59
|
-
const redactedRecord = (value) => SessionEvidence.redactSessionEvidenceValue(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined
|
|
59
|
+
const redactedRecord = (value) => SessionEvidence.redactSessionEvidenceValue(Object.fromEntries(Object.entries(value).filter(([key, entry]) => entry !== undefined &&
|
|
60
|
+
key !== "recipe_dir" &&
|
|
61
|
+
key !== "skill_dir")));
|
|
62
|
+
const launchValues = meta.values && typeof meta.values === "object" && !Array.isArray(meta.values)
|
|
63
|
+
? Object.fromEntries(Object.entries(meta.values).filter(([key]) => key !== "recipe_dir" && key !== "skill_dir"))
|
|
64
|
+
: meta.values;
|
|
60
65
|
return {
|
|
61
66
|
composition: contexts.map((entry) => redactedRecord({
|
|
62
67
|
...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
|
|
@@ -64,6 +69,7 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
64
69
|
file: entry.file,
|
|
65
70
|
import_path: entry.import_path,
|
|
66
71
|
name: entry.name,
|
|
72
|
+
qualified_name: entry.qualified_name,
|
|
67
73
|
})),
|
|
68
74
|
...(definition ? { definition: redactedRecord(definition) } : {}),
|
|
69
75
|
diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
|
|
@@ -76,7 +82,7 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
76
82
|
launch: redactedRecord({
|
|
77
83
|
cwd: meta.cwd,
|
|
78
84
|
template: meta.template,
|
|
79
|
-
values:
|
|
85
|
+
values: launchValues,
|
|
80
86
|
model_policy: meta.model_policy,
|
|
81
87
|
control: meta.control,
|
|
82
88
|
artifacts: meta.artifacts,
|
|
@@ -53,17 +53,33 @@ function getRecipeCommandTemplateConfig(config) {
|
|
|
53
53
|
? config.template
|
|
54
54
|
: config);
|
|
55
55
|
}
|
|
56
|
+
function getRecipeDiagnosticTemplateConfig(config) {
|
|
57
|
+
const templateConfig = getRecipeCommandTemplateConfig(config);
|
|
58
|
+
return typeof templateConfig === "object" && !Array.isArray(templateConfig)
|
|
59
|
+
? {
|
|
60
|
+
...templateConfig,
|
|
61
|
+
defaults: {
|
|
62
|
+
...(templateConfig.defaults ?? {}),
|
|
63
|
+
...(config.defaults ?? {}),
|
|
64
|
+
...(config.values ?? {}),
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
: {
|
|
68
|
+
defaults: { ...(config.defaults ?? {}), ...(config.values ?? {}) },
|
|
69
|
+
template: templateConfig,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
56
72
|
function getRecipeConfigDiagnostics(file, config) {
|
|
57
73
|
if (!config) {
|
|
58
74
|
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(file);
|
|
59
75
|
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
60
76
|
}
|
|
61
|
-
return CommandTemplates.getCommandTemplateWarnings(
|
|
77
|
+
return CommandTemplates.getCommandTemplateWarnings(getRecipeDiagnosticTemplateConfig(config)).map((warning) => `Recipe ${file}: ${warning}`);
|
|
62
78
|
}
|
|
63
79
|
function getRecipeRiskLabels(config) {
|
|
64
80
|
if (!config)
|
|
65
81
|
return [];
|
|
66
|
-
const labels = new Set(CommandTemplates.getCommandTemplateRiskLabels(
|
|
82
|
+
const labels = new Set(CommandTemplates.getCommandTemplateRiskLabels(getRecipeDiagnosticTemplateConfig(config)));
|
|
67
83
|
if (config.async === true)
|
|
68
84
|
labels.add("risk.long_running");
|
|
69
85
|
return [
|
|
@@ -652,8 +668,9 @@ export function toRegisteredTool(entry) {
|
|
|
652
668
|
const parsed = Schema.parseToolArgToken(String(arg));
|
|
653
669
|
return [parsed.arg, parsed.type];
|
|
654
670
|
}));
|
|
655
|
-
assertToolSafeRepeatConfig(argTemplateConfig, explicitArgTypes, cfg.defaults ?? {});
|
|
671
|
+
assertToolSafeRepeatConfig(argTemplateConfig, explicitArgTypes, { ...(cfg.defaults ?? {}), ...(cfg.values ?? {}) });
|
|
656
672
|
const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
673
|
+
Schema.assertCompatibleToolArgTypes(explicitArgTypes, argTypes);
|
|
657
674
|
return {
|
|
658
675
|
name: entry.id,
|
|
659
676
|
description,
|
|
@@ -40,6 +40,8 @@ export interface TemplateRecipeDefinition {
|
|
|
40
40
|
}
|
|
41
41
|
export interface TemplateRecipeConfig extends TemplateRecipeDefinition {
|
|
42
42
|
async?: boolean;
|
|
43
|
+
recipe_dir?: string;
|
|
44
|
+
skill_dir?: string;
|
|
43
45
|
}
|
|
44
46
|
export interface TemplateRecipeContextRecord {
|
|
45
47
|
alias?: string;
|
|
@@ -47,12 +49,20 @@ export interface TemplateRecipeContextRecord {
|
|
|
47
49
|
file: string;
|
|
48
50
|
import_path: string[];
|
|
49
51
|
name: string;
|
|
52
|
+
qualified_name?: string;
|
|
50
53
|
recipe: Record<string, unknown>;
|
|
51
54
|
role: "entry" | "import";
|
|
52
55
|
}
|
|
53
56
|
export interface ReadResolvedRecipeConfigOptions {
|
|
54
57
|
includeActorRecipeContext?: boolean;
|
|
55
58
|
}
|
|
59
|
+
export interface ActiveSkillRecipeSource {
|
|
60
|
+
name: string;
|
|
61
|
+
filePath?: string;
|
|
62
|
+
baseDir?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function setActiveSkillRecipeSources(skills: ActiveSkillRecipeSource[]): void;
|
|
65
|
+
export declare function getActiveSkillRecipeNamespaces(): Record<string, string[]>;
|
|
56
66
|
export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
|
|
57
67
|
export declare function resolveRecipeReferencePath(value: unknown, currentRecipeRoot?: string): string | undefined;
|
|
58
68
|
export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
|
|
@@ -5,15 +5,106 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { basename, dirname, extname, resolve } from "node:path";
|
|
8
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
9
9
|
import * as CommandTemplates from "./command-templates.js";
|
|
10
10
|
import * as Paths from "./paths.js";
|
|
11
11
|
import * as RecipeControl from "./recipe-control.js";
|
|
12
|
+
import * as Schema from "./schema.js";
|
|
12
13
|
const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
|
|
13
14
|
const MAX_RECIPE_IMPORT_DEPTH = 32;
|
|
15
|
+
const RUNTIME_RECIPE_VALUE_NAMES = new Set(["recipe_dir", "skill_dir"]);
|
|
16
|
+
const activeSkillRecipeRoots = new Map();
|
|
17
|
+
export function setActiveSkillRecipeSources(skills) {
|
|
18
|
+
activeSkillRecipeRoots.clear();
|
|
19
|
+
for (const skill of skills) {
|
|
20
|
+
const name = skill.name.trim();
|
|
21
|
+
const skillDir = skill.baseDir
|
|
22
|
+
? resolve(skill.baseDir)
|
|
23
|
+
: skill.filePath
|
|
24
|
+
? dirname(resolve(skill.filePath))
|
|
25
|
+
: undefined;
|
|
26
|
+
if (!name || !skillDir)
|
|
27
|
+
continue;
|
|
28
|
+
const roots = activeSkillRecipeRoots.get(name) ?? new Set();
|
|
29
|
+
roots.add(resolve(skillDir, "recipes"));
|
|
30
|
+
activeSkillRecipeRoots.set(name, roots);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function getActiveSkillRecipeNamespaces() {
|
|
34
|
+
return Object.fromEntries([...activeSkillRecipeRoots.entries()]
|
|
35
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
36
|
+
.map(([name, roots]) => [name, [...roots].sort()]));
|
|
37
|
+
}
|
|
38
|
+
function qualifiedRecipeNameForFile(file) {
|
|
39
|
+
const path = resolve(file);
|
|
40
|
+
const stdRelation = relative(Paths.getPackagedRecipeRoot(), path);
|
|
41
|
+
if (stdRelation &&
|
|
42
|
+
!stdRelation.startsWith("..") &&
|
|
43
|
+
!isAbsolute(stdRelation)) {
|
|
44
|
+
return `std:${stdRelation.replace(/\.(?:json|md)$/u, "")}`;
|
|
45
|
+
}
|
|
46
|
+
for (const [skillName, roots] of activeSkillRecipeRoots) {
|
|
47
|
+
if (roots.size !== 1)
|
|
48
|
+
continue;
|
|
49
|
+
const root = [...roots][0];
|
|
50
|
+
const relation = relative(root, path);
|
|
51
|
+
if (relation && !relation.startsWith("..") && !isAbsolute(relation))
|
|
52
|
+
return `skill:${skillName}/${relation.replaceAll("\\", "/").replace(/\.(?:json|md)$/u, "")}`;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function qualifiedRecipeCandidates(value) {
|
|
57
|
+
if (value.startsWith("std:")) {
|
|
58
|
+
return recipeNameFiles(value.slice(4)).map((file) => resolve(Paths.getPackagedRecipeRoot(), file));
|
|
59
|
+
}
|
|
60
|
+
if (!value.startsWith("skill:"))
|
|
61
|
+
return undefined;
|
|
62
|
+
const qualified = value.slice(6);
|
|
63
|
+
const separator = qualified.indexOf("/");
|
|
64
|
+
if (separator <= 0 || separator === qualified.length - 1)
|
|
65
|
+
throw new Error(`Invalid Skill Recipe reference: ${value}`);
|
|
66
|
+
const skillName = qualified.slice(0, separator);
|
|
67
|
+
const recipeName = qualified.slice(separator + 1);
|
|
68
|
+
const roots = [...(activeSkillRecipeRoots.get(skillName) ?? [])];
|
|
69
|
+
if (roots.length === 0)
|
|
70
|
+
throw new Error(`Active Skill Recipe namespace not found: ${skillName}`);
|
|
71
|
+
if (roots.length > 1)
|
|
72
|
+
throw new Error(`Ambiguous active Skill Recipe namespace ${skillName}: ${roots.join(", ")}`);
|
|
73
|
+
const root = roots[0];
|
|
74
|
+
return recipeNameFiles(recipeName).map((file) => {
|
|
75
|
+
const candidate = resolve(root, file);
|
|
76
|
+
const relation = relative(root, candidate);
|
|
77
|
+
if (!relation || relation.startsWith("..") || isAbsolute(relation))
|
|
78
|
+
throw new Error(`Skill Recipe reference escapes its recipes root: ${value}`);
|
|
79
|
+
return candidate;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
14
82
|
function hasWhitespace(value) {
|
|
15
83
|
return /\s/.test(value);
|
|
16
84
|
}
|
|
85
|
+
function findOwningSkillDir(recipeFile) {
|
|
86
|
+
let current = dirname(recipeFile);
|
|
87
|
+
while (true) {
|
|
88
|
+
if (existsSync(resolve(current, "SKILL.md")))
|
|
89
|
+
return current;
|
|
90
|
+
const parent = dirname(current);
|
|
91
|
+
if (parent === current)
|
|
92
|
+
return undefined;
|
|
93
|
+
current = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function assertRuntimeRecipeValuesNotDeclared(raw) {
|
|
97
|
+
const args = Array.isArray(raw.args)
|
|
98
|
+
? Schema.getExplicitToolArgNames(raw.args.map(String))
|
|
99
|
+
: [];
|
|
100
|
+
for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
|
|
101
|
+
if (args.includes(name) ||
|
|
102
|
+
(isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
|
|
103
|
+
(isRecord(raw.values) && Object.hasOwn(raw.values, name))) {
|
|
104
|
+
throw new Error(`Recipe runtime value is reserved: ${name}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
17
108
|
export function resolveRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
|
|
18
109
|
const trimmed = value.trim();
|
|
19
110
|
const repoRoot = resolve(recipeRoot, "..");
|
|
@@ -42,6 +133,9 @@ function recipeNameFiles(value) {
|
|
|
42
133
|
return [`${trimmed}.json`, `${trimmed}.md`];
|
|
43
134
|
}
|
|
44
135
|
function recipeCandidatePaths(value, currentRecipeRoot) {
|
|
136
|
+
const qualified = qualifiedRecipeCandidates(value.trim());
|
|
137
|
+
if (qualified)
|
|
138
|
+
return qualified;
|
|
45
139
|
if (!isBareRecipeName(value))
|
|
46
140
|
return [resolveRecipePath(value, currentRecipeRoot)];
|
|
47
141
|
const roots = [
|
|
@@ -495,11 +589,46 @@ function mergeDefaults(...items) {
|
|
|
495
589
|
const merged = Object.assign({}, ...items.filter(Boolean));
|
|
496
590
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
497
591
|
}
|
|
592
|
+
function assertRecipeDefaultsDeclared(args, defaults, label) {
|
|
593
|
+
const spec = Schema.parseToolArgDeclarationList(args ?? []);
|
|
594
|
+
if (spec.error)
|
|
595
|
+
throw new Error(spec.error);
|
|
596
|
+
if (spec.args.length === 0)
|
|
597
|
+
return;
|
|
598
|
+
const declared = new Set(spec.args);
|
|
599
|
+
for (const key of Object.keys(defaults ?? {})) {
|
|
600
|
+
if (!declared.has(key))
|
|
601
|
+
throw new Error(`Unknown ${label} argument: ${key}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function assertRecipeArgumentContract(config, label = "Recipe default") {
|
|
605
|
+
assertRecipeDefaultsDeclared(config.args, config.defaults, label);
|
|
606
|
+
const spec = Schema.parseToolArgDeclarationList(config.args ?? []);
|
|
607
|
+
if (spec.error)
|
|
608
|
+
throw new Error(spec.error);
|
|
609
|
+
Schema.assertCompatibleToolArgTypes(spec.argTypes, Schema.getTemplateArgTypes({ template: config.template }));
|
|
610
|
+
const staticValues = Object.fromEntries(Object.entries({ ...config.defaults, ...config.values }).filter(([, value]) => typeof value !== "string" || !/\{[^{}]+\}/.test(value)));
|
|
611
|
+
Schema.normalizeRuntimeValues(staticValues, spec.argTypes);
|
|
612
|
+
}
|
|
613
|
+
function resolveRecipeValueLayers(args, ...layers) {
|
|
614
|
+
const spec = Schema.parseToolArgDeclarationList(args ?? []);
|
|
615
|
+
if (spec.error)
|
|
616
|
+
throw new Error(spec.error);
|
|
617
|
+
const merged = mergeDefaults(spec.defaults, ...layers);
|
|
618
|
+
if (!merged)
|
|
619
|
+
return undefined;
|
|
620
|
+
const staticValues = Object.fromEntries(Object.entries(merged).filter(([, value]) => typeof value !== "string" || !/\{[^{}]+\}/.test(value)));
|
|
621
|
+
return {
|
|
622
|
+
...merged,
|
|
623
|
+
...Schema.normalizeRuntimeValues(staticValues, spec.argTypes),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
498
626
|
function applyDefaultsToTemplate(template, defaults, overrides) {
|
|
499
627
|
const cleanOverrides = { ...overrides };
|
|
500
628
|
delete cleanOverrides.name;
|
|
501
629
|
delete cleanOverrides.template;
|
|
502
630
|
delete cleanOverrides.values;
|
|
631
|
+
delete cleanOverrides.defaults;
|
|
503
632
|
if (typeof template === "object" && !Array.isArray(template)) {
|
|
504
633
|
return {
|
|
505
634
|
...template,
|
|
@@ -537,7 +666,15 @@ function loadDelegatedRecipe(value, currentRecipeFile, stack, options) {
|
|
|
537
666
|
return config;
|
|
538
667
|
}
|
|
539
668
|
function applyDelegatedRecipeToNode(delegated, overrides = {}) {
|
|
540
|
-
|
|
669
|
+
const nodeDefaults = isRecord(overrides.defaults)
|
|
670
|
+
? overrides.defaults
|
|
671
|
+
: undefined;
|
|
672
|
+
const nodeValues = isRecord(overrides.values) ? overrides.values : undefined;
|
|
673
|
+
const values = resolveRecipeValueLayers(delegated.args, delegated.defaults, delegated.values, nodeDefaults, nodeValues, {
|
|
674
|
+
...(delegated.recipe_dir ? { recipe_dir: delegated.recipe_dir } : {}),
|
|
675
|
+
...(delegated.skill_dir ? { skill_dir: delegated.skill_dir } : {}),
|
|
676
|
+
});
|
|
677
|
+
return applyDefaultsToTemplate(delegated.template, values, overrides);
|
|
541
678
|
}
|
|
542
679
|
function expandRecipeDelegations(value, currentRecipeFile, stack, options = {}) {
|
|
543
680
|
if (typeof value === "string") {
|
|
@@ -593,7 +730,7 @@ function expandImportNodes(value, imports, options = {}) {
|
|
|
593
730
|
? record.defaults
|
|
594
731
|
: undefined;
|
|
595
732
|
const nodeValues = isRecord(record.values) ? record.values : undefined;
|
|
596
|
-
const defaults =
|
|
733
|
+
const defaults = resolveRecipeValueLayers(imported.config.args, imported.defaults, imported.values, nodeDefaults, nodeValues);
|
|
597
734
|
const expanded = applyDefaultsToTemplate(imported.config.template, defaults, record);
|
|
598
735
|
return options.includeActorRecipeContext
|
|
599
736
|
? withActorRecipeContext(expanded, {
|
|
@@ -631,6 +768,8 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
631
768
|
if (!raw || !Object.hasOwn(raw, "template"))
|
|
632
769
|
return undefined;
|
|
633
770
|
RecipeControl.assertRecipeHasNoMailbox(raw);
|
|
771
|
+
assertRuntimeRecipeValuesNotDeclared(raw);
|
|
772
|
+
assertRecipeArgumentContract(raw);
|
|
634
773
|
const imports = {};
|
|
635
774
|
for (const [alias, binding] of Object.entries(getRecipeImports(raw))) {
|
|
636
775
|
const importPath = resolveRecipeImportPath(getImportFrom(binding), dirname(path));
|
|
@@ -639,13 +778,18 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
639
778
|
throw new Error(`Recipe import not found: ${alias}`);
|
|
640
779
|
const bindingDefaults = typeof binding === "string" ? undefined : binding.defaults;
|
|
641
780
|
const bindingValues = typeof binding === "string" ? undefined : binding.values;
|
|
781
|
+
assertRecipeDefaultsDeclared(config.args, bindingDefaults, `Recipe import ${alias} default`);
|
|
642
782
|
imports[alias] = {
|
|
643
783
|
alias,
|
|
644
784
|
file: importPath,
|
|
645
785
|
name: config.name ?? alias,
|
|
646
786
|
config,
|
|
647
787
|
defaults: { ...(config.defaults ?? {}), ...(bindingDefaults ?? {}) },
|
|
648
|
-
values: {
|
|
788
|
+
values: {
|
|
789
|
+
...(bindingValues ?? {}),
|
|
790
|
+
...(config.recipe_dir ? { recipe_dir: config.recipe_dir } : {}),
|
|
791
|
+
...(config.skill_dir ? { skill_dir: config.skill_dir } : {}),
|
|
792
|
+
},
|
|
649
793
|
};
|
|
650
794
|
}
|
|
651
795
|
const substituted = substituteImportRefs(raw, imports);
|
|
@@ -661,6 +805,17 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
661
805
|
: {})
|
|
662
806
|
: expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
|
|
663
807
|
const recipeName = getRecipeIdFromPath(path);
|
|
808
|
+
const recipeDir = dirname(path);
|
|
809
|
+
const skillDir = findOwningSkillDir(path);
|
|
810
|
+
if (!skillDir &&
|
|
811
|
+
JSON.stringify({
|
|
812
|
+
artifacts: substituted.artifacts,
|
|
813
|
+
defaults: substituted.defaults,
|
|
814
|
+
template: substituted.template,
|
|
815
|
+
values: substituted.values,
|
|
816
|
+
}).includes("{skill_dir}")) {
|
|
817
|
+
throw new Error(`Recipe uses {skill_dir} outside an owning Skill directory: ${path}`);
|
|
818
|
+
}
|
|
664
819
|
const templateWithContext = options.includeActorRecipeContext
|
|
665
820
|
? withActorRecipeContext(expandedTemplate, {
|
|
666
821
|
file: path,
|
|
@@ -678,6 +833,8 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
678
833
|
: delegated?.control);
|
|
679
834
|
return {
|
|
680
835
|
name: recipeName,
|
|
836
|
+
recipe_dir: recipeDir,
|
|
837
|
+
...(skillDir ? { skill_dir: skillDir } : {}),
|
|
681
838
|
...(typeof substituted.description === "string" &&
|
|
682
839
|
substituted.description.trim()
|
|
683
840
|
? { description: substituted.description.trim() }
|
|
@@ -765,7 +922,11 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
765
922
|
...(typeof substituted.repeat === "number"
|
|
766
923
|
? { repeat: substituted.repeat }
|
|
767
924
|
: {}),
|
|
768
|
-
|
|
925
|
+
values: {
|
|
926
|
+
...(isRecord(substituted.values) ? substituted.values : {}),
|
|
927
|
+
recipe_dir: recipeDir,
|
|
928
|
+
...(skillDir ? { skill_dir: skillDir } : {}),
|
|
929
|
+
},
|
|
769
930
|
...(isRecord(substituted.usage) ? { usage: substituted.usage } : {}),
|
|
770
931
|
};
|
|
771
932
|
}
|
|
@@ -780,12 +941,14 @@ function collectRecipeContextRecords(file, stack, importPath, alias) {
|
|
|
780
941
|
const raw = readRawRecipeConfig(path);
|
|
781
942
|
if (!raw || !Object.hasOwn(raw, "template"))
|
|
782
943
|
return [];
|
|
944
|
+
const qualifiedName = qualifiedRecipeNameForFile(path);
|
|
783
945
|
const record = {
|
|
784
946
|
...(alias ? { alias } : {}),
|
|
785
947
|
depth: stack.length,
|
|
786
948
|
file: path,
|
|
787
949
|
import_path: importPath,
|
|
788
950
|
name: getRecipeIdFromPath(path),
|
|
951
|
+
...(qualifiedName ? { qualified_name: qualifiedName } : {}),
|
|
789
952
|
recipe: raw,
|
|
790
953
|
role: stack.length === 0 ? "entry" : "import",
|
|
791
954
|
};
|
package/dist/lib/schema.d.ts
CHANGED
|
@@ -44,14 +44,17 @@ export declare function objectSchema(properties: Record<string, JsonSchema>, req
|
|
|
44
44
|
export declare function looseObjectSchema(description: string): JsonSchema;
|
|
45
45
|
export declare function parseToolArgToken(value: string): ParsedToolArgToken;
|
|
46
46
|
export declare function parseToolArgDeclarations(value: string): ToolArgSpec;
|
|
47
|
+
export declare function parseToolArgDeclarationList(value: unknown[]): ToolArgSpec;
|
|
47
48
|
export declare function normalizeStoredToolArgDeclarations(argsValue: unknown, defaultsValue: unknown): ToolArgSpec & {
|
|
48
49
|
changed: boolean;
|
|
49
50
|
provided: boolean;
|
|
50
51
|
};
|
|
51
52
|
export declare function formatToolArgs(args: string[]): string;
|
|
52
53
|
export declare function getTemplatePlaceholderNames(config: CommandTemplates.CommandTemplateConfig): string[];
|
|
54
|
+
export declare function assertCompatibleToolArgTypes(...sources: Array<Record<string, ToolArgType> | undefined>): void;
|
|
53
55
|
export declare function getTemplateArgTypes(config: CommandTemplates.CommandTemplateConfig): Record<string, ToolArgType>;
|
|
54
56
|
export declare function getExplicitToolArgNames(args: string[] | undefined): string[];
|
|
57
|
+
export declare function getExplicitToolArgDefaults(args: string[] | string | undefined): Record<string, string>;
|
|
55
58
|
export declare function getToolArgNames(config: CommandTemplates.CommandTemplateConfig): string[];
|
|
56
59
|
export declare function getRequiredToolArgNames(config: CommandTemplates.CommandTemplateConfig): Set<string>;
|
|
57
60
|
export declare function normalizeRuntimeValues(values: Record<string, unknown>, argTypes: Record<string, ToolArgType> | undefined): Record<string, unknown>;
|
package/dist/lib/schema.js
CHANGED
|
@@ -147,7 +147,10 @@ function isValidDefault(type, value) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
export function parseToolArgDeclarations(value) {
|
|
150
|
-
|
|
150
|
+
return parseToolArgDeclarationList(splitArgDeclarations(value));
|
|
151
|
+
}
|
|
152
|
+
export function parseToolArgDeclarationList(value) {
|
|
153
|
+
const source = value.map(String);
|
|
151
154
|
const seen = new Set();
|
|
152
155
|
const duplicates = new Set();
|
|
153
156
|
const args = [];
|
|
@@ -272,6 +275,8 @@ function collectCommandTemplateConfigDeclarations(config, declarations) {
|
|
|
272
275
|
collectTemplatePlaceholderDeclarations(config, declarations);
|
|
273
276
|
return;
|
|
274
277
|
}
|
|
278
|
+
if (!config || typeof config !== "object")
|
|
279
|
+
return;
|
|
275
280
|
if (Array.isArray(config)) {
|
|
276
281
|
for (const step of config)
|
|
277
282
|
collectCommandTemplateConfigDeclarations(step, declarations);
|
|
@@ -296,9 +301,30 @@ function getTemplatePlaceholderDeclarations(config) {
|
|
|
296
301
|
export function getTemplatePlaceholderNames(config) {
|
|
297
302
|
return mergeUnique(getTemplatePlaceholderDeclarations(config).map((item) => item.arg));
|
|
298
303
|
}
|
|
304
|
+
function toolArgTypesEqual(left, right) {
|
|
305
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
306
|
+
}
|
|
307
|
+
export function assertCompatibleToolArgTypes(...sources) {
|
|
308
|
+
const seen = {};
|
|
309
|
+
for (const source of sources) {
|
|
310
|
+
for (const [name, type] of Object.entries(source ?? {})) {
|
|
311
|
+
const previous = seen[name];
|
|
312
|
+
if (previous && !toolArgTypesEqual(previous, type)) {
|
|
313
|
+
throw new Error(`Conflicting argument type for ${name}: ${canonicalArgDeclaration(name, previous)} vs ${canonicalArgDeclaration(name, type)}`);
|
|
314
|
+
}
|
|
315
|
+
seen[name] = type;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
299
319
|
export function getTemplateArgTypes(config) {
|
|
300
320
|
const argTypes = {};
|
|
301
321
|
for (const declaration of getTemplatePlaceholderDeclarations(config)) {
|
|
322
|
+
const previous = argTypes[declaration.arg];
|
|
323
|
+
if (previous &&
|
|
324
|
+
declaration.type.kind !== "string" &&
|
|
325
|
+
!toolArgTypesEqual(previous, declaration.type)) {
|
|
326
|
+
throw new Error(`Conflicting argument type for ${declaration.arg}: ${canonicalArgDeclaration(declaration.arg, previous)} vs ${declaration.declaration}`);
|
|
327
|
+
}
|
|
302
328
|
if (declaration.type.kind !== "string")
|
|
303
329
|
argTypes[declaration.arg] = declaration.type;
|
|
304
330
|
}
|
|
@@ -307,6 +333,13 @@ export function getTemplateArgTypes(config) {
|
|
|
307
333
|
export function getExplicitToolArgNames(args) {
|
|
308
334
|
return mergeUnique((args ?? []).map((item) => parseToolArgToken(String(item)).arg));
|
|
309
335
|
}
|
|
336
|
+
export function getExplicitToolArgDefaults(args) {
|
|
337
|
+
const declarations = typeof args === "string" ? splitArgDeclarations(args) : (args ?? []);
|
|
338
|
+
return Object.fromEntries(declarations
|
|
339
|
+
.map((item) => parseToolArgToken(String(item)))
|
|
340
|
+
.filter((item) => item.defaultValue !== undefined)
|
|
341
|
+
.map((item) => [item.arg, item.defaultValue]));
|
|
342
|
+
}
|
|
310
343
|
export function getToolArgNames(config) {
|
|
311
344
|
const normalizedConfig = CommandTemplates.normalizeCommandTemplateConfig(config);
|
|
312
345
|
const declaredArgs = Array.isArray(normalizedConfig.args)
|
|
@@ -12,6 +12,7 @@ import * as Limits from "./limits.js";
|
|
|
12
12
|
import { computeRunControlCapacity } from "./run-evidence-policy.js";
|
|
13
13
|
import * as Paths from "./paths.js";
|
|
14
14
|
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
15
|
+
import * as RecipesReferences from "./recipes-references.js";
|
|
15
16
|
import * as ReviewDiagnostics from "./review-diagnostics.js";
|
|
16
17
|
import * as RunsControls from "./runs-controls.js";
|
|
17
18
|
import * as RunControlDelivery from "./runs-control-delivery.js";
|
|
@@ -172,8 +173,17 @@ function inspectRecipes(view, deps) {
|
|
|
172
173
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
173
174
|
{ root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
|
|
174
175
|
]);
|
|
176
|
+
const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces();
|
|
175
177
|
const summary = {
|
|
176
178
|
...RecipesDiscovery.summarizeDiscovery(discovered),
|
|
179
|
+
skill_recipe_namespaces: skillRecipeNamespaces,
|
|
180
|
+
skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
|
|
181
|
+
.filter(([, roots]) => roots.length > 1)
|
|
182
|
+
.map(([name, roots]) => ({
|
|
183
|
+
name,
|
|
184
|
+
roots,
|
|
185
|
+
error: `Ambiguous active Skill Recipe namespace ${name}`,
|
|
186
|
+
})),
|
|
177
187
|
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
178
188
|
};
|
|
179
189
|
return {
|