@plasm_lang/vercel-agent 0.3.82 → 0.3.84

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 (47) hide show
  1. package/README.md +1 -1
  2. package/agent/hooks/trace-log.ts +1 -1
  3. package/agent/instructions.md +1 -1
  4. package/package.json +8 -4
  5. package/scripts/plasm-cli.ts +6 -0
  6. package/src/archive/paths.ts +4 -1
  7. package/src/authoring/slot-loader.ts +57 -4
  8. package/src/cli/build.ts +41 -2
  9. package/src/cli/compile-authored-slots.ts +79 -0
  10. package/src/cli/init-scaffold.ts +5 -6
  11. package/src/cli/init.ts +3 -1
  12. package/src/cli/nitro-dev.ts +23 -62
  13. package/src/gateway-model.ts +17 -6
  14. package/src/index.ts +5 -0
  15. package/src/instrumentation.ts +1 -1
  16. package/src/nitro/agent-summary.ts +133 -0
  17. package/src/nitro/build-application.ts +98 -0
  18. package/src/nitro/build-nitro-output.ts +14 -0
  19. package/src/nitro/copy-vercel-function-assets.ts +31 -0
  20. package/src/nitro/create-plasm-nitro.ts +111 -0
  21. package/src/nitro/patch-vercel-config.ts +37 -0
  22. package/src/nitro/paths.ts +30 -0
  23. package/src/nitro/prepare-host.ts +69 -0
  24. package/src/nitro/vercel-build-output-config.ts +18 -0
  25. package/src/nitro/workflow-directives.ts +43 -0
  26. package/src/nitro/write-nitro-entry.ts +87 -0
  27. package/src/package-version.ts +3 -11
  28. package/src/runtime/agent-runtime.ts +13 -12
  29. package/src/runtime/compaction.ts +1 -1
  30. package/src/server/plasm-handler.ts +22 -2
  31. package/src/telemetry/plasm-spans.ts +3 -3
  32. package/src/tools/descriptions.ts +29 -42
  33. package/src/tools/format.ts +2 -2
  34. package/src/tools/plasm-tools.ts +4 -4
  35. package/src/workflow/world-bootstrap.ts +12 -1
  36. package/templates/mcp-radar/agent/instrumentation.ts +18 -0
  37. package/templates/mcp-radar/lib/proof-store.ts +10 -3
  38. package/templates/mcp-radar/vercel.json +2 -13
  39. package/templates/scaffold/agent/instrumentation.ts +18 -0
  40. package/templates/scaffold/vercel.default.json +2 -2
  41. package/templates/scaffold/vercel.mcp-radar.json +2 -13
  42. package/templates/mcp-radar/api/[[...path]].ts +0 -23
  43. package/templates/mcp-radar/nitro.config.ts +0 -17
  44. package/templates/mcp-radar/routes/[...path].ts +0 -29
  45. package/templates/scaffold/api/[[...path]].ts +0 -23
  46. package/templates/scaffold/nitro.config.ts +0 -17
  47. package/templates/scaffold/routes/[...path].ts +0 -29
package/README.md CHANGED
@@ -154,7 +154,7 @@ npm run agent -- "your prompt" # requires AI_GATEWAY_API_KEY (Vercel AI Gatewa
154
154
 
155
155
  ### Dev server (`plasm-agent dev`)
156
156
 
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.
157
+ **Default:** `plasm-agent dev` runs programmatic **Nitro v3** (same host as Vercel). `plasm-agent build` emits **`.vercel/output`** on Vercel (Build Output API) or **`.plasm/nitro-output`** locally, plus **`.plasm/agent-summary.json`** for Agent Observability.
158
158
 
159
159
  ```bash
160
160
  plasm-agent dev # Nitro (default)
@@ -4,7 +4,7 @@ export default defineHook({
4
4
  name: "trace-log",
5
5
  on: ["run:complete", "plan:commit"],
6
6
  handler: (_ctx, detail) => {
7
- const event = detail?.planCommitRef ? "plan:commit" : "run:complete";
7
+ const event = detail?.runRef ? "plan:commit" : "run:complete";
8
8
  console.log(`[plasm:hook:trace-log] ${event}`, detail ?? {});
9
9
  },
10
10
  });
@@ -6,7 +6,7 @@ You operate external APIs through **Plasm catalogs**, not ad-hoc REST tools.
6
6
 
7
7
  1. **`discover_capabilities`** — when you do not know which `api` / `entity` to use (optional).
8
8
  2. **`plasm_context`** — open or extend a session with `{api, entity}` seeds. Returns **`logical_session_ref`** + teaching TSV (`e#`, `m#`, `p#`, `r#`).
9
- 3. **`plasm`** — dry-run a Plasm **program** using symbols from the teaching TSV. Returns **`plan_commit_ref`** (`pcN`).
9
+ 3. **`plasm`** — dry-run a Plasm **program** using symbols from the teaching TSV. Returns **`run_ref`** (`pcN`).
10
10
  4. **`plasm_run`** — live execute the reviewed plan (`pcN` only — never resend the program).
11
11
 
12
12
  ## Session discipline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasm_lang/vercel-agent",
3
- "version": "0.3.82",
3
+ "version": "0.3.84",
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": {
@@ -63,17 +63,21 @@
63
63
  "dependencies": {
64
64
  "@ai-sdk/otel": "^1.0.3",
65
65
  "@opentelemetry/api": "^1.9.0",
66
- "@plasm_lang/engine": "^0.3.82",
66
+ "@plasm_lang/engine": "^0.3.84",
67
67
  "@vercel/blob": "^0.27.3",
68
68
  "@vercel/connect": "^0.2.6",
69
69
  "@vercel/kv": "^3.0.0",
70
+ "@vercel/otel": "^1.14.2",
71
+ "@workflow/nitro": "^4.1.1",
70
72
  "@workflow/world-postgres": "^4.2.0",
71
73
  "ai": "^6.0.0",
74
+ "esbuild": "^0.25.12",
72
75
  "js-yaml": "^4.1.0",
76
+ "nitro": "^3.0.260610-beta",
73
77
  "pg": "^8.22.0",
78
+ "tsx": "^4.19.4",
74
79
  "workflow": "^4.5.0",
75
- "zod": "^3.25.76",
76
- "tsx": "^4.19.4"
80
+ "zod": "^3.25.76"
77
81
  },
78
82
  "devDependencies": {
79
83
  "@types/js-yaml": "^4.0.9",
@@ -85,6 +85,12 @@ async function cmdBuild(): Promise<void> {
85
85
  const result = await runPlasmBuild(project);
86
86
  console.log(`Built ${result.stubs.length} stub(s)`);
87
87
  console.log(`manifest: ${result.manifestPath}`);
88
+ if (result.outputDir) {
89
+ console.log(`nitro output: ${result.outputDir}${result.vercelOutput ? " (vercel build output)" : ""}`);
90
+ }
91
+ if (result.agentSummaryPath) {
92
+ console.log(`agent summary: ${result.agentSummaryPath}`);
93
+ }
88
94
  for (const stub of result.stubs) {
89
95
  console.log(` - ${stub.entryId} → ${path.relative(project.projectRoot, stub.outPath)}`);
90
96
  }
@@ -4,7 +4,10 @@ import type { ArchivePaths } from "./types.js";
4
4
 
5
5
  /** Resolve local trace/run archive roots from env or agent defaults. */
6
6
  export function resolveArchivePaths(agentRoot: string): ArchivePaths {
7
- const archivesDir = path.join(agentRoot, ".plasm", "archives");
7
+ const archivesDir =
8
+ process.env.VERCEL === "1"
9
+ ? path.join("/tmp", "plasm-archives", path.basename(path.resolve(agentRoot)))
10
+ : path.join(agentRoot, ".plasm", "archives");
8
11
  const traceRoot =
9
12
  process.env.PLASM_TRACE_ARCHIVE_DIR?.trim() ||
10
13
  path.join(archivesDir, "traces");
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
@@ -45,6 +45,10 @@ export interface LoadedProjectSlots {
45
45
  export interface LoadAuthoredSlotsOptions {
46
46
  discovery: ProjectDiscovery;
47
47
  importCacheBust?: number;
48
+ agentRoot?: string;
49
+ projectRoot?: string;
50
+ /** agentRoot-relative source → projectRoot-relative compiled `.mjs` from build manifest. */
51
+ compiledSlots?: Record<string, string>;
48
52
  }
49
53
 
50
54
  type SlotName = "channels" | "schedules" | "hooks" | "skills";
@@ -58,8 +62,52 @@ function slotDiagnostic(
58
62
  return { level, slot, path: filePath, message };
59
63
  }
60
64
 
61
- async function importSlotModule(filePath: string, cacheBust: number): Promise<unknown> {
62
- const url = `${pathToFileURL(filePath).href}?t=${cacheBust}`;
65
+ async function pathExists(p: string): Promise<boolean> {
66
+ try {
67
+ await access(p);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ async function resolveSlotImportPath(
75
+ filePath: string,
76
+ options: LoadAuthoredSlotsOptions,
77
+ ): Promise<string> {
78
+ const agentRoot = path.resolve(options.agentRoot ?? options.discovery.agentRoot);
79
+ const relFromAgent = path.relative(agentRoot, filePath);
80
+ const fromManifest = options.compiledSlots?.[relFromAgent];
81
+ if (fromManifest && options.projectRoot) {
82
+ const compiled = path.join(options.projectRoot, fromManifest);
83
+ if (await pathExists(compiled)) return compiled;
84
+ }
85
+ const mirror = path.join(
86
+ agentRoot,
87
+ ".plasm",
88
+ "compiled",
89
+ relFromAgent.replace(/\.ts$/, ".mjs"),
90
+ );
91
+ if (await pathExists(mirror)) return mirror;
92
+ return filePath;
93
+ }
94
+
95
+ async function importSlotModule(
96
+ filePath: string,
97
+ cacheBust: number,
98
+ options: LoadAuthoredSlotsOptions,
99
+ ): Promise<unknown> {
100
+ const agentRoot = path.resolve(options.agentRoot ?? options.discovery.agentRoot);
101
+ const relFromAgent = path.relative(agentRoot, filePath);
102
+ const preloaded = (
103
+ globalThis as typeof globalThis & { __PLASM_PRELOADED_SLOTS?: Record<string, unknown> }
104
+ ).__PLASM_PRELOADED_SLOTS?.[relFromAgent];
105
+ if (preloaded !== undefined) {
106
+ return (preloaded as { default?: unknown }).default ?? preloaded;
107
+ }
108
+
109
+ const resolved = await resolveSlotImportPath(filePath, options);
110
+ const url = `${pathToFileURL(resolved).href}?t=${cacheBust}`;
63
111
  const mod = await import(url);
64
112
  return mod.default ?? mod;
65
113
  }
@@ -85,9 +133,10 @@ async function loadTypescriptSlot<T extends ChannelDefinition | ScheduleDefiniti
85
133
  slot: SlotName,
86
134
  expected: "channel" | "schedule" | "hook" | "skill",
87
135
  cacheBust: number,
136
+ options: LoadAuthoredSlotsOptions,
88
137
  ): Promise<{ definition: T } | { diagnostic: DiscoveryDiagnostic }> {
89
138
  try {
90
- const exported = await importSlotModule(filePath, cacheBust);
139
+ const exported = await importSlotModule(filePath, cacheBust, options);
91
140
  const actual = classifySlotExport(exported);
92
141
  if (actual !== expected) {
93
142
  const helper =
@@ -147,6 +196,7 @@ export async function loadAuthoredSlots(
147
196
  "channels",
148
197
  EXPECTED_KIND.channels,
149
198
  cacheBust,
199
+ options,
150
200
  );
151
201
  if ("diagnostic" in result) {
152
202
  diagnostics.push(result.diagnostic);
@@ -163,6 +213,7 @@ export async function loadAuthoredSlots(
163
213
  "schedules",
164
214
  EXPECTED_KIND.schedules,
165
215
  cacheBust,
216
+ options,
166
217
  );
167
218
  if ("diagnostic" in result) {
168
219
  diagnostics.push(result.diagnostic);
@@ -179,6 +230,7 @@ export async function loadAuthoredSlots(
179
230
  "hooks",
180
231
  EXPECTED_KIND.hooks,
181
232
  cacheBust,
233
+ options,
182
234
  );
183
235
  if ("diagnostic" in result) {
184
236
  diagnostics.push(result.diagnostic);
@@ -198,6 +250,7 @@ export async function loadAuthoredSlots(
198
250
  "skills",
199
251
  "skill",
200
252
  cacheBust,
253
+ options,
201
254
  );
202
255
  if ("diagnostic" in result) {
203
256
  diagnostics.push(result.diagnostic);
package/src/cli/build.ts CHANGED
@@ -1,21 +1,33 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { compileAuthoredSlots } from "./compile-authored-slots.js";
5
+ import type { ResolvedAgentProject } from "./project-root.js";
4
6
  import { isNativeEngineAvailable } from "../engine/napi-binding.js";
5
7
  import { walkAgentProject } from "../discovery/project-walker.js";
6
8
  import { generateAllStubs } from "../stubs/generator.js";
7
- import type { ResolvedAgentProject } from "./project-root.js";
9
+ import { buildPlasmApplication } from "../nitro/build-application.js";
10
+ import { readPackageName } from "./project-root.js";
8
11
 
9
12
  export interface PlasmBuildResult {
10
13
  stubsDir: string;
11
14
  discoveryDir: string;
12
15
  manifestPath: string;
13
16
  stubs: Array<{ entryId: string; catalogCgsHash: string; outPath: string }>;
17
+ outputDir?: string;
18
+ agentSummaryPath?: string;
19
+ vercelOutput?: boolean;
14
20
  }
15
21
 
16
22
  export async function runPlasmBuild(project: ResolvedAgentProject): Promise<PlasmBuildResult> {
17
23
  const stubs = await generateAllStubs(project.agentRoot);
18
24
  const discovery = await walkAgentProject(project.agentRoot);
25
+ const { compiledSlots } = await compileAuthoredSlots(
26
+ project.projectRoot,
27
+ project.agentRoot,
28
+ discovery,
29
+ );
30
+
19
31
  const discoveryDir = path.join(project.agentRoot, ".plasm", "discovery");
20
32
  await mkdir(discoveryDir, { recursive: true });
21
33
  const manifestPath = path.join(discoveryDir, "manifest.json");
@@ -24,6 +36,7 @@ export async function runPlasmBuild(project: ResolvedAgentProject): Promise<Plas
24
36
  builtAt: new Date().toISOString(),
25
37
  projectRoot: project.projectRoot,
26
38
  agentRoot: project.agentRoot,
39
+ compiledSlots,
27
40
  stubs: stubs.map((s) => ({
28
41
  entryId: s.entryId,
29
42
  catalogCgsHash: s.catalogCgsHash,
@@ -45,10 +58,36 @@ export async function runPlasmBuild(project: ResolvedAgentProject): Promise<Plas
45
58
  },
46
59
  };
47
60
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
61
+
62
+ const packageName = await readPackageName(project.projectRoot);
63
+ const appBuild = await buildPlasmApplication({
64
+ projectRoot: project.projectRoot,
65
+ agentRoot: project.agentRoot,
66
+ discovery,
67
+ compiledSlots,
68
+ packageName: packageName ?? undefined,
69
+ });
70
+
48
71
  return {
49
72
  stubsDir: path.join(project.agentRoot, ".plasm", "stubs"),
50
73
  discoveryDir,
51
74
  manifestPath,
52
75
  stubs,
76
+ outputDir: appBuild.outputDir,
77
+ agentSummaryPath: appBuild.agentSummaryPath,
78
+ vercelOutput: appBuild.vercelOutput,
53
79
  };
54
80
  }
81
+
82
+ export async function readBuildManifest(agentRoot: string): Promise<{
83
+ compiledSlots?: Record<string, string>;
84
+ projectRoot?: string;
85
+ } | null> {
86
+ const manifestPath = path.join(agentRoot, ".plasm", "discovery", "manifest.json");
87
+ try {
88
+ const raw = await readFile(manifestPath, "utf8");
89
+ return JSON.parse(raw) as { compiledSlots?: Record<string, string>; projectRoot?: string };
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
@@ -0,0 +1,79 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import * as esbuild from "esbuild";
5
+
6
+ import type { ProjectDiscovery } from "../discovery/project-walker.js";
7
+
8
+ const ESBUILD_BANNER =
9
+ 'import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);';
10
+
11
+ export type CompiledSlotMap = Record<string, string>;
12
+
13
+ export interface CompileAuthoredSlotsResult {
14
+ /** agentRoot-relative source path → projectRoot-relative compiled `.mjs`. */
15
+ compiledSlots: CompiledSlotMap;
16
+ compiledDir: string;
17
+ }
18
+
19
+ function slotFiles(
20
+ discovery: ProjectDiscovery,
21
+ ): Array<{ filePath: string }> {
22
+ const out: Array<{ filePath: string }> = [];
23
+ for (const file of discovery.channels) {
24
+ if (file.kind === "typescript") out.push({ filePath: file.path });
25
+ }
26
+ for (const file of discovery.schedules) {
27
+ if (file.kind === "typescript") out.push({ filePath: file.path });
28
+ }
29
+ for (const file of discovery.hooks) {
30
+ if (file.kind === "typescript") out.push({ filePath: file.path });
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /** Compile authored TS slots to `.plasm/compiled/` for Vercel runtime (no dynamic `.ts` import). */
36
+ export async function compileAuthoredSlots(
37
+ projectRoot: string,
38
+ agentRoot: string,
39
+ discovery: ProjectDiscovery,
40
+ ): Promise<CompileAuthoredSlotsResult> {
41
+ const compiledRoot = path.join(agentRoot, ".plasm", "compiled");
42
+ await mkdir(compiledRoot, { recursive: true });
43
+
44
+ const compiledSlots: CompiledSlotMap = {};
45
+
46
+ await Promise.all(
47
+ slotFiles(discovery).map(async ({ filePath }) => {
48
+ const relFromAgent = path.relative(agentRoot, filePath);
49
+ const outFile = path.join(
50
+ agentRoot,
51
+ ".plasm",
52
+ "compiled",
53
+ relFromAgent.replace(/\.ts$/, ".mjs"),
54
+ );
55
+ await mkdir(path.dirname(outFile), { recursive: true });
56
+
57
+ await esbuild.build({
58
+ entryPoints: [filePath],
59
+ bundle: true,
60
+ platform: "node",
61
+ target: "node22",
62
+ format: "esm",
63
+ outfile: outFile,
64
+ banner: { js: ESBUILD_BANNER },
65
+ logLevel: "silent",
66
+ });
67
+
68
+ compiledSlots[relFromAgent] = path.relative(projectRoot, outFile);
69
+ }),
70
+ );
71
+
72
+ await writeFile(
73
+ path.join(compiledRoot, "package.json"),
74
+ `${JSON.stringify({ type: "module" }, null, 2)}\n`,
75
+ "utf8",
76
+ );
77
+
78
+ return { compiledSlots, compiledDir: compiledRoot };
79
+ }
@@ -11,7 +11,7 @@ export interface InitOptions {
11
11
  npm?: boolean;
12
12
  }
13
13
 
14
- const SKIP_TEMPLATE_DIRS = new Set(["node_modules", ".plasm", ".nitro", ".output", "server"]);
14
+ const SKIP_TEMPLATE_DIRS = new Set(["node_modules", ".plasm", ".nitro", ".output"]);
15
15
  const SKIP_TEMPLATE_FILES = new Set(["package-lock.json", "vercel-build.ts"]);
16
16
 
17
17
  function plasmAgentPackageRoot(): string {
@@ -65,9 +65,7 @@ export async function writeDeployScaffold(
65
65
  ): Promise<void> {
66
66
  const vercelJsonName =
67
67
  template === "mcp-radar" ? "vercel.mcp-radar.json" : "vercel.default.json";
68
- await copyScaffoldFile("api/[[...path]].ts", projectRoot);
69
- await copyScaffoldFile("routes/[...path].ts", projectRoot);
70
- await copyScaffoldFile("nitro.config.ts", projectRoot);
68
+ await copyScaffoldFile("agent/instrumentation.ts", projectRoot);
71
69
  await copyScaffoldFile(".vercelignore", projectRoot);
72
70
  const publicIndex =
73
71
  template === "mcp-radar" ? "public/index.mcp-radar.html" : "public/index.html";
@@ -119,6 +117,7 @@ export async function patchProjectPackageJson(
119
117
  "@vercel/blob": "^0.27.3",
120
118
  "@vercel/functions": "^3.4.3",
121
119
  "@vercel/kv": "^3.0.0",
120
+ "@vercel/otel": "^1.5.0",
122
121
  };
123
122
  } else {
124
123
  pkg.dependencies = {
@@ -128,6 +127,7 @@ export async function patchProjectPackageJson(
128
127
  "@vercel/blob": "^0.27.3",
129
128
  "@vercel/functions": "^3.4.3",
130
129
  "@vercel/kv": "^3.0.0",
130
+ "@vercel/otel": "^1.5.0",
131
131
  };
132
132
  }
133
133
 
@@ -143,9 +143,9 @@ export async function patchProjectPackageJson(
143
143
 
144
144
  pkg.devDependencies = {
145
145
  ...pkg.devDependencies,
146
- nitropack: "^2.13.4",
147
146
  tsx: "^4.19.4",
148
147
  };
148
+ delete pkg.devDependencies?.nitropack;
149
149
 
150
150
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
151
151
  }
@@ -168,7 +168,6 @@ export function blankPackageJsonScaffold(): Record<string, unknown> {
168
168
  "@plasm_lang/engine": `^${version}`,
169
169
  },
170
170
  devDependencies: {
171
- nitropack: "^2.13.4",
172
171
  tsx: "^4.19.4",
173
172
  },
174
173
  };
package/src/cli/init.ts CHANGED
@@ -89,7 +89,9 @@ capabilities:
89
89
  path: /items
90
90
  `;
91
91
 
92
- const ENV_EXAMPLE = `# Vercel AI Gateway (run \`plasm-agent link\` to pull from a linked project)
92
+ const ENV_EXAMPLE = `# Vercel AI Gateway
93
+ # On Vercel: OIDC auth is automatic — no key required for gateway model slugs.
94
+ # Local / self-host: run \`plasm-agent link\` or set AI_GATEWAY_API_KEY.
93
95
  AI_GATEWAY_API_KEY=
94
96
 
95
97
  # Vercel Cron auth (production)
@@ -1,69 +1,30 @@
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";
1
+ import { compileAuthoredSlots } from "./compile-authored-slots.js";
2
+ import { walkAgentProject } from "../discovery/project-walker.js";
3
+ import { generateAllStubs } from "../stubs/generator.js";
4
+ import { startPlasmNitroDev } from "../nitro/build-application.js";
7
5
  import type { ResolvedAgentProject } from "./project-root.js";
8
6
 
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
7
  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",
8
+ await generateAllStubs(project.agentRoot);
9
+ const discovery = await walkAgentProject(project.agentRoot);
10
+ const { compiledSlots } = await compileAuthoredSlots(
11
+ project.projectRoot,
12
+ project.agentRoot,
13
+ discovery,
14
+ );
15
+
16
+ const server = await startPlasmNitroDev({
17
+ projectRoot: project.projectRoot,
18
+ agentRoot: project.agentRoot,
19
+ discovery,
20
+ compiledSlots,
54
21
  });
55
22
 
56
- const wait = new Promise<number>((resolve, reject) => {
57
- child.on("error", reject);
58
- child.on("close", (code) => resolve(code ?? 0));
23
+ await new Promise<void>((resolve) => {
24
+ const shutdown = () => {
25
+ void server.close().finally(() => resolve());
26
+ };
27
+ process.on("SIGINT", shutdown);
28
+ process.on("SIGTERM", shutdown);
59
29
  });
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
30
  }
@@ -11,10 +11,20 @@ function ensureGatewayApiKey(): void {
11
11
  }
12
12
  }
13
13
 
14
- /** Whether `resolveGatewayModel` can run (API key or mapped alias present). */
14
+ /** Hosted Vercel Functions (OIDC gateway auth available without an API key). */
15
+ export function isVercelHosted(): boolean {
16
+ return (
17
+ process.env.VERCEL === "1" ||
18
+ Boolean(process.env.VERCEL_DEPLOYMENT_ID?.trim()) ||
19
+ Boolean(process.env.VERCEL_ENV?.trim())
20
+ );
21
+ }
22
+
23
+ /** Whether `resolveGatewayModel` can run (API key, alias, or Vercel OIDC). */
15
24
  export function isGatewayConfigured(): boolean {
16
25
  ensureGatewayApiKey();
17
- return Boolean(process.env.AI_GATEWAY_API_KEY?.trim());
26
+ if (Boolean(process.env.AI_GATEWAY_API_KEY?.trim())) return true;
27
+ return isVercelHosted();
18
28
  }
19
29
 
20
30
  /** Resolve a Gateway model slug (`provider/model`) to an AI SDK `LanguageModel`. */
@@ -29,15 +39,16 @@ export function resolveGatewayModel(
29
39
  }
30
40
 
31
41
  ensureGatewayApiKey();
32
- if (!process.env.AI_GATEWAY_API_KEY?.trim()) {
42
+ if (!isGatewayConfigured()) {
33
43
  throw new Error(
34
44
  [
35
- "Vercel AI Gateway requires AI_GATEWAY_API_KEY.",
36
- "Create one in the Vercel dashboard (AI Gateway API Keys) and add it to plasm-oss/.env.",
37
- "Local dev: `vercel env pull` from a linked project also works.",
45
+ "Vercel AI Gateway is not configured.",
46
+ "On Vercel: link the project gateway model ids authenticate via OIDC (no API key).",
47
+ "Off Vercel: set AI_GATEWAY_API_KEY or run `plasm-agent link`.",
38
48
  ].join(" "),
39
49
  );
40
50
  }
41
51
 
52
+ // On Vercel without an explicit key, @ai-sdk/gateway uses @vercel/oidc automatically.
42
53
  return gateway(slug);
43
54
  }
package/src/index.ts CHANGED
@@ -132,6 +132,11 @@ export { createDefaultHostTransport } from "./engine/host-transport.js";
132
132
  export type { HostTransportOptions } from "./engine/host-transport.js";
133
133
  export { createProductionHostTransport, createStubHostTransport } from "./engine/create-host-transport.js";
134
134
  export { loadAgentEnv } from "./load-env.js";
135
+ export {
136
+ isGatewayConfigured,
137
+ isVercelHosted,
138
+ resolveGatewayModel,
139
+ } from "./gateway-model.js";
135
140
  export {
136
141
  connectorUidForEntry,
137
142
  connectAuthOptionsForEntry,
@@ -12,7 +12,7 @@ export const PlasmSpanAttributes = {
12
12
  INTENT: "plasm.intent",
13
13
  LOGICAL_SESSION_REF: "plasm.logical_session_ref",
14
14
  CATALOG_CGS_HASH: "plasm.catalog_cgs_hash",
15
- PLAN_COMMIT_REF: "plasm.plan_commit_ref",
15
+ RUN_REF: "plasm.run_ref",
16
16
  RUN_ID: "plasm.run_id",
17
17
  ENTRY_ID: "plasm.entry_id",
18
18
  TOOL_NAME: "plasm.tool.name",