@plasm_lang/vercel-agent 0.3.112 → 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.
Files changed (36) hide show
  1. package/package.json +5 -4
  2. package/src/archive/prod-archive-store.ts +3 -2
  3. package/src/instrumentation.ts +8 -13
  4. package/src/load-env.ts +50 -2
  5. package/src/nitro/create-plasm-nitro.ts +5 -0
  6. package/src/nitro/write-workflow-dispatch-route.ts +2 -1
  7. package/src/runtime/agent-runtime.ts +4 -0
  8. package/src/server/plasm-handler.ts +12 -6
  9. package/src/server/resolve-app-options.ts +33 -0
  10. package/src/stubs/plasm-value-emitter.ts +21 -0
  11. package/src/stubs/stub-symbols.ts +34 -0
  12. package/templates/mcp-radar/README.md +11 -6
  13. package/templates/mcp-radar/agent/channels/mcp-radar.ts +21 -14
  14. package/templates/mcp-radar/agent/hooks/proof-audit.ts +16 -2
  15. package/templates/mcp-radar/agent/instructions.md +23 -11
  16. package/templates/mcp-radar/agent/instrumentation.ts +3 -4
  17. package/templates/mcp-radar/agent/skills/proof-publish.md +15 -0
  18. package/templates/mcp-radar/lib/hn-algolia-preflight.ts +42 -0
  19. package/templates/mcp-radar/lib/instructions.md +36 -0
  20. package/templates/mcp-radar/lib/mcp-radar-scan.ts +28 -0
  21. package/templates/mcp-radar/lib/mcp-radar.ts +92 -0
  22. package/templates/mcp-radar/lib/proof-extract.ts +43 -0
  23. package/templates/mcp-radar/lib/proof-store.ts +38 -0
  24. package/templates/mcp-radar/lib/provision-vercel.mjs +98 -0
  25. package/templates/mcp-radar/lib/radar-state.ts +53 -0
  26. package/templates/mcp-radar/lib/run-audit.ts +41 -0
  27. package/templates/mcp-radar/lib/run-radar.ts +37 -139
  28. package/templates/mcp-radar/lib/smoke-channel.ts +42 -0
  29. package/templates/mcp-radar/lib/start-mcp-radar.ts +1 -0
  30. package/templates/mcp-radar/lib/test-hn-algolia-preflight.mjs +27 -0
  31. package/templates/mcp-radar/package.json +21 -3
  32. package/templates/mcp-radar/scripts/provision-vercel.mjs +111 -0
  33. package/templates/mcp-radar/scripts/test-hn-algolia-preflight.mjs +27 -0
  34. package/templates/mcp-radar/scripts/test-proof-extract.mjs +24 -0
  35. package/templates/mcp-radar/workflows/mcp-radar-scan.ts +4 -4
  36. 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.112",
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": "^1.0.3",
66
+ "@ai-sdk/otel": "1.0.14",
66
67
  "@opentelemetry/api": "^1.9.0",
67
- "@plasm_lang/engine": "^0.3.112",
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": "^6.0.0",
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?: number) {
122
- return this.local.listArchives(limit);
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
 
@@ -1,9 +1,8 @@
1
1
  import { LegacyOpenTelemetry } from "@ai-sdk/otel";
2
- import type { AttributeValue } from "@opentelemetry/api";
3
2
  import {
4
- registerTelemetryIntegration,
5
- type TelemetryIntegration,
6
- type TelemetrySettings,
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(options: AgentInstrumentationOptions): TelemetryIntegration {
31
- return new LegacyOpenTelemetry({ tracer: options.tracer }) as unknown as TelemetryIntegration;
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
- registerTelemetryIntegration(otelIntegration(options));
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
- ): TelemetrySettings {
45
+ ): TelemetryOptions {
48
46
  registerAgentInstrumentation(options);
49
47
  return {
50
48
  isEnabled: true,
51
- metadata: {
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: options.tenantScope,
239
- maxSteps: options.maxSteps,
240
- telemetry: options.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: options.tenantScope,
272
- maxSteps: options.maxSteps,
273
- telemetry: options.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
- Eve-aligned production path: link Blob once, deploy — **no manual env vars on Vercel** for Gateway, Blob, or cron auth.
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-storage.mjs # vercel blob create-store project OIDC
35
- plasm-agent build
36
- vercel deploy --prod
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 | `vercel env pull .env.local` + optional `AI_GATEWAY_API_KEY` |
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` | Optional | Tavily corroboration; HN-only when unset |
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 = { force: body.force === true };
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 (req, res, ctx) => {
65
- const url = new URL(req.url ?? "/", "http://localhost");
66
- const format = url.searchParams.get("format");
67
- const markdown = await readProofMarkdown(ctx.agentRoot);
68
- if (format === "json") {
69
- sendJson(res, 200, { markdown, path: "agent/research/mcp-innovations-proof.md" });
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
- const keys = detail && typeof detail === "object" ? Object.keys(detail) : [];
8
- console.log(`[mcp-radar:hook:proof-audit] event detail keys=${keys.join(",")}`);
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 signals on Hacker News and corroborate them with Tavily web search.
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
- - Use stable intent: `track MCP innovations from Hacker News and corroborate with Tavily web search`
12
- 3. **`plasm`** dry-run programs using teaching TSV symbols.
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
- ## Per-run workflow
16
+ Reuse **`logical_session_ref`** across the whole radar cycle. Do not open a new context per story.
16
17
 
17
- 1. Search HN (`item_search` or `item_search_by_date`) for MCP-related stories from the candidate list in the user goal.
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
- If Tavily is unavailable, synthesize from HN only and set **Confidence: low**.
20
+ Use `item_search_by_date` (or `item_search`) with an MCP-focused query, e.g.:
23
21
 
24
- Do not invent `e#` / `p#` symbols copy from the teaching TSV.
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 { registerTelemetryIntegration } from "ai";
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
- registerTelemetryIntegration(new OpenTelemetry());
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.