@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,469 @@
1
+ import path from "node:path";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+
4
+ import type { LoadedCatalog } from "../catalog/loader.js";
5
+ import { FilesystemCatalogLoader } from "../catalog/loader.js";
6
+ import {
7
+ createEngine,
8
+ type HostTransportFn,
9
+ type PlasmEngine,
10
+ } from "../engine/napi-binding.js";
11
+ import { createDefaultHostTransport } from "../engine/host-transport.js";
12
+ import { mintLogicalSessionId } from "../runtime/logical-session.js";
13
+ import { SessionManager, type AgentSessionState } from "../session-state.js";
14
+ import {
15
+ formatPlasmContextMarkdown,
16
+ formatPlasmDryRunMarkdown,
17
+ formatPlasmRunMarkdown,
18
+ } from "../tools/format.js";
19
+ import { LocalArchiveStore } from "../archive/index.js";
20
+ import { createArchiveStore } from "../archive/resolve-backend.js";
21
+ import type { ProdArchiveStore } from "../archive/prod-archive-store.js";
22
+ import { computeRunId } from "../archive/run-id.js";
23
+ import { activeTraceId, plasmSpans } from "../telemetry/plasm-spans.js";
24
+ import type { AuthoringContext } from "../authoring/context.js";
25
+ import type { HookRunner } from "../authoring/hook-runner.js";
26
+ import type { AgentWorkflowWorldDefinition } from "../define-agent.js";
27
+ import { createAgentStateStore } from "../state/define-state.js";
28
+
29
+ export type AgentArchiveStore = LocalArchiveStore | ProdArchiveStore;
30
+
31
+ export interface AgentRuntimeConfig {
32
+ agentRoot: string;
33
+ tenantScope?: string;
34
+ engine?: PlasmEngine;
35
+ /** Outbound HTTP for live `plasm_run`. Defaults to fetch + env bearer + Connect. Set `null` to validate-only. */
36
+ hostTransport?: HostTransportFn | null;
37
+ /** When false, skip local archive writes. Default true. */
38
+ archiveEnabled?: boolean;
39
+ archive?: AgentArchiveStore | null;
40
+ /** Workflow/state world — selects fs vs KV vs Postgres session mirror. */
41
+ stateWorld?: AgentWorkflowWorldDefinition;
42
+ hookRunner?: HookRunner;
43
+ getAuthoringContext?: () => AuthoringContext;
44
+ }
45
+
46
+ export interface DiscoverInput {
47
+ intent: string;
48
+ }
49
+
50
+ export interface PlasmContextInput {
51
+ intent: string;
52
+ seeds: Array<{ api: string; entity: string }>;
53
+ rankedCapabilities?: string[] | null;
54
+ }
55
+
56
+ export interface PlasmPlanInput {
57
+ logicalSessionRef: string;
58
+ program: string;
59
+ reasoning?: string;
60
+ }
61
+
62
+ export interface PlasmRunInput {
63
+ logicalSessionRef: string;
64
+ planCommitRef: string;
65
+ reasoning?: string;
66
+ }
67
+
68
+ function seedKey(seed: { api: string; entity: string }): string {
69
+ return `${seed.api}:${seed.entity}`;
70
+ }
71
+
72
+ function mergeSeeds(
73
+ existing: Array<{ api: string; entity: string }>,
74
+ incoming: Array<{ api: string; entity: string }>,
75
+ ): Array<{ api: string; entity: string }> {
76
+ const seen = new Set(existing.map(seedKey));
77
+ const out = [...existing];
78
+ for (const seed of incoming) {
79
+ const key = seedKey(seed);
80
+ if (seen.has(key)) continue;
81
+ seen.add(key);
82
+ out.push(seed);
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function planArchiveEnabled(): boolean {
88
+ const flag = process.env.PLASM_WRITE_PLAN_ARCHIVE?.trim();
89
+ if (flag === "0" || flag === "false") return false;
90
+ return true;
91
+ }
92
+
93
+ interface RunPlasmMeta {
94
+ steps?: Array<{ request_fingerprints?: string[] }>;
95
+ request_fingerprints?: string[];
96
+ }
97
+
98
+ function parseRunMeta(metaJson?: string): RunPlasmMeta | undefined {
99
+ if (!metaJson?.trim()) return undefined;
100
+ try {
101
+ const parsed = JSON.parse(metaJson) as { plasm?: RunPlasmMeta };
102
+ return parsed.plasm;
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ function collectRequestFingerprints(meta?: RunPlasmMeta): string[] {
109
+ if (!meta) return [];
110
+ const fromSteps = (meta.steps ?? []).flatMap((step) => step.request_fingerprints ?? []);
111
+ const direct = meta.request_fingerprints ?? [];
112
+ return [...new Set([...fromSteps, ...direct])];
113
+ }
114
+
115
+ function parseRowsJson(rowsJson?: string): unknown[] | undefined {
116
+ if (!rowsJson?.trim()) return undefined;
117
+ try {
118
+ const parsed = JSON.parse(rowsJson) as unknown;
119
+ return Array.isArray(parsed) ? parsed : [parsed];
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ export class AgentRuntime {
126
+ readonly engine: PlasmEngine;
127
+ readonly sessionManager: SessionManager;
128
+ readonly archive: AgentArchiveStore | null;
129
+ readonly hostTransport: HostTransportFn | null;
130
+ private readonly agentRoot: string;
131
+ private readonly archiveEnabled: boolean;
132
+ private readonly hookRunner?: HookRunner;
133
+ private readonly getAuthoringContext?: () => AuthoringContext;
134
+ private loadedCatalogs: LoadedCatalog[] = [];
135
+
136
+ constructor(config: AgentRuntimeConfig) {
137
+ this.agentRoot = path.resolve(config.agentRoot);
138
+ this.engine = config.engine ?? createEngine();
139
+ this.hostTransport =
140
+ config.hostTransport === null
141
+ ? null
142
+ : (config.hostTransport ?? createDefaultHostTransport({ useConnect: true }));
143
+ const tenantScope = config.tenantScope ?? "local";
144
+ const stateStore = createAgentStateStore({
145
+ agentRoot: this.agentRoot,
146
+ tenantScope,
147
+ world: config.stateWorld,
148
+ });
149
+ this.sessionManager = new SessionManager(stateStore, tenantScope);
150
+ this.archiveEnabled = config.archiveEnabled ?? true;
151
+ this.hookRunner = config.hookRunner;
152
+ this.getAuthoringContext = config.getAuthoringContext;
153
+ if (config.archive === null) {
154
+ this.archive = null;
155
+ } else if (config.archive) {
156
+ this.archive = config.archive;
157
+ } else if (this.archiveEnabled) {
158
+ this.archive = createArchiveStore(this.agentRoot, { world: config.stateWorld });
159
+ } else {
160
+ this.archive = null;
161
+ }
162
+ }
163
+
164
+ async bootstrap(): Promise<LoadedCatalog[]> {
165
+ if (this.archive) {
166
+ await this.archive.bootstrap();
167
+ }
168
+ const loader = new FilesystemCatalogLoader();
169
+ const catalogs = await loader.discover(this.agentRoot);
170
+ for (const catalog of catalogs) {
171
+ await this.engine.loadCatalog(catalog);
172
+ }
173
+ this.loadedCatalogs = catalogs;
174
+ return catalogs;
175
+ }
176
+
177
+ listCatalogs(): LoadedCatalog[] {
178
+ return [...this.loadedCatalogs];
179
+ }
180
+
181
+ async discoverCapabilities(input: DiscoverInput): Promise<string> {
182
+ return plasmSpans.toolDiscover({ intent: input.intent.trim() }, async (span) => {
183
+ const intent = input.intent.trim();
184
+ if (!intent) {
185
+ throw new Error("discover_capabilities `intent` must be a non-empty string");
186
+ }
187
+ const started = Date.now();
188
+ const result = await this.engine.discover(intent);
189
+ await this.recordToolTrace("tool", "discover_capabilities", started, {
190
+ intent,
191
+ trace_id: activeTraceId() ?? span.spanContext().traceId,
192
+ });
193
+ return result.markdown;
194
+ });
195
+ }
196
+
197
+ async openOrExtendSession(intent: string): Promise<AgentSessionState> {
198
+ const trimmed = intent.trim();
199
+ let session = await this.sessionManager.get(trimmed);
200
+ if (session) return session;
201
+ const ids = mintLogicalSessionId(this.sessionManager.tenant(), trimmed);
202
+ return this.sessionManager.getOrCreate(
203
+ trimmed,
204
+ ids.logicalSessionRef,
205
+ ids.logicalSessionId,
206
+ );
207
+ }
208
+
209
+ async plasmContext(input: PlasmContextInput): Promise<string> {
210
+ const intent = input.intent.trim();
211
+ const entryId = input.seeds[0]?.api;
212
+ const catalogCgsHash = entryId ? this.catalogHashForEntry(entryId) : undefined;
213
+
214
+ return plasmSpans.toolContext(
215
+ {
216
+ intent,
217
+ entryId,
218
+ catalogCgsHash,
219
+ },
220
+ async (span) => {
221
+ if (!intent) throw new Error("plasm_context requires `intent`");
222
+ if (!input.seeds.length) throw new Error("plasm_context requires non-empty `seeds`");
223
+
224
+ void input.rankedCapabilities;
225
+
226
+ let session = await this.openOrExtendSession(intent);
227
+ const before = new Set(session.seeds.map(seedKey));
228
+ const merged = mergeSeeds(session.seeds, input.seeds);
229
+ const hasNew = merged.length > before.size;
230
+
231
+ const started = Date.now();
232
+ const exposure = await this.engine.synthesizeTeaching(intent, input.seeds);
233
+ const reused = !hasNew && !exposure.tsv.trim();
234
+
235
+ if (exposure.tsv.trim()) {
236
+ session.teachingTsv = session.teachingTsv
237
+ ? `${session.teachingTsv.trim()}\n\n${exposure.tsv.trim()}`
238
+ : exposure.tsv.trim();
239
+ session.waves.push({
240
+ entryId: input.seeds[0]?.api ?? "unknown",
241
+ entities: input.seeds.map((s) => s.entity),
242
+ tsv: exposure.tsv,
243
+ at: new Date().toISOString(),
244
+ });
245
+ }
246
+ session.seeds = merged;
247
+ await this.sessionManager.update(session);
248
+
249
+ span.setAttribute("plasm.logical_session_ref", session.logicalSessionRef);
250
+ await this.recordToolTrace("tool", "plasm_context", started, {
251
+ intent,
252
+ logical_session_ref: session.logicalSessionRef,
253
+ entry_id: entryId,
254
+ trace_id: activeTraceId() ?? span.spanContext().traceId,
255
+ });
256
+
257
+ return formatPlasmContextMarkdown(session.logicalSessionRef, exposure.tsv, reused);
258
+ },
259
+ );
260
+ }
261
+
262
+ async plasm(input: PlasmPlanInput): Promise<string> {
263
+ const session = await this.requireSessionByRef(input.logicalSessionRef);
264
+ void input.reasoning;
265
+ const entryId = session.seeds[0]?.api;
266
+ const catalogCgsHash = entryId ? this.catalogHashForEntry(entryId) : undefined;
267
+
268
+ return plasmSpans.dryRun(
269
+ {
270
+ intent: session.intent,
271
+ logicalSessionRef: session.logicalSessionRef,
272
+ sessionId: session.logicalSessionId,
273
+ entryId,
274
+ catalogCgsHash,
275
+ },
276
+ async (span) => {
277
+ const started = Date.now();
278
+ const dry = await this.engine.dryRun(input.program);
279
+ session.planCommits.push({
280
+ ref: dry.planCommitRef,
281
+ program: input.program,
282
+ at: new Date().toISOString(),
283
+ });
284
+ await this.sessionManager.update(session);
285
+
286
+ span.setAttribute("plasm.plan_commit_ref", dry.planCommitRef);
287
+ if (catalogCgsHash) {
288
+ span.setAttribute("plasm.catalog_cgs_hash", catalogCgsHash);
289
+ }
290
+
291
+ if (this.archive && planArchiveEnabled() && catalogCgsHash) {
292
+ await this.archive.writePlanArchive({
293
+ plan_commit_ref: dry.planCommitRef,
294
+ program: input.program,
295
+ catalog_cgs_hash: catalogCgsHash,
296
+ entry_id: entryId,
297
+ logical_session_ref: session.logicalSessionRef,
298
+ intent: session.intent,
299
+ comp_json: dry.compJson,
300
+ archived_at: new Date().toISOString(),
301
+ });
302
+ }
303
+
304
+ await this.recordToolTrace("plasm", "plasm.dry_run", started, {
305
+ intent: session.intent,
306
+ logical_session_ref: session.logicalSessionRef,
307
+ plan_commit_ref: dry.planCommitRef,
308
+ catalog_cgs_hash: catalogCgsHash,
309
+ trace_id: activeTraceId() ?? span.spanContext().traceId,
310
+ });
311
+
312
+ await this.emitHook("plan:commit", {
313
+ planCommitRef: dry.planCommitRef,
314
+ program: input.program,
315
+ logicalSessionRef: session.logicalSessionRef,
316
+ });
317
+
318
+ return formatPlasmDryRunMarkdown(dry.summary, dry.planCommitRef);
319
+ },
320
+ );
321
+ }
322
+
323
+ async plasmRun(input: PlasmRunInput): Promise<string> {
324
+ const session = await this.requireSessionByRef(input.logicalSessionRef);
325
+ void input.reasoning;
326
+ const entryId = session.seeds[0]?.api;
327
+ const catalogCgsHash = entryId ? this.catalogHashForEntry(entryId) : undefined;
328
+ const planCommit = session.planCommits.find((pc) => pc.ref === input.planCommitRef);
329
+
330
+ return plasmSpans.liveRun(
331
+ {
332
+ intent: session.intent,
333
+ logicalSessionRef: session.logicalSessionRef,
334
+ sessionId: session.logicalSessionId,
335
+ planCommitRef: input.planCommitRef,
336
+ entryId,
337
+ catalogCgsHash,
338
+ },
339
+ async (span) => {
340
+ const started = Date.now();
341
+ const result =
342
+ this.hostTransport && typeof this.engine.runPlanLive === "function"
343
+ ? await this.engine.runPlanLive(input.planCommitRef, this.hostTransport)
344
+ : await this.engine.runPlan(input.planCommitRef);
345
+ const program = planCommit?.program ?? "";
346
+ const runMeta = parseRunMeta(result.metaJson);
347
+ const requestFingerprints = collectRequestFingerprints(runMeta);
348
+ const runId =
349
+ catalogCgsHash && program
350
+ ? computeRunId({
351
+ catalogCgsHash,
352
+ planCommitRef: input.planCommitRef,
353
+ program,
354
+ entryId,
355
+ requestFingerprints,
356
+ })
357
+ : `pr${createHash("sha256").update(randomUUID()).digest("hex")}`;
358
+
359
+ span.setAttribute("plasm.run_id", runId);
360
+ span.setAttribute("plasm.plan_commit_ref", input.planCommitRef);
361
+ if (catalogCgsHash) {
362
+ span.setAttribute("plasm.catalog_cgs_hash", catalogCgsHash);
363
+ }
364
+
365
+ if (this.archive) {
366
+ await this.archive.writeRunSnapshot({
367
+ run_id: runId,
368
+ plan_commit_ref: input.planCommitRef,
369
+ catalog_cgs_hash: catalogCgsHash ?? "unknown",
370
+ entry_id: entryId,
371
+ logical_session_ref: session.logicalSessionRef,
372
+ intent: session.intent,
373
+ ok: result.ok,
374
+ message: result.message,
375
+ results: parseRowsJson(result.rowsJson),
376
+ _meta: {
377
+ plasm: {
378
+ steps: runMeta?.steps ?? [],
379
+ request_fingerprints: requestFingerprints,
380
+ },
381
+ },
382
+ archived_at: new Date().toISOString(),
383
+ });
384
+ }
385
+
386
+ await this.recordToolTrace("plasm", "plasm.live_run", started, {
387
+ intent: session.intent,
388
+ logical_session_ref: session.logicalSessionRef,
389
+ plan_commit_ref: input.planCommitRef,
390
+ run_id: runId,
391
+ catalog_cgs_hash: catalogCgsHash,
392
+ ok: result.ok,
393
+ trace_id: activeTraceId() ?? span.spanContext().traceId,
394
+ });
395
+
396
+ await this.emitHook("run:complete", {
397
+ planCommitRef: input.planCommitRef,
398
+ runId,
399
+ ok: result.ok,
400
+ logicalSessionRef: session.logicalSessionRef,
401
+ });
402
+
403
+ return formatPlasmRunMarkdown(result.message, result.ok, result.rowsJson);
404
+ },
405
+ );
406
+ }
407
+
408
+ private async emitHook(
409
+ event: "plan:commit" | "run:complete",
410
+ detail: Record<string, unknown>,
411
+ ): Promise<void> {
412
+ if (!this.hookRunner || !this.getAuthoringContext) return;
413
+ await this.hookRunner.emit(event, this.getAuthoringContext(), detail);
414
+ }
415
+
416
+ private catalogHashForEntry(entryId: string): string | undefined {
417
+ return this.loadedCatalogs.find((c) => c.manifest.entryId === entryId)?.manifest.cgsHash;
418
+ }
419
+
420
+ private async recordToolTrace(
421
+ kind: string,
422
+ name: string,
423
+ startedMs: number,
424
+ attributes: Record<string, string | number | boolean | undefined>,
425
+ ): Promise<void> {
426
+ if (!this.archive) return;
427
+ const tenantId = this.sessionManager.tenant();
428
+ const traceId = (attributes.trace_id as string | undefined) ?? randomUUID();
429
+ const cleaned: Record<string, string | number | boolean> = {};
430
+ for (const [key, value] of Object.entries(attributes)) {
431
+ if (value !== undefined) cleaned[key] = value;
432
+ }
433
+ await this.archive.recordToolEvent(tenantId, traceId, kind, name, cleaned);
434
+ await this.archive.finalizeTrace({
435
+ summary: {
436
+ trace_id: traceId,
437
+ tenant_id: tenantId,
438
+ logical_session_ref:
439
+ typeof cleaned.logical_session_ref === "string"
440
+ ? cleaned.logical_session_ref
441
+ : undefined,
442
+ intent: typeof cleaned.intent === "string" ? cleaned.intent : undefined,
443
+ status: "completed",
444
+ started_at_ms: startedMs,
445
+ ended_at_ms: Date.now(),
446
+ project_slug: "main",
447
+ totals: { tool_calls: 1 },
448
+ },
449
+ records: [
450
+ {
451
+ at_ms: startedMs,
452
+ kind,
453
+ name,
454
+ attributes: cleaned,
455
+ },
456
+ ],
457
+ });
458
+ }
459
+
460
+ private async requireSessionByRef(ref: string): Promise<AgentSessionState> {
461
+ const session = await this.sessionManager.getByLogicalRef(ref);
462
+ if (!session) {
463
+ throw new Error(
464
+ `unknown logical_session_ref \`${ref}\` — call plasm_context first with a stable intent`,
465
+ );
466
+ }
467
+ return session;
468
+ }
469
+ }
@@ -0,0 +1,81 @@
1
+ import { generateText, type LanguageModel, type ModelMessage } from "ai";
2
+
3
+ import type { AgentCompactionConfig } from "../define-agent.js";
4
+ import { resolveGatewayModel } from "../gateway-model.js";
5
+
6
+ const DEFAULT_CONTEXT_TOKENS = 200_000;
7
+
8
+ function estimateTokens(messages: ModelMessage[]): number {
9
+ let chars = 0;
10
+ for (const message of messages) {
11
+ if (typeof message.content === "string") {
12
+ chars += message.content.length;
13
+ } else if (Array.isArray(message.content)) {
14
+ for (const part of message.content) {
15
+ if (typeof part === "object" && part && "text" in part && typeof part.text === "string") {
16
+ chars += part.text.length;
17
+ }
18
+ }
19
+ }
20
+ }
21
+ return Math.ceil(chars / 4);
22
+ }
23
+
24
+ function splitForCompaction(messages: ModelMessage[]): {
25
+ prefix: ModelMessage[];
26
+ suffix: ModelMessage[];
27
+ } {
28
+ if (messages.length <= 4) {
29
+ return { prefix: [], suffix: messages };
30
+ }
31
+ const keepRecent = Math.min(6, messages.length);
32
+ return {
33
+ prefix: messages.slice(0, messages.length - keepRecent),
34
+ suffix: messages.slice(messages.length - keepRecent),
35
+ };
36
+ }
37
+
38
+ /** Eve-shaped context trimming when transcript exceeds compaction threshold. */
39
+ export async function maybeCompactMessages(
40
+ messages: ModelMessage[],
41
+ compaction: AgentCompactionConfig | undefined,
42
+ primaryModel: string | LanguageModel,
43
+ ): Promise<ModelMessage[]> {
44
+ if (!compaction?.thresholdPercent || messages.length < 3) {
45
+ return messages;
46
+ }
47
+
48
+ const threshold = Math.floor(
49
+ DEFAULT_CONTEXT_TOKENS * (compaction.thresholdPercent / 100),
50
+ );
51
+ const estimated = estimateTokens(messages);
52
+ if (estimated < threshold) {
53
+ return messages;
54
+ }
55
+
56
+ const { prefix, suffix } = splitForCompaction(messages);
57
+ if (!prefix.length) {
58
+ return messages;
59
+ }
60
+
61
+ const summaryModel = resolveGatewayModel(compaction.model ?? primaryModel);
62
+ const transcript = prefix
63
+ .map((m) => `${m.role}: ${typeof m.content === "string" ? m.content : JSON.stringify(m.content)}`)
64
+ .join("\n\n");
65
+
66
+ const summary = await generateText({
67
+ model: summaryModel,
68
+ system:
69
+ "Summarize the prior agent conversation for continuation. Preserve goals, catalog picks, logical_session_ref, plan_commit_ref, and unresolved tasks. Be concise.",
70
+ prompt: transcript,
71
+ temperature: 0,
72
+ });
73
+
74
+ return [
75
+ {
76
+ role: "user",
77
+ content: `[compacted context]\n${summary.text.trim()}`,
78
+ },
79
+ ...suffix,
80
+ ];
81
+ }
@@ -0,0 +1,72 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+
3
+ const WIRE_PREFIX = "l_";
4
+ const TOKEN_LEN = 22;
5
+
6
+ /** Canonical MCP logical session wire ref: `l_<base64url-unpadded-uuid-bytes>`. */
7
+ export function formatLogicalSessionWireRef(uuidBytes: Buffer): string {
8
+ if (uuidBytes.length !== 16) {
9
+ throw new Error("logical session UUID must be 16 bytes");
10
+ }
11
+ const token = uuidBytes.toString("base64url");
12
+ if (token.length !== TOKEN_LEN) {
13
+ throw new Error(`unexpected wire token length ${token.length}`);
14
+ }
15
+ return `${WIRE_PREFIX}${token}`;
16
+ }
17
+
18
+ export function parseLogicalSessionWireRef(ref: string): Buffer {
19
+ const trimmed = ref.trim();
20
+ if (!trimmed.startsWith(WIRE_PREFIX)) {
21
+ throw new Error(`invalid logical_session_ref: ${ref}`);
22
+ }
23
+ const token = trimmed.slice(WIRE_PREFIX.length);
24
+ if (token.length !== TOKEN_LEN) {
25
+ throw new Error(`invalid logical_session_ref token length: ${ref}`);
26
+ }
27
+ const bytes = Buffer.from(token, "base64url");
28
+ if (bytes.length !== 16) {
29
+ throw new Error(`invalid logical_session_ref payload: ${ref}`);
30
+ }
31
+ return bytes;
32
+ }
33
+
34
+ /**
35
+ * Idempotent logical session identity for `(tenantScope, intent)`.
36
+ * Persisted via SessionStore on first `plasm_context` open.
37
+ */
38
+ export function mintLogicalSessionId(tenantScope: string, intent: string): {
39
+ logicalSessionId: string;
40
+ logicalSessionRef: string;
41
+ } {
42
+ const digest = createHash("sha256")
43
+ .update(`${tenantScope}\0${intent}`, "utf8")
44
+ .digest();
45
+ const bytes = Buffer.alloc(16);
46
+ digest.copy(bytes, 0, 0, 16);
47
+ bytes[6] = (bytes[6]! & 0x0f) | 0x50;
48
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
49
+ const logicalSessionId = [
50
+ bytes.toString("hex", 0, 4),
51
+ bytes.toString("hex", 4, 6),
52
+ bytes.toString("hex", 6, 8),
53
+ bytes.toString("hex", 8, 10),
54
+ bytes.toString("hex", 10, 16),
55
+ ].join("-");
56
+ return {
57
+ logicalSessionId,
58
+ logicalSessionRef: formatLogicalSessionWireRef(bytes),
59
+ };
60
+ }
61
+
62
+ export function newEphemeralLogicalSession(): {
63
+ logicalSessionId: string;
64
+ logicalSessionRef: string;
65
+ } {
66
+ const id = randomUUID();
67
+ const bytes = Buffer.from(id.replace(/-/g, ""), "hex");
68
+ return {
69
+ logicalSessionId: id,
70
+ logicalSessionRef: formatLogicalSessionWireRef(bytes),
71
+ };
72
+ }