@llblab/pi-actors 0.46.0 → 0.47.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/AGENTS.md +7 -5
- package/CHANGELOG.md +20 -0
- package/README.md +2 -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 +6 -5
- package/dist/lib/prompts.js +22 -23
- package/dist/lib/recipes-context.d.ts +12 -3
- package/dist/lib/recipes-context.js +28 -4
- package/dist/lib/recipes-discovery.d.ts +22 -0
- package/dist/lib/recipes-discovery.js +108 -23
- package/dist/lib/recipes-references.d.ts +18 -0
- package/dist/lib/recipes-references.js +129 -38
- package/dist/lib/registry.d.ts +23 -6
- package/dist/lib/registry.js +294 -101
- package/dist/lib/runtime.d.ts +30 -3
- package/dist/lib/runtime.js +108 -9
- package/dist/lib/tools-inspect.d.ts +3 -0
- package/dist/lib/tools-inspect.js +190 -26
- package/dist/lib/tools-local.d.ts +2 -2
- package/dist/lib/tools-local.js +5 -2
- package/dist/lib/tools-register.js +2 -1
- package/dist/lib/tools-response.js +7 -1
- 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/scripts/conformance.mjs +1 -0
- package/dist/skills/actors/SKILL.md +76 -65
- package/dist/skills/actors/references/diagnostics.md +44 -0
- package/dist/skills/actors/references/persistent-tools.md +74 -0
- package/dist/skills/actors/references/recipes.md +51 -0
- package/dist/skills/actors/references/runs.md +39 -0
- package/dist/skills/artifacts/SKILL.md +24 -7
- package/dist/skills/media/SKILL.md +35 -7
- package/dist/skills/project-work/SKILL.md +28 -7
- package/dist/skills/recipe-memory/SKILL.md +27 -7
- package/dist/skills/swarm/SKILL.md +41 -445
- package/dist/skills/swarm/references/development-swarm.md +87 -539
- package/dist/skills/swarm/references/review-swarms.md +115 -0
- package/docs/README.md +5 -5
- package/docs/recipe-library.md +15 -10
- package/docs/template-recipes.md +3 -1
- package/docs/tool-registry.md +18 -6
- 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 +24 -24
- package/lib/recipes-context.ts +49 -4
- package/lib/recipes-discovery.ts +186 -25
- package/lib/recipes-references.ts +176 -44
- package/lib/registry.ts +441 -113
- package/lib/runtime.ts +147 -12
- package/lib/tools-inspect.ts +254 -28
- package/lib/tools-local.ts +9 -3
- package/lib/tools-register.ts +4 -3
- package/lib/tools-response.ts +7 -1
- package/lib/tools-spawn.ts +3 -2
- package/lib/tools.ts +4 -1
- package/package.json +1 -1
- package/scripts/conformance.mjs +1 -0
- package/skills/actors/SKILL.md +76 -65
- package/skills/actors/references/diagnostics.md +44 -0
- package/skills/actors/references/persistent-tools.md +74 -0
- package/skills/actors/references/recipes.md +51 -0
- package/skills/actors/references/runs.md +39 -0
- package/skills/artifacts/SKILL.md +24 -7
- package/skills/media/SKILL.md +35 -7
- package/skills/project-work/SKILL.md +28 -7
- package/skills/recipe-memory/SKILL.md +27 -7
- package/skills/swarm/SKILL.md +41 -445
- package/skills/swarm/references/development-swarm.md +87 -539
- package/skills/swarm/references/review-swarms.md +115 -0
package/lib/registry.ts
CHANGED
|
@@ -10,9 +10,15 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
10
10
|
import * as CommandTemplates from "./command-templates.ts";
|
|
11
11
|
import * as Config from "./config.ts";
|
|
12
12
|
import * as ExecutionOutput from "./execution-output.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
withFileMutationLock,
|
|
15
|
+
writeJsonAtomic,
|
|
16
|
+
writeTextAtomic,
|
|
17
|
+
} from "./file-state.ts";
|
|
14
18
|
import * as Identity from "./identity.ts";
|
|
15
19
|
import * as Paths from "./paths.ts";
|
|
20
|
+
import * as RecipesContext from "./recipes-context.ts";
|
|
21
|
+
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
16
22
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
17
23
|
import * as Schema from "./schema.ts";
|
|
18
24
|
|
|
@@ -21,23 +27,33 @@ export interface RegisterToolInput {
|
|
|
21
27
|
description?: string;
|
|
22
28
|
async?: boolean;
|
|
23
29
|
template?: CommandTemplates.CommandTemplateValue | null;
|
|
30
|
+
from?: string;
|
|
31
|
+
defaults?: Record<string, unknown>;
|
|
24
32
|
draft?: string;
|
|
25
33
|
args?: string;
|
|
26
34
|
update?: boolean;
|
|
27
|
-
values?: Record<string, unknown>;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export interface RegisterToolResultDetails {
|
|
38
|
+
active_tool?: boolean;
|
|
39
|
+
activation?: "current_session" | "unverified";
|
|
40
|
+
activation_boundary?: string;
|
|
31
41
|
args?: string[];
|
|
32
42
|
async?: boolean;
|
|
33
|
-
|
|
43
|
+
callable_now?: boolean;
|
|
34
44
|
defaults?: Record<string, string>;
|
|
35
|
-
|
|
45
|
+
host_registered?: boolean;
|
|
46
|
+
next_actions?: string[];
|
|
47
|
+
optional_args?: string[];
|
|
48
|
+
persisted?: boolean;
|
|
36
49
|
promoted?: boolean;
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
registry_active?: boolean;
|
|
51
|
+
required_args?: string[];
|
|
52
|
+
resolved?: boolean;
|
|
53
|
+
source?: string;
|
|
39
54
|
templateWarnings?: string[];
|
|
40
55
|
tool: string;
|
|
56
|
+
validated?: boolean;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export interface RegisterToolResult {
|
|
@@ -45,6 +61,13 @@ export interface RegisterToolResult {
|
|
|
45
61
|
details: RegisterToolResultDetails;
|
|
46
62
|
}
|
|
47
63
|
|
|
64
|
+
interface RuntimeActivation {
|
|
65
|
+
active_tool: boolean;
|
|
66
|
+
activation: "current_session" | "unverified";
|
|
67
|
+
callable_now: boolean;
|
|
68
|
+
host_registered: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
export interface RegisterToolRuntimeDeps<TContext> {
|
|
49
72
|
configPath: string;
|
|
50
73
|
recipeRoot?: string;
|
|
@@ -56,7 +79,7 @@ export interface RegisterToolRuntimeDeps<TContext> {
|
|
|
56
79
|
message: string,
|
|
57
80
|
type: "info" | "warning" | "error",
|
|
58
81
|
) => void;
|
|
59
|
-
registerRuntimeTool: (cfg: Config.RegisteredTool) => void;
|
|
82
|
+
registerRuntimeTool: (cfg: Config.RegisteredTool) => RuntimeActivation | void;
|
|
60
83
|
reservedToolNames: Set<string>;
|
|
61
84
|
setActiveTools: (toolNames: string[]) => void;
|
|
62
85
|
}
|
|
@@ -96,6 +119,54 @@ function getToolRecipePath<TContext>(
|
|
|
96
119
|
return join(getRecipeRoot(deps), `${name}.json`);
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
function activationBoundary(
|
|
123
|
+
activation: RuntimeActivation | undefined,
|
|
124
|
+
): string {
|
|
125
|
+
if (activation?.callable_now) return "current_session";
|
|
126
|
+
if (!activation) return "session_activation_unverified";
|
|
127
|
+
if (!activation.host_registered) return "host_registration";
|
|
128
|
+
if (!activation.active_tool) return "active_tool_set";
|
|
129
|
+
return "callability_verification";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function recipeDoctorAction(source: string): string {
|
|
133
|
+
return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(source)
|
|
134
|
+
? `inspect target=recipes view=doctor identity=${source}`
|
|
135
|
+
: "inspect target=recipes view=doctor";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function registrationNextActions(
|
|
139
|
+
tool: string,
|
|
140
|
+
source: string,
|
|
141
|
+
callableNow: boolean,
|
|
142
|
+
): string[] {
|
|
143
|
+
if (callableNow) {
|
|
144
|
+
return [
|
|
145
|
+
`call tool ${tool}`,
|
|
146
|
+
`inspect target=tool:${tool} view=status`,
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
return [
|
|
150
|
+
`inspect target=tool:${tool} view=status`,
|
|
151
|
+
...(source === "template" || source === "draft"
|
|
152
|
+
? []
|
|
153
|
+
: [recipeDoctorAction(source)]),
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function activeSkillSummary(
|
|
158
|
+
resolutionContext: RecipesContext.RecipeResolutionContext,
|
|
159
|
+
): string {
|
|
160
|
+
const names = Object.keys(
|
|
161
|
+
RecipesReferences.getActiveSkillRecipeNamespaces(
|
|
162
|
+
resolutionContext.activeSkills,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
.sort()
|
|
166
|
+
.slice(0, 20);
|
|
167
|
+
return names.length > 0 ? names.join(", ") : "<none>";
|
|
168
|
+
}
|
|
169
|
+
|
|
99
170
|
function assertDraftPath<TContext>(
|
|
100
171
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
101
172
|
draft: string,
|
|
@@ -139,8 +210,8 @@ function promoteDraftRecipe<TContext>(
|
|
|
139
210
|
),
|
|
140
211
|
);
|
|
141
212
|
}
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
213
|
+
const raw = RecipesReferences.readRawRecipeConfig(draftPath);
|
|
214
|
+
if (!raw) {
|
|
144
215
|
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
|
|
145
216
|
throw new Error(
|
|
146
217
|
ExecutionOutput.formatToolText(
|
|
@@ -148,55 +219,79 @@ function promoteDraftRecipe<TContext>(
|
|
|
148
219
|
),
|
|
149
220
|
);
|
|
150
221
|
}
|
|
151
|
-
const
|
|
152
|
-
name,
|
|
222
|
+
const authoredRecipe = buildAuthoredRecipe(
|
|
153
223
|
{
|
|
154
224
|
...input,
|
|
155
|
-
description:
|
|
156
|
-
|
|
225
|
+
description:
|
|
226
|
+
input.description ??
|
|
227
|
+
(typeof raw.description === "string" ? raw.description : undefined),
|
|
157
228
|
},
|
|
158
229
|
existing,
|
|
230
|
+
raw,
|
|
231
|
+
);
|
|
232
|
+
const resolutionContext = getRegistrationResolutionContext(ctx, deps);
|
|
233
|
+
const candidate = RecipesDiscovery.admitUserRecipe(
|
|
234
|
+
targetPath,
|
|
235
|
+
resolutionContext,
|
|
236
|
+
authoredRecipe,
|
|
159
237
|
);
|
|
160
|
-
|
|
238
|
+
if (!candidate.validated || !candidate.tool) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
ExecutionOutput.formatToolText(
|
|
241
|
+
`Draft recipe is invalid: ${candidate.diagnostics.join("; ")}`,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
161
245
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
162
|
-
writeJsonAtomic(targetPath,
|
|
163
|
-
promoted
|
|
164
|
-
|
|
246
|
+
writeJsonAtomic(targetPath, authoredRecipe);
|
|
247
|
+
const promoted = RecipesDiscovery.admitUserRecipe(
|
|
248
|
+
targetPath,
|
|
249
|
+
resolutionContext,
|
|
250
|
+
).tool!;
|
|
165
251
|
tools.set(name, promoted);
|
|
166
|
-
deps.registerRuntimeTool(promoted);
|
|
252
|
+
const activation = deps.registerRuntimeTool(promoted) ?? undefined;
|
|
253
|
+
const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(promoted);
|
|
254
|
+
const callableNow = activation?.callable_now ?? false;
|
|
255
|
+
const nextActions = registrationNextActions(name, "draft", callableNow);
|
|
167
256
|
deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
|
|
168
257
|
return {
|
|
169
258
|
content: [
|
|
170
259
|
textContent(
|
|
171
260
|
ExecutionOutput.formatToolText(
|
|
172
|
-
`${existing ? "Updated" : "Registered"} tool "${name}" from draft
|
|
261
|
+
`${existing ? "Updated" : "Registered"} tool "${name}" from draft; callable_now=${callableNow}; next=${nextActions[0]}.`,
|
|
173
262
|
),
|
|
174
263
|
),
|
|
175
264
|
],
|
|
176
265
|
details: {
|
|
266
|
+
active_tool: activation?.active_tool ?? false,
|
|
267
|
+
activation: activation?.activation ?? "unverified",
|
|
268
|
+
activation_boundary: activationBoundary(activation),
|
|
177
269
|
args: promoted.args,
|
|
178
|
-
|
|
179
|
-
|
|
270
|
+
callable_now: callableNow,
|
|
271
|
+
defaults: promoted.defaults,
|
|
272
|
+
host_registered: activation?.host_registered ?? false,
|
|
273
|
+
next_actions: nextActions,
|
|
274
|
+
optional_args: argSummary.optional,
|
|
275
|
+
persisted: true,
|
|
180
276
|
promoted: true,
|
|
277
|
+
registry_active: tools.get(name) === promoted,
|
|
278
|
+
required_args: argSummary.required,
|
|
279
|
+
resolved: true,
|
|
280
|
+
source: "draft",
|
|
181
281
|
tool: name,
|
|
182
|
-
|
|
282
|
+
validated: true,
|
|
283
|
+
},
|
|
183
284
|
};
|
|
184
285
|
}
|
|
185
286
|
|
|
186
287
|
function persistToolRecipe<TContext>(
|
|
187
288
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
188
|
-
|
|
289
|
+
name: string,
|
|
290
|
+
recipe: Record<string, unknown>,
|
|
189
291
|
): string {
|
|
190
|
-
const path = getToolRecipePath(deps,
|
|
292
|
+
const path = getToolRecipePath(deps, name);
|
|
191
293
|
mkdirSync(dirname(path), { recursive: true });
|
|
192
|
-
writeJsonAtomic(path,
|
|
193
|
-
description: cfg.description,
|
|
194
|
-
...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
|
|
195
|
-
...(cfg.storedArgs ? { args: cfg.storedArgs } : {}),
|
|
196
|
-
...(cfg.storedDefaults ? { defaults: cfg.storedDefaults } : {}),
|
|
197
|
-
...(cfg.recipe?.values ? { values: cfg.recipe.values } : {}),
|
|
198
|
-
template: cfg.template,
|
|
199
|
-
});
|
|
294
|
+
writeJsonAtomic(path, recipe);
|
|
200
295
|
return path;
|
|
201
296
|
}
|
|
202
297
|
|
|
@@ -231,25 +326,34 @@ function deleteTool<TContext>(
|
|
|
231
326
|
),
|
|
232
327
|
),
|
|
233
328
|
],
|
|
234
|
-
details: {
|
|
329
|
+
details: { tool: name },
|
|
235
330
|
};
|
|
236
331
|
}
|
|
237
332
|
|
|
238
333
|
function readAuthoritativeStoredTool<TContext>(
|
|
239
334
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
240
335
|
name: string,
|
|
241
|
-
): {
|
|
336
|
+
): {
|
|
337
|
+
bytes?: string;
|
|
338
|
+
exists: boolean;
|
|
339
|
+
recipe?: Record<string, unknown>;
|
|
340
|
+
tool?: Config.RegisteredTool;
|
|
341
|
+
} {
|
|
242
342
|
const path = getToolRecipePath(deps, name);
|
|
243
343
|
if (!existsSync(path)) return { exists: false };
|
|
244
344
|
try {
|
|
345
|
+
const bytes = readFileSync(path, "utf8");
|
|
346
|
+
const recipe = JSON.parse(bytes);
|
|
245
347
|
const normalized = Config.normalizeStoredTool(
|
|
246
348
|
name,
|
|
247
|
-
|
|
349
|
+
recipe,
|
|
248
350
|
deps.reservedToolNames,
|
|
249
351
|
);
|
|
250
|
-
if (!normalized.cfg) return { exists: true };
|
|
352
|
+
if (!normalized.cfg) return { bytes, exists: true, recipe };
|
|
251
353
|
return {
|
|
354
|
+
bytes,
|
|
252
355
|
exists: true,
|
|
356
|
+
recipe,
|
|
253
357
|
tool: { ...normalized.cfg, sourcePath: path },
|
|
254
358
|
};
|
|
255
359
|
} catch {
|
|
@@ -257,9 +361,38 @@ function readAuthoritativeStoredTool<TContext>(
|
|
|
257
361
|
}
|
|
258
362
|
}
|
|
259
363
|
|
|
364
|
+
function looksLikeRecipeReference(value: string): boolean {
|
|
365
|
+
const trimmed = value.trim();
|
|
366
|
+
return (
|
|
367
|
+
trimmed.endsWith(".json") ||
|
|
368
|
+
trimmed.endsWith(".md") ||
|
|
369
|
+
/^[^/:\\]+\/[^/:\\]+$/.test(trimmed)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function assertTemplateIsNotNestedRecipe(value: unknown): void {
|
|
374
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
375
|
+
const record = value as Record<string, unknown>;
|
|
376
|
+
if (
|
|
377
|
+
typeof record.template === "string" &&
|
|
378
|
+
looksLikeRecipeReference(record.template) &&
|
|
379
|
+
(Object.keys(record).length === 1 ||
|
|
380
|
+
["artifacts", "async", "control", "description", "imports", "values"].some(
|
|
381
|
+
(key) => Object.hasOwn(record, key),
|
|
382
|
+
))
|
|
383
|
+
) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
ExecutionOutput.formatToolText(
|
|
386
|
+
"Nested Recipe-shaped templates are not a register_tool source mode. Next: retry with from=<skill>/<recipe> and defaults={...}.",
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
260
392
|
function getInputTemplate(
|
|
261
393
|
value: CommandTemplates.CommandTemplateValue | null | undefined,
|
|
262
394
|
): CommandTemplates.CommandTemplateValue | null | undefined {
|
|
395
|
+
assertTemplateIsNotNestedRecipe(value);
|
|
263
396
|
if (typeof value === "string") return value.trim();
|
|
264
397
|
if (value === null || value === undefined) return value;
|
|
265
398
|
if (Array.isArray(value)) {
|
|
@@ -285,25 +418,120 @@ function getInputTemplate(
|
|
|
285
418
|
);
|
|
286
419
|
}
|
|
287
420
|
|
|
288
|
-
function
|
|
289
|
-
|
|
421
|
+
function getRegistrationResolutionContext<TContext>(
|
|
422
|
+
ctx: TContext,
|
|
423
|
+
deps: RegisterToolRuntimeDeps<TContext>,
|
|
424
|
+
): RecipesContext.RecipeResolutionContext {
|
|
425
|
+
if (
|
|
426
|
+
ctx &&
|
|
427
|
+
typeof ctx === "object" &&
|
|
428
|
+
"recipeResolutionContext" in ctx
|
|
429
|
+
) {
|
|
430
|
+
const resolutionContext = (ctx as {
|
|
431
|
+
recipeResolutionContext?: RecipesContext.RecipeResolutionContext;
|
|
432
|
+
}).recipeResolutionContext;
|
|
433
|
+
if (resolutionContext) return resolutionContext;
|
|
434
|
+
}
|
|
435
|
+
return RecipesContext.createEmptyRecipeResolutionContext(
|
|
436
|
+
"offline-register-tool",
|
|
437
|
+
getRecipeRoot(deps),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function normalizeDefaults(value: unknown): Record<string, unknown> | undefined {
|
|
442
|
+
if (value === undefined) return undefined;
|
|
443
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
ExecutionOutput.formatToolText("Tool defaults must be an object."),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return { ...(value as Record<string, unknown>) };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function normalizeFromReference(
|
|
452
|
+
value: string,
|
|
453
|
+
resolutionContext: RecipesContext.RecipeResolutionContext,
|
|
454
|
+
): string {
|
|
455
|
+
const trimmed = value.trim();
|
|
456
|
+
if (!trimmed) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
ExecutionOutput.formatToolText(
|
|
459
|
+
"Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path.",
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
if (trimmed.startsWith("std:")) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
ExecutionOutput.formatToolText(
|
|
466
|
+
"std: Recipe references were removed; use <skill>/<recipe> or an explicit .json/.md file path.",
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (trimmed.startsWith("skill:")) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
ExecutionOutput.formatToolText(
|
|
473
|
+
`skill: Recipe references were removed; use ${trimmed.slice("skill:".length)} without a prefix.`,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) {
|
|
478
|
+
return resolve(resolutionContext.cwd, trimmed);
|
|
479
|
+
}
|
|
480
|
+
if (!looksLikeRecipeReference(trimmed)) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
ExecutionOutput.formatToolText(
|
|
483
|
+
"Tool from must use <skill>/<recipe> or an explicit .json/.md file path.",
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return trimmed;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function buildAuthoredRecipe(
|
|
290
491
|
input: RegisterToolInput,
|
|
291
492
|
existing: Config.RegisteredTool | undefined,
|
|
292
|
-
|
|
493
|
+
existingRecipe: Record<string, unknown> | undefined,
|
|
494
|
+
): Record<string, unknown> {
|
|
293
495
|
const explicitArgs =
|
|
294
496
|
input.args === undefined
|
|
295
497
|
? undefined
|
|
296
498
|
: Schema.parseToolArgDeclarations(input.args);
|
|
297
499
|
if (explicitArgs?.error)
|
|
298
500
|
throw new Error(ExecutionOutput.formatToolText(explicitArgs.error));
|
|
501
|
+
const from = typeof input.from === "string" ? input.from.trim() : "";
|
|
299
502
|
const description = (input.description ?? existing?.description ?? "").trim();
|
|
300
|
-
if (!description) {
|
|
503
|
+
if (!description && !from) {
|
|
301
504
|
throw new Error(
|
|
302
505
|
ExecutionOutput.formatToolText(
|
|
303
|
-
"Tool description is required
|
|
506
|
+
"Tool description is required for command-template registration.",
|
|
304
507
|
),
|
|
305
508
|
);
|
|
306
509
|
}
|
|
510
|
+
const inputDefaults = normalizeDefaults(input.defaults);
|
|
511
|
+
if (from) {
|
|
512
|
+
const runtimeOwnedDefault = Object.keys(inputDefaults ?? {}).find((key) =>
|
|
513
|
+
RecipesReferences.isRuntimeOwnedRecipeInput(key),
|
|
514
|
+
);
|
|
515
|
+
if (runtimeOwnedDefault) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
ExecutionOutput.formatToolText(
|
|
518
|
+
`Tool defaults cannot set runtime-owned input: ${runtimeOwnedDefault}. Next: remove that default and retry register_tool; the runtime supplies it.`,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const authored: Record<string, unknown> = {
|
|
523
|
+
...(description ? { description } : {}),
|
|
524
|
+
template: from,
|
|
525
|
+
};
|
|
526
|
+
const retainedDefaults =
|
|
527
|
+
inputDefaults ??
|
|
528
|
+
(existingRecipe?.defaults && typeof existingRecipe.defaults === "object"
|
|
529
|
+
? (existingRecipe.defaults as Record<string, unknown>)
|
|
530
|
+
: undefined);
|
|
531
|
+
if (retainedDefaults && Object.keys(retainedDefaults).length > 0)
|
|
532
|
+
authored.defaults = retainedDefaults;
|
|
533
|
+
return authored;
|
|
534
|
+
}
|
|
307
535
|
const template = getInputTemplate(input.template);
|
|
308
536
|
if (template === null) {
|
|
309
537
|
throw new Error(
|
|
@@ -311,68 +539,74 @@ function buildConfig(
|
|
|
311
539
|
);
|
|
312
540
|
}
|
|
313
541
|
const finalTemplate =
|
|
314
|
-
template === undefined || template === ""
|
|
542
|
+
template === undefined || template === ""
|
|
543
|
+
? existingRecipe?.template ?? existing?.template
|
|
544
|
+
: template;
|
|
315
545
|
if (!finalTemplate) {
|
|
316
546
|
throw new Error(
|
|
317
547
|
ExecutionOutput.formatToolText("Tool template is required."),
|
|
318
548
|
);
|
|
319
549
|
}
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
? {
|
|
323
|
-
name: inputRecipe,
|
|
324
|
-
...(typeof input.async === "boolean" ? { async: input.async } : {}),
|
|
325
|
-
template: finalTemplate,
|
|
326
|
-
...(input.values && typeof input.values === "object"
|
|
327
|
-
? { values: input.values }
|
|
328
|
-
: {}),
|
|
329
|
-
}
|
|
330
|
-
: template === undefined
|
|
331
|
-
? existing?.recipe
|
|
332
|
-
: undefined;
|
|
333
|
-
const defaults = explicitArgs?.defaults ?? existing?.storedDefaults ?? {};
|
|
334
|
-
const storedArgs = explicitArgs
|
|
335
|
-
? explicitArgs.declarations
|
|
336
|
-
: existing?.storedArgs;
|
|
337
|
-
const storedDefaults =
|
|
338
|
-
Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
339
|
-
const recipeTemplate = RecipesReferences.getRecipeTemplate(finalTemplate);
|
|
340
|
-
const argTemplate = recipeTemplate ?? finalTemplate;
|
|
341
|
-
const argTemplateConfig: CommandTemplates.CommandTemplateConfig =
|
|
342
|
-
typeof argTemplate === "object" && !Array.isArray(argTemplate)
|
|
343
|
-
? {
|
|
344
|
-
...argTemplate,
|
|
345
|
-
...(storedArgs !== undefined ? { args: storedArgs } : {}),
|
|
346
|
-
defaults: { ...(argTemplate.defaults ?? {}), ...defaults },
|
|
347
|
-
}
|
|
348
|
-
: {
|
|
349
|
-
args: storedArgs,
|
|
350
|
-
defaults,
|
|
351
|
-
template: argTemplate,
|
|
352
|
-
};
|
|
353
|
-
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
354
|
-
const argTypes = {
|
|
355
|
-
...inferredArgTypes,
|
|
356
|
-
...(existing?.argTypes ?? {}),
|
|
357
|
-
...(explicitArgs?.argTypes ?? {}),
|
|
358
|
-
};
|
|
359
|
-
return {
|
|
360
|
-
name,
|
|
550
|
+
const authored: Record<string, unknown> = {
|
|
551
|
+
...(existingRecipe ?? {}),
|
|
361
552
|
description,
|
|
362
553
|
template: finalTemplate,
|
|
363
|
-
...(recipe ? { recipe } : {}),
|
|
364
|
-
args:
|
|
365
|
-
RecipesReferences.isRecipeTool(finalTemplate, recipe) &&
|
|
366
|
-
storedArgs !== undefined
|
|
367
|
-
? Schema.getExplicitToolArgNames(storedArgs)
|
|
368
|
-
: RecipesReferences.isRecipeReference(finalTemplate) && !recipeTemplate
|
|
369
|
-
? Schema.getExplicitToolArgNames(storedArgs)
|
|
370
|
-
: Schema.getToolArgNames(argTemplateConfig),
|
|
371
|
-
defaults,
|
|
372
|
-
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
373
|
-
...(storedArgs !== undefined ? { storedArgs } : {}),
|
|
374
|
-
...(storedDefaults !== undefined ? { storedDefaults } : {}),
|
|
375
554
|
};
|
|
555
|
+
if (typeof input.async === "boolean") authored.async = input.async;
|
|
556
|
+
if (explicitArgs) {
|
|
557
|
+
authored.args = explicitArgs.declarations;
|
|
558
|
+
const defaults = { ...explicitArgs.defaults, ...(inputDefaults ?? {}) };
|
|
559
|
+
if (Object.keys(defaults).length > 0) authored.defaults = defaults;
|
|
560
|
+
else delete authored.defaults;
|
|
561
|
+
} else if (inputDefaults !== undefined) {
|
|
562
|
+
if (Object.keys(inputDefaults).length > 0) authored.defaults = inputDefaults;
|
|
563
|
+
else delete authored.defaults;
|
|
564
|
+
}
|
|
565
|
+
return authored;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function assertRegisterToolInputModes(input: RegisterToolInput): void {
|
|
569
|
+
const record = input as RegisterToolInput & Record<string, unknown>;
|
|
570
|
+
if (Object.hasOwn(record, "values")) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
ExecutionOutput.formatToolText(
|
|
573
|
+
"register_tool values was removed. Next: retry register_tool with defaults for caller inputs, or use from=<skill>/<recipe> for maintained composition.",
|
|
574
|
+
),
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const fromProvided = Object.hasOwn(record, "from");
|
|
578
|
+
if (
|
|
579
|
+
fromProvided &&
|
|
580
|
+
(typeof input.from !== "string" || input.from.trim().length === 0)
|
|
581
|
+
) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
ExecutionOutput.formatToolText(
|
|
584
|
+
"Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path.",
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const draftProvided =
|
|
589
|
+
typeof input.draft === "string" && input.draft.trim().length > 0;
|
|
590
|
+
const templateProvided = Object.hasOwn(record, "template");
|
|
591
|
+
const modeCount =
|
|
592
|
+
Number(fromProvided) + Number(draftProvided) + Number(templateProvided);
|
|
593
|
+
if (modeCount > 1) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
ExecutionOutput.formatToolText(
|
|
596
|
+
"Use exactly one register_tool source mode: from, template, or draft. Next: remove the extra source fields and retry register_tool.",
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (
|
|
601
|
+
fromProvided &&
|
|
602
|
+
(Object.hasOwn(record, "args") || Object.hasOwn(record, "async"))
|
|
603
|
+
) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
ExecutionOutput.formatToolText(
|
|
606
|
+
"register_tool from inherits args and async behavior. Next: retry register_tool with from and optional defaults only; omit args and async.",
|
|
607
|
+
),
|
|
608
|
+
);
|
|
609
|
+
}
|
|
376
610
|
}
|
|
377
611
|
|
|
378
612
|
function executeRegisterToolUnlocked<TContext>(
|
|
@@ -380,7 +614,12 @@ function executeRegisterToolUnlocked<TContext>(
|
|
|
380
614
|
ctx: TContext,
|
|
381
615
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
382
616
|
): RegisterToolResult {
|
|
383
|
-
const
|
|
617
|
+
const rawInput =
|
|
618
|
+
params && typeof params === "object"
|
|
619
|
+
? (params as RegisterToolInput)
|
|
620
|
+
: ({} as RegisterToolInput);
|
|
621
|
+
assertRegisterToolInputModes(rawInput);
|
|
622
|
+
let input = rawInput;
|
|
384
623
|
if (!input.name) return listTools(deps);
|
|
385
624
|
const name = Identity.normalizeToolName(input.name);
|
|
386
625
|
if (!name)
|
|
@@ -393,6 +632,27 @@ function executeRegisterToolUnlocked<TContext>(
|
|
|
393
632
|
if (typeof input.draft === "string" && input.draft.trim()) {
|
|
394
633
|
return promoteDraftRecipe(name, input, ctx, deps);
|
|
395
634
|
}
|
|
635
|
+
const resolutionContext = getRegistrationResolutionContext(ctx, deps);
|
|
636
|
+
const requestedSource =
|
|
637
|
+
typeof rawInput.from === "string" ? rawInput.from.trim() : undefined;
|
|
638
|
+
if (typeof input.from === "string") {
|
|
639
|
+
try {
|
|
640
|
+
input = {
|
|
641
|
+
...input,
|
|
642
|
+
from: normalizeFromReference(input.from, resolutionContext),
|
|
643
|
+
};
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const reason = error instanceof Error ? error.message.trim() : String(error);
|
|
646
|
+
const diagnosticSource = requestedSource?.startsWith("skill:")
|
|
647
|
+
? requestedSource.slice("skill:".length)
|
|
648
|
+
: requestedSource ?? "";
|
|
649
|
+
throw new Error(
|
|
650
|
+
ExecutionOutput.formatToolText(
|
|
651
|
+
`Recipe source "${requestedSource}" is invalid: ${reason}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(diagnosticSource)}`,
|
|
652
|
+
),
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
396
656
|
const templateProvided = Object.hasOwn(input, "template");
|
|
397
657
|
const template = getInputTemplate(input.template);
|
|
398
658
|
if (templateProvided && (template === null || template === ""))
|
|
@@ -409,54 +669,122 @@ function executeRegisterToolUnlocked<TContext>(
|
|
|
409
669
|
),
|
|
410
670
|
);
|
|
411
671
|
}
|
|
412
|
-
if (template === undefined && !existing) {
|
|
672
|
+
if (template === undefined && !input.from && !existing) {
|
|
413
673
|
throw new Error(
|
|
414
674
|
ExecutionOutput.formatToolText(
|
|
415
|
-
"
|
|
675
|
+
"New registration requires exactly one source mode: from, template, or draft. Next: retry register_tool with one source field.",
|
|
416
676
|
),
|
|
417
677
|
);
|
|
418
678
|
}
|
|
419
|
-
const
|
|
420
|
-
|
|
679
|
+
const recipePath = getToolRecipePath(deps, name);
|
|
680
|
+
const authoredRecipe = buildAuthoredRecipe(
|
|
681
|
+
input,
|
|
682
|
+
existing,
|
|
683
|
+
authoritative.recipe,
|
|
684
|
+
);
|
|
685
|
+
const candidate = RecipesDiscovery.admitUserRecipe(
|
|
686
|
+
recipePath,
|
|
687
|
+
resolutionContext,
|
|
688
|
+
authoredRecipe,
|
|
689
|
+
);
|
|
690
|
+
if (!candidate.validated || !candidate.tool) {
|
|
691
|
+
const sourceDiagnostic = requestedSource
|
|
692
|
+
? `Recipe source "${requestedSource}" validation failed: ${candidate.diagnostics.join("; ")}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(requestedSource)}`
|
|
693
|
+
: `Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`;
|
|
694
|
+
throw new Error(ExecutionOutput.formatToolText(sourceDiagnostic));
|
|
695
|
+
}
|
|
696
|
+
const activeBefore = deps.getActiveTools();
|
|
697
|
+
let persisted = false;
|
|
698
|
+
let activation: RuntimeActivation | undefined;
|
|
699
|
+
let cfg: Config.RegisteredTool;
|
|
421
700
|
try {
|
|
422
|
-
|
|
701
|
+
persistToolRecipe(deps, name, authoredRecipe);
|
|
702
|
+
persisted = true;
|
|
703
|
+
const admitted = RecipesDiscovery.admitUserRecipe(
|
|
704
|
+
recipePath,
|
|
705
|
+
resolutionContext,
|
|
706
|
+
);
|
|
707
|
+
if (!admitted.validated || !admitted.tool) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
cfg = admitted.tool;
|
|
713
|
+
tools.set(name, cfg);
|
|
714
|
+
activation = deps.registerRuntimeTool(cfg) ?? undefined;
|
|
715
|
+
if (
|
|
716
|
+
activation &&
|
|
717
|
+
(!activation.host_registered ||
|
|
718
|
+
!activation.active_tool ||
|
|
719
|
+
!activation.callable_now)
|
|
720
|
+
) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`Tool "${name}" activation failed at ${activationBoundary(activation)}; host_registered=${activation.host_registered}, active_tool=${activation.active_tool}, callable_now=${activation.callable_now}. Registration was rolled back. Next: inspect target=runtime view=status; do not use spawn as proof of tool invocation.`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
423
725
|
} catch (error) {
|
|
726
|
+
if (persisted) {
|
|
727
|
+
if (authoritative.bytes !== undefined)
|
|
728
|
+
writeTextAtomic(recipePath, authoritative.bytes);
|
|
729
|
+
else if (existsSync(recipePath)) unlinkSync(recipePath);
|
|
730
|
+
}
|
|
731
|
+
if (existing) {
|
|
732
|
+
tools.set(name, existing);
|
|
733
|
+
deps.registerRuntimeTool(existing);
|
|
734
|
+
} else {
|
|
735
|
+
tools.delete(name);
|
|
736
|
+
}
|
|
737
|
+
deps.setActiveTools(activeBefore);
|
|
424
738
|
throw new Error(
|
|
425
739
|
ExecutionOutput.formatToolText(
|
|
426
|
-
`
|
|
740
|
+
`Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
427
741
|
),
|
|
428
742
|
);
|
|
429
743
|
}
|
|
430
|
-
|
|
431
|
-
tools.set(name, cfg);
|
|
432
|
-
deps.registerRuntimeTool(cfg);
|
|
433
|
-
deps.notify(ctx, `Tool persisted: ${name}`, "info");
|
|
744
|
+
deps.notify(ctx, `Tool activated: ${name}`, "info");
|
|
434
745
|
const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
|
|
435
|
-
typeof cfg.template === "object" && !Array.isArray(cfg.template)
|
|
436
|
-
? cfg.template
|
|
437
|
-
: { template: cfg.template! },
|
|
746
|
+
typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
|
|
747
|
+
? cfg.recipe.template
|
|
748
|
+
: { template: cfg.recipe?.template ?? cfg.template! },
|
|
438
749
|
);
|
|
439
750
|
const warningText =
|
|
440
751
|
templateWarnings.length > 0
|
|
441
752
|
? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
|
|
442
753
|
: "";
|
|
754
|
+
const callableNow = activation?.callable_now ?? false;
|
|
755
|
+
const source = requestedSource ?? "template";
|
|
756
|
+
const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
|
|
757
|
+
const nextActions = registrationNextActions(name, source, callableNow);
|
|
758
|
+
const outcomeText = callableNow
|
|
759
|
+
? `${existing ? "Updated" : "Registered"} tool "${name}" from ${source}; callable_now=true`
|
|
760
|
+
: `Persisted tool "${name}" from ${source}; callable_now=false; activation_boundary=${activationBoundary(activation)}`;
|
|
443
761
|
return {
|
|
444
762
|
content: [
|
|
445
763
|
textContent(
|
|
446
764
|
ExecutionOutput.formatToolText(
|
|
447
|
-
`${
|
|
765
|
+
`${outcomeText}; required=${Schema.formatToolArgs(argSummary.required)}; optional=${Schema.formatToolArgs(argSummary.optional)}; next=${nextActions[0]}.${warningText}`,
|
|
448
766
|
),
|
|
449
767
|
),
|
|
450
768
|
],
|
|
451
769
|
details: {
|
|
770
|
+
active_tool: activation?.active_tool ?? false,
|
|
771
|
+
activation: activation?.activation ?? "unverified",
|
|
772
|
+
activation_boundary: activationBoundary(activation),
|
|
452
773
|
args: cfg.args,
|
|
453
|
-
|
|
774
|
+
callable_now: callableNow,
|
|
454
775
|
defaults: cfg.defaults,
|
|
776
|
+
host_registered: activation?.host_registered ?? false,
|
|
777
|
+
next_actions: nextActions,
|
|
778
|
+
optional_args: argSummary.optional,
|
|
779
|
+
persisted: true,
|
|
780
|
+
registry_active: tools.get(name) === cfg,
|
|
781
|
+
required_args: argSummary.required,
|
|
782
|
+
resolved: true,
|
|
783
|
+
source,
|
|
455
784
|
...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
|
|
456
|
-
...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
|
|
457
|
-
...(cfg.template ? { template: cfg.template } : {}),
|
|
458
785
|
...(templateWarnings.length > 0 ? { templateWarnings } : {}),
|
|
459
786
|
tool: name,
|
|
787
|
+
validated: true,
|
|
460
788
|
},
|
|
461
789
|
};
|
|
462
790
|
}
|