@plasm_lang/vercel-agent 0.3.77 → 0.3.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,9 +152,16 @@ npm run smoke:stubs:matrix
152
152
  npm run agent -- "your prompt" # requires AI_GATEWAY_API_KEY (Vercel AI Gateway)
153
153
  ```
154
154
 
155
- ### Dev server (`npm run dev` / `plasm-agent dev`)
155
+ ### Dev server (`plasm-agent dev`)
156
156
 
157
- When stdin is a TTY, `plasm-agent dev` starts the server **and** opens an interactive terminal client (eve-style). Use `--no-tui` or `PLASM_DEV_NO_TUI=1` for headless mode. Attach later with `plasm-agent chat --url http://127.0.0.1:3000`.
157
+ **Default:** Nitro dev server with **Vercel routing parity** (channels, cron, `/plasm/v1/info`) same `vercelPlasmHandler` as deploy. Init scaffolds `nitro.config.ts` + `routes/[...path].ts` and adds `nitropack` as a devDependency.
158
+
159
+ ```bash
160
+ plasm-agent dev # Nitro (default)
161
+ plasm-agent dev --interactive # in-process server + TUI when TTY
162
+ plasm-agent dev --interactive --no-tui
163
+ plasm-agent chat --url http://127.0.0.1:3000
164
+ ```
158
165
 
159
166
  Slash commands in the TUI: `/help`, `/info`, `/model`, `/channels`, `/catalogs`, `/new`, `/quit`.
160
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasm_lang/vercel-agent",
3
- "version": "0.3.77",
3
+ "version": "0.3.78",
4
4
  "description": "Catalog-native TypeScript agent framework (Plasm CGS/CML, Vercel AI SDK, Nitro-oriented)",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -26,6 +26,9 @@
26
26
  "src",
27
27
  "bin",
28
28
  "scripts/plasm-node.mjs",
29
+ "scripts/plasm-cli.ts",
30
+ "scripts/build-stubs.ts",
31
+ "scripts/register-plasm-loader.mjs",
29
32
  "scripts/resolve-ts-extension.mjs",
30
33
  "agent",
31
34
  "README.md"
@@ -54,7 +57,7 @@
54
57
  "dependencies": {
55
58
  "@ai-sdk/otel": "^1.0.0-beta.6",
56
59
  "@opentelemetry/api": "^1.9.0",
57
- "@plasm_lang/engine": "^0.3.77",
60
+ "@plasm_lang/engine": "^0.3.78",
58
61
  "@vercel/blob": "^0.27.3",
59
62
  "@vercel/connect": "^0.2.6",
60
63
  "@vercel/kv": "^3.0.0",
@@ -63,7 +66,7 @@
63
66
  "js-yaml": "^4.1.0",
64
67
  "pg": "^8.22.0",
65
68
  "workflow": "^4.5.0",
66
- "zod": "^3.24.0"
69
+ "zod": "^3.25.76"
67
70
  },
68
71
  "devDependencies": {
69
72
  "@types/js-yaml": "^4.0.9",
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scan agent/catalogs/ and emit TypeScript stubs to agent/.plasm/stubs/<entry_id>.ts
4
+ */
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import { generateAllStubs } from "../src/stubs/generator.js";
9
+
10
+ const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
11
+ const agentRoot = path.join(packageRoot, "agent");
12
+
13
+ async function main() {
14
+ const results = await generateAllStubs(agentRoot);
15
+ if (!results.length) {
16
+ console.error("no catalogs found under agent/catalogs/");
17
+ process.exit(1);
18
+ }
19
+ for (const result of results) {
20
+ console.log(`${result.entryId}\t${result.catalogCgsHash}\t${result.outPath}`);
21
+ }
22
+ }
23
+
24
+ main().catch((err) => {
25
+ console.error(err);
26
+ process.exit(1);
27
+ });
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TypeScript CLI for @plasm_lang/vercel-agent (not the Rust plasm-server TUI).
4
+ */
5
+ import path from "node:path";
6
+
7
+ import { runPlasmBuild } from "../src/cli/build.js";
8
+ import {
9
+ installDevServerShutdown,
10
+ startDevServerForProject,
11
+ } from "../src/cli/dev.js";
12
+ import { startNitroDevForProject } from "../src/cli/nitro-dev.js";
13
+ import { runDevTui } from "../src/dev/client/repl.js";
14
+ import { collectProjectInfo, formatPlasmInfoHuman } from "../src/cli/info.js";
15
+ import { runPlasmInit, formatInitSuccess } from "../src/cli/init.js";
16
+ import { runPlasmLink } from "../src/cli/link.js";
17
+ import {
18
+ requireAgentProject,
19
+ readPackageName,
20
+ resolveAgentProject,
21
+ } from "../src/cli/project-root.js";
22
+ import { loadAgentEnv } from "../src/load-env.js";
23
+
24
+ loadAgentEnv();
25
+
26
+ const args = process.argv.slice(2);
27
+ const command = args[0];
28
+
29
+ function flag(name: string): boolean {
30
+ return args.includes(name);
31
+ }
32
+
33
+ function optionValue(flagName: string): string | undefined {
34
+ const index = args.indexOf(flagName);
35
+ if (index < 0) return undefined;
36
+ const value = args[index + 1];
37
+ if (!value || value.startsWith("-")) return undefined;
38
+ return value;
39
+ }
40
+
41
+ function initTargetDir(): string {
42
+ for (let i = 1; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg.startsWith("-")) {
45
+ if (arg === "--template") i++;
46
+ continue;
47
+ }
48
+ return arg;
49
+ }
50
+ return process.cwd();
51
+ }
52
+
53
+ async function cmdInit(): Promise<void> {
54
+ const template = optionValue("--template");
55
+ const target = initTargetDir();
56
+ const project = await runPlasmInit(target, { template });
57
+ console.log(formatInitSuccess(project, { template }));
58
+ }
59
+
60
+ async function cmdInfo(): Promise<void> {
61
+ const project = await requireAgentProject();
62
+ const packageName = await readPackageName(project.projectRoot);
63
+ const info = await collectProjectInfo({
64
+ projectRoot: project.projectRoot,
65
+ agentRoot: project.agentRoot,
66
+ packageName,
67
+ });
68
+ if (flag("--json")) {
69
+ console.log(JSON.stringify(info, null, 2));
70
+ return;
71
+ }
72
+ console.log(formatPlasmInfoHuman(info));
73
+ }
74
+
75
+ async function cmdLink(): Promise<void> {
76
+ const project = await requireAgentProject();
77
+ const result = await runPlasmLink(project.projectRoot);
78
+ for (const line of result.messages) console.log(line);
79
+ if (!result.linked && !result.envPulled) process.exitCode = 1;
80
+ }
81
+
82
+ async function cmdBuild(): Promise<void> {
83
+ const project = await requireAgentProject();
84
+ const result = await runPlasmBuild(project);
85
+ console.log(`Built ${result.stubs.length} stub(s)`);
86
+ console.log(`manifest: ${result.manifestPath}`);
87
+ for (const stub of result.stubs) {
88
+ console.log(` - ${stub.entryId} → ${path.relative(project.projectRoot, stub.outPath)}`);
89
+ }
90
+ }
91
+
92
+ async function cmdDev(): Promise<void> {
93
+ const project = await requireAgentProject();
94
+ if (flag("--interactive")) {
95
+ const handle = await startDevServerForProject({
96
+ project,
97
+ tui: flag("--no-tui") ? false : "auto",
98
+ });
99
+ installDevServerShutdown(handle);
100
+ return;
101
+ }
102
+ await startNitroDevForProject(project);
103
+ }
104
+
105
+ async function cmdChat(): Promise<void> {
106
+ const urlFlag = args.find((a, i) => args[i - 1] === "--url");
107
+ const baseUrl = urlFlag ?? process.env.PLASM_DEV_URL ?? "http://127.0.0.1:3000";
108
+ await runDevTui({ baseUrl });
109
+ }
110
+
111
+ async function main(): Promise<void> {
112
+ if (!command || command === "--help" || command === "-h") {
113
+ console.log(`plasm-agent — @plasm_lang/vercel-agent CLI
114
+
115
+ Commands:
116
+ init [dir] [--template NAME] Scaffold agent project (templates: mcp-radar)
117
+ info [--json] Project + catalog diagnostics
118
+ link vercel link + env pull (AI Gateway key)
119
+ build CGS stubs + .plasm/discovery/manifest.json
120
+ dev [--interactive] [--no-tui] Nitro dev (Vercel routing parity; default). --interactive for TUI/sessions.
121
+ chat [--url URL] Terminal client for a running dev server
122
+ `);
123
+ return;
124
+ }
125
+
126
+ switch (command) {
127
+ case "init":
128
+ await cmdInit();
129
+ break;
130
+ case "info": {
131
+ const resolved = await resolveAgentProject();
132
+ if (!resolved) {
133
+ console.error("No agent project found. Run `plasm-agent init` first.");
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ await cmdInfo();
138
+ break;
139
+ }
140
+ case "link":
141
+ await cmdLink();
142
+ break;
143
+ case "build":
144
+ await cmdBuild();
145
+ break;
146
+ case "dev":
147
+ await cmdDev();
148
+ break;
149
+ case "chat":
150
+ await cmdChat();
151
+ break;
152
+ default:
153
+ console.error(`Unknown command: ${command}`);
154
+ process.exitCode = 1;
155
+ }
156
+ }
157
+
158
+ await main().catch((err) => {
159
+ console.error(err);
160
+ process.exit(1);
161
+ });
@@ -0,0 +1,6 @@
1
+ import { register } from "node:module";
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+
5
+ const root = path.dirname(fileURLToPath(import.meta.url));
6
+ register(pathToFileURL(path.join(root, "resolve-ts-extension.mjs")));
package/src/cli/init.ts CHANGED
@@ -9,7 +9,7 @@ export interface InitOptions {
9
9
  template?: string;
10
10
  }
11
11
 
12
- const SKIP_TEMPLATE_DIRS = new Set(["node_modules", ".plasm"]);
12
+ const SKIP_TEMPLATE_DIRS = new Set(["node_modules", ".plasm", ".nitro", ".output", "server"]);
13
13
  const SKIP_TEMPLATE_FILES = new Set(["package-lock.json"]);
14
14
 
15
15
  function plasmAgentPackageRoot(): string {
@@ -55,6 +55,7 @@ async function patchBootstrapPackageJson(
55
55
  name?: string;
56
56
  scripts?: Record<string, string>;
57
57
  dependencies?: Record<string, string>;
58
+ devDependencies?: Record<string, string>;
58
59
  };
59
60
  pkg.name = path.basename(projectRoot);
60
61
  const engineRoot = path.resolve(packageRoot, "../plasm-engine");
@@ -72,12 +73,17 @@ async function patchBootstrapPackageJson(
72
73
  build: "plasm-agent build",
73
74
  "vercel-build": "plasm-agent build",
74
75
  dev: "plasm-agent dev",
76
+ "dev:interactive": "plasm-agent dev --interactive",
75
77
  info: "plasm-agent info",
76
78
  deploy: "vercel deploy",
77
79
  ...pkg.scripts,
78
80
  eval: `${nodeRunner} scripts/run-evals.ts`,
79
81
  "smoke:channel": `${nodeRunner} scripts/smoke-channel.ts`,
80
82
  };
83
+ pkg.devDependencies = {
84
+ ...pkg.devDependencies,
85
+ nitropack: "^2.13.4",
86
+ };
81
87
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
82
88
  }
83
89
 
@@ -98,6 +104,7 @@ async function runTemplateInit(
98
104
  await copyTemplate(templateRoot, projectRoot);
99
105
  await patchBootstrapPackageJson(projectRoot, plasmAgentPackageRoot());
100
106
  await writeVercelScaffold(projectRoot, template);
107
+ await writeNitroScaffold(projectRoot);
101
108
  return { projectRoot, agentRoot };
102
109
  }
103
110
 
@@ -249,18 +256,72 @@ export default async function handler(
249
256
  }
250
257
  `;
251
258
 
259
+ const NITRO_CONFIG_TS = `import { defineNitroConfig } from "nitropack/config";
260
+
261
+ export default defineNitroConfig({
262
+ compatibilityDate: "2026-06-26",
263
+ srcDir: ".",
264
+ ignore: ["api/**"],
265
+ devServer: {
266
+ port: Number(process.env.PORT ?? 3000),
267
+ host: process.env.HOST ?? "127.0.0.1",
268
+ },
269
+ typescript: {
270
+ strict: false,
271
+ },
272
+ externals: {
273
+ inline: ["@plasm_lang/engine"],
274
+ },
275
+ });
276
+ `;
277
+
278
+ const NITRO_CATCHALL_ROUTE_TS = `import path from "node:path";
279
+
280
+ import { fromNodeMiddleware } from "h3";
281
+
282
+ import agentDefinition from "../agent/agent.js";
283
+ import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent";
284
+
285
+ const agentRoot = path.join(process.cwd(), "agent");
286
+
287
+ let appPromise: ReturnType<typeof createPlasmApp> | undefined;
288
+
289
+ async function plasmApp() {
290
+ appPromise ??= createPlasmApp({
291
+ agentRoot,
292
+ definition: agentDefinition,
293
+ mode: "prod",
294
+ sessions: false,
295
+ });
296
+ return appPromise;
297
+ }
298
+
299
+ export default fromNodeMiddleware(async (req, res) => {
300
+ const app = await plasmApp();
301
+ await new Promise<void>((resolve, reject) => {
302
+ res.once("finish", () => resolve());
303
+ res.once("error", reject);
304
+ void vercelPlasmHandler(app)(req, res).catch(reject);
305
+ });
306
+ });
307
+ `;
308
+
252
309
  const PACKAGE_JSON_TEMPLATE = {
253
310
  name: "my-plasm-agent",
254
311
  private: true,
255
312
  type: "module",
256
313
  scripts: {
257
314
  dev: "plasm-agent dev",
315
+ "dev:interactive": "plasm-agent dev --interactive",
258
316
  build: "plasm-agent build",
259
317
  "vercel-build": "plasm-agent build",
260
318
  info: "plasm-agent info",
261
319
  deploy: "vercel deploy",
262
320
  },
263
321
  dependencies: {} as Record<string, string>,
322
+ devDependencies: {
323
+ nitropack: "^2.13.4",
324
+ },
264
325
  };
265
326
 
266
327
  async function writeVercelScaffold(
@@ -279,6 +340,16 @@ async function writeVercelScaffold(
279
340
  );
280
341
  }
281
342
 
343
+ async function writeNitroScaffold(projectRoot: string): Promise<void> {
344
+ await mkdir(path.join(projectRoot, "routes"), { recursive: true });
345
+ await writeFile(path.join(projectRoot, "nitro.config.ts"), NITRO_CONFIG_TS, "utf8");
346
+ await writeFile(
347
+ path.join(projectRoot, "routes", "[...path].ts"),
348
+ NITRO_CATCHALL_ROUTE_TS,
349
+ "utf8",
350
+ );
351
+ }
352
+
282
353
  function packageJsonScaffold(): Record<string, unknown> {
283
354
  return {
284
355
  ...PACKAGE_JSON_TEMPLATE,
@@ -335,6 +406,7 @@ export async function runPlasmInit(
335
406
  }
336
407
 
337
408
  await writeVercelScaffold(projectRoot);
409
+ await writeNitroScaffold(projectRoot);
338
410
 
339
411
  return { projectRoot, agentRoot };
340
412
  }
@@ -353,7 +425,8 @@ export function formatInitSuccess(
353
425
  " plasm-agent link # pull AI_GATEWAY_API_KEY from Vercel",
354
426
  " plasm-agent build",
355
427
  " npm run smoke:channel",
356
- " plasm-agent dev # local dev server + TUI",
428
+ " plasm-agent dev # Nitro dev server (Vercel routing parity)",
429
+ " plasm-agent dev --interactive # optional TUI + sessions + hot reload",
357
430
  " vercel deploy # production (channels + cron)",
358
431
  ].join("\n");
359
432
  }
@@ -366,7 +439,8 @@ export function formatInitSuccess(
366
439
  " npm install",
367
440
  " plasm-agent link # pull AI_GATEWAY_API_KEY from Vercel",
368
441
  " plasm-agent build # generate CGS stubs + discovery manifest",
369
- " plasm-agent dev # HTTP dev server (sessions + SSE)",
442
+ " plasm-agent dev # Nitro dev server (Vercel routing parity)",
443
+ " plasm-agent dev --interactive # optional TUI + sessions",
370
444
  " vercel deploy # production on Vercel",
371
445
  ].join("\n");
372
446
  }
@@ -0,0 +1,73 @@
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 type { ResolvedAgentProject } from "./project-root.js";
7
+
8
+ function plasmAgentPackageRoot(): string {
9
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
10
+ }
11
+
12
+ async function assertNitroScaffold(projectRoot: string): Promise<void> {
13
+ const required = [
14
+ path.join(projectRoot, "nitro.config.ts"),
15
+ path.join(projectRoot, "routes", "[...path].ts"),
16
+ ];
17
+ for (const filePath of required) {
18
+ try {
19
+ await access(filePath);
20
+ } catch {
21
+ throw new Error(
22
+ `Missing ${path.relative(projectRoot, filePath)} — run \`plasm-agent init\` to scaffold Nitro dev`,
23
+ );
24
+ }
25
+ }
26
+ }
27
+
28
+ function nitroBin(projectRoot: string): string {
29
+ return path.join(projectRoot, "node_modules", ".bin", "nitro");
30
+ }
31
+
32
+ export async function startNitroDevForProject(project: ResolvedAgentProject): Promise<void> {
33
+ await assertNitroScaffold(project.projectRoot);
34
+
35
+ const loader = path.join(plasmAgentPackageRoot(), "scripts", "register-plasm-loader.mjs");
36
+ const nodeOptions = [
37
+ "--experimental-strip-types",
38
+ "--experimental-transform-types",
39
+ `--import=${loader}`,
40
+ ].join(" ");
41
+
42
+ const bin = nitroBin(project.projectRoot);
43
+ try {
44
+ await access(bin);
45
+ } catch {
46
+ throw new Error(
47
+ "nitropack is not installed — run `npm install` in the project root (init adds nitropack as a devDependency)",
48
+ );
49
+ }
50
+
51
+ const child: ChildProcess = spawn(bin, ["dev"], {
52
+ cwd: project.projectRoot,
53
+ env: {
54
+ ...process.env,
55
+ NODE_OPTIONS: nodeOptions,
56
+ },
57
+ stdio: "inherit",
58
+ });
59
+
60
+ const wait = new Promise<number>((resolve, reject) => {
61
+ child.on("error", reject);
62
+ child.on("close", (code) => resolve(code ?? 0));
63
+ });
64
+
65
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
66
+ process.on(signal, () => {
67
+ child.kill(signal);
68
+ });
69
+ }
70
+
71
+ const exitCode = await wait;
72
+ process.exit(exitCode);
73
+ }
@@ -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,
@@ -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 {
@@ -1,4 +1,4 @@
1
- import { tool } from "ai";
1
+ import { tool, type ToolSet } from "ai";
2
2
  import { z } from "zod";
3
3
 
4
4
  import type { AgentRuntime } from "../runtime/agent-runtime.js";
@@ -8,89 +8,98 @@ import {
8
8
  PLASM_RUN_TOOL_DESCRIPTION,
9
9
  PLASM_TOOL_DESCRIPTION,
10
10
  } from "./descriptions.js";
11
+ import { toolInput } from "./tool-input.js";
11
12
 
12
13
  const seedSchema = z.object({
13
14
  api: z.string().describe("Registry entry_id / catalog api"),
14
15
  entity: z.string().describe("CGS entity name from discovery or prior knowledge"),
15
16
  });
16
17
 
17
- export function createPlasmTools(runtime: AgentRuntime) {
18
+ const discoverInputSchema = z.object({
19
+ intent: z
20
+ .string()
21
+ .min(1)
22
+ .describe(
23
+ "One plain-language task description for the whole user goal. Returns catalog api/entity picks — not program symbols.",
24
+ ),
25
+ });
26
+
27
+ const plasmContextInputSchema = z.object({
28
+ intent: z
29
+ .string()
30
+ .min(1)
31
+ .describe(
32
+ "Stable string for one user goal (same value every turn for that goal — do not rotate per message).",
33
+ ),
34
+ seeds: z
35
+ .array(seedSchema)
36
+ .min(1)
37
+ .describe("Non-empty array of {api, entity} capability picks"),
38
+ ranked_capabilities: z
39
+ .array(z.string())
40
+ .nullable()
41
+ .optional()
42
+ .describe(
43
+ "Optional capability wire names from discover_capabilities. Omit on expand; null or [] clears.",
44
+ ),
45
+ });
46
+
47
+ const plasmInputSchema = z.object({
48
+ logical_session_ref: z
49
+ .string()
50
+ .describe("Same logical_session_ref returned by plasm_context"),
51
+ program: z
52
+ .string()
53
+ .min(1)
54
+ .describe("Plasm source program using e#/m#/p#/r# from the session teaching TSV"),
55
+ reasoning: z
56
+ .string()
57
+ .optional()
58
+ .describe("Optional short note explaining the intent of this call"),
59
+ });
60
+
61
+ const plasmRunInputSchema = z.object({
62
+ logical_session_ref: z
63
+ .string()
64
+ .describe("Same logical_session_ref returned by plasm_context"),
65
+ plan_commit_ref: z
66
+ .string()
67
+ .describe("Executable plan token (pcN) from a prior plasm dry-run"),
68
+ reasoning: z
69
+ .string()
70
+ .optional()
71
+ .describe("Optional short note explaining the intent of this call"),
72
+ });
73
+
74
+ export function createPlasmTools(runtime: AgentRuntime): ToolSet {
18
75
  return {
19
76
  discover_capabilities: tool({
20
77
  description: DISCOVER_TOOL_DESCRIPTION,
21
- inputSchema: z.object({
22
- intent: z
23
- .string()
24
- .min(1)
25
- .describe(
26
- "One plain-language task description for the whole user goal. Returns catalog api/entity picks — not program symbols.",
27
- ),
28
- }),
78
+ inputSchema: toolInput(discoverInputSchema),
29
79
  execute: async ({ intent }) => runtime.discoverCapabilities({ intent }),
30
80
  }),
31
81
 
32
82
  plasm_context: tool({
33
83
  description: PLASM_CONTEXT_TOOL_DESCRIPTION,
34
- inputSchema: z.object({
35
- intent: z
36
- .string()
37
- .min(1)
38
- .describe(
39
- "Stable string for one user goal (same value every turn for that goal — do not rotate per message).",
40
- ),
41
- seeds: z
42
- .array(seedSchema)
43
- .min(1)
44
- .describe("Non-empty array of {api, entity} capability picks"),
45
- ranked_capabilities: z
46
- .array(z.string())
47
- .nullable()
48
- .optional()
49
- .describe(
50
- "Optional capability wire names from discover_capabilities. Omit on expand; null or [] clears.",
51
- ),
52
- }),
84
+ inputSchema: toolInput(plasmContextInputSchema),
53
85
  execute: async ({ intent, seeds, ranked_capabilities }) =>
54
86
  runtime.plasmContext({
55
87
  intent,
56
- seeds,
88
+ seeds: seeds as Array<{ api: string; entity: string }>,
57
89
  rankedCapabilities: ranked_capabilities,
58
90
  }),
59
91
  }),
60
92
 
61
93
  plasm: tool({
62
94
  description: PLASM_TOOL_DESCRIPTION,
63
- inputSchema: z.object({
64
- logical_session_ref: z
65
- .string()
66
- .describe("Same logical_session_ref returned by plasm_context"),
67
- program: z
68
- .string()
69
- .min(1)
70
- .describe("Plasm source program using e#/m#/p#/r# from the session teaching TSV"),
71
- reasoning: z
72
- .string()
73
- .optional()
74
- .describe("Optional short note explaining the intent of this call"),
75
- }),
95
+ inputSchema: toolInput(plasmInputSchema),
76
96
  execute: async ({ logical_session_ref, program, reasoning }) =>
77
97
  runtime.plasm({ logicalSessionRef: logical_session_ref, program, reasoning }),
78
98
  }),
79
99
 
80
100
  plasm_run: tool({
81
101
  description: PLASM_RUN_TOOL_DESCRIPTION,
82
- inputSchema: z.object({
83
- logical_session_ref: z
84
- .string()
85
- .describe("Same logical_session_ref returned by plasm_context"),
86
- plan_commit_ref: z
87
- .string()
88
- .describe("Executable plan token (pcN) from a prior plasm dry-run"),
89
- reasoning: z
90
- .string()
91
- .optional()
92
- .describe("Optional short note explaining the intent of this call"),
93
- }),
102
+ inputSchema: toolInput(plasmRunInputSchema),
94
103
  execute: async ({ logical_session_ref, plan_commit_ref, reasoning }) =>
95
104
  runtime.plasmRun({
96
105
  logicalSessionRef: logical_session_ref,
@@ -0,0 +1,9 @@
1
+ import { zodSchema, type FlexibleSchema } from "ai";
2
+ import type { z } from "zod";
3
+
4
+ /** Wrap Zod for AI SDK `tool()` — avoids TS2589 deep instantiation during Vercel typecheck. */
5
+ export function toolInput<T extends z.ZodTypeAny>(
6
+ schema: T,
7
+ ): FlexibleSchema<z.infer<T>> {
8
+ return zodSchema(schema);
9
+ }