@plasm_lang/vercel-agent 0.3.77 → 0.3.80

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.
Files changed (58) hide show
  1. package/README.md +9 -2
  2. package/agent/instructions.md +1 -2
  3. package/bin/plasm-agent.mjs +5 -7
  4. package/package.json +29 -19
  5. package/scripts/build-stubs.ts +27 -0
  6. package/scripts/plasm-cli.ts +162 -0
  7. package/scripts/register-plasm-loader.mjs +6 -0
  8. package/scripts/run-plasm-cli.mjs +41 -0
  9. package/src/cli/init-scaffold.ts +198 -0
  10. package/src/cli/init.ts +20 -208
  11. package/src/cli/nitro-dev.ts +69 -0
  12. package/src/cli/node-dev-imports.ts +14 -0
  13. package/src/engine/create-host-transport.ts +7 -0
  14. package/src/index.ts +1 -1
  15. package/src/package-version.ts +14 -0
  16. package/src/project-info.ts +2 -1
  17. package/src/stubs/catalog-client.ts +2 -2
  18. package/src/tools/harness-tools.ts +42 -41
  19. package/src/tools/plasm-tools.ts +64 -55
  20. package/src/tools/tool-input.ts +9 -0
  21. package/templates/mcp-radar/.vercelignore +3 -0
  22. package/templates/mcp-radar/README.md +139 -0
  23. package/templates/mcp-radar/agent/agent.ts +38 -0
  24. package/templates/mcp-radar/agent/catalogs/hackernews/README.md +23 -0
  25. package/templates/mcp-radar/agent/catalogs/hackernews/domain.yaml +329 -0
  26. package/templates/mcp-radar/agent/catalogs/hackernews/eval/cases.yaml +112 -0
  27. package/templates/mcp-radar/agent/catalogs/hackernews/mappings.yaml +129 -0
  28. package/templates/mcp-radar/agent/catalogs/tavily/README.md +218 -0
  29. package/templates/mcp-radar/agent/catalogs/tavily/domain.yaml +373 -0
  30. package/templates/mcp-radar/agent/catalogs/tavily/eval/cases.yaml +56 -0
  31. package/templates/mcp-radar/agent/catalogs/tavily/eval/google-gemma-3-4b-it.latest.human.txt +67 -0
  32. package/templates/mcp-radar/agent/catalogs/tavily/eval/google-gemma-3-4b-it.latest.json +363 -0
  33. package/templates/mcp-radar/agent/catalogs/tavily/mappings.yaml +171 -0
  34. package/templates/mcp-radar/agent/channels/mcp-radar.ts +85 -0
  35. package/templates/mcp-radar/agent/hooks/proof-audit.ts +10 -0
  36. package/templates/mcp-radar/agent/instructions.md +24 -0
  37. package/templates/mcp-radar/agent/research/mcp-innovations-proof.md +4 -0
  38. package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +14 -0
  39. package/templates/mcp-radar/agent/skills/mcp-proof-format.md +19 -0
  40. package/templates/mcp-radar/api/[[...path]].ts +23 -0
  41. package/templates/mcp-radar/evals/mcp-radar-discover.eval.ts +14 -0
  42. package/templates/mcp-radar/lib/proof-store.ts +265 -0
  43. package/templates/mcp-radar/lib/run-radar.ts +214 -0
  44. package/templates/mcp-radar/nitro.config.ts +17 -0
  45. package/templates/mcp-radar/package.json +14 -0
  46. package/templates/mcp-radar/public/index.html +10 -0
  47. package/templates/mcp-radar/routes/[...path].ts +29 -0
  48. package/templates/mcp-radar/scripts/run-evals.ts +46 -0
  49. package/templates/mcp-radar/scripts/smoke-channel.ts +66 -0
  50. package/templates/mcp-radar/vercel.json +15 -0
  51. package/templates/scaffold/.vercelignore +3 -0
  52. package/templates/scaffold/api/[[...path]].ts +23 -0
  53. package/templates/scaffold/nitro.config.ts +17 -0
  54. package/templates/scaffold/public/index.html +10 -0
  55. package/templates/scaffold/public/index.mcp-radar.html +10 -0
  56. package/templates/scaffold/routes/[...path].ts +29 -0
  57. package/templates/scaffold/vercel.default.json +4 -0
  58. package/templates/scaffold/vercel.mcp-radar.json +15 -0
package/src/cli/init.ts CHANGED
@@ -1,105 +1,17 @@
1
- import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
 
5
- import { FRAMEWORK_VERSION } from "../project-info.js";
4
+ import {
5
+ blankPackageJsonScaffold,
6
+ exists,
7
+ patchProjectPackageJson,
8
+ plasmAgentPackageRoot,
9
+ runTemplateInit,
10
+ writeDeployScaffold,
11
+ } from "./init-scaffold.js";
6
12
  import type { ResolvedAgentProject } from "./project-root.js";
7
13
 
8
- export interface InitOptions {
9
- template?: string;
10
- }
11
-
12
- const SKIP_TEMPLATE_DIRS = new Set(["node_modules", ".plasm"]);
13
- const SKIP_TEMPLATE_FILES = new Set(["package-lock.json"]);
14
-
15
- function plasmAgentPackageRoot(): string {
16
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
17
- }
18
-
19
- function resolveTemplateDir(template: string): string {
20
- const templates: Record<string, string> = {
21
- "mcp-radar": path.join(plasmAgentPackageRoot(), "../../../examples/mcp-radar-agent"),
22
- };
23
- const dir = templates[template];
24
- if (!dir) {
25
- throw new Error(
26
- `Unknown template "${template}". Available: ${Object.keys(templates).join(", ")}`,
27
- );
28
- }
29
- return dir;
30
- }
31
-
32
- function shouldCopyTemplateEntry(src: string, templateRoot: string): boolean {
33
- const rel = path.relative(templateRoot, src);
34
- if (!rel || rel === "") return true;
35
- const parts = rel.split(path.sep);
36
- if (parts.some((part) => SKIP_TEMPLATE_DIRS.has(part))) return false;
37
- if (SKIP_TEMPLATE_FILES.has(path.basename(src))) return false;
38
- return true;
39
- }
40
-
41
- async function copyTemplate(templateRoot: string, projectRoot: string): Promise<void> {
42
- await cp(templateRoot, projectRoot, {
43
- recursive: true,
44
- filter: (src) => src === templateRoot || shouldCopyTemplateEntry(src, templateRoot),
45
- });
46
- }
47
-
48
- async function patchBootstrapPackageJson(
49
- projectRoot: string,
50
- packageRoot: string,
51
- ): Promise<void> {
52
- const pkgPath = path.join(projectRoot, "package.json");
53
- const raw = await readFile(pkgPath, "utf8");
54
- const pkg = JSON.parse(raw) as {
55
- name?: string;
56
- scripts?: Record<string, string>;
57
- dependencies?: Record<string, string>;
58
- };
59
- pkg.name = path.basename(projectRoot);
60
- const engineRoot = path.resolve(packageRoot, "../plasm-engine");
61
- pkg.dependencies = {
62
- ...pkg.dependencies,
63
- "@plasm_lang/vercel-agent": `file:${path.resolve(packageRoot)}`,
64
- "@plasm_lang/engine": `file:${engineRoot}`,
65
- "@vercel/blob": "^0.27.3",
66
- "@vercel/functions": "^3.4.3",
67
- "@vercel/kv": "^3.0.0",
68
- };
69
- const nodeRunner =
70
- "node --experimental-strip-types --experimental-transform-types ./node_modules/@plasm_lang/vercel-agent/scripts/plasm-node.mjs";
71
- pkg.scripts = {
72
- build: "plasm-agent build",
73
- "vercel-build": "plasm-agent build",
74
- dev: "plasm-agent dev",
75
- info: "plasm-agent info",
76
- deploy: "vercel deploy",
77
- ...pkg.scripts,
78
- eval: `${nodeRunner} scripts/run-evals.ts`,
79
- "smoke:channel": `${nodeRunner} scripts/smoke-channel.ts`,
80
- };
81
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
82
- }
83
-
84
- async function runTemplateInit(
85
- targetDir: string,
86
- template: string,
87
- ): Promise<ResolvedAgentProject> {
88
- const projectRoot = path.resolve(targetDir);
89
- const agentRoot = path.join(projectRoot, "agent");
90
- if (await exists(path.join(agentRoot, "agent.ts"))) {
91
- throw new Error(`agent/agent.ts already exists in ${projectRoot}`);
92
- }
93
- const templateRoot = resolveTemplateDir(template);
94
- if (!(await exists(templateRoot))) {
95
- throw new Error(`Template source missing: ${templateRoot}`);
96
- }
97
- await mkdir(projectRoot, { recursive: true });
98
- await copyTemplate(templateRoot, projectRoot);
99
- await patchBootstrapPackageJson(projectRoot, plasmAgentPackageRoot());
100
- await writeVercelScaffold(projectRoot, template);
101
- return { projectRoot, agentRoot };
102
- }
14
+ export type { InitOptions } from "./init-scaffold.js";
103
15
 
104
16
  const AGENT_TS = `import path from "node:path";
105
17
  import { fileURLToPath } from "node:url";
@@ -196,113 +108,12 @@ PLASM_TENANT_SCOPE=local
196
108
  PORT=3000
197
109
  `;
198
110
 
199
- const VERCEL_JSON_DEFAULT = `{
200
- "buildCommand": "plasm-agent build",
201
- "rewrites": [{ "source": "/(.*)", "destination": "/api/$1" }]
202
- }
203
- `;
204
-
205
- const VERCEL_JSON_MCP_RADAR = `{
206
- "buildCommand": "plasm-agent build",
207
- "rewrites": [{ "source": "/(.*)", "destination": "/api/$1" }],
208
- "crons": [
209
- {
210
- "path": "/internal/cron/mcp-radar-scan",
211
- "schedule": "0 */6 * * *"
212
- }
213
- ],
214
- "functions": {
215
- "api/**": {
216
- "maxDuration": 300
217
- }
218
- }
219
- }
220
- `;
221
-
222
- const VERCELIGNORE = `node_modules
223
- .env*
224
- agent/.plasm/research
225
- `;
226
-
227
- const API_HANDLER_TS = `import path from "node:path";
228
- import { fileURLToPath } from "node:url";
229
-
230
- import agentDefinition from "../agent/agent.js";
231
- import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent";
232
-
233
- const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
234
- const agentRoot = path.join(packageRoot, "agent");
235
-
236
- let app: Awaited<ReturnType<typeof createPlasmApp>> | undefined;
237
-
238
- export default async function handler(
239
- req: import("node:http").IncomingMessage,
240
- res: import("node:http").ServerResponse,
241
- ): Promise<void> {
242
- app ??= await createPlasmApp({
243
- agentRoot,
244
- definition: agentDefinition,
245
- mode: "prod",
246
- sessions: false,
247
- });
248
- await vercelPlasmHandler(app)(req, res);
249
- }
250
- `;
251
-
252
- const PACKAGE_JSON_TEMPLATE = {
253
- name: "my-plasm-agent",
254
- private: true,
255
- type: "module",
256
- scripts: {
257
- dev: "plasm-agent dev",
258
- build: "plasm-agent build",
259
- "vercel-build": "plasm-agent build",
260
- info: "plasm-agent info",
261
- deploy: "vercel deploy",
262
- },
263
- dependencies: {} as Record<string, string>,
264
- };
265
-
266
- async function writeVercelScaffold(
267
- projectRoot: string,
268
- template?: string,
269
- ): Promise<void> {
270
- const vercelJson =
271
- template === "mcp-radar" ? VERCEL_JSON_MCP_RADAR : VERCEL_JSON_DEFAULT;
272
- await mkdir(path.join(projectRoot, "api"), { recursive: true });
273
- await writeFile(path.join(projectRoot, "vercel.json"), vercelJson, "utf8");
274
- await writeFile(path.join(projectRoot, ".vercelignore"), VERCELIGNORE, "utf8");
275
- await writeFile(
276
- path.join(projectRoot, "api", "[[...path]].ts"),
277
- API_HANDLER_TS,
278
- "utf8",
279
- );
280
- }
281
-
282
- function packageJsonScaffold(): Record<string, unknown> {
283
- return {
284
- ...PACKAGE_JSON_TEMPLATE,
285
- dependencies: {
286
- "@plasm_lang/vercel-agent": `^${FRAMEWORK_VERSION}`,
287
- },
288
- };
289
- }
290
-
291
- async function exists(p: string): Promise<boolean> {
292
- try {
293
- await access(p);
294
- return true;
295
- } catch {
296
- return false;
297
- }
298
- }
299
-
300
111
  export async function runPlasmInit(
301
112
  targetDir: string,
302
- options?: InitOptions,
113
+ options?: import("./init-scaffold.js").InitOptions,
303
114
  ): Promise<ResolvedAgentProject> {
304
115
  if (options?.template) {
305
- return runTemplateInit(targetDir, options.template);
116
+ return runTemplateInit(targetDir, options.template, options);
306
117
  }
307
118
 
308
119
  const projectRoot = path.resolve(targetDir);
@@ -328,20 +139,19 @@ export async function runPlasmInit(
328
139
  if (!(await exists(path.join(projectRoot, "package.json")))) {
329
140
  await writeFile(
330
141
  path.join(projectRoot, "package.json"),
331
- `${JSON.stringify(packageJsonScaffold(), null, 2)}\n`,
142
+ `${JSON.stringify(blankPackageJsonScaffold(), null, 2)}\n`,
332
143
  "utf8",
333
144
  );
334
- await patchBootstrapPackageJson(projectRoot, plasmAgentPackageRoot());
145
+ await patchProjectPackageJson(projectRoot, plasmAgentPackageRoot(), { npm: options?.npm });
335
146
  }
336
147
 
337
- await writeVercelScaffold(projectRoot);
338
-
148
+ await writeDeployScaffold(projectRoot);
339
149
  return { projectRoot, agentRoot };
340
150
  }
341
151
 
342
152
  export function formatInitSuccess(
343
153
  project: ResolvedAgentProject,
344
- options?: InitOptions,
154
+ options?: import("./init-scaffold.js").InitOptions,
345
155
  ): string {
346
156
  if (options?.template === "mcp-radar") {
347
157
  return [
@@ -353,7 +163,8 @@ export function formatInitSuccess(
353
163
  " plasm-agent link # pull AI_GATEWAY_API_KEY from Vercel",
354
164
  " plasm-agent build",
355
165
  " npm run smoke:channel",
356
- " plasm-agent dev # local dev server + TUI",
166
+ " plasm-agent dev # Nitro dev server (Vercel routing parity)",
167
+ " plasm-agent dev --interactive # optional TUI + sessions + hot reload",
357
168
  " vercel deploy # production (channels + cron)",
358
169
  ].join("\n");
359
170
  }
@@ -366,7 +177,8 @@ export function formatInitSuccess(
366
177
  " npm install",
367
178
  " plasm-agent link # pull AI_GATEWAY_API_KEY from Vercel",
368
179
  " plasm-agent build # generate CGS stubs + discovery manifest",
369
- " plasm-agent dev # HTTP dev server (sessions + SSE)",
180
+ " plasm-agent dev # Nitro dev server (Vercel routing parity)",
181
+ " plasm-agent dev --interactive # optional TUI + sessions",
370
182
  " vercel deploy # production on Vercel",
371
183
  ].join("\n");
372
184
  }
@@ -0,0 +1,69 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { nitroDevNodeOptions } from "./node-dev-imports.js";
7
+ import type { ResolvedAgentProject } from "./project-root.js";
8
+
9
+ function plasmAgentPackageRoot(): string {
10
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
11
+ }
12
+
13
+ async function assertNitroScaffold(projectRoot: string): Promise<void> {
14
+ const required = [
15
+ path.join(projectRoot, "nitro.config.ts"),
16
+ path.join(projectRoot, "routes", "[...path].ts"),
17
+ ];
18
+ for (const filePath of required) {
19
+ try {
20
+ await access(filePath);
21
+ } catch {
22
+ throw new Error(
23
+ `Missing ${path.relative(projectRoot, filePath)} — run \`plasm-agent init\` to scaffold Nitro dev`,
24
+ );
25
+ }
26
+ }
27
+ }
28
+
29
+ function nitroBin(projectRoot: string): string {
30
+ return path.join(projectRoot, "node_modules", ".bin", "nitro");
31
+ }
32
+
33
+ export async function startNitroDevForProject(project: ResolvedAgentProject): Promise<void> {
34
+ await assertNitroScaffold(project.projectRoot);
35
+
36
+ const nodeOptions = nitroDevNodeOptions(plasmAgentPackageRoot());
37
+
38
+ const bin = nitroBin(project.projectRoot);
39
+ try {
40
+ await access(bin);
41
+ } catch {
42
+ throw new Error(
43
+ "nitropack is not installed — run `npm install` in the project root (init adds nitropack as a devDependency)",
44
+ );
45
+ }
46
+
47
+ const child: ChildProcess = spawn(bin, ["dev"], {
48
+ cwd: project.projectRoot,
49
+ env: {
50
+ ...process.env,
51
+ NODE_OPTIONS: nodeOptions,
52
+ },
53
+ stdio: "inherit",
54
+ });
55
+
56
+ const wait = new Promise<number>((resolve, reject) => {
57
+ child.on("error", reject);
58
+ child.on("close", (code) => resolve(code ?? 0));
59
+ });
60
+
61
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
62
+ process.on(signal, () => {
63
+ child.kill(signal);
64
+ });
65
+ }
66
+
67
+ const exitCode = await wait;
68
+ process.exit(exitCode);
69
+ }
@@ -0,0 +1,14 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ /** NODE_OPTIONS imports for Nitro dev (tsx + Plasm .ts resolution). */
8
+ export function nitroDevNodeOptions(packageRoot: string): string {
9
+ const tsxImport = require.resolve("tsx/esm");
10
+ const plasmLoader = pathToFileURL(
11
+ path.join(packageRoot, "scripts", "register-plasm-loader.mjs"),
12
+ ).href;
13
+ return [`--import=${tsxImport}`, `--import=${plasmLoader}`].join(" ");
14
+ }
@@ -1,6 +1,13 @@
1
1
  import { createDefaultHostTransport } from "./host-transport.js";
2
+ import { connectAuthOptionsForEntry } from "./connect-auth.js";
2
3
  import type { HostTransportFn } from "./napi-binding.js";
3
4
 
5
+ /** Outbound HTTP for CGS stub live execute: Connect only when configured for the catalog. */
6
+ export function createStubHostTransport(entryId?: string): HostTransportFn {
7
+ const useConnect = connectAuthOptionsForEntry(entryId)?.connector != null;
8
+ return createDefaultHostTransport({ useConnect });
9
+ }
10
+
4
11
  /** Production host transport: env bearer → Vercel Connect `getToken()` → fetch. */
5
12
  export function createProductionHostTransport(): HostTransportFn {
6
13
  return createDefaultHostTransport({ useConnect: true });
package/src/index.ts CHANGED
@@ -130,7 +130,7 @@ export type {
130
130
  export { StubPlasmEngine, NapiPlasmEngine, createEngine, isNativeEngineAvailable } from "./engine/napi-binding.js";
131
131
  export { createDefaultHostTransport } from "./engine/host-transport.js";
132
132
  export type { HostTransportOptions } from "./engine/host-transport.js";
133
- export { createProductionHostTransport } from "./engine/create-host-transport.js";
133
+ export { createProductionHostTransport, createStubHostTransport } from "./engine/create-host-transport.js";
134
134
  export { loadAgentEnv } from "./load-env.js";
135
135
  export {
136
136
  connectorUidForEntry,
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ let cachedVersion: string | undefined;
6
+
7
+ /** Published semver from @plasm_lang/vercel-agent package.json. */
8
+ export function frameworkPackageVersion(): string {
9
+ if (cachedVersion) return cachedVersion;
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const raw = readFileSync(path.join(packageRoot, "package.json"), "utf8");
12
+ cachedVersion = (JSON.parse(raw) as { version: string }).version;
13
+ return cachedVersion;
14
+ }
@@ -10,6 +10,7 @@ import { isNativeEngineAvailable } from "./engine/napi-binding.js";
10
10
  import { exportScheduleCronManifest } from "./authoring/schedule-manager.js";
11
11
  import { isGatewayConfigured } from "./gateway-model.js";
12
12
  import type { LoadedProjectSlots } from "./authoring/slot-loader.js";
13
+ import { frameworkPackageVersion } from "./package-version.js";
13
14
  import { resolveCatalogLiveHash } from "./stubs/catalog-hash.js";
14
15
  import { stubFreshness } from "./stubs/generator.js";
15
16
 
@@ -21,7 +22,7 @@ export const PLASM_LANGUAGE_TOOLS = [
21
22
  ] as const;
22
23
 
23
24
  export const FRAMEWORK_NAME = "@plasm_lang/vercel-agent";
24
- export const FRAMEWORK_VERSION = "0.0.1";
25
+ export const FRAMEWORK_VERSION = frameworkPackageVersion();
25
26
 
26
27
  export interface CatalogInfoEntry {
27
28
  name: string;
@@ -1,5 +1,5 @@
1
1
  import type { HostTransportFn } from "../engine/napi-binding.js";
2
- import { createProductionHostTransport } from "../engine/create-host-transport.js";
2
+ import { createStubHostTransport } from "../engine/create-host-transport.js";
3
3
  import {
4
4
  createEngine,
5
5
  type DryRunResult,
@@ -143,7 +143,7 @@ export async function executeRows<TRow>(
143
143
  options?.transport ??
144
144
  (process.env.PLASM_STUB_USE_MOCK_TRANSPORT === "1"
145
145
  ? createFixtureMockTransport()
146
- : createProductionHostTransport());
146
+ : createStubHostTransport(builder.entryId));
147
147
 
148
148
  if (typeof bound.run !== "function") {
149
149
  throw new Error("executeRows: program builder missing run()");
@@ -1,58 +1,59 @@
1
- import { tool } from "ai";
1
+ import { tool, type ToolSet } from "ai";
2
2
  import { z } from "zod";
3
3
 
4
4
  import type { SkillDefinition } from "../authoring/define-skill.js";
5
5
  import type { SubagentRegistry } from "../authoring/subagent-loader.js";
6
+ import { toolInput } from "./tool-input.js";
7
+
8
+ const readSkillInputSchema = z.object({
9
+ name: z.string().min(1).describe("Skill name from the index"),
10
+ });
6
11
 
7
12
  export function createHarnessTools(options: {
8
13
  skills?: SkillDefinition[];
9
14
  subagents?: SubagentRegistry;
10
- }) {
15
+ }): ToolSet {
16
+ const tools: ToolSet = {};
11
17
  const skillByName = new Map((options.skills ?? []).map((s) => [s.name, s]));
12
18
 
13
- const readSkill =
14
- skillByName.size > 0
15
- ? {
16
- read_skill: tool({
17
- description:
18
- "Load full text for a filesystem skill by name. Use when the skill index in the system prompt is not enough.",
19
- inputSchema: z.object({
20
- name: z.string().min(1).describe("Skill name from the index"),
21
- }),
22
- execute: async ({ name }: { name: string }) => {
23
- const skill = skillByName.get(name);
24
- if (!skill) {
25
- return `Unknown skill "${name}". Available: ${[...skillByName.keys()].join(", ")}`;
26
- }
27
- return skill.body.trim();
28
- },
29
- }),
19
+ if (skillByName.size > 0) {
20
+ tools.read_skill = tool({
21
+ description:
22
+ "Load full text for a filesystem skill by name. Use when the skill index in the system prompt is not enough.",
23
+ inputSchema: toolInput(readSkillInputSchema),
24
+ execute: async ({ name }: { name: string }) => {
25
+ const skill = skillByName.get(name);
26
+ if (!skill) {
27
+ return `Unknown skill "${name}". Available: ${[...skillByName.keys()].join(", ")}`;
30
28
  }
31
- : {};
29
+ return skill.body.trim();
30
+ },
31
+ });
32
+ }
32
33
 
33
34
  const subagentNames = options.subagents?.list().map((s) => s.name) ?? [];
34
- const delegateSubagent =
35
- subagentNames.length > 0 && options.subagents
36
- ? {
37
- delegate_subagent: tool({
38
- description:
39
- "Delegate a sub-task to a filesystem-isolated child agent. Each subagent has its own catalogs and session scope.",
40
- inputSchema: z.object({
41
- name: z
42
- .string()
43
- .min(1)
44
- .describe(`Subagent name. One of: ${subagentNames.join(", ")}`),
45
- message: z.string().min(1).describe("User message for the child agent turn"),
46
- }),
47
- execute: async ({ name, message }: { name: string; message: string }) => {
48
- const result = await options.subagents!.delegate(name, message);
49
- return `${result.text}\n\n(steps: ${result.steps})`;
50
- },
51
- }),
52
- }
53
- : {};
35
+ if (subagentNames.length > 0 && options.subagents) {
36
+ const subagents = options.subagents;
37
+ tools.delegate_subagent = tool({
38
+ description:
39
+ "Delegate a sub-task to a filesystem-isolated child agent. Each subagent has its own catalogs and session scope.",
40
+ inputSchema: toolInput(
41
+ z.object({
42
+ name: z
43
+ .string()
44
+ .min(1)
45
+ .describe(`Subagent name. One of: ${subagentNames.join(", ")}`),
46
+ message: z.string().min(1).describe("User message for the child agent turn"),
47
+ }),
48
+ ),
49
+ execute: async ({ name, message }: { name: string; message: string }) => {
50
+ const result = await subagents.delegate(name, message);
51
+ return `${result.text}\n\n(steps: ${result.steps})`;
52
+ },
53
+ });
54
+ }
54
55
 
55
- return { ...readSkill, ...delegateSubagent };
56
+ return tools;
56
57
  }
57
58
 
58
59
  export function renderSkillIndex(skills: SkillDefinition[]): string {