@plasm_lang/vercel-agent 0.3.111 → 0.3.113
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/package.json +5 -4
- package/src/archive/prod-archive-store.ts +3 -2
- package/src/instrumentation.ts +8 -13
- package/src/load-env.ts +50 -2
- package/src/nitro/create-plasm-nitro.ts +5 -0
- package/src/nitro/write-workflow-dispatch-route.ts +2 -1
- package/src/runtime/agent-runtime.ts +4 -0
- package/src/server/plasm-handler.ts +12 -6
- package/src/server/resolve-app-options.ts +33 -0
- package/src/stubs/plasm-value-emitter.ts +21 -0
- package/src/stubs/stub-symbols.ts +34 -0
- package/templates/mcp-radar/README.md +11 -6
- package/templates/mcp-radar/agent/channels/mcp-radar.ts +21 -14
- package/templates/mcp-radar/agent/hooks/proof-audit.ts +16 -2
- package/templates/mcp-radar/agent/instructions.md +23 -11
- package/templates/mcp-radar/agent/instrumentation.ts +3 -4
- package/templates/mcp-radar/agent/skills/proof-publish.md +15 -0
- package/templates/mcp-radar/lib/hn-algolia-preflight.ts +42 -0
- package/templates/mcp-radar/lib/instructions.md +36 -0
- package/templates/mcp-radar/lib/mcp-radar-scan.ts +28 -0
- package/templates/mcp-radar/lib/mcp-radar.ts +92 -0
- package/templates/mcp-radar/lib/proof-extract.ts +43 -0
- package/templates/mcp-radar/lib/proof-store.ts +38 -0
- package/templates/mcp-radar/lib/provision-vercel.mjs +98 -0
- package/templates/mcp-radar/lib/radar-state.ts +53 -0
- package/templates/mcp-radar/lib/run-audit.ts +41 -0
- package/templates/mcp-radar/lib/run-radar.ts +37 -139
- package/templates/mcp-radar/lib/smoke-channel.ts +42 -0
- package/templates/mcp-radar/lib/start-mcp-radar.ts +1 -0
- package/templates/mcp-radar/lib/test-hn-algolia-preflight.mjs +27 -0
- package/templates/mcp-radar/package.json +21 -3
- package/templates/mcp-radar/scripts/provision-vercel.mjs +111 -0
- package/templates/mcp-radar/scripts/test-hn-algolia-preflight.mjs +27 -0
- package/templates/mcp-radar/scripts/test-proof-extract.mjs +24 -0
- package/templates/mcp-radar/workflows/mcp-radar-scan.ts +4 -4
- package/templates/scaffold/agent/instrumentation.ts +3 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasm_lang/vercel-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.113",
|
|
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": {
|
|
@@ -57,21 +57,22 @@
|
|
|
57
57
|
"smoke:bootstrap:npm": "node scripts/run-plasm-cli.mjs scripts/smoke-bootstrap-npm.ts",
|
|
58
58
|
"smoke:vercel-build": "node scripts/run-plasm-cli.mjs scripts/smoke-vercel-build.ts",
|
|
59
59
|
"smoke:vercel-handler": "node scripts/run-plasm-cli.mjs scripts/smoke-vercel-handler.ts",
|
|
60
|
+
"test:resolve-app-options": "node scripts/run-plasm-cli.mjs scripts/test-resolve-app-options.ts",
|
|
60
61
|
"agent": "node scripts/run-plasm-cli.mjs scripts/run-agent.ts",
|
|
61
62
|
"dev": "node scripts/run-plasm-cli.mjs scripts/dev-server.ts",
|
|
62
63
|
"plasm:dev": "npm run dev"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
|
-
"@ai-sdk/otel": "
|
|
66
|
+
"@ai-sdk/otel": "1.0.14",
|
|
66
67
|
"@opentelemetry/api": "^1.9.0",
|
|
67
|
-
"@plasm_lang/engine": "^0.3.
|
|
68
|
+
"@plasm_lang/engine": "^0.3.113",
|
|
68
69
|
"@vercel/blob": "^0.27.3",
|
|
69
70
|
"@vercel/connect": "^0.2.6",
|
|
70
71
|
"@vercel/kv": "^3.0.0",
|
|
71
72
|
"@vercel/otel": "^1.14.2",
|
|
72
73
|
"@workflow/nitro": "^4.1.1",
|
|
73
74
|
"@workflow/world-postgres": "^4.2.0",
|
|
74
|
-
"ai": "
|
|
75
|
+
"ai": "7.0.14",
|
|
75
76
|
"esbuild": "^0.25.12",
|
|
76
77
|
"js-yaml": "^4.1.0",
|
|
77
78
|
"nitro": "^3.0.260610-beta",
|
|
@@ -118,8 +118,9 @@ export class ProdArchiveStore {
|
|
|
118
118
|
return this.local.listRuns(limit);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
async listArchives(limit
|
|
122
|
-
|
|
121
|
+
async listArchives(limit = 50) {
|
|
122
|
+
const [plans, runs] = await Promise.all([this.listPlans(limit), this.listRuns(limit)]);
|
|
123
|
+
return { plans, runs, paths: this.local.paths };
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { LegacyOpenTelemetry } from "@ai-sdk/otel";
|
|
2
|
-
import type { AttributeValue } from "@opentelemetry/api";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
type
|
|
3
|
+
registerTelemetry,
|
|
4
|
+
type Telemetry,
|
|
5
|
+
type TelemetryOptions,
|
|
7
6
|
} from "ai";
|
|
8
7
|
|
|
9
8
|
/** OpenTelemetry attribute keys for Plasm agent spans (Section 9). */
|
|
@@ -22,13 +21,12 @@ export const PlasmSpanAttributes = {
|
|
|
22
21
|
|
|
23
22
|
export interface AgentInstrumentationOptions {
|
|
24
23
|
serviceName?: string;
|
|
25
|
-
tracer?: TelemetrySettings["tracer"];
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
let registered = false;
|
|
29
27
|
|
|
30
|
-
function otelIntegration(
|
|
31
|
-
return new LegacyOpenTelemetry(
|
|
28
|
+
function otelIntegration(_options: AgentInstrumentationOptions): Telemetry {
|
|
29
|
+
return new LegacyOpenTelemetry();
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
/** Register global AI SDK OTEL integration (eve-style auto-discovered instrumentation). */
|
|
@@ -36,7 +34,7 @@ export function registerAgentInstrumentation(
|
|
|
36
34
|
options: AgentInstrumentationOptions = {},
|
|
37
35
|
): void {
|
|
38
36
|
if (registered) return;
|
|
39
|
-
|
|
37
|
+
registerTelemetry(otelIntegration(options));
|
|
40
38
|
registered = true;
|
|
41
39
|
void options.serviceName;
|
|
42
40
|
}
|
|
@@ -44,13 +42,10 @@ export function registerAgentInstrumentation(
|
|
|
44
42
|
/** Per-call AI SDK telemetry settings with OpenTelemetry integration. */
|
|
45
43
|
export function createAgentTelemetry(
|
|
46
44
|
options: AgentInstrumentationOptions = {},
|
|
47
|
-
):
|
|
45
|
+
): TelemetryOptions {
|
|
48
46
|
registerAgentInstrumentation(options);
|
|
49
47
|
return {
|
|
50
48
|
isEnabled: true,
|
|
51
|
-
|
|
52
|
-
"service.name": options.serviceName ?? "plasm-agent",
|
|
53
|
-
} satisfies Record<string, AttributeValue>,
|
|
54
|
-
integrations: otelIntegration(options),
|
|
49
|
+
functionId: options.serviceName ?? "plasm-agent",
|
|
55
50
|
};
|
|
56
51
|
}
|
package/src/load-env.ts
CHANGED
|
@@ -14,6 +14,54 @@ export function defaultEnvFileCandidates(): string[] {
|
|
|
14
14
|
];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Load project env by walking ancestors; nearer files override, empty values are ignored. */
|
|
18
|
+
export function loadProjectAgentEnv(projectRoot: string): string | undefined {
|
|
19
|
+
const files = [...projectEnvFileCandidates(projectRoot)].reverse();
|
|
20
|
+
let loaded: string | undefined;
|
|
21
|
+
const pending = new Map<string, string>();
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
if (!existsSync(file)) continue;
|
|
24
|
+
const text = readFileSync(file, "utf8");
|
|
25
|
+
for (const line of text.split(/\r?\n/)) {
|
|
26
|
+
const parsed = parseEnvLine(line);
|
|
27
|
+
if (!parsed || !parsed.value.trim()) continue;
|
|
28
|
+
pending.set(parsed.key, parsed.value);
|
|
29
|
+
}
|
|
30
|
+
loaded = file;
|
|
31
|
+
}
|
|
32
|
+
for (const [key, value] of pending) {
|
|
33
|
+
if (process.env[key] === undefined) {
|
|
34
|
+
process.env[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!process.env.AI_GATEWAY_API_KEY?.trim()) {
|
|
38
|
+
const alias =
|
|
39
|
+
process.env.AI_API_GATEWAY_KEY?.trim() ??
|
|
40
|
+
process.env.AI_GATEWAY_KEY?.trim();
|
|
41
|
+
if (alias) {
|
|
42
|
+
process.env.AI_GATEWAY_API_KEY = alias;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return loaded;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `.env` candidates for an agent project root and ancestor directories. */
|
|
49
|
+
export function projectEnvFileCandidates(projectRoot: string): string[] {
|
|
50
|
+
const resolved = path.resolve(projectRoot);
|
|
51
|
+
const files = [
|
|
52
|
+
path.join(resolved, ".env.local"),
|
|
53
|
+
path.join(resolved, ".env"),
|
|
54
|
+
];
|
|
55
|
+
let dir = resolved;
|
|
56
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) break;
|
|
59
|
+
files.push(path.join(parent, ".env"));
|
|
60
|
+
dir = parent;
|
|
61
|
+
}
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
|
|
17
65
|
function parseEnvLine(line: string): { key: string; value: string } | null {
|
|
18
66
|
const trimmed = line.trim();
|
|
19
67
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
@@ -33,7 +81,7 @@ function parseEnvLine(line: string): { key: string; value: string } | null {
|
|
|
33
81
|
return { key, value };
|
|
34
82
|
}
|
|
35
83
|
|
|
36
|
-
/** Load `.env` into `process.env` without overriding existing variables. */
|
|
84
|
+
/** Load `.env` into `process.env` without overriding existing variables. Empty values are skipped. */
|
|
37
85
|
export function loadAgentEnv(files: string[] = defaultEnvFileCandidates()): string | undefined {
|
|
38
86
|
let loaded: string | undefined;
|
|
39
87
|
for (const file of files) {
|
|
@@ -41,7 +89,7 @@ export function loadAgentEnv(files: string[] = defaultEnvFileCandidates()): stri
|
|
|
41
89
|
const text = readFileSync(file, "utf8");
|
|
42
90
|
for (const line of text.split(/\r?\n/)) {
|
|
43
91
|
const parsed = parseEnvLine(line);
|
|
44
|
-
if (!parsed) continue;
|
|
92
|
+
if (!parsed || !parsed.value.trim()) continue;
|
|
45
93
|
if (process.env[parsed.key] === undefined) {
|
|
46
94
|
process.env[parsed.key] = parsed.value;
|
|
47
95
|
}
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
import { createPlasmVercelOptions } from "./vercel-build-output-config.js";
|
|
18
18
|
|
|
19
19
|
const SERVER_TRACE_DEPS = [
|
|
20
|
+
"@ai-sdk/gateway",
|
|
20
21
|
"@ai-sdk/otel",
|
|
22
|
+
"@ai-sdk/provider",
|
|
23
|
+
"@ai-sdk/provider-utils",
|
|
21
24
|
"@opentelemetry/api",
|
|
22
25
|
"@plasm_lang/engine",
|
|
23
26
|
"@plasm_lang/engine-linux-x64-gnu",
|
|
@@ -27,10 +30,12 @@ const SERVER_TRACE_DEPS = [
|
|
|
27
30
|
"@plasm_lang/vercel-agent",
|
|
28
31
|
"@vercel/functions",
|
|
29
32
|
"@vercel/blob",
|
|
33
|
+
"@vercel/oidc",
|
|
30
34
|
"@vercel/otel",
|
|
31
35
|
"ai",
|
|
32
36
|
"workflow",
|
|
33
37
|
"workflow/api",
|
|
38
|
+
"zod",
|
|
34
39
|
];
|
|
35
40
|
|
|
36
41
|
function resolveNitroPreset(dev: boolean): NitroConfig["preset"] {
|
|
@@ -39,6 +39,7 @@ export default async (event: NitroNodeEvent) => {
|
|
|
39
39
|
const body = await readJson(req);
|
|
40
40
|
const job = String(body.job ?? "");
|
|
41
41
|
const force = body.force === true;
|
|
42
|
+
const reset = body.reset === true;
|
|
42
43
|
|
|
43
44
|
if (job !== "mcp-radar-scan") {
|
|
44
45
|
res.statusCode = 404;
|
|
@@ -54,7 +55,7 @@ export default async (event: NitroNodeEvent) => {
|
|
|
54
55
|
sessions: false,
|
|
55
56
|
});
|
|
56
57
|
const { runRadar } = await import("../../../../../lib/run-radar.js");
|
|
57
|
-
const result = await runRadar(app.getAuthoringContext(), { force });
|
|
58
|
+
const result = await runRadar(app.getAuthoringContext(), { force, reset });
|
|
58
59
|
|
|
59
60
|
res.statusCode = 200;
|
|
60
61
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -311,6 +311,8 @@ export class AgentRuntime {
|
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
await this.emitHook("plan:commit", {
|
|
314
|
+
intent: session.intent,
|
|
315
|
+
planCommitRef: dry.planCommitRef,
|
|
314
316
|
runRef: dry.planCommitRef,
|
|
315
317
|
program: input.program,
|
|
316
318
|
logicalSessionRef: session.logicalSessionRef,
|
|
@@ -395,6 +397,8 @@ export class AgentRuntime {
|
|
|
395
397
|
});
|
|
396
398
|
|
|
397
399
|
await this.emitHook("run:complete", {
|
|
400
|
+
intent: session.intent,
|
|
401
|
+
planCommitRef: input.runRef,
|
|
398
402
|
runRef: input.runRef,
|
|
399
403
|
runId,
|
|
400
404
|
ok: result.ok,
|
|
@@ -32,6 +32,7 @@ import { sendJson } from "../dev/http.js";
|
|
|
32
32
|
import { tryHandleSessionRoutes } from "../dev/session-routes.js";
|
|
33
33
|
import { nitroOperatorHandler } from "../operator/routes.js";
|
|
34
34
|
import { renderOperatorShell } from "../operator/ui-shell.js";
|
|
35
|
+
import { resolvePlasmAppOptions } from "./resolve-app-options.js";
|
|
35
36
|
|
|
36
37
|
export type PlasmAppMode = "dev" | "prod";
|
|
37
38
|
|
|
@@ -51,6 +52,7 @@ export interface PlasmApp {
|
|
|
51
52
|
projectRoot: string;
|
|
52
53
|
definition: AgentDefinition;
|
|
53
54
|
mode: PlasmAppMode;
|
|
55
|
+
tenantScope: string;
|
|
54
56
|
sessionsEnabled: boolean;
|
|
55
57
|
reload(): Promise<void>;
|
|
56
58
|
getAgent(): Promise<PlasmAgent>;
|
|
@@ -191,6 +193,7 @@ export async function handlePlasmOperatorRequest(
|
|
|
191
193
|
|
|
192
194
|
const operatorHandler = nitroOperatorHandler({
|
|
193
195
|
agentRoot: app.agentRoot,
|
|
196
|
+
tenantScope: app.tenantScope,
|
|
194
197
|
runtime: undefined,
|
|
195
198
|
});
|
|
196
199
|
|
|
@@ -213,6 +216,7 @@ export async function createPlasmApp(options: PlasmAppOptions): Promise<PlasmApp
|
|
|
213
216
|
const projectRoot = path.dirname(agentRoot);
|
|
214
217
|
const mode = options.mode ?? "prod";
|
|
215
218
|
const sessionsEnabled = options.sessions ?? mode === "dev";
|
|
219
|
+
const resolved = resolvePlasmAppOptions(agentRoot, options);
|
|
216
220
|
const definition = resolveAgentDefinition(options.definition, agentRoot);
|
|
217
221
|
const sessionStore = sessionsEnabled ? new DevSessionStore() : undefined;
|
|
218
222
|
|
|
@@ -235,9 +239,10 @@ export async function createPlasmApp(options: PlasmAppOptions): Promise<PlasmApp
|
|
|
235
239
|
const bootstrap = async (): Promise<PlasmAgent> => {
|
|
236
240
|
agent = await createAgentFromDefinition(definition, {
|
|
237
241
|
agentRoot,
|
|
238
|
-
tenantScope:
|
|
239
|
-
maxSteps:
|
|
240
|
-
telemetry:
|
|
242
|
+
tenantScope: resolved.tenantScope,
|
|
243
|
+
maxSteps: resolved.maxSteps,
|
|
244
|
+
telemetry: resolved.telemetry,
|
|
245
|
+
hostTransport: resolved.hostTransport,
|
|
241
246
|
loadedSlots,
|
|
242
247
|
subagentRegistry,
|
|
243
248
|
getAuthoringContext,
|
|
@@ -268,9 +273,9 @@ export async function createPlasmApp(options: PlasmAppOptions): Promise<PlasmApp
|
|
|
268
273
|
const loaded = await loadSubagents({
|
|
269
274
|
discovery: currentDiscovery,
|
|
270
275
|
parentSlots: loadedSlots,
|
|
271
|
-
tenantScope:
|
|
272
|
-
maxSteps:
|
|
273
|
-
telemetry:
|
|
276
|
+
tenantScope: resolved.tenantScope,
|
|
277
|
+
maxSteps: resolved.maxSteps,
|
|
278
|
+
telemetry: resolved.telemetry,
|
|
274
279
|
importCacheBust,
|
|
275
280
|
});
|
|
276
281
|
if (loadedSlots) {
|
|
@@ -317,6 +322,7 @@ export async function createPlasmApp(options: PlasmAppOptions): Promise<PlasmApp
|
|
|
317
322
|
projectRoot,
|
|
318
323
|
definition,
|
|
319
324
|
mode,
|
|
325
|
+
tenantScope: resolved.tenantScope,
|
|
320
326
|
sessionsEnabled,
|
|
321
327
|
reload,
|
|
322
328
|
getAgent,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { createProductionHostTransport } from "../engine/create-host-transport.js";
|
|
4
|
+
import { loadProjectAgentEnv } from "../load-env.js";
|
|
5
|
+
import type { AgentRuntimeConfig } from "../runtime/agent-runtime.js";
|
|
6
|
+
|
|
7
|
+
import type { PlasmAppOptions } from "./plasm-handler.js";
|
|
8
|
+
|
|
9
|
+
export interface ResolvedPlasmAppOptions {
|
|
10
|
+
tenantScope: string;
|
|
11
|
+
maxSteps: number;
|
|
12
|
+
telemetry: boolean;
|
|
13
|
+
hostTransport: NonNullable<AgentRuntimeConfig["hostTransport"]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Load project `.env` files and merge runtime options for prod Nitro bootstrap. */
|
|
17
|
+
export function resolvePlasmAppOptions(
|
|
18
|
+
agentRoot: string,
|
|
19
|
+
partial: PlasmAppOptions,
|
|
20
|
+
): ResolvedPlasmAppOptions {
|
|
21
|
+
const projectRoot = path.dirname(path.resolve(agentRoot));
|
|
22
|
+
loadProjectAgentEnv(projectRoot);
|
|
23
|
+
|
|
24
|
+
const maxStepsRaw = partial.maxSteps ?? Number(process.env.PLASM_AGENT_MAX_STEPS ?? 24);
|
|
25
|
+
const maxSteps = Number.isFinite(maxStepsRaw) && maxStepsRaw > 0 ? maxStepsRaw : 24;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
tenantScope: partial.tenantScope ?? process.env.PLASM_TENANT_SCOPE?.trim() ?? "local",
|
|
29
|
+
maxSteps,
|
|
30
|
+
telemetry: partial.telemetry ?? process.env.PLASM_AGENT_TELEMETRY !== "0",
|
|
31
|
+
hostTransport: createProductionHostTransport(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -109,6 +109,27 @@ export function renderProgramStatements(
|
|
|
109
109
|
}
|
|
110
110
|
case "SearchFiltered": {
|
|
111
111
|
const q = searchFieldName(cap) ?? "q";
|
|
112
|
+
if (binding.searchSurface === "named-dot" && binding.searchMethodSegment) {
|
|
113
|
+
const body = invokeBodyFields(cap, shape, entityIdField);
|
|
114
|
+
const allArgs = body.map((f) => ({
|
|
115
|
+
key: f.name,
|
|
116
|
+
valueExpr: `${inputVar}.${f.name}`,
|
|
117
|
+
kind: emissionKindForField(f, catalogValues),
|
|
118
|
+
optional: !f.required,
|
|
119
|
+
}));
|
|
120
|
+
const qField = body.find((f) => f.name === q);
|
|
121
|
+
if (!qField) {
|
|
122
|
+
allArgs.unshift({
|
|
123
|
+
key: q,
|
|
124
|
+
valueExpr: `${inputVar}.${q}`,
|
|
125
|
+
kind: "literal" as const,
|
|
126
|
+
optional: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const dottedCall = renderBuildDottedArgsCall(allArgs);
|
|
130
|
+
return `const args = ${dottedCall};
|
|
131
|
+
const program = \`${sym}.${binding.searchMethodSegment}(\${args})\`;`;
|
|
132
|
+
}
|
|
112
133
|
const args = dottedArgsCodegen(cap, catalogValues, shape, entityIdField, inputVar);
|
|
113
134
|
const dottedCall = renderBuildDottedArgsCall(args);
|
|
114
135
|
return `const filterArgs = ${dottedCall};
|
|
@@ -4,6 +4,23 @@ import {
|
|
|
4
4
|
type CapabilityInvokeShape,
|
|
5
5
|
} from "./capability-invoke-shape.js";
|
|
6
6
|
|
|
7
|
+
function capabilitySearchMethodSegment(cap: CapabilityIntrospectionJson): string {
|
|
8
|
+
const ent = cap.entity.toLowerCase();
|
|
9
|
+
const prefix = `${ent}_`;
|
|
10
|
+
const stripped = cap.name.startsWith(prefix) ? cap.name.slice(prefix.length) : cap.name;
|
|
11
|
+
return stripped.replaceAll("_", "-");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function primarySearchCapabilityName(
|
|
15
|
+
catalog: CatalogIntrospectionJson,
|
|
16
|
+
entity: string,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
const caps = catalog.capabilities
|
|
19
|
+
.filter((c) => c.entity === entity && c.kind.toLowerCase() === "search")
|
|
20
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
21
|
+
return caps[0]?.name;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
export interface EntitySymbolBinding {
|
|
8
25
|
entity: string;
|
|
9
26
|
symbol: string;
|
|
@@ -15,6 +32,9 @@ export interface CapabilityBinding {
|
|
|
15
32
|
methodSymbol?: string;
|
|
16
33
|
methodWire: string;
|
|
17
34
|
invokeShape: CapabilityInvokeShape;
|
|
35
|
+
/** Primary search uses `e#~text`; non-primary uses `e#.search-by-date(...)`. */
|
|
36
|
+
searchSurface?: "tilde" | "named-dot";
|
|
37
|
+
searchMethodSegment?: string;
|
|
18
38
|
}
|
|
19
39
|
|
|
20
40
|
/** Entities with capabilities, stable lexicographic order → `e1`…`eN`. */
|
|
@@ -73,12 +93,26 @@ export function assignCapabilityBindings(
|
|
|
73
93
|
if (!entityBinding) continue;
|
|
74
94
|
const entityCaps = capsByEntity.get(cap.entity) ?? [];
|
|
75
95
|
const invokeShape = classifyInvokeShape(cap);
|
|
96
|
+
const primarySearch =
|
|
97
|
+
invokeShape === "SearchText" || invokeShape === "SearchFiltered"
|
|
98
|
+
? primarySearchCapabilityName(catalog, cap.entity)
|
|
99
|
+
: undefined;
|
|
76
100
|
out.set(cap.name, {
|
|
77
101
|
capability: cap.name,
|
|
78
102
|
entitySymbol: entityBinding.symbol,
|
|
79
103
|
methodSymbol: methodSymbolForCapability(cap, entityCaps),
|
|
80
104
|
methodWire: cap.invoke_wire_name,
|
|
81
105
|
invokeShape,
|
|
106
|
+
searchSurface:
|
|
107
|
+
primarySearch != null
|
|
108
|
+
? cap.name === primarySearch
|
|
109
|
+
? "tilde"
|
|
110
|
+
: "named-dot"
|
|
111
|
+
: undefined,
|
|
112
|
+
searchMethodSegment:
|
|
113
|
+
primarySearch != null && cap.name !== primarySearch
|
|
114
|
+
? capabilitySearchMethodSegment(cap)
|
|
115
|
+
: undefined,
|
|
82
116
|
});
|
|
83
117
|
}
|
|
84
118
|
return out;
|
|
@@ -26,23 +26,27 @@ The canonical template source is this directory (`examples/mcp-radar-agent/`).
|
|
|
26
26
|
|
|
27
27
|
## Deploy to Vercel
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Canonical stack: **AI SDK v7** (`ai@7.0.14`, `@ai-sdk/otel@1.0.14`) + **Workflow 4.5** + pinned `zod@4.3.6`. Use **pnpm** (lockfile committed).
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
cd examples/mcp-radar-agent
|
|
33
33
|
plasm-agent link
|
|
34
|
-
node scripts/provision-vercel
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
node scripts/provision-vercel.mjs # sync monorepo .env secrets + Blob store
|
|
35
|
+
pnpm install
|
|
36
|
+
pnpm run build # local Nitro output
|
|
37
|
+
# Remote build on Vercel requires @plasm_lang/vercel-agent@0.3.114+ on npm
|
|
38
|
+
VERCEL=1 pnpm run build && vercel deploy --prebuilt --prod # monorepo prebuilt path
|
|
37
39
|
curl -s "$DEPLOY_URL/channel/mcp-radar/status" | jq .
|
|
38
40
|
```
|
|
39
41
|
|
|
40
42
|
| Concern | On Vercel |
|
|
41
43
|
|---------|-----------|
|
|
42
44
|
| AI Gateway | OIDC via linked project — no API key |
|
|
45
|
+
| Tavily | `TAVILY_API_TOKEN` synced from monorepo `.env` via provision script |
|
|
46
|
+
| Tenant scope | `PLASM_TENANT_SCOPE` (default `mcp-radar`) synced from `.env` |
|
|
43
47
|
| Proof log + dedupe state | `@vercel/blob` only (markdown + JSON objects) |
|
|
44
48
|
| Cron | Platform `x-vercel-cron` — no `CRON_SECRET` required |
|
|
45
|
-
| Local dev |
|
|
49
|
+
| Local dev | monorepo `.env` loaded automatically; optional `AI_GATEWAY_API_KEY` off-Vercel |
|
|
46
50
|
|
|
47
51
|
`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)).
|
|
48
52
|
|
|
@@ -61,7 +65,8 @@ cp .env.example .env.local
|
|
|
61
65
|
| Variable | Required | Role |
|
|
62
66
|
|----------|----------|------|
|
|
63
67
|
| `AI_GATEWAY_API_KEY` | Local only | Agent model turns off-Vercel |
|
|
64
|
-
| `TAVILY_API_TOKEN` |
|
|
68
|
+
| `TAVILY_API_TOKEN` | Recommended | Tavily corroboration; set in monorepo `.env`, synced to Vercel via `provision-vercel.mjs` |
|
|
69
|
+
| `PLASM_TENANT_SCOPE` | Optional | Session/operator partition (default `mcp-radar`) |
|
|
65
70
|
|
|
66
71
|
Build CGS stubs (symlinked catalogs):
|
|
67
72
|
|
|
@@ -3,7 +3,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
3
3
|
import { waitUntil } from "@vercel/functions";
|
|
4
4
|
import { defineChannel } from "@plasm_lang/vercel-agent";
|
|
5
5
|
|
|
6
|
-
import { readProofMarkdown } from "../../lib/proof-store.js";
|
|
7
6
|
import { radarStatus } from "../../lib/run-radar.js";
|
|
8
7
|
import { startMcpRadarRun } from "../../lib/start-mcp-radar.js";
|
|
9
8
|
|
|
@@ -41,8 +40,11 @@ export default defineChannel({
|
|
|
41
40
|
method: "POST",
|
|
42
41
|
path: "/channel/mcp-radar/run",
|
|
43
42
|
handler: async (req, res, ctx) => {
|
|
44
|
-
const body = (await readJsonBody(req)) as { force?: boolean };
|
|
45
|
-
const options = {
|
|
43
|
+
const body = (await readJsonBody(req)) as { force?: boolean; reset?: boolean };
|
|
44
|
+
const options = {
|
|
45
|
+
force: body.force === true,
|
|
46
|
+
reset: body.reset === true,
|
|
47
|
+
};
|
|
46
48
|
|
|
47
49
|
if (process.env.VERCEL === "1") {
|
|
48
50
|
waitUntil(
|
|
@@ -58,20 +60,25 @@ export default defineChannel({
|
|
|
58
60
|
sendJson(res, 200, { accepted: true, workflow: true, run });
|
|
59
61
|
},
|
|
60
62
|
},
|
|
63
|
+
{
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: "/channel/mcp-radar/reset",
|
|
66
|
+
handler: async (_req, res, _ctx) => {
|
|
67
|
+
sendJson(res, 200, {
|
|
68
|
+
ok: true,
|
|
69
|
+
message: 'Trigger POST /channel/mcp-radar/run with {"reset":true}',
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
},
|
|
61
73
|
{
|
|
62
74
|
method: "GET",
|
|
63
75
|
path: "/channel/mcp-radar/proof",
|
|
64
|
-
handler: async (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
res.statusCode = 200;
|
|
73
|
-
res.setHeader("content-type", "text/markdown; charset=utf-8");
|
|
74
|
-
res.end(markdown);
|
|
76
|
+
handler: async (_req, res, _ctx) => {
|
|
77
|
+
sendJson(res, 200, {
|
|
78
|
+
note: "Proof document lives in the proof catalog; read/write via agent Plasm session.",
|
|
79
|
+
sessions: "/operator/sessions",
|
|
80
|
+
runs: "/operator/runs",
|
|
81
|
+
});
|
|
75
82
|
},
|
|
76
83
|
},
|
|
77
84
|
{
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { defineHook } from "@plasm_lang/vercel-agent";
|
|
2
2
|
|
|
3
|
+
import { recordRunAudit } from "../../lib/run-audit.js";
|
|
4
|
+
|
|
3
5
|
export default defineHook({
|
|
4
6
|
name: "proof-audit",
|
|
5
7
|
on: ["run:complete", "plan:commit"],
|
|
6
8
|
handler: (_ctx, detail) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
if (!detail || typeof detail !== "object") return;
|
|
10
|
+
recordRunAudit(detail as Record<string, unknown>);
|
|
11
|
+
const d = detail as Record<string, unknown>;
|
|
12
|
+
console.log(
|
|
13
|
+
"[mcp-radar:hook:proof-audit]",
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
event: "audit",
|
|
16
|
+
runId: d.runId,
|
|
17
|
+
planCommitRef: d.planCommitRef ?? d.runRef,
|
|
18
|
+
logicalSessionRef: d.logicalSessionRef,
|
|
19
|
+
intent: d.intent,
|
|
20
|
+
ok: d.ok,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
9
23
|
},
|
|
10
24
|
});
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
# MCP Radar
|
|
2
2
|
|
|
3
|
-
You track **Model Context Protocol (MCP)** innovation
|
|
3
|
+
You track **Model Context Protocol (MCP)** innovation on Hacker News, corroborate with Tavily, and **publish** to a live **Proof** document — all via **Plasm programs** in one federated execute session.
|
|
4
4
|
|
|
5
|
-
## Tool order
|
|
5
|
+
## Tool order (mandatory)
|
|
6
6
|
|
|
7
7
|
1. **`discover_capabilities`** — only when unsure which catalog entities to use.
|
|
8
8
|
2. **`plasm_context`** — open or extend a session with seeds:
|
|
9
9
|
- `hackernews:Item`
|
|
10
10
|
- `tavily:SearchResult`
|
|
11
|
-
-
|
|
12
|
-
|
|
11
|
+
- `proof:Document`
|
|
12
|
+
- **Stable intent** (same every turn): `track MCP innovations from Hacker News and corroborate with Tavily web search`
|
|
13
|
+
3. **`plasm`** — dry-run programs using teaching TSV symbols (`e#`, `m#`, `p#`, `r#`).
|
|
13
14
|
4. **`plasm_run`** — live execute reviewed plans (`pcN` only).
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
Reuse **`logical_session_ref`** across the whole radar cycle. Do not open a new context per story.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
2. For each **new** story, optionally `item_get` for score and metadata.
|
|
19
|
-
3. If Tavily is available, run `web_search` with a query derived from the story title to corroborate.
|
|
20
|
-
4. Emit **proof entries** using the `mcp-proof-format` skill template (one `###` block per story).
|
|
18
|
+
## HN search (Plasm)
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
Use `item_search_by_date` (or `item_search`) with an MCP-focused query, e.g.:
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
`"Model Context Protocol" OR MCP server OR MCP tool`
|
|
23
|
+
|
|
24
|
+
Filter to stories **about MCP** — not general Hacker News. Skip ids already in the Proof document body (read via `document_get_markdown` first).
|
|
25
|
+
|
|
26
|
+
## Proof document (Plasm)
|
|
27
|
+
|
|
28
|
+
See skill **`proof-publish`**: bind share link (`document_share_bind`), `presence_update`, read markdown, append via `document_edit_v2`.
|
|
29
|
+
|
|
30
|
+
If no document exists yet, `share_link_create` then bind. Do not emit proof only in chat — **`plasm_run` must mutate Proof**.
|
|
31
|
+
|
|
32
|
+
## Tavily
|
|
33
|
+
|
|
34
|
+
When Tavily is on the session, corroborate with `web_search`. If unavailable, set **Confidence: low** per `mcp-proof-format`.
|
|
35
|
+
|
|
36
|
+
Do not invent symbols — copy from the teaching TSV.
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
|
-
import { registerOTel } from "@vercel/otel";
|
|
5
4
|
import { OpenTelemetry } from "@ai-sdk/otel";
|
|
6
|
-
import {
|
|
5
|
+
import { registerOTel } from "@vercel/otel";
|
|
6
|
+
import { registerTelemetry } from "ai";
|
|
7
7
|
|
|
8
8
|
const agentRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const serviceName =
|
|
10
10
|
process.env.PLASM_AGENT_NAME?.trim() || path.basename(path.dirname(agentRoot));
|
|
11
11
|
|
|
12
|
-
/** Eve-shaped OTEL bootstrap — optional export to Braintrust/Datadog via registerOTel. */
|
|
13
12
|
export function register(): void {
|
|
14
13
|
registerOTel({ serviceName });
|
|
15
|
-
|
|
14
|
+
registerTelemetry(new OpenTelemetry());
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
register();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Publish MCP proof entries to Proof
|
|
2
|
+
|
|
3
|
+
After composing proof markdown (see `mcp-proof-format`), **append to the live Proof document** via Plasm — not chat-only output.
|
|
4
|
+
|
|
5
|
+
## Sequence (same `logical_session_ref`)
|
|
6
|
+
|
|
7
|
+
1. `document_share_bind` — once per doc if not already bound (`share_url` or `share_token`).
|
|
8
|
+
2. `presence_update` — agent_id e.g. `mcp-radar`.
|
|
9
|
+
3. `editor_state_get` — host caches `base_token` for edits.
|
|
10
|
+
4. `block_query` — list blocks; pick last block ref for append.
|
|
11
|
+
5. `document_edit_v2` — `insert_after` with the proof markdown block(s), `base_revision` from editor state, `by` = agent name.
|
|
12
|
+
|
|
13
|
+
On stale revision, re-run `editor_state_get` + `block_query` before retrying the edit.
|
|
14
|
+
|
|
15
|
+
Reset runs: replace body with `# MCP Innovations Proof Log` only, then append fresh entries.
|