@plasm_lang/vercel-agent 0.3.85 → 0.3.87

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 (38) hide show
  1. package/README.md +8 -10
  2. package/package.json +2 -2
  3. package/src/archive/adapters.ts +4 -0
  4. package/src/archive/prod-archive-store.ts +28 -45
  5. package/src/archive/resolve-backend.ts +8 -26
  6. package/src/archive/types.ts +1 -0
  7. package/src/archive/vercel-blob-adapter.ts +5 -0
  8. package/src/authoring/schedule-manager.ts +36 -22
  9. package/src/cli/build-manifest.ts +18 -0
  10. package/src/cli/build.ts +4 -13
  11. package/src/cli/compile-authored-slots.ts +9 -0
  12. package/src/cli/init-scaffold.ts +6 -2
  13. package/src/cli/init.ts +2 -6
  14. package/src/index.ts +3 -2
  15. package/src/nitro/agent-summary.ts +7 -7
  16. package/src/nitro/build-application.ts +43 -0
  17. package/src/nitro/create-plasm-nitro.ts +16 -2
  18. package/src/nitro/ensure-workflow-builder-dirs.ts +42 -0
  19. package/src/nitro/patch-vercel-config.ts +0 -3
  20. package/src/nitro/prepare-host.ts +5 -6
  21. package/src/nitro/workflow-directives.ts +1 -0
  22. package/src/nitro/write-nitro-schedule-tasks.ts +67 -0
  23. package/src/nitro/write-workflow-dispatch-route.ts +68 -0
  24. package/src/operator/routes.ts +6 -6
  25. package/src/project-info.ts +10 -6
  26. package/src/server/plasm-handler.ts +4 -4
  27. package/src/state/blob-state-adapter.ts +65 -0
  28. package/src/state/define-state.ts +10 -2
  29. package/src/state/fs-state-adapter.ts +1 -1
  30. package/src/storage/vercel-blob.ts +47 -0
  31. package/templates/mcp-radar/README.md +19 -16
  32. package/templates/mcp-radar/agent/channels/mcp-radar.ts +7 -7
  33. package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +3 -5
  34. package/templates/mcp-radar/lib/proof-store.ts +43 -44
  35. package/templates/mcp-radar/lib/run-radar.ts +1 -1
  36. package/templates/mcp-radar/lib/start-mcp-radar.ts +12 -0
  37. package/templates/mcp-radar/scripts/provision-vercel-storage.mjs +18 -0
  38. package/templates/mcp-radar/workflows/mcp-radar-scan.ts +26 -0
package/README.md CHANGED
@@ -95,10 +95,10 @@ cd ../plasm-agent && npm install
95
95
 
96
96
  ## Storage (`defineState` + archives)
97
97
 
98
- | Concern | Dev (`fs`) | Prod |
99
- |---------|------------|------|
100
- | Sessions / symbols | `agent/.plasm/` | `PLASM_STATE_BACKEND=kv` or `postgres` |
101
- | Run/trace archives | local dirs | `BLOB_READ_WRITE_TOKEN` + `KV_REST_API_*` |
98
+ | Concern | Dev (`fs`) | Prod (Vercel) |
99
+ |---------|------------|----------------|
100
+ | Sessions / symbols | `agent/.plasm/` | Vercel Blob (`PLASM_STATE_BACKEND=blob`, default on Vercel) |
101
+ | Run/trace archives | local dirs | Vercel Blob (`vercel blob create-store` — OIDC, no manual token) |
102
102
 
103
103
  See **Connect / state / archive env** below.
104
104
 
@@ -180,7 +180,7 @@ Hot reload watches `catalogs/`, `skills/`, `channels/`, `schedules/`, `hooks/`,
180
180
  |------|--------|-------|
181
181
  | `skills/` | markdown or `defineSkill()` | Progressive index + `read_skill` tool (`experimental.skills: "inline"` for full inject) |
182
182
  | `channels/` | `defineChannel()` | HTTP ingress; call stubs, not `plasm_context` |
183
- | `schedules/` | `defineSchedule()` | Dev: `*/N * * * *` timers; prod: `GET /internal/cron/:name` + Vercel Cron manifest in `/plasm/v1/info` |
183
+ | `schedules/` | `defineSchedule()` | Dev: `*/N * * * *` timers; prod: Nitro scheduled tasks + Vercel Cron (zero config) |
184
184
  | `hooks/` | `defineHook()` | `agent:start`, `plan:commit`, `run:complete`, … |
185
185
  | `subagents/` | `defineAgent()` in `subagents/<name>/agent.ts` | `delegate_subagent` harness tool |
186
186
 
@@ -222,11 +222,9 @@ export PLASM_CONNECT_SUBJECT_TYPE=app # or user + PLASM_CONNECT
222
222
  ### Production state + Workflow
223
223
 
224
224
  ```bash
225
- export PLASM_STATE_BACKEND=kv|postgres|fs
226
- export PLASM_ARCHIVE_BACKEND=vercel|postgres|local
227
- export BLOB_READ_WRITE_TOKEN=...
228
- export KV_REST_API_URL=...
229
- export KV_REST_API_TOKEN=...
225
+ # On Vercel: Blob + Gateway OIDC automatic after `vercel blob create-store` + `plasm-agent link`
226
+ export PLASM_STATE_BACKEND=blob|postgres|fs|kv # blob default on Vercel
227
+ export PLASM_ARCHIVE_BACKEND=vercel|local
230
228
 
231
229
  # Workflow (agent.ts experimental.workflow.world.type)
232
230
  export PLASM_WORKFLOW_WORLD=postgres
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasm_lang/vercel-agent",
3
- "version": "0.3.85",
3
+ "version": "0.3.87",
4
4
  "description": "Catalog-native TypeScript agent framework (Plasm CGS/CML, Vercel AI SDK, Nitro-oriented)",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "@ai-sdk/otel": "^1.0.3",
65
65
  "@opentelemetry/api": "^1.9.0",
66
- "@plasm_lang/engine": "^0.3.85",
66
+ "@plasm_lang/engine": "^0.3.87",
67
67
  "@vercel/blob": "^0.27.3",
68
68
  "@vercel/connect": "^0.2.6",
69
69
  "@vercel/kv": "^3.0.0",
@@ -9,6 +9,10 @@ export class UnimplementedBlobArchiveAdapter implements BlobArchiveAdapter {
9
9
  async get(): Promise<Uint8Array | null> {
10
10
  throw new Error("BlobArchiveAdapter not configured — set PLASM_RUN_ARTIFACTS_URL in prod");
11
11
  }
12
+
13
+ async list(): Promise<string[]> {
14
+ throw new Error("BlobArchiveAdapter not configured — set PLASM_RUN_ARTIFACTS_URL in prod");
15
+ }
12
16
  }
13
17
 
14
18
  /** Stub KV index adapter for production archive listings. */
@@ -1,7 +1,6 @@
1
1
  import { LocalArchiveStore } from "./index.js";
2
2
  import type {
3
3
  BlobArchiveAdapter,
4
- KvArchiveIndexAdapter,
5
4
  PlanArchiveSnapshot,
6
5
  RunSnapshot,
7
6
  TraceDetail,
@@ -13,18 +12,17 @@ function blobKey(kind: "plan" | "run" | "trace", id: string): string {
13
12
  return `plasm/archives/${kind}/${id}.json`;
14
13
  }
15
14
 
16
- function indexKey(kind: "plan" | "run" | "trace", id: string): string {
17
- return `plasm:index:${kind}:${id}`;
15
+ function archivePrefix(kind: "plan" | "run" | "trace"): string {
16
+ return `plasm/archives/${kind}/`;
18
17
  }
19
18
 
20
- /** Durable archive store: local FS cache + Blob bodies + KV/Postgres index. */
19
+ /** Durable archive store: local FS cache + Blob bodies (Eve-aligned, no KV index). */
21
20
  export class ProdArchiveStore {
22
21
  readonly local: LocalArchiveStore;
23
22
 
24
23
  constructor(
25
24
  agentRoot: string,
26
25
  private readonly blob: BlobArchiveAdapter,
27
- private readonly index: KvArchiveIndexAdapter,
28
26
  ) {
29
27
  this.local = LocalArchiveStore.fromAgentRoot(agentRoot);
30
28
  }
@@ -47,6 +45,17 @@ export class ProdArchiveStore {
47
45
  return JSON.parse(Buffer.from(bytes).toString("utf8")) as T;
48
46
  }
49
47
 
48
+ private async listJson<T>(kind: "plan" | "run" | "trace"): Promise<T[]> {
49
+ const paths = await this.blob.list(archivePrefix(kind));
50
+ const items: T[] = [];
51
+ for (const key of paths) {
52
+ if (!key.endsWith(".json")) continue;
53
+ const item = await this.getJson<T>(key);
54
+ if (item) items.push(item);
55
+ }
56
+ return items;
57
+ }
58
+
50
59
  async recordToolEvent(
51
60
  tenantId: string,
52
61
  traceId: string,
@@ -59,59 +68,40 @@ export class ProdArchiveStore {
59
68
 
60
69
  async finalizeTrace(detail: TraceDetail): Promise<void> {
61
70
  await this.local.finalizeTrace(detail);
62
- const key = blobKey("trace", detail.summary.trace_id);
63
- await this.putJson(key, detail);
64
- await this.index.set(indexKey("trace", detail.summary.trace_id), key);
71
+ await this.putJson(blobKey("trace", detail.summary.trace_id), detail);
65
72
  }
66
73
 
67
74
  async writePlanArchive(snapshot: PlanArchiveSnapshot): Promise<void> {
68
75
  await this.local.writePlanArchive(snapshot);
69
- const key = blobKey("plan", snapshot.plan_commit_ref);
70
- await this.putJson(key, snapshot);
71
- await this.index.set(indexKey("plan", snapshot.plan_commit_ref), key);
76
+ await this.putJson(blobKey("plan", snapshot.plan_commit_ref), snapshot);
72
77
  }
73
78
 
74
79
  async writeRunSnapshot(snapshot: RunSnapshot): Promise<void> {
75
80
  await this.local.writeRunSnapshot(snapshot);
76
- const key = blobKey("run", snapshot.run_id);
77
- await this.putJson(key, snapshot);
78
- await this.index.set(indexKey("run", snapshot.run_id), key);
81
+ await this.putJson(blobKey("run", snapshot.run_id), snapshot);
79
82
  }
80
83
 
81
84
  async listTraces(tenantId: string, limit = 50): Promise<TraceSummary[]> {
82
- const keys = await this.index.list("plasm:index:trace:");
83
- const items: TraceSummary[] = [];
84
- for (const idx of keys) {
85
- const blobRef = await this.index.get(idx);
86
- if (!blobRef) continue;
87
- const detail = await this.getJson<TraceDetail>(blobRef);
88
- if (detail?.summary) items.push(detail.summary);
89
- }
85
+ const items = await this.listJson<TraceDetail>("trace");
90
86
  if (items.length) {
91
- items.sort((a, b) => b.started_at_ms - a.started_at_ms);
92
- return items.slice(0, limit);
87
+ const summaries = items
88
+ .map((detail) => detail.summary)
89
+ .filter((summary) => summary.tenant_id === tenantId);
90
+ summaries.sort((a, b) => b.started_at_ms - a.started_at_ms);
91
+ return summaries.slice(0, limit);
93
92
  }
94
93
  return this.local.listTraces(tenantId, limit);
95
94
  }
96
95
 
97
96
  async getTrace(tenantId: string, traceId: string): Promise<TraceDetail | null> {
98
- const blobRef = await this.index.get(indexKey("trace", traceId));
99
- if (blobRef) {
100
- const detail = await this.getJson<TraceDetail>(blobRef);
101
- if (detail) return detail;
102
- }
97
+ void tenantId;
98
+ const detail = await this.getJson<TraceDetail>(blobKey("trace", traceId));
99
+ if (detail) return detail;
103
100
  return this.local.getTrace(tenantId, traceId);
104
101
  }
105
102
 
106
103
  async listPlans(limit = 50): Promise<PlanArchiveSnapshot[]> {
107
- const keys = await this.index.list("plasm:index:plan:");
108
- const items: PlanArchiveSnapshot[] = [];
109
- for (const idx of keys) {
110
- const blobRef = await this.index.get(idx);
111
- if (!blobRef) continue;
112
- const plan = await this.getJson<PlanArchiveSnapshot>(blobRef);
113
- if (plan) items.push(plan);
114
- }
104
+ const items = await this.listJson<PlanArchiveSnapshot>("plan");
115
105
  if (items.length) {
116
106
  items.sort((a, b) => b.archived_at.localeCompare(a.archived_at));
117
107
  return items.slice(0, limit);
@@ -120,14 +110,7 @@ export class ProdArchiveStore {
120
110
  }
121
111
 
122
112
  async listRuns(limit = 50): Promise<RunSnapshot[]> {
123
- const keys = await this.index.list("plasm:index:run:");
124
- const items: RunSnapshot[] = [];
125
- for (const idx of keys) {
126
- const blobRef = await this.index.get(idx);
127
- if (!blobRef) continue;
128
- const run = await this.getJson<RunSnapshot>(blobRef);
129
- if (run) items.push(run);
130
- }
113
+ const items = await this.listJson<RunSnapshot>("run");
131
114
  if (items.length) {
132
115
  items.sort((a, b) => b.archived_at.localeCompare(a.archived_at));
133
116
  return items.slice(0, limit);
@@ -1,10 +1,10 @@
1
1
  import type { AgentWorkflowWorldDefinition } from "../define-agent.js";
2
+ import { isVercelHosted } from "../gateway-model.js";
3
+ import { hasLinkedBlobStore } from "../storage/vercel-blob.js";
2
4
  import { LocalArchiveStore } from "./index.js";
3
- import { PostgresArchiveIndexAdapter } from "./postgres-kv-adapter.js";
4
5
  import { ProdArchiveStore } from "./prod-archive-store.js";
5
6
  import { VercelBlobArchiveAdapter } from "./vercel-blob-adapter.js";
6
- import { VercelKvArchiveIndexAdapter } from "./vercel-kv-adapter.js";
7
- import type { BlobArchiveAdapter, KvArchiveIndexAdapter } from "./types.js";
7
+ import type { BlobArchiveAdapter } from "./types.js";
8
8
 
9
9
  export type ArchiveBackend = "local" | "vercel" | "postgres";
10
10
 
@@ -15,46 +15,28 @@ export function resolveArchiveBackend(
15
15
  if (explicit === "local" || explicit === "vercel" || explicit === "postgres") {
16
16
  return explicit;
17
17
  }
18
- if (
19
- process.env.BLOB_READ_WRITE_TOKEN?.trim() ||
20
- process.env.PLASM_RUN_ARTIFACTS_URL?.trim()
21
- ) {
22
- if (process.env.KV_REST_API_URL?.trim() || process.env.PLASM_KV_REST_API_URL?.trim()) {
23
- return "vercel";
24
- }
18
+ if (isVercelHosted() || hasLinkedBlobStore()) {
19
+ return "vercel";
25
20
  }
26
21
  if (world?.type === "postgres") return "postgres";
27
22
  return "local";
28
23
  }
29
24
 
30
25
  function resolveBlobAdapter(): BlobArchiveAdapter | undefined {
31
- if (process.env.BLOB_READ_WRITE_TOKEN?.trim()) {
26
+ if (hasLinkedBlobStore() || isVercelHosted()) {
32
27
  return new VercelBlobArchiveAdapter();
33
28
  }
34
29
  return undefined;
35
30
  }
36
31
 
37
- function resolveIndexAdapter(
38
- backend: ArchiveBackend,
39
- ): KvArchiveIndexAdapter | undefined {
40
- if (backend === "vercel") {
41
- return new VercelKvArchiveIndexAdapter();
42
- }
43
- if (backend === "postgres") {
44
- return new PostgresArchiveIndexAdapter();
45
- }
46
- return undefined;
47
- }
48
-
49
32
  export function createArchiveStore(
50
33
  agentRoot: string,
51
34
  options?: { backend?: ArchiveBackend; world?: AgentWorkflowWorldDefinition },
52
35
  ): LocalArchiveStore | ProdArchiveStore {
53
36
  const backend = options?.backend ?? resolveArchiveBackend(options?.world);
54
37
  const blob = resolveBlobAdapter();
55
- const index = resolveIndexAdapter(backend);
56
- if (backend !== "local" && blob && index) {
57
- return new ProdArchiveStore(agentRoot, blob, index);
38
+ if (backend === "vercel" && blob) {
39
+ return new ProdArchiveStore(agentRoot, blob);
58
40
  }
59
41
  return LocalArchiveStore.fromAgentRoot(agentRoot);
60
42
  }
@@ -65,6 +65,7 @@ export interface ArchivePaths {
65
65
  export interface BlobArchiveAdapter {
66
66
  put(key: string, body: string | Uint8Array): Promise<void>;
67
67
  get(key: string): Promise<Uint8Array | null>;
68
+ list(prefix: string): Promise<string[]>;
68
69
  }
69
70
 
70
71
  /** Production KV / Postgres index adapter for archive listings. */
@@ -1,4 +1,5 @@
1
1
  import type { BlobArchiveAdapter } from "./types.js";
2
+ import { blobList } from "../storage/vercel-blob.js";
2
3
 
3
4
  export class VercelBlobArchiveAdapter implements BlobArchiveAdapter {
4
5
  async put(key: string, body: string | Uint8Array): Promise<void> {
@@ -18,4 +19,8 @@ export class VercelBlobArchiveAdapter implements BlobArchiveAdapter {
18
19
  if (!response.ok) return null;
19
20
  return new Uint8Array(await response.arrayBuffer());
20
21
  }
22
+
23
+ async list(prefix: string): Promise<string[]> {
24
+ return blobList(prefix);
25
+ }
21
26
  }
@@ -10,28 +10,48 @@ export interface ScheduleHandle {
10
10
  stop(): void;
11
11
  }
12
12
 
13
+ export interface ScheduleTaskManifest {
14
+ scheduledTasks: Record<string, string | string[]>;
15
+ tasks: Array<{ name: string; cron: string }>;
16
+ }
17
+
13
18
  export interface ScheduleCronManifest {
14
19
  crons: Array<{ path: string; schedule: string; name: string }>;
15
20
  }
16
21
 
17
- export function exportScheduleCronManifest(schedules: LoadedSchedule[]): ScheduleCronManifest {
22
+ export function exportScheduleTaskManifest(schedules: LoadedSchedule[]): ScheduleTaskManifest {
23
+ const scheduledTasks: Record<string, string | string[]> = {};
24
+ const tasks: Array<{ name: string; cron: string }> = [];
25
+
26
+ for (const schedule of schedules) {
27
+ const { name, cron } = schedule.definition;
28
+ tasks.push({ name, cron });
29
+ const existing = scheduledTasks[cron];
30
+ if (!existing) {
31
+ scheduledTasks[cron] = name;
32
+ } else if (Array.isArray(existing)) {
33
+ existing.push(name);
34
+ } else {
35
+ scheduledTasks[cron] = [existing, name];
36
+ }
37
+ }
38
+
39
+ return { scheduledTasks, tasks };
40
+ }
41
+
42
+ /** @deprecated Use Nitro scheduled tasks in production. */
43
+ export function exportScheduleCronManifest(schedules: LoadedSchedule[]) {
18
44
  return {
19
45
  crons: schedules.map((schedule) => ({
20
46
  name: schedule.definition.name,
21
- path: `/internal/cron/${schedule.definition.name}`,
47
+ path: `/internal/schedule/${schedule.definition.name}`,
22
48
  schedule: schedule.definition.cron,
23
49
  })),
24
50
  };
25
51
  }
26
52
 
27
- function authorizeCronRequest(req: IncomingMessage): boolean {
28
- const secret = process.env.CRON_SECRET?.trim();
29
- if (!secret) return process.env.NODE_ENV !== "production";
30
- const auth = req.headers.authorization ?? "";
31
- return auth === `Bearer ${secret}`;
32
- }
33
-
34
- export function tryHandleScheduleCronRoute(
53
+ /** Eve-style dev dispatch — manually trigger a schedule by name. */
54
+ export function tryHandleScheduleDevDispatch(
35
55
  req: IncomingMessage,
36
56
  res: ServerResponse,
37
57
  schedules: LoadedSchedule[],
@@ -39,14 +59,8 @@ export function tryHandleScheduleCronRoute(
39
59
  ): boolean {
40
60
  const method = (req.method ?? "GET").toUpperCase();
41
61
  const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
42
- const match = pathname.match(/^\/internal\/cron\/([^/]+)$/);
43
- if (method !== "GET" || !match) return false;
44
-
45
- if (!authorizeCronRequest(req)) {
46
- res.statusCode = 401;
47
- res.end("Unauthorized");
48
- return true;
49
- }
62
+ const match = pathname.match(/^\/internal\/schedule\/([^/]+)$/);
63
+ if (method !== "POST" || !match) return false;
50
64
 
51
65
  const name = match[1]!;
52
66
  const schedule = schedules.find((s) => s.definition.name === name);
@@ -61,7 +75,7 @@ export function tryHandleScheduleCronRoute(
61
75
  .then(() => {
62
76
  res.statusCode = 200;
63
77
  res.setHeader("content-type", "application/json; charset=utf-8");
64
- res.end(JSON.stringify({ ok: true, schedule: name }));
78
+ res.end(JSON.stringify({ ok: true, schedule: name, mode: "dev_dispatch" }));
65
79
  })
66
80
  .catch((err: unknown) => {
67
81
  res.statusCode = 500;
@@ -79,10 +93,10 @@ export function startScheduleTimers(
79
93
  ): ScheduleHandle {
80
94
  const world = resolveWorkflowWorldType(workflow);
81
95
  if (world !== "local") {
82
- const manifest = exportScheduleCronManifest(schedules);
96
+ const manifest = exportScheduleTaskManifest(schedules);
83
97
  console.log(
84
- `[plasm:schedule] prod world=${world} — register Vercel Cron routes:`,
85
- JSON.stringify(manifest.crons, null, 2),
98
+ `[plasm:schedule] prod world=${world} — Nitro scheduled tasks:`,
99
+ JSON.stringify(manifest.scheduledTasks, null, 2),
86
100
  );
87
101
  return { stop: () => {} };
88
102
  }
@@ -0,0 +1,18 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export interface PlasmBuildManifest {
5
+ compiledSlots?: Record<string, string>;
6
+ projectRoot?: string;
7
+ }
8
+
9
+ /** Read `.plasm/discovery/manifest.json` written by `plasm-agent build`. */
10
+ export async function readBuildManifest(agentRoot: string): Promise<PlasmBuildManifest | null> {
11
+ const manifestPath = path.join(agentRoot, ".plasm", "discovery", "manifest.json");
12
+ try {
13
+ const raw = await readFile(manifestPath, "utf8");
14
+ return JSON.parse(raw) as PlasmBuildManifest;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
package/src/cli/build.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { mkdir, writeFile, readFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { readBuildManifest } from "./build-manifest.js";
4
5
  import { compileAuthoredSlots } from "./compile-authored-slots.js";
5
6
  import type { ResolvedAgentProject } from "./project-root.js";
6
7
  import { isNativeEngineAvailable } from "../engine/napi-binding.js";
@@ -79,15 +80,5 @@ export async function runPlasmBuild(project: ResolvedAgentProject): Promise<Plas
79
80
  };
80
81
  }
81
82
 
82
- export async function readBuildManifest(agentRoot: string): Promise<{
83
- compiledSlots?: Record<string, string>;
84
- projectRoot?: string;
85
- } | null> {
86
- const manifestPath = path.join(agentRoot, ".plasm", "discovery", "manifest.json");
87
- try {
88
- const raw = await readFile(manifestPath, "utf8");
89
- return JSON.parse(raw) as { compiledSlots?: Record<string, string>; projectRoot?: string };
90
- } catch {
91
- return null;
92
- }
93
- }
83
+ export { readBuildManifest } from "./build-manifest.js";
84
+ export type { PlasmBuildManifest } from "./build-manifest.js";
@@ -62,6 +62,15 @@ export async function compileAuthoredSlots(
62
62
  format: "esm",
63
63
  outfile: outFile,
64
64
  banner: { js: ESBUILD_BANNER },
65
+ external: [
66
+ "@plasm_lang/vercel-agent",
67
+ "@plasm_lang/vercel-agent/*",
68
+ "@plasm_lang/engine",
69
+ "@vercel/functions",
70
+ "@vercel/blob",
71
+ "workflow",
72
+ "workflow/api",
73
+ ],
65
74
  logLevel: "silent",
66
75
  });
67
76
 
@@ -114,20 +114,24 @@ export async function patchProjectPackageJson(
114
114
  ...pkg.dependencies,
115
115
  "@plasm_lang/vercel-agent": `^${version}`,
116
116
  "@plasm_lang/engine": `^${version}`,
117
+ "@ai-sdk/otel": "^1.0.3",
117
118
  "@vercel/blob": "^0.27.3",
118
119
  "@vercel/functions": "^3.4.3",
119
- "@vercel/kv": "^3.0.0",
120
120
  "@vercel/otel": "^1.5.0",
121
+ ai: "^6.0.0",
122
+ workflow: "^4.5.0",
121
123
  };
122
124
  } else {
123
125
  pkg.dependencies = {
124
126
  ...pkg.dependencies,
125
127
  "@plasm_lang/vercel-agent": `file:${path.resolve(packageRoot)}`,
126
128
  "@plasm_lang/engine": `file:${monorepoEngineRoot(packageRoot)}`,
129
+ "@ai-sdk/otel": "^1.0.3",
127
130
  "@vercel/blob": "^0.27.3",
128
131
  "@vercel/functions": "^3.4.3",
129
- "@vercel/kv": "^3.0.0",
130
132
  "@vercel/otel": "^1.5.0",
133
+ ai: "^6.0.0",
134
+ workflow: "^4.5.0",
131
135
  };
132
136
  }
133
137
 
package/src/cli/init.ts CHANGED
@@ -94,15 +94,11 @@ const ENV_EXAMPLE = `# Vercel AI Gateway
94
94
  # Local / self-host: run \`plasm-agent link\` or set AI_GATEWAY_API_KEY.
95
95
  AI_GATEWAY_API_KEY=
96
96
 
97
- # Vercel Cron auth (production)
97
+ # Optional: manual cron trigger auth (Vercel Cron uses x-vercel-cron by default)
98
98
  CRON_SECRET=
99
99
 
100
- # Durable state on Vercel (optional locally)
101
- KV_REST_API_URL=
102
- KV_REST_API_TOKEN=
100
+ # Optional local overrides (Vercel Blob OIDC is automatic when a store is linked)
103
101
  BLOB_READ_WRITE_TOKEN=
104
- PLASM_STATE_BACKEND=kv
105
- PLASM_ARCHIVE_BACKEND=vercel
106
102
 
107
103
  # Optional overrides
108
104
  PLASM_AGENT_MODEL=anthropic/claude-sonnet-4.6
package/src/index.ts CHANGED
@@ -52,10 +52,11 @@ export type {
52
52
  export { tryHandleChannelRoute, listChannelRoutes } from "./authoring/channel-dispatch.js";
53
53
  export {
54
54
  exportScheduleCronManifest,
55
+ exportScheduleTaskManifest,
55
56
  startScheduleTimers,
56
- tryHandleScheduleCronRoute,
57
+ tryHandleScheduleDevDispatch,
57
58
  } from "./authoring/schedule-manager.js";
58
- export type { ScheduleCronManifest, ScheduleHandle } from "./authoring/schedule-manager.js";
59
+ export type { ScheduleCronManifest, ScheduleHandle, ScheduleTaskManifest } from "./authoring/schedule-manager.js";
59
60
  export {
60
61
  createSubagentRegistry,
61
62
  loadSubagents,
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import type { LoadedProjectSlots } from "../authoring/slot-loader.js";
5
5
  import { listChannelRoutes } from "../authoring/channel-dispatch.js";
6
- import { exportScheduleCronManifest } from "../authoring/schedule-manager.js";
6
+ import { exportScheduleTaskManifest } from "../authoring/schedule-manager.js";
7
7
  import type { AgentDefinition } from "../define-agent.js";
8
8
  import type { ProjectDiscovery } from "../discovery/project-walker.js";
9
9
  import { frameworkPackageVersion } from "../package-version.js";
@@ -78,7 +78,7 @@ export function buildPlasmAgentSummary(options: {
78
78
  })),
79
79
  );
80
80
 
81
- const scheduleManifest = exportScheduleCronManifest(loadedSlots.schedules);
81
+ const scheduleManifest = exportScheduleTaskManifest(loadedSlots.schedules);
82
82
 
83
83
  const diagnostics = [...loadedSlots.diagnostics, ...discovery.diagnostics];
84
84
 
@@ -92,12 +92,12 @@ export function buildPlasmAgentSummary(options: {
92
92
  modelId: definition.model,
93
93
  },
94
94
  instructions,
95
- schedules: scheduleManifest.crons.map((cron) => ({
96
- name: cron.name,
97
- cron: cron.schedule,
95
+ schedules: scheduleManifest.tasks.map((task) => ({
96
+ name: task.name,
97
+ cron: task.cron,
98
98
  logicalPath: rel(
99
- loadedSlots.schedules.find((s) => s.definition.name === cron.name)?.sourcePath ??
100
- path.join(agentRoot, "schedules", `${cron.name}.ts`),
99
+ loadedSlots.schedules.find((s) => s.definition.name === task.name)?.sourcePath ??
100
+ path.join(agentRoot, "schedules", `${task.name}.ts`),
101
101
  ),
102
102
  })),
103
103
  tools: [],
@@ -11,6 +11,9 @@ import { copyVercelFunctionAssets } from "./copy-vercel-function-assets.js";
11
11
  import { isVercelBuildEnvironment, vercelOutputDir } from "./paths.js";
12
12
  import { preparePlasmHost } from "./prepare-host.js";
13
13
  import { writePlasmNitroCatchAllRoute } from "./write-nitro-entry.js";
14
+ import { ensureWorkflowBuilderDirs } from "./ensure-workflow-builder-dirs.js";
15
+ import { writeNitroScheduleTasks } from "./write-nitro-schedule-tasks.js";
16
+ import { writeWorkflowDispatchRoute } from "./write-workflow-dispatch-route.js";
14
17
 
15
18
  export interface PlasmApplicationBuildResult {
16
19
  outputDir: string;
@@ -36,6 +39,26 @@ export async function buildPlasmApplication(options: {
36
39
 
37
40
  await writePlasmNitroCatchAllRoute(projectRoot, compiledSlots);
38
41
 
42
+ const loadedSlotsForTasks = await loadAuthoredSlots({
43
+ discovery,
44
+ agentRoot,
45
+ projectRoot,
46
+ compiledSlots,
47
+ });
48
+ if (loadedSlotsForTasks.schedules.length > 0) {
49
+ await writeNitroScheduleTasks({
50
+ projectRoot,
51
+ agentRoot,
52
+ schedules: loadedSlotsForTasks.schedules,
53
+ compiledSlots,
54
+ });
55
+ }
56
+
57
+ if (host.workflowEnabled) {
58
+ await ensureWorkflowBuilderDirs(projectRoot);
59
+ await writeWorkflowDispatchRoute(projectRoot);
60
+ }
61
+
39
62
  const nitro = await createPlasmNitro(host, false);
40
63
  try {
41
64
  const outputDir = await buildNitroOutput(nitro);
@@ -86,6 +109,26 @@ export async function startPlasmNitroDev(options: {
86
109
  const host = await preparePlasmHost(options);
87
110
  await writePlasmNitroCatchAllRoute(options.projectRoot, options.compiledSlots);
88
111
 
112
+ const loadedSlotsForTasks = await loadAuthoredSlots({
113
+ discovery: options.discovery,
114
+ agentRoot: options.agentRoot,
115
+ projectRoot: options.projectRoot,
116
+ compiledSlots: options.compiledSlots,
117
+ });
118
+ if (loadedSlotsForTasks.schedules.length > 0) {
119
+ await writeNitroScheduleTasks({
120
+ projectRoot: options.projectRoot,
121
+ agentRoot: options.agentRoot,
122
+ schedules: loadedSlotsForTasks.schedules,
123
+ compiledSlots: options.compiledSlots,
124
+ });
125
+ }
126
+
127
+ if (host.workflowEnabled) {
128
+ await ensureWorkflowBuilderDirs(options.projectRoot);
129
+ await writeWorkflowDispatchRoute(options.projectRoot);
130
+ }
131
+
89
132
  const nitro = await createPlasmNitro(host, true);
90
133
  const server = createDevServer(nitro);
91
134