@plasm_lang/vercel-agent 0.3.82 → 0.3.85
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 +1 -1
- package/agent/hooks/trace-log.ts +1 -1
- package/agent/instructions.md +1 -1
- package/package.json +8 -4
- package/scripts/plasm-cli.ts +6 -0
- package/src/archive/paths.ts +4 -1
- package/src/authoring/slot-loader.ts +57 -4
- package/src/cli/build.ts +41 -2
- package/src/cli/compile-authored-slots.ts +79 -0
- package/src/cli/init-scaffold.ts +5 -6
- package/src/cli/init.ts +3 -1
- package/src/cli/nitro-dev.ts +23 -62
- package/src/gateway-model.ts +17 -6
- package/src/index.ts +5 -0
- package/src/instrumentation.ts +1 -1
- package/src/nitro/agent-summary.ts +133 -0
- package/src/nitro/build-application.ts +98 -0
- package/src/nitro/build-nitro-output.ts +14 -0
- package/src/nitro/copy-vercel-function-assets.ts +31 -0
- package/src/nitro/create-plasm-nitro.ts +111 -0
- package/src/nitro/patch-vercel-config.ts +37 -0
- package/src/nitro/paths.ts +30 -0
- package/src/nitro/prepare-host.ts +69 -0
- package/src/nitro/vercel-build-output-config.ts +18 -0
- package/src/nitro/workflow-directives.ts +43 -0
- package/src/nitro/write-nitro-entry.ts +87 -0
- package/src/package-version.ts +3 -11
- package/src/runtime/agent-runtime.ts +13 -12
- package/src/runtime/compaction.ts +1 -1
- package/src/server/plasm-handler.ts +22 -2
- package/src/telemetry/plasm-spans.ts +3 -3
- package/src/tools/descriptions.ts +29 -42
- package/src/tools/format.ts +2 -2
- package/src/tools/plasm-tools.ts +4 -4
- package/src/workflow/world-bootstrap.ts +12 -1
- package/templates/mcp-radar/agent/instrumentation.ts +18 -0
- package/templates/mcp-radar/lib/proof-store.ts +10 -3
- package/templates/mcp-radar/vercel.json +2 -13
- package/templates/scaffold/agent/instrumentation.ts +18 -0
- package/templates/scaffold/vercel.default.json +2 -2
- package/templates/scaffold/vercel.mcp-radar.json +2 -13
- package/templates/mcp-radar/api/[[...path]].ts +0 -23
- package/templates/mcp-radar/nitro.config.ts +0 -17
- package/templates/mcp-radar/routes/[...path].ts +0 -29
- package/templates/scaffold/api/[[...path]].ts +0 -23
- package/templates/scaffold/nitro.config.ts +0 -17
- package/templates/scaffold/routes/[...path].ts +0 -29
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { LoadedProjectSlots } from "../authoring/slot-loader.js";
|
|
5
|
+
import { listChannelRoutes } from "../authoring/channel-dispatch.js";
|
|
6
|
+
import { exportScheduleCronManifest } from "../authoring/schedule-manager.js";
|
|
7
|
+
import type { AgentDefinition } from "../define-agent.js";
|
|
8
|
+
import type { ProjectDiscovery } from "../discovery/project-walker.js";
|
|
9
|
+
import { frameworkPackageVersion } from "../package-version.js";
|
|
10
|
+
import { plasmAgentSummaryPath } from "./paths.js";
|
|
11
|
+
|
|
12
|
+
export const VERCEL_PLASM_AGENT_SUMMARY_KIND = "vercel-plasm-agent-summary";
|
|
13
|
+
export const VERCEL_PLASM_AGENT_SUMMARY_VERSION = 3;
|
|
14
|
+
|
|
15
|
+
export interface PlasmAgentSummaryPayload {
|
|
16
|
+
kind: typeof VERCEL_PLASM_AGENT_SUMMARY_KIND;
|
|
17
|
+
schemaVersion: typeof VERCEL_PLASM_AGENT_SUMMARY_VERSION;
|
|
18
|
+
generatorVersion: string;
|
|
19
|
+
agent: {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string | null;
|
|
22
|
+
modelId: string;
|
|
23
|
+
};
|
|
24
|
+
instructions: {
|
|
25
|
+
logicalPath: string;
|
|
26
|
+
sourceKind: string;
|
|
27
|
+
markdown: string | null;
|
|
28
|
+
} | null;
|
|
29
|
+
schedules: Array<{ name: string; cron: string; logicalPath: string }>;
|
|
30
|
+
tools: Array<{ name: string; description: string; logicalPath: string }>;
|
|
31
|
+
skills: Array<{ name: string; description: string; logicalPath: string; sourceKind: string }>;
|
|
32
|
+
connections: [];
|
|
33
|
+
channels: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
method: string;
|
|
36
|
+
urlPath: string;
|
|
37
|
+
type: string;
|
|
38
|
+
logicalPath: string;
|
|
39
|
+
}>;
|
|
40
|
+
sandbox: null;
|
|
41
|
+
subagents: Array<{ name: string; description: string; logicalPath: string }>;
|
|
42
|
+
diagnostics: { errors: number; warnings: number };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeChannelKind(method: string): string {
|
|
46
|
+
if (method === "WEBSOCKET") return "websocket";
|
|
47
|
+
return "http";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildPlasmAgentSummary(options: {
|
|
51
|
+
projectRoot: string;
|
|
52
|
+
agentRoot: string;
|
|
53
|
+
packageName?: string;
|
|
54
|
+
definition: AgentDefinition;
|
|
55
|
+
discovery: ProjectDiscovery;
|
|
56
|
+
loadedSlots: import("../authoring/slot-loader.js").LoadedProjectSlots;
|
|
57
|
+
generatorVersion?: string;
|
|
58
|
+
}): PlasmAgentSummaryPayload {
|
|
59
|
+
const { projectRoot, agentRoot, packageName, definition, discovery, loadedSlots } = options;
|
|
60
|
+
const rel = (p: string) => path.relative(projectRoot, p);
|
|
61
|
+
|
|
62
|
+
const instructions =
|
|
63
|
+
discovery.instructions === undefined
|
|
64
|
+
? null
|
|
65
|
+
: {
|
|
66
|
+
logicalPath: rel(discovery.instructions.path),
|
|
67
|
+
sourceKind: discovery.instructions.kind,
|
|
68
|
+
markdown: null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const channels = loadedSlots.channels.flatMap((channel) =>
|
|
72
|
+
channel.definition.routes.map((route) => ({
|
|
73
|
+
name: path.basename(channel.sourcePath, path.extname(channel.sourcePath)),
|
|
74
|
+
method: route.method,
|
|
75
|
+
urlPath: route.path,
|
|
76
|
+
type: normalizeChannelKind(route.method),
|
|
77
|
+
logicalPath: rel(channel.sourcePath),
|
|
78
|
+
})),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const scheduleManifest = exportScheduleCronManifest(loadedSlots.schedules);
|
|
82
|
+
|
|
83
|
+
const diagnostics = [...loadedSlots.diagnostics, ...discovery.diagnostics];
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
kind: VERCEL_PLASM_AGENT_SUMMARY_KIND,
|
|
87
|
+
schemaVersion: VERCEL_PLASM_AGENT_SUMMARY_VERSION,
|
|
88
|
+
generatorVersion: options.generatorVersion ?? frameworkPackageVersion(),
|
|
89
|
+
agent: {
|
|
90
|
+
name: packageName ?? path.basename(path.dirname(agentRoot)),
|
|
91
|
+
description: null,
|
|
92
|
+
modelId: definition.model,
|
|
93
|
+
},
|
|
94
|
+
instructions,
|
|
95
|
+
schedules: scheduleManifest.crons.map((cron) => ({
|
|
96
|
+
name: cron.name,
|
|
97
|
+
cron: cron.schedule,
|
|
98
|
+
logicalPath: rel(
|
|
99
|
+
loadedSlots.schedules.find((s) => s.definition.name === cron.name)?.sourcePath ??
|
|
100
|
+
path.join(agentRoot, "schedules", `${cron.name}.ts`),
|
|
101
|
+
),
|
|
102
|
+
})),
|
|
103
|
+
tools: [],
|
|
104
|
+
skills: loadedSlots.skills.map((skill) => ({
|
|
105
|
+
name: skill.definition.name,
|
|
106
|
+
description: skill.definition.description ?? "",
|
|
107
|
+
logicalPath: rel(skill.sourcePath),
|
|
108
|
+
sourceKind: skill.sourcePath.endsWith(".md") ? "markdown" : "typescript",
|
|
109
|
+
})),
|
|
110
|
+
connections: [],
|
|
111
|
+
channels,
|
|
112
|
+
sandbox: null,
|
|
113
|
+
subagents: discovery.subagents.map((sub) => ({
|
|
114
|
+
name: sub.name,
|
|
115
|
+
description: "",
|
|
116
|
+
logicalPath: rel(sub.path),
|
|
117
|
+
})),
|
|
118
|
+
diagnostics: {
|
|
119
|
+
errors: diagnostics.filter((d) => d.level === "error").length,
|
|
120
|
+
warnings: diagnostics.filter((d) => d.level === "warning").length,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function emitPlasmAgentSummary(options: {
|
|
126
|
+
projectRoot: string;
|
|
127
|
+
summary: PlasmAgentSummaryPayload;
|
|
128
|
+
}): Promise<string> {
|
|
129
|
+
const outPath = plasmAgentSummaryPath(options.projectRoot);
|
|
130
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
131
|
+
await writeFile(outPath, `${JSON.stringify(options.summary, null, 2)}\n`, "utf8");
|
|
132
|
+
return outPath;
|
|
133
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { CompiledSlotMap } from "../cli/compile-authored-slots.js";
|
|
4
|
+
import { loadAuthoredSlots } from "../authoring/slot-loader.js";
|
|
5
|
+
import type { ProjectDiscovery } from "../discovery/project-walker.js";
|
|
6
|
+
import { buildPlasmAgentSummary, emitPlasmAgentSummary } from "./agent-summary.js";
|
|
7
|
+
import { buildNitroOutput } from "./build-nitro-output.js";
|
|
8
|
+
import { createPlasmNitro } from "./create-plasm-nitro.js";
|
|
9
|
+
import { patchVercelOutputConfig } from "./patch-vercel-config.js";
|
|
10
|
+
import { copyVercelFunctionAssets } from "./copy-vercel-function-assets.js";
|
|
11
|
+
import { isVercelBuildEnvironment, vercelOutputDir } from "./paths.js";
|
|
12
|
+
import { preparePlasmHost } from "./prepare-host.js";
|
|
13
|
+
import { writePlasmNitroCatchAllRoute } from "./write-nitro-entry.js";
|
|
14
|
+
|
|
15
|
+
export interface PlasmApplicationBuildResult {
|
|
16
|
+
outputDir: string;
|
|
17
|
+
agentSummaryPath: string;
|
|
18
|
+
vercelOutput: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function buildPlasmApplication(options: {
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
agentRoot: string;
|
|
24
|
+
discovery: ProjectDiscovery;
|
|
25
|
+
compiledSlots: CompiledSlotMap;
|
|
26
|
+
packageName?: string;
|
|
27
|
+
}): Promise<PlasmApplicationBuildResult> {
|
|
28
|
+
const { projectRoot, agentRoot, discovery, compiledSlots, packageName } = options;
|
|
29
|
+
|
|
30
|
+
const host = await preparePlasmHost({
|
|
31
|
+
projectRoot,
|
|
32
|
+
agentRoot,
|
|
33
|
+
compiledSlots,
|
|
34
|
+
discovery,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await writePlasmNitroCatchAllRoute(projectRoot, compiledSlots);
|
|
38
|
+
|
|
39
|
+
const nitro = await createPlasmNitro(host, false);
|
|
40
|
+
try {
|
|
41
|
+
const outputDir = await buildNitroOutput(nitro);
|
|
42
|
+
|
|
43
|
+
if (isVercelBuildEnvironment()) {
|
|
44
|
+
await copyVercelFunctionAssets({ projectRoot, agentRoot });
|
|
45
|
+
await patchVercelOutputConfig(projectRoot, host);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const loadedSlots = await loadAuthoredSlots({
|
|
49
|
+
discovery,
|
|
50
|
+
agentRoot,
|
|
51
|
+
projectRoot,
|
|
52
|
+
compiledSlots,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const summary = buildPlasmAgentSummary({
|
|
56
|
+
projectRoot,
|
|
57
|
+
agentRoot,
|
|
58
|
+
packageName,
|
|
59
|
+
definition: host.definition,
|
|
60
|
+
discovery,
|
|
61
|
+
loadedSlots,
|
|
62
|
+
});
|
|
63
|
+
const agentSummaryPath = await emitPlasmAgentSummary({ projectRoot, summary });
|
|
64
|
+
|
|
65
|
+
const vercelOutput = isVercelBuildEnvironment();
|
|
66
|
+
const resolvedOutput = vercelOutput ? vercelOutputDir(projectRoot) : outputDir;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
outputDir: resolvedOutput,
|
|
70
|
+
agentSummaryPath,
|
|
71
|
+
vercelOutput,
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
await nitro.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function startPlasmNitroDev(options: {
|
|
79
|
+
projectRoot: string;
|
|
80
|
+
agentRoot: string;
|
|
81
|
+
discovery: ProjectDiscovery;
|
|
82
|
+
compiledSlots: CompiledSlotMap;
|
|
83
|
+
}): Promise<Awaited<ReturnType<typeof import("nitro/builder").createDevServer>>> {
|
|
84
|
+
const { createDevServer } = await import("nitro/builder");
|
|
85
|
+
|
|
86
|
+
const host = await preparePlasmHost(options);
|
|
87
|
+
await writePlasmNitroCatchAllRoute(options.projectRoot, options.compiledSlots);
|
|
88
|
+
|
|
89
|
+
const nitro = await createPlasmNitro(host, true);
|
|
90
|
+
const server = createDevServer(nitro);
|
|
91
|
+
|
|
92
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
93
|
+
const listenHost = process.env.HOST ?? "127.0.0.1";
|
|
94
|
+
server.listen({ port, hostname: listenHost });
|
|
95
|
+
console.log(`[plasm-agent] Nitro dev → http://${listenHost}:${port}`);
|
|
96
|
+
|
|
97
|
+
return server;
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { build, copyPublicAssets, prepare, prerender } from "nitro/builder";
|
|
2
|
+
import type { Nitro } from "nitro/types";
|
|
3
|
+
|
|
4
|
+
export async function buildNitroOutput(nitro: Nitro): Promise<string> {
|
|
5
|
+
await prepare(nitro);
|
|
6
|
+
await copyPublicAssets(nitro);
|
|
7
|
+
await prerender(nitro);
|
|
8
|
+
await build(nitro);
|
|
9
|
+
const outDir = nitro.options.output.dir;
|
|
10
|
+
if (!outDir) {
|
|
11
|
+
throw new Error("Nitro build completed without output.dir");
|
|
12
|
+
}
|
|
13
|
+
return outDir;
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { access, cp } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { vercelOutputDir } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
await access(p);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Copy agent project files into the Vercel serverless bundle (runtime slot discovery). */
|
|
16
|
+
export async function copyVercelFunctionAssets(options: {
|
|
17
|
+
projectRoot: string;
|
|
18
|
+
agentRoot: string;
|
|
19
|
+
}): Promise<void> {
|
|
20
|
+
const funcDir = path.join(vercelOutputDir(options.projectRoot), "functions", "__server.func");
|
|
21
|
+
if (!(await pathExists(funcDir))) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await cp(options.agentRoot, path.join(funcDir, "agent"), { recursive: true });
|
|
26
|
+
|
|
27
|
+
const libDir = path.join(options.projectRoot, "lib");
|
|
28
|
+
if (await pathExists(libDir)) {
|
|
29
|
+
await cp(libDir, path.join(funcDir, "lib"), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import workflowNitro from "@workflow/nitro";
|
|
5
|
+
import { createNitro } from "nitro/builder";
|
|
6
|
+
import type { Nitro, NitroConfig } from "nitro/types";
|
|
7
|
+
|
|
8
|
+
import type { PreparedPlasmHost } from "./prepare-host.js";
|
|
9
|
+
import {
|
|
10
|
+
isVercelBuildEnvironment,
|
|
11
|
+
plasmNitroBuildDir,
|
|
12
|
+
plasmNitroOutputDir,
|
|
13
|
+
plasmNitroRoutesDir,
|
|
14
|
+
vercelOutputDir,
|
|
15
|
+
} from "./paths.js";
|
|
16
|
+
import { createPlasmVercelOptions } from "./vercel-build-output-config.js";
|
|
17
|
+
|
|
18
|
+
const SERVER_TRACE_DEPS = [
|
|
19
|
+
"@plasm_lang/engine",
|
|
20
|
+
"@plasm_lang/vercel-agent",
|
|
21
|
+
"@vercel/functions",
|
|
22
|
+
"@vercel/blob",
|
|
23
|
+
"@vercel/kv",
|
|
24
|
+
"@vercel/otel",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function resolveNitroPreset(dev: boolean): NitroConfig["preset"] {
|
|
28
|
+
if (dev) return "nitro-dev";
|
|
29
|
+
if (isVercelBuildEnvironment()) return "vercel";
|
|
30
|
+
return "node-server";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function publicDirExists(projectRoot: string): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
await access(path.join(projectRoot, "public"));
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createPlasmNitro(
|
|
43
|
+
host: PreparedPlasmHost,
|
|
44
|
+
dev: boolean,
|
|
45
|
+
): Promise<Nitro> {
|
|
46
|
+
const preset = resolveNitroPreset(dev);
|
|
47
|
+
const isVercel = preset === "vercel";
|
|
48
|
+
const plasmVercel = createPlasmVercelOptions(isVercel);
|
|
49
|
+
|
|
50
|
+
const modules: NitroConfig["modules"] = [];
|
|
51
|
+
if (host.workflowEnabled) {
|
|
52
|
+
modules.push(workflowNitro);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const traceDeps = [...new Set([...SERVER_TRACE_DEPS, ...host.externalDeps])];
|
|
56
|
+
|
|
57
|
+
const catchAllHandler = path.join(plasmNitroRoutesDir(host.projectRoot), "[[path]].ts");
|
|
58
|
+
|
|
59
|
+
const publicAssets = (await publicDirExists(host.projectRoot))
|
|
60
|
+
? [
|
|
61
|
+
{
|
|
62
|
+
dir: path.join(host.projectRoot, "public"),
|
|
63
|
+
baseURL: "/",
|
|
64
|
+
maxAge: 3600,
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
: [];
|
|
68
|
+
|
|
69
|
+
const vercelConfig = plasmVercel?.config ?? { version: 3 as const };
|
|
70
|
+
const config: NitroConfig = {
|
|
71
|
+
rootDir: host.projectRoot,
|
|
72
|
+
buildDir: plasmNitroBuildDir(host.projectRoot),
|
|
73
|
+
preset,
|
|
74
|
+
dev,
|
|
75
|
+
serverDir: false,
|
|
76
|
+
ignore: ["api/**", "server/**", "routes/**", "nitro.config.ts"],
|
|
77
|
+
handlers: [{ route: "/**", handler: catchAllHandler }],
|
|
78
|
+
publicAssets,
|
|
79
|
+
modules,
|
|
80
|
+
traceDeps: [
|
|
81
|
+
...traceDeps,
|
|
82
|
+
"./agent/**",
|
|
83
|
+
"./lib/**",
|
|
84
|
+
"./agent/.plasm/compiled/**",
|
|
85
|
+
"./agent/.plasm/stubs/**",
|
|
86
|
+
],
|
|
87
|
+
vercel: isVercel
|
|
88
|
+
? {
|
|
89
|
+
entryFormat: "node",
|
|
90
|
+
functions: {
|
|
91
|
+
maxDuration: 300,
|
|
92
|
+
environment: {
|
|
93
|
+
PLASM_RUN_ARTIFACTS_DIR: "/tmp/plasm-archives/runs",
|
|
94
|
+
PLASM_TRACE_ARCHIVE_DIR: "/tmp/plasm-archives/traces",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
config: {
|
|
98
|
+
...vercelConfig,
|
|
99
|
+
crons: host.scheduleCrons,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
: undefined,
|
|
103
|
+
output: isVercel
|
|
104
|
+
? { dir: vercelOutputDir(host.projectRoot) }
|
|
105
|
+
: dev
|
|
106
|
+
? undefined
|
|
107
|
+
: { dir: plasmNitroOutputDir(host.projectRoot) },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return createNitro(config, dev ? { watch: true } : undefined);
|
|
111
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { frameworkPackageVersion } from "../package-version.js";
|
|
5
|
+
import type { PreparedPlasmHost } from "./prepare-host.js";
|
|
6
|
+
import { vercelOutputDir } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
interface VercelOutputConfig {
|
|
9
|
+
version?: number;
|
|
10
|
+
framework?: { name?: string; version?: string };
|
|
11
|
+
crons?: Array<{ path: string; schedule: string }>;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Merge Plasm crons + framework metadata into Nitro-emitted config.json. */
|
|
16
|
+
export async function patchVercelOutputConfig(
|
|
17
|
+
projectRoot: string,
|
|
18
|
+
host: PreparedPlasmHost,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const configPath = path.join(vercelOutputDir(projectRoot), "config.json");
|
|
21
|
+
let config: VercelOutputConfig;
|
|
22
|
+
try {
|
|
23
|
+
config = JSON.parse(await readFile(configPath, "utf8")) as VercelOutputConfig;
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
config.version = 3;
|
|
29
|
+
config.framework = {
|
|
30
|
+
version: frameworkPackageVersion(),
|
|
31
|
+
};
|
|
32
|
+
if (host.scheduleCrons.length > 0) {
|
|
33
|
+
config.crons = host.scheduleCrons;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const PLASM_NITRO_BUILD_DIR = ".plasm/nitro";
|
|
4
|
+
export const PLASM_NITRO_ROUTES_DIR = ".plasm/nitro/routes";
|
|
5
|
+
export const PLASM_NITRO_OUTPUT_DIR = ".plasm/nitro-output";
|
|
6
|
+
export const PLASM_AGENT_SUMMARY_PATH = ".plasm/agent-summary.json";
|
|
7
|
+
|
|
8
|
+
export function plasmNitroBuildDir(projectRoot: string): string {
|
|
9
|
+
return path.join(projectRoot, PLASM_NITRO_BUILD_DIR);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function plasmNitroRoutesDir(projectRoot: string): string {
|
|
13
|
+
return path.join(projectRoot, PLASM_NITRO_ROUTES_DIR);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function plasmNitroOutputDir(projectRoot: string): string {
|
|
17
|
+
return path.join(projectRoot, PLASM_NITRO_OUTPUT_DIR);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function plasmAgentSummaryPath(projectRoot: string): string {
|
|
21
|
+
return path.join(projectRoot, PLASM_AGENT_SUMMARY_PATH);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function vercelOutputDir(projectRoot: string): string {
|
|
25
|
+
return path.join(projectRoot, ".vercel", "output");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isVercelBuildEnvironment(): boolean {
|
|
29
|
+
return process.env.VERCEL === "1" || process.env.VERCEL === "true";
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
import type { CompiledSlotMap } from "../cli/compile-authored-slots.js";
|
|
5
|
+
import { loadAuthoredSlots } from "../authoring/slot-loader.js";
|
|
6
|
+
import type { AgentDefinition } from "../define-agent.js";
|
|
7
|
+
import { walkAgentProject, type ProjectDiscovery } from "../discovery/project-walker.js";
|
|
8
|
+
import { exportScheduleCronManifest } from "../authoring/schedule-manager.js";
|
|
9
|
+
import { projectUsesWorkflowDirectives } from "./workflow-directives.js";
|
|
10
|
+
|
|
11
|
+
export interface PreparedPlasmHost {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
agentRoot: string;
|
|
14
|
+
definition: AgentDefinition;
|
|
15
|
+
discovery: ProjectDiscovery;
|
|
16
|
+
compiledSlots: CompiledSlotMap;
|
|
17
|
+
scheduleCrons: Array<{ path: string; schedule: string }>;
|
|
18
|
+
externalDeps: string[];
|
|
19
|
+
workflowEnabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadAgentDefinition(agentRoot: string): Promise<AgentDefinition> {
|
|
23
|
+
const agentPath = path.join(agentRoot, "agent.ts");
|
|
24
|
+
const mod = (await import(`${pathToFileURL(agentPath).href}?v=${Date.now()}`)) as {
|
|
25
|
+
default: AgentDefinition;
|
|
26
|
+
};
|
|
27
|
+
if (!mod.default?.model) {
|
|
28
|
+
throw new Error(`agent/agent.ts must default-export defineAgent({ model: ... })`);
|
|
29
|
+
}
|
|
30
|
+
return mod.default;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function preparePlasmHost(options: {
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
agentRoot: string;
|
|
36
|
+
compiledSlots: CompiledSlotMap;
|
|
37
|
+
discovery: ProjectDiscovery;
|
|
38
|
+
}): Promise<PreparedPlasmHost> {
|
|
39
|
+
const { projectRoot, agentRoot, compiledSlots, discovery } = options;
|
|
40
|
+
const definition = await loadAgentDefinition(agentRoot);
|
|
41
|
+
|
|
42
|
+
const loadedSlots = await loadAuthoredSlots({
|
|
43
|
+
discovery,
|
|
44
|
+
agentRoot,
|
|
45
|
+
projectRoot,
|
|
46
|
+
compiledSlots,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const scheduleCrons = exportScheduleCronManifest(loadedSlots.schedules).crons.map((c) => ({
|
|
50
|
+
path: c.path,
|
|
51
|
+
schedule: c.schedule,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const externalDeps = definition.build?.externalDependencies ?? [];
|
|
55
|
+
const workflowConfigured = definition.experimental?.workflow !== undefined;
|
|
56
|
+
const workflowEnabled =
|
|
57
|
+
workflowConfigured && (await projectUsesWorkflowDirectives(projectRoot));
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
projectRoot,
|
|
61
|
+
agentRoot,
|
|
62
|
+
definition,
|
|
63
|
+
discovery,
|
|
64
|
+
compiledSlots,
|
|
65
|
+
scheduleCrons,
|
|
66
|
+
externalDeps,
|
|
67
|
+
workflowEnabled,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { frameworkPackageVersion } from "../package-version.js";
|
|
2
|
+
|
|
3
|
+
export function createPlasmVercelOptions(enabled: boolean):
|
|
4
|
+
| {
|
|
5
|
+
config: {
|
|
6
|
+
version: 3;
|
|
7
|
+
framework: { version: string };
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
| undefined {
|
|
11
|
+
if (!enabled) return undefined;
|
|
12
|
+
return {
|
|
13
|
+
config: {
|
|
14
|
+
version: 3,
|
|
15
|
+
framework: { version: frameworkPackageVersion() },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const WORKFLOW_MARKERS = ["use workflow", "use step", "'use workflow'", '"use workflow"'];
|
|
5
|
+
|
|
6
|
+
async function walkTsFiles(dir: string): Promise<string[]> {
|
|
7
|
+
const out: string[] = [];
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
11
|
+
} catch {
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const full = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (entry.name === "node_modules" || entry.name === ".plasm") continue;
|
|
18
|
+
out.push(...(await walkTsFiles(full)));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
22
|
+
out.push(full);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True when authored sources contain Workflow SDK directives (needs @workflow/nitro). */
|
|
29
|
+
export async function projectUsesWorkflowDirectives(projectRoot: string): Promise<boolean> {
|
|
30
|
+
const roots = [
|
|
31
|
+
path.join(projectRoot, "agent"),
|
|
32
|
+
path.join(projectRoot, "lib"),
|
|
33
|
+
];
|
|
34
|
+
for (const root of roots) {
|
|
35
|
+
for (const file of await walkTsFiles(root)) {
|
|
36
|
+
const source = await readFile(file, "utf8");
|
|
37
|
+
if (WORKFLOW_MARKERS.some((marker) => source.includes(marker))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { CompiledSlotMap } from "../cli/compile-authored-slots.js";
|
|
5
|
+
import { plasmNitroRoutesDir } from "./paths.js";
|
|
6
|
+
|
|
7
|
+
function slotImportId(relFromAgent: string): string {
|
|
8
|
+
return relFromAgent.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Generate Nitro catch-all route that boots PlasmApp (eve-style programmatic host). */
|
|
12
|
+
export async function writePlasmNitroCatchAllRoute(
|
|
13
|
+
projectRoot: string,
|
|
14
|
+
compiledSlots: CompiledSlotMap = {},
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const routesDir = plasmNitroRoutesDir(projectRoot);
|
|
17
|
+
await mkdir(routesDir, { recursive: true });
|
|
18
|
+
const routePath = path.join(routesDir, "[[path]].ts");
|
|
19
|
+
|
|
20
|
+
const slotImports = Object.entries(compiledSlots)
|
|
21
|
+
.map(([relFromAgent, projectRelOut]) => {
|
|
22
|
+
const id = slotImportId(relFromAgent);
|
|
23
|
+
const importPath = `../../../${projectRelOut.replace(/\\/g, "/")}`;
|
|
24
|
+
return `import __slot_${id} from "${importPath}";`;
|
|
25
|
+
})
|
|
26
|
+
.join("\n");
|
|
27
|
+
|
|
28
|
+
const slotEntries = Object.keys(compiledSlots)
|
|
29
|
+
.map((relFromAgent) => {
|
|
30
|
+
const id = slotImportId(relFromAgent);
|
|
31
|
+
return ` ${JSON.stringify(relFromAgent)}: __slot_${id},`;
|
|
32
|
+
})
|
|
33
|
+
.join("\n");
|
|
34
|
+
|
|
35
|
+
const preloadBlock =
|
|
36
|
+
Object.keys(compiledSlots).length > 0
|
|
37
|
+
? `
|
|
38
|
+
${slotImports}
|
|
39
|
+
|
|
40
|
+
(globalThis as typeof globalThis & {
|
|
41
|
+
__PLASM_PRELOADED_SLOTS?: Record<string, unknown>;
|
|
42
|
+
}).__PLASM_PRELOADED_SLOTS = {
|
|
43
|
+
${slotEntries}
|
|
44
|
+
};
|
|
45
|
+
`
|
|
46
|
+
: "";
|
|
47
|
+
|
|
48
|
+
const source = `import path from "node:path";
|
|
49
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
50
|
+
|
|
51
|
+
import agentDefinition from "../../../agent/agent.js";
|
|
52
|
+
import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent/server";
|
|
53
|
+
|
|
54
|
+
import "../../../agent/instrumentation.js";
|
|
55
|
+
${preloadBlock}
|
|
56
|
+
const agentRoot = path.join(process.cwd(), "agent");
|
|
57
|
+
|
|
58
|
+
let appPromise: ReturnType<typeof createPlasmApp> | undefined;
|
|
59
|
+
|
|
60
|
+
async function plasmApp() {
|
|
61
|
+
appPromise ??= createPlasmApp({
|
|
62
|
+
agentRoot,
|
|
63
|
+
definition: agentDefinition,
|
|
64
|
+
mode: "prod",
|
|
65
|
+
sessions: false,
|
|
66
|
+
});
|
|
67
|
+
return appPromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Nitro wraps routes with \`toEventHandler\` — node req/res live on \`event.node\`. */
|
|
71
|
+
type NitroNodeEvent = {
|
|
72
|
+
node: { req: IncomingMessage; res: ServerResponse };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default async (event: NitroNodeEvent) => {
|
|
76
|
+
const app = await plasmApp();
|
|
77
|
+
const { req, res } = event.node;
|
|
78
|
+
await new Promise<void>((resolve, reject) => {
|
|
79
|
+
res.once("finish", () => resolve());
|
|
80
|
+
res.once("error", reject);
|
|
81
|
+
void vercelPlasmHandler(app)(req, res).catch(reject);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
`;
|
|
85
|
+
await writeFile(routePath, source, "utf8");
|
|
86
|
+
return routePath;
|
|
87
|
+
}
|