@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.
@@ -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
- return applyDefaultsToTemplate(
720
- delegated.template,
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
- overrides,
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 = mergeDefaults(
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: { ...(bindingValues ?? {}) },
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
- ...(isRecord(substituted.values) ? { values: substituted.values } : {}),
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
- const source = splitArgDeclarations(value);
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[] {
@@ -14,6 +14,7 @@ import * as Limits from "./limits.ts";
14
14
  import { computeRunControlCapacity } from "./run-evidence-policy.ts";
15
15
  import * as Paths from "./paths.ts";
16
16
  import * as RecipesDiscovery from "./recipes-discovery.ts";
17
+ import * as RecipesReferences from "./recipes-references.ts";
17
18
  import * as ReviewDiagnostics from "./review-diagnostics.ts";
18
19
  import * as RunsControls from "./runs-controls.ts";
19
20
  import * as RunControlDelivery from "./runs-control-delivery.ts";
@@ -214,8 +215,18 @@ function inspectRecipes(
214
215
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
215
216
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
216
217
  ]);
218
+ const skillRecipeNamespaces =
219
+ RecipesReferences.getActiveSkillRecipeNamespaces();
217
220
  const summary = {
218
221
  ...RecipesDiscovery.summarizeDiscovery(discovered),
222
+ skill_recipe_namespaces: skillRecipeNamespaces,
223
+ skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
224
+ .filter(([, roots]) => roots.length > 1)
225
+ .map(([name, roots]) => ({
226
+ name,
227
+ roots,
228
+ error: `Ambiguous active Skill Recipe namespace ${name}`,
229
+ })),
219
230
  drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
220
231
  };
221
232
  return {
@@ -67,11 +67,16 @@ function formatRuntimeToolUsageHint(
67
67
  includeRunId: boolean,
68
68
  ): string {
69
69
  const optional = cfg.args.filter((arg) => !required.includes(arg));
70
+ const exampleDefaults = {
71
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
72
+ ...cfg.defaults,
73
+ ...(cfg.recipe?.values ?? {}),
74
+ };
70
75
  const example: Record<string, unknown> = {};
71
76
  for (const arg of required)
72
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
77
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
73
78
  for (const arg of optional)
74
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
79
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
75
80
  if (includeRunId) example.run_id = `${cfg.name}-1`;
76
81
  const lines = [
77
82
  `Expected call shape for ${cfg.name}:`,
@@ -85,6 +90,32 @@ function formatRuntimeToolUsageHint(
85
90
  return lines.join("\n");
86
91
  }
87
92
 
93
+ function resolveRecipeToolValues(
94
+ cfg: RegisteredTool,
95
+ callerValues: Record<string, unknown>,
96
+ ctx: RuntimeToolContext,
97
+ ): Record<string, unknown> {
98
+ const recipeRuntimeValues = {
99
+ ...(cfg.recipe?.recipe_dir
100
+ ? { recipe_dir: cfg.recipe.recipe_dir }
101
+ : {}),
102
+ ...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
103
+ };
104
+ return Schema.normalizeRuntimeValues(
105
+ ModelContext.withCurrentModelValues(
106
+ {
107
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
108
+ ...cfg.defaults,
109
+ ...(cfg.recipe?.values ?? {}),
110
+ ...callerValues,
111
+ ...recipeRuntimeValues,
112
+ },
113
+ ctx,
114
+ ),
115
+ cfg.argTypes,
116
+ );
117
+ }
118
+
88
119
  function formatRuntimeToolArgumentError(
89
120
  cfg: RegisteredTool,
90
121
  error: unknown,
@@ -128,9 +159,18 @@ export function createRuntimeToolDefinition(
128
159
  defaults: cfg.defaults,
129
160
  template: requiredTemplate,
130
161
  };
162
+ const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(
163
+ cfg.recipe?.args,
164
+ );
131
165
  const requiredArgs =
132
166
  isRecipe && cfg.storedArgs !== undefined
133
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
167
+ ? new Set(
168
+ cfg.args.filter(
169
+ (arg) =>
170
+ !Object.hasOwn(cfg.defaults, arg) &&
171
+ !Object.hasOwn(recipeInlineDefaults, arg),
172
+ ),
173
+ )
134
174
  : RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
135
175
  ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
136
176
  : Schema.getRequiredToolArgNames(requiredTemplateConfig);
@@ -200,13 +240,7 @@ export function createRuntimeToolDefinition(
200
240
  { ...(cfg.recipe?.values ?? {}), ...values },
201
241
  ctx,
202
242
  ),
203
- values: Schema.normalizeRuntimeValues(
204
- ModelContext.withCurrentModelValues(
205
- { ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values },
206
- ctx,
207
- ),
208
- cfg.argTypes,
209
- ),
243
+ values: resolveRecipeToolValues(cfg, values, ctx),
210
244
  },
211
245
  ctx.cwd,
212
246
  );
@@ -221,17 +255,13 @@ export function createRuntimeToolDefinition(
221
255
  };
222
256
  }
223
257
  if (isRecipe && recipeTemplate) {
224
- const paramsWithDefaults = ModelContext.withCurrentModelValues(
225
- {
226
- ...(cfg.recipe?.values ?? {}),
227
- ...cfg.defaults,
228
- ...(params as Record<string, unknown>),
229
- },
230
- ctx,
231
- );
232
258
  return await Execution.executeRegisteredTool(
233
259
  { ...cfg, template: recipeTemplate },
234
- Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes),
260
+ resolveRecipeToolValues(
261
+ cfg,
262
+ params as Record<string, unknown>,
263
+ ctx,
264
+ ),
235
265
  exec,
236
266
  ctx.cwd,
237
267
  signal,
@@ -138,10 +138,10 @@ export function createSpawnToolDefinition<
138
138
  "Optional Run identity in exact run:<id> form.",
139
139
  ),
140
140
  file: Schema.stringSchema(
141
- "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
141
+ "Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>.",
142
142
  ),
143
143
  recipe: Schema.stringSchema(
144
- "Alias for file; template recipe JSON file/name to spawn.",
144
+ "Alias for file; qualified Skill/std or ordinary Recipe reference to spawn.",
145
145
  ),
146
146
  template: Schema.unionSchema([
147
147
  Schema.stringSchema("Inline command template string"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -198,7 +198,7 @@ try {
198
198
  );
199
199
  console.log("[release] zero-warning Recipe QA gate checked");
200
200
 
201
- const maximumShippedLines = 29_598;
201
+ const maximumShippedLines = 29_767;
202
202
  const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
203
203
  const shippedLines = files
204
204
  .filter((path) => shippedPath.test(path))
@@ -25,11 +25,11 @@ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `contro
25
25
 
26
26
  ## Recipe
27
27
 
28
- A Recipe defines execution. It may declare args, defaults, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
28
+ A Recipe defines execution. It may declare named typed args, inline fallbacks, configuration `defaults`, composition `values`, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
29
29
 
30
30
  Do not declare Control for ordinary one-shot work. Runtime lifecycle actions such as `kill` stay runtime-owned and must not appear in Recipe Control declarations. Imported Recipes act as local definitions inside one Run; they do not create nested Runs unless execution explicitly spawns them.
31
31
 
32
- Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
32
+ Prefer maintained packaged Recipes over ad hoc wrappers. Use `std:<recipe>` for exact packaged lookup and `skill:<skill>/<recipe-path>` for a component bundled with a Pi-active Skill. File-backed Recipes own `{recipe_dir}` and Skill Recipes own `{skill_dir}`; callers never pass or override these origins. Skill Recipes are components, not automatic tools. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
33
33
 
34
34
  ## Trace
35
35