@plasm_lang/vercel-agent 0.3.86 → 0.3.88
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 +8 -10
- package/package.json +2 -2
- package/src/archive/adapters.ts +4 -0
- package/src/archive/prod-archive-store.ts +28 -45
- package/src/archive/resolve-backend.ts +8 -26
- package/src/archive/types.ts +1 -0
- package/src/archive/vercel-blob-adapter.ts +5 -0
- package/src/authoring/schedule-manager.ts +36 -22
- package/src/cli/build-manifest.ts +18 -0
- package/src/cli/build.ts +4 -13
- package/src/cli/compile-authored-slots.ts +9 -0
- package/src/cli/init-scaffold.ts +6 -2
- package/src/cli/init.ts +2 -6
- package/src/index.ts +3 -2
- package/src/nitro/agent-summary.ts +7 -7
- package/src/nitro/build-application.ts +43 -0
- package/src/nitro/create-plasm-nitro.ts +16 -2
- package/src/nitro/ensure-workflow-builder-dirs.ts +42 -0
- package/src/nitro/patch-vercel-config.ts +0 -3
- package/src/nitro/prepare-host.ts +5 -6
- package/src/nitro/workflow-directives.ts +1 -0
- package/src/nitro/write-nitro-schedule-tasks.ts +67 -0
- package/src/nitro/write-workflow-dispatch-route.ts +68 -0
- package/src/operator/routes.ts +6 -6
- package/src/project-info.ts +10 -6
- package/src/server/plasm-handler.ts +4 -4
- package/src/state/blob-state-adapter.ts +65 -0
- package/src/state/define-state.ts +10 -2
- package/src/state/fs-state-adapter.ts +1 -1
- package/src/storage/vercel-blob.ts +47 -0
- package/templates/mcp-radar/README.md +19 -16
- package/templates/mcp-radar/agent/channels/mcp-radar.ts +7 -7
- package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +3 -5
- package/templates/mcp-radar/lib/proof-store.ts +43 -44
- package/templates/mcp-radar/lib/run-radar.ts +1 -1
- package/templates/mcp-radar/lib/start-mcp-radar.ts +12 -0
- package/templates/mcp-radar/scripts/provision-vercel-storage.mjs +18 -0
- package/templates/mcp-radar/workflows/mcp-radar-scan.ts +26 -0
|
@@ -9,6 +9,7 @@ import type { PreparedPlasmHost } from "./prepare-host.js";
|
|
|
9
9
|
import {
|
|
10
10
|
isVercelBuildEnvironment,
|
|
11
11
|
plasmNitroBuildDir,
|
|
12
|
+
PLASM_NITRO_BUILD_DIR,
|
|
12
13
|
plasmNitroOutputDir,
|
|
13
14
|
plasmNitroRoutesDir,
|
|
14
15
|
vercelOutputDir,
|
|
@@ -16,12 +17,16 @@ import {
|
|
|
16
17
|
import { createPlasmVercelOptions } from "./vercel-build-output-config.js";
|
|
17
18
|
|
|
18
19
|
const SERVER_TRACE_DEPS = [
|
|
20
|
+
"@ai-sdk/otel",
|
|
21
|
+
"@opentelemetry/api",
|
|
19
22
|
"@plasm_lang/engine",
|
|
20
23
|
"@plasm_lang/vercel-agent",
|
|
21
24
|
"@vercel/functions",
|
|
22
25
|
"@vercel/blob",
|
|
23
|
-
"@vercel/kv",
|
|
24
26
|
"@vercel/otel",
|
|
27
|
+
"ai",
|
|
28
|
+
"workflow",
|
|
29
|
+
"workflow/api",
|
|
25
30
|
];
|
|
26
31
|
|
|
27
32
|
function resolveNitroPreset(dev: boolean): NitroConfig["preset"] {
|
|
@@ -70,6 +75,7 @@ export async function createPlasmNitro(
|
|
|
70
75
|
const config: NitroConfig = {
|
|
71
76
|
rootDir: host.projectRoot,
|
|
72
77
|
buildDir: plasmNitroBuildDir(host.projectRoot),
|
|
78
|
+
scanDirs: [path.join(host.projectRoot, PLASM_NITRO_BUILD_DIR)],
|
|
73
79
|
preset,
|
|
74
80
|
dev,
|
|
75
81
|
serverDir: false,
|
|
@@ -77,6 +83,15 @@ export async function createPlasmNitro(
|
|
|
77
83
|
handlers: [{ route: "/**", handler: catchAllHandler }],
|
|
78
84
|
publicAssets,
|
|
79
85
|
modules,
|
|
86
|
+
experimental: {
|
|
87
|
+
tasks: Object.keys(host.scheduledTasks).length > 0,
|
|
88
|
+
},
|
|
89
|
+
scheduledTasks: host.scheduledTasks,
|
|
90
|
+
workflow: host.workflowEnabled
|
|
91
|
+
? {
|
|
92
|
+
dirs: ["workflows"],
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
80
95
|
traceDeps: [
|
|
81
96
|
...traceDeps,
|
|
82
97
|
"./agent/**",
|
|
@@ -96,7 +111,6 @@ export async function createPlasmNitro(
|
|
|
96
111
|
},
|
|
97
112
|
config: {
|
|
98
113
|
...vercelConfig,
|
|
99
|
-
crons: host.scheduleCrons,
|
|
100
114
|
},
|
|
101
115
|
}
|
|
102
116
|
: undefined,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const WORKFLOW_DIRS_NEEDLE =
|
|
6
|
+
"dirs: ['.'], // Different apps that use nitro have different directories";
|
|
7
|
+
const WORKFLOW_DIRS_REPLACEMENT =
|
|
8
|
+
"dirs: nitro.options.workflow?.dirs ?? ['workflows'], // plasm: scoped workflow scan";
|
|
9
|
+
|
|
10
|
+
async function patchBuildersFile(buildersPath: string): Promise<void> {
|
|
11
|
+
const source = await readFile(buildersPath, "utf8");
|
|
12
|
+
if (!source.includes(WORKFLOW_DIRS_NEEDLE)) return;
|
|
13
|
+
await writeFile(
|
|
14
|
+
buildersPath,
|
|
15
|
+
source.replaceAll(WORKFLOW_DIRS_NEEDLE, WORKFLOW_DIRS_REPLACEMENT),
|
|
16
|
+
"utf8",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @workflow/nitro hardcodes dirs: ['.'] — scope to nitro.options.workflow.dirs for Eve apps. */
|
|
21
|
+
export async function ensureWorkflowBuilderDirs(projectRoot: string): Promise<void> {
|
|
22
|
+
const candidates = new Set<string>();
|
|
23
|
+
candidates.add(path.join(projectRoot, "node_modules/@workflow/nitro/dist/builders.js"));
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const nitroEntry = require.resolve("@workflow/nitro");
|
|
28
|
+
candidates.add(path.join(path.dirname(nitroEntry), "builders.js"));
|
|
29
|
+
} catch {
|
|
30
|
+
// framework package may not resolve @workflow/nitro in all contexts
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await Promise.all(
|
|
34
|
+
[...candidates].map(async (buildersPath) => {
|
|
35
|
+
try {
|
|
36
|
+
await patchBuildersFile(buildersPath);
|
|
37
|
+
} catch {
|
|
38
|
+
// optional copy
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -29,9 +29,6 @@ export async function patchVercelOutputConfig(
|
|
|
29
29
|
config.framework = {
|
|
30
30
|
version: frameworkPackageVersion(),
|
|
31
31
|
};
|
|
32
|
-
if (host.scheduleCrons.length > 0) {
|
|
33
|
-
config.crons = host.scheduleCrons;
|
|
34
|
-
}
|
|
35
32
|
|
|
36
33
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
37
34
|
}
|
|
@@ -5,7 +5,7 @@ import type { CompiledSlotMap } from "../cli/compile-authored-slots.js";
|
|
|
5
5
|
import { loadAuthoredSlots } from "../authoring/slot-loader.js";
|
|
6
6
|
import type { AgentDefinition } from "../define-agent.js";
|
|
7
7
|
import { walkAgentProject, type ProjectDiscovery } from "../discovery/project-walker.js";
|
|
8
|
-
import {
|
|
8
|
+
import { exportScheduleTaskManifest } from "../authoring/schedule-manager.js";
|
|
9
9
|
import { projectUsesWorkflowDirectives } from "./workflow-directives.js";
|
|
10
10
|
|
|
11
11
|
export interface PreparedPlasmHost {
|
|
@@ -15,6 +15,7 @@ export interface PreparedPlasmHost {
|
|
|
15
15
|
discovery: ProjectDiscovery;
|
|
16
16
|
compiledSlots: CompiledSlotMap;
|
|
17
17
|
scheduleCrons: Array<{ path: string; schedule: string }>;
|
|
18
|
+
scheduledTasks: Record<string, string | string[]>;
|
|
18
19
|
externalDeps: string[];
|
|
19
20
|
workflowEnabled: boolean;
|
|
20
21
|
}
|
|
@@ -46,10 +47,7 @@ export async function preparePlasmHost(options: {
|
|
|
46
47
|
compiledSlots,
|
|
47
48
|
});
|
|
48
49
|
|
|
49
|
-
const
|
|
50
|
-
path: c.path,
|
|
51
|
-
schedule: c.schedule,
|
|
52
|
-
}));
|
|
50
|
+
const taskManifest = exportScheduleTaskManifest(loadedSlots.schedules);
|
|
53
51
|
|
|
54
52
|
const externalDeps = definition.build?.externalDependencies ?? [];
|
|
55
53
|
const workflowConfigured = definition.experimental?.workflow !== undefined;
|
|
@@ -62,7 +60,8 @@ export async function preparePlasmHost(options: {
|
|
|
62
60
|
definition,
|
|
63
61
|
discovery,
|
|
64
62
|
compiledSlots,
|
|
65
|
-
scheduleCrons,
|
|
63
|
+
scheduleCrons: [],
|
|
64
|
+
scheduledTasks: taskManifest.scheduledTasks,
|
|
66
65
|
externalDeps,
|
|
67
66
|
workflowEnabled,
|
|
68
67
|
};
|
|
@@ -28,6 +28,7 @@ async function walkTsFiles(dir: string): Promise<string[]> {
|
|
|
28
28
|
/** True when authored sources contain Workflow SDK directives (needs @workflow/nitro). */
|
|
29
29
|
export async function projectUsesWorkflowDirectives(projectRoot: string): Promise<boolean> {
|
|
30
30
|
const roots = [
|
|
31
|
+
path.join(projectRoot, "workflows"),
|
|
31
32
|
path.join(projectRoot, "agent"),
|
|
32
33
|
path.join(projectRoot, "lib"),
|
|
33
34
|
];
|
|
@@ -0,0 +1,67 @@
|
|
|
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 type { LoadedSchedule } from "../authoring/slot-loader.js";
|
|
6
|
+
import { PLASM_NITRO_BUILD_DIR } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
function compiledScheduleImport(
|
|
9
|
+
schedule: LoadedSchedule,
|
|
10
|
+
agentRoot: string,
|
|
11
|
+
compiledSlots: CompiledSlotMap,
|
|
12
|
+
projectRoot: string,
|
|
13
|
+
): string {
|
|
14
|
+
const relFromAgent = path.relative(agentRoot, schedule.sourcePath);
|
|
15
|
+
const compiled = compiledSlots[relFromAgent];
|
|
16
|
+
if (!compiled) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`missing compiled schedule slot for ${schedule.definition.name} (${relFromAgent})`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const relImport = path
|
|
22
|
+
.relative(path.join(projectRoot, PLASM_NITRO_BUILD_DIR, "tasks"), path.join(projectRoot, compiled))
|
|
23
|
+
.replace(/\\/g, "/");
|
|
24
|
+
return relImport.startsWith(".") ? relImport : `./${relImport}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Emit Nitro scheduled tasks (Eve-aligned — no HTTP /internal/cron routes). */
|
|
28
|
+
export async function writeNitroScheduleTasks(options: {
|
|
29
|
+
projectRoot: string;
|
|
30
|
+
agentRoot: string;
|
|
31
|
+
schedules: LoadedSchedule[];
|
|
32
|
+
compiledSlots: CompiledSlotMap;
|
|
33
|
+
}): Promise<void> {
|
|
34
|
+
const { projectRoot, agentRoot, schedules, compiledSlots } = options;
|
|
35
|
+
const tasksDir = path.join(projectRoot, PLASM_NITRO_BUILD_DIR, "tasks");
|
|
36
|
+
await mkdir(tasksDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
for (const schedule of schedules) {
|
|
39
|
+
const name = schedule.definition.name;
|
|
40
|
+
const importPath = compiledScheduleImport(schedule, agentRoot, compiledSlots, projectRoot);
|
|
41
|
+
const source = `import path from "node:path";
|
|
42
|
+
|
|
43
|
+
import agentDefinition from "../../../agent/agent.js";
|
|
44
|
+
import { createPlasmApp } from "@plasm_lang/vercel-agent/server";
|
|
45
|
+
import scheduleSlot from "${importPath}";
|
|
46
|
+
|
|
47
|
+
const agentRoot = path.join(process.cwd(), "agent");
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
meta: {
|
|
51
|
+
description: ${JSON.stringify(`Plasm schedule: ${name}`)},
|
|
52
|
+
},
|
|
53
|
+
async run() {
|
|
54
|
+
const app = await createPlasmApp({
|
|
55
|
+
agentRoot,
|
|
56
|
+
definition: agentDefinition,
|
|
57
|
+
mode: "prod",
|
|
58
|
+
sessions: false,
|
|
59
|
+
});
|
|
60
|
+
const definition = scheduleSlot.default ?? scheduleSlot;
|
|
61
|
+
await definition.handler(app.getAuthoringContext());
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
`;
|
|
65
|
+
await writeFile(path.join(tasksDir, `${name}.ts`), source, "utf8");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const WORKFLOW_DISPATCH_ROUTE = "/internal/workflow/dispatch";
|
|
5
|
+
|
|
6
|
+
/** Emit workflow dispatch route for durable steps (no Plasm imports in workflow bundles). */
|
|
7
|
+
export async function writeWorkflowDispatchRoute(projectRoot: string): Promise<string> {
|
|
8
|
+
const routesDir = path.join(projectRoot, ".plasm", "nitro", "routes", "internal", "workflow");
|
|
9
|
+
await mkdir(routesDir, { recursive: true });
|
|
10
|
+
const routePath = path.join(routesDir, "dispatch.post.ts");
|
|
11
|
+
const source = `import path from "node:path";
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
13
|
+
|
|
14
|
+
import agentDefinition from "../../../../agent/agent.js";
|
|
15
|
+
import { createPlasmApp } from "@plasm_lang/vercel-agent/server";
|
|
16
|
+
|
|
17
|
+
const agentRoot = path.join(process.cwd(), "agent");
|
|
18
|
+
|
|
19
|
+
type NitroNodeEvent = {
|
|
20
|
+
node: { req: IncomingMessage; res: ServerResponse };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {
|
|
24
|
+
const chunks: Buffer[] = [];
|
|
25
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
26
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
27
|
+
if (!raw) return {};
|
|
28
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async (event: NitroNodeEvent) => {
|
|
32
|
+
const { req, res } = event.node;
|
|
33
|
+
if ((req.method ?? "GET").toUpperCase() !== "POST") {
|
|
34
|
+
res.statusCode = 405;
|
|
35
|
+
res.end("Method Not Allowed");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const body = await readJson(req);
|
|
40
|
+
const job = String(body.job ?? "");
|
|
41
|
+
const force = body.force === true;
|
|
42
|
+
|
|
43
|
+
if (job !== "mcp-radar-scan") {
|
|
44
|
+
res.statusCode = 404;
|
|
45
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
46
|
+
res.end(JSON.stringify({ error: "unknown_job", job }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const app = await createPlasmApp({
|
|
51
|
+
agentRoot,
|
|
52
|
+
definition: agentDefinition,
|
|
53
|
+
mode: "prod",
|
|
54
|
+
sessions: false,
|
|
55
|
+
});
|
|
56
|
+
const { runRadar } = await import("../../../../lib/run-radar.js");
|
|
57
|
+
const result = await runRadar(app.getAuthoringContext(), { force });
|
|
58
|
+
|
|
59
|
+
res.statusCode = 200;
|
|
60
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
61
|
+
res.end(JSON.stringify(result));
|
|
62
|
+
};
|
|
63
|
+
`;
|
|
64
|
+
await writeFile(routePath, source, "utf8");
|
|
65
|
+
return WORKFLOW_DISPATCH_ROUTE;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { WORKFLOW_DISPATCH_ROUTE };
|
package/src/operator/routes.ts
CHANGED
|
@@ -200,23 +200,23 @@ export function createOperatorRoutes(ctx: OperatorRouteContext): OperatorHandler
|
|
|
200
200
|
},
|
|
201
201
|
|
|
202
202
|
async listTraces() {
|
|
203
|
-
const {
|
|
204
|
-
const archive =
|
|
203
|
+
const { createArchiveStore } = await import("../archive/resolve-backend.js");
|
|
204
|
+
const archive = createArchiveStore(ctx.agentRoot);
|
|
205
205
|
const tenantScope = ctx.tenantScope ?? "local";
|
|
206
206
|
const traces = await archive.listTraces(tenantScope);
|
|
207
207
|
return { traces };
|
|
208
208
|
},
|
|
209
209
|
|
|
210
210
|
async listArchives() {
|
|
211
|
-
const {
|
|
212
|
-
const archive =
|
|
211
|
+
const { createArchiveStore } = await import("../archive/resolve-backend.js");
|
|
212
|
+
const archive = createArchiveStore(ctx.agentRoot);
|
|
213
213
|
const { plans, runs, paths } = await archive.listArchives();
|
|
214
214
|
return { plans, runs, paths };
|
|
215
215
|
},
|
|
216
216
|
|
|
217
217
|
async listRuns() {
|
|
218
|
-
const {
|
|
219
|
-
const archive =
|
|
218
|
+
const { createArchiveStore } = await import("../archive/resolve-backend.js");
|
|
219
|
+
const archive = createArchiveStore(ctx.agentRoot);
|
|
220
220
|
const runs = await archive.listRuns();
|
|
221
221
|
return { runs };
|
|
222
222
|
},
|
package/src/project-info.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { AgentDefinition } from "./define-agent.js";
|
|
|
7
7
|
import type { ProjectDiscovery } from "./discovery/project-walker.js";
|
|
8
8
|
import { walkAgentProject } from "./discovery/project-walker.js";
|
|
9
9
|
import { isNativeEngineAvailable } from "./engine/napi-binding.js";
|
|
10
|
-
import {
|
|
10
|
+
import { exportScheduleTaskManifest } from "./authoring/schedule-manager.js";
|
|
11
11
|
import { isGatewayConfigured } from "./gateway-model.js";
|
|
12
12
|
import type { LoadedProjectSlots } from "./authoring/slot-loader.js";
|
|
13
13
|
import { frameworkPackageVersion } from "./package-version.js";
|
|
@@ -38,7 +38,8 @@ export interface ProjectInfoRoutes {
|
|
|
38
38
|
sessionContinue: string;
|
|
39
39
|
sessionStream: string;
|
|
40
40
|
channels: Array<{ method: string; path: string }>;
|
|
41
|
-
|
|
41
|
+
scheduledTasks: string[];
|
|
42
|
+
scheduleDevDispatch: string[];
|
|
42
43
|
operator: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -75,7 +76,7 @@ export interface ProjectInfoPayload {
|
|
|
75
76
|
modelOptions: AgentDefinition["modelOptions"] | null;
|
|
76
77
|
build: AgentDefinition["build"] | null;
|
|
77
78
|
experimental: AgentDefinition["experimental"] | null;
|
|
78
|
-
|
|
79
|
+
scheduledTasks: ReturnType<typeof exportScheduleTaskManifest> | null;
|
|
79
80
|
sessions: { active: number };
|
|
80
81
|
routes: ProjectInfoRoutes;
|
|
81
82
|
};
|
|
@@ -176,14 +177,14 @@ export async function collectProjectInfo(
|
|
|
176
177
|
};
|
|
177
178
|
|
|
178
179
|
if (dev) {
|
|
179
|
-
const
|
|
180
|
+
const scheduledTasks = exportScheduleTaskManifest(loadedSlots.schedules);
|
|
180
181
|
payload.dev = {
|
|
181
182
|
model: dev.definition.model,
|
|
182
183
|
compaction: dev.definition.compaction ?? null,
|
|
183
184
|
modelOptions: dev.definition.modelOptions ?? null,
|
|
184
185
|
build: dev.definition.build ?? null,
|
|
185
186
|
experimental: dev.definition.experimental ?? null,
|
|
186
|
-
|
|
187
|
+
scheduledTasks,
|
|
187
188
|
sessions: { active: dev.sessionCount },
|
|
188
189
|
routes: {
|
|
189
190
|
health: "GET /plasm/v1/health",
|
|
@@ -192,7 +193,10 @@ export async function collectProjectInfo(
|
|
|
192
193
|
sessionContinue: "POST /plasm/v1/session/:sessionId",
|
|
193
194
|
sessionStream: "GET /plasm/v1/session/:id/stream",
|
|
194
195
|
channels: listChannelRoutes(loadedSlots.channels),
|
|
195
|
-
|
|
196
|
+
scheduledTasks: scheduledTasks.tasks.map((t) => `nitro:task/${t.name}`),
|
|
197
|
+
scheduleDevDispatch: scheduledTasks.tasks.map(
|
|
198
|
+
(t) => `POST /internal/schedule/${t.name}`,
|
|
199
|
+
),
|
|
196
200
|
operator: "GET /operator",
|
|
197
201
|
},
|
|
198
202
|
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
createAgentFromDefinition,
|
|
7
7
|
resolveAgentDefinition,
|
|
8
8
|
} from "../define-agent.js";
|
|
9
|
-
import { readBuildManifest } from "../cli/build.js";
|
|
9
|
+
import { readBuildManifest } from "../cli/build-manifest.js";
|
|
10
10
|
import { createAuthoringContext, type AuthoringContext } from "../authoring/context.js";
|
|
11
11
|
import { tryHandleChannelRoute } from "../authoring/channel-dispatch.js";
|
|
12
12
|
import {
|
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
type SubagentRegistry,
|
|
21
21
|
} from "../authoring/subagent-loader.js";
|
|
22
22
|
import {
|
|
23
|
+
tryHandleScheduleDevDispatch,
|
|
23
24
|
startScheduleTimers,
|
|
24
|
-
tryHandleScheduleCronRoute,
|
|
25
25
|
type ScheduleHandle,
|
|
26
26
|
} from "../authoring/schedule-manager.js";
|
|
27
27
|
import { walkAgentProject, type ProjectDiscovery } from "../discovery/project-walker.js";
|
|
@@ -143,13 +143,13 @@ export async function handlePlasmRequest(
|
|
|
143
143
|
|
|
144
144
|
const loadedSlots = await app.getLoadedSlots();
|
|
145
145
|
if (loadedSlots?.schedules.length) {
|
|
146
|
-
const
|
|
146
|
+
const handledSchedule = tryHandleScheduleDevDispatch(
|
|
147
147
|
req,
|
|
148
148
|
res,
|
|
149
149
|
loadedSlots.schedules,
|
|
150
150
|
app.getAuthoringContext(),
|
|
151
151
|
);
|
|
152
|
-
if (
|
|
152
|
+
if (handledSchedule) return;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
if (loadedSlots?.channels.length) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SymbolRegistrySnapshot } from "../symbol-registry.js";
|
|
2
|
+
import type { AgentSessionState } from "../session-state.js";
|
|
3
|
+
import {
|
|
4
|
+
blobGetJson,
|
|
5
|
+
blobList,
|
|
6
|
+
blobPutJson,
|
|
7
|
+
} from "../storage/vercel-blob.js";
|
|
8
|
+
import type { AgentStateStore, StateBackend } from "./define-state.js";
|
|
9
|
+
import { intentKey } from "./fs-state-adapter.js";
|
|
10
|
+
|
|
11
|
+
function sessionBlobKey(tenantScope: string, intent: string): string {
|
|
12
|
+
return `plasm/state/${tenantScope}/sessions/${intentKey(intent)}.json`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function symbolsBlobKey(tenantScope: string): string {
|
|
16
|
+
return `plasm/state/${tenantScope}/symbols.json`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sessionPrefix(tenantScope: string): string {
|
|
20
|
+
return `plasm/state/${tenantScope}/sessions/`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BlobStateAdapter implements AgentStateStore {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly agentRoot: string,
|
|
26
|
+
private readonly tenantScope: string,
|
|
27
|
+
) {
|
|
28
|
+
void this.agentRoot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
backend(): StateBackend {
|
|
32
|
+
return "blob";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(intent: string): Promise<AgentSessionState | null> {
|
|
36
|
+
return blobGetJson<AgentSessionState>(sessionBlobKey(this.tenantScope, intent));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async put(state: AgentSessionState): Promise<void> {
|
|
40
|
+
await blobPutJson(sessionBlobKey(this.tenantScope, state.intent), state);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async listIntents(): Promise<string[]> {
|
|
44
|
+
const prefix = sessionPrefix(this.tenantScope);
|
|
45
|
+
const paths = await blobList(prefix);
|
|
46
|
+
const intents: string[] = [];
|
|
47
|
+
for (const pathname of paths) {
|
|
48
|
+
if (!pathname.endsWith(".json")) continue;
|
|
49
|
+
const state = await blobGetJson<AgentSessionState>(pathname);
|
|
50
|
+
if (state?.intent) intents.push(state.intent);
|
|
51
|
+
}
|
|
52
|
+
return intents;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getSymbolRegistry(tenantId: string): Promise<SymbolRegistrySnapshot | null> {
|
|
56
|
+
return blobGetJson<SymbolRegistrySnapshot>(symbolsBlobKey(tenantId));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async putSymbolRegistry(
|
|
60
|
+
tenantId: string,
|
|
61
|
+
snapshot: SymbolRegistrySnapshot,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
await blobPutJson(symbolsBlobKey(tenantId), snapshot);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { AgentWorkflowWorldDefinition } from "../define-agent.js";
|
|
2
|
+
import { isVercelHosted } from "../gateway-model.js";
|
|
2
3
|
import type { SymbolRegistrySnapshot } from "../symbol-registry.js";
|
|
3
4
|
import type { AgentSessionState, SessionStore } from "../session-state.js";
|
|
5
|
+
import { hasLinkedBlobStore } from "../storage/vercel-blob.js";
|
|
6
|
+
import { BlobStateAdapter } from "./blob-state-adapter.js";
|
|
4
7
|
import { FsStateAdapter } from "./fs-state-adapter.js";
|
|
5
8
|
import { KvStateAdapter } from "./kv-state-adapter.js";
|
|
6
9
|
import { PostgresStateAdapter } from "./postgres-state-adapter.js";
|
|
7
10
|
|
|
8
|
-
export type StateBackend = "fs" | "kv" | "postgres";
|
|
11
|
+
export type StateBackend = "fs" | "blob" | "kv" | "postgres";
|
|
9
12
|
|
|
10
13
|
export interface AgentStateStore extends SessionStore {
|
|
11
14
|
backend(): StateBackend;
|
|
@@ -17,12 +20,15 @@ export function resolveStateBackend(
|
|
|
17
20
|
world?: AgentWorkflowWorldDefinition,
|
|
18
21
|
): StateBackend {
|
|
19
22
|
const explicit = process.env.PLASM_STATE_BACKEND?.trim().toLowerCase();
|
|
20
|
-
if (explicit === "fs" || explicit === "kv" || explicit === "postgres") {
|
|
23
|
+
if (explicit === "fs" || explicit === "blob" || explicit === "kv" || explicit === "postgres") {
|
|
21
24
|
return explicit;
|
|
22
25
|
}
|
|
23
26
|
if (process.env.KV_REST_API_URL?.trim() || process.env.PLASM_KV_REST_API_URL?.trim()) {
|
|
24
27
|
return "kv";
|
|
25
28
|
}
|
|
29
|
+
if (isVercelHosted() || hasLinkedBlobStore()) {
|
|
30
|
+
return "blob";
|
|
31
|
+
}
|
|
26
32
|
if (
|
|
27
33
|
process.env.WORKFLOW_POSTGRES_URL?.trim() ||
|
|
28
34
|
process.env.DATABASE_URL?.trim() ||
|
|
@@ -47,6 +53,8 @@ export function createAgentStateStore(options: {
|
|
|
47
53
|
const backend = options.backend ?? resolveStateBackend(options.world);
|
|
48
54
|
const tenantScope = options.tenantScope ?? "local";
|
|
49
55
|
switch (backend) {
|
|
56
|
+
case "blob":
|
|
57
|
+
return new BlobStateAdapter(options.agentRoot, tenantScope);
|
|
50
58
|
case "kv":
|
|
51
59
|
return new KvStateAdapter(options.agentRoot, tenantScope);
|
|
52
60
|
case "postgres":
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "../session-state.js";
|
|
9
9
|
import type { AgentStateStore, StateBackend } from "./define-state.js";
|
|
10
10
|
|
|
11
|
-
function intentKey(intent: string): string {
|
|
11
|
+
export function intentKey(intent: string): string {
|
|
12
12
|
return Buffer.from(intent, "utf8").toString("base64url");
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Eve-aligned Vercel Blob helpers — OIDC on hosted Vercel, no manual tokens required. */
|
|
2
|
+
|
|
3
|
+
export function hasLinkedBlobStore(): boolean {
|
|
4
|
+
return Boolean(
|
|
5
|
+
process.env.BLOB_READ_WRITE_TOKEN?.trim() ||
|
|
6
|
+
process.env.BLOB_STORE_ID?.trim() ||
|
|
7
|
+
process.env.PLASM_RUN_ARTIFACTS_URL?.trim(),
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function blobGetText(key: string): Promise<string | null> {
|
|
12
|
+
const { head } = await import("@vercel/blob");
|
|
13
|
+
const meta = await head(key).catch(() => null);
|
|
14
|
+
if (!meta?.url) return null;
|
|
15
|
+
const response = await fetch(meta.url);
|
|
16
|
+
if (!response.ok) return null;
|
|
17
|
+
return response.text();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function blobPutText(key: string, body: string): Promise<void> {
|
|
21
|
+
const { put } = await import("@vercel/blob");
|
|
22
|
+
await put(key, body, { access: "public", addRandomSuffix: false });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function blobGetJson<T>(key: string): Promise<T | null> {
|
|
26
|
+
const text = await blobGetText(key);
|
|
27
|
+
if (!text) return null;
|
|
28
|
+
return JSON.parse(text) as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function blobPutJson(key: string, value: unknown): Promise<void> {
|
|
32
|
+
await blobPutText(key, `${JSON.stringify(value, null, 2)}\n`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function blobList(prefix: string): Promise<string[]> {
|
|
36
|
+
const { list } = await import("@vercel/blob");
|
|
37
|
+
const keys: string[] = [];
|
|
38
|
+
let cursor: string | undefined;
|
|
39
|
+
do {
|
|
40
|
+
const page = await list({ prefix, cursor });
|
|
41
|
+
for (const blob of page.blobs) {
|
|
42
|
+
keys.push(blob.pathname);
|
|
43
|
+
}
|
|
44
|
+
cursor = page.hasMore ? page.cursor : undefined;
|
|
45
|
+
} while (cursor);
|
|
46
|
+
return keys;
|
|
47
|
+
}
|
|
@@ -26,24 +26,25 @@ The canonical template source is this directory (`examples/mcp-radar-agent/`).
|
|
|
26
26
|
|
|
27
27
|
## Deploy to Vercel
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Eve-aligned production path: link Blob once, deploy — **no manual env vars on Vercel** for Gateway, Blob, or cron auth.
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
cd examples/mcp-radar-agent
|
|
32
|
+
cd examples/mcp-radar-agent
|
|
33
33
|
plasm-agent link
|
|
34
|
+
node scripts/provision-vercel-storage.mjs # vercel blob create-store → project OIDC
|
|
34
35
|
plasm-agent build
|
|
35
|
-
vercel
|
|
36
|
-
vercel deploy
|
|
36
|
+
vercel deploy --prod
|
|
37
37
|
curl -s "$DEPLOY_URL/channel/mcp-radar/status" | jq .
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
40
|
+
| Concern | On Vercel |
|
|
41
|
+
|---------|-----------|
|
|
42
|
+
| AI Gateway | OIDC via linked project — no API key |
|
|
43
|
+
| Proof log + dedupe state | `@vercel/blob` only (markdown + JSON objects) |
|
|
44
|
+
| Cron | Platform `x-vercel-cron` — no `CRON_SECRET` required |
|
|
45
|
+
| Local dev | `vercel env pull .env.local` + optional `AI_GATEWAY_API_KEY` |
|
|
46
|
+
|
|
47
|
+
`vercel blob create-store` wires Blob to the project; runtime auth is OIDC (same model as [eve content agent](https://github.com/vercel-labs/eve-content-agent-template)).
|
|
47
48
|
|
|
48
49
|
On Vercel, `POST /channel/mcp-radar/run` returns `202` and continues in the background via `waitUntil`. Locally it runs synchronously for smoke tests.
|
|
49
50
|
|
|
@@ -59,7 +60,7 @@ cp .env.example .env.local
|
|
|
59
60
|
|
|
60
61
|
| Variable | Required | Role |
|
|
61
62
|
|----------|----------|------|
|
|
62
|
-
| `AI_GATEWAY_API_KEY` |
|
|
63
|
+
| `AI_GATEWAY_API_KEY` | Local only | Agent model turns off-Vercel |
|
|
63
64
|
| `TAVILY_API_TOKEN` | Optional | Tavily corroboration; HN-only when unset |
|
|
64
65
|
|
|
65
66
|
Build CGS stubs (symlinked catalogs):
|
|
@@ -82,7 +83,7 @@ Slash commands in the interactive TUI: `/info`, `/catalogs`, `/new`, `/quit`.
|
|
|
82
83
|
|
|
83
84
|
## Channel API
|
|
84
85
|
|
|
85
|
-
Start headless dev server (`npm run dev
|
|
86
|
+
Start headless dev server (`npm run dev`), then:
|
|
86
87
|
|
|
87
88
|
```bash
|
|
88
89
|
BASE=http://127.0.0.1:3000
|
|
@@ -102,16 +103,18 @@ curl -s -X POST "$BASE/channel/mcp-radar/run" -H 'content-type: application/json
|
|
|
102
103
|
|
|
103
104
|
Proof artifact (local fs): [`agent/research/mcp-innovations-proof.md`](./agent/research/mcp-innovations-proof.md)
|
|
104
105
|
|
|
105
|
-
Dedupe state (local fs): `agent/.plasm/research/seen-hn-items.json` (gitignored). On Vercel
|
|
106
|
+
Dedupe state (local fs): `agent/.plasm/research/seen-hn-items.json` (gitignored). On Vercel, proof + dedupe + last-run use Blob paths under `research/`.
|
|
106
107
|
|
|
107
108
|
## Schedule
|
|
108
109
|
|
|
109
|
-
`agent/schedules/mcp-radar-scan.ts` runs every **6 hours** (`0 */6 * * *`)
|
|
110
|
+
`agent/schedules/mcp-radar-scan.ts` runs every **6 hours** (`0 */6 * * *`) as a **Nitro scheduled task** (Vercel Cron auto-wired at deploy). Durable execution uses **Vercel Workflows** (`workflow/api` `start()`).
|
|
111
|
+
|
|
112
|
+
Manual dev trigger: `POST /internal/schedule/mcp-radar-scan`
|
|
110
113
|
|
|
111
114
|
## Evals
|
|
112
115
|
|
|
113
116
|
```bash
|
|
114
|
-
npm run eval # requires AI_GATEWAY_API_KEY
|
|
117
|
+
npm run eval # requires AI_GATEWAY_API_KEY locally
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
Live eval: `evals/mcp-radar-discover.eval.ts`
|