@llblab/pi-actors 0.46.1 → 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/BACKLOG.md +0 -582
- package/CHANGELOG.md +11 -0
- package/README.md +2 -1
- package/dist/lib/prompts.d.ts +6 -5
- package/dist/lib/prompts.js +22 -23
- package/dist/lib/recipes-discovery.d.ts +4 -0
- package/dist/lib/recipes-discovery.js +13 -1
- package/dist/lib/recipes-references.js +10 -6
- package/dist/lib/registry.d.ts +15 -11
- package/dist/lib/registry.js +195 -25
- package/dist/lib/runtime.js +37 -2
- package/dist/lib/tools-inspect.js +156 -12
- package/dist/lib/tools-register.js +2 -1
- package/dist/lib/tools-response.js +5 -1
- 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/tool-registry.md +10 -4
- package/lib/prompts.ts +24 -24
- package/lib/recipes-discovery.ts +22 -1
- package/lib/recipes-references.ts +14 -6
- package/lib/registry.ts +288 -51
- package/lib/runtime.ts +41 -2
- package/lib/tools-inspect.ts +202 -10
- package/lib/tools-register.ts +4 -3
- package/lib/tools-response.ts +5 -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/dist/lib/prompts.js
CHANGED
|
@@ -3,41 +3,40 @@
|
|
|
3
3
|
* Zones: prompts, onboarding, tool schema copy
|
|
4
4
|
* Owns LLM-facing descriptions, prompt snippets, guidelines, and parameter descriptions
|
|
5
5
|
*/
|
|
6
|
-
export const REGISTER_TOOL_DESCRIPTION = "Register a persistent custom tool from
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
export const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
|
|
6
|
+
export const REGISTER_TOOL_DESCRIPTION = "Register a persistent custom tool from one maintained Recipe, command template, or captured draft. " +
|
|
7
|
+
"Use from for Recipe specialization, template for trusted commands, or draft for promotion. " +
|
|
8
|
+
"Definitions persist under ~/.pi/agent/recipes; activation is reported separately.";
|
|
9
|
+
export const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent Recipes or command templates as agent-callable tools";
|
|
10
10
|
export const REGISTER_TOOL_GUIDELINES = [
|
|
11
|
-
"Use register_tool
|
|
12
|
-
"
|
|
11
|
+
"Use register_tool from=<skill>/<recipe> with defaults={...} to specialize a maintained Recipe without copying its contract.",
|
|
12
|
+
"Use register_tool template only for trusted command templates, and use register_tool draft only for captured draft promotion.",
|
|
13
|
+
"After register_tool succeeds, trust its callable_now and activation result; persistence alone is not callability.",
|
|
13
14
|
'Set template=null or template="" in register_tool to delete a persisted tool.',
|
|
14
15
|
"Set update=true in register_tool to overwrite an existing tool registration.",
|
|
15
16
|
];
|
|
16
|
-
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; active-Skill and explicit file Recipes remain components outside user tool discovery; offer to save successful recurring patterns only after confirmation.
|
|
28
|
-
- Prefer maintained active-Skill Recipes with spawn recipe=<skill>/<recipe> before ad hoc scripts/wrappers; explicit file Recipes use exact .json/.md paths; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.
|
|
29
|
-
- For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
|
|
17
|
+
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors Skill routing:
|
|
18
|
+
- Treat active bundled Skills as the operating authority.
|
|
19
|
+
- For non-trivial pi-actors operation, diagnosis, or development, load and read the actors Skill before acting.
|
|
20
|
+
- For work requiring multiple actors or subagents, additionally load and read the swarm Skill.
|
|
21
|
+
- For capability-specific selection or constraints, load the owning capability Skill; actors owns generic mechanics and swarm owns multi-actor methodology.
|
|
22
|
+
- Keep a Skill Recipe distinct from a registered tool and a Recipe spawn distinct from registered-tool invocation; actors owns the proof rules.
|
|
23
|
+
- Treat persistence or registration as distinct from current callability; actors owns activation proof.
|
|
24
|
+
- On failure or disagreement, preserve the logical Recipe identity, stop, and follow actors diagnosis; never bypass the owning Skills with copied contracts, helper paths, shell evaluation, or background-process workarounds.
|
|
25
|
+
- If a capability Skill conflicts with actors about generic mechanics, follow actors and report the stale capability guidance.
|
|
26
|
+
- README and docs are human-facing references, not the normal agent operating path.
|
|
27
|
+
- AGENTS, source, and tests are implementation protocol and evidence; use them when changing or debugging the extension, not as substitutes for operating Skills.`;
|
|
30
28
|
export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
31
29
|
name: "Tool name in snake_case (e.g., 'transcribe')",
|
|
32
30
|
description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
|
|
33
|
-
|
|
31
|
+
from: "Recipe to specialize by canonical <skill>/<recipe> identity or explicit .json/.md path. Inherits async, args/types, source defaults, artifacts, Control, and runtime origins.",
|
|
32
|
+
defaults: "Optional caller-owned defaults. Keys and values must satisfy the effective source or command-template argument contract.",
|
|
33
|
+
draft: "Promote a captured draft Recipe path from ~/.pi/agent/recipes/drafts. This is a source mode; do not combine it with from or template.",
|
|
34
34
|
async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
35
|
-
template: "
|
|
35
|
+
template: "Trusted command template with {arg} or {arg=default} placeholders. To specialize a Recipe, use from instead. Omitted updates keep the old template; empty string deletes the tool.",
|
|
36
36
|
templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.",
|
|
37
37
|
templateNull: "Delete the tool when template is null.",
|
|
38
38
|
args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast",
|
|
39
39
|
update: "Set to true to overwrite an existing tool registration.",
|
|
40
|
-
values: "Optional default runtime placeholder values for a co-located template recipe.",
|
|
41
40
|
};
|
|
42
41
|
export function formatRegisteredToolPromptSnippet(template) {
|
|
43
42
|
const rendered = typeof template === "string" ? template : JSON.stringify(template);
|
|
@@ -69,5 +69,9 @@ export declare function createRecipeIntegrityManifest(result: RecipesDiscoveryRe
|
|
|
69
69
|
export declare function getShadowedLaunchDiagnostic(result: RecipesDiscoveryResult, id: string): Record<string, unknown> | undefined;
|
|
70
70
|
export declare function listDraftRecipes(root: string): Array<Record<string, unknown>>;
|
|
71
71
|
export declare function summarizeDiscovery(result: RecipesDiscoveryResult): Record<string, unknown>;
|
|
72
|
+
export declare function summarizeRegisteredToolArgs(tool: RegisteredTool): {
|
|
73
|
+
optional: string[];
|
|
74
|
+
required: string[];
|
|
75
|
+
};
|
|
72
76
|
export declare function admitUserRecipe(sourcePath: string, resolutionContext: RecipeResolutionContext, authoredRecipe?: Record<string, unknown>, mutableUsage?: boolean): UserRecipeAdmission;
|
|
73
77
|
export declare function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | undefined;
|
|
@@ -134,7 +134,7 @@ function readDiscoveredRecipe(root, file, priority, defaultTool = false, mutable
|
|
|
134
134
|
mutableUsage,
|
|
135
135
|
resolutionContext,
|
|
136
136
|
diagnostics: [
|
|
137
|
-
`
|
|
137
|
+
`Recipe ${id} rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
138
138
|
],
|
|
139
139
|
riskLabels: [],
|
|
140
140
|
shadows: [],
|
|
@@ -699,6 +699,18 @@ function projectRegisteredTool(identity, path, cfg, mutableUsage) {
|
|
|
699
699
|
: {}),
|
|
700
700
|
};
|
|
701
701
|
}
|
|
702
|
+
export function summarizeRegisteredToolArgs(tool) {
|
|
703
|
+
const publicArgs = tool.args.filter((arg) => !RecipesReferences.isRuntimeOwnedRecipeInput(arg));
|
|
704
|
+
const inlineDefaults = Schema.getExplicitToolArgDefaults(tool.recipe?.args);
|
|
705
|
+
const required = publicArgs.filter((arg) => !Object.hasOwn(tool.defaults, arg) &&
|
|
706
|
+
!Object.hasOwn(inlineDefaults, arg));
|
|
707
|
+
const requiredSet = new Set(required);
|
|
708
|
+
const optional = publicArgs.filter((arg) => !requiredSet.has(arg));
|
|
709
|
+
if (tool.recipe?.async === true) {
|
|
710
|
+
optional.push("run_id", "transport_context");
|
|
711
|
+
}
|
|
712
|
+
return { optional, required };
|
|
713
|
+
}
|
|
702
714
|
function boundedAdmissionDiagnostic(error) {
|
|
703
715
|
const message = error instanceof Error ? error.message : String(error);
|
|
704
716
|
return message.length <= 4096 ? message : `${message.slice(0, 4095)}…`;
|
|
@@ -72,7 +72,7 @@ export function inventoryActiveSkillRecipeComponents(context) {
|
|
|
72
72
|
for (const [skill, roots] of Object.entries(context.namespaces)) {
|
|
73
73
|
if (roots.length > 1) {
|
|
74
74
|
rejected.push({
|
|
75
|
-
reason: `Duplicate active Skill identity ${skill}
|
|
75
|
+
reason: `Duplicate active Skill identity ${skill} (${roots.length} active definitions); exact Recipe resolution is ambiguous. Next: inspect target=recipes view=doctor identity=${skill}/<recipe>`,
|
|
76
76
|
skill,
|
|
77
77
|
});
|
|
78
78
|
continue;
|
|
@@ -175,10 +175,11 @@ function qualifiedRecipeNameForFile(file, context) {
|
|
|
175
175
|
}
|
|
176
176
|
function assertRecipeReferencePrefixWasNotRemoved(value) {
|
|
177
177
|
if (value.startsWith("std:")) {
|
|
178
|
-
throw new Error("std: Recipe references were removed in pi-actors 0.46
|
|
178
|
+
throw new Error("std: Recipe references were removed in pi-actors 0.46. Use <skill>/<recipe> or an explicit .json/.md file path. Next: inspect target=recipes view=status to identify the active owning Skill.");
|
|
179
179
|
}
|
|
180
180
|
if (value.startsWith("skill:")) {
|
|
181
|
-
|
|
181
|
+
const identity = value.slice("skill:".length);
|
|
182
|
+
throw new Error(`skill: Recipe references were removed in pi-actors 0.46. Use ${identity} without a prefix. Next: inspect target=recipes view=doctor identity=${identity}`);
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
function skillRecipeCandidates(value, context) {
|
|
@@ -188,12 +189,15 @@ function skillRecipeCandidates(value, context) {
|
|
|
188
189
|
const [, skillName, recipeName] = match;
|
|
189
190
|
const roots = [...(context.namespaces[skillName] ?? [])];
|
|
190
191
|
if (roots.length === 0)
|
|
191
|
-
throw new Error(`Active Skill Recipe not found: ${skillName}/${recipeName}`);
|
|
192
|
+
throw new Error(`Active Skill Recipe not found: ${skillName}/${recipeName}. Owning Skill "${skillName}" is not active. Next: activate Skill ${skillName}, then inspect target=recipes view=doctor identity=${skillName}/${recipeName}`);
|
|
192
193
|
if (roots.length > 1)
|
|
193
|
-
throw new Error(`Duplicate active Skill identity ${skillName}
|
|
194
|
+
throw new Error(`Duplicate active Skill identity ${skillName} (${roots.length} active definitions); ${skillName}/${recipeName} cannot resolve unambiguously. Next: inspect target=recipes view=doctor identity=${skillName}/${recipeName}`);
|
|
194
195
|
const root = roots[0];
|
|
195
196
|
const candidates = ["json", "md"].map((extension) => resolve(root, `${recipeName}.${extension}`));
|
|
196
197
|
const existing = candidates.filter((candidate) => existsSync(candidate));
|
|
198
|
+
if (existing.length === 0) {
|
|
199
|
+
throw new Error(`Skill Recipe component not found: ${skillName}/${recipeName}; owning Skill "${skillName}" is active. Next: inspect target=recipes view=doctor identity=${skillName}/${recipeName}`);
|
|
200
|
+
}
|
|
197
201
|
if (existing.length > 1) {
|
|
198
202
|
throw new Error(`Skill Recipe stem collision: ${skillName}/${recipeName} has both .json and .md files`);
|
|
199
203
|
}
|
|
@@ -223,7 +227,7 @@ function findOwningSkillDir(recipeFile) {
|
|
|
223
227
|
function assertRecipeHasNoDeclaredName(raw) {
|
|
224
228
|
if (!Object.hasOwn(raw, "name"))
|
|
225
229
|
return;
|
|
226
|
-
throw new Error("Recipe.name was removed in pi-actors 0.46
|
|
230
|
+
throw new Error("Recipe.name was removed in pi-actors 0.46. File-backed Recipe identity is derived from its file name. Next: remove the name field, keep the intended filename stem, then inspect target=recipes view=doctor.");
|
|
227
231
|
}
|
|
228
232
|
function assertRuntimeRecipeValuesNotDeclared(raw) {
|
|
229
233
|
const args = Array.isArray(raw.args)
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -10,27 +10,29 @@ export interface RegisterToolInput {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
async?: boolean;
|
|
12
12
|
template?: CommandTemplates.CommandTemplateValue | null;
|
|
13
|
+
from?: string;
|
|
14
|
+
defaults?: Record<string, unknown>;
|
|
13
15
|
draft?: string;
|
|
14
16
|
args?: string;
|
|
15
17
|
update?: boolean;
|
|
16
|
-
values?: Record<string, unknown>;
|
|
17
18
|
}
|
|
18
19
|
export interface RegisterToolResultDetails {
|
|
19
20
|
active_tool?: boolean;
|
|
20
21
|
activation?: "current_session" | "unverified";
|
|
22
|
+
activation_boundary?: string;
|
|
21
23
|
args?: string[];
|
|
22
24
|
async?: boolean;
|
|
23
25
|
callable_now?: boolean;
|
|
24
|
-
config?: string;
|
|
25
26
|
defaults?: Record<string, string>;
|
|
26
|
-
draft?: string;
|
|
27
27
|
host_registered?: boolean;
|
|
28
|
+
next_actions?: string[];
|
|
29
|
+
optional_args?: string[];
|
|
28
30
|
persisted?: boolean;
|
|
29
31
|
promoted?: boolean;
|
|
30
32
|
registry_active?: boolean;
|
|
33
|
+
required_args?: string[];
|
|
31
34
|
resolved?: boolean;
|
|
32
|
-
|
|
33
|
-
template?: CommandTemplates.CommandTemplateValue;
|
|
35
|
+
source?: string;
|
|
34
36
|
templateWarnings?: string[];
|
|
35
37
|
tool: string;
|
|
36
38
|
validated?: boolean;
|
|
@@ -42,6 +44,12 @@ export interface RegisterToolResult {
|
|
|
42
44
|
}>;
|
|
43
45
|
details: RegisterToolResultDetails;
|
|
44
46
|
}
|
|
47
|
+
interface RuntimeActivation {
|
|
48
|
+
active_tool: boolean;
|
|
49
|
+
activation: "current_session" | "unverified";
|
|
50
|
+
callable_now: boolean;
|
|
51
|
+
host_registered: boolean;
|
|
52
|
+
}
|
|
45
53
|
export interface RegisterToolRuntimeDeps<TContext> {
|
|
46
54
|
configPath: string;
|
|
47
55
|
recipeRoot?: string;
|
|
@@ -49,13 +57,9 @@ export interface RegisterToolRuntimeDeps<TContext> {
|
|
|
49
57
|
getTools: () => Map<string, Config.RegisteredTool>;
|
|
50
58
|
getActiveTools: () => string[];
|
|
51
59
|
notify: (ctx: TContext, message: string, type: "info" | "warning" | "error") => void;
|
|
52
|
-
registerRuntimeTool: (cfg: Config.RegisteredTool) =>
|
|
53
|
-
active_tool: boolean;
|
|
54
|
-
activation: "current_session" | "unverified";
|
|
55
|
-
callable_now: boolean;
|
|
56
|
-
host_registered: boolean;
|
|
57
|
-
} | void;
|
|
60
|
+
registerRuntimeTool: (cfg: Config.RegisteredTool) => RuntimeActivation | void;
|
|
58
61
|
reservedToolNames: Set<string>;
|
|
59
62
|
setActiveTools: (toolNames: string[]) => void;
|
|
60
63
|
}
|
|
61
64
|
export declare function executeRegisterTool<TContext>(params: unknown, ctx: TContext, deps: RegisterToolRuntimeDeps<TContext>): Promise<RegisterToolResult>;
|
|
65
|
+
export {};
|
package/dist/lib/registry.js
CHANGED
|
@@ -35,6 +35,42 @@ function getRecipeRoot(deps) {
|
|
|
35
35
|
function getToolRecipePath(deps, name) {
|
|
36
36
|
return join(getRecipeRoot(deps), `${name}.json`);
|
|
37
37
|
}
|
|
38
|
+
function activationBoundary(activation) {
|
|
39
|
+
if (activation?.callable_now)
|
|
40
|
+
return "current_session";
|
|
41
|
+
if (!activation)
|
|
42
|
+
return "session_activation_unverified";
|
|
43
|
+
if (!activation.host_registered)
|
|
44
|
+
return "host_registration";
|
|
45
|
+
if (!activation.active_tool)
|
|
46
|
+
return "active_tool_set";
|
|
47
|
+
return "callability_verification";
|
|
48
|
+
}
|
|
49
|
+
function recipeDoctorAction(source) {
|
|
50
|
+
return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(source)
|
|
51
|
+
? `inspect target=recipes view=doctor identity=${source}`
|
|
52
|
+
: "inspect target=recipes view=doctor";
|
|
53
|
+
}
|
|
54
|
+
function registrationNextActions(tool, source, callableNow) {
|
|
55
|
+
if (callableNow) {
|
|
56
|
+
return [
|
|
57
|
+
`call tool ${tool}`,
|
|
58
|
+
`inspect target=tool:${tool} view=status`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
return [
|
|
62
|
+
`inspect target=tool:${tool} view=status`,
|
|
63
|
+
...(source === "template" || source === "draft"
|
|
64
|
+
? []
|
|
65
|
+
: [recipeDoctorAction(source)]),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
function activeSkillSummary(resolutionContext) {
|
|
69
|
+
const names = Object.keys(RecipesReferences.getActiveSkillRecipeNamespaces(resolutionContext.activeSkills))
|
|
70
|
+
.sort()
|
|
71
|
+
.slice(0, 20);
|
|
72
|
+
return names.length > 0 ? names.join(", ") : "<none>";
|
|
73
|
+
}
|
|
38
74
|
function assertDraftPath(deps, draft) {
|
|
39
75
|
const draftRoot = resolve(getRecipeRoot(deps), "drafts");
|
|
40
76
|
const path = resolve(draft);
|
|
@@ -79,18 +115,33 @@ function promoteDraftRecipe(name, input, ctx, deps) {
|
|
|
79
115
|
writeJsonAtomic(targetPath, authoredRecipe);
|
|
80
116
|
const promoted = RecipesDiscovery.admitUserRecipe(targetPath, resolutionContext).tool;
|
|
81
117
|
tools.set(name, promoted);
|
|
82
|
-
deps.registerRuntimeTool(promoted);
|
|
118
|
+
const activation = deps.registerRuntimeTool(promoted) ?? undefined;
|
|
119
|
+
const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(promoted);
|
|
120
|
+
const callableNow = activation?.callable_now ?? false;
|
|
121
|
+
const nextActions = registrationNextActions(name, "draft", callableNow);
|
|
83
122
|
deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
|
|
84
123
|
return {
|
|
85
124
|
content: [
|
|
86
|
-
textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft
|
|
125
|
+
textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft; callable_now=${callableNow}; next=${nextActions[0]}.`)),
|
|
87
126
|
],
|
|
88
127
|
details: {
|
|
128
|
+
active_tool: activation?.active_tool ?? false,
|
|
129
|
+
activation: activation?.activation ?? "unverified",
|
|
130
|
+
activation_boundary: activationBoundary(activation),
|
|
89
131
|
args: promoted.args,
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
callable_now: callableNow,
|
|
133
|
+
defaults: promoted.defaults,
|
|
134
|
+
host_registered: activation?.host_registered ?? false,
|
|
135
|
+
next_actions: nextActions,
|
|
136
|
+
optional_args: argSummary.optional,
|
|
137
|
+
persisted: true,
|
|
92
138
|
promoted: true,
|
|
139
|
+
registry_active: tools.get(name) === promoted,
|
|
140
|
+
required_args: argSummary.required,
|
|
141
|
+
resolved: true,
|
|
142
|
+
source: "draft",
|
|
93
143
|
tool: name,
|
|
144
|
+
validated: true,
|
|
94
145
|
},
|
|
95
146
|
};
|
|
96
147
|
}
|
|
@@ -120,7 +171,7 @@ function deleteTool(name, ctx, deps) {
|
|
|
120
171
|
content: [
|
|
121
172
|
textContent(ExecutionOutput.formatToolText(`Deleted tool "${name}". Reload to remove it from the complete registry.`)),
|
|
122
173
|
],
|
|
123
|
-
details: {
|
|
174
|
+
details: { tool: name },
|
|
124
175
|
};
|
|
125
176
|
}
|
|
126
177
|
function readAuthoritativeStoredTool(deps, name) {
|
|
@@ -144,7 +195,25 @@ function readAuthoritativeStoredTool(deps, name) {
|
|
|
144
195
|
return { exists: true };
|
|
145
196
|
}
|
|
146
197
|
}
|
|
198
|
+
function looksLikeRecipeReference(value) {
|
|
199
|
+
const trimmed = value.trim();
|
|
200
|
+
return (trimmed.endsWith(".json") ||
|
|
201
|
+
trimmed.endsWith(".md") ||
|
|
202
|
+
/^[^/:\\]+\/[^/:\\]+$/.test(trimmed));
|
|
203
|
+
}
|
|
204
|
+
function assertTemplateIsNotNestedRecipe(value) {
|
|
205
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
206
|
+
return;
|
|
207
|
+
const record = value;
|
|
208
|
+
if (typeof record.template === "string" &&
|
|
209
|
+
looksLikeRecipeReference(record.template) &&
|
|
210
|
+
(Object.keys(record).length === 1 ||
|
|
211
|
+
["artifacts", "async", "control", "description", "imports", "values"].some((key) => Object.hasOwn(record, key)))) {
|
|
212
|
+
throw new Error(ExecutionOutput.formatToolText("Nested Recipe-shaped templates are not a register_tool source mode. Next: retry with from=<skill>/<recipe> and defaults={...}."));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
147
215
|
function getInputTemplate(value) {
|
|
216
|
+
assertTemplateIsNotNestedRecipe(value);
|
|
148
217
|
if (typeof value === "string")
|
|
149
218
|
return value.trim();
|
|
150
219
|
if (value === null || value === undefined)
|
|
@@ -173,15 +242,61 @@ function getRegistrationResolutionContext(ctx, deps) {
|
|
|
173
242
|
}
|
|
174
243
|
return RecipesContext.createEmptyRecipeResolutionContext("offline-register-tool", getRecipeRoot(deps));
|
|
175
244
|
}
|
|
245
|
+
function normalizeDefaults(value) {
|
|
246
|
+
if (value === undefined)
|
|
247
|
+
return undefined;
|
|
248
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
249
|
+
throw new Error(ExecutionOutput.formatToolText("Tool defaults must be an object."));
|
|
250
|
+
}
|
|
251
|
+
return { ...value };
|
|
252
|
+
}
|
|
253
|
+
function normalizeFromReference(value, resolutionContext) {
|
|
254
|
+
const trimmed = value.trim();
|
|
255
|
+
if (!trimmed) {
|
|
256
|
+
throw new Error(ExecutionOutput.formatToolText("Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path."));
|
|
257
|
+
}
|
|
258
|
+
if (trimmed.startsWith("std:")) {
|
|
259
|
+
throw new Error(ExecutionOutput.formatToolText("std: Recipe references were removed; use <skill>/<recipe> or an explicit .json/.md file path."));
|
|
260
|
+
}
|
|
261
|
+
if (trimmed.startsWith("skill:")) {
|
|
262
|
+
throw new Error(ExecutionOutput.formatToolText(`skill: Recipe references were removed; use ${trimmed.slice("skill:".length)} without a prefix.`));
|
|
263
|
+
}
|
|
264
|
+
if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) {
|
|
265
|
+
return resolve(resolutionContext.cwd, trimmed);
|
|
266
|
+
}
|
|
267
|
+
if (!looksLikeRecipeReference(trimmed)) {
|
|
268
|
+
throw new Error(ExecutionOutput.formatToolText("Tool from must use <skill>/<recipe> or an explicit .json/.md file path."));
|
|
269
|
+
}
|
|
270
|
+
return trimmed;
|
|
271
|
+
}
|
|
176
272
|
function buildAuthoredRecipe(input, existing, existingRecipe) {
|
|
177
273
|
const explicitArgs = input.args === undefined
|
|
178
274
|
? undefined
|
|
179
275
|
: Schema.parseToolArgDeclarations(input.args);
|
|
180
276
|
if (explicitArgs?.error)
|
|
181
277
|
throw new Error(ExecutionOutput.formatToolText(explicitArgs.error));
|
|
278
|
+
const from = typeof input.from === "string" ? input.from.trim() : "";
|
|
182
279
|
const description = (input.description ?? existing?.description ?? "").trim();
|
|
183
|
-
if (!description) {
|
|
184
|
-
throw new Error(ExecutionOutput.formatToolText("Tool description is required
|
|
280
|
+
if (!description && !from) {
|
|
281
|
+
throw new Error(ExecutionOutput.formatToolText("Tool description is required for command-template registration."));
|
|
282
|
+
}
|
|
283
|
+
const inputDefaults = normalizeDefaults(input.defaults);
|
|
284
|
+
if (from) {
|
|
285
|
+
const runtimeOwnedDefault = Object.keys(inputDefaults ?? {}).find((key) => RecipesReferences.isRuntimeOwnedRecipeInput(key));
|
|
286
|
+
if (runtimeOwnedDefault) {
|
|
287
|
+
throw new Error(ExecutionOutput.formatToolText(`Tool defaults cannot set runtime-owned input: ${runtimeOwnedDefault}. Next: remove that default and retry register_tool; the runtime supplies it.`));
|
|
288
|
+
}
|
|
289
|
+
const authored = {
|
|
290
|
+
...(description ? { description } : {}),
|
|
291
|
+
template: from,
|
|
292
|
+
};
|
|
293
|
+
const retainedDefaults = inputDefaults ??
|
|
294
|
+
(existingRecipe?.defaults && typeof existingRecipe.defaults === "object"
|
|
295
|
+
? existingRecipe.defaults
|
|
296
|
+
: undefined);
|
|
297
|
+
if (retainedDefaults && Object.keys(retainedDefaults).length > 0)
|
|
298
|
+
authored.defaults = retainedDefaults;
|
|
299
|
+
return authored;
|
|
185
300
|
}
|
|
186
301
|
const template = getInputTemplate(input.template);
|
|
187
302
|
if (template === null) {
|
|
@@ -202,17 +317,47 @@ function buildAuthoredRecipe(input, existing, existingRecipe) {
|
|
|
202
317
|
authored.async = input.async;
|
|
203
318
|
if (explicitArgs) {
|
|
204
319
|
authored.args = explicitArgs.declarations;
|
|
205
|
-
|
|
206
|
-
|
|
320
|
+
const defaults = { ...explicitArgs.defaults, ...(inputDefaults ?? {}) };
|
|
321
|
+
if (Object.keys(defaults).length > 0)
|
|
322
|
+
authored.defaults = defaults;
|
|
323
|
+
else
|
|
324
|
+
delete authored.defaults;
|
|
325
|
+
}
|
|
326
|
+
else if (inputDefaults !== undefined) {
|
|
327
|
+
if (Object.keys(inputDefaults).length > 0)
|
|
328
|
+
authored.defaults = inputDefaults;
|
|
207
329
|
else
|
|
208
330
|
delete authored.defaults;
|
|
209
331
|
}
|
|
210
|
-
if (input.values && typeof input.values === "object")
|
|
211
|
-
authored.values = input.values;
|
|
212
332
|
return authored;
|
|
213
333
|
}
|
|
334
|
+
function assertRegisterToolInputModes(input) {
|
|
335
|
+
const record = input;
|
|
336
|
+
if (Object.hasOwn(record, "values")) {
|
|
337
|
+
throw new Error(ExecutionOutput.formatToolText("register_tool values was removed. Next: retry register_tool with defaults for caller inputs, or use from=<skill>/<recipe> for maintained composition."));
|
|
338
|
+
}
|
|
339
|
+
const fromProvided = Object.hasOwn(record, "from");
|
|
340
|
+
if (fromProvided &&
|
|
341
|
+
(typeof input.from !== "string" || input.from.trim().length === 0)) {
|
|
342
|
+
throw new Error(ExecutionOutput.formatToolText("Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path."));
|
|
343
|
+
}
|
|
344
|
+
const draftProvided = typeof input.draft === "string" && input.draft.trim().length > 0;
|
|
345
|
+
const templateProvided = Object.hasOwn(record, "template");
|
|
346
|
+
const modeCount = Number(fromProvided) + Number(draftProvided) + Number(templateProvided);
|
|
347
|
+
if (modeCount > 1) {
|
|
348
|
+
throw new Error(ExecutionOutput.formatToolText("Use exactly one register_tool source mode: from, template, or draft. Next: remove the extra source fields and retry register_tool."));
|
|
349
|
+
}
|
|
350
|
+
if (fromProvided &&
|
|
351
|
+
(Object.hasOwn(record, "args") || Object.hasOwn(record, "async"))) {
|
|
352
|
+
throw new Error(ExecutionOutput.formatToolText("register_tool from inherits args and async behavior. Next: retry register_tool with from and optional defaults only; omit args and async."));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
214
355
|
function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
215
|
-
const
|
|
356
|
+
const rawInput = params && typeof params === "object"
|
|
357
|
+
? params
|
|
358
|
+
: {};
|
|
359
|
+
assertRegisterToolInputModes(rawInput);
|
|
360
|
+
let input = rawInput;
|
|
216
361
|
if (!input.name)
|
|
217
362
|
return listTools(deps);
|
|
218
363
|
const name = Identity.normalizeToolName(input.name);
|
|
@@ -224,6 +369,23 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
224
369
|
if (typeof input.draft === "string" && input.draft.trim()) {
|
|
225
370
|
return promoteDraftRecipe(name, input, ctx, deps);
|
|
226
371
|
}
|
|
372
|
+
const resolutionContext = getRegistrationResolutionContext(ctx, deps);
|
|
373
|
+
const requestedSource = typeof rawInput.from === "string" ? rawInput.from.trim() : undefined;
|
|
374
|
+
if (typeof input.from === "string") {
|
|
375
|
+
try {
|
|
376
|
+
input = {
|
|
377
|
+
...input,
|
|
378
|
+
from: normalizeFromReference(input.from, resolutionContext),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const reason = error instanceof Error ? error.message.trim() : String(error);
|
|
383
|
+
const diagnosticSource = requestedSource?.startsWith("skill:")
|
|
384
|
+
? requestedSource.slice("skill:".length)
|
|
385
|
+
: requestedSource ?? "";
|
|
386
|
+
throw new Error(ExecutionOutput.formatToolText(`Recipe source "${requestedSource}" is invalid: ${reason}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(diagnosticSource)}`));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
227
389
|
const templateProvided = Object.hasOwn(input, "template");
|
|
228
390
|
const template = getInputTemplate(input.template);
|
|
229
391
|
if (templateProvided && (template === null || template === ""))
|
|
@@ -237,15 +399,17 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
237
399
|
if ((authoritative.exists || existing) && !input.update) {
|
|
238
400
|
throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
|
|
239
401
|
}
|
|
240
|
-
if (template === undefined && !existing) {
|
|
241
|
-
throw new Error(ExecutionOutput.formatToolText("
|
|
402
|
+
if (template === undefined && !input.from && !existing) {
|
|
403
|
+
throw new Error(ExecutionOutput.formatToolText("New registration requires exactly one source mode: from, template, or draft. Next: retry register_tool with one source field."));
|
|
242
404
|
}
|
|
243
405
|
const recipePath = getToolRecipePath(deps, name);
|
|
244
406
|
const authoredRecipe = buildAuthoredRecipe(input, existing, authoritative.recipe);
|
|
245
|
-
const resolutionContext = getRegistrationResolutionContext(ctx, deps);
|
|
246
407
|
const candidate = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext, authoredRecipe);
|
|
247
408
|
if (!candidate.validated || !candidate.tool) {
|
|
248
|
-
|
|
409
|
+
const sourceDiagnostic = requestedSource
|
|
410
|
+
? `Recipe source "${requestedSource}" validation failed: ${candidate.diagnostics.join("; ")}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(requestedSource)}`
|
|
411
|
+
: `Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`;
|
|
412
|
+
throw new Error(ExecutionOutput.formatToolText(sourceDiagnostic));
|
|
249
413
|
}
|
|
250
414
|
const activeBefore = deps.getActiveTools();
|
|
251
415
|
let persisted = false;
|
|
@@ -265,7 +429,7 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
265
429
|
(!activation.host_registered ||
|
|
266
430
|
!activation.active_tool ||
|
|
267
431
|
!activation.callable_now)) {
|
|
268
|
-
throw new Error(`
|
|
432
|
+
throw new Error(`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.`);
|
|
269
433
|
}
|
|
270
434
|
}
|
|
271
435
|
catch (error) {
|
|
@@ -292,27 +456,33 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
292
456
|
const warningText = templateWarnings.length > 0
|
|
293
457
|
? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
|
|
294
458
|
: "";
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
459
|
+
const callableNow = activation?.callable_now ?? false;
|
|
460
|
+
const source = requestedSource ?? "template";
|
|
461
|
+
const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
|
|
462
|
+
const nextActions = registrationNextActions(name, source, callableNow);
|
|
463
|
+
const outcomeText = callableNow
|
|
464
|
+
? `${existing ? "Updated" : "Registered"} tool "${name}" from ${source}; callable_now=true`
|
|
465
|
+
: `Persisted tool "${name}" from ${source}; callable_now=false; activation_boundary=${activationBoundary(activation)}`;
|
|
298
466
|
return {
|
|
299
467
|
content: [
|
|
300
|
-
textContent(ExecutionOutput.formatToolText(`${outcomeText} (
|
|
468
|
+
textContent(ExecutionOutput.formatToolText(`${outcomeText}; required=${Schema.formatToolArgs(argSummary.required)}; optional=${Schema.formatToolArgs(argSummary.optional)}; next=${nextActions[0]}.${warningText}`)),
|
|
301
469
|
],
|
|
302
470
|
details: {
|
|
303
471
|
active_tool: activation?.active_tool ?? false,
|
|
304
472
|
activation: activation?.activation ?? "unverified",
|
|
473
|
+
activation_boundary: activationBoundary(activation),
|
|
305
474
|
args: cfg.args,
|
|
306
|
-
callable_now:
|
|
307
|
-
config: recipePath,
|
|
475
|
+
callable_now: callableNow,
|
|
308
476
|
defaults: cfg.defaults,
|
|
309
477
|
host_registered: activation?.host_registered ?? false,
|
|
478
|
+
next_actions: nextActions,
|
|
479
|
+
optional_args: argSummary.optional,
|
|
310
480
|
persisted: true,
|
|
311
481
|
registry_active: tools.get(name) === cfg,
|
|
482
|
+
required_args: argSummary.required,
|
|
312
483
|
resolved: true,
|
|
484
|
+
source,
|
|
313
485
|
...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
|
|
314
|
-
...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
|
|
315
|
-
...(cfg.template ? { template: cfg.template } : {}),
|
|
316
486
|
...(templateWarnings.length > 0 ? { templateWarnings } : {}),
|
|
317
487
|
tool: name,
|
|
318
488
|
validated: true,
|
package/dist/lib/runtime.js
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, watch } from "node:fs";
|
|
|
7
7
|
import { basename, dirname } from "node:path";
|
|
8
8
|
import * as Paths from "./paths.js";
|
|
9
9
|
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
10
|
+
import * as RecipesReferences from "./recipes-references.js";
|
|
10
11
|
import * as RecipesUsage from "./recipes-usage.js";
|
|
11
12
|
import * as ToolsLocal from "./tools-local.js";
|
|
12
13
|
export function createAutoToolsRuntime(deps) {
|
|
@@ -40,6 +41,21 @@ export function createAutoToolsRuntime(deps) {
|
|
|
40
41
|
template: cfg.template,
|
|
41
42
|
});
|
|
42
43
|
}
|
|
44
|
+
function registeredToolSource(cfg) {
|
|
45
|
+
const raw = cfg.sourcePath
|
|
46
|
+
? RecipesReferences.readRawRecipeConfig(cfg.sourcePath)
|
|
47
|
+
: undefined;
|
|
48
|
+
const template = raw?.template;
|
|
49
|
+
if (typeof template === "string" &&
|
|
50
|
+
/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(template)) {
|
|
51
|
+
return template;
|
|
52
|
+
}
|
|
53
|
+
if (typeof template === "string" &&
|
|
54
|
+
(template.endsWith(".json") || template.endsWith(".md"))) {
|
|
55
|
+
return `<explicit-file:${basename(template)}>`;
|
|
56
|
+
}
|
|
57
|
+
return "template";
|
|
58
|
+
}
|
|
43
59
|
function deactivateMissingRuntimeTools(activeNames) {
|
|
44
60
|
const stale = [...runtimeTools].filter((name) => !activeNames.has(name));
|
|
45
61
|
if (stale.length === 0)
|
|
@@ -105,6 +121,7 @@ export function createAutoToolsRuntime(deps) {
|
|
|
105
121
|
lines.push("Other registry diagnostics:");
|
|
106
122
|
lines.push(...other.map((warning) => `• ${warning}`));
|
|
107
123
|
}
|
|
124
|
+
lines.push("Next: inspect target=recipes view=doctor");
|
|
108
125
|
return `${lines.join("\n")}\n`;
|
|
109
126
|
}
|
|
110
127
|
function loadTools(ctx, resolutionContext) {
|
|
@@ -164,9 +181,27 @@ export function createAutoToolsRuntime(deps) {
|
|
|
164
181
|
const usage = cfg.sourcePath
|
|
165
182
|
? RecipesUsage.readRecipeUsage(cfg.sourcePath)
|
|
166
183
|
: undefined;
|
|
184
|
+
const activation = activationFor(name);
|
|
185
|
+
const args = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
|
|
167
186
|
return {
|
|
168
|
-
...
|
|
169
|
-
|
|
187
|
+
...activation,
|
|
188
|
+
activation_boundary: activation.callable_now
|
|
189
|
+
? "current_session"
|
|
190
|
+
: !activation.host_registered
|
|
191
|
+
? "host_registration"
|
|
192
|
+
: "active_tool_set",
|
|
193
|
+
persisted: Boolean(cfg.sourcePath),
|
|
194
|
+
registry_active: true,
|
|
195
|
+
source: registeredToolSource(cfg),
|
|
196
|
+
required_args: args.required,
|
|
197
|
+
optional_args: args.optional,
|
|
198
|
+
next_actions: activation.callable_now
|
|
199
|
+
? [`call tool ${name}`]
|
|
200
|
+
: [
|
|
201
|
+
`register_tool name=${name} update=true`,
|
|
202
|
+
`inspect target=tool:${name} view=status`,
|
|
203
|
+
],
|
|
204
|
+
...(usage?.launch_kind ? { launch_kind: usage.launch_kind } : {}),
|
|
170
205
|
spawn_calls: Number(usage?.spawn_calls ?? 0),
|
|
171
206
|
tool_calls: Number(usage?.tool_calls ?? 0),
|
|
172
207
|
};
|