@llblab/pi-actors 0.46.0 → 0.46.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/BACKLOG.md +582 -0
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/lib/async-runs.d.ts +1 -0
- package/dist/lib/async-runs.js +4 -1
- package/dist/lib/execution.d.ts +1 -0
- package/dist/lib/execution.js +1 -0
- package/dist/lib/extension-runtime.js +25 -9
- package/dist/lib/inspector.js +1 -0
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipes-context.d.ts +12 -3
- package/dist/lib/recipes-context.js +28 -4
- package/dist/lib/recipes-discovery.d.ts +18 -0
- package/dist/lib/recipes-discovery.js +95 -22
- package/dist/lib/recipes-references.d.ts +18 -0
- package/dist/lib/recipes-references.js +120 -33
- package/dist/lib/registry.d.ts +14 -1
- package/dist/lib/registry.js +110 -87
- package/dist/lib/runtime.d.ts +30 -3
- package/dist/lib/runtime.js +73 -9
- package/dist/lib/tools-inspect.d.ts +3 -0
- package/dist/lib/tools-inspect.js +40 -20
- package/dist/lib/tools-local.d.ts +2 -2
- package/dist/lib/tools-local.js +5 -2
- package/dist/lib/tools-response.js +2 -0
- package/dist/lib/tools-spawn.d.ts +2 -2
- package/dist/lib/tools-spawn.js +1 -1
- package/dist/lib/tools.d.ts +4 -1
- package/dist/lib/tools.js +2 -0
- package/dist/skills/actors/SKILL.md +3 -3
- package/docs/template-recipes.md +3 -1
- package/docs/tool-registry.md +9 -3
- package/lib/async-runs.ts +5 -1
- package/lib/execution.ts +2 -0
- package/lib/extension-runtime.ts +33 -14
- package/lib/inspector.ts +1 -0
- package/lib/prompts.ts +2 -2
- package/lib/recipes-context.ts +49 -4
- package/lib/recipes-discovery.ts +164 -24
- package/lib/recipes-references.ts +163 -39
- package/lib/registry.ts +185 -94
- package/lib/runtime.ts +108 -12
- package/lib/tools-inspect.ts +57 -23
- package/lib/tools-local.ts +9 -3
- package/lib/tools-response.ts +2 -0
- package/lib/tools-spawn.ts +3 -2
- package/lib/tools.ts +4 -1
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -3
package/lib/recipes-context.ts
CHANGED
|
@@ -1,15 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recipe context prompt assembly.
|
|
3
|
-
* Zones: async runner prompt context, recipe provenance, LLM child launches
|
|
4
|
-
* Owns compact actor
|
|
2
|
+
* Recipe context contracts and prompt assembly.
|
|
3
|
+
* Zones: live session resolution, async runner prompt context, recipe provenance, LLM child launches
|
|
4
|
+
* Owns the immutable live Recipe environment and compact actor context appended to child-agent prompts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { writeFileSync } from "node:fs";
|
|
8
|
-
import { basename } from "node:path";
|
|
9
|
+
import { basename, resolve } from "node:path";
|
|
9
10
|
|
|
10
11
|
import type { CommandTemplateActorRecipeContext } from "./command-templates.ts";
|
|
12
|
+
import * as RecipesReferences from "./recipes-references.ts";
|
|
11
13
|
import type { TemplateRecipeContextRecord } from "./recipes-references.ts";
|
|
12
14
|
|
|
15
|
+
export interface RecipeResolutionContext {
|
|
16
|
+
readonly activeSkills: RecipesReferences.ActiveSkillRecipeContext;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly generation: string;
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createRecipeResolutionContext(
|
|
23
|
+
sessionId: string,
|
|
24
|
+
cwd: string,
|
|
25
|
+
activeSkills: RecipesReferences.ActiveSkillRecipeContext,
|
|
26
|
+
): RecipeResolutionContext {
|
|
27
|
+
const normalizedSessionId = sessionId.trim();
|
|
28
|
+
if (!normalizedSessionId) throw new Error("Recipe resolution session id is required.");
|
|
29
|
+
const normalizedCwd = resolve(cwd);
|
|
30
|
+
const generation = createHash("sha256")
|
|
31
|
+
.update(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
activeSkills: RecipesReferences.getActiveSkillRecipeNamespaces(activeSkills),
|
|
34
|
+
cwd: normalizedCwd,
|
|
35
|
+
sessionId: normalizedSessionId,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
.digest("hex");
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
activeSkills,
|
|
41
|
+
cwd: normalizedCwd,
|
|
42
|
+
generation,
|
|
43
|
+
sessionId: normalizedSessionId,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createEmptyRecipeResolutionContext(
|
|
48
|
+
sessionId: string,
|
|
49
|
+
cwd: string,
|
|
50
|
+
): RecipeResolutionContext {
|
|
51
|
+
return createRecipeResolutionContext(
|
|
52
|
+
sessionId,
|
|
53
|
+
cwd,
|
|
54
|
+
RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
13
58
|
export interface MarkedRecipeContextRecord extends TemplateRecipeContextRecord {
|
|
14
59
|
you_are_here?: true;
|
|
15
60
|
you_are_here_path?: string;
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import * as CommandTemplates from "./command-templates.ts";
|
|
13
13
|
import type { RegisteredTool } from "./config.ts";
|
|
14
14
|
import * as ModelContext from "./model-context.ts";
|
|
15
|
+
import type { RecipeResolutionContext } from "./recipes-context.ts";
|
|
15
16
|
import type { TemplateRecipeConfig } from "./recipes-references.ts";
|
|
16
17
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
17
18
|
import * as RecipesUsage from "./recipes-usage.ts";
|
|
@@ -29,6 +30,7 @@ export interface DiscoveredRecipe {
|
|
|
29
30
|
disabled: boolean;
|
|
30
31
|
tool: boolean;
|
|
31
32
|
mutableUsage: boolean;
|
|
33
|
+
resolutionContext?: RecipeResolutionContext;
|
|
32
34
|
diagnostics: string[];
|
|
33
35
|
riskLabels: CommandTemplates.CommandTemplateRiskLabel[];
|
|
34
36
|
shadows: string[];
|
|
@@ -58,6 +60,22 @@ export interface RecipesDiscoverySource {
|
|
|
58
60
|
file?: string;
|
|
59
61
|
defaultTool?: boolean;
|
|
60
62
|
mutableUsage?: boolean;
|
|
63
|
+
resolutionContext?: RecipeResolutionContext;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface UserRecipeAdmission {
|
|
67
|
+
args?: string[];
|
|
68
|
+
argTypes?: RegisteredTool["argTypes"];
|
|
69
|
+
artifacts?: Record<string, string>;
|
|
70
|
+
async?: boolean;
|
|
71
|
+
control?: string[];
|
|
72
|
+
defaults?: Record<string, unknown>;
|
|
73
|
+
diagnostics: string[];
|
|
74
|
+
effectiveRecipe?: TemplateRecipeConfig;
|
|
75
|
+
identity: string;
|
|
76
|
+
sourcePath: string;
|
|
77
|
+
tool?: RegisteredTool;
|
|
78
|
+
validated: boolean;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
function assertToolSafeRepeatConfig(
|
|
@@ -185,10 +203,13 @@ function readDiscoveredRecipe(
|
|
|
185
203
|
priority: number,
|
|
186
204
|
defaultTool = false,
|
|
187
205
|
mutableUsage = false,
|
|
206
|
+
resolutionContext?: RecipeResolutionContext,
|
|
188
207
|
): DiscoveredRecipe {
|
|
189
208
|
const id = RecipesReferences.getRecipeIdFromPath(file);
|
|
190
209
|
try {
|
|
191
|
-
const config = RecipesReferences.readResolvedRecipeConfig(file
|
|
210
|
+
const config = RecipesReferences.readResolvedRecipeConfig(file, [], {
|
|
211
|
+
skillContext: resolutionContext?.activeSkills,
|
|
212
|
+
});
|
|
192
213
|
const invalid = !config;
|
|
193
214
|
const disabled = config?.disabled === true;
|
|
194
215
|
return {
|
|
@@ -203,6 +224,7 @@ function readDiscoveredRecipe(
|
|
|
203
224
|
disabled,
|
|
204
225
|
tool: defaultTool && !disabled && !invalid,
|
|
205
226
|
mutableUsage,
|
|
227
|
+
resolutionContext,
|
|
206
228
|
diagnostics: getRecipeConfigDiagnostics(file, config),
|
|
207
229
|
riskLabels: getRecipeRiskLabels(config),
|
|
208
230
|
shadows: [],
|
|
@@ -219,6 +241,7 @@ function readDiscoveredRecipe(
|
|
|
219
241
|
disabled: false,
|
|
220
242
|
tool: false,
|
|
221
243
|
mutableUsage,
|
|
244
|
+
resolutionContext,
|
|
222
245
|
diagnostics: [
|
|
223
246
|
`Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
224
247
|
],
|
|
@@ -233,6 +256,7 @@ function filesForSource(source: RecipesDiscoverySource): Array<{
|
|
|
233
256
|
file: string;
|
|
234
257
|
defaultTool: boolean;
|
|
235
258
|
mutableUsage: boolean;
|
|
259
|
+
resolutionContext?: RecipeResolutionContext;
|
|
236
260
|
}> {
|
|
237
261
|
const defaultTool = source.defaultTool === true;
|
|
238
262
|
const mutableUsage = source.mutableUsage === true;
|
|
@@ -243,6 +267,7 @@ function filesForSource(source: RecipesDiscoverySource): Array<{
|
|
|
243
267
|
file: source.file,
|
|
244
268
|
defaultTool,
|
|
245
269
|
mutableUsage,
|
|
270
|
+
resolutionContext: source.resolutionContext,
|
|
246
271
|
},
|
|
247
272
|
];
|
|
248
273
|
return source.root
|
|
@@ -251,6 +276,7 @@ function filesForSource(source: RecipesDiscoverySource): Array<{
|
|
|
251
276
|
file,
|
|
252
277
|
defaultTool,
|
|
253
278
|
mutableUsage,
|
|
279
|
+
resolutionContext: source.resolutionContext,
|
|
254
280
|
}))
|
|
255
281
|
: [];
|
|
256
282
|
}
|
|
@@ -338,8 +364,16 @@ export function discoverRecipeSources(
|
|
|
338
364
|
sources: RecipesDiscoverySource[],
|
|
339
365
|
): RecipesDiscoveryResult {
|
|
340
366
|
const entries = sources.flatMap((source, priority) =>
|
|
341
|
-
filesForSource(source).map(
|
|
342
|
-
|
|
367
|
+
filesForSource(source).map(
|
|
368
|
+
({ root, file, defaultTool, mutableUsage, resolutionContext }) =>
|
|
369
|
+
readDiscoveredRecipe(
|
|
370
|
+
root,
|
|
371
|
+
file,
|
|
372
|
+
priority,
|
|
373
|
+
defaultTool,
|
|
374
|
+
mutableUsage,
|
|
375
|
+
resolutionContext,
|
|
376
|
+
),
|
|
343
377
|
),
|
|
344
378
|
);
|
|
345
379
|
const byId = new Map<string, DiscoveredRecipe[]>();
|
|
@@ -836,14 +870,13 @@ export function summarizeDiscovery(
|
|
|
836
870
|
};
|
|
837
871
|
}
|
|
838
872
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
const description = cfg.description ?? `Execute template recipe: ${entry.id}`;
|
|
873
|
+
function projectRegisteredTool(
|
|
874
|
+
identity: string,
|
|
875
|
+
path: string,
|
|
876
|
+
cfg: TemplateRecipeConfig,
|
|
877
|
+
mutableUsage: boolean,
|
|
878
|
+
): RegisteredTool {
|
|
879
|
+
const description = cfg.description ?? `Execute template recipe: ${identity}`;
|
|
847
880
|
const argTemplate = cfg.template;
|
|
848
881
|
const argTemplateConfig =
|
|
849
882
|
typeof argTemplate === "object" && !Array.isArray(argTemplate)
|
|
@@ -867,27 +900,38 @@ export function toRegisteredTool(
|
|
|
867
900
|
explicitArgTypes,
|
|
868
901
|
{ ...(cfg.defaults ?? {}), ...(cfg.values ?? {}) },
|
|
869
902
|
);
|
|
870
|
-
const
|
|
871
|
-
Schema.assertCompatibleToolArgTypes(explicitArgTypes,
|
|
903
|
+
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
904
|
+
Schema.assertCompatibleToolArgTypes(explicitArgTypes, inferredArgTypes);
|
|
905
|
+
const effectiveArgTypes = { ...inferredArgTypes, ...explicitArgTypes };
|
|
906
|
+
const args = Schema.getToolArgNames(argTemplateConfig).filter(
|
|
907
|
+
(arg) => !RecipesReferences.isRuntimeOwnedRecipeInput(arg),
|
|
908
|
+
);
|
|
909
|
+
const callerArgSet = new Set(args);
|
|
910
|
+
const argTypes = Object.fromEntries(
|
|
911
|
+
Object.entries(effectiveArgTypes).filter(([arg]) => callerArgSet.has(arg)),
|
|
912
|
+
);
|
|
913
|
+
const defaults = Object.fromEntries(
|
|
914
|
+
Object.entries(cfg.defaults ?? {}).filter(([arg]) => callerArgSet.has(arg)),
|
|
915
|
+
);
|
|
916
|
+
const storedArgs = cfg.args?.filter((arg) =>
|
|
917
|
+
callerArgSet.has(Schema.parseToolArgToken(String(arg)).arg),
|
|
918
|
+
);
|
|
872
919
|
return {
|
|
873
|
-
name:
|
|
920
|
+
name: identity,
|
|
874
921
|
description,
|
|
875
|
-
template,
|
|
922
|
+
template: path,
|
|
876
923
|
recipe: cfg,
|
|
877
|
-
args
|
|
924
|
+
args,
|
|
878
925
|
defaults: Object.fromEntries(
|
|
879
|
-
Object.entries(
|
|
880
|
-
key,
|
|
881
|
-
String(value),
|
|
882
|
-
]),
|
|
926
|
+
Object.entries(defaults).map(([key, value]) => [key, String(value)]),
|
|
883
927
|
),
|
|
884
928
|
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
885
|
-
...(
|
|
886
|
-
...(
|
|
887
|
-
...(
|
|
929
|
+
...(mutableUsage ? { sourcePath: path } : {}),
|
|
930
|
+
...(storedArgs ? { storedArgs } : {}),
|
|
931
|
+
...(Object.keys(defaults).length > 0
|
|
888
932
|
? {
|
|
889
933
|
storedDefaults: Object.fromEntries(
|
|
890
|
-
Object.entries(
|
|
934
|
+
Object.entries(defaults).map(([key, value]) => [
|
|
891
935
|
key,
|
|
892
936
|
String(value),
|
|
893
937
|
]),
|
|
@@ -896,3 +940,99 @@ export function toRegisteredTool(
|
|
|
896
940
|
: {}),
|
|
897
941
|
};
|
|
898
942
|
}
|
|
943
|
+
|
|
944
|
+
function boundedAdmissionDiagnostic(error: unknown): string {
|
|
945
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
946
|
+
return message.length <= 4096 ? message : `${message.slice(0, 4095)}…`;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function admitUserRecipe(
|
|
950
|
+
sourcePath: string,
|
|
951
|
+
resolutionContext: RecipeResolutionContext,
|
|
952
|
+
authoredRecipe?: Record<string, unknown>,
|
|
953
|
+
mutableUsage = true,
|
|
954
|
+
): UserRecipeAdmission {
|
|
955
|
+
const identity = RecipesReferences.getRecipeIdFromPath(sourcePath);
|
|
956
|
+
try {
|
|
957
|
+
const effectiveRecipe = RecipesReferences.readResolvedRecipeConfig(
|
|
958
|
+
sourcePath,
|
|
959
|
+
[],
|
|
960
|
+
{
|
|
961
|
+
...(authoredRecipe
|
|
962
|
+
? { authoredEntry: { path: sourcePath, recipe: authoredRecipe } }
|
|
963
|
+
: {}),
|
|
964
|
+
skillContext: resolutionContext.activeSkills,
|
|
965
|
+
},
|
|
966
|
+
);
|
|
967
|
+
if (!effectiveRecipe) {
|
|
968
|
+
return {
|
|
969
|
+
diagnostics: [
|
|
970
|
+
authoredRecipe
|
|
971
|
+
? "Recipe must define a valid template."
|
|
972
|
+
: RecipesReferences.diagnoseRawRecipeConfigFailure(sourcePath) ??
|
|
973
|
+
"Recipe must define a valid template.",
|
|
974
|
+
],
|
|
975
|
+
identity,
|
|
976
|
+
sourcePath,
|
|
977
|
+
validated: false,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
if (effectiveRecipe.disabled === true) {
|
|
981
|
+
return {
|
|
982
|
+
diagnostics: ["Recipe is disabled."],
|
|
983
|
+
effectiveRecipe,
|
|
984
|
+
identity,
|
|
985
|
+
sourcePath,
|
|
986
|
+
validated: false,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const tool = projectRegisteredTool(
|
|
990
|
+
identity,
|
|
991
|
+
sourcePath,
|
|
992
|
+
effectiveRecipe,
|
|
993
|
+
mutableUsage,
|
|
994
|
+
);
|
|
995
|
+
return {
|
|
996
|
+
args: tool.args,
|
|
997
|
+
argTypes: tool.argTypes,
|
|
998
|
+
artifacts: effectiveRecipe.artifacts,
|
|
999
|
+
async: effectiveRecipe.async === true,
|
|
1000
|
+
control: effectiveRecipe.control,
|
|
1001
|
+
defaults: effectiveRecipe.defaults,
|
|
1002
|
+
diagnostics: [],
|
|
1003
|
+
effectiveRecipe,
|
|
1004
|
+
identity,
|
|
1005
|
+
sourcePath,
|
|
1006
|
+
tool,
|
|
1007
|
+
validated: true,
|
|
1008
|
+
};
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return {
|
|
1011
|
+
diagnostics: [boundedAdmissionDiagnostic(error)],
|
|
1012
|
+
identity,
|
|
1013
|
+
sourcePath,
|
|
1014
|
+
validated: false,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export function toRegisteredTool(
|
|
1020
|
+
entry: DiscoveredRecipe,
|
|
1021
|
+
): RegisteredTool | undefined {
|
|
1022
|
+
if (!entry.tool || entry.invalid || entry.disabled || !entry.config)
|
|
1023
|
+
return undefined;
|
|
1024
|
+
if (entry.resolutionContext) {
|
|
1025
|
+
return admitUserRecipe(
|
|
1026
|
+
entry.path,
|
|
1027
|
+
entry.resolutionContext,
|
|
1028
|
+
undefined,
|
|
1029
|
+
entry.mutableUsage,
|
|
1030
|
+
).tool;
|
|
1031
|
+
}
|
|
1032
|
+
return projectRegisteredTool(
|
|
1033
|
+
entry.id,
|
|
1034
|
+
entry.path,
|
|
1035
|
+
entry.config,
|
|
1036
|
+
entry.mutableUsage,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
@@ -87,11 +87,34 @@ export interface TemplateRecipeContextRecord {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export interface ReadResolvedRecipeConfigOptions {
|
|
90
|
+
authoredEntry?: { path: string; recipe: Record<string, unknown> };
|
|
90
91
|
includeActorRecipeContext?: boolean;
|
|
91
92
|
skillContext?: ActiveSkillRecipeContext;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
const
|
|
95
|
+
const RESERVED_AUTHORED_RECIPE_VALUE_NAMES = new Set([
|
|
96
|
+
"recipe_dir",
|
|
97
|
+
"skill_dir",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
export const RUNTIME_OWNED_RECIPE_INPUT_NAMES = Object.freeze([
|
|
101
|
+
"owner_id",
|
|
102
|
+
"recipe_dir",
|
|
103
|
+
"run_instance_id",
|
|
104
|
+
"runtime_state_root",
|
|
105
|
+
"session_id",
|
|
106
|
+
"skill_dir",
|
|
107
|
+
"state_dir",
|
|
108
|
+
"trace_file",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const RUNTIME_OWNED_RECIPE_INPUT_SET = new Set(
|
|
112
|
+
RUNTIME_OWNED_RECIPE_INPUT_NAMES,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
export function isRuntimeOwnedRecipeInput(name: string): boolean {
|
|
116
|
+
return RUNTIME_OWNED_RECIPE_INPUT_SET.has(name);
|
|
117
|
+
}
|
|
95
118
|
|
|
96
119
|
export interface ActiveSkillRecipeSource {
|
|
97
120
|
name: string;
|
|
@@ -149,6 +172,19 @@ export interface ActiveSkillRecipeComponent {
|
|
|
149
172
|
stem: string;
|
|
150
173
|
}
|
|
151
174
|
|
|
175
|
+
export interface SkillRecipeComponentDiagnostic {
|
|
176
|
+
file?: string;
|
|
177
|
+
reason: string;
|
|
178
|
+
skill: string;
|
|
179
|
+
stem?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface SkillRecipeComponentInventory {
|
|
183
|
+
components: ActiveSkillRecipeComponent[];
|
|
184
|
+
partial: boolean;
|
|
185
|
+
rejected: SkillRecipeComponentDiagnostic[];
|
|
186
|
+
}
|
|
187
|
+
|
|
152
188
|
function nestedRecipeFiles(root: string): string[] {
|
|
153
189
|
if (!existsSync(root)) return [];
|
|
154
190
|
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
@@ -158,20 +194,32 @@ function nestedRecipeFiles(root: string): string[] {
|
|
|
158
194
|
});
|
|
159
195
|
}
|
|
160
196
|
|
|
161
|
-
export function
|
|
197
|
+
export function inventoryActiveSkillRecipeComponents(
|
|
162
198
|
context: ActiveSkillRecipeContext,
|
|
163
|
-
):
|
|
199
|
+
): SkillRecipeComponentInventory {
|
|
164
200
|
const components: ActiveSkillRecipeComponent[] = [];
|
|
201
|
+
const rejected: SkillRecipeComponentDiagnostic[] = [];
|
|
165
202
|
for (const [skill, roots] of Object.entries(context.namespaces)) {
|
|
166
|
-
if (roots.length > 1)
|
|
167
|
-
|
|
203
|
+
if (roots.length > 1) {
|
|
204
|
+
rejected.push({
|
|
205
|
+
reason: `Duplicate active Skill identity ${skill}: ${roots.join(", ")}`,
|
|
206
|
+
skill,
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
168
210
|
const root = roots[0];
|
|
169
211
|
if (!root || !existsSync(root)) continue;
|
|
170
212
|
const direct = readdirSync(root, { withFileTypes: true });
|
|
171
213
|
for (const entry of direct.filter((candidate) => candidate.isDirectory())) {
|
|
172
214
|
const nested = nestedRecipeFiles(join(root, entry.name));
|
|
173
|
-
|
|
174
|
-
|
|
215
|
+
for (const file of nested) {
|
|
216
|
+
rejected.push({
|
|
217
|
+
file,
|
|
218
|
+
reason: `Nested Skill Recipe file is not addressable: ${file}`,
|
|
219
|
+
skill,
|
|
220
|
+
stem: getRecipeIdFromPath(file),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
175
223
|
}
|
|
176
224
|
const files = direct
|
|
177
225
|
.filter(
|
|
@@ -186,30 +234,62 @@ export function listActiveSkillRecipeComponents(
|
|
|
186
234
|
byStem.set(stem, matches);
|
|
187
235
|
}
|
|
188
236
|
for (const [stem, matches] of [...byStem].sort(([a], [b]) => a.localeCompare(b))) {
|
|
189
|
-
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem))
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
237
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem)) {
|
|
238
|
+
rejected.push({
|
|
239
|
+
file: matches[0],
|
|
240
|
+
reason: `Invalid Skill Recipe filename stem: ${stem}`,
|
|
241
|
+
skill,
|
|
242
|
+
stem,
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (matches.length > 1) {
|
|
247
|
+
rejected.push({
|
|
248
|
+
file: matches[0],
|
|
249
|
+
reason: `Skill Recipe stem collision: ${skill}/${stem} has both .json and .md files`,
|
|
250
|
+
skill,
|
|
251
|
+
stem,
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
193
255
|
const file = matches[0];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Object.
|
|
203
|
-
alias,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
256
|
+
try {
|
|
257
|
+
const raw = readRawRecipeConfig(file);
|
|
258
|
+
if (!raw || !Object.hasOwn(raw, "template"))
|
|
259
|
+
throw new Error(`Invalid Skill Recipe component: ${skill}/${stem}`);
|
|
260
|
+
assertRecipeHasNoDeclaredName(raw);
|
|
261
|
+
components.push({
|
|
262
|
+
file,
|
|
263
|
+
identity: `${skill}/${stem}`,
|
|
264
|
+
imports: Object.fromEntries(
|
|
265
|
+
Object.entries(getRecipeImports(raw)).map(([alias, value]) => [
|
|
266
|
+
alias,
|
|
267
|
+
getImportFrom(value),
|
|
268
|
+
]),
|
|
269
|
+
),
|
|
270
|
+
skill,
|
|
271
|
+
stem,
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
rejected.push({
|
|
275
|
+
file,
|
|
276
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
277
|
+
skill,
|
|
278
|
+
stem,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
210
281
|
}
|
|
211
282
|
}
|
|
212
|
-
return components;
|
|
283
|
+
return { components, partial: rejected.length > 0, rejected };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function listActiveSkillRecipeComponents(
|
|
287
|
+
context: ActiveSkillRecipeContext,
|
|
288
|
+
): ActiveSkillRecipeComponent[] {
|
|
289
|
+
const inventory = inventoryActiveSkillRecipeComponents(context);
|
|
290
|
+
if (inventory.rejected.length > 0)
|
|
291
|
+
throw new Error(inventory.rejected[0].reason);
|
|
292
|
+
return inventory.components;
|
|
213
293
|
}
|
|
214
294
|
|
|
215
295
|
const SKILL_RECIPE_REFERENCE =
|
|
@@ -311,7 +391,7 @@ function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): voi
|
|
|
311
391
|
const args = Array.isArray(raw.args)
|
|
312
392
|
? Schema.getExplicitToolArgNames(raw.args.map(String))
|
|
313
393
|
: [];
|
|
314
|
-
for (const name of
|
|
394
|
+
for (const name of RESERVED_AUTHORED_RECIPE_VALUE_NAMES) {
|
|
315
395
|
if (
|
|
316
396
|
args.includes(name) ||
|
|
317
397
|
(isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
|
|
@@ -1068,6 +1148,46 @@ function getDirectDelegatedRecipe(
|
|
|
1068
1148
|
return undefined;
|
|
1069
1149
|
}
|
|
1070
1150
|
|
|
1151
|
+
function resolveDirectDelegationArgs(
|
|
1152
|
+
delegated: TemplateRecipeConfig,
|
|
1153
|
+
wrapperArgs: unknown,
|
|
1154
|
+
wrapperDefaults: Record<string, unknown> | undefined,
|
|
1155
|
+
): string[] | undefined {
|
|
1156
|
+
if (!Array.isArray(delegated.args))
|
|
1157
|
+
return Array.isArray(wrapperArgs) ? wrapperArgs.map(String) : undefined;
|
|
1158
|
+
const delegatedSpec = Schema.parseToolArgDeclarationList(delegated.args);
|
|
1159
|
+
if (delegatedSpec.error) throw new Error(delegatedSpec.error);
|
|
1160
|
+
const wrapperDeclarations = Array.isArray(wrapperArgs)
|
|
1161
|
+
? wrapperArgs.map(String)
|
|
1162
|
+
: [];
|
|
1163
|
+
const wrapperSpec = Schema.parseToolArgDeclarationList(wrapperDeclarations);
|
|
1164
|
+
if (wrapperSpec.error) throw new Error(wrapperSpec.error);
|
|
1165
|
+
const declared = new Set(delegatedSpec.args);
|
|
1166
|
+
for (const arg of wrapperSpec.args) {
|
|
1167
|
+
if (!declared.has(arg))
|
|
1168
|
+
throw new Error(`Unknown delegated Recipe argument: ${arg}`);
|
|
1169
|
+
}
|
|
1170
|
+
Schema.assertCompatibleToolArgTypes(
|
|
1171
|
+
delegatedSpec.argTypes,
|
|
1172
|
+
wrapperSpec.argTypes,
|
|
1173
|
+
);
|
|
1174
|
+
assertRecipeDefaultsDeclared(
|
|
1175
|
+
delegated.args,
|
|
1176
|
+
wrapperDefaults,
|
|
1177
|
+
"delegated Recipe default",
|
|
1178
|
+
);
|
|
1179
|
+
Schema.normalizeRuntimeValues(
|
|
1180
|
+
Object.fromEntries(
|
|
1181
|
+
Object.entries(wrapperDefaults ?? {}).filter(
|
|
1182
|
+
([, value]) =>
|
|
1183
|
+
typeof value !== "string" || !/\{[^{}]+\}/.test(value),
|
|
1184
|
+
),
|
|
1185
|
+
),
|
|
1186
|
+
delegatedSpec.argTypes,
|
|
1187
|
+
);
|
|
1188
|
+
return delegated.args;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1071
1191
|
function expandImportNodes(
|
|
1072
1192
|
value: CommandTemplateValue,
|
|
1073
1193
|
imports: Record<string, ImportedRecipe>,
|
|
@@ -1157,7 +1277,10 @@ export function readResolvedRecipeConfig(
|
|
|
1157
1277
|
`Recipe import depth exceeds limit ${MAX_RECIPE_IMPORT_DEPTH}: ${[...stack, path].join(" -> ")}`,
|
|
1158
1278
|
);
|
|
1159
1279
|
}
|
|
1160
|
-
const raw =
|
|
1280
|
+
const raw =
|
|
1281
|
+
options.authoredEntry?.path === path
|
|
1282
|
+
? options.authoredEntry.recipe
|
|
1283
|
+
: readRawRecipeConfig(path);
|
|
1161
1284
|
if (!raw || !Object.hasOwn(raw, "template")) return undefined;
|
|
1162
1285
|
assertRecipeHasNoDeclaredName(raw);
|
|
1163
1286
|
RecipeControl.assertRecipeHasNoMailbox(raw);
|
|
@@ -1244,10 +1367,15 @@ export function readResolvedRecipeConfig(
|
|
|
1244
1367
|
role: stack.length > 0 ? "import" : "entry",
|
|
1245
1368
|
})
|
|
1246
1369
|
: expandedTemplate;
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
);
|
|
1370
|
+
const wrapperDefaults = isRecord(substituted.defaults)
|
|
1371
|
+
? substituted.defaults
|
|
1372
|
+
: undefined;
|
|
1373
|
+
const mergedDefaults = mergeDefaults(delegated?.defaults, wrapperDefaults);
|
|
1374
|
+
const effectiveArgs = delegated
|
|
1375
|
+
? resolveDirectDelegationArgs(delegated, substituted.args, wrapperDefaults)
|
|
1376
|
+
: Array.isArray(substituted.args)
|
|
1377
|
+
? (substituted.args as string[])
|
|
1378
|
+
: undefined;
|
|
1251
1379
|
const artifactSource = isRecord(substituted.artifacts)
|
|
1252
1380
|
? substituted.artifacts
|
|
1253
1381
|
: delegated?.artifacts;
|
|
@@ -1284,11 +1412,7 @@ export function readResolvedRecipeConfig(
|
|
|
1284
1412
|
? { imports: getRecipeImports(raw) }
|
|
1285
1413
|
: {}),
|
|
1286
1414
|
template: templateWithContext,
|
|
1287
|
-
...(
|
|
1288
|
-
? { args: substituted.args as string[] }
|
|
1289
|
-
: Array.isArray(delegated?.args)
|
|
1290
|
-
? { args: delegated.args }
|
|
1291
|
-
: {}),
|
|
1415
|
+
...(effectiveArgs ? { args: effectiveArgs } : {}),
|
|
1292
1416
|
...(mergedDefaults ? { defaults: mergedDefaults } : {}),
|
|
1293
1417
|
...(typeof substituted.parallel === "boolean"
|
|
1294
1418
|
? { parallel: substituted.parallel }
|