@llblab/pi-actors 0.46.0 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +582 -0
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/lib/async-runs.d.ts +1 -0
- package/dist/lib/async-runs.js +4 -1
- package/dist/lib/execution.d.ts +1 -0
- package/dist/lib/execution.js +1 -0
- package/dist/lib/extension-runtime.js +25 -9
- package/dist/lib/inspector.js +1 -0
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipes-context.d.ts +12 -3
- package/dist/lib/recipes-context.js +28 -4
- package/dist/lib/recipes-discovery.d.ts +18 -0
- package/dist/lib/recipes-discovery.js +95 -22
- package/dist/lib/recipes-references.d.ts +18 -0
- package/dist/lib/recipes-references.js +120 -33
- package/dist/lib/registry.d.ts +14 -1
- package/dist/lib/registry.js +110 -87
- package/dist/lib/runtime.d.ts +30 -3
- package/dist/lib/runtime.js +73 -9
- package/dist/lib/tools-inspect.d.ts +3 -0
- package/dist/lib/tools-inspect.js +40 -20
- package/dist/lib/tools-local.d.ts +2 -2
- package/dist/lib/tools-local.js +5 -2
- package/dist/lib/tools-response.js +2 -0
- package/dist/lib/tools-spawn.d.ts +2 -2
- package/dist/lib/tools-spawn.js +1 -1
- package/dist/lib/tools.d.ts +4 -1
- package/dist/lib/tools.js +2 -0
- package/dist/skills/actors/SKILL.md +3 -3
- package/docs/template-recipes.md +3 -1
- package/docs/tool-registry.md +9 -3
- package/lib/async-runs.ts +5 -1
- package/lib/execution.ts +2 -0
- package/lib/extension-runtime.ts +33 -14
- package/lib/inspector.ts +1 -0
- package/lib/prompts.ts +2 -2
- package/lib/recipes-context.ts +49 -4
- package/lib/recipes-discovery.ts +164 -24
- package/lib/recipes-references.ts +163 -39
- package/lib/registry.ts +185 -94
- package/lib/runtime.ts +108 -12
- package/lib/tools-inspect.ts +57 -23
- package/lib/tools-local.ts +9 -3
- package/lib/tools-response.ts +2 -0
- package/lib/tools-spawn.ts +3 -2
- package/lib/tools.ts +4 -1
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -3
|
@@ -11,7 +11,24 @@ import * as RecipeControl from "./recipe-control.js";
|
|
|
11
11
|
import * as Schema from "./schema.js";
|
|
12
12
|
const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
|
|
13
13
|
const MAX_RECIPE_IMPORT_DEPTH = 32;
|
|
14
|
-
const
|
|
14
|
+
const RESERVED_AUTHORED_RECIPE_VALUE_NAMES = new Set([
|
|
15
|
+
"recipe_dir",
|
|
16
|
+
"skill_dir",
|
|
17
|
+
]);
|
|
18
|
+
export const RUNTIME_OWNED_RECIPE_INPUT_NAMES = Object.freeze([
|
|
19
|
+
"owner_id",
|
|
20
|
+
"recipe_dir",
|
|
21
|
+
"run_instance_id",
|
|
22
|
+
"runtime_state_root",
|
|
23
|
+
"session_id",
|
|
24
|
+
"skill_dir",
|
|
25
|
+
"state_dir",
|
|
26
|
+
"trace_file",
|
|
27
|
+
]);
|
|
28
|
+
const RUNTIME_OWNED_RECIPE_INPUT_SET = new Set(RUNTIME_OWNED_RECIPE_INPUT_NAMES);
|
|
29
|
+
export function isRuntimeOwnedRecipeInput(name) {
|
|
30
|
+
return RUNTIME_OWNED_RECIPE_INPUT_SET.has(name);
|
|
31
|
+
}
|
|
15
32
|
export function createActiveSkillRecipeContext(skills) {
|
|
16
33
|
const rootsByName = new Map();
|
|
17
34
|
for (const skill of skills) {
|
|
@@ -49,19 +66,31 @@ function nestedRecipeFiles(root) {
|
|
|
49
66
|
return entry.isFile() && /\.(?:json|md)$/u.test(entry.name) ? [path] : [];
|
|
50
67
|
});
|
|
51
68
|
}
|
|
52
|
-
export function
|
|
69
|
+
export function inventoryActiveSkillRecipeComponents(context) {
|
|
53
70
|
const components = [];
|
|
71
|
+
const rejected = [];
|
|
54
72
|
for (const [skill, roots] of Object.entries(context.namespaces)) {
|
|
55
|
-
if (roots.length > 1)
|
|
56
|
-
|
|
73
|
+
if (roots.length > 1) {
|
|
74
|
+
rejected.push({
|
|
75
|
+
reason: `Duplicate active Skill identity ${skill}: ${roots.join(", ")}`,
|
|
76
|
+
skill,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
57
80
|
const root = roots[0];
|
|
58
81
|
if (!root || !existsSync(root))
|
|
59
82
|
continue;
|
|
60
83
|
const direct = readdirSync(root, { withFileTypes: true });
|
|
61
84
|
for (const entry of direct.filter((candidate) => candidate.isDirectory())) {
|
|
62
85
|
const nested = nestedRecipeFiles(join(root, entry.name));
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
for (const file of nested) {
|
|
87
|
+
rejected.push({
|
|
88
|
+
file,
|
|
89
|
+
reason: `Nested Skill Recipe file is not addressable: ${file}`,
|
|
90
|
+
skill,
|
|
91
|
+
stem: getRecipeIdFromPath(file),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
65
94
|
}
|
|
66
95
|
const files = direct
|
|
67
96
|
.filter((entry) => entry.isFile() && /\.(?:json|md)$/u.test(entry.name))
|
|
@@ -74,28 +103,58 @@ export function listActiveSkillRecipeComponents(context) {
|
|
|
74
103
|
byStem.set(stem, matches);
|
|
75
104
|
}
|
|
76
105
|
for (const [stem, matches] of [...byStem].sort(([a], [b]) => a.localeCompare(b))) {
|
|
77
|
-
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem))
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem)) {
|
|
107
|
+
rejected.push({
|
|
108
|
+
file: matches[0],
|
|
109
|
+
reason: `Invalid Skill Recipe filename stem: ${stem}`,
|
|
110
|
+
skill,
|
|
111
|
+
stem,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (matches.length > 1) {
|
|
116
|
+
rejected.push({
|
|
117
|
+
file: matches[0],
|
|
118
|
+
reason: `Skill Recipe stem collision: ${skill}/${stem} has both .json and .md files`,
|
|
119
|
+
skill,
|
|
120
|
+
stem,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
81
124
|
const file = matches[0];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
alias,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
try {
|
|
126
|
+
const raw = readRawRecipeConfig(file);
|
|
127
|
+
if (!raw || !Object.hasOwn(raw, "template"))
|
|
128
|
+
throw new Error(`Invalid Skill Recipe component: ${skill}/${stem}`);
|
|
129
|
+
assertRecipeHasNoDeclaredName(raw);
|
|
130
|
+
components.push({
|
|
131
|
+
file,
|
|
132
|
+
identity: `${skill}/${stem}`,
|
|
133
|
+
imports: Object.fromEntries(Object.entries(getRecipeImports(raw)).map(([alias, value]) => [
|
|
134
|
+
alias,
|
|
135
|
+
getImportFrom(value),
|
|
136
|
+
])),
|
|
137
|
+
skill,
|
|
138
|
+
stem,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
rejected.push({
|
|
143
|
+
file,
|
|
144
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
145
|
+
skill,
|
|
146
|
+
stem,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
96
149
|
}
|
|
97
150
|
}
|
|
98
|
-
return components;
|
|
151
|
+
return { components, partial: rejected.length > 0, rejected };
|
|
152
|
+
}
|
|
153
|
+
export function listActiveSkillRecipeComponents(context) {
|
|
154
|
+
const inventory = inventoryActiveSkillRecipeComponents(context);
|
|
155
|
+
if (inventory.rejected.length > 0)
|
|
156
|
+
throw new Error(inventory.rejected[0].reason);
|
|
157
|
+
return inventory.components;
|
|
99
158
|
}
|
|
100
159
|
const SKILL_RECIPE_REFERENCE = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
|
|
101
160
|
function qualifiedRecipeNameForFile(file, context) {
|
|
@@ -170,7 +229,7 @@ function assertRuntimeRecipeValuesNotDeclared(raw) {
|
|
|
170
229
|
const args = Array.isArray(raw.args)
|
|
171
230
|
? Schema.getExplicitToolArgNames(raw.args.map(String))
|
|
172
231
|
: [];
|
|
173
|
-
for (const name of
|
|
232
|
+
for (const name of RESERVED_AUTHORED_RECIPE_VALUE_NAMES) {
|
|
174
233
|
if (args.includes(name) ||
|
|
175
234
|
(isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
|
|
176
235
|
(isRecord(raw.values) && Object.hasOwn(raw.values, name))) {
|
|
@@ -731,6 +790,28 @@ function getDirectDelegatedRecipe(value, currentRecipeFile, stack, options = {})
|
|
|
731
790
|
}
|
|
732
791
|
return undefined;
|
|
733
792
|
}
|
|
793
|
+
function resolveDirectDelegationArgs(delegated, wrapperArgs, wrapperDefaults) {
|
|
794
|
+
if (!Array.isArray(delegated.args))
|
|
795
|
+
return Array.isArray(wrapperArgs) ? wrapperArgs.map(String) : undefined;
|
|
796
|
+
const delegatedSpec = Schema.parseToolArgDeclarationList(delegated.args);
|
|
797
|
+
if (delegatedSpec.error)
|
|
798
|
+
throw new Error(delegatedSpec.error);
|
|
799
|
+
const wrapperDeclarations = Array.isArray(wrapperArgs)
|
|
800
|
+
? wrapperArgs.map(String)
|
|
801
|
+
: [];
|
|
802
|
+
const wrapperSpec = Schema.parseToolArgDeclarationList(wrapperDeclarations);
|
|
803
|
+
if (wrapperSpec.error)
|
|
804
|
+
throw new Error(wrapperSpec.error);
|
|
805
|
+
const declared = new Set(delegatedSpec.args);
|
|
806
|
+
for (const arg of wrapperSpec.args) {
|
|
807
|
+
if (!declared.has(arg))
|
|
808
|
+
throw new Error(`Unknown delegated Recipe argument: ${arg}`);
|
|
809
|
+
}
|
|
810
|
+
Schema.assertCompatibleToolArgTypes(delegatedSpec.argTypes, wrapperSpec.argTypes);
|
|
811
|
+
assertRecipeDefaultsDeclared(delegated.args, wrapperDefaults, "delegated Recipe default");
|
|
812
|
+
Schema.normalizeRuntimeValues(Object.fromEntries(Object.entries(wrapperDefaults ?? {}).filter(([, value]) => typeof value !== "string" || !/\{[^{}]+\}/.test(value))), delegatedSpec.argTypes);
|
|
813
|
+
return delegated.args;
|
|
814
|
+
}
|
|
734
815
|
function expandImportNodes(value, imports, options = {}) {
|
|
735
816
|
if (typeof value === "string")
|
|
736
817
|
return value;
|
|
@@ -783,7 +864,9 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
783
864
|
if (stack.length >= MAX_RECIPE_IMPORT_DEPTH) {
|
|
784
865
|
throw new Error(`Recipe import depth exceeds limit ${MAX_RECIPE_IMPORT_DEPTH}: ${[...stack, path].join(" -> ")}`);
|
|
785
866
|
}
|
|
786
|
-
const raw =
|
|
867
|
+
const raw = options.authoredEntry?.path === path
|
|
868
|
+
? options.authoredEntry.recipe
|
|
869
|
+
: readRawRecipeConfig(path);
|
|
787
870
|
if (!raw || !Object.hasOwn(raw, "template"))
|
|
788
871
|
return undefined;
|
|
789
872
|
assertRecipeHasNoDeclaredName(raw);
|
|
@@ -844,7 +927,15 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
844
927
|
role: stack.length > 0 ? "import" : "entry",
|
|
845
928
|
})
|
|
846
929
|
: expandedTemplate;
|
|
847
|
-
const
|
|
930
|
+
const wrapperDefaults = isRecord(substituted.defaults)
|
|
931
|
+
? substituted.defaults
|
|
932
|
+
: undefined;
|
|
933
|
+
const mergedDefaults = mergeDefaults(delegated?.defaults, wrapperDefaults);
|
|
934
|
+
const effectiveArgs = delegated
|
|
935
|
+
? resolveDirectDelegationArgs(delegated, substituted.args, wrapperDefaults)
|
|
936
|
+
: Array.isArray(substituted.args)
|
|
937
|
+
? substituted.args
|
|
938
|
+
: undefined;
|
|
848
939
|
const artifactSource = isRecord(substituted.artifacts)
|
|
849
940
|
? substituted.artifacts
|
|
850
941
|
: delegated?.artifacts;
|
|
@@ -879,11 +970,7 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
879
970
|
? { imports: getRecipeImports(raw) }
|
|
880
971
|
: {}),
|
|
881
972
|
template: templateWithContext,
|
|
882
|
-
...(
|
|
883
|
-
? { args: substituted.args }
|
|
884
|
-
: Array.isArray(delegated?.args)
|
|
885
|
-
? { args: delegated.args }
|
|
886
|
-
: {}),
|
|
973
|
+
...(effectiveArgs ? { args: effectiveArgs } : {}),
|
|
887
974
|
...(mergedDefaults ? { defaults: mergedDefaults } : {}),
|
|
888
975
|
...(typeof substituted.parallel === "boolean"
|
|
889
976
|
? { parallel: substituted.parallel }
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -16,16 +16,24 @@ export interface RegisterToolInput {
|
|
|
16
16
|
values?: Record<string, unknown>;
|
|
17
17
|
}
|
|
18
18
|
export interface RegisterToolResultDetails {
|
|
19
|
+
active_tool?: boolean;
|
|
20
|
+
activation?: "current_session" | "unverified";
|
|
19
21
|
args?: string[];
|
|
20
22
|
async?: boolean;
|
|
23
|
+
callable_now?: boolean;
|
|
21
24
|
config?: string;
|
|
22
25
|
defaults?: Record<string, string>;
|
|
23
26
|
draft?: string;
|
|
27
|
+
host_registered?: boolean;
|
|
28
|
+
persisted?: boolean;
|
|
24
29
|
promoted?: boolean;
|
|
30
|
+
registry_active?: boolean;
|
|
31
|
+
resolved?: boolean;
|
|
25
32
|
recipeName?: string;
|
|
26
33
|
template?: CommandTemplates.CommandTemplateValue;
|
|
27
34
|
templateWarnings?: string[];
|
|
28
35
|
tool: string;
|
|
36
|
+
validated?: boolean;
|
|
29
37
|
}
|
|
30
38
|
export interface RegisterToolResult {
|
|
31
39
|
content: Array<{
|
|
@@ -41,7 +49,12 @@ export interface RegisterToolRuntimeDeps<TContext> {
|
|
|
41
49
|
getTools: () => Map<string, Config.RegisteredTool>;
|
|
42
50
|
getActiveTools: () => string[];
|
|
43
51
|
notify: (ctx: TContext, message: string, type: "info" | "warning" | "error") => void;
|
|
44
|
-
registerRuntimeTool: (cfg: Config.RegisteredTool) =>
|
|
52
|
+
registerRuntimeTool: (cfg: Config.RegisteredTool) => {
|
|
53
|
+
active_tool: boolean;
|
|
54
|
+
activation: "current_session" | "unverified";
|
|
55
|
+
callable_now: boolean;
|
|
56
|
+
host_registered: boolean;
|
|
57
|
+
} | void;
|
|
45
58
|
reservedToolNames: Set<string>;
|
|
46
59
|
setActiveTools: (toolNames: string[]) => void;
|
|
47
60
|
}
|
package/dist/lib/registry.js
CHANGED
|
@@ -8,9 +8,11 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
8
8
|
import * as CommandTemplates from "./command-templates.js";
|
|
9
9
|
import * as Config from "./config.js";
|
|
10
10
|
import * as ExecutionOutput from "./execution-output.js";
|
|
11
|
-
import { withFileMutationLock, writeJsonAtomic } from "./file-state.js";
|
|
11
|
+
import { withFileMutationLock, writeJsonAtomic, writeTextAtomic, } from "./file-state.js";
|
|
12
12
|
import * as Identity from "./identity.js";
|
|
13
13
|
import * as Paths from "./paths.js";
|
|
14
|
+
import * as RecipesContext from "./recipes-context.js";
|
|
15
|
+
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
14
16
|
import * as RecipesReferences from "./recipes-references.js";
|
|
15
17
|
import * as Schema from "./schema.js";
|
|
16
18
|
function textContent(text) {
|
|
@@ -58,21 +60,24 @@ function promoteDraftRecipe(name, input, ctx, deps) {
|
|
|
58
60
|
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
59
61
|
throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
|
|
60
62
|
}
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
+
const raw = RecipesReferences.readRawRecipeConfig(draftPath);
|
|
64
|
+
if (!raw) {
|
|
63
65
|
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
|
|
64
66
|
throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid${reason ? `: ${reason}` : "."}`));
|
|
65
67
|
}
|
|
66
|
-
const
|
|
68
|
+
const authoredRecipe = buildAuthoredRecipe({
|
|
67
69
|
...input,
|
|
68
|
-
description: input.description ??
|
|
69
|
-
|
|
70
|
-
}, existing);
|
|
71
|
-
const
|
|
70
|
+
description: input.description ??
|
|
71
|
+
(typeof raw.description === "string" ? raw.description : undefined),
|
|
72
|
+
}, existing, raw);
|
|
73
|
+
const resolutionContext = getRegistrationResolutionContext(ctx, deps);
|
|
74
|
+
const candidate = RecipesDiscovery.admitUserRecipe(targetPath, resolutionContext, authoredRecipe);
|
|
75
|
+
if (!candidate.validated || !candidate.tool) {
|
|
76
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid: ${candidate.diagnostics.join("; ")}`));
|
|
77
|
+
}
|
|
72
78
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
73
|
-
writeJsonAtomic(targetPath,
|
|
74
|
-
promoted
|
|
75
|
-
promoted.sourcePath = targetPath;
|
|
79
|
+
writeJsonAtomic(targetPath, authoredRecipe);
|
|
80
|
+
const promoted = RecipesDiscovery.admitUserRecipe(targetPath, resolutionContext).tool;
|
|
76
81
|
tools.set(name, promoted);
|
|
77
82
|
deps.registerRuntimeTool(promoted);
|
|
78
83
|
deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
|
|
@@ -89,17 +94,10 @@ function promoteDraftRecipe(name, input, ctx, deps) {
|
|
|
89
94
|
},
|
|
90
95
|
};
|
|
91
96
|
}
|
|
92
|
-
function persistToolRecipe(deps,
|
|
93
|
-
const path = getToolRecipePath(deps,
|
|
97
|
+
function persistToolRecipe(deps, name, recipe) {
|
|
98
|
+
const path = getToolRecipePath(deps, name);
|
|
94
99
|
mkdirSync(dirname(path), { recursive: true });
|
|
95
|
-
writeJsonAtomic(path,
|
|
96
|
-
description: cfg.description,
|
|
97
|
-
...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
|
|
98
|
-
...(cfg.storedArgs ? { args: cfg.storedArgs } : {}),
|
|
99
|
-
...(cfg.storedDefaults ? { defaults: cfg.storedDefaults } : {}),
|
|
100
|
-
...(cfg.recipe?.values ? { values: cfg.recipe.values } : {}),
|
|
101
|
-
template: cfg.template,
|
|
102
|
-
});
|
|
100
|
+
writeJsonAtomic(path, recipe);
|
|
103
101
|
return path;
|
|
104
102
|
}
|
|
105
103
|
function deleteTool(name, ctx, deps) {
|
|
@@ -130,11 +128,15 @@ function readAuthoritativeStoredTool(deps, name) {
|
|
|
130
128
|
if (!existsSync(path))
|
|
131
129
|
return { exists: false };
|
|
132
130
|
try {
|
|
133
|
-
const
|
|
131
|
+
const bytes = readFileSync(path, "utf8");
|
|
132
|
+
const recipe = JSON.parse(bytes);
|
|
133
|
+
const normalized = Config.normalizeStoredTool(name, recipe, deps.reservedToolNames);
|
|
134
134
|
if (!normalized.cfg)
|
|
135
|
-
return { exists: true };
|
|
135
|
+
return { bytes, exists: true, recipe };
|
|
136
136
|
return {
|
|
137
|
+
bytes,
|
|
137
138
|
exists: true,
|
|
139
|
+
recipe,
|
|
138
140
|
tool: { ...normalized.cfg, sourcePath: path },
|
|
139
141
|
};
|
|
140
142
|
}
|
|
@@ -161,7 +163,17 @@ function getInputTemplate(value) {
|
|
|
161
163
|
}
|
|
162
164
|
throw new Error(ExecutionOutput.formatToolText("Tool template must be a string, object, or sequence."));
|
|
163
165
|
}
|
|
164
|
-
function
|
|
166
|
+
function getRegistrationResolutionContext(ctx, deps) {
|
|
167
|
+
if (ctx &&
|
|
168
|
+
typeof ctx === "object" &&
|
|
169
|
+
"recipeResolutionContext" in ctx) {
|
|
170
|
+
const resolutionContext = ctx.recipeResolutionContext;
|
|
171
|
+
if (resolutionContext)
|
|
172
|
+
return resolutionContext;
|
|
173
|
+
}
|
|
174
|
+
return RecipesContext.createEmptyRecipeResolutionContext("offline-register-tool", getRecipeRoot(deps));
|
|
175
|
+
}
|
|
176
|
+
function buildAuthoredRecipe(input, existing, existingRecipe) {
|
|
165
177
|
const explicitArgs = input.args === undefined
|
|
166
178
|
? undefined
|
|
167
179
|
: Schema.parseToolArgDeclarations(input.args);
|
|
@@ -175,63 +187,29 @@ function buildConfig(name, input, existing) {
|
|
|
175
187
|
if (template === null) {
|
|
176
188
|
throw new Error(ExecutionOutput.formatToolText("Tool template cannot be null here."));
|
|
177
189
|
}
|
|
178
|
-
const finalTemplate = template === undefined || template === ""
|
|
190
|
+
const finalTemplate = template === undefined || template === ""
|
|
191
|
+
? existingRecipe?.template ?? existing?.template
|
|
192
|
+
: template;
|
|
179
193
|
if (!finalTemplate) {
|
|
180
194
|
throw new Error(ExecutionOutput.formatToolText("Tool template is required."));
|
|
181
195
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
? {
|
|
185
|
-
name: inputRecipe,
|
|
186
|
-
...(typeof input.async === "boolean" ? { async: input.async } : {}),
|
|
187
|
-
template: finalTemplate,
|
|
188
|
-
...(input.values && typeof input.values === "object"
|
|
189
|
-
? { values: input.values }
|
|
190
|
-
: {}),
|
|
191
|
-
}
|
|
192
|
-
: template === undefined
|
|
193
|
-
? existing?.recipe
|
|
194
|
-
: undefined;
|
|
195
|
-
const defaults = explicitArgs?.defaults ?? existing?.storedDefaults ?? {};
|
|
196
|
-
const storedArgs = explicitArgs
|
|
197
|
-
? explicitArgs.declarations
|
|
198
|
-
: existing?.storedArgs;
|
|
199
|
-
const storedDefaults = Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
200
|
-
const recipeTemplate = RecipesReferences.getRecipeTemplate(finalTemplate);
|
|
201
|
-
const argTemplate = recipeTemplate ?? finalTemplate;
|
|
202
|
-
const argTemplateConfig = typeof argTemplate === "object" && !Array.isArray(argTemplate)
|
|
203
|
-
? {
|
|
204
|
-
...argTemplate,
|
|
205
|
-
...(storedArgs !== undefined ? { args: storedArgs } : {}),
|
|
206
|
-
defaults: { ...(argTemplate.defaults ?? {}), ...defaults },
|
|
207
|
-
}
|
|
208
|
-
: {
|
|
209
|
-
args: storedArgs,
|
|
210
|
-
defaults,
|
|
211
|
-
template: argTemplate,
|
|
212
|
-
};
|
|
213
|
-
const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
214
|
-
const argTypes = {
|
|
215
|
-
...inferredArgTypes,
|
|
216
|
-
...(existing?.argTypes ?? {}),
|
|
217
|
-
...(explicitArgs?.argTypes ?? {}),
|
|
218
|
-
};
|
|
219
|
-
return {
|
|
220
|
-
name,
|
|
196
|
+
const authored = {
|
|
197
|
+
...(existingRecipe ?? {}),
|
|
221
198
|
description,
|
|
222
199
|
template: finalTemplate,
|
|
223
|
-
...(recipe ? { recipe } : {}),
|
|
224
|
-
args: RecipesReferences.isRecipeTool(finalTemplate, recipe) &&
|
|
225
|
-
storedArgs !== undefined
|
|
226
|
-
? Schema.getExplicitToolArgNames(storedArgs)
|
|
227
|
-
: RecipesReferences.isRecipeReference(finalTemplate) && !recipeTemplate
|
|
228
|
-
? Schema.getExplicitToolArgNames(storedArgs)
|
|
229
|
-
: Schema.getToolArgNames(argTemplateConfig),
|
|
230
|
-
defaults,
|
|
231
|
-
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
232
|
-
...(storedArgs !== undefined ? { storedArgs } : {}),
|
|
233
|
-
...(storedDefaults !== undefined ? { storedDefaults } : {}),
|
|
234
200
|
};
|
|
201
|
+
if (typeof input.async === "boolean")
|
|
202
|
+
authored.async = input.async;
|
|
203
|
+
if (explicitArgs) {
|
|
204
|
+
authored.args = explicitArgs.declarations;
|
|
205
|
+
if (Object.keys(explicitArgs.defaults).length > 0)
|
|
206
|
+
authored.defaults = explicitArgs.defaults;
|
|
207
|
+
else
|
|
208
|
+
delete authored.defaults;
|
|
209
|
+
}
|
|
210
|
+
if (input.values && typeof input.values === "object")
|
|
211
|
+
authored.values = input.values;
|
|
212
|
+
return authored;
|
|
235
213
|
}
|
|
236
214
|
function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
237
215
|
const input = params;
|
|
@@ -262,37 +240,82 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
262
240
|
if (template === undefined && !existing) {
|
|
263
241
|
throw new Error(ExecutionOutput.formatToolText("Tool template is required for new registrations."));
|
|
264
242
|
}
|
|
265
|
-
const
|
|
266
|
-
|
|
243
|
+
const recipePath = getToolRecipePath(deps, name);
|
|
244
|
+
const authoredRecipe = buildAuthoredRecipe(input, existing, authoritative.recipe);
|
|
245
|
+
const resolutionContext = getRegistrationResolutionContext(ctx, deps);
|
|
246
|
+
const candidate = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext, authoredRecipe);
|
|
247
|
+
if (!candidate.validated || !candidate.tool) {
|
|
248
|
+
throw new Error(ExecutionOutput.formatToolText(`Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`));
|
|
249
|
+
}
|
|
250
|
+
const activeBefore = deps.getActiveTools();
|
|
251
|
+
let persisted = false;
|
|
252
|
+
let activation;
|
|
253
|
+
let cfg;
|
|
267
254
|
try {
|
|
268
|
-
|
|
255
|
+
persistToolRecipe(deps, name, authoredRecipe);
|
|
256
|
+
persisted = true;
|
|
257
|
+
const admitted = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext);
|
|
258
|
+
if (!admitted.validated || !admitted.tool) {
|
|
259
|
+
throw new Error(`Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`);
|
|
260
|
+
}
|
|
261
|
+
cfg = admitted.tool;
|
|
262
|
+
tools.set(name, cfg);
|
|
263
|
+
activation = deps.registerRuntimeTool(cfg) ?? undefined;
|
|
264
|
+
if (activation &&
|
|
265
|
+
(!activation.host_registered ||
|
|
266
|
+
!activation.active_tool ||
|
|
267
|
+
!activation.callable_now)) {
|
|
268
|
+
throw new Error(`Host activation verification failed: ${JSON.stringify(activation)}`);
|
|
269
|
+
}
|
|
269
270
|
}
|
|
270
271
|
catch (error) {
|
|
271
|
-
|
|
272
|
+
if (persisted) {
|
|
273
|
+
if (authoritative.bytes !== undefined)
|
|
274
|
+
writeTextAtomic(recipePath, authoritative.bytes);
|
|
275
|
+
else if (existsSync(recipePath))
|
|
276
|
+
unlinkSync(recipePath);
|
|
277
|
+
}
|
|
278
|
+
if (existing) {
|
|
279
|
+
tools.set(name, existing);
|
|
280
|
+
deps.registerRuntimeTool(existing);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
tools.delete(name);
|
|
284
|
+
}
|
|
285
|
+
deps.setActiveTools(activeBefore);
|
|
286
|
+
throw new Error(ExecutionOutput.formatToolText(`Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
272
287
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const templateWarnings = CommandTemplates.getCommandTemplateWarnings(typeof cfg.template === "object" && !Array.isArray(cfg.template)
|
|
278
|
-
? cfg.template
|
|
279
|
-
: { template: cfg.template });
|
|
288
|
+
deps.notify(ctx, `Tool activated: ${name}`, "info");
|
|
289
|
+
const templateWarnings = CommandTemplates.getCommandTemplateWarnings(typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
|
|
290
|
+
? cfg.recipe.template
|
|
291
|
+
: { template: cfg.recipe?.template ?? cfg.template });
|
|
280
292
|
const warningText = templateWarnings.length > 0
|
|
281
293
|
? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
|
|
282
294
|
: "";
|
|
295
|
+
const outcomeText = activation?.callable_now
|
|
296
|
+
? `${existing ? "Updated" : "Registered"} and activated tool "${name}"`
|
|
297
|
+
: `Persisted tool "${name}"; current-session activation is unverified`;
|
|
283
298
|
return {
|
|
284
299
|
content: [
|
|
285
|
-
textContent(ExecutionOutput.formatToolText(`${
|
|
300
|
+
textContent(ExecutionOutput.formatToolText(`${outcomeText} (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`)),
|
|
286
301
|
],
|
|
287
302
|
details: {
|
|
303
|
+
active_tool: activation?.active_tool ?? false,
|
|
304
|
+
activation: activation?.activation ?? "unverified",
|
|
288
305
|
args: cfg.args,
|
|
306
|
+
callable_now: activation?.callable_now ?? false,
|
|
289
307
|
config: recipePath,
|
|
290
308
|
defaults: cfg.defaults,
|
|
309
|
+
host_registered: activation?.host_registered ?? false,
|
|
310
|
+
persisted: true,
|
|
311
|
+
registry_active: tools.get(name) === cfg,
|
|
312
|
+
resolved: true,
|
|
291
313
|
...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
|
|
292
314
|
...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
|
|
293
315
|
...(cfg.template ? { template: cfg.template } : {}),
|
|
294
316
|
...(templateWarnings.length > 0 ? { templateWarnings } : {}),
|
|
295
317
|
tool: name,
|
|
318
|
+
validated: true,
|
|
296
319
|
},
|
|
297
320
|
};
|
|
298
321
|
}
|
package/dist/lib/runtime.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { watch } from "node:fs";
|
|
7
7
|
import * as Config from "./config.ts";
|
|
8
8
|
import type { RegisteredToolExec } from "./execution.ts";
|
|
9
|
+
import type { RecipeResolutionContext } from "./recipes-context.ts";
|
|
9
10
|
import * as ToolsLocal from "./tools-local.ts";
|
|
10
11
|
export interface RuntimeContext {
|
|
11
12
|
hasUI: boolean;
|
|
@@ -13,21 +14,46 @@ export interface RuntimeContext {
|
|
|
13
14
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
export interface RuntimeToolActivation {
|
|
18
|
+
active_tool: boolean;
|
|
19
|
+
activation: "current_session" | "unverified";
|
|
20
|
+
callable_now: boolean;
|
|
21
|
+
host_registered: boolean;
|
|
22
|
+
}
|
|
16
23
|
export interface ToolRegistryRuntimeDeps {
|
|
17
24
|
configPath: string;
|
|
18
25
|
exec: RegisteredToolExec;
|
|
19
26
|
recipeRoot?: string;
|
|
20
27
|
getActiveTools?: () => string[];
|
|
28
|
+
getAllTools?: () => Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
}>;
|
|
21
31
|
registerTool: (definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>) => void;
|
|
22
32
|
reservedToolNames: Set<string>;
|
|
23
33
|
setActiveTools?: (toolNames: string[]) => void;
|
|
24
34
|
}
|
|
35
|
+
export interface RecipeRegistryStatus {
|
|
36
|
+
active_tool_count: number;
|
|
37
|
+
last_scan_counts: {
|
|
38
|
+
active: number;
|
|
39
|
+
rejected: number;
|
|
40
|
+
scanned: number;
|
|
41
|
+
};
|
|
42
|
+
last_scan_at?: string;
|
|
43
|
+
registry_generation: number;
|
|
44
|
+
resolution_generation?: string;
|
|
45
|
+
watch_status: "closed" | "failed" | "watching_parent" | "watching_root";
|
|
46
|
+
watched_root: string;
|
|
47
|
+
}
|
|
25
48
|
export interface ToolRegistryRuntime {
|
|
49
|
+
getStatus(): RecipeRegistryStatus;
|
|
50
|
+
getToolStatus(name: string): Record<string, unknown> | undefined;
|
|
26
51
|
getToolNameBlocker(name: string): string | undefined;
|
|
27
52
|
getTools(): Map<string, Config.RegisteredTool>;
|
|
28
|
-
loadTools(ctx: RuntimeContext): void;
|
|
53
|
+
loadTools(ctx: RuntimeContext, resolutionContext?: RecipeResolutionContext): void;
|
|
29
54
|
notify(ctx: RuntimeContext, message: string, type: "info" | "warning" | "error"): void;
|
|
30
|
-
registerRuntimeTool(cfg: Config.RegisteredTool):
|
|
55
|
+
registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation;
|
|
56
|
+
setWatchStatus(status: RecipeRegistryStatus["watch_status"]): void;
|
|
31
57
|
}
|
|
32
58
|
export interface RecipeToolReloadWatcher {
|
|
33
59
|
close(): void;
|
|
@@ -36,7 +62,8 @@ export interface RecipeToolReloadWatcher {
|
|
|
36
62
|
export declare function createAutoToolsRuntime(deps: ToolRegistryRuntimeDeps): ToolRegistryRuntime;
|
|
37
63
|
export interface RecipeToolReloadWatcherDeps {
|
|
38
64
|
exists?: (path: string) => boolean;
|
|
65
|
+
getResolutionContext?: () => RecipeResolutionContext | undefined;
|
|
39
66
|
recipeRoot?: string;
|
|
40
67
|
watchPath?: typeof watch;
|
|
41
68
|
}
|
|
42
|
-
export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools"
|
|
69
|
+
export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools"> & Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>, deps?: RecipeToolReloadWatcherDeps): RecipeToolReloadWatcher;
|