@plasm_lang/vercel-agent 0.3.63

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 (121) hide show
  1. package/README.md +235 -0
  2. package/agent/.plasm/archives/runs/runs/pr2d5c4a99707b4c19b650553d50229a1d600d28e3d98a9c58f18e5026cecc86ca.json +64 -0
  3. package/agent/.plasm/archives/runs/runs/pr2e0c0d8ad443c63c82da7435ee1a002b0e0fa718b640263c0a9d3e6e5944812f.json +64 -0
  4. package/agent/.plasm/archives/runs/runs/pr2faedb8354f40ee6d828e3af07b421fda9ccda973a4f7347fce3639f03a0a869.json +64 -0
  5. package/agent/.plasm/archives/runs/runs/pr586b47c55547b0702c572bce4255558b22dbe5e682d6359169577e0ea75fe98f.json +64 -0
  6. package/agent/.plasm/archives/runs/runs/pr76212356445e3b00fcf256835aaec18bac68576324b90d0be92d47f0b4a862a7.json +56 -0
  7. package/agent/.plasm/archives/runs/runs/pr9ec805d689e00db9270a9539858f2fb7216c24acbfea943d450e37b641149da1.json +64 -0
  8. package/agent/.plasm/archives/runs/runs/prc3c0c4ba2e28fc94ed6d37b6796e277a7997d9cb3184640d14c35c98bc6d136f.json +64 -0
  9. package/agent/.plasm/archives/runs/runs/prf04de32522f2fdcb17818907d91bccce7dcaecbd1259041cc448d447b6993244.json +64 -0
  10. package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/records.ndjson +1 -0
  11. package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/summary.json +13 -0
  12. package/agent/.plasm/discovery/manifest.json +126 -0
  13. package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.json +44 -0
  14. package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.teaching.tsv +23 -0
  15. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.json +151 -0
  16. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.teaching.tsv +131 -0
  17. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.json +44 -0
  18. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.teaching.tsv +23 -0
  19. package/agent/.plasm/stubs/.gitkeep +0 -0
  20. package/agent/.plasm/stubs/execute_tiny.ts +107 -0
  21. package/agent/agent.ts +52 -0
  22. package/agent/catalogs/README.md +15 -0
  23. package/agent/channels/.gitkeep +0 -0
  24. package/agent/channels/execute-tiny-webhook.ts +59 -0
  25. package/agent/channels/health.ts +16 -0
  26. package/agent/hooks/.gitkeep +0 -0
  27. package/agent/hooks/trace-log.ts +10 -0
  28. package/agent/instructions.md +25 -0
  29. package/agent/schedules/.gitkeep +0 -0
  30. package/agent/schedules/ping.ts +13 -0
  31. package/agent/skills/.gitkeep +0 -0
  32. package/agent/skills/plasm-authoring.md +8 -0
  33. package/agent/subagents/.gitkeep +0 -0
  34. package/agent/subagents/tiny/agent.ts +28 -0
  35. package/bin/plasm-agent.mjs +18 -0
  36. package/package.json +77 -0
  37. package/scripts/plasm-node.mjs +19 -0
  38. package/scripts/resolve-ts-extension.mjs +18 -0
  39. package/src/archive/adapters.ts +27 -0
  40. package/src/archive/index.ts +99 -0
  41. package/src/archive/local-run-archive.ts +90 -0
  42. package/src/archive/local-trace-archive.ts +91 -0
  43. package/src/archive/paths.ts +15 -0
  44. package/src/archive/postgres-kv-adapter.ts +72 -0
  45. package/src/archive/prod-archive-store.ts +143 -0
  46. package/src/archive/resolve-backend.ts +60 -0
  47. package/src/archive/run-id.ts +23 -0
  48. package/src/archive/types.ts +75 -0
  49. package/src/archive/vercel-blob-adapter.ts +21 -0
  50. package/src/archive/vercel-kv-adapter.ts +24 -0
  51. package/src/authoring/channel-dispatch.ts +44 -0
  52. package/src/authoring/context.ts +34 -0
  53. package/src/authoring/define-channel.ts +83 -0
  54. package/src/authoring/define-hook.ts +51 -0
  55. package/src/authoring/define-schedule.ts +64 -0
  56. package/src/authoring/define-skill.ts +38 -0
  57. package/src/authoring/hook-runner.ts +18 -0
  58. package/src/authoring/schedule-manager.ts +118 -0
  59. package/src/authoring/slot-loader.ts +253 -0
  60. package/src/authoring/subagent-loader.ts +121 -0
  61. package/src/catalog/loader.ts +71 -0
  62. package/src/cli/build.ts +54 -0
  63. package/src/cli/dev.ts +60 -0
  64. package/src/cli/info.ts +12 -0
  65. package/src/cli/init.ts +372 -0
  66. package/src/cli/link.ts +68 -0
  67. package/src/cli/project-root.ts +57 -0
  68. package/src/define-agent.ts +150 -0
  69. package/src/dev/client/ansi.ts +36 -0
  70. package/src/dev/client/http-session.ts +180 -0
  71. package/src/dev/client/repl.ts +92 -0
  72. package/src/dev/client/slash.ts +119 -0
  73. package/src/dev/dev-session.ts +153 -0
  74. package/src/dev/http.ts +29 -0
  75. package/src/dev/server.ts +147 -0
  76. package/src/dev/session-routes.ts +185 -0
  77. package/src/discovery/project-walker.ts +272 -0
  78. package/src/engine/connect-auth.ts +135 -0
  79. package/src/engine/create-host-transport.ts +7 -0
  80. package/src/engine/fixture-mock-transport.ts +54 -0
  81. package/src/engine/host-transport-bridge.ts +32 -0
  82. package/src/engine/host-transport.ts +84 -0
  83. package/src/engine/napi-binding.ts +265 -0
  84. package/src/evals/define-eval.ts +56 -0
  85. package/src/evals/run-eval.ts +136 -0
  86. package/src/gateway-model.ts +43 -0
  87. package/src/index.ts +296 -0
  88. package/src/instrumentation.ts +56 -0
  89. package/src/load-env.ts +63 -0
  90. package/src/operator/routes.ts +287 -0
  91. package/src/operator/types.ts +63 -0
  92. package/src/operator/ui-shell.ts +134 -0
  93. package/src/project-info.ts +229 -0
  94. package/src/runtime/agent-runtime.ts +469 -0
  95. package/src/runtime/compaction.ts +81 -0
  96. package/src/runtime/logical-session.ts +72 -0
  97. package/src/runtime/plasm-agent.ts +199 -0
  98. package/src/server/plasm-handler.ts +331 -0
  99. package/src/session-state.ts +135 -0
  100. package/src/state/define-state.ts +57 -0
  101. package/src/state/fs-state-adapter.ts +72 -0
  102. package/src/state/kv-state-adapter.ts +62 -0
  103. package/src/state/postgres-state-adapter.ts +116 -0
  104. package/src/stubs/capability-invoke-shape.ts +135 -0
  105. package/src/stubs/catalog-client.ts +170 -0
  106. package/src/stubs/catalog-hash.ts +11 -0
  107. package/src/stubs/catalog-introspection.ts +121 -0
  108. package/src/stubs/cgs-ts-types.ts +164 -0
  109. package/src/stubs/domain-parser.ts +203 -0
  110. package/src/stubs/generator.ts +390 -0
  111. package/src/stubs/input-type-to-ts.ts +233 -0
  112. package/src/stubs/plasm-value-emitter.ts +162 -0
  113. package/src/stubs/program-builder.ts +82 -0
  114. package/src/stubs/stub-symbols.ts +89 -0
  115. package/src/symbol-registry.ts +74 -0
  116. package/src/telemetry/plasm-spans.ts +83 -0
  117. package/src/tools/descriptions.ts +94 -0
  118. package/src/tools/format.ts +29 -0
  119. package/src/tools/harness-tools.ts +65 -0
  120. package/src/tools/plasm-tools.ts +104 -0
  121. package/src/workflow/world-bootstrap.ts +52 -0
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import type { PlasmAgent } from "../runtime/plasm-agent.js";
5
+
6
+ export interface AuthoringContext {
7
+ agentRoot: string;
8
+ getAgent: () => Promise<PlasmAgent>;
9
+ /** Dynamic import of a generated stub module by catalog `entry_id`. */
10
+ importStub: (entryId: string) => Promise<unknown>;
11
+ }
12
+
13
+ export interface CreateAuthoringContextOptions {
14
+ agentRoot: string;
15
+ getAgent: () => Promise<PlasmAgent>;
16
+ importCacheBust?: number;
17
+ }
18
+
19
+ export function createAuthoringContext(
20
+ options: CreateAuthoringContextOptions,
21
+ ): AuthoringContext {
22
+ const agentRoot = path.resolve(options.agentRoot);
23
+ const bust = options.importCacheBust ?? Date.now();
24
+
25
+ return {
26
+ agentRoot,
27
+ getAgent: options.getAgent,
28
+ importStub: async (entryId: string) => {
29
+ const stubPath = path.join(agentRoot, ".plasm", "stubs", `${entryId}.ts`);
30
+ const url = `${pathToFileURL(stubPath).href}?t=${bust}`;
31
+ return import(url);
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,83 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import type { AuthoringContext } from "./context.js";
4
+
5
+ export const PLASM_CHANNEL_KIND = "channel" as const;
6
+
7
+ export type HttpMethod =
8
+ | "GET"
9
+ | "POST"
10
+ | "PUT"
11
+ | "PATCH"
12
+ | "DELETE"
13
+ | "HEAD"
14
+ | "OPTIONS";
15
+
16
+ export type ChannelHandler = (
17
+ req: IncomingMessage,
18
+ res: ServerResponse,
19
+ ctx: AuthoringContext,
20
+ ) => void | Promise<void>;
21
+
22
+ export interface ChannelRoute {
23
+ method: HttpMethod;
24
+ path: string;
25
+ handler: ChannelHandler;
26
+ }
27
+
28
+ export interface ChannelDefinition {
29
+ readonly __plasmSlotKind: typeof PLASM_CHANNEL_KIND;
30
+ name: string;
31
+ routes: ChannelRoute[];
32
+ }
33
+
34
+ export interface DefineChannelInput {
35
+ name: string;
36
+ routes: ChannelRoute[];
37
+ }
38
+
39
+ const RESERVED_PATH_PREFIXES = ["/plasm/", "/operator"] as const;
40
+
41
+ export function validateChannelRoute(route: ChannelRoute): string | null {
42
+ if (!route.path.startsWith("/")) {
43
+ return `channel route path must start with '/': ${route.path}`;
44
+ }
45
+ for (const prefix of RESERVED_PATH_PREFIXES) {
46
+ if (route.path === prefix.replace(/\/$/, "") || route.path.startsWith(prefix)) {
47
+ return `channel route conflicts with reserved prefix ${prefix}: ${route.path}`;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /** Nitro-shaped HTTP channel (deterministic ingress; call stubs, not plasm_context). */
54
+ export function defineChannel(input: DefineChannelInput): ChannelDefinition {
55
+ if (!input.name?.trim()) {
56
+ throw new Error("defineChannel: name is required");
57
+ }
58
+ if (!input.routes?.length) {
59
+ throw new Error("defineChannel: at least one route is required");
60
+ }
61
+ const routes = input.routes.map((route) => ({
62
+ ...route,
63
+ method: route.method.toUpperCase() as HttpMethod,
64
+ path: route.path.trim(),
65
+ }));
66
+ for (const route of routes) {
67
+ const err = validateChannelRoute(route);
68
+ if (err) throw new Error(`defineChannel(${input.name}): ${err}`);
69
+ }
70
+ return Object.freeze({
71
+ __plasmSlotKind: PLASM_CHANNEL_KIND,
72
+ name: input.name.trim(),
73
+ routes,
74
+ });
75
+ }
76
+
77
+ export function isChannelDefinition(value: unknown): value is ChannelDefinition {
78
+ return (
79
+ typeof value === "object" &&
80
+ value !== null &&
81
+ (value as ChannelDefinition).__plasmSlotKind === PLASM_CHANNEL_KIND
82
+ );
83
+ }
@@ -0,0 +1,51 @@
1
+ import type { AuthoringContext } from "./context.js";
2
+
3
+ export const PLASM_HOOK_KIND = "hook" as const;
4
+
5
+ export type HookEvent = "agent:start" | "agent:step" | "plan:commit" | "run:complete";
6
+
7
+ export type HookHandler = (
8
+ ctx: AuthoringContext,
9
+ detail?: Record<string, unknown>,
10
+ ) => void | Promise<void>;
11
+
12
+ export interface HookDefinition {
13
+ readonly __plasmSlotKind: typeof PLASM_HOOK_KIND;
14
+ name: string;
15
+ on: HookEvent | HookEvent[];
16
+ handler: HookHandler;
17
+ }
18
+
19
+ export interface DefineHookInput {
20
+ name: string;
21
+ on: HookEvent | HookEvent[];
22
+ handler: HookHandler;
23
+ }
24
+
25
+ /** Lifecycle hook (trace/archive side effects). */
26
+ export function defineHook(input: DefineHookInput): HookDefinition {
27
+ if (!input.name?.trim()) {
28
+ throw new Error("defineHook: name is required");
29
+ }
30
+ const events = Array.isArray(input.on) ? input.on : [input.on];
31
+ if (!events.length) {
32
+ throw new Error("defineHook: on requires at least one event");
33
+ }
34
+ if (typeof input.handler !== "function") {
35
+ throw new Error("defineHook: handler must be a function");
36
+ }
37
+ return Object.freeze({
38
+ __plasmSlotKind: PLASM_HOOK_KIND,
39
+ name: input.name.trim(),
40
+ on: input.on,
41
+ handler: input.handler,
42
+ });
43
+ }
44
+
45
+ export function isHookDefinition(value: unknown): value is HookDefinition {
46
+ return (
47
+ typeof value === "object" &&
48
+ value !== null &&
49
+ (value as HookDefinition).__plasmSlotKind === PLASM_HOOK_KIND
50
+ );
51
+ }
@@ -0,0 +1,64 @@
1
+ import type { AuthoringContext } from "./context.js";
2
+
3
+ export const PLASM_SCHEDULE_KIND = "schedule" as const;
4
+
5
+ export type ScheduleHandler = (ctx: AuthoringContext) => void | Promise<void>;
6
+
7
+ export interface ScheduleDefinition {
8
+ readonly __plasmSlotKind: typeof PLASM_SCHEDULE_KIND;
9
+ name: string;
10
+ cron: string;
11
+ handler: ScheduleHandler;
12
+ }
13
+
14
+ export interface DefineScheduleInput {
15
+ name: string;
16
+ cron: string;
17
+ handler: ScheduleHandler;
18
+ }
19
+
20
+ /** Cron schedule (Workflow-friendly; dev server uses a minimal every-N-minutes timer). */
21
+ export function defineSchedule(input: DefineScheduleInput): ScheduleDefinition {
22
+ if (!input.name?.trim()) {
23
+ throw new Error("defineSchedule: name is required");
24
+ }
25
+ if (!input.cron?.trim()) {
26
+ throw new Error("defineSchedule: cron is required");
27
+ }
28
+ if (typeof input.handler !== "function") {
29
+ throw new Error("defineSchedule: handler must be a function");
30
+ }
31
+ return Object.freeze({
32
+ __plasmSlotKind: PLASM_SCHEDULE_KIND,
33
+ name: input.name.trim(),
34
+ cron: input.cron.trim(),
35
+ handler: input.handler,
36
+ });
37
+ }
38
+
39
+ export function isScheduleDefinition(value: unknown): value is ScheduleDefinition {
40
+ return (
41
+ typeof value === "object" &&
42
+ value !== null &&
43
+ (value as ScheduleDefinition).__plasmSlotKind === PLASM_SCHEDULE_KIND
44
+ );
45
+ }
46
+
47
+ /** Parse every-N-minutes or every-N-hours cron into interval ms; returns null when unsupported. */
48
+ export function cronIntervalMs(cron: string): number | null {
49
+ const everyMinutes = cron.match(/^\*\/(\d+)\s+\*\s+\*\s+\*\s+\*$/);
50
+ if (everyMinutes) {
51
+ const minutes = Number(everyMinutes[1]);
52
+ if (Number.isFinite(minutes) && minutes > 0) {
53
+ return minutes * 60_000;
54
+ }
55
+ }
56
+ const everyHours = cron.match(/^0\s+\*\/(\d+)\s+\*\s+\*\s+\*$/);
57
+ if (everyHours) {
58
+ const hours = Number(everyHours[1]);
59
+ if (Number.isFinite(hours) && hours > 0) {
60
+ return hours * 3_600_000;
61
+ }
62
+ }
63
+ return null;
64
+ }
@@ -0,0 +1,38 @@
1
+ export const PLASM_SKILL_KIND = "skill" as const;
2
+
3
+ export interface SkillDefinition {
4
+ readonly __plasmSlotKind: typeof PLASM_SKILL_KIND;
5
+ name: string;
6
+ description?: string;
7
+ body: string;
8
+ }
9
+
10
+ export interface DefineSkillInput {
11
+ name: string;
12
+ description?: string;
13
+ body: string;
14
+ }
15
+
16
+ /** Eve-shaped skill definition (markdown body for progressive disclosure). */
17
+ export function defineSkill(input: DefineSkillInput): SkillDefinition {
18
+ if (!input.name?.trim()) {
19
+ throw new Error("defineSkill: name is required");
20
+ }
21
+ if (!input.body?.trim()) {
22
+ throw new Error("defineSkill: body is required");
23
+ }
24
+ return Object.freeze({
25
+ __plasmSlotKind: PLASM_SKILL_KIND,
26
+ name: input.name.trim(),
27
+ description: input.description?.trim(),
28
+ body: input.body,
29
+ });
30
+ }
31
+
32
+ export function isSkillDefinition(value: unknown): value is SkillDefinition {
33
+ return (
34
+ typeof value === "object" &&
35
+ value !== null &&
36
+ (value as SkillDefinition).__plasmSlotKind === PLASM_SKILL_KIND
37
+ );
38
+ }
@@ -0,0 +1,18 @@
1
+ import type { AuthoringContext } from "./context.js";
2
+ import type { HookDefinition, HookEvent } from "./define-hook.js";
3
+
4
+ export class HookRunner {
5
+ constructor(private readonly hooks: HookDefinition[]) {}
6
+
7
+ async emit(
8
+ event: HookEvent,
9
+ ctx: AuthoringContext,
10
+ detail?: Record<string, unknown>,
11
+ ): Promise<void> {
12
+ for (const hook of this.hooks) {
13
+ const events = Array.isArray(hook.on) ? hook.on : [hook.on];
14
+ if (!events.includes(event)) continue;
15
+ await hook.handler(ctx, detail);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,118 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import type { AuthoringContext } from "./context.js";
4
+ import { cronIntervalMs } from "./define-schedule.js";
5
+ import type { LoadedSchedule } from "./slot-loader.js";
6
+ import { resolveWorkflowWorldType } from "../workflow/world-bootstrap.js";
7
+ import type { AgentWorkflowDefinition } from "../define-agent.js";
8
+
9
+ export interface ScheduleHandle {
10
+ stop(): void;
11
+ }
12
+
13
+ export interface ScheduleCronManifest {
14
+ crons: Array<{ path: string; schedule: string; name: string }>;
15
+ }
16
+
17
+ export function exportScheduleCronManifest(schedules: LoadedSchedule[]): ScheduleCronManifest {
18
+ return {
19
+ crons: schedules.map((schedule) => ({
20
+ name: schedule.definition.name,
21
+ path: `/internal/cron/${schedule.definition.name}`,
22
+ schedule: schedule.definition.cron,
23
+ })),
24
+ };
25
+ }
26
+
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(
35
+ req: IncomingMessage,
36
+ res: ServerResponse,
37
+ schedules: LoadedSchedule[],
38
+ ctx: AuthoringContext,
39
+ ): boolean {
40
+ const method = (req.method ?? "GET").toUpperCase();
41
+ 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
+ }
50
+
51
+ const name = match[1]!;
52
+ const schedule = schedules.find((s) => s.definition.name === name);
53
+ if (!schedule) {
54
+ res.statusCode = 404;
55
+ res.setHeader("content-type", "application/json; charset=utf-8");
56
+ res.end(JSON.stringify({ error: "schedule_not_found", name }));
57
+ return true;
58
+ }
59
+
60
+ void Promise.resolve(schedule.definition.handler(ctx))
61
+ .then(() => {
62
+ res.statusCode = 200;
63
+ res.setHeader("content-type", "application/json; charset=utf-8");
64
+ res.end(JSON.stringify({ ok: true, schedule: name }));
65
+ })
66
+ .catch((err: unknown) => {
67
+ res.statusCode = 500;
68
+ res.setHeader("content-type", "application/json; charset=utf-8");
69
+ res.end(JSON.stringify({ error: "schedule_failed", message: String(err) }));
70
+ });
71
+
72
+ return true;
73
+ }
74
+
75
+ export function startScheduleTimers(
76
+ schedules: LoadedSchedule[],
77
+ ctx: AuthoringContext,
78
+ workflow?: AgentWorkflowDefinition,
79
+ ): ScheduleHandle {
80
+ const world = resolveWorkflowWorldType(workflow);
81
+ if (world !== "local") {
82
+ const manifest = exportScheduleCronManifest(schedules);
83
+ console.log(
84
+ `[plasm:schedule] prod world=${world} — register Vercel Cron routes:`,
85
+ JSON.stringify(manifest.crons, null, 2),
86
+ );
87
+ return { stop: () => {} };
88
+ }
89
+
90
+ const timers: ReturnType<typeof setInterval>[] = [];
91
+
92
+ for (const schedule of schedules) {
93
+ const intervalMs = cronIntervalMs(schedule.definition.cron);
94
+ if (!intervalMs) {
95
+ console.warn(
96
+ `[plasm:dev] schedule ${schedule.definition.name}: unsupported cron "${schedule.definition.cron}" (use */N * * * *)`,
97
+ );
98
+ continue;
99
+ }
100
+
101
+ const run = () => {
102
+ void Promise.resolve(schedule.definition.handler(ctx)).catch((err: unknown) => {
103
+ console.error(`[plasm:dev] schedule ${schedule.definition.name} failed:`, err);
104
+ });
105
+ };
106
+
107
+ timers.push(setInterval(run, intervalMs));
108
+ console.log(
109
+ `[plasm:dev] schedule ${schedule.definition.name} every ${intervalMs / 60_000}m (${schedule.definition.cron})`,
110
+ );
111
+ }
112
+
113
+ return {
114
+ stop: () => {
115
+ for (const timer of timers) clearInterval(timer);
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,253 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import type { ProjectDiscovery, DiscoveryDiagnostic } from "../discovery/project-walker.js";
6
+ import type { ChannelDefinition } from "./define-channel.js";
7
+ import { isChannelDefinition } from "./define-channel.js";
8
+ import type { HookDefinition } from "./define-hook.js";
9
+ import { isHookDefinition } from "./define-hook.js";
10
+ import type { ScheduleDefinition } from "./define-schedule.js";
11
+ import { isScheduleDefinition } from "./define-schedule.js";
12
+ import type { SkillDefinition } from "./define-skill.js";
13
+ import { isSkillDefinition } from "./define-skill.js";
14
+ import { HookRunner } from "./hook-runner.js";
15
+
16
+ export interface LoadedChannel {
17
+ sourcePath: string;
18
+ definition: ChannelDefinition;
19
+ }
20
+
21
+ export interface LoadedSchedule {
22
+ sourcePath: string;
23
+ definition: ScheduleDefinition;
24
+ }
25
+
26
+ export interface LoadedHook {
27
+ sourcePath: string;
28
+ definition: HookDefinition;
29
+ }
30
+
31
+ export interface LoadedSkill {
32
+ sourcePath: string;
33
+ definition: SkillDefinition;
34
+ }
35
+
36
+ export interface LoadedProjectSlots {
37
+ channels: LoadedChannel[];
38
+ schedules: LoadedSchedule[];
39
+ hooks: LoadedHook[];
40
+ skills: LoadedSkill[];
41
+ hookRunner: HookRunner;
42
+ diagnostics: DiscoveryDiagnostic[];
43
+ }
44
+
45
+ export interface LoadAuthoredSlotsOptions {
46
+ discovery: ProjectDiscovery;
47
+ importCacheBust?: number;
48
+ }
49
+
50
+ type SlotName = "channels" | "schedules" | "hooks" | "skills";
51
+
52
+ function slotDiagnostic(
53
+ slot: SlotName,
54
+ filePath: string,
55
+ message: string,
56
+ level: DiscoveryDiagnostic["level"] = "error",
57
+ ): DiscoveryDiagnostic {
58
+ return { level, slot, path: filePath, message };
59
+ }
60
+
61
+ async function importSlotModule(filePath: string, cacheBust: number): Promise<unknown> {
62
+ const url = `${pathToFileURL(filePath).href}?t=${cacheBust}`;
63
+ const mod = await import(url);
64
+ return mod.default ?? mod;
65
+ }
66
+
67
+ function classifySlotExport(
68
+ exported: unknown,
69
+ ): "channel" | "schedule" | "hook" | "skill" | null {
70
+ if (isChannelDefinition(exported)) return "channel";
71
+ if (isScheduleDefinition(exported)) return "schedule";
72
+ if (isHookDefinition(exported)) return "hook";
73
+ if (isSkillDefinition(exported)) return "skill";
74
+ return null;
75
+ }
76
+
77
+ const EXPECTED_KIND: Record<Exclude<SlotName, "skills">, "channel" | "schedule" | "hook"> = {
78
+ channels: "channel",
79
+ schedules: "schedule",
80
+ hooks: "hook",
81
+ };
82
+
83
+ async function loadTypescriptSlot<T extends ChannelDefinition | ScheduleDefinition | HookDefinition | SkillDefinition>(
84
+ filePath: string,
85
+ slot: SlotName,
86
+ expected: "channel" | "schedule" | "hook" | "skill",
87
+ cacheBust: number,
88
+ ): Promise<{ definition: T } | { diagnostic: DiscoveryDiagnostic }> {
89
+ try {
90
+ const exported = await importSlotModule(filePath, cacheBust);
91
+ const actual = classifySlotExport(exported);
92
+ if (actual !== expected) {
93
+ const helper =
94
+ expected === "channel"
95
+ ? "defineChannel"
96
+ : expected === "schedule"
97
+ ? "defineSchedule"
98
+ : expected === "hook"
99
+ ? "defineHook"
100
+ : "defineSkill";
101
+ return {
102
+ diagnostic: slotDiagnostic(
103
+ slot,
104
+ filePath,
105
+ `default export must be ${helper}() result`,
106
+ ),
107
+ };
108
+ }
109
+ return { definition: exported as T };
110
+ } catch (err) {
111
+ return {
112
+ diagnostic: slotDiagnostic(slot, filePath, `failed to import: ${String(err)}`),
113
+ };
114
+ }
115
+ }
116
+
117
+ async function loadMarkdownSkill(filePath: string, name: string): Promise<LoadedSkill> {
118
+ const body = await readFile(filePath, "utf8");
119
+ const firstLine = body.split("\n").find((line) => line.trim())?.trim() ?? "";
120
+ const description = firstLine.startsWith("#")
121
+ ? firstLine.replace(/^#+\s*/, "").trim()
122
+ : undefined;
123
+ return {
124
+ sourcePath: filePath,
125
+ definition: {
126
+ __plasmSlotKind: "skill",
127
+ name,
128
+ description,
129
+ body,
130
+ },
131
+ };
132
+ }
133
+
134
+ /** Import authored slots from discovery metadata. */
135
+ export async function loadAuthoredSlots(
136
+ options: LoadAuthoredSlotsOptions,
137
+ ): Promise<LoadedProjectSlots> {
138
+ const { discovery } = options;
139
+ const cacheBust = options.importCacheBust ?? Date.now();
140
+ const diagnostics: DiscoveryDiagnostic[] = [...discovery.diagnostics];
141
+
142
+ const channels: LoadedChannel[] = [];
143
+ for (const file of discovery.channels) {
144
+ if (file.kind !== "typescript") continue;
145
+ const result = await loadTypescriptSlot<ChannelDefinition>(
146
+ file.path,
147
+ "channels",
148
+ EXPECTED_KIND.channels,
149
+ cacheBust,
150
+ );
151
+ if ("diagnostic" in result) {
152
+ diagnostics.push(result.diagnostic);
153
+ continue;
154
+ }
155
+ channels.push({ sourcePath: file.path, definition: result.definition });
156
+ }
157
+
158
+ const schedules: LoadedSchedule[] = [];
159
+ for (const file of discovery.schedules) {
160
+ if (file.kind !== "typescript") continue;
161
+ const result = await loadTypescriptSlot<ScheduleDefinition>(
162
+ file.path,
163
+ "schedules",
164
+ EXPECTED_KIND.schedules,
165
+ cacheBust,
166
+ );
167
+ if ("diagnostic" in result) {
168
+ diagnostics.push(result.diagnostic);
169
+ continue;
170
+ }
171
+ schedules.push({ sourcePath: file.path, definition: result.definition });
172
+ }
173
+
174
+ const hooks: LoadedHook[] = [];
175
+ for (const file of discovery.hooks) {
176
+ if (file.kind !== "typescript") continue;
177
+ const result = await loadTypescriptSlot<HookDefinition>(
178
+ file.path,
179
+ "hooks",
180
+ EXPECTED_KIND.hooks,
181
+ cacheBust,
182
+ );
183
+ if ("diagnostic" in result) {
184
+ diagnostics.push(result.diagnostic);
185
+ continue;
186
+ }
187
+ hooks.push({ sourcePath: file.path, definition: result.definition });
188
+ }
189
+
190
+ const skills: LoadedSkill[] = [];
191
+ for (const file of discovery.skills) {
192
+ if (file.kind === "markdown") {
193
+ skills.push(await loadMarkdownSkill(file.path, file.name));
194
+ continue;
195
+ }
196
+ const result = await loadTypescriptSlot<SkillDefinition>(
197
+ file.path,
198
+ "skills",
199
+ "skill",
200
+ cacheBust,
201
+ );
202
+ if ("diagnostic" in result) {
203
+ diagnostics.push(result.diagnostic);
204
+ continue;
205
+ }
206
+ skills.push({ sourcePath: file.path, definition: result.definition });
207
+ }
208
+
209
+ return {
210
+ channels,
211
+ schedules,
212
+ hooks,
213
+ skills,
214
+ hookRunner: new HookRunner(hooks.map((h) => h.definition)),
215
+ diagnostics,
216
+ };
217
+ }
218
+
219
+ export interface LoadedSlotsSummary {
220
+ skills: Array<{ name: string; path: string; kind: "markdown" | "typescript" }>;
221
+ channels: Array<{ name: string; path: string; routes: Array<{ method: string; path: string }> }>;
222
+ schedules: Array<{ name: string; path: string; cron: string }>;
223
+ hooks: Array<{ name: string; path: string; on: string | string[] }>;
224
+ }
225
+
226
+ export function summarizeLoadedSlots(
227
+ slots: LoadedProjectSlots,
228
+ agentRoot: string,
229
+ ): LoadedSlotsSummary {
230
+ const rel = (p: string) => path.relative(agentRoot, p);
231
+ return {
232
+ skills: slots.skills.map((s) => ({
233
+ name: s.definition.name,
234
+ path: rel(s.sourcePath),
235
+ kind: s.sourcePath.endsWith(".md") ? "markdown" : "typescript",
236
+ })),
237
+ channels: slots.channels.map((c) => ({
238
+ name: c.definition.name,
239
+ path: rel(c.sourcePath),
240
+ routes: c.definition.routes.map((r) => ({ method: r.method, path: r.path })),
241
+ })),
242
+ schedules: slots.schedules.map((s) => ({
243
+ name: s.definition.name,
244
+ path: rel(s.sourcePath),
245
+ cron: s.definition.cron,
246
+ })),
247
+ hooks: slots.hooks.map((h) => ({
248
+ name: h.definition.name,
249
+ path: rel(h.sourcePath),
250
+ on: h.definition.on,
251
+ })),
252
+ };
253
+ }