@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,199 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { generateText, stepCountIs, type LanguageModel, type ModelMessage, type ToolSet } from "ai";
5
+
6
+ import type {
7
+ AgentBuildConfig,
8
+ AgentCompactionConfig,
9
+ AgentExperimentalConfig,
10
+ AgentModelOptions,
11
+ } from "../define-agent.js";
12
+ import type { AuthoringContext } from "../authoring/context.js";
13
+ import type { HookRunner } from "../authoring/hook-runner.js";
14
+ import type { SkillDefinition } from "../authoring/define-skill.js";
15
+ import type { SubagentRegistry } from "../authoring/subagent-loader.js";
16
+ import { resolveGatewayModel } from "../gateway-model.js";
17
+ import { createAgentTelemetry } from "../instrumentation.js";
18
+ import { maybeCompactMessages } from "../runtime/compaction.js";
19
+ import { AgentRuntime, type AgentRuntimeConfig } from "../runtime/agent-runtime.js";
20
+ import { createHarnessTools, renderSkillIndex } from "../tools/harness-tools.js";
21
+ import { createPlasmTools } from "../tools/plasm-tools.js";
22
+
23
+ export interface PlasmAgentConfig extends AgentRuntimeConfig {
24
+ /** AI Gateway model slug, e.g. `anthropic/claude-sonnet-4.6`. */
25
+ model: string | LanguageModel;
26
+ instructionsPath?: string;
27
+ maxSteps?: number;
28
+ telemetry?: boolean;
29
+ compaction?: AgentCompactionConfig;
30
+ modelOptions?: AgentModelOptions;
31
+ build?: AgentBuildConfig;
32
+ experimental?: AgentExperimentalConfig;
33
+ loadedSkills?: SkillDefinition[];
34
+ hookRunner?: HookRunner;
35
+ subagentRegistry?: SubagentRegistry;
36
+ getAuthoringContext?: () => AuthoringContext;
37
+ }
38
+
39
+ export interface AgentStepEvent {
40
+ toolCalls?: Array<{ toolName: string }>;
41
+ text?: string;
42
+ finishReason?: string;
43
+ }
44
+
45
+ export interface AgentGenerateOptions {
46
+ messages?: ModelMessage[];
47
+ resetConversation?: boolean;
48
+ onStepFinish?: (step: AgentStepEvent) => void | Promise<void>;
49
+ }
50
+
51
+ export interface AgentTurnResult {
52
+ text: string;
53
+ steps: unknown[];
54
+ usage: Awaited<ReturnType<typeof generateText>>["usage"];
55
+ }
56
+
57
+ export class PlasmAgent {
58
+ readonly runtime: AgentRuntime;
59
+ private readonly model: string | LanguageModel;
60
+ private readonly instructionsPath: string;
61
+ private readonly maxSteps: number;
62
+ private readonly modelOptions?: AgentModelOptions;
63
+ private readonly telemetryEnabled: boolean;
64
+ private readonly loadedSkills: SkillDefinition[];
65
+ private readonly skillsMode: false | "index" | "inline";
66
+ private readonly compaction?: AgentCompactionConfig;
67
+ private readonly hookRunner?: HookRunner;
68
+ private readonly subagentRegistry?: SubagentRegistry;
69
+ private readonly getAuthoringContext?: () => AuthoringContext;
70
+ private conversation: ModelMessage[] = [];
71
+
72
+ constructor(config: PlasmAgentConfig) {
73
+ this.runtime = new AgentRuntime(config);
74
+ this.model = config.model;
75
+ this.modelOptions = config.modelOptions;
76
+ this.instructionsPath =
77
+ config.instructionsPath ?? path.join(config.agentRoot, "instructions.md");
78
+ this.maxSteps = config.maxSteps ?? 20;
79
+ this.telemetryEnabled = config.telemetry ?? true;
80
+ this.loadedSkills = config.loadedSkills ?? [];
81
+ this.compaction = config.compaction;
82
+ const skillsFlag = config.experimental?.skills;
83
+ if (this.loadedSkills.length === 0 || skillsFlag === false) {
84
+ this.skillsMode = false;
85
+ } else if (skillsFlag === "inline") {
86
+ this.skillsMode = "inline";
87
+ } else {
88
+ this.skillsMode = "index";
89
+ }
90
+ this.hookRunner = config.hookRunner;
91
+ this.subagentRegistry = config.subagentRegistry;
92
+ this.getAuthoringContext = config.getAuthoringContext;
93
+ }
94
+
95
+ async bootstrap(): Promise<void> {
96
+ await this.runtime.bootstrap();
97
+ }
98
+
99
+ async loadInstructions(): Promise<string> {
100
+ let base: string;
101
+ try {
102
+ base = (await readFile(this.instructionsPath, "utf8")).trim();
103
+ } catch {
104
+ base = [
105
+ "You are a catalog-native Plasm agent.",
106
+ "Use discover_capabilities → plasm_context → plasm → plasm_run.",
107
+ "Keep one stable intent per user goal.",
108
+ ].join("\n");
109
+ }
110
+
111
+ if (this.skillsMode === "inline") {
112
+ const skillBlock = this.loadedSkills
113
+ .map((skill) => `## Skill: ${skill.name}\n${skill.body.trim()}`)
114
+ .join("\n\n");
115
+ return `${base}\n\n# Skills\n\n${skillBlock}`;
116
+ }
117
+
118
+ if (this.skillsMode === "index") {
119
+ return `${base}\n\n${renderSkillIndex(this.loadedSkills)}`;
120
+ }
121
+
122
+ return base;
123
+ }
124
+
125
+ async generate(
126
+ prompt: string,
127
+ options: AgentGenerateOptions = {},
128
+ ): Promise<AgentTurnResult> {
129
+ if (this.hookRunner && this.getAuthoringContext) {
130
+ await this.hookRunner.emit("agent:start", this.getAuthoringContext(), { prompt });
131
+ }
132
+
133
+ const system = await this.loadInstructions();
134
+ const plasmTools = createPlasmTools(this.runtime);
135
+ const harnessTools = createHarnessTools({
136
+ skills: this.skillsMode === "index" ? this.loadedSkills : undefined,
137
+ subagents: this.subagentRegistry,
138
+ });
139
+ const tools = {
140
+ ...plasmTools,
141
+ ...harnessTools,
142
+ } as ToolSet;
143
+
144
+ const telemetry = this.telemetryEnabled
145
+ ? createAgentTelemetry({ serviceName: "plasm-agent" })
146
+ : { isEnabled: false };
147
+ const model = resolveGatewayModel(this.model, this.modelOptions);
148
+
149
+ const externalMessages = options.messages !== undefined;
150
+ let messages: ModelMessage[];
151
+ if (externalMessages) {
152
+ messages = options.messages ?? [];
153
+ } else if (options.resetConversation) {
154
+ this.conversation = [{ role: "user", content: prompt }];
155
+ messages = this.conversation;
156
+ } else {
157
+ this.conversation.push({ role: "user", content: prompt });
158
+ messages = this.conversation;
159
+ }
160
+
161
+ messages = await maybeCompactMessages(messages, this.compaction, this.model);
162
+
163
+ const generation = {
164
+ model,
165
+ system,
166
+ tools,
167
+ stopWhen: stepCountIs(this.maxSteps),
168
+ experimental_telemetry: telemetry,
169
+ onStepFinish: async (step: AgentStepEvent) => {
170
+ await options.onStepFinish?.(step);
171
+ if (!this.hookRunner || !this.getAuthoringContext) return;
172
+ const toolsUsed = (step.toolCalls ?? []).map((call) => call.toolName);
173
+ await this.hookRunner.emit("agent:step", this.getAuthoringContext(), { toolsUsed });
174
+ },
175
+ ...(this.modelOptions?.temperature !== undefined
176
+ ? { temperature: this.modelOptions.temperature }
177
+ : {}),
178
+ ...(this.modelOptions?.maxOutputTokens !== undefined
179
+ ? { maxOutputTokens: this.modelOptions.maxOutputTokens }
180
+ : {}),
181
+ ...(this.modelOptions?.topP !== undefined ? { topP: this.modelOptions.topP } : {}),
182
+ ...(this.modelOptions?.topK !== undefined ? { topK: this.modelOptions.topK } : {}),
183
+ };
184
+
185
+ const result = await generateText({
186
+ ...generation,
187
+ messages,
188
+ });
189
+
190
+ if (!externalMessages) {
191
+ this.conversation.push({ role: "assistant", content: result.text });
192
+ }
193
+ return {
194
+ text: result.text,
195
+ steps: result.steps,
196
+ usage: result.usage,
197
+ };
198
+ }
199
+ }
@@ -0,0 +1,331 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import path from "node:path";
3
+
4
+ import type { AgentDefinition } from "../define-agent.js";
5
+ import {
6
+ createAgentFromDefinition,
7
+ resolveAgentDefinition,
8
+ } from "../define-agent.js";
9
+ import { createAuthoringContext, type AuthoringContext } from "../authoring/context.js";
10
+ import { tryHandleChannelRoute } from "../authoring/channel-dispatch.js";
11
+ import {
12
+ loadAuthoredSlots,
13
+ type LoadedProjectSlots,
14
+ } from "../authoring/slot-loader.js";
15
+ import {
16
+ createSubagentRegistry,
17
+ loadSubagents,
18
+ summarizeSubagents,
19
+ type SubagentRegistry,
20
+ } from "../authoring/subagent-loader.js";
21
+ import {
22
+ startScheduleTimers,
23
+ tryHandleScheduleCronRoute,
24
+ type ScheduleHandle,
25
+ } from "../authoring/schedule-manager.js";
26
+ import { walkAgentProject, type ProjectDiscovery } from "../discovery/project-walker.js";
27
+ import { collectProjectInfo } from "../project-info.js";
28
+ import type { PlasmAgent } from "../runtime/plasm-agent.js";
29
+ import { DevSessionStore } from "../dev/dev-session.js";
30
+ import { sendJson } from "../dev/http.js";
31
+ import { tryHandleSessionRoutes } from "../dev/session-routes.js";
32
+ import { nitroOperatorHandler } from "../operator/routes.js";
33
+ import { renderOperatorShell } from "../operator/ui-shell.js";
34
+
35
+ export type PlasmAppMode = "dev" | "prod";
36
+
37
+ export interface PlasmAppOptions {
38
+ agentRoot: string;
39
+ definition: AgentDefinition;
40
+ mode?: PlasmAppMode;
41
+ tenantScope?: string;
42
+ maxSteps?: number;
43
+ telemetry?: boolean;
44
+ /** Session SSE routes; default true in dev, false in prod. */
45
+ sessions?: boolean;
46
+ }
47
+
48
+ export interface PlasmApp {
49
+ agentRoot: string;
50
+ projectRoot: string;
51
+ definition: AgentDefinition;
52
+ mode: PlasmAppMode;
53
+ sessionsEnabled: boolean;
54
+ reload(): Promise<void>;
55
+ getAgent(): Promise<PlasmAgent>;
56
+ getDiscovery(): Promise<ProjectDiscovery>;
57
+ getLoadedSlots(): Promise<LoadedProjectSlots | undefined>;
58
+ getSubagents(): Promise<ReturnType<typeof summarizeSubagents>>;
59
+ getAuthoringContext(): AuthoringContext;
60
+ sessionStore: DevSessionStore | undefined;
61
+ handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
62
+ handleOperatorRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
63
+ }
64
+
65
+ export type VercelHandler = (
66
+ req: IncomingMessage,
67
+ res: ServerResponse,
68
+ ) => void | Promise<void>;
69
+
70
+ /** Strip `/api` prefix from Vercel catch-all rewrites so routes match dev paths. */
71
+ export function normalizePlasmPathname(url: string | undefined): string {
72
+ const pathname = new URL(url ?? "/", "http://localhost").pathname;
73
+ if (pathname === "/api" || pathname === "/api/") return "/";
74
+ if (pathname.startsWith("/api/")) return pathname.slice(4) || "/";
75
+ return pathname;
76
+ }
77
+
78
+ export function rewriteRequestPath(req: IncomingMessage): IncomingMessage {
79
+ const pathname = normalizePlasmPathname(req.url);
80
+ const original = req.url ?? "/";
81
+ const query = original.includes("?") ? original.slice(original.indexOf("?")) : "";
82
+ const proxy = Object.create(req) as IncomingMessage;
83
+ Object.defineProperty(proxy, "url", {
84
+ value: `${pathname}${query}`,
85
+ writable: true,
86
+ configurable: true,
87
+ });
88
+ return proxy;
89
+ }
90
+
91
+ export async function handlePlasmRequest(
92
+ req: IncomingMessage,
93
+ res: ServerResponse,
94
+ app: PlasmApp,
95
+ ): Promise<void> {
96
+ const method = req.method ?? "GET";
97
+ const url = new URL(req.url ?? "/", "http://localhost");
98
+
99
+ if (method === "GET" && url.pathname === "/plasm/v1/health") {
100
+ sendJson(res, 200, { status: "ok" });
101
+ return;
102
+ }
103
+
104
+ if (method === "GET" && url.pathname === "/plasm/v1/info") {
105
+ const discovery = await app.getDiscovery();
106
+ const loadedSlots = await app.getLoadedSlots();
107
+ const subagents = await app.getSubagents();
108
+ if (!loadedSlots) {
109
+ sendJson(res, 503, { error: "slots_not_ready" });
110
+ return;
111
+ }
112
+ const info = await collectProjectInfo({
113
+ projectRoot: app.projectRoot,
114
+ agentRoot: app.agentRoot,
115
+ cached: { discovery, loadedSlots, subagents },
116
+ dev:
117
+ app.mode === "dev"
118
+ ? {
119
+ definition: app.definition,
120
+ sessionCount: app.sessionStore?.list().length ?? 0,
121
+ }
122
+ : undefined,
123
+ });
124
+ sendJson(res, 200, info);
125
+ return;
126
+ }
127
+
128
+ const loadedSlots = await app.getLoadedSlots();
129
+ if (loadedSlots?.schedules.length) {
130
+ const handledCron = tryHandleScheduleCronRoute(
131
+ req,
132
+ res,
133
+ loadedSlots.schedules,
134
+ app.getAuthoringContext(),
135
+ );
136
+ if (handledCron) return;
137
+ }
138
+
139
+ if (loadedSlots?.channels.length) {
140
+ const handled = tryHandleChannelRoute(
141
+ req,
142
+ res,
143
+ loadedSlots.channels,
144
+ app.getAuthoringContext(),
145
+ );
146
+ if (handled) return;
147
+ }
148
+
149
+ if (
150
+ app.sessionsEnabled &&
151
+ app.sessionStore &&
152
+ (await tryHandleSessionRoutes(req, res, url, {
153
+ sessionStore: app.sessionStore,
154
+ getAgent: app.getAgent,
155
+ }))
156
+ ) {
157
+ return;
158
+ }
159
+
160
+ sendJson(res, 404, { error: "not_found", path: url.pathname });
161
+ }
162
+
163
+ export async function handlePlasmOperatorRequest(
164
+ req: IncomingMessage,
165
+ res: ServerResponse,
166
+ app: PlasmApp,
167
+ ): Promise<void> {
168
+ const url = new URL(req.url ?? "/", "http://localhost");
169
+ if (req.method === "GET" && (url.pathname === "/operator" || url.pathname === "/operator/")) {
170
+ res.statusCode = 200;
171
+ res.setHeader("content-type", "text/html; charset=utf-8");
172
+ res.end(renderOperatorShell("/operator"));
173
+ return;
174
+ }
175
+
176
+ const operatorHandler = nitroOperatorHandler({
177
+ agentRoot: app.agentRoot,
178
+ runtime: undefined,
179
+ });
180
+
181
+ const opReq = {
182
+ req: { method: req.method, url: url.pathname + url.search },
183
+ res: {
184
+ statusCode: 200,
185
+ end: (body: string) => {
186
+ res.statusCode = opReq.res.statusCode;
187
+ res.setHeader("content-type", "application/json; charset=utf-8");
188
+ res.end(body);
189
+ },
190
+ },
191
+ };
192
+ await operatorHandler(opReq);
193
+ }
194
+
195
+ export async function createPlasmApp(options: PlasmAppOptions): Promise<PlasmApp> {
196
+ const agentRoot = path.resolve(options.agentRoot);
197
+ const projectRoot = path.dirname(agentRoot);
198
+ const mode = options.mode ?? "prod";
199
+ const sessionsEnabled = options.sessions ?? mode === "dev";
200
+ const definition = resolveAgentDefinition(options.definition, agentRoot);
201
+ const sessionStore = sessionsEnabled ? new DevSessionStore() : undefined;
202
+
203
+ let agent: PlasmAgent | undefined;
204
+ let discovery: ProjectDiscovery | undefined;
205
+ let loadedSlots: LoadedProjectSlots | undefined;
206
+ let subagentRegistry: SubagentRegistry | undefined;
207
+ let subagentSummary: ReturnType<typeof summarizeSubagents> = [];
208
+ let scheduleHandle: ScheduleHandle | undefined;
209
+ let importCacheBust = Date.now();
210
+
211
+ const getAuthoringContext = () =>
212
+ createAuthoringContext({
213
+ agentRoot,
214
+ getAgent: async () => agent ?? bootstrap(),
215
+ importCacheBust,
216
+ });
217
+
218
+ const bootstrap = async (): Promise<PlasmAgent> => {
219
+ agent = await createAgentFromDefinition(definition, {
220
+ agentRoot,
221
+ tenantScope: options.tenantScope,
222
+ maxSteps: options.maxSteps,
223
+ telemetry: options.telemetry,
224
+ loadedSlots,
225
+ subagentRegistry,
226
+ getAuthoringContext,
227
+ });
228
+ return agent;
229
+ };
230
+
231
+ const refreshDiscovery = async (): Promise<ProjectDiscovery> => {
232
+ discovery = await walkAgentProject(agentRoot);
233
+ return discovery;
234
+ };
235
+
236
+ const refreshSlots = async (): Promise<LoadedProjectSlots> => {
237
+ const currentDiscovery = discovery ?? (await refreshDiscovery());
238
+ loadedSlots = await loadAuthoredSlots({
239
+ discovery: currentDiscovery,
240
+ importCacheBust,
241
+ });
242
+ return loadedSlots;
243
+ };
244
+
245
+ const refreshSubagents = async (): Promise<SubagentRegistry> => {
246
+ const currentDiscovery = discovery ?? (await refreshDiscovery());
247
+ const loaded = await loadSubagents({
248
+ discovery: currentDiscovery,
249
+ parentSlots: loadedSlots,
250
+ tenantScope: options.tenantScope,
251
+ maxSteps: options.maxSteps,
252
+ telemetry: options.telemetry,
253
+ importCacheBust,
254
+ });
255
+ if (loadedSlots) {
256
+ loadedSlots.diagnostics.push(...loaded.diagnostics);
257
+ }
258
+ subagentRegistry = createSubagentRegistry(loaded.subagents);
259
+ subagentSummary = summarizeSubagents(loaded.subagents, agentRoot);
260
+ return subagentRegistry;
261
+ };
262
+
263
+ const reload = async (): Promise<void> => {
264
+ importCacheBust = Date.now();
265
+ scheduleHandle?.stop();
266
+ await refreshDiscovery();
267
+ await refreshSlots();
268
+ await refreshSubagents();
269
+ await bootstrap();
270
+ if (mode === "dev") {
271
+ scheduleHandle = startScheduleTimers(
272
+ loadedSlots?.schedules ?? [],
273
+ getAuthoringContext(),
274
+ definition.experimental?.workflow,
275
+ );
276
+ }
277
+ if (loadedSlots?.hookRunner) {
278
+ await loadedSlots.hookRunner.emit("agent:start", getAuthoringContext(), {
279
+ reason: mode === "dev" ? "hot_reload" : "bootstrap",
280
+ });
281
+ }
282
+ };
283
+
284
+ await reload();
285
+
286
+ const getAgent = async () => agent ?? bootstrap();
287
+ const getDiscovery = async () => discovery ?? refreshDiscovery();
288
+ const getLoadedSlots = async () => loadedSlots ?? refreshSlots();
289
+ const getSubagents = async () => {
290
+ if (!subagentRegistry) await refreshSubagents();
291
+ return subagentSummary;
292
+ };
293
+
294
+ const app: PlasmApp = {
295
+ agentRoot,
296
+ projectRoot,
297
+ definition,
298
+ mode,
299
+ sessionsEnabled,
300
+ reload,
301
+ getAgent,
302
+ getDiscovery,
303
+ getLoadedSlots,
304
+ getSubagents,
305
+ getAuthoringContext,
306
+ sessionStore,
307
+ handleRequest: (req, res) => handlePlasmRequest(req, res, app),
308
+ handleOperatorRequest: (req, res) => handlePlasmOperatorRequest(req, res, app),
309
+ };
310
+
311
+ return app;
312
+ }
313
+
314
+ export function vercelPlasmHandler(app: PlasmApp): VercelHandler {
315
+ return async (req, res) => {
316
+ const proxied = rewriteRequestPath(req);
317
+ const url = new URL(proxied.url ?? "/", "http://localhost");
318
+ try {
319
+ if (url.pathname === "/operator" || url.pathname.startsWith("/operator/")) {
320
+ await app.handleOperatorRequest(proxied, res);
321
+ return;
322
+ }
323
+ await app.handleRequest(proxied, res);
324
+ } catch (err) {
325
+ console.error("[plasm:vercel] request error:", err);
326
+ if (!res.writableEnded) {
327
+ sendJson(res, 500, { error: "internal_error", message: String(err) });
328
+ }
329
+ }
330
+ };
331
+ }
@@ -0,0 +1,135 @@
1
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type { SymbolRegistrySnapshot } from "./symbol-registry.js";
5
+
6
+ export interface ExecuteSessionRef {
7
+ promptHash: string;
8
+ sessionId: string;
9
+ }
10
+
11
+ export interface TeachingWave {
12
+ entryId: string;
13
+ entities: string[];
14
+ tsv: string;
15
+ at: string;
16
+ }
17
+
18
+ export interface AgentSessionState {
19
+ intent: string;
20
+ logicalSessionRef: string;
21
+ logicalSessionId: string;
22
+ tenantScope: string;
23
+ seeds: Array<{ api: string; entity: string }>;
24
+ teachingTsv: string;
25
+ waves: TeachingWave[];
26
+ symbolRegistry?: SymbolRegistrySnapshot;
27
+ planCommits: Array<{ ref: string; program: string; at: string }>;
28
+ updatedAt: string;
29
+ }
30
+
31
+ export interface SessionStore {
32
+ get(intent: string): Promise<AgentSessionState | null>;
33
+ put(state: AgentSessionState): Promise<void>;
34
+ listIntents(): Promise<string[]>;
35
+ }
36
+
37
+ function intentKey(intent: string): string {
38
+ return Buffer.from(intent, "utf8").toString("base64url");
39
+ }
40
+
41
+ export class LocalSessionStore implements SessionStore {
42
+ constructor(private readonly rootDir: string) {}
43
+
44
+ private sessionDir(): string {
45
+ return path.join(this.rootDir, ".plasm", "sessions");
46
+ }
47
+
48
+ private sessionPath(intent: string): string {
49
+ return path.join(this.sessionDir(), `${intentKey(intent)}.json`);
50
+ }
51
+
52
+ private teachingPath(intent: string): string {
53
+ return path.join(this.sessionDir(), `${intentKey(intent)}.teaching.tsv`);
54
+ }
55
+
56
+ async get(intent: string): Promise<AgentSessionState | null> {
57
+ try {
58
+ const raw = await readFile(this.sessionPath(intent), "utf8");
59
+ return JSON.parse(raw) as AgentSessionState;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ async put(state: AgentSessionState): Promise<void> {
66
+ const dir = this.sessionDir();
67
+ await mkdir(dir, { recursive: true });
68
+ await writeFile(this.sessionPath(state.intent), JSON.stringify(state, null, 2), "utf8");
69
+ await writeFile(this.teachingPath(state.intent), state.teachingTsv, "utf8");
70
+ }
71
+
72
+ async listIntents(): Promise<string[]> {
73
+ try {
74
+ const dir = this.sessionDir();
75
+ const files = await readdir(dir);
76
+ const intents: string[] = [];
77
+ for (const file of files) {
78
+ if (!file.endsWith(".json")) continue;
79
+ const raw = await readFile(path.join(dir, file), "utf8");
80
+ const state = JSON.parse(raw) as AgentSessionState;
81
+ intents.push(state.intent);
82
+ }
83
+ return intents;
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+ }
89
+
90
+ export class SessionManager {
91
+ constructor(
92
+ readonly store: SessionStore,
93
+ private readonly tenantScope = "local",
94
+ ) {}
95
+
96
+ tenant(): string {
97
+ return this.tenantScope;
98
+ }
99
+
100
+ async get(intent: string): Promise<AgentSessionState | null> {
101
+ return this.store.get(intent);
102
+ }
103
+
104
+ async getByLogicalRef(ref: string): Promise<AgentSessionState | null> {
105
+ const intents = await this.store.listIntents();
106
+ for (const intent of intents) {
107
+ const session = await this.store.get(intent);
108
+ if (session?.logicalSessionRef === ref) return session;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ async getOrCreate(intent: string, logicalSessionRef: string, logicalSessionId: string) {
114
+ const existing = await this.store.get(intent);
115
+ if (existing) return existing;
116
+ const fresh: AgentSessionState = {
117
+ intent,
118
+ logicalSessionRef,
119
+ logicalSessionId,
120
+ tenantScope: this.tenantScope,
121
+ seeds: [],
122
+ teachingTsv: "",
123
+ waves: [],
124
+ planCommits: [],
125
+ updatedAt: new Date().toISOString(),
126
+ };
127
+ await this.store.put(fresh);
128
+ return fresh;
129
+ }
130
+
131
+ async update(state: AgentSessionState): Promise<void> {
132
+ state.updatedAt = new Date().toISOString();
133
+ await this.store.put(state);
134
+ }
135
+ }