@llblab/pi-actors 0.44.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -0
- package/README.md +5 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +10 -106
- 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/extension-runtime.d.ts +21 -0
- package/dist/lib/extension-runtime.js +116 -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/async-runner.mjs +0 -16
- package/dist/scripts/executable-block-style.d.mts +1 -0
- package/dist/scripts/executable-block-style.mjs +73 -0
- package/dist/scripts/locker.mjs +0 -21
- package/dist/scripts/recipe-utils.mjs +0 -1
- package/dist/scripts/release-gates.mjs +14 -11
- package/dist/scripts/validate-recipe.mjs +0 -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 +18 -107
- package/lib/async-runs.ts +43 -9
- package/lib/config.ts +4 -0
- package/lib/extension-runtime.ts +139 -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 +2 -2
- package/scripts/async-runner.mjs +0 -16
- package/scripts/executable-block-style.d.mts +1 -0
- package/scripts/executable-block-style.mjs +73 -0
- package/scripts/locker.mjs +0 -21
- package/scripts/recipe-utils.mjs +0 -1
- package/scripts/release-gates.mjs +14 -11
- package/scripts/validate-recipe.mjs +0 -1
- package/skills/actors/SKILL.md +2 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi actor extension runtime coordinator.
|
|
3
|
+
* Zones: extension session lifecycle, host tool adaptation, runtime service composition
|
|
4
|
+
* Owns low-level Pi lifecycle effects and tool wrapping without owning event registration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as AutomaticReviewRuntime from "./automatic-review-runtime.ts";
|
|
8
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
9
|
+
import * as Paths from "./paths.ts";
|
|
10
|
+
import * as Pi from "./pi.ts";
|
|
11
|
+
import * as Prompts from "./prompts.ts";
|
|
12
|
+
import * as RecipesReferences from "./recipes-references.ts";
|
|
13
|
+
import * as RunUiRuntime from "./run-ui-runtime.ts";
|
|
14
|
+
import * as Runtime from "./runtime.ts";
|
|
15
|
+
import * as Temp from "./temp.ts";
|
|
16
|
+
import * as Tools from "./tools.ts";
|
|
17
|
+
import * as ToolsResponse from "./tools-response.ts";
|
|
18
|
+
|
|
19
|
+
export interface ActorExtensionRuntime {
|
|
20
|
+
beforeAgentStart(
|
|
21
|
+
systemPrompt: string,
|
|
22
|
+
skills: RecipesReferences.ActiveSkillRecipeSource[],
|
|
23
|
+
): { systemPrompt: string };
|
|
24
|
+
discoverResources(metaUrl: string): { skillPaths: string[] } | undefined;
|
|
25
|
+
getRunOwnerId(ctx: Pi.ExtensionContext): string;
|
|
26
|
+
onAgentEnd(ctx: Pi.ExtensionContext): void;
|
|
27
|
+
onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
|
|
28
|
+
onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
|
|
29
|
+
registerCoreTools(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createActorExtensionRuntime(
|
|
33
|
+
pi: Pi.ExtensionAPI,
|
|
34
|
+
): ActorExtensionRuntime {
|
|
35
|
+
let activeRunContext: Pi.ExtensionContext | undefined;
|
|
36
|
+
const getRunOwnerId = Pi.getSessionId;
|
|
37
|
+
const automaticReview = AutomaticReviewRuntime.createAutomaticReviewRuntime({
|
|
38
|
+
getActiveContext: () => activeRunContext,
|
|
39
|
+
getRunOwnerId,
|
|
40
|
+
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
41
|
+
});
|
|
42
|
+
const runUiRuntime = RunUiRuntime.createRunUiRuntime({
|
|
43
|
+
getActiveContext: () => activeRunContext,
|
|
44
|
+
getRunOwnerId,
|
|
45
|
+
onRunEvent: automaticReview.schedule,
|
|
46
|
+
pi,
|
|
47
|
+
});
|
|
48
|
+
const actorToolDefinitions = new Map<string, Tools.ActorToolDefinition>();
|
|
49
|
+
const withCurrentThinkingContext = <T extends Tools.ActorToolDefinition>(
|
|
50
|
+
definition: T,
|
|
51
|
+
): T => {
|
|
52
|
+
if (typeof definition.execute !== "function") return definition;
|
|
53
|
+
const execute = definition.execute as (...args: unknown[]) => unknown;
|
|
54
|
+
return {
|
|
55
|
+
...definition,
|
|
56
|
+
execute: async (...args: unknown[]) => {
|
|
57
|
+
const nextArgs = [...args];
|
|
58
|
+
const ctx = nextArgs[4];
|
|
59
|
+
if (ctx && typeof ctx === "object") {
|
|
60
|
+
nextArgs[4] = {
|
|
61
|
+
...(ctx as Record<string, unknown>),
|
|
62
|
+
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return ToolsResponse.spaceToolResult(await execute(...nextArgs));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw ToolsResponse.spaceToolError(error);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
} as T;
|
|
72
|
+
};
|
|
73
|
+
const runtime = Runtime.createAutoToolsRuntime({
|
|
74
|
+
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
75
|
+
exec: CommandTemplates.execCommandTemplate,
|
|
76
|
+
getActiveTools: () => pi.getActiveTools(),
|
|
77
|
+
registerTool: (definition) => {
|
|
78
|
+
const wrapped = withCurrentThinkingContext(definition);
|
|
79
|
+
actorToolDefinitions.set(wrapped.name, wrapped);
|
|
80
|
+
pi.registerTool(wrapped);
|
|
81
|
+
},
|
|
82
|
+
reservedToolNames: Tools.RESERVED_TOOL_NAMES,
|
|
83
|
+
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
84
|
+
});
|
|
85
|
+
const recipeReload = Runtime.createRecipeToolReloadWatcher(runtime);
|
|
86
|
+
return {
|
|
87
|
+
beforeAgentStart(systemPrompt, skills) {
|
|
88
|
+
RecipesReferences.setActiveSkillRecipeSources(skills);
|
|
89
|
+
return {
|
|
90
|
+
systemPrompt: `${systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
discoverResources(metaUrl) {
|
|
94
|
+
const skillPaths = Paths.getExistingExtensionSkillPaths(metaUrl);
|
|
95
|
+
return skillPaths.length > 0 ? { skillPaths } : undefined;
|
|
96
|
+
},
|
|
97
|
+
getRunOwnerId,
|
|
98
|
+
onAgentEnd(ctx) {
|
|
99
|
+
if (activeRunContext === ctx) automaticReview.schedule();
|
|
100
|
+
},
|
|
101
|
+
onSessionShutdown(reason, ctx) {
|
|
102
|
+
activeRunContext = undefined;
|
|
103
|
+
automaticReview.close();
|
|
104
|
+
recipeReload.close();
|
|
105
|
+
runUiRuntime.shutdown(reason, ctx);
|
|
106
|
+
},
|
|
107
|
+
async onSessionStart(ctx) {
|
|
108
|
+
ctx.ui.setWidget("zz-pi-actors-comms", undefined);
|
|
109
|
+
activeRunContext = ctx;
|
|
110
|
+
runUiRuntime.close();
|
|
111
|
+
automaticReview.close();
|
|
112
|
+
recipeReload.close();
|
|
113
|
+
await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
|
|
114
|
+
if (activeRunContext !== ctx) return;
|
|
115
|
+
automaticReview.start(ctx);
|
|
116
|
+
runtime.loadTools(ctx);
|
|
117
|
+
runUiRuntime.start(ctx);
|
|
118
|
+
recipeReload.watch(ctx);
|
|
119
|
+
},
|
|
120
|
+
registerCoreTools() {
|
|
121
|
+
Pi.registerToolDefinitions(
|
|
122
|
+
pi,
|
|
123
|
+
Tools.createCoreActorToolDefinitions<Pi.ExtensionContext>({
|
|
124
|
+
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
125
|
+
getActiveTools: () => pi.getActiveTools(),
|
|
126
|
+
getRuntimeTool: (name) =>
|
|
127
|
+
Tools.resolveActiveRuntimeTool(
|
|
128
|
+
name,
|
|
129
|
+
runtime.getTools(),
|
|
130
|
+
(activeName) => actorToolDefinitions.get(activeName),
|
|
131
|
+
),
|
|
132
|
+
handleRuntimeControl: automaticReview.handleControl,
|
|
133
|
+
registryRuntime: runtime,
|
|
134
|
+
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
135
|
+
}).map(withCurrentThinkingContext),
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
package/lib/inspector.ts
CHANGED
|
@@ -105,9 +105,22 @@ export function readActorInspectorRecipe(
|
|
|
105
105
|
const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
|
|
106
106
|
SessionEvidence.redactSessionEvidenceValue(
|
|
107
107
|
Object.fromEntries(
|
|
108
|
-
Object.entries(value).filter(
|
|
108
|
+
Object.entries(value).filter(
|
|
109
|
+
([key, entry]) =>
|
|
110
|
+
entry !== undefined &&
|
|
111
|
+
key !== "recipe_dir" &&
|
|
112
|
+
key !== "skill_dir",
|
|
113
|
+
),
|
|
109
114
|
),
|
|
110
115
|
) as Record<string, unknown>;
|
|
116
|
+
const launchValues =
|
|
117
|
+
meta.values && typeof meta.values === "object" && !Array.isArray(meta.values)
|
|
118
|
+
? Object.fromEntries(
|
|
119
|
+
Object.entries(meta.values as Record<string, unknown>).filter(
|
|
120
|
+
([key]) => key !== "recipe_dir" && key !== "skill_dir",
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
: meta.values;
|
|
111
124
|
return {
|
|
112
125
|
composition: contexts.map((entry) =>
|
|
113
126
|
redactedRecord({
|
|
@@ -116,6 +129,7 @@ export function readActorInspectorRecipe(
|
|
|
116
129
|
file: entry.file,
|
|
117
130
|
import_path: entry.import_path,
|
|
118
131
|
name: entry.name,
|
|
132
|
+
qualified_name: entry.qualified_name,
|
|
119
133
|
}),
|
|
120
134
|
),
|
|
121
135
|
...(definition ? { definition: redactedRecord(definition) } : {}),
|
|
@@ -129,7 +143,7 @@ export function readActorInspectorRecipe(
|
|
|
129
143
|
launch: redactedRecord({
|
|
130
144
|
cwd: meta.cwd,
|
|
131
145
|
template: meta.template,
|
|
132
|
-
values:
|
|
146
|
+
values: launchValues,
|
|
133
147
|
model_policy: meta.model_policy,
|
|
134
148
|
control: meta.control,
|
|
135
149
|
artifacts: meta.artifacts,
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -122,6 +122,25 @@ function getRecipeCommandTemplateConfig(
|
|
|
122
122
|
: config) as CommandTemplates.CommandTemplateConfig;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function getRecipeDiagnosticTemplateConfig(
|
|
126
|
+
config: TemplateRecipeConfig,
|
|
127
|
+
): CommandTemplates.CommandTemplateObjectConfig {
|
|
128
|
+
const templateConfig = getRecipeCommandTemplateConfig(config);
|
|
129
|
+
return typeof templateConfig === "object" && !Array.isArray(templateConfig)
|
|
130
|
+
? {
|
|
131
|
+
...templateConfig,
|
|
132
|
+
defaults: {
|
|
133
|
+
...(templateConfig.defaults ?? {}),
|
|
134
|
+
...(config.defaults ?? {}),
|
|
135
|
+
...(config.values ?? {}),
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: {
|
|
139
|
+
defaults: { ...(config.defaults ?? {}), ...(config.values ?? {}) },
|
|
140
|
+
template: templateConfig,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
function getRecipeConfigDiagnostics(
|
|
126
145
|
file: string,
|
|
127
146
|
config: TemplateRecipeConfig | undefined,
|
|
@@ -131,7 +150,7 @@ function getRecipeConfigDiagnostics(
|
|
|
131
150
|
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
132
151
|
}
|
|
133
152
|
return CommandTemplates.getCommandTemplateWarnings(
|
|
134
|
-
|
|
153
|
+
getRecipeDiagnosticTemplateConfig(config),
|
|
135
154
|
).map((warning) => `Recipe ${file}: ${warning}`);
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -141,7 +160,7 @@ function getRecipeRiskLabels(
|
|
|
141
160
|
if (!config) return [];
|
|
142
161
|
const labels = new Set(
|
|
143
162
|
CommandTemplates.getCommandTemplateRiskLabels(
|
|
144
|
-
|
|
163
|
+
getRecipeDiagnosticTemplateConfig(config),
|
|
145
164
|
),
|
|
146
165
|
);
|
|
147
166
|
if (config.async === true) labels.add("risk.long_running");
|
|
@@ -846,9 +865,10 @@ export function toRegisteredTool(
|
|
|
846
865
|
assertToolSafeRepeatConfig(
|
|
847
866
|
argTemplateConfig,
|
|
848
867
|
explicitArgTypes,
|
|
849
|
-
cfg.defaults ?? {},
|
|
868
|
+
{ ...(cfg.defaults ?? {}), ...(cfg.values ?? {}) },
|
|
850
869
|
);
|
|
851
870
|
const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
871
|
+
Schema.assertCompatibleToolArgTypes(explicitArgTypes, argTypes);
|
|
852
872
|
return {
|
|
853
873
|
name: entry.id,
|
|
854
874
|
description,
|
|
@@ -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[] {
|