@databricks/appkit 0.66.1 → 0.67.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/dist/appkit/package.js +1 -1
- package/dist/core/agent/frontmatter.js +23 -0
- package/dist/core/agent/frontmatter.js.map +1 -0
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +36 -6
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/skills/index.js +7 -0
- package/dist/core/agent/skills/load-skills.js +78 -0
- package/dist/core/agent/skills/load-skills.js.map +1 -0
- package/dist/core/agent/skills/parse-skill.js +69 -0
- package/dist/core/agent/skills/parse-skill.js.map +1 -0
- package/dist/core/agent/skills/read-resource.js +31 -0
- package/dist/core/agent/skills/read-resource.js.map +1 -0
- package/dist/core/agent/skills/render.js +33 -0
- package/dist/core/agent/skills/render.js.map +1 -0
- package/dist/core/agent/skills/resolve-catalog.js +78 -0
- package/dist/core/agent/skills/resolve-catalog.js.map +1 -0
- package/dist/core/agent/skills/types.d.ts +50 -0
- package/dist/core/agent/skills/types.d.ts.map +1 -0
- package/dist/core/agent/types.d.ts +47 -0
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +50 -0
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +225 -13
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/manifest.js +40 -21
- package/dist/plugins/agents/schemas.js +2 -1
- package/dist/plugins/agents/schemas.js.map +1 -1
- package/dist/plugins/server/index.js +2 -2
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js +3 -3
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js +3 -3
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js +4 -4
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +2 -2
- package/dist/type-generator/database/generate.js +3 -3
- package/dist/type-generator/database/generate.js.map +1 -1
- package/dist/type-generator/migration.js +2 -2
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js +3 -3
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +35 -0
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit/TypeAlias.ResolvedToolEntry.md +46 -0
- package/docs/plugins/agents.md +68 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/dist/appkit/package.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/core/agent/frontmatter.ts
|
|
2
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
3
|
+
/**
|
|
4
|
+
* Splits a `--- yaml ---\nbody` markdown string into its raw YAML block and
|
|
5
|
+
* trimmed body. Returns `yaml: null` when there is no leading frontmatter
|
|
6
|
+
* fence. Shared by the agent loader ({@link parseFrontmatter}) and the skill
|
|
7
|
+
* parser so the fence regex lives in one place.
|
|
8
|
+
*/
|
|
9
|
+
function splitFrontmatter(raw) {
|
|
10
|
+
const match = raw.match(FRONTMATTER_RE);
|
|
11
|
+
if (!match) return {
|
|
12
|
+
yaml: null,
|
|
13
|
+
body: raw.trim()
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
yaml: match[1],
|
|
17
|
+
body: match[2].trim()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { splitFrontmatter };
|
|
23
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.js","names":[],"sources":["../../../src/core/agent/frontmatter.ts"],"sourcesContent":["const FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\n\n/**\n * Splits a `--- yaml ---\\nbody` markdown string into its raw YAML block and\n * trimmed body. Returns `yaml: null` when there is no leading frontmatter\n * fence. Shared by the agent loader ({@link parseFrontmatter}) and the skill\n * parser so the fence regex lives in one place.\n */\nexport function splitFrontmatter(raw: string): {\n yaml: string | null;\n body: string;\n} {\n const match = raw.match(FRONTMATTER_RE);\n if (!match) {\n return { yaml: null, body: raw.trim() };\n }\n return { yaml: match[1], body: match[2].trim() };\n}\n"],"mappings":";AAAA,MAAM,iBAAiB;;;;;;;AAQvB,SAAgB,iBAAiB,KAG/B;CACA,MAAM,QAAQ,IAAI,MAAM,eAAe;AACvC,KAAI,CAAC,MACH,QAAO;EAAE,MAAM;EAAM,MAAM,IAAI,MAAM;EAAE;AAEzC,QAAO;EAAE,MAAM,MAAM;EAAI,MAAM,MAAM,GAAG,MAAM;EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"load-agents.d.ts","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"mappings":";;;;;UAsBU,eAAA;EACR,OAAA,GAAU,IAAA,GAAO,cAAA,KAAmB,MAAA;AAAA;AAAA,UAGrB,WAAA;EAJQ;EAMvB,YAAA,GAAe,YAAA,GAAe,OAAA,CAAQ,YAAA;EALI;EAO1C,cAAA,GAAiB,MAAA,SAAe,SAAA;EAPf;;;;;AAGnB;EAWE,OAAA,GAAU,GAAA,SAAY,eAAA;;;;;;;;EAQtB,UAAA,GAAa,MAAA,SAAe,eAAA;AAAA;AAAA,UAGb,UAAA;EAHI;EAKnB,IAAA,EAAM,MAAA,SAAe,eAAA;EAtBrB;EAwBA,YAAA;AAAA;;;;;;iBAuEc,uBAAA,CAAwB,QAAA;;;;;;;;;iBAgClB,iBAAA,CACpB,QAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,eAAA;;;;;;;;;;;;;;;;;;;iBAgCW,iBAAA,CACpB,GAAA,UACA,GAAA,EAAK,WAAA,GACJ,OAAA,CAAQ,UAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
2
|
import { isToolkitEntry } from "./types.js";
|
|
3
3
|
import { agentDirNames } from "./agent-dirs.js";
|
|
4
|
+
import { splitFrontmatter } from "./frontmatter.js";
|
|
4
5
|
import fs from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import yaml from "js-yaml";
|
|
@@ -25,6 +26,7 @@ const ALLOWED_KEYS = new Set([
|
|
|
25
26
|
"model",
|
|
26
27
|
"tools",
|
|
27
28
|
"agents",
|
|
29
|
+
"skills",
|
|
28
30
|
"maxSteps",
|
|
29
31
|
"maxTokens",
|
|
30
32
|
"generationParams",
|
|
@@ -143,21 +145,21 @@ function normalizeAgentsFrontmatter(value, agentName, filePath) {
|
|
|
143
145
|
}
|
|
144
146
|
/** Exposed for tests. Parses `--- yaml ---\nbody` and validates frontmatter keys. */
|
|
145
147
|
function parseFrontmatter(raw, sourcePath) {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
+
const { yaml: yamlBlock, body } = splitFrontmatter(raw);
|
|
149
|
+
if (yamlBlock === null) return {
|
|
148
150
|
data: null,
|
|
149
|
-
content:
|
|
151
|
+
content: body
|
|
150
152
|
};
|
|
151
153
|
let parsed;
|
|
152
154
|
try {
|
|
153
|
-
parsed = yaml.load(
|
|
155
|
+
parsed = yaml.load(yamlBlock);
|
|
154
156
|
} catch (err) {
|
|
155
157
|
const src = sourcePath ? ` (${sourcePath})` : "";
|
|
156
158
|
throw new Error(`Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
159
|
}
|
|
158
160
|
if (parsed === null || parsed === void 0) return {
|
|
159
161
|
data: {},
|
|
160
|
-
content:
|
|
162
|
+
content: body
|
|
161
163
|
};
|
|
162
164
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
163
165
|
const src = sourcePath ? ` (${sourcePath})` : "";
|
|
@@ -167,7 +169,7 @@ function parseFrontmatter(raw, sourcePath) {
|
|
|
167
169
|
for (const key of Object.keys(data)) if (!ALLOWED_KEYS.has(key)) logger.warn("Ignoring unknown frontmatter key '%s' in %s", key, sourcePath ?? "<inline>");
|
|
168
170
|
return {
|
|
169
171
|
data,
|
|
170
|
-
content:
|
|
172
|
+
content: body
|
|
171
173
|
};
|
|
172
174
|
}
|
|
173
175
|
const isNumber = (v) => typeof v === "number";
|
|
@@ -220,6 +222,33 @@ function parseGenerationParams(value, sourcePath) {
|
|
|
220
222
|
}
|
|
221
223
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
222
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Defensively parses a frontmatter `skills:` list into deduped skill names.
|
|
227
|
+
* Non-array values and non-string/empty entries are dropped with a warning,
|
|
228
|
+
* so a malformed list is visible rather than silently applied. Returns
|
|
229
|
+
* `undefined` when nothing valid is present.
|
|
230
|
+
*/
|
|
231
|
+
function parseSkillsFrontmatter(value, sourcePath) {
|
|
232
|
+
if (value === void 0) return void 0;
|
|
233
|
+
const where = sourcePath ?? "<inline>";
|
|
234
|
+
if (!Array.isArray(value)) {
|
|
235
|
+
logger.warn("Ignoring 'skills' in %s: expected an array of skill names", where);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const out = [];
|
|
239
|
+
const seen = /* @__PURE__ */ new Set();
|
|
240
|
+
for (const item of value) {
|
|
241
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
242
|
+
logger.warn("Ignoring invalid 'skills' entry in %s: %s", where, JSON.stringify(item));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const name = item.trim();
|
|
246
|
+
if (seen.has(name)) continue;
|
|
247
|
+
seen.add(name);
|
|
248
|
+
out.push(name);
|
|
249
|
+
}
|
|
250
|
+
return out.length > 0 ? out : void 0;
|
|
251
|
+
}
|
|
223
252
|
function buildDefinition(name, raw, filePath, ctx) {
|
|
224
253
|
const { data, content } = parseFrontmatter(raw, filePath);
|
|
225
254
|
const fm = data ?? {};
|
|
@@ -233,6 +262,7 @@ function buildDefinition(name, raw, filePath, ctx) {
|
|
|
233
262
|
instructions: content,
|
|
234
263
|
model,
|
|
235
264
|
tools: Object.keys(tools).length > 0 ? tools : void 0,
|
|
265
|
+
skills: parseSkillsFrontmatter(fm.skills, filePath),
|
|
236
266
|
maxSteps: typeof fm.maxSteps === "number" ? fm.maxSteps : void 0,
|
|
237
267
|
maxTokens: typeof fm.maxTokens === "number" ? fm.maxTokens : void 0,
|
|
238
268
|
generationParams: parseGenerationParams(fm.generationParams, filePath),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type {\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\nimport { agentDirNames } from \"./agent-dirs\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /**\n * Registered plugin toolkits referenced by `plugin:NAME` entries in the\n * unified `tools:` frontmatter list. Keyed by plugin name; each value\n * exposes the same `toolkit(opts?)` surface as the `plugins` argument to\n * `tools(plugins) => Record<...>` in the code form.\n */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n /**\n * Unified tool list. Each entry is one of:\n *\n * - **`plugin:<name>`** (string) — pull every tool from the named plugin.\n * - **`plugin:<name>: [tool1, tool2]`** — pull only the listed tools\n * (shorthand for `{ only: [...] }`).\n * - **`plugin:<name>: { ...ToolkitOptions }`** — pass full\n * `prefix` / `only` / `except` / `rename` options.\n * - **`<key>`** (string, no `plugin:` prefix) — ambient tool name\n * resolved against the `agents({ tools: { ... } })` config.\n *\n * Mirrors the TS function form `tools(plugins) { ... }` where plugin\n * tools and inline tools live in the same record.\n */\n tools?: FrontmatterToolEntry[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional OpenAI-compatible generation params forwarded to the serving\n * request body (`temperature`, `top_p`, `stop`, `frequency_penalty`,\n * `presence_penalty`). Parsed defensively in {@link buildDefinition}.\n */\n generationParams?: Record<string, unknown>;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n}\n\n/**\n * Each item in {@link Frontmatter.tools}. Strings are either ambient tool\n * names (no prefix) or bare plugin references (`plugin:NAME`). Objects are\n * single-key mappings whose key is `plugin:NAME` and whose value is either\n * an array of local tool names (sugar for `{ only: [...] }`) or a full\n * `ToolkitOptions` record.\n *\n * Named `FrontmatterToolEntry` to avoid colliding with the exported\n * `ToolEntry` from `tools/define-tool.ts` — that is the plugin-author API\n * surface (`defineTool({ ... }) : ToolEntry`); this is the frontmatter\n * parse type. They are unrelated and live in different layers.\n */\ntype FrontmatterToolEntry =\n | string\n | { [key: string]: ToolkitOptions | string[] };\n\nconst PLUGIN_PREFIX = \"plugin:\";\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"tools\",\n \"agents\",\n \"maxSteps\",\n \"maxTokens\",\n \"generationParams\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n // A symlink to a file is filtered out below when reading agent.md (ENOTDIR).\n const agentIds = agentDirNames(entries);\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n // No agent.md → a code-agent folder (agent.ts) or an asset dir (skills/);\n // ENOTDIR → the entry is a symlink to a file, not an agent folder.\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\" || code === \"ENOTDIR\") continue;\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!match) {\n return { data: null, content: raw.trim() };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(match[1]);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: match[2].trim() };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: match[2].trim() };\n}\n\nconst isNumber = (v: unknown): v is number => typeof v === \"number\";\n\n/**\n * Per-key validators for {@link GenerationParams} frontmatter. Keyed by wire\n * name and typed as `Record<keyof GenerationParams, ...>`, so a new param on\n * the interface is a compile error until it gets an entry here — parsing,\n * unknown-key detection, and warnings all derive from this one table.\n */\nconst GENERATION_PARAM_SPECS: Record<\n keyof GenerationParams,\n { label: string; valid: (v: unknown) => boolean }\n> = {\n temperature: { label: \"number\", valid: isNumber },\n top_p: { label: \"number\", valid: isNumber },\n frequency_penalty: { label: \"number\", valid: isNumber },\n presence_penalty: { label: \"number\", valid: isNumber },\n stop: {\n label: \"string or string[]\",\n valid: (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((s) => typeof s === \"string\")),\n },\n};\n\n/**\n * Defensively maps a frontmatter `generationParams` map to {@link GenerationParams}.\n * Picks only known keys with the expected wire types. A key present with the\n * wrong type (e.g. `temperature: \"0.5\"`) or an unknown key (e.g. `top-p`) is\n * dropped and logged at warn level, so a silently-ignored param is visible\n * rather than mistaken for \"applied\". Returns `undefined` when no valid key is\n * present.\n */\nfunction parseGenerationParams(\n value: unknown,\n sourcePath?: string,\n): GenerationParams | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return undefined;\n }\n const raw = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n const where = sourcePath ?? \"<inline>\";\n\n for (const [key, v] of Object.entries(raw)) {\n const spec = GENERATION_PARAM_SPECS[key as keyof GenerationParams];\n if (!spec) {\n logger.warn(\n \"Ignoring unknown generationParams key '%s' in %s\",\n key,\n where,\n );\n } else if (spec.valid(v)) {\n out[key] = v;\n } else {\n logger.warn(\n \"Ignoring generationParams.%s in %s: expected %s, got %s\",\n key,\n where,\n spec.label,\n typeof v,\n );\n }\n }\n\n return Object.keys(out).length > 0 ? (out as GenerationParams) : undefined;\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n generationParams: parseGenerationParams(fm.generationParams, filePath),\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const entry of fm.tools ?? []) {\n const parsed = parseToolEntry(entry, filePath, agentName);\n if (parsed.kind === \"plugin\") {\n const provider = pluginIdx.get(parsed.pluginName);\n if (!provider) {\n const available =\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`,\n );\n }\n const entries = provider.toolkit(parsed.opts) as Record<string, unknown>;\n for (const [key, value] of Object.entries(entries)) {\n if (!isToolkitEntry(value)) {\n throw new Error(\n `Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = value as ToolkitEntry;\n }\n } else {\n const tool = ctx.availableTools?.[parsed.toolName];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. ` +\n \"If you meant to reference a plugin, use the 'plugin:NAME' prefix.\",\n );\n }\n out[parsed.toolName] = tool;\n }\n }\n\n return out;\n}\n\ntype ParsedToolEntry =\n | { kind: \"plugin\"; pluginName: string; opts: ToolkitOptions | undefined }\n | { kind: \"ambient\"; toolName: string };\n\n/**\n * Classify one item in the `tools:` frontmatter list into either a plugin\n * reference (with optional ToolkitOptions) or an ambient tool lookup.\n *\n * Strings starting with `plugin:` are bare plugin references. Strings\n * without the prefix are ambient tool names. Object entries are\n * single-key mappings keyed by `plugin:NAME`; the value is either an\n * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.\n */\nfunction parseToolEntry(\n entry: FrontmatterToolEntry,\n filePath: string,\n agentName: string,\n): ParsedToolEntry {\n if (typeof entry === \"string\") {\n if (entry.startsWith(PLUGIN_PREFIX)) {\n const pluginName = entry.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n return { kind: \"plugin\", pluginName, opts: undefined };\n }\n if (entry.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`,\n );\n }\n return { kind: \"ambient\", toolName: entry };\n }\n if (typeof entry !== \"object\" || entry === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`,\n );\n }\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const key = keys[0];\n // Bare `- plugin:` (no name after the colon) parses as a mapping with the\n // key `\"plugin\"`. Catch that as a friendly error rather than dumping it\n // through the generic \"expected key 'plugin:NAME'\" branch.\n if (key === \"plugin\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n if (!key.startsWith(PLUGIN_PREFIX)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. ` +\n \"Use a bare string for ambient tools (e.g. `- get_weather`).\",\n );\n }\n const pluginName = key.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n const value = entry[key];\n if (Array.isArray(value)) {\n return { kind: \"plugin\", pluginName, opts: { only: value } };\n }\n if (typeof value === \"object\" && value !== null) {\n return {\n kind: \"plugin\",\n pluginName,\n opts: value as ToolkitOptions,\n };\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`,\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"load-agents.js","names":[],"sources":["../../../src/core/agent/load-agents.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport yaml from \"js-yaml\";\nimport type { AgentAdapter } from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type {\n AgentDefinition,\n AgentTool,\n BaseSystemPromptOption,\n ToolkitEntry,\n ToolkitOptions,\n} from \"../../core/agent/types\";\nimport { isToolkitEntry } from \"../../core/agent/types\";\nimport { createLogger } from \"../../logging/logger\";\nimport { agentDirNames } from \"./agent-dirs\";\nimport { splitFrontmatter } from \"./frontmatter\";\n\nconst logger = createLogger(\"agents:loader\");\n\ninterface ToolkitProvider {\n toolkit: (opts?: ToolkitOptions) => Record<string, unknown>;\n}\n\nexport interface LoadContext {\n /** Default model when frontmatter has no `endpoint` and the def has no `model`. */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library referenced by frontmatter `tools: [key1, key2]`. */\n availableTools?: Record<string, AgentTool>;\n /**\n * Registered plugin toolkits referenced by `plugin:NAME` entries in the\n * unified `tools:` frontmatter list. Keyed by plugin name; each value\n * exposes the same `toolkit(opts?)` surface as the `plugins` argument to\n * `tools(plugins) => Record<...>` in the code form.\n */\n plugins?: Map<string, ToolkitProvider>;\n /**\n * Code-defined agents contributed by `agents({ agents: { ... } })`. The\n * directory loader resolves `agents:` frontmatter references against\n * these alongside sibling markdown files, so a markdown parent can\n * delegate to a code-defined child. Code-defined names win on collision\n * with markdown names, matching the plugin's top-level merge precedence.\n */\n codeAgents?: Record<string, AgentDefinition>;\n}\n\nexport interface LoadResult {\n /** Agent definitions keyed by agent id (directory name under `dir`). */\n defs: Record<string, AgentDefinition>;\n /** First agent with `default: true` frontmatter (sorted id order), or `null`. */\n defaultAgent: string | null;\n}\n\ninterface Frontmatter {\n endpoint?: string;\n model?: string;\n /**\n * Unified tool list. Each entry is one of:\n *\n * - **`plugin:<name>`** (string) — pull every tool from the named plugin.\n * - **`plugin:<name>: [tool1, tool2]`** — pull only the listed tools\n * (shorthand for `{ only: [...] }`).\n * - **`plugin:<name>: { ...ToolkitOptions }`** — pass full\n * `prefix` / `only` / `except` / `rename` options.\n * - **`<key>`** (string, no `plugin:` prefix) — ambient tool name\n * resolved against the `agents({ tools: { ... } })` config.\n *\n * Mirrors the TS function form `tools(plugins) { ... }` where plugin\n * tools and inline tools live in the same record.\n */\n tools?: FrontmatterToolEntry[];\n /**\n * Other agent ids to expose as sub-agents. Each becomes an `agent-<id>`\n * tool at runtime. Resolution happens at directory-load time in\n * {@link loadAgentsFromDir}; the single-file {@link loadAgentFromFile} path\n * rejects non-empty values since there are no siblings to resolve against.\n */\n agents?: string[];\n /**\n * Names of global skills (from the shared `skills/` pool or a catalog\n * volume) to make visible to this agent. Per-agent skills under\n * `<id>/skills/` are always visible and need not be listed here. Ignored\n * when the plugin's `autoInheritSkills` makes every global skill visible.\n */\n skills?: string[];\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional OpenAI-compatible generation params forwarded to the serving\n * request body (`temperature`, `top_p`, `stop`, `frequency_penalty`,\n * `presence_penalty`). Parsed defensively in {@link buildDefinition}.\n */\n generationParams?: Record<string, unknown>;\n default?: boolean;\n baseSystemPrompt?: false | string;\n ephemeral?: boolean;\n}\n\n/**\n * Each item in {@link Frontmatter.tools}. Strings are either ambient tool\n * names (no prefix) or bare plugin references (`plugin:NAME`). Objects are\n * single-key mappings whose key is `plugin:NAME` and whose value is either\n * an array of local tool names (sugar for `{ only: [...] }`) or a full\n * `ToolkitOptions` record.\n *\n * Named `FrontmatterToolEntry` to avoid colliding with the exported\n * `ToolEntry` from `tools/define-tool.ts` — that is the plugin-author API\n * surface (`defineTool({ ... }) : ToolEntry`); this is the frontmatter\n * parse type. They are unrelated and live in different layers.\n */\ntype FrontmatterToolEntry =\n | string\n | { [key: string]: ToolkitOptions | string[] };\n\nconst PLUGIN_PREFIX = \"plugin:\";\n\n/**\n * Derives the logical agent id from a markdown path. When the file is named\n * `agent.md`, the id is the parent directory name (folder-based layout);\n * otherwise the id is the file stem (e.g. legacy single-file paths).\n */\nexport function agentIdFromMarkdownPath(filePath: string): string {\n const normalized = path.normalize(filePath);\n const base = path.basename(normalized);\n const parent = path.basename(path.dirname(normalized));\n if (base === \"agent.md\" && parent && parent !== \".\" && parent !== \"..\") {\n return parent;\n }\n return path.basename(normalized, \".md\");\n}\n\nconst ALLOWED_KEYS = new Set([\n \"endpoint\",\n \"model\",\n \"tools\",\n \"agents\",\n \"skills\",\n \"maxSteps\",\n \"maxTokens\",\n \"generationParams\",\n \"default\",\n \"baseSystemPrompt\",\n \"ephemeral\",\n]);\n\n/**\n * Loads a single markdown agent file and resolves its frontmatter against\n * registered plugin toolkits + ambient tool library.\n *\n * Rejects non-empty `agents:` frontmatter because single-file loads have\n * no siblings to resolve sub-agent references against — callers must use\n * {@link loadAgentsFromDir} when markdown agents delegate to one another.\n */\nexport async function loadAgentFromFile(\n filePath: string,\n ctx: LoadContext,\n): Promise<AgentDefinition> {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const name = agentIdFromMarkdownPath(filePath);\n const { data } = parseFrontmatter(raw, filePath);\n if (Array.isArray(data?.agents) && data.agents.length > 0) {\n throw new Error(\n `Agent '${name}' (${filePath}) declares 'agents:' in frontmatter, ` +\n `which requires loadAgentsFromDir to resolve sibling references. ` +\n `Use loadAgentsFromDir, or wire sub-agents in code via createAgent({ agents: { ... } }).`,\n );\n }\n return buildDefinition(name, raw, filePath, ctx);\n}\n\n/**\n * Scans a directory for one subdirectory per agent, each containing\n * `agent.md` (frontmatter + body). Produces an `AgentDefinition` record keyed\n * by agent id (folder name). Throws on frontmatter errors or unresolved\n * references. Returns an empty map if the directory does not exist.\n *\n * Legacy top-level `*.md` files are rejected with an error — migrate each to\n * `<id>/agent.md` under a sibling folder named for the agent id.\n *\n * Runs in two passes so sub-agent references in frontmatter (`agents: [...]`)\n * can be resolved regardless of directory iteration order:\n *\n * 1. Build every agent's definition from its own `agent.md`.\n * 2. Walk `agents:` references and wire `def.agents = { child: childDef }`\n * by looking them up in the complete map. Dangling names and\n * self-references fail loudly; mutual delegation is allowed and bounded\n * at runtime by `limits.maxSubAgentDepth`.\n */\nexport async function loadAgentsFromDir(\n dir: string,\n ctx: LoadContext,\n): Promise<LoadResult> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { defs: {}, defaultAgent: null };\n }\n throw err;\n }\n const orphanMd = entries\n .filter((e) => e.isFile() && e.name.endsWith(\".md\"))\n .map((e) => e.name)\n .sort();\n\n if (orphanMd.length > 0) {\n const hint = orphanMd\n .map((f) => `${path.basename(f, \".md\")}/agent.md`)\n .join(\", \");\n throw new Error(\n `Agents directory contains unsupported top-level markdown file(s): ${orphanMd.join(\", \")}. ` +\n `Use one folder per agent with a fixed entry file, e.g. ${hint}.`,\n );\n }\n\n // A symlink to a file is filtered out below when reading agent.md (ENOTDIR).\n const agentIds = agentDirNames(entries);\n\n const defs: Record<string, AgentDefinition> = {};\n const subAgentRefs: Record<string, string[]> = {};\n let defaultAgent: string | null = null;\n\n // Pass 1: build every agent's definition; collect sub-agent refs.\n for (const id of agentIds) {\n const agentPath = path.join(dir, id, \"agent.md\");\n let raw: string;\n try {\n raw = await fs.readFile(agentPath, \"utf-8\");\n } catch (err) {\n // No agent.md → a code-agent folder (agent.ts) or an asset dir (skills/);\n // ENOTDIR → the entry is a symlink to a file, not an agent folder.\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\" || code === \"ENOTDIR\") continue;\n throw err;\n }\n defs[id] = buildDefinition(id, raw, agentPath, ctx);\n const { data } = parseFrontmatter(raw, agentPath);\n if (data?.agents !== undefined) {\n subAgentRefs[id] = normalizeAgentsFrontmatter(data.agents, id, agentPath);\n }\n if (data?.default === true && !defaultAgent) {\n defaultAgent = id;\n }\n }\n\n // Pass 2: resolve sub-agent references against the complete defs map.\n // Code-defined agents (ctx.codeAgents) take precedence over markdown ones\n // with the same name, matching the plugin's top-level merge behaviour.\n for (const [name, refs] of Object.entries(subAgentRefs)) {\n if (refs.length === 0) continue;\n const children: Record<string, AgentDefinition> = {};\n const missing: string[] = [];\n for (const ref of refs) {\n if (ref === name) {\n throw new Error(\n `Agent '${name}' (${path.join(dir, name, \"agent.md\")}) cannot reference itself in 'agents:'.`,\n );\n }\n const sibling = ctx.codeAgents?.[ref] ?? defs[ref];\n if (!sibling) {\n missing.push(ref);\n continue;\n }\n children[ref] = sibling;\n }\n if (missing.length > 0) {\n const available =\n [...Object.keys(ctx.codeAgents ?? {}), ...Object.keys(defs)]\n .sort()\n .join(\", \") || \"<none>\";\n throw new Error(\n `Agent '${name}' references sub-agent(s) '${missing.join(\", \")}' in 'agents:', ` +\n `but no markdown or code agent(s) with those names exist. ` +\n `Available: ${available}.`,\n );\n }\n defs[name].agents = children;\n }\n\n return { defs, defaultAgent };\n}\n\n/**\n * Validates that `agents:` frontmatter is an array of non-empty strings and\n * returns it with duplicates removed. Throws with a clear per-file message\n * on malformed input rather than silently ignoring.\n */\nfunction normalizeAgentsFrontmatter(\n value: unknown,\n agentName: string,\n filePath: string,\n): string[] {\n if (!Array.isArray(value)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' frontmatter: ` +\n `expected an array of sibling agent ids, got ${typeof value}.`,\n );\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'agents:' entry: ` +\n `expected non-empty string, got ${JSON.stringify(item)}.`,\n );\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\n/** Exposed for tests. Parses `--- yaml ---\\nbody` and validates frontmatter keys. */\nexport function parseFrontmatter(\n raw: string,\n sourcePath?: string,\n): { data: Frontmatter | null; content: string } {\n const { yaml: yamlBlock, body } = splitFrontmatter(raw);\n if (yamlBlock === null) {\n return { data: null, content: body };\n }\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlBlock);\n } catch (err) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(\n `Invalid YAML frontmatter${src}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (parsed === null || parsed === undefined) {\n return { data: {}, content: body };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n const src = sourcePath ? ` (${sourcePath})` : \"\";\n throw new Error(`Frontmatter must be a YAML object${src}`);\n }\n const data = parsed as Record<string, unknown>;\n for (const key of Object.keys(data)) {\n if (!ALLOWED_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown frontmatter key '%s' in %s\",\n key,\n sourcePath ?? \"<inline>\",\n );\n }\n }\n return { data: data as Frontmatter, content: body };\n}\n\nconst isNumber = (v: unknown): v is number => typeof v === \"number\";\n\n/**\n * Per-key validators for {@link GenerationParams} frontmatter. Keyed by wire\n * name and typed as `Record<keyof GenerationParams, ...>`, so a new param on\n * the interface is a compile error until it gets an entry here — parsing,\n * unknown-key detection, and warnings all derive from this one table.\n */\nconst GENERATION_PARAM_SPECS: Record<\n keyof GenerationParams,\n { label: string; valid: (v: unknown) => boolean }\n> = {\n temperature: { label: \"number\", valid: isNumber },\n top_p: { label: \"number\", valid: isNumber },\n frequency_penalty: { label: \"number\", valid: isNumber },\n presence_penalty: { label: \"number\", valid: isNumber },\n stop: {\n label: \"string or string[]\",\n valid: (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((s) => typeof s === \"string\")),\n },\n};\n\n/**\n * Defensively maps a frontmatter `generationParams` map to {@link GenerationParams}.\n * Picks only known keys with the expected wire types. A key present with the\n * wrong type (e.g. `temperature: \"0.5\"`) or an unknown key (e.g. `top-p`) is\n * dropped and logged at warn level, so a silently-ignored param is visible\n * rather than mistaken for \"applied\". Returns `undefined` when no valid key is\n * present.\n */\nfunction parseGenerationParams(\n value: unknown,\n sourcePath?: string,\n): GenerationParams | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return undefined;\n }\n const raw = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n const where = sourcePath ?? \"<inline>\";\n\n for (const [key, v] of Object.entries(raw)) {\n const spec = GENERATION_PARAM_SPECS[key as keyof GenerationParams];\n if (!spec) {\n logger.warn(\n \"Ignoring unknown generationParams key '%s' in %s\",\n key,\n where,\n );\n } else if (spec.valid(v)) {\n out[key] = v;\n } else {\n logger.warn(\n \"Ignoring generationParams.%s in %s: expected %s, got %s\",\n key,\n where,\n spec.label,\n typeof v,\n );\n }\n }\n\n return Object.keys(out).length > 0 ? (out as GenerationParams) : undefined;\n}\n\n/**\n * Defensively parses a frontmatter `skills:` list into deduped skill names.\n * Non-array values and non-string/empty entries are dropped with a warning,\n * so a malformed list is visible rather than silently applied. Returns\n * `undefined` when nothing valid is present.\n */\nfunction parseSkillsFrontmatter(\n value: unknown,\n sourcePath?: string,\n): string[] | undefined {\n if (value === undefined) return undefined;\n const where = sourcePath ?? \"<inline>\";\n if (!Array.isArray(value)) {\n logger.warn(\n \"Ignoring 'skills' in %s: expected an array of skill names\",\n where,\n );\n return undefined;\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const item of value) {\n if (typeof item !== \"string\" || item.trim() === \"\") {\n logger.warn(\n \"Ignoring invalid 'skills' entry in %s: %s\",\n where,\n JSON.stringify(item),\n );\n continue;\n }\n const name = item.trim();\n if (seen.has(name)) continue;\n seen.add(name);\n out.push(name);\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction buildDefinition(\n name: string,\n raw: string,\n filePath: string,\n ctx: LoadContext,\n): AgentDefinition {\n const { data, content } = parseFrontmatter(raw, filePath);\n const fm: Frontmatter = data ?? {};\n\n const tools = resolveFrontmatterTools(name, fm, filePath, ctx);\n const model = fm.model ?? fm.endpoint ?? ctx.defaultModel;\n\n let baseSystemPrompt: BaseSystemPromptOption | undefined;\n if (fm.baseSystemPrompt === false) baseSystemPrompt = false;\n else if (typeof fm.baseSystemPrompt === \"string\")\n baseSystemPrompt = fm.baseSystemPrompt;\n\n return {\n name,\n instructions: content,\n model,\n tools: Object.keys(tools).length > 0 ? tools : undefined,\n skills: parseSkillsFrontmatter(fm.skills, filePath),\n maxSteps: typeof fm.maxSteps === \"number\" ? fm.maxSteps : undefined,\n maxTokens: typeof fm.maxTokens === \"number\" ? fm.maxTokens : undefined,\n generationParams: parseGenerationParams(fm.generationParams, filePath),\n baseSystemPrompt,\n ephemeral: typeof fm.ephemeral === \"boolean\" ? fm.ephemeral : undefined,\n };\n}\n\nfunction resolveFrontmatterTools(\n agentName: string,\n fm: Frontmatter,\n filePath: string,\n ctx: LoadContext,\n): Record<string, AgentTool> {\n const out: Record<string, AgentTool> = {};\n const pluginIdx = ctx.plugins ?? new Map<string, ToolkitProvider>();\n\n for (const entry of fm.tools ?? []) {\n const parsed = parseToolEntry(entry, filePath, agentName);\n if (parsed.kind === \"plugin\") {\n const provider = pluginIdx.get(parsed.pluginName);\n if (!provider) {\n const available =\n pluginIdx.size > 0\n ? Array.from(pluginIdx.keys()).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references 'plugin:${parsed.pluginName}', but plugin '${parsed.pluginName}' is not registered. Available: ${available}`,\n );\n }\n const entries = provider.toolkit(parsed.opts) as Record<string, unknown>;\n for (const [key, value] of Object.entries(entries)) {\n if (!isToolkitEntry(value)) {\n throw new Error(\n `Plugin '${parsed.pluginName}'.toolkit() returned a value at key '${key}' that is not a ToolkitEntry`,\n );\n }\n out[key] = value as ToolkitEntry;\n }\n } else {\n const tool = ctx.availableTools?.[parsed.toolName];\n if (!tool) {\n const available = ctx.availableTools\n ? Object.keys(ctx.availableTools).join(\", \")\n : \"<none>\";\n throw new Error(\n `Agent '${agentName}' (${filePath}) references ambient tool '${parsed.toolName}', which is not in the agents() plugin's tools field. Available: ${available}. ` +\n \"If you meant to reference a plugin, use the 'plugin:NAME' prefix.\",\n );\n }\n out[parsed.toolName] = tool;\n }\n }\n\n return out;\n}\n\ntype ParsedToolEntry =\n | { kind: \"plugin\"; pluginName: string; opts: ToolkitOptions | undefined }\n | { kind: \"ambient\"; toolName: string };\n\n/**\n * Classify one item in the `tools:` frontmatter list into either a plugin\n * reference (with optional ToolkitOptions) or an ambient tool lookup.\n *\n * Strings starting with `plugin:` are bare plugin references. Strings\n * without the prefix are ambient tool names. Object entries are\n * single-key mappings keyed by `plugin:NAME`; the value is either an\n * array (sugar for `{ only: [...] }`) or a full `ToolkitOptions` record.\n */\nfunction parseToolEntry(\n entry: FrontmatterToolEntry,\n filePath: string,\n agentName: string,\n): ParsedToolEntry {\n if (typeof entry === \"string\") {\n if (entry.startsWith(PLUGIN_PREFIX)) {\n const pluginName = entry.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n return { kind: \"plugin\", pluginName, opts: undefined };\n }\n if (entry.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty string in 'tools:'.`,\n );\n }\n return { kind: \"ambient\", toolName: entry };\n }\n if (typeof entry !== \"object\" || entry === null) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has invalid 'tools:' entry: ${JSON.stringify(entry)}`,\n );\n }\n const keys = Object.keys(entry);\n if (keys.length !== 1) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entry must have exactly one key, got: ${keys.join(\", \")}`,\n );\n }\n const key = keys[0];\n // Bare `- plugin:` (no name after the colon) parses as a mapping with the\n // key `\"plugin\"`. Catch that as a friendly error rather than dumping it\n // through the generic \"expected key 'plugin:NAME'\" branch.\n if (key === \"plugin\") {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n if (!key.startsWith(PLUGIN_PREFIX)) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'tools:' object entries are reserved for plugin references; expected key 'plugin:NAME', got '${key}'. ` +\n \"Use a bare string for ambient tools (e.g. `- get_weather`).\",\n );\n }\n const pluginName = key.slice(PLUGIN_PREFIX.length);\n if (pluginName.length === 0) {\n throw new Error(\n `Agent '${agentName}' (${filePath}) has an empty plugin name in 'plugin:'.`,\n );\n }\n const value = entry[key];\n if (Array.isArray(value)) {\n return { kind: \"plugin\", pluginName, opts: { only: value } };\n }\n if (typeof value === \"object\" && value !== null) {\n return {\n kind: \"plugin\",\n pluginName,\n opts: value as ToolkitOptions,\n };\n }\n throw new Error(\n `Agent '${agentName}' (${filePath}) 'plugin:${pluginName}' options must be an array of tool names or a ToolkitOptions object.`,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { parseSkill } from "./parse-skill.js";
|
|
2
|
+
import { loadSkillsFromDir } from "./load-skills.js";
|
|
3
|
+
import { readSkillResource } from "./read-resource.js";
|
|
4
|
+
import { renderLoadedSkill, renderSkillCatalog } from "./render.js";
|
|
5
|
+
import { resolveSkill, resolveSkillCatalog } from "./resolve-catalog.js";
|
|
6
|
+
|
|
7
|
+
export { };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createLogger } from "../../../logging/logger.js";
|
|
2
|
+
import { parseSkill } from "./parse-skill.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/core/agent/skills/load-skills.ts
|
|
7
|
+
const logger = createLogger("agents:skills");
|
|
8
|
+
const SKILL_FILE = "SKILL.md";
|
|
9
|
+
/**
|
|
10
|
+
* Discovers skills under `dir` — one subfolder per skill, each containing a
|
|
11
|
+
* `SKILL.md`. Returns `[]` if the directory does not exist. Folders without a
|
|
12
|
+
* `SKILL.md` are skipped with a warning (they may be non-skill assets).
|
|
13
|
+
*
|
|
14
|
+
* Reads bodies eagerly at load time; the body is only *injected* into model
|
|
15
|
+
* context on demand, so reading a small markdown file at boot is cheap.
|
|
16
|
+
*/
|
|
17
|
+
async function loadSkillsFromDir(dir, source) {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code === "ENOENT") return [];
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
const skillDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
26
|
+
const skills = [];
|
|
27
|
+
for (const name of skillDirs) {
|
|
28
|
+
const skillDir = path.join(dir, name);
|
|
29
|
+
const skillFile = path.join(skillDir, SKILL_FILE);
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = await fs.readFile(skillFile, "utf-8");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err.code === "ENOENT") {
|
|
35
|
+
logger.warn("Skipping '%s': no %s found.", skillDir, SKILL_FILE);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const parsed = parseSkill(raw, skillFile);
|
|
41
|
+
const files = await listResourceFiles(skillDir);
|
|
42
|
+
skills.push({
|
|
43
|
+
name: parsed.name,
|
|
44
|
+
description: parsed.description,
|
|
45
|
+
body: parsed.body,
|
|
46
|
+
source,
|
|
47
|
+
dir: skillDir,
|
|
48
|
+
files,
|
|
49
|
+
allowedTools: parsed.allowedTools
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return skills;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Recursively lists resource files under a skill directory, returning relative
|
|
56
|
+
* posix paths and excluding the top-level `SKILL.md`. Used to build the file
|
|
57
|
+
* manifest `load_skill` returns so the model knows what else it can read.
|
|
58
|
+
*/
|
|
59
|
+
async function listResourceFiles(baseDir) {
|
|
60
|
+
const out = [];
|
|
61
|
+
async function walk(current, rel) {
|
|
62
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
63
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
64
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
65
|
+
if (entry.isDirectory()) await walk(path.join(current, entry.name), childRel);
|
|
66
|
+
else if (entry.isFile()) {
|
|
67
|
+
if (rel === "" && entry.name === SKILL_FILE) continue;
|
|
68
|
+
out.push(childRel);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await walk(baseDir, "");
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { loadSkillsFromDir };
|
|
78
|
+
//# sourceMappingURL=load-skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-skills.js","names":[],"sources":["../../../../src/core/agent/skills/load-skills.ts"],"sourcesContent":["import type { Dirent } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport { parseSkill } from \"./parse-skill\";\nimport type { SkillDefinition, SkillSource } from \"./types\";\n\nconst logger = createLogger(\"agents:skills\");\n\nconst SKILL_FILE = \"SKILL.md\";\n\n/**\n * Discovers skills under `dir` — one subfolder per skill, each containing a\n * `SKILL.md`. Returns `[]` if the directory does not exist. Folders without a\n * `SKILL.md` are skipped with a warning (they may be non-skill assets).\n *\n * Reads bodies eagerly at load time; the body is only *injected* into model\n * context on demand, so reading a small markdown file at boot is cheap.\n */\nexport async function loadSkillsFromDir(\n dir: string,\n source: SkillSource,\n): Promise<SkillDefinition[]> {\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return [];\n }\n throw err;\n }\n\n const skillDirs = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name)\n .sort();\n\n const skills: SkillDefinition[] = [];\n for (const name of skillDirs) {\n const skillDir = path.join(dir, name);\n const skillFile = path.join(skillDir, SKILL_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(skillFile, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n logger.warn(\"Skipping '%s': no %s found.\", skillDir, SKILL_FILE);\n continue;\n }\n throw err;\n }\n\n const parsed = parseSkill(raw, skillFile);\n const files = await listResourceFiles(skillDir);\n skills.push({\n name: parsed.name,\n description: parsed.description,\n body: parsed.body,\n source,\n dir: skillDir,\n files,\n allowedTools: parsed.allowedTools,\n });\n }\n\n return skills;\n}\n\n/**\n * Recursively lists resource files under a skill directory, returning relative\n * posix paths and excluding the top-level `SKILL.md`. Used to build the file\n * manifest `load_skill` returns so the model knows what else it can read.\n */\nasync function listResourceFiles(baseDir: string): Promise<string[]> {\n const out: string[] = [];\n\n async function walk(current: string, rel: string): Promise<void> {\n const entries = await fs.readdir(current, { withFileTypes: true });\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n await walk(path.join(current, entry.name), childRel);\n } else if (entry.isFile()) {\n if (rel === \"\" && entry.name === SKILL_FILE) continue;\n out.push(childRel);\n }\n }\n }\n\n await walk(baseDir, \"\");\n return out;\n}\n"],"mappings":";;;;;;AAQA,MAAM,SAAS,aAAa,gBAAgB;AAE5C,MAAM,aAAa;;;;;;;;;AAUnB,eAAsB,kBACpB,KACA,QAC4B;CAC5B,IAAI;AACJ,KAAI;AACF,YAAU,MAAM,GAAG,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC;UACjD,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO,EAAE;AAEX,QAAM;;CAGR,MAAM,YAAY,QACf,QAAQ,MAAM,EAAE,aAAa,CAAC,CAC9B,KAAK,MAAM,EAAE,KAAK,CAClB,MAAM;CAET,MAAM,SAA4B,EAAE;AACpC,MAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,WAAW,KAAK,KAAK,KAAK,KAAK;EACrC,MAAM,YAAY,KAAK,KAAK,UAAU,WAAW;EACjD,IAAI;AACJ,MAAI;AACF,SAAM,MAAM,GAAG,SAAS,WAAW,QAAQ;WACpC,KAAK;AACZ,OAAK,IAA8B,SAAS,UAAU;AACpD,WAAO,KAAK,+BAA+B,UAAU,WAAW;AAChE;;AAEF,SAAM;;EAGR,MAAM,SAAS,WAAW,KAAK,UAAU;EACzC,MAAM,QAAQ,MAAM,kBAAkB,SAAS;AAC/C,SAAO,KAAK;GACV,MAAM,OAAO;GACb,aAAa,OAAO;GACpB,MAAM,OAAO;GACb;GACA,KAAK;GACL;GACA,cAAc,OAAO;GACtB,CAAC;;AAGJ,QAAO;;;;;;;AAQT,eAAe,kBAAkB,SAAoC;CACnE,MAAM,MAAgB,EAAE;CAExB,eAAe,KAAK,SAAiB,KAA4B;EAC/D,MAAM,UAAU,MAAM,GAAG,QAAQ,SAAS,EAAE,eAAe,MAAM,CAAC;AAClE,OAAK,MAAM,SAAS,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC,EAAE;GACxE,MAAM,WAAW,MAAM,GAAG,IAAI,GAAG,MAAM,SAAS,MAAM;AACtD,OAAI,MAAM,aAAa,CACrB,OAAM,KAAK,KAAK,KAAK,SAAS,MAAM,KAAK,EAAE,SAAS;YAC3C,MAAM,QAAQ,EAAE;AACzB,QAAI,QAAQ,MAAM,MAAM,SAAS,WAAY;AAC7C,QAAI,KAAK,SAAS;;;;AAKxB,OAAM,KAAK,SAAS,GAAG;AACvB,QAAO"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createLogger } from "../../../logging/logger.js";
|
|
2
|
+
import { splitFrontmatter } from "../frontmatter.js";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
|
|
5
|
+
//#region src/core/agent/skills/parse-skill.ts
|
|
6
|
+
const logger = createLogger("agents:skills");
|
|
7
|
+
/**
|
|
8
|
+
* Frontmatter keys AppKit recognizes. Compatibility-first: this is the
|
|
9
|
+
* Anthropic `SKILL.md` surface (`name`, `description`, `license`,
|
|
10
|
+
* `allowed-tools`, `metadata`) so skills authored for Claude Code / Cursor
|
|
11
|
+
* load unmodified. Unknown keys warn rather than error.
|
|
12
|
+
*/
|
|
13
|
+
const KNOWN_SKILL_KEYS = new Set([
|
|
14
|
+
"name",
|
|
15
|
+
"description",
|
|
16
|
+
"license",
|
|
17
|
+
"allowed-tools",
|
|
18
|
+
"metadata"
|
|
19
|
+
]);
|
|
20
|
+
/** Addressable-name guard: no `:` (qualified-name separator), no `/`, no whitespace. */
|
|
21
|
+
const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
22
|
+
/**
|
|
23
|
+
* Parses a `SKILL.md` string. Requires non-empty `name` + `description`;
|
|
24
|
+
* validates the name is addressable; warns on unknown frontmatter keys.
|
|
25
|
+
*/
|
|
26
|
+
function parseSkill(raw, sourcePath) {
|
|
27
|
+
const { yaml: yamlBlock, body } = splitFrontmatter(raw);
|
|
28
|
+
if (yamlBlock === null) throw new Error(`Skill file ${sourcePath} has no YAML frontmatter (expected '--- name/description ---').`);
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = yaml.load(yamlBlock);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new Error(`Invalid YAML frontmatter in ${sourcePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
34
|
+
}
|
|
35
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Skill frontmatter in ${sourcePath} must be a YAML object.`);
|
|
36
|
+
const data = parsed;
|
|
37
|
+
const { name, description } = data;
|
|
38
|
+
if (typeof name !== "string" || name.trim() === "") throw new Error(`Skill ${sourcePath} is missing a non-empty 'name' in frontmatter.`);
|
|
39
|
+
const trimmedName = name.trim();
|
|
40
|
+
if (!NAME_RE.test(trimmedName)) throw new Error(`Skill '${trimmedName}' (${sourcePath}) has an invalid name: use letters, digits, '.', '_', '-' only (no ':', '/', or spaces).`);
|
|
41
|
+
if (typeof description !== "string" || description.trim() === "") throw new Error(`Skill '${trimmedName}' (${sourcePath}) is missing a non-empty 'description' in frontmatter.`);
|
|
42
|
+
for (const key of Object.keys(data)) if (!KNOWN_SKILL_KEYS.has(key)) logger.warn("Ignoring unknown SKILL.md frontmatter key '%s' in %s", key, sourcePath);
|
|
43
|
+
return {
|
|
44
|
+
name: trimmedName,
|
|
45
|
+
description: description.trim(),
|
|
46
|
+
body,
|
|
47
|
+
allowedTools: parseAllowedTools(data["allowed-tools"], trimmedName, sourcePath)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Accepts `allowed-tools` as a string[] or a comma-separated string (both
|
|
52
|
+
* appear in the wild). Returns `undefined` when absent/empty/malformed.
|
|
53
|
+
*/
|
|
54
|
+
function parseAllowedTools(value, skillName, sourcePath) {
|
|
55
|
+
if (value === void 0) return void 0;
|
|
56
|
+
let list;
|
|
57
|
+
if (typeof value === "string") list = value.split(",");
|
|
58
|
+
else if (Array.isArray(value) && value.every((v) => typeof v === "string")) list = value;
|
|
59
|
+
else {
|
|
60
|
+
logger.warn("Ignoring 'allowed-tools' for skill '%s' in %s: expected string or string[]", skillName, sourcePath);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const cleaned = list.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
64
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { parseSkill };
|
|
69
|
+
//# sourceMappingURL=parse-skill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-skill.js","names":[],"sources":["../../../../src/core/agent/skills/parse-skill.ts"],"sourcesContent":["import yaml from \"js-yaml\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport { splitFrontmatter } from \"../frontmatter\";\n\nconst logger = createLogger(\"agents:skills\");\n\n/**\n * Frontmatter keys AppKit recognizes. Compatibility-first: this is the\n * Anthropic `SKILL.md` surface (`name`, `description`, `license`,\n * `allowed-tools`, `metadata`) so skills authored for Claude Code / Cursor\n * load unmodified. Unknown keys warn rather than error.\n */\nconst KNOWN_SKILL_KEYS = new Set([\n \"name\",\n \"description\",\n \"license\",\n \"allowed-tools\",\n \"metadata\",\n]);\n\n/** Addressable-name guard: no `:` (qualified-name separator), no `/`, no whitespace. */\nconst NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;\n\ninterface ParsedSkill {\n name: string;\n description: string;\n body: string;\n allowedTools?: string[];\n}\n\n/**\n * Parses a `SKILL.md` string. Requires non-empty `name` + `description`;\n * validates the name is addressable; warns on unknown frontmatter keys.\n */\nexport function parseSkill(raw: string, sourcePath: string): ParsedSkill {\n const { yaml: yamlBlock, body } = splitFrontmatter(raw);\n if (yamlBlock === null) {\n throw new Error(\n `Skill file ${sourcePath} has no YAML frontmatter (expected '--- name/description ---').`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlBlock);\n } catch (err) {\n throw new Error(\n `Invalid YAML frontmatter in ${sourcePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Skill frontmatter in ${sourcePath} must be a YAML object.`,\n );\n }\n\n const data = parsed as Record<string, unknown>;\n const { name, description } = data;\n\n if (typeof name !== \"string\" || name.trim() === \"\") {\n throw new Error(\n `Skill ${sourcePath} is missing a non-empty 'name' in frontmatter.`,\n );\n }\n const trimmedName = name.trim();\n if (!NAME_RE.test(trimmedName)) {\n throw new Error(\n `Skill '${trimmedName}' (${sourcePath}) has an invalid name: use letters, digits, '.', '_', '-' only (no ':', '/', or spaces).`,\n );\n }\n if (typeof description !== \"string\" || description.trim() === \"\") {\n throw new Error(\n `Skill '${trimmedName}' (${sourcePath}) is missing a non-empty 'description' in frontmatter.`,\n );\n }\n\n for (const key of Object.keys(data)) {\n if (!KNOWN_SKILL_KEYS.has(key)) {\n logger.warn(\n \"Ignoring unknown SKILL.md frontmatter key '%s' in %s\",\n key,\n sourcePath,\n );\n }\n }\n\n return {\n name: trimmedName,\n description: description.trim(),\n body,\n allowedTools: parseAllowedTools(\n data[\"allowed-tools\"],\n trimmedName,\n sourcePath,\n ),\n };\n}\n\n/**\n * Accepts `allowed-tools` as a string[] or a comma-separated string (both\n * appear in the wild). Returns `undefined` when absent/empty/malformed.\n */\nfunction parseAllowedTools(\n value: unknown,\n skillName: string,\n sourcePath: string,\n): string[] | undefined {\n if (value === undefined) return undefined;\n\n let list: string[];\n if (typeof value === \"string\") {\n list = value.split(\",\");\n } else if (\n Array.isArray(value) &&\n value.every((v) => typeof v === \"string\")\n ) {\n list = value as string[];\n } else {\n logger.warn(\n \"Ignoring 'allowed-tools' for skill '%s' in %s: expected string or string[]\",\n skillName,\n sourcePath,\n );\n return undefined;\n }\n\n const cleaned = list.map((s) => s.trim()).filter((s) => s.length > 0);\n return cleaned.length > 0 ? cleaned : undefined;\n}\n"],"mappings":";;;;;AAKA,MAAM,SAAS,aAAa,gBAAgB;;;;;;;AAQ5C,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,MAAM,UAAU;;;;;AAahB,SAAgB,WAAW,KAAa,YAAiC;CACvE,MAAM,EAAE,MAAM,WAAW,SAAS,iBAAiB,IAAI;AACvD,KAAI,cAAc,KAChB,OAAM,IAAI,MACR,cAAc,WAAW,iEAC1B;CAGH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,KAAK,UAAU;UACtB,KAAK;AACZ,QAAM,IAAI,MACR,+BAA+B,WAAW,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC/F;;AAEH,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MACR,wBAAwB,WAAW,yBACpC;CAGH,MAAM,OAAO;CACb,MAAM,EAAE,MAAM,gBAAgB;AAE9B,KAAI,OAAO,SAAS,YAAY,KAAK,MAAM,KAAK,GAC9C,OAAM,IAAI,MACR,SAAS,WAAW,gDACrB;CAEH,MAAM,cAAc,KAAK,MAAM;AAC/B,KAAI,CAAC,QAAQ,KAAK,YAAY,CAC5B,OAAM,IAAI,MACR,UAAU,YAAY,KAAK,WAAW,0FACvC;AAEH,KAAI,OAAO,gBAAgB,YAAY,YAAY,MAAM,KAAK,GAC5D,OAAM,IAAI,MACR,UAAU,YAAY,KAAK,WAAW,wDACvC;AAGH,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,CACjC,KAAI,CAAC,iBAAiB,IAAI,IAAI,CAC5B,QAAO,KACL,wDACA,KACA,WACD;AAIL,QAAO;EACL,MAAM;EACN,aAAa,YAAY,MAAM;EAC/B;EACA,cAAc,kBACZ,KAAK,kBACL,aACA,WACD;EACF;;;;;;AAOH,SAAS,kBACP,OACA,WACA,YACsB;AACtB,KAAI,UAAU,OAAW,QAAO;CAEhC,IAAI;AACJ,KAAI,OAAO,UAAU,SACnB,QAAO,MAAM,MAAM,IAAI;UAEvB,MAAM,QAAQ,MAAM,IACpB,MAAM,OAAO,MAAM,OAAO,MAAM,SAAS,CAEzC,QAAO;MACF;AACL,SAAO,KACL,8EACA,WACA,WACD;AACD;;CAGF,MAAM,UAAU,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,QAAQ,MAAM,EAAE,SAAS,EAAE;AACrE,QAAO,QAAQ,SAAS,IAAI,UAAU"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/core/agent/skills/read-resource.ts
|
|
5
|
+
/** Read cap for a bundled skill resource file (bytes). */
|
|
6
|
+
const MAX_SKILL_FILE_BYTES = 1e6;
|
|
7
|
+
/**
|
|
8
|
+
* Reads a bundled skill resource file from local disk, constrained to the
|
|
9
|
+
* skill's own directory. The path must be relative; `..` traversal, null
|
|
10
|
+
* bytes, and absolute paths are rejected, and the resolved path is verified
|
|
11
|
+
* to stay within `baseDir` (a containment guard the agent markdown loader
|
|
12
|
+
* does not itself apply). Throws when the target is missing, not a file, or
|
|
13
|
+
* exceeds the size cap.
|
|
14
|
+
*/
|
|
15
|
+
async function readSkillResource(baseDir, relPath, maxSize = MAX_SKILL_FILE_BYTES) {
|
|
16
|
+
if (relPath.includes("\0")) throw new Error("Path must not contain null bytes.");
|
|
17
|
+
if (relPath.length > 4096) throw new Error("Path exceeds the maximum length of 4096 characters.");
|
|
18
|
+
if (path.isAbsolute(relPath)) throw new Error("Skill resource path must be relative to the skill directory.");
|
|
19
|
+
if (relPath.split(/[\\/]/).some((segment) => segment === "..")) throw new Error("Path traversal (\"../\") is not allowed.");
|
|
20
|
+
const root = path.resolve(baseDir);
|
|
21
|
+
const abs = path.resolve(root, relPath);
|
|
22
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) throw new Error("Resolved path escapes the skill directory.");
|
|
23
|
+
const stat = await fs.stat(abs);
|
|
24
|
+
if (!stat.isFile()) throw new Error(`Skill resource '${relPath}' is not a file.`);
|
|
25
|
+
if (stat.size > maxSize) throw new Error(`Skill resource '${relPath}' exceeds the ${maxSize}-byte read limit.`);
|
|
26
|
+
return fs.readFile(abs, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { readSkillResource };
|
|
31
|
+
//# sourceMappingURL=read-resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-resource.js","names":[],"sources":["../../../../src/core/agent/skills/read-resource.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/** Read cap for a bundled skill resource file (bytes). */\nconst MAX_SKILL_FILE_BYTES = 1_000_000;\n\n/**\n * Reads a bundled skill resource file from local disk, constrained to the\n * skill's own directory. The path must be relative; `..` traversal, null\n * bytes, and absolute paths are rejected, and the resolved path is verified\n * to stay within `baseDir` (a containment guard the agent markdown loader\n * does not itself apply). Throws when the target is missing, not a file, or\n * exceeds the size cap.\n */\nexport async function readSkillResource(\n baseDir: string,\n relPath: string,\n maxSize = MAX_SKILL_FILE_BYTES,\n): Promise<string> {\n if (relPath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n if (relPath.length > 4096) {\n throw new Error(\"Path exceeds the maximum length of 4096 characters.\");\n }\n if (path.isAbsolute(relPath)) {\n throw new Error(\n \"Skill resource path must be relative to the skill directory.\",\n );\n }\n if (relPath.split(/[\\\\/]/).some((segment) => segment === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n\n const root = path.resolve(baseDir);\n const abs = path.resolve(root, relPath);\n if (abs !== root && !abs.startsWith(root + path.sep)) {\n throw new Error(\"Resolved path escapes the skill directory.\");\n }\n\n const stat = await fs.stat(abs);\n if (!stat.isFile()) {\n throw new Error(`Skill resource '${relPath}' is not a file.`);\n }\n if (stat.size > maxSize) {\n throw new Error(\n `Skill resource '${relPath}' exceeds the ${maxSize}-byte read limit.`,\n );\n }\n\n return fs.readFile(abs, \"utf-8\");\n}\n"],"mappings":";;;;;AAIA,MAAM,uBAAuB;;;;;;;;;AAU7B,eAAsB,kBACpB,SACA,SACA,UAAU,sBACO;AACjB,KAAI,QAAQ,SAAS,KAAK,CACxB,OAAM,IAAI,MAAM,oCAAoC;AAEtD,KAAI,QAAQ,SAAS,KACnB,OAAM,IAAI,MAAM,sDAAsD;AAExE,KAAI,KAAK,WAAW,QAAQ,CAC1B,OAAM,IAAI,MACR,+DACD;AAEH,KAAI,QAAQ,MAAM,QAAQ,CAAC,MAAM,YAAY,YAAY,KAAK,CAC5D,OAAM,IAAI,MAAM,2CAAyC;CAG3D,MAAM,OAAO,KAAK,QAAQ,QAAQ;CAClC,MAAM,MAAM,KAAK,QAAQ,MAAM,QAAQ;AACvC,KAAI,QAAQ,QAAQ,CAAC,IAAI,WAAW,OAAO,KAAK,IAAI,CAClD,OAAM,IAAI,MAAM,6CAA6C;CAG/D,MAAM,OAAO,MAAM,GAAG,KAAK,IAAI;AAC/B,KAAI,CAAC,KAAK,QAAQ,CAChB,OAAM,IAAI,MAAM,mBAAmB,QAAQ,kBAAkB;AAE/D,KAAI,KAAK,OAAO,QACd,OAAM,IAAI,MACR,mBAAmB,QAAQ,gBAAgB,QAAQ,mBACpD;AAGH,QAAO,GAAG,SAAS,KAAK,QAAQ"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/core/agent/skills/render.ts
|
|
2
|
+
/**
|
|
3
|
+
* Renders the always-on skill catalog block appended to an agent's system
|
|
4
|
+
* prompt. Lists each visible skill's name + description and tells the model to
|
|
5
|
+
* call `load_skill` before acting on a matching task.
|
|
6
|
+
*/
|
|
7
|
+
function renderSkillCatalog(entries) {
|
|
8
|
+
return [
|
|
9
|
+
"## Available skills",
|
|
10
|
+
"When a task matches one of these skills, call the `load_skill` tool with the skill's exact name to load its full instructions before proceeding.",
|
|
11
|
+
"",
|
|
12
|
+
...entries.map((e) => `- **${e.name}**: ${e.description}`)
|
|
13
|
+
].join("\n");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Renders the tool-result payload returned by `load_skill`: the skill body
|
|
17
|
+
* plus a manifest of bundled files (readable via `read_skill_file`) and any
|
|
18
|
+
* advisory `allowed-tools` hint.
|
|
19
|
+
*/
|
|
20
|
+
function renderLoadedSkill(skill) {
|
|
21
|
+
const parts = [
|
|
22
|
+
`# Skill: ${skill.name}`,
|
|
23
|
+
"",
|
|
24
|
+
skill.body
|
|
25
|
+
];
|
|
26
|
+
if (skill.files.length > 0) parts.push("", "## Bundled files", "Read any of these with the `read_skill_file` tool (pass this skill's name and the file path):", ...skill.files.map((f) => `- ${f}`));
|
|
27
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) parts.push("", `_Suggested tools for this skill: ${skill.allowedTools.join(", ")}._`);
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { renderLoadedSkill, renderSkillCatalog };
|
|
33
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","names":[],"sources":["../../../../src/core/agent/skills/render.ts"],"sourcesContent":["import type { SkillCatalogEntry, SkillDefinition } from \"./types\";\n\n/**\n * Renders the always-on skill catalog block appended to an agent's system\n * prompt. Lists each visible skill's name + description and tells the model to\n * call `load_skill` before acting on a matching task.\n */\nexport function renderSkillCatalog(entries: SkillCatalogEntry[]): string {\n return [\n \"## Available skills\",\n \"When a task matches one of these skills, call the `load_skill` tool with the skill's exact name to load its full instructions before proceeding.\",\n \"\",\n ...entries.map((e) => `- **${e.name}**: ${e.description}`),\n ].join(\"\\n\");\n}\n\n/**\n * Renders the tool-result payload returned by `load_skill`: the skill body\n * plus a manifest of bundled files (readable via `read_skill_file`) and any\n * advisory `allowed-tools` hint.\n */\nexport function renderLoadedSkill(skill: SkillDefinition): string {\n const parts = [`# Skill: ${skill.name}`, \"\", skill.body];\n\n if (skill.files.length > 0) {\n parts.push(\n \"\",\n \"## Bundled files\",\n \"Read any of these with the `read_skill_file` tool (pass this skill's name and the file path):\",\n ...skill.files.map((f) => `- ${f}`),\n );\n }\n\n if (skill.allowedTools && skill.allowedTools.length > 0) {\n parts.push(\n \"\",\n `_Suggested tools for this skill: ${skill.allowedTools.join(\", \")}._`,\n );\n }\n\n return parts.join(\"\\n\");\n}\n"],"mappings":";;;;;;AAOA,SAAgB,mBAAmB,SAAsC;AACvE,QAAO;EACL;EACA;EACA;EACA,GAAG,QAAQ,KAAK,MAAM,OAAO,EAAE,KAAK,MAAM,EAAE,cAAc;EAC3D,CAAC,KAAK,KAAK;;;;;;;AAQd,SAAgB,kBAAkB,OAAgC;CAChE,MAAM,QAAQ;EAAC,YAAY,MAAM;EAAQ;EAAI,MAAM;EAAK;AAExD,KAAI,MAAM,MAAM,SAAS,EACvB,OAAM,KACJ,IACA,oBACA,iGACA,GAAG,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,CACpC;AAGH,KAAI,MAAM,gBAAgB,MAAM,aAAa,SAAS,EACpD,OAAM,KACJ,IACA,oCAAoC,MAAM,aAAa,KAAK,KAAK,CAAC,IACnE;AAGH,QAAO,MAAM,KAAK,KAAK"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createLogger } from "../../../logging/logger.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/agent/skills/resolve-catalog.ts
|
|
4
|
+
const logger = createLogger("agents:skills");
|
|
5
|
+
/** Qualified-name scope prefix per source, used only on cross-source collision. */
|
|
6
|
+
const SCOPE_BY_SOURCE = {
|
|
7
|
+
"bundle-agent": "agent",
|
|
8
|
+
"bundle-global": "bundle",
|
|
9
|
+
volume: "volume"
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Applies visibility (per-agent auto; global opt-in or auto-inherit) then
|
|
13
|
+
* collision handling: a unique name is addressable bare; a name provided by
|
|
14
|
+
* multiple sources becomes `<scope>:name` per source and the bare name is
|
|
15
|
+
* marked ambiguous (addressing it errors with the alternatives). Two skills
|
|
16
|
+
* with the same name from the *same* source is a fatal config error.
|
|
17
|
+
*/
|
|
18
|
+
function resolveSkillCatalog(input) {
|
|
19
|
+
const { agentName, agentSkillNames, perAgentSkills, globalSkills, autoInherit } = input;
|
|
20
|
+
const visible = [...perAgentSkills];
|
|
21
|
+
if (autoInherit) visible.push(...globalSkills);
|
|
22
|
+
else if (agentSkillNames && agentSkillNames.length > 0) {
|
|
23
|
+
const wanted = new Set(agentSkillNames);
|
|
24
|
+
for (const skill of globalSkills) if (wanted.has(skill.name)) visible.push(skill);
|
|
25
|
+
const localNames = new Set(perAgentSkills.map((s) => s.name));
|
|
26
|
+
const globalNames = new Set(globalSkills.map((s) => s.name));
|
|
27
|
+
for (const want of agentSkillNames) if (!globalNames.has(want) && !localNames.has(want)) logger.warn("Agent '%s' lists skill '%s' in 'skills:', but no global or per-agent skill with that name exists.", agentName, want);
|
|
28
|
+
}
|
|
29
|
+
const byName = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const skill of visible) {
|
|
31
|
+
const group = byName.get(skill.name) ?? [];
|
|
32
|
+
group.push(skill);
|
|
33
|
+
byName.set(skill.name, group);
|
|
34
|
+
}
|
|
35
|
+
const byAddress = /* @__PURE__ */ new Map();
|
|
36
|
+
const ambiguous = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const [name, group] of byName) {
|
|
38
|
+
if (group.length === 1) {
|
|
39
|
+
byAddress.set(name, group[0]);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const alternatives = [];
|
|
43
|
+
for (const skill of group) {
|
|
44
|
+
const qualified = `${SCOPE_BY_SOURCE[skill.source]}:${name}`;
|
|
45
|
+
const existing = byAddress.get(qualified);
|
|
46
|
+
if (existing) throw new Error(`Agent '${agentName}': two '${skill.source}' skills are both named '${name}' (${existing.dir} and ${skill.dir}). Skill names must be unique within a source.`);
|
|
47
|
+
byAddress.set(qualified, skill);
|
|
48
|
+
alternatives.push(qualified);
|
|
49
|
+
}
|
|
50
|
+
alternatives.sort();
|
|
51
|
+
ambiguous.set(name, alternatives);
|
|
52
|
+
logger.warn("Agent '%s': skill name '%s' is provided by multiple sources; address it as %s.", agentName, name, alternatives.join(" or "));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
byAddress,
|
|
56
|
+
ambiguous,
|
|
57
|
+
catalog: [...byAddress.entries()].map(([address, skill]) => ({
|
|
58
|
+
name: address,
|
|
59
|
+
description: skill.description
|
|
60
|
+
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves a requested skill name (bare or qualified) against a catalog.
|
|
65
|
+
* Throws a helpful error on ambiguous or unknown names.
|
|
66
|
+
*/
|
|
67
|
+
function resolveSkill(catalog, requested) {
|
|
68
|
+
const direct = catalog.byAddress.get(requested);
|
|
69
|
+
if (direct) return direct;
|
|
70
|
+
const alternatives = catalog.ambiguous.get(requested);
|
|
71
|
+
if (alternatives) throw new Error(`Skill '${requested}' is ambiguous; specify one of: ${alternatives.join(", ")}.`);
|
|
72
|
+
const available = [...catalog.byAddress.keys()].sort().join(", ") || "<none>";
|
|
73
|
+
throw new Error(`Unknown skill '${requested}'. Available: ${available}.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { resolveSkill, resolveSkillCatalog };
|
|
78
|
+
//# sourceMappingURL=resolve-catalog.js.map
|