@introspection-ai/recipes 0.13.0 → 0.14.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/README.md +46 -57
- package/dist/{agent-tool.d.ts → agents.d.ts} +4 -1
- package/dist/agents.d.ts.map +1 -0
- package/dist/{agent-tool.js → agents.js} +1 -1
- package/dist/agents.js.map +1 -0
- package/dist/api/extensions.d.ts +3 -0
- package/dist/api/extensions.d.ts.map +1 -0
- package/dist/api/extensions.js +2 -0
- package/dist/api/extensions.js.map +1 -0
- package/dist/api/mcp.d.ts +4 -0
- package/dist/api/mcp.d.ts.map +1 -0
- package/dist/api/mcp.js +2 -0
- package/dist/api/mcp.js.map +1 -0
- package/dist/api/session.d.ts +3 -0
- package/dist/api/session.d.ts.map +1 -0
- package/dist/api/session.js +2 -0
- package/dist/api/session.js.map +1 -0
- package/dist/child-agent.d.ts +5 -4
- package/dist/child-agent.d.ts.map +1 -1
- package/dist/child-agent.js +26 -117
- package/dist/child-agent.js.map +1 -1
- package/dist/child-session.d.ts +23 -0
- package/dist/child-session.d.ts.map +1 -0
- package/dist/child-session.js +48 -0
- package/dist/child-session.js.map +1 -0
- package/dist/extensions.d.ts +29 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +119 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index.d.ts +4 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -13
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +38 -5
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +92 -41
- package/dist/inspect.js.map +1 -1
- package/dist/interactions.d.ts +18 -2
- package/dist/interactions.d.ts.map +1 -1
- package/dist/interactions.js +41 -10
- package/dist/interactions.js.map +1 -1
- package/dist/mcp-catalog.d.ts +1 -0
- package/dist/mcp-catalog.d.ts.map +1 -1
- package/dist/mcp-catalog.js +14 -6
- package/dist/mcp-catalog.js.map +1 -1
- package/dist/mcp-chunks/auth-command-ZLGYIEUY.js +273 -0
- package/dist/mcp-chunks/call-arguments-LU3W6D3K.js +13 -0
- package/dist/mcp-chunks/call-command-LKTKSYTH.js +612 -0
- package/dist/mcp-chunks/chunk-2U4BSDC4.js +3951 -0
- package/dist/mcp-chunks/chunk-2XL7MG7R.js +272 -0
- package/dist/mcp-chunks/chunk-3BJA27PY.js +2321 -0
- package/dist/mcp-chunks/chunk-4SQKWYFF.js +15 -0
- package/dist/mcp-chunks/chunk-5QOSCSQB.js +290 -0
- package/dist/mcp-chunks/chunk-6W5QFASN.js +77 -0
- package/dist/mcp-chunks/chunk-6XCZGU3G.js +1004 -0
- package/dist/mcp-chunks/chunk-7FHEBT5G.js +149 -0
- package/dist/mcp-chunks/chunk-7KA2VJVZ.js +114 -0
- package/dist/mcp-chunks/chunk-7UDVSOOF.js +138 -0
- package/dist/mcp-chunks/chunk-BNV3TPE4.js +66 -0
- package/dist/mcp-chunks/chunk-BTLAWHTA.js +129 -0
- package/dist/mcp-chunks/chunk-BV56DXPW.js +13 -0
- package/dist/mcp-chunks/chunk-BWHIA4PC.js +90 -0
- package/dist/mcp-chunks/chunk-BZIQOWE6.js +267 -0
- package/dist/mcp-chunks/chunk-D3MRAZ6H.js +16140 -0
- package/dist/mcp-chunks/chunk-DHBE7UBH.js +750 -0
- package/dist/mcp-chunks/chunk-E22DYOW5.js +251 -0
- package/dist/mcp-chunks/chunk-ECIVB7P4.js +139 -0
- package/dist/mcp-chunks/chunk-HKRJ6O36.js +1076 -0
- package/dist/mcp-chunks/chunk-IABRYROW.js +44 -0
- package/dist/mcp-chunks/chunk-J6LYIZXN.js +45 -0
- package/dist/mcp-chunks/chunk-L7NTQVYN.js +161 -0
- package/dist/mcp-chunks/chunk-LEZSACDE.js +141 -0
- package/dist/mcp-chunks/chunk-LKIBAYKU.js +208 -0
- package/dist/mcp-chunks/chunk-LNH5LAJD.js +152 -0
- package/dist/mcp-chunks/chunk-LQQEYD76.js +124 -0
- package/dist/mcp-chunks/chunk-LRKZP7DJ.js +47 -0
- package/dist/mcp-chunks/chunk-LS7PMWSR.js +109 -0
- package/dist/mcp-chunks/chunk-M3TPODXI.js +95 -0
- package/dist/mcp-chunks/chunk-MZ2NARG7.js +400 -0
- package/dist/mcp-chunks/chunk-N46NEFAF.js +45 -0
- package/dist/mcp-chunks/chunk-NNOEGEUZ.js +17273 -0
- package/dist/mcp-chunks/chunk-NRAJX5E4.js +69 -0
- package/dist/mcp-chunks/chunk-P3PSC54P.js +869 -0
- package/dist/mcp-chunks/chunk-PIIYJR44.js +6341 -0
- package/dist/mcp-chunks/chunk-PWYFKGKG.js +385 -0
- package/dist/mcp-chunks/chunk-SJCCBNZ4.js +45 -0
- package/dist/mcp-chunks/chunk-TATVT5IT.js +194 -0
- package/dist/mcp-chunks/chunk-TEUKFFQL.js +29 -0
- package/dist/mcp-chunks/chunk-UKYNQGNE.js +150 -0
- package/dist/mcp-chunks/chunk-UNKB4SBV.js +37 -0
- package/dist/mcp-chunks/chunk-WD6BOD24.js +138 -0
- package/dist/mcp-chunks/chunk-WQJZMCJN.js +73 -0
- package/dist/mcp-chunks/chunk-XANPGTLI.js +364 -0
- package/dist/mcp-chunks/chunk-XQK2JSW4.js +35 -0
- package/dist/mcp-chunks/chunk-YHQUTU6L.js +571 -0
- package/dist/mcp-chunks/chunk-YVZKMV5H.js +57 -0
- package/dist/mcp-chunks/cli-UV4QJZZM.js +929 -0
- package/dist/mcp-chunks/client-S7RQUG5U.js +13 -0
- package/dist/mcp-chunks/config-command-NLAGKID5.js +1101 -0
- package/dist/mcp-chunks/daemon-command-U242Z6VY.js +26 -0
- package/dist/mcp-chunks/dist-LK3SHAMX.js +6478 -0
- package/dist/mcp-chunks/emit-ts-command-VBXVXO6V.js +386 -0
- package/dist/mcp-chunks/generate-cli-runner-6CAZZA3P.js +1426 -0
- package/dist/mcp-chunks/inspect-cli-command-3ZORTOR7.js +111 -0
- package/dist/mcp-chunks/launch-YKRXHXJI.js +10 -0
- package/dist/mcp-chunks/lifecycle-XVKRHPH3.js +16 -0
- package/dist/mcp-chunks/list-command-2MEGGNFQ.js +4190 -0
- package/dist/mcp-chunks/output-utils-NKESLAJ6.js +12 -0
- package/dist/mcp-chunks/prompt-B1Yc1NPt-VGOTWJMN.js +918 -0
- package/dist/mcp-chunks/record-command-YXHTTTT5.js +17 -0
- package/dist/mcp-chunks/replay-command-25HJ6AQU.js +85 -0
- package/dist/mcp-chunks/resource-command-OO4NYF5C.js +97 -0
- package/dist/mcp-chunks/result-utils-MOHHLNM7.js +15 -0
- package/dist/mcp-chunks/runtime-5WJ3G25Y.js +33 -0
- package/dist/mcp-chunks/runtime-wrapper-RPWDIDBF.js +10 -0
- package/dist/mcp-chunks/serve-command-DHXJZMMN.js +3302 -0
- package/dist/mcp-chunks/timeouts-W2XWCCXR.js +18 -0
- package/dist/mcp-chunks/vault-command-PIYIUEZL.js +154 -0
- package/dist/mcp-daemon-client.d.ts +6 -1
- package/dist/mcp-daemon-client.d.ts.map +1 -1
- package/dist/mcp-daemon-client.js +119 -2
- package/dist/mcp-daemon-client.js.map +1 -1
- package/dist/mcp-daemon-protocol.d.ts +18 -1
- package/dist/mcp-daemon-protocol.d.ts.map +1 -1
- package/dist/mcp-daemon-protocol.js +31 -0
- package/dist/mcp-daemon-protocol.js.map +1 -1
- package/dist/mcp-daemon.js +169 -76602
- package/dist/mcp-daemon.js.map +1 -1
- package/dist/mcp-policy.d.ts +11 -0
- package/dist/mcp-policy.d.ts.map +1 -0
- package/dist/mcp-policy.js +27 -0
- package/dist/mcp-policy.js.map +1 -0
- package/dist/mcp-run-worker.js +38 -76540
- package/dist/mcp-tools.d.ts +39 -0
- package/dist/mcp-tools.d.ts.map +1 -0
- package/dist/mcp-tools.js +493 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/mcp.d.ts +21 -9
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +101 -31
- package/dist/mcp.js.map +1 -1
- package/dist/model-binding.d.ts +39 -0
- package/dist/model-binding.d.ts.map +1 -0
- package/dist/model-binding.js +103 -0
- package/dist/model-binding.js.map +1 -0
- package/dist/pi-extension.d.ts +2 -22
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +296 -71
- package/dist/pi-extension.js.map +1 -1
- package/dist/recipe/resolve.d.ts +43 -20
- package/dist/recipe/resolve.d.ts.map +1 -1
- package/dist/recipe/resolve.js +215 -70
- package/dist/recipe/resolve.js.map +1 -1
- package/dist/recipe-agent.d.ts +24 -27
- package/dist/recipe-agent.d.ts.map +1 -1
- package/dist/recipe-agent.js +278 -230
- package/dist/recipe-agent.js.map +1 -1
- package/dist/recipe-check.d.ts +18 -0
- package/dist/recipe-check.d.ts.map +1 -0
- package/dist/recipe-check.js +261 -0
- package/dist/recipe-check.js.map +1 -0
- package/dist/recipe-extensions.d.ts.map +1 -1
- package/dist/recipe-extensions.js +0 -3
- package/dist/recipe-extensions.js.map +1 -1
- package/dist/recipe-model.d.ts +3 -1
- package/dist/recipe-model.d.ts.map +1 -1
- package/dist/recipe-model.js +15 -12
- package/dist/recipe-model.js.map +1 -1
- package/dist/recipe-package.d.ts +6 -28
- package/dist/recipe-package.d.ts.map +1 -1
- package/dist/recipe-package.js +248 -191
- package/dist/recipe-package.js.map +1 -1
- package/dist/recipe-skills.d.ts.map +1 -1
- package/dist/recipe-skills.js +42 -29
- package/dist/recipe-skills.js.map +1 -1
- package/dist/run-controller.d.ts +10 -9
- package/dist/run-controller.d.ts.map +1 -1
- package/dist/run-controller.js +28 -21
- package/dist/run-controller.js.map +1 -1
- package/dist/session.d.ts +40 -53
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +312 -173
- package/dist/session.js.map +1 -1
- package/dist/test-utils.d.ts +7 -7
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +32 -15
- package/dist/test-utils.js.map +1 -1
- package/docs/agent-composition.md +41 -35
- package/docs/host-api.md +201 -0
- package/docs/index.md +9 -14
- package/docs/interactions.md +12 -11
- package/docs/mcp-auth.md +6 -6
- package/docs/mcp-configuration.md +73 -25
- package/docs/pi-extension.md +39 -11
- package/docs/recipe-flow.md +9 -9
- package/docs/recipe-format.md +106 -41
- package/docs/recipe-judges.md +24 -36
- package/package.json +23 -17
- package/vendor/mcp-client/darwin-arm64/mcp-client +0 -0
- package/vendor/mcp-client/darwin-x64/mcp-client +0 -0
- package/vendor/mcp-client/linux-arm64/mcp-client +0 -0
- package/vendor/recipe-check/darwin-arm64/recipe-check +0 -0
- package/vendor/recipe-check/darwin-x64/recipe-check +0 -0
- package/vendor/recipe-check/linux-arm64/recipe-check +0 -0
- package/vendor/recipe-check/linux-x64/recipe-check +0 -0
- package/vendor/recipe-check/linux-x64-musl/recipe-check +0 -0
- package/vendor/recipe-check/win32-x64/recipe-check.exe +0 -0
- package/dist/agent-tool.d.ts.map +0 -1
- package/dist/agent-tool.js.map +0 -1
- package/dist/run.d.ts +0 -28
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -84
- package/dist/run.js.map +0 -1
- package/dist/testing.d.ts +0 -25
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -96
- package/dist/testing.js.map +0 -1
- package/docs/deployment-configuration.md +0 -65
- package/docs/migration.md +0 -70
- package/docs/python-bindings-release.md +0 -47
- package/docs/recipe-evals.md +0 -70
- package/docs/runtime-library.md +0 -171
package/dist/recipe-agent.js
CHANGED
|
@@ -2,15 +2,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
4
|
import { mergeRecipeAgentModelConfig, parseRecipeAgentModelConfig, RecipeModelConfigError, } from "./recipe-model.js";
|
|
5
|
-
import { isValidRecipeMcpToolSelection, packageResourcePaths, parseRecipeMcpToolSelection, readPiPackageManifest, RecipePackageError, } from "./recipe-package.js";
|
|
6
|
-
import { mcpSelectionAllowsTool, normalizeMcpServerId, } from "./mcp.js";
|
|
5
|
+
import { assertRecipePathContained, isValidRecipeMcpToolSelection, packageResourcePaths, parseRecipeMcpToolSelection, readPiPackageManifest, RecipePackageError, } from "./recipe-package.js";
|
|
6
|
+
import { mcpSelectionAllowsTool, normalizeMcpServerId, } from "./mcp-policy.js";
|
|
7
7
|
const INVALID_AGENT_MCP = Symbol("invalidAgentMcp");
|
|
8
|
-
export const REQUIRED_RECIPE_AGENT_FIELDS = [
|
|
9
|
-
"model.name",
|
|
10
|
-
"model.thinkingLevel",
|
|
11
|
-
"tools",
|
|
12
|
-
"systemInstructions",
|
|
13
|
-
];
|
|
14
8
|
const AGENT_YAML_KEYS = new Set([
|
|
15
9
|
"name",
|
|
16
10
|
"from",
|
|
@@ -20,11 +14,10 @@ const AGENT_YAML_KEYS = new Set([
|
|
|
20
14
|
"mcp",
|
|
21
15
|
"skills",
|
|
22
16
|
"subagents",
|
|
23
|
-
"extensions",
|
|
24
17
|
"system_instructions",
|
|
25
|
-
"systemInstructions",
|
|
26
|
-
"prompt",
|
|
27
18
|
]);
|
|
19
|
+
const PORTABLE_AGENT_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
20
|
+
const MAX_INHERITANCE_DEPTH = 128;
|
|
28
21
|
function asRecord(value) {
|
|
29
22
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
30
23
|
? value
|
|
@@ -32,9 +25,70 @@ function asRecord(value) {
|
|
|
32
25
|
}
|
|
33
26
|
function stringArray(value) {
|
|
34
27
|
return Array.isArray(value)
|
|
35
|
-
? value
|
|
28
|
+
? value
|
|
29
|
+
.filter((item) => typeof item === "string")
|
|
30
|
+
.map((item) => item.trim())
|
|
36
31
|
: [];
|
|
37
32
|
}
|
|
33
|
+
function isNonEmptyStringArray(value) {
|
|
34
|
+
return (Array.isArray(value) &&
|
|
35
|
+
value.every((item) => typeof item === "string" && Boolean(item.trim())));
|
|
36
|
+
}
|
|
37
|
+
function invalidAgentField(data) {
|
|
38
|
+
if (!Object.hasOwn(data, "name")) {
|
|
39
|
+
return "name is required";
|
|
40
|
+
}
|
|
41
|
+
for (const key of ["name", "from"]) {
|
|
42
|
+
if (Object.hasOwn(data, key) &&
|
|
43
|
+
(typeof data[key] !== "string" || !data[key].trim())) {
|
|
44
|
+
return `${key} must be a non-empty string`;
|
|
45
|
+
}
|
|
46
|
+
if (Object.hasOwn(data, key) &&
|
|
47
|
+
typeof data[key] === "string" &&
|
|
48
|
+
!PORTABLE_AGENT_NAME.test(data[key])) {
|
|
49
|
+
return `${key} must use lowercase kebab-case`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (Object.hasOwn(data, "description") &&
|
|
53
|
+
typeof data.description !== "string") {
|
|
54
|
+
return "description must be a string";
|
|
55
|
+
}
|
|
56
|
+
for (const key of ["tools", "skills", "subagents"]) {
|
|
57
|
+
if (Object.hasOwn(data, key) && !isNonEmptyStringArray(data[key])) {
|
|
58
|
+
return `${key} must be an array of non-empty strings`;
|
|
59
|
+
}
|
|
60
|
+
if (Object.hasOwn(data, key) &&
|
|
61
|
+
new Set(stringArray(data[key])).size !== stringArray(data[key]).length) {
|
|
62
|
+
return `${key} must not contain duplicate entries`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (stringArray(data.tools).includes("agent")) {
|
|
66
|
+
return "tools must not declare the session-generated agent tool";
|
|
67
|
+
}
|
|
68
|
+
if (Object.hasOwn(data, "system_instructions")) {
|
|
69
|
+
const value = data.system_instructions;
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
71
|
+
return "system_instructions must be an object";
|
|
72
|
+
}
|
|
73
|
+
const instructions = value;
|
|
74
|
+
const unknown = Object.keys(instructions).filter((key) => key !== "mode" && key !== "content");
|
|
75
|
+
if (unknown.length > 0) {
|
|
76
|
+
return `system_instructions has unsupported key(s): ${unknown.join(", ")}`;
|
|
77
|
+
}
|
|
78
|
+
if (typeof instructions.content !== "string") {
|
|
79
|
+
return "system_instructions.content must be a string";
|
|
80
|
+
}
|
|
81
|
+
if (!instructions.content.trim()) {
|
|
82
|
+
return "system_instructions.content must be non-empty";
|
|
83
|
+
}
|
|
84
|
+
if (Object.hasOwn(instructions, "mode") &&
|
|
85
|
+
instructions.mode !== "append" &&
|
|
86
|
+
instructions.mode !== "replace") {
|
|
87
|
+
return "system_instructions.mode must be append or replace";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
38
92
|
function modelProjection(config) {
|
|
39
93
|
if (!config)
|
|
40
94
|
return undefined;
|
|
@@ -46,22 +100,48 @@ function modelProjection(config) {
|
|
|
46
100
|
};
|
|
47
101
|
}
|
|
48
102
|
function parseSystemInstructions(data) {
|
|
49
|
-
const raw = asRecord(data.system_instructions
|
|
103
|
+
const raw = asRecord(data.system_instructions);
|
|
50
104
|
if (Object.hasOwn(raw, "content") && typeof raw.content === "string") {
|
|
51
105
|
const mode = raw.mode === "replace" ? "replace" : "append";
|
|
52
106
|
return { mode, content: raw.content.trim() };
|
|
53
107
|
}
|
|
54
|
-
|
|
55
|
-
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function mergeSystemInstructions(base, child) {
|
|
111
|
+
if (!base)
|
|
112
|
+
return child;
|
|
113
|
+
if (!child)
|
|
114
|
+
return base;
|
|
115
|
+
if (child.mode === "replace")
|
|
116
|
+
return child;
|
|
117
|
+
return {
|
|
118
|
+
mode: base.mode,
|
|
119
|
+
content: `${base.content}\n\n${child.content}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function normalizedMcpServerId(value) {
|
|
123
|
+
return (value
|
|
124
|
+
.trim()
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
127
|
+
.replace(/^-+|-+$/g, "") || "mcp");
|
|
128
|
+
}
|
|
129
|
+
function hasNormalizedMcpServerCollision(values) {
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
for (const value of values) {
|
|
132
|
+
const normalized = normalizedMcpServerId(value);
|
|
133
|
+
if (seen.has(normalized))
|
|
134
|
+
return true;
|
|
135
|
+
seen.add(normalized);
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
56
138
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
extensions.exclude = stringArray(raw.exclude);
|
|
64
|
-
return extensions.include || extensions.exclude ? extensions : undefined;
|
|
139
|
+
function validActivationSelectors(selectors) {
|
|
140
|
+
if (selectors === undefined)
|
|
141
|
+
return true;
|
|
142
|
+
return (selectors.every((selector) => Boolean(selector.trim()) &&
|
|
143
|
+
(!selector.includes("*") || selector === "*")) &&
|
|
144
|
+
(!selectors.includes("*") || selectors.length === 1));
|
|
65
145
|
}
|
|
66
146
|
function parseMcp(data) {
|
|
67
147
|
if (!Object.hasOwn(data, "mcp"))
|
|
@@ -69,11 +149,67 @@ function parseMcp(data) {
|
|
|
69
149
|
const value = data.mcp;
|
|
70
150
|
const validObject = Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
71
151
|
const raw = asRecord(value);
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
152
|
+
const rawServers = asRecord(raw.servers);
|
|
153
|
+
const servers = {};
|
|
154
|
+
for (const [serverId, serverValue] of Object.entries(rawServers)) {
|
|
155
|
+
const serverRaw = asRecord(serverValue);
|
|
156
|
+
servers[normalizedMcpServerId(serverId)] = {
|
|
157
|
+
...parseRecipeMcpToolSelection(serverValue),
|
|
158
|
+
...(Object.hasOwn(serverRaw, "defer")
|
|
159
|
+
? {
|
|
160
|
+
defer: stringArray(serverRaw.defer).map((selector) => selector.trim()),
|
|
161
|
+
}
|
|
162
|
+
: {}),
|
|
163
|
+
...(Object.hasOwn(serverRaw, "eager")
|
|
164
|
+
? {
|
|
165
|
+
eager: stringArray(serverRaw.eager).map((selector) => selector.trim()),
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
};
|
|
75
169
|
}
|
|
76
|
-
|
|
170
|
+
const mode = raw.mode === "cli" || raw.mode === "tools"
|
|
171
|
+
? raw.mode
|
|
172
|
+
: undefined;
|
|
173
|
+
const mcp = {
|
|
174
|
+
...(mode ? { mode } : {}),
|
|
175
|
+
servers,
|
|
176
|
+
};
|
|
177
|
+
const malformedServers = Object.entries(rawServers).some(([, serverValue]) => {
|
|
178
|
+
const server = asRecord(serverValue);
|
|
179
|
+
return (!serverValue ||
|
|
180
|
+
typeof serverValue !== "object" ||
|
|
181
|
+
Array.isArray(serverValue) ||
|
|
182
|
+
Object.keys(server).some((key) => !["include", "exclude", "defer", "eager"].includes(key)) ||
|
|
183
|
+
["include", "exclude", "defer", "eager"].some((key) => {
|
|
184
|
+
if (!Object.hasOwn(server, key))
|
|
185
|
+
return false;
|
|
186
|
+
const values = stringArray(server[key]);
|
|
187
|
+
return new Set(values).size !== values.length;
|
|
188
|
+
}) ||
|
|
189
|
+
["defer", "eager"].some((key) => Object.hasOwn(server, key) &&
|
|
190
|
+
(!Array.isArray(server[key]) ||
|
|
191
|
+
server[key].some((selector) => typeof selector !== "string"))) ||
|
|
192
|
+
!validActivationSelectors(Object.hasOwn(server, "defer")
|
|
193
|
+
? stringArray(server.defer).map((selector) => selector.trim())
|
|
194
|
+
: undefined) ||
|
|
195
|
+
!validActivationSelectors(Object.hasOwn(server, "eager")
|
|
196
|
+
? stringArray(server.eager).map((selector) => selector.trim())
|
|
197
|
+
: undefined) ||
|
|
198
|
+
(mode === "cli" &&
|
|
199
|
+
(Object.hasOwn(server, "defer") || Object.hasOwn(server, "eager"))));
|
|
200
|
+
});
|
|
201
|
+
const malformedStructured = malformedServers ||
|
|
202
|
+
!Object.hasOwn(raw, "servers") ||
|
|
203
|
+
(Object.hasOwn(raw, "mode") &&
|
|
204
|
+
raw.mode !== "cli" &&
|
|
205
|
+
raw.mode !== "tools") ||
|
|
206
|
+
Object.keys(rawServers).some((serverId) => !serverId.trim()) ||
|
|
207
|
+
hasNormalizedMcpServerCollision(Object.keys(rawServers)) ||
|
|
208
|
+
!raw.servers ||
|
|
209
|
+
typeof raw.servers !== "object" ||
|
|
210
|
+
Array.isArray(raw.servers) ||
|
|
211
|
+
Object.keys(raw).some((key) => !["mode", "servers"].includes(key));
|
|
212
|
+
if (!validObject || malformedStructured) {
|
|
77
213
|
Object.defineProperty(mcp, INVALID_AGENT_MCP, {
|
|
78
214
|
value: true,
|
|
79
215
|
enumerable: false,
|
|
@@ -86,17 +222,9 @@ function mergeMcp(base, child) {
|
|
|
86
222
|
return child;
|
|
87
223
|
if (!child)
|
|
88
224
|
return base;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
merged[serverId] = {
|
|
93
|
-
...(baseServer?.include !== undefined ? { include: baseServer.include } : {}),
|
|
94
|
-
...(baseServer?.exclude !== undefined ? { exclude: baseServer.exclude } : {}),
|
|
95
|
-
...(childServer.include !== undefined ? { include: childServer.include } : {}),
|
|
96
|
-
...(childServer.exclude !== undefined ? { exclude: childServer.exclude } : {}),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
return merged;
|
|
225
|
+
// Capability policy is a review boundary: once a child declares `mcp`, the
|
|
226
|
+
// whole block replaces its base instead of silently retaining parent servers.
|
|
227
|
+
return child;
|
|
100
228
|
}
|
|
101
229
|
function readYaml(path) {
|
|
102
230
|
return asRecord(parse(readFileSync(path, "utf8")));
|
|
@@ -141,14 +269,17 @@ function readRecipeAgentSources(recipeDir, opts = {}) {
|
|
|
141
269
|
const sources = [];
|
|
142
270
|
for (const path of recipeAgentFiles(recipeDir)) {
|
|
143
271
|
const data = readYaml(path);
|
|
144
|
-
const fallbackName = basename(path).replace(/\.ya?ml$/i, "");
|
|
145
|
-
const explicitName = typeof data.name === "string" && Boolean(data.name.trim());
|
|
146
|
-
const name = explicitName ? data.name.trim() : fallbackName;
|
|
147
272
|
const unknownKeys = Object.keys(data).filter((key) => !AGENT_YAML_KEYS.has(key));
|
|
148
273
|
if (unknownKeys.length > 0) {
|
|
149
274
|
opts.onInvalidFile?.(path, new Error(`Agent YAML at ${path} has unsupported key(s): ${unknownKeys.join(", ")}`));
|
|
150
275
|
continue;
|
|
151
276
|
}
|
|
277
|
+
const fieldError = invalidAgentField(data);
|
|
278
|
+
if (fieldError) {
|
|
279
|
+
opts.onInvalidFile?.(path, new Error(`Agent YAML at ${path}: ${fieldError}`));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const name = data.name;
|
|
152
283
|
let modelConfig;
|
|
153
284
|
try {
|
|
154
285
|
modelConfig = parseRecipeAgentModelConfig(`Agent YAML at ${path}`, data.model);
|
|
@@ -160,11 +291,9 @@ function readRecipeAgentSources(recipeDir, opts = {}) {
|
|
|
160
291
|
continue;
|
|
161
292
|
}
|
|
162
293
|
sources.push({
|
|
163
|
-
fallbackName,
|
|
164
|
-
explicitName,
|
|
165
294
|
definition: {
|
|
166
295
|
name,
|
|
167
|
-
from: typeof data.from === "string"
|
|
296
|
+
from: typeof data.from === "string" ? data.from : undefined,
|
|
168
297
|
description: typeof data.description === "string" ? data.description : undefined,
|
|
169
298
|
model: modelProjection(modelConfig),
|
|
170
299
|
modelConfig,
|
|
@@ -172,53 +301,40 @@ function readRecipeAgentSources(recipeDir, opts = {}) {
|
|
|
172
301
|
mcp: parseMcp(data),
|
|
173
302
|
skills: Object.hasOwn(data, "skills") ? stringArray(data.skills) : undefined,
|
|
174
303
|
subagents: Object.hasOwn(data, "subagents") ? stringArray(data.subagents) : undefined,
|
|
175
|
-
extensions: parseExtensions(data),
|
|
176
304
|
systemInstructions: parseSystemInstructions(data),
|
|
305
|
+
declaredFields: [
|
|
306
|
+
"description",
|
|
307
|
+
"model",
|
|
308
|
+
"tools",
|
|
309
|
+
"mcp",
|
|
310
|
+
"skills",
|
|
311
|
+
"subagents",
|
|
312
|
+
"system_instructions",
|
|
313
|
+
].filter((key) => Object.hasOwn(data, key)),
|
|
177
314
|
},
|
|
178
315
|
});
|
|
179
316
|
}
|
|
180
317
|
return sources;
|
|
181
318
|
}
|
|
182
|
-
|
|
319
|
+
function definitionsFromSources(sources) {
|
|
183
320
|
const rawDefinitions = new Map();
|
|
184
|
-
const aliases = new Map();
|
|
185
321
|
const resolvedDefinitions = new Map();
|
|
186
322
|
const definitions = new Map();
|
|
187
|
-
const sources = readRecipeAgentSources(recipeDir, {
|
|
188
|
-
onInvalidFile: (path, error) => {
|
|
189
|
-
console.warn(`[recipes] skipping ${path}: ${error.message}`);
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
323
|
for (const source of sources) {
|
|
193
324
|
rawDefinitions.set(source.definition.name, source.definition);
|
|
194
|
-
aliases.set(source.fallbackName, source.definition.name);
|
|
195
|
-
}
|
|
196
|
-
function resolveName(name) {
|
|
197
|
-
return rawDefinitions.has(name) ? name : aliases.get(name) ?? name;
|
|
198
|
-
}
|
|
199
|
-
function mergeExtensions(base, child) {
|
|
200
|
-
if (!base)
|
|
201
|
-
return child;
|
|
202
|
-
if (!child)
|
|
203
|
-
return base;
|
|
204
|
-
return {
|
|
205
|
-
...(base.include ? { include: base.include } : {}),
|
|
206
|
-
...(base.exclude ? { exclude: base.exclude } : {}),
|
|
207
|
-
...(child.include ? { include: child.include } : {}),
|
|
208
|
-
...(child.exclude ? { exclude: child.exclude } : {}),
|
|
209
|
-
};
|
|
210
325
|
}
|
|
211
326
|
function resolveDefinition(name, stack = []) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (stack.includes(resolvedName))
|
|
327
|
+
if (resolvedDefinitions.has(name))
|
|
328
|
+
return resolvedDefinitions.get(name);
|
|
329
|
+
if (stack.length >= MAX_INHERITANCE_DEPTH)
|
|
216
330
|
return undefined;
|
|
217
|
-
|
|
331
|
+
if (stack.includes(name))
|
|
332
|
+
return undefined;
|
|
333
|
+
const raw = rawDefinitions.get(name);
|
|
218
334
|
if (!raw)
|
|
219
335
|
return undefined;
|
|
220
336
|
const base = raw.from
|
|
221
|
-
? resolveDefinition(raw.from, [...stack,
|
|
337
|
+
? resolveDefinition(raw.from, [...stack, name])
|
|
222
338
|
: undefined;
|
|
223
339
|
if (raw.from && !base)
|
|
224
340
|
return undefined;
|
|
@@ -233,183 +349,128 @@ export function loadRecipeAgentDefinitions(recipeDir) {
|
|
|
233
349
|
mcp: mergeMcp(base?.mcp, raw.mcp),
|
|
234
350
|
skills: raw.skills ?? base?.skills ?? [],
|
|
235
351
|
subagents: raw.subagents ?? base?.subagents ?? [],
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
extensions: mergeExtensions(base?.extensions, raw.extensions),
|
|
239
|
-
systemInstructions: raw.systemInstructions ?? base?.systemInstructions,
|
|
352
|
+
systemInstructions: mergeSystemInstructions(base?.systemInstructions, raw.systemInstructions),
|
|
353
|
+
declaredFields: [...(raw.declaredFields ?? [])],
|
|
240
354
|
};
|
|
241
|
-
resolvedDefinitions.set(
|
|
355
|
+
resolvedDefinitions.set(name, definition);
|
|
242
356
|
return definition;
|
|
243
357
|
}
|
|
244
358
|
for (const name of rawDefinitions.keys()) {
|
|
245
359
|
const definition = resolveDefinition(name);
|
|
246
|
-
if (!definition)
|
|
247
|
-
console.warn(`[recipes] skipping agent "${name}": ${fromChainSkipReason(name, rawDefinitions, resolveName)}`);
|
|
360
|
+
if (!definition)
|
|
248
361
|
continue;
|
|
249
|
-
}
|
|
250
362
|
definitions.set(name, definition);
|
|
251
363
|
}
|
|
252
|
-
for (const [alias, name] of aliases) {
|
|
253
|
-
if (definitions.has(alias))
|
|
254
|
-
continue;
|
|
255
|
-
const definition = definitions.get(name);
|
|
256
|
-
if (definition)
|
|
257
|
-
definitions.set(alias, definition);
|
|
258
|
-
}
|
|
259
364
|
return definitions;
|
|
260
365
|
}
|
|
261
|
-
|
|
262
|
-
function fromChainSkipReason(name, rawDefinitions, resolveName) {
|
|
263
|
-
const stack = [];
|
|
264
|
-
let current = resolveName(name);
|
|
265
|
-
for (;;) {
|
|
266
|
-
const raw = rawDefinitions.get(current);
|
|
267
|
-
if (!raw?.from)
|
|
268
|
-
return "from: chain failed to resolve";
|
|
269
|
-
const next = resolveName(raw.from);
|
|
270
|
-
if (!rawDefinitions.has(next)) {
|
|
271
|
-
return `from: "${raw.from}" does not exist in the recipe`;
|
|
272
|
-
}
|
|
273
|
-
if (stack.includes(next) || next === current) {
|
|
274
|
-
return `from: cycle detected (${[...stack, current, next].join(" -> ")})`;
|
|
275
|
-
}
|
|
276
|
-
stack.push(current);
|
|
277
|
-
current = next;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function recipeAgentFieldProvided(definition, field) {
|
|
281
|
-
if (field === "model.name")
|
|
282
|
-
return Boolean(definition.model?.name);
|
|
283
|
-
if (field === "model.thinkingLevel")
|
|
284
|
-
return Boolean(definition.model?.thinkingLevel);
|
|
285
|
-
if (field === "tools")
|
|
286
|
-
return definition.tools !== undefined;
|
|
287
|
-
if (field === "skills")
|
|
288
|
-
return definition.skills !== undefined;
|
|
289
|
-
if (field === "subagents")
|
|
290
|
-
return definition.subagents !== undefined;
|
|
291
|
-
return definition.systemInstructions !== undefined;
|
|
292
|
-
}
|
|
293
|
-
export function validateResolvedRecipeAgentDefinition(opts) {
|
|
366
|
+
function validateResolvedRecipeAgentSource(opts, sources) {
|
|
294
367
|
const rawDefinitions = new Map();
|
|
295
|
-
const
|
|
296
|
-
const explicitNames = new Map();
|
|
297
|
-
for (const source of readRecipeAgentSources(opts.recipeDir)) {
|
|
368
|
+
for (const source of sources) {
|
|
298
369
|
rawDefinitions.set(source.definition.name, source.definition);
|
|
299
|
-
aliases.set(source.fallbackName, source.definition.name);
|
|
300
|
-
explicitNames.set(source.definition.name, source.explicitName);
|
|
301
|
-
}
|
|
302
|
-
function resolveName(name) {
|
|
303
|
-
return rawDefinitions.has(name) ? name : aliases.get(name) ?? name;
|
|
304
370
|
}
|
|
305
371
|
function inheritanceFinding(name, stack = []) {
|
|
306
|
-
|
|
307
|
-
|
|
372
|
+
if (stack.length >= MAX_INHERITANCE_DEPTH) {
|
|
373
|
+
return {
|
|
374
|
+
agentName: name,
|
|
375
|
+
field: "from",
|
|
376
|
+
message: `Recipe agent "${name}" exceeds the maximum from depth of ${MAX_INHERITANCE_DEPTH}`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const definition = rawDefinitions.get(name);
|
|
308
380
|
if (!definition) {
|
|
309
381
|
return {
|
|
310
|
-
agentName:
|
|
382
|
+
agentName: name,
|
|
311
383
|
field: "from",
|
|
312
|
-
message: `Recipe agent "${
|
|
384
|
+
message: `Recipe agent "${name}" was not found`,
|
|
313
385
|
};
|
|
314
386
|
}
|
|
315
387
|
if (!definition.from)
|
|
316
388
|
return undefined;
|
|
317
|
-
const resolvedFrom =
|
|
389
|
+
const resolvedFrom = definition.from;
|
|
318
390
|
if (stack.includes(resolvedFrom)) {
|
|
319
391
|
return {
|
|
320
|
-
agentName:
|
|
392
|
+
agentName: name,
|
|
321
393
|
field: "from",
|
|
322
|
-
message: `Recipe agent "${
|
|
394
|
+
message: `Recipe agent "${name}" has cyclic from chain: ${[
|
|
323
395
|
...stack,
|
|
324
|
-
|
|
396
|
+
name,
|
|
325
397
|
resolvedFrom,
|
|
326
398
|
].join(" -> ")}`,
|
|
327
399
|
};
|
|
328
400
|
}
|
|
329
401
|
if (!rawDefinitions.has(resolvedFrom)) {
|
|
330
402
|
return {
|
|
331
|
-
agentName:
|
|
403
|
+
agentName: name,
|
|
332
404
|
field: "from",
|
|
333
|
-
message: `Recipe agent "${
|
|
405
|
+
message: `Recipe agent "${name}" inherits from missing agent "${definition.from}"`,
|
|
334
406
|
};
|
|
335
407
|
}
|
|
336
|
-
return inheritanceFinding(definition.from, [...stack,
|
|
408
|
+
return inheritanceFinding(definition.from, [...stack, name]);
|
|
337
409
|
}
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
410
|
+
function resolvedModelProvided(name, stack = []) {
|
|
411
|
+
if (stack.includes(name) ||
|
|
412
|
+
stack.length >= MAX_INHERITANCE_DEPTH)
|
|
341
413
|
return false;
|
|
342
|
-
const definition = rawDefinitions.get(
|
|
414
|
+
const definition = rawDefinitions.get(name);
|
|
343
415
|
if (!definition)
|
|
344
416
|
return false;
|
|
345
|
-
if (
|
|
417
|
+
if (definition.model?.name)
|
|
346
418
|
return true;
|
|
347
419
|
return definition.from
|
|
348
|
-
?
|
|
420
|
+
? resolvedModelProvided(definition.from, [...stack, name])
|
|
349
421
|
: false;
|
|
350
422
|
}
|
|
351
423
|
function resolvedTools(name, stack = []) {
|
|
352
|
-
|
|
353
|
-
|
|
424
|
+
if (stack.includes(name) ||
|
|
425
|
+
stack.length >= MAX_INHERITANCE_DEPTH)
|
|
354
426
|
return undefined;
|
|
355
|
-
const definition = rawDefinitions.get(
|
|
427
|
+
const definition = rawDefinitions.get(name);
|
|
356
428
|
if (!definition)
|
|
357
429
|
return undefined;
|
|
358
430
|
if (definition.tools !== undefined)
|
|
359
431
|
return definition.tools;
|
|
360
432
|
return definition.from
|
|
361
|
-
? resolvedTools(definition.from, [...stack,
|
|
433
|
+
? resolvedTools(definition.from, [...stack, name])
|
|
362
434
|
: undefined;
|
|
363
435
|
}
|
|
364
436
|
function resolvedMcp(name, stack = []) {
|
|
365
|
-
|
|
366
|
-
|
|
437
|
+
if (stack.includes(name) ||
|
|
438
|
+
stack.length >= MAX_INHERITANCE_DEPTH)
|
|
367
439
|
return undefined;
|
|
368
|
-
const definition = rawDefinitions.get(
|
|
440
|
+
const definition = rawDefinitions.get(name);
|
|
369
441
|
if (!definition)
|
|
370
442
|
return undefined;
|
|
371
443
|
const base = definition.from
|
|
372
|
-
? resolvedMcp(definition.from, [...stack,
|
|
444
|
+
? resolvedMcp(definition.from, [...stack, name])
|
|
373
445
|
: undefined;
|
|
374
446
|
return mergeMcp(base, definition.mcp);
|
|
375
447
|
}
|
|
376
448
|
function rawMcpChainInvalid(name, stack = []) {
|
|
377
|
-
|
|
378
|
-
|
|
449
|
+
if (stack.includes(name) ||
|
|
450
|
+
stack.length >= MAX_INHERITANCE_DEPTH)
|
|
379
451
|
return false;
|
|
380
|
-
const definition = rawDefinitions.get(
|
|
452
|
+
const definition = rawDefinitions.get(name);
|
|
381
453
|
if (!definition)
|
|
382
454
|
return false;
|
|
383
455
|
const rawMcp = definition.mcp;
|
|
384
456
|
if (rawMcp?.[INVALID_AGENT_MCP] ||
|
|
385
|
-
Object.values(rawMcp ?? {}).some((selection) => !isValidRecipeMcpToolSelection(selection))) {
|
|
457
|
+
Object.values(rawMcp?.servers ?? {}).some((selection) => !isValidRecipeMcpToolSelection(selection))) {
|
|
386
458
|
return true;
|
|
387
459
|
}
|
|
388
460
|
return definition.from
|
|
389
|
-
? rawMcpChainInvalid(definition.from, [...stack,
|
|
461
|
+
? rawMcpChainInvalid(definition.from, [...stack, name])
|
|
390
462
|
: false;
|
|
391
463
|
}
|
|
392
|
-
const agentName =
|
|
464
|
+
const agentName = opts.agentName;
|
|
393
465
|
const findings = [];
|
|
394
|
-
if (opts.requireExplicitName && explicitNames.get(agentName) !== true) {
|
|
395
|
-
findings.push({
|
|
396
|
-
agentName,
|
|
397
|
-
field: "name",
|
|
398
|
-
severity: "warning",
|
|
399
|
-
message: `Recipe agent "${agentName}" must declare name`,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
466
|
const inheritance = inheritanceFinding(agentName);
|
|
403
467
|
if (inheritance)
|
|
404
468
|
findings.push(inheritance);
|
|
405
|
-
|
|
406
|
-
if (resolvedFieldProvided(agentName, field))
|
|
407
|
-
continue;
|
|
469
|
+
if (!resolvedModelProvided(agentName)) {
|
|
408
470
|
findings.push({
|
|
409
471
|
agentName,
|
|
410
|
-
field,
|
|
411
|
-
|
|
412
|
-
message: `Recipe agent "${agentName}" must declare ${field} directly or inherit it with from`,
|
|
472
|
+
field: "model.name",
|
|
473
|
+
message: `Recipe agent "${agentName}" must declare model.name directly or inherit it with from`,
|
|
413
474
|
});
|
|
414
475
|
}
|
|
415
476
|
const mcp = resolvedMcp(agentName);
|
|
@@ -420,10 +481,10 @@ export function validateResolvedRecipeAgentDefinition(opts) {
|
|
|
420
481
|
server.tools,
|
|
421
482
|
]))
|
|
422
483
|
: undefined;
|
|
423
|
-
//
|
|
484
|
+
// The Recipe checker owns detailed authoring diagnostics. This session guard
|
|
424
485
|
// only ensures malformed raw policies fail closed before agent startup.
|
|
425
486
|
const invalidMcpPolicy = rawMcpChainInvalid(agentName) ||
|
|
426
|
-
Object.entries(mcp ?? {}).some(([serverId, selection]) => {
|
|
487
|
+
Object.entries(mcp?.servers ?? {}).some(([serverId, selection]) => {
|
|
427
488
|
if (!serverId.trim() || !isValidRecipeMcpToolSelection(selection)) {
|
|
428
489
|
return true;
|
|
429
490
|
}
|
|
@@ -436,17 +497,25 @@ export function validateResolvedRecipeAgentDefinition(opts) {
|
|
|
436
497
|
const value = toolName.trim();
|
|
437
498
|
return value !== "*" && !mcpSelectionAllowsTool(packageSelection, value);
|
|
438
499
|
});
|
|
439
|
-
})
|
|
500
|
+
}) ||
|
|
501
|
+
Object.values(mcp?.servers ?? {}).some((selection) => (mcp?.mode !== "tools" &&
|
|
502
|
+
(selection.defer !== undefined || selection.eager !== undefined)) ||
|
|
503
|
+
!validActivationSelectors(selection.defer) ||
|
|
504
|
+
!validActivationSelectors(selection.eager) ||
|
|
505
|
+
[...(selection.defer ?? []), ...(selection.eager ?? [])].some((selector) => selector !== "*" && !mcpSelectionAllowsTool(selection, selector)));
|
|
440
506
|
if (invalidMcpPolicy) {
|
|
441
507
|
findings.push({
|
|
442
508
|
agentName,
|
|
443
509
|
field: "mcp",
|
|
444
510
|
code: "mcp_invalid",
|
|
445
|
-
message: `Recipe agent "${agentName}" has an invalid MCP policy
|
|
511
|
+
message: `Recipe agent "${agentName}" has an invalid MCP policy`,
|
|
446
512
|
});
|
|
447
513
|
}
|
|
448
514
|
return findings;
|
|
449
515
|
}
|
|
516
|
+
export function validateResolvedRecipeAgentDefinition(opts) {
|
|
517
|
+
return validateResolvedRecipeAgentSource(opts, readRecipeAgentSources(opts.recipeDir));
|
|
518
|
+
}
|
|
450
519
|
function isValidRecipeModelSpec(spec) {
|
|
451
520
|
const slash = spec.indexOf("/");
|
|
452
521
|
if (slash <= 0 || slash === spec.length - 1)
|
|
@@ -470,15 +539,11 @@ function validateRecipeAgentModelSpecs(sources) {
|
|
|
470
539
|
}
|
|
471
540
|
function validateRecipeAgentNames(sources) {
|
|
472
541
|
const findings = [];
|
|
473
|
-
const
|
|
474
|
-
const explicitNames = new Set();
|
|
542
|
+
const nameCounts = new Map();
|
|
475
543
|
for (const source of sources) {
|
|
476
|
-
|
|
477
|
-
continue;
|
|
478
|
-
explicitNames.add(source.definition.name);
|
|
479
|
-
explicitNameCounts.set(source.definition.name, (explicitNameCounts.get(source.definition.name) ?? 0) + 1);
|
|
544
|
+
nameCounts.set(source.definition.name, (nameCounts.get(source.definition.name) ?? 0) + 1);
|
|
480
545
|
}
|
|
481
|
-
for (const [name, count] of
|
|
546
|
+
for (const [name, count] of nameCounts) {
|
|
482
547
|
if (count <= 1)
|
|
483
548
|
continue;
|
|
484
549
|
findings.push({
|
|
@@ -487,20 +552,27 @@ function validateRecipeAgentNames(sources) {
|
|
|
487
552
|
message: `Recipe agent name "${name}" is declared by multiple files`,
|
|
488
553
|
});
|
|
489
554
|
}
|
|
490
|
-
for (const source of sources) {
|
|
491
|
-
if (source.fallbackName === source.definition.name ||
|
|
492
|
-
!explicitNames.has(source.fallbackName)) {
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
findings.push({
|
|
496
|
-
agentName: source.definition.name,
|
|
497
|
-
field: "name",
|
|
498
|
-
message: `Recipe agent file alias "${source.fallbackName}" conflicts with an explicit agent name`,
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
555
|
return findings;
|
|
502
556
|
}
|
|
503
|
-
|
|
557
|
+
function validateRecipeAgentSources(recipeDir, sources, invalidFileFindings) {
|
|
558
|
+
const agentNames = [
|
|
559
|
+
...new Set(sources.map((source) => source.definition.name)),
|
|
560
|
+
].sort();
|
|
561
|
+
return [
|
|
562
|
+
...invalidFileFindings,
|
|
563
|
+
...validateRecipeAgentNames(sources),
|
|
564
|
+
...validateRecipeAgentModelSpecs(sources),
|
|
565
|
+
...agentNames.flatMap((agentName) => validateResolvedRecipeAgentSource({
|
|
566
|
+
recipeDir,
|
|
567
|
+
agentName,
|
|
568
|
+
}, sources)),
|
|
569
|
+
];
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Parse agent YAML once, then validate and resolve the complete inheritance
|
|
573
|
+
* Recipe snapshot from that exact source.
|
|
574
|
+
*/
|
|
575
|
+
export function loadValidatedRecipeAgentDefinitions(recipeDir) {
|
|
504
576
|
const invalidFileFindings = [];
|
|
505
577
|
const sources = readRecipeAgentSources(recipeDir, {
|
|
506
578
|
onInvalidFile: (path, error) => {
|
|
@@ -511,44 +583,20 @@ export function validateRecipeAgentDefinitions(recipeDir) {
|
|
|
511
583
|
});
|
|
512
584
|
},
|
|
513
585
|
});
|
|
514
|
-
const
|
|
515
|
-
return
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
...agentNames.flatMap((agentName) => validateResolvedRecipeAgentDefinition({
|
|
520
|
-
recipeDir,
|
|
521
|
-
agentName,
|
|
522
|
-
requireExplicitName: true,
|
|
523
|
-
requiredFields: REQUIRED_RECIPE_AGENT_FIELDS,
|
|
524
|
-
})),
|
|
525
|
-
];
|
|
526
|
-
}
|
|
527
|
-
export function resolveRecipeAgentName(opts) {
|
|
528
|
-
if (opts.agentName?.trim())
|
|
529
|
-
return opts.agentName.trim();
|
|
530
|
-
const definitions = loadRecipeAgentDefinitions(opts.recipeDir);
|
|
531
|
-
const defaultAgent = definitions.get("agent");
|
|
532
|
-
if (defaultAgent)
|
|
533
|
-
return defaultAgent.name;
|
|
534
|
-
const uniqueNames = [
|
|
535
|
-
...new Set([...definitions.values()].map((definition) => definition.name)),
|
|
536
|
-
];
|
|
537
|
-
if (uniqueNames.length === 1)
|
|
538
|
-
return uniqueNames[0];
|
|
539
|
-
if (uniqueNames.length === 0)
|
|
540
|
-
return "agent";
|
|
541
|
-
throw new Error("Recipe has multiple agents and no default entrypoint; add agents/agent.yaml or set PI_AGENT_NAME");
|
|
586
|
+
const definitions = definitionsFromSources(sources);
|
|
587
|
+
return {
|
|
588
|
+
definitions,
|
|
589
|
+
findings: validateRecipeAgentSources(recipeDir, sources, invalidFileFindings),
|
|
590
|
+
};
|
|
542
591
|
}
|
|
543
|
-
export function
|
|
544
|
-
|
|
545
|
-
const agent = loadRecipeAgentDefinitions(opts.recipeDir).get(agentName) ?? null;
|
|
546
|
-
return { agentName, agent };
|
|
592
|
+
export function validateRecipeAgentDefinitions(recipeDir) {
|
|
593
|
+
return loadValidatedRecipeAgentDefinitions(recipeDir).findings;
|
|
547
594
|
}
|
|
548
595
|
export function loadRecipeSystemPrompt(recipeDir) {
|
|
549
596
|
const path = join(recipeDir, "SYSTEM.md");
|
|
550
597
|
if (!existsSync(path))
|
|
551
598
|
return undefined;
|
|
599
|
+
assertRecipePathContained(recipeDir, path, "SYSTEM.md");
|
|
552
600
|
const content = readFileSync(path, "utf8").trim();
|
|
553
601
|
return content || undefined;
|
|
554
602
|
}
|