@assistant-ui/x-generative-compiler 0.0.5 → 0.0.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","names":[],"sources":["../src/compile.ts"],"mappings":";;;KAoBY,QAAA;AAAA,UAkCK,cAAA;EAlCL;EAoCV,MAAA,EAAQ,MAAM;;EAEd,QAAA;EAtCkB;EAwClB,UAAA;EAN6B;;;;;;;;EAe7B,gBAAA;AAAA;AAAA,UAGe,aAAA;EACf,IAAA;EACA,GAAG;AAAA;AAAA;AAAA,cAIQ,sBAAA,SAA+B,KAAK;cACnC,OAAA,UAAiB,QAAA;AAAA;;iBAOf,kBAAA,CAAmB,IAAY;;;;;iBA4B/B,iBAAA,CACd,IAAA,UACA,OAAA,EAAS,cAAA,GACR,aAAa"}
1
+ {"version":3,"file":"compile.d.ts","names":[],"sources":["../src/compile.ts"],"mappings":";;;KAoBY,QAAA;AAAA,UAyCK,cAAA;EAzCL;EA2CV,MAAA,EAAQ,MAAM;;EAEd,QAAA;EA7CkB;EA+ClB,UAAA;EAN6B;;;;;;;;EAe7B,gBAAA;AAAA;AAAA,UAGe,aAAA;EACf,IAAA;EACA,GAAG;AAAA;AAAA;AAAA,cAIQ,sBAAA,SAA+B,KAAK;cACnC,OAAA,UAAiB,QAAA;AAAA;;iBASf,kBAAA,CAAmB,IAAY;;;;;iBA+I/B,iBAAA,CACd,IAAA,UACA,OAAA,EAAS,cAAA,GACR,aAAa"}
package/dist/compile.js CHANGED
@@ -34,10 +34,17 @@ const DISTRIBUTION_PACKAGES = [
34
34
  * through untouched — the library, not this compiler, routes its halves.
35
35
  */
36
36
  const GENERATIVE_FACTORY = "JSONGenerativeUI";
37
+ /**
38
+ * The factory producing an interactable's complete tool entry. Unlike
39
+ * `JSONGenerativeUI`, its package has no per-target builds, so this compiler
40
+ * splits the inline config: the client keeps `render`, the server drops it.
41
+ * The factory's own `execute` is internal and client-safe (frontend tool).
42
+ */
43
+ const INTERACTABLE_TOOL_FACTORY = "unstable_interactableTool";
37
44
  /** Thrown when a `"use generative"` file violates an authoring constraint. */
38
45
  var GenerativeCompileError = class extends Error {
39
46
  constructor(message, filename) {
40
- super(`[assistant-ui/next]${filename ? ` ${filename}:` : ""} ${message}`);
47
+ super(`[assistant-ui/use-generative]${filename ? ` ${filename}:` : ""} ${message}`);
41
48
  this.name = "GenerativeCompileError";
42
49
  }
43
50
  };
@@ -56,7 +63,89 @@ function isGenerativeModule(code) {
56
63
  i = end + 2;
57
64
  } else break;
58
65
  }
59
- return code.startsWith(`"use generative"`, i) || code.startsWith(`'use generative'`, i);
66
+ const quote = code[i];
67
+ if (quote !== "\"" && quote !== "'") return false;
68
+ const directiveStart = i + 1;
69
+ if (!code.startsWith("use generative", directiveStart)) return false;
70
+ const directiveEnd = directiveStart + DIRECTIVE.length;
71
+ if (code[directiveEnd] !== quote) return false;
72
+ return hasDirectiveTerminator(code, directiveEnd + 1);
73
+ }
74
+ function hasDirectiveTerminator(code, start) {
75
+ let i = start;
76
+ let sawLineTerminator = false;
77
+ for (;;) {
78
+ if (i >= code.length) return true;
79
+ const char = code.charCodeAt(i);
80
+ if (isSemicolon(char)) return true;
81
+ if (isLineTerminator(char)) {
82
+ sawLineTerminator = true;
83
+ i++;
84
+ continue;
85
+ }
86
+ if (code.startsWith("//", i)) {
87
+ const lineEnd = nextLineTerminatorIndex(code, i + 2);
88
+ if (lineEnd === -1) return true;
89
+ sawLineTerminator = true;
90
+ i = lineEnd + 1;
91
+ continue;
92
+ }
93
+ if (code.startsWith("/*", i)) {
94
+ const end = code.indexOf("*/", i + 2);
95
+ if (end === -1) return false;
96
+ sawLineTerminator ||= containsLineTerminator(code, i + 2, end);
97
+ i = end + 2;
98
+ continue;
99
+ }
100
+ if (/\s/.test(code[i])) {
101
+ i++;
102
+ continue;
103
+ }
104
+ return sawLineTerminator && !startsExpressionContinuation(code, i);
105
+ }
106
+ }
107
+ function nextLineTerminatorIndex(code, start) {
108
+ for (let i = start; i < code.length; i++) if (isLineTerminator(code.charCodeAt(i))) return i;
109
+ return -1;
110
+ }
111
+ function containsLineTerminator(code, start, end) {
112
+ for (let i = start; i < end; i++) if (isLineTerminator(code.charCodeAt(i))) return true;
113
+ return false;
114
+ }
115
+ function startsExpressionContinuation(code, start) {
116
+ switch (code[start]) {
117
+ case ".":
118
+ case "+":
119
+ case "-":
120
+ case "*":
121
+ case "/":
122
+ case "%":
123
+ case "<":
124
+ case ">":
125
+ case "=":
126
+ case "!":
127
+ case "&":
128
+ case "|":
129
+ case "^":
130
+ case "?":
131
+ case ":":
132
+ case ",":
133
+ case "[":
134
+ case "(":
135
+ case "`": return true;
136
+ }
137
+ return startsKeywordContinuation(code, start, "as") || startsKeywordContinuation(code, start, "in") || startsKeywordContinuation(code, start, "instanceof") || startsKeywordContinuation(code, start, "satisfies");
138
+ }
139
+ function startsKeywordContinuation(code, start, keyword) {
140
+ if (!code.startsWith(keyword, start)) return false;
141
+ const next = code[start + keyword.length];
142
+ return next === void 0 || !/[\p{ID_Continue}$]/u.test(next);
143
+ }
144
+ function isSemicolon(char) {
145
+ return char === 59;
146
+ }
147
+ function isLineTerminator(char) {
148
+ return char === 10 || char === 13 || char === 8232 || char === 8233;
60
149
  }
61
150
  /**
62
151
  * Rewrites a `"use generative"` module for a single build target, keeping only the
@@ -76,7 +165,8 @@ function compileGenerative(code, options) {
76
165
  ensureCompilerCompatibleWithCore(ast, filename);
77
166
  ensureDefaultExport(ast, filename);
78
167
  const generativeInstances = collectGenerativeInstances(ast);
79
- const safeToolkitSpreads = collectSafeToolkitSpreads(ast, filename);
168
+ const interactableToolImports = collectInteractableToolImports(ast);
169
+ const toolkitSpreadNames = collectToolkitSpreadNames(ast, filename, createToolkitNameContext());
80
170
  const flags = {
81
171
  keptRender: false,
82
172
  keptBackendExecute: false
@@ -93,7 +183,7 @@ function compileGenerative(code, options) {
93
183
  }
94
184
  if (t.isIdentifier(callee, { name: TOOLKIT_WRAPPER })) {
95
185
  if (!object) throw new GenerativeCompileError(`${TOOLKIT_WRAPPER}() takes an inline object literal of tools`, filename);
96
- compileToolkit(object, target, generativeInstances, safeToolkitSpreads, flags, filename);
186
+ compileToolkit(object, target, generativeInstances, interactableToolImports, toolkitSpreadNames, flags, filename);
97
187
  path.replaceWith(object);
98
188
  path.skip();
99
189
  }
@@ -244,7 +334,36 @@ function collectGenerativeInstances(ast) {
244
334
  return names;
245
335
  }
246
336
  /**
247
- * Toolkit identifiers that are safe to spread into a `defineToolkit({ ... })`.
337
+ * Collects the local names `unstable_interactableTool` is imported under from a
338
+ * distribution package, so toolkit entries calling it can be recognized
339
+ * (and a same-named local function can't smuggle an arbitrary call through).
340
+ */
341
+ function collectInteractableToolImports(ast) {
342
+ const names = /* @__PURE__ */ new Set();
343
+ for (const statement of ast.program.body) {
344
+ if (!t.isImportDeclaration(statement)) continue;
345
+ if (!packageNameFromSpecifier(statement.source.value)) continue;
346
+ for (const specifier of statement.specifiers) if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported, { name: INTERACTABLE_TOOL_FACTORY })) names.add(specifier.local.name);
347
+ }
348
+ return names;
349
+ }
350
+ function createToolkitNameContext() {
351
+ return {
352
+ importedToolkitNamesByFile: /* @__PURE__ */ new Map(),
353
+ resolvingImportedToolkitNames: /* @__PURE__ */ new Set()
354
+ };
355
+ }
356
+ /**
357
+ * The inline config of an `unstable_interactableTool({ ... })` toolkit entry, or `null`
358
+ * when the entry is some other expression.
359
+ */
360
+ function interactableToolConfig(value, imports) {
361
+ return t.isCallExpression(value) && t.isIdentifier(value.callee) && imports.has(value.callee.name) && t.isObjectExpression(value.arguments[0]) ? value.arguments[0] : null;
362
+ }
363
+ /**
364
+ * Toolkit identifiers that are safe to spread into a `defineToolkit({ ... })`,
365
+ * paired with the static tool names they contain. A `null` name list means the
366
+ * spread is safe, but its names are not statically known for duplicate checks.
248
367
  *
249
368
  * Two kinds qualify:
250
369
  *
@@ -257,30 +376,38 @@ function collectGenerativeInstances(ast) {
257
376
  * export crosses the generative-module boundary, so named imports don't
258
377
  * qualify — they would be `undefined` once that module is build-split.
259
378
  */
260
- function collectSafeToolkitSpreads(ast, filename) {
261
- const names = /* @__PURE__ */ new Set();
262
- const generativeBySource = /* @__PURE__ */ new Map();
379
+ function collectToolkitSpreadNames(ast, filename, context) {
380
+ const spreadNames = /* @__PURE__ */ new Map();
381
+ const localToolkitCalls = /* @__PURE__ */ new Map();
263
382
  for (const statement of ast.program.body) {
264
383
  if (t.isVariableDeclaration(statement)) {
265
384
  for (const declaration of statement.declarations) {
266
385
  const { id, init } = declaration;
267
- if (t.isIdentifier(id) && init && unwrapToToolkitCall(init)) names.add(id.name);
386
+ if (t.isIdentifier(id) && init) {
387
+ const toolkitCall = unwrapToToolkitCall(init);
388
+ if (toolkitCall) localToolkitCalls.set(id.name, toolkitCall);
389
+ }
268
390
  }
269
391
  continue;
270
392
  }
271
393
  if (t.isImportDeclaration(statement)) {
272
394
  const defaultSpecifier = statement.specifiers.find((specifier) => t.isImportDefaultSpecifier(specifier));
273
395
  if (!defaultSpecifier) continue;
274
- const source = statement.source.value;
275
- let isGenerative = generativeBySource.get(source);
276
- if (isGenerative === void 0) {
277
- isGenerative = isGenerativeImport(source, filename);
278
- generativeBySource.set(source, isGenerative);
279
- }
280
- if (isGenerative) names.add(defaultSpecifier.local.name);
396
+ const names = getGenerativeImportToolkitNames(statement.source.value, filename, context);
397
+ if (names !== void 0) spreadNames.set(defaultSpecifier.local.name, names);
281
398
  }
282
399
  }
283
- return names;
400
+ const resolveLocal = (name) => {
401
+ if (spreadNames.has(name)) return spreadNames.get(name);
402
+ const call = localToolkitCalls.get(name);
403
+ if (!call) return void 0;
404
+ const object = t.isObjectExpression(call.arguments[0]) ? call.arguments[0] : null;
405
+ const publicNames = uniqueToolkitNames(object ? collectToolkitObjectNames(object, spreadNames) : null);
406
+ spreadNames.set(name, publicNames);
407
+ return publicNames;
408
+ };
409
+ for (const name of localToolkitCalls.keys()) resolveLocal(name);
410
+ return spreadNames;
284
411
  }
285
412
  const MODULE_EXTENSIONS = [
286
413
  ".ts",
@@ -293,28 +420,63 @@ const MODULE_EXTENSIONS = [
293
420
  ".cjs"
294
421
  ];
295
422
  /** Extensions a specifier may carry that actually map to a TS source file. */
296
- const REWRITABLE_JS_EXTENSIONS = new Set([
423
+ const REWRITABLE_JS_EXTENSIONS = /* @__PURE__ */ new Set([
297
424
  ".js",
298
425
  ".jsx",
299
426
  ".mjs",
300
427
  ".cjs"
301
428
  ]);
302
429
  /**
303
- * Whether an import specifier resolves on disk to a `"use generative"` module.
304
- * Relative specifiers and `tsconfig` path aliases (e.g. `@/tools`) are resolved;
305
- * anything else (a bare package, an unresolvable alias) is treated as
306
- * non-generative, and thus an unsafe spread.
430
+ * Reads the static tool names from the default export of an imported
431
+ * `"use generative"` module. Relative specifiers and `tsconfig` path aliases
432
+ * (e.g. `@/tools`) are resolved; anything else (a bare package, an unresolvable
433
+ * alias) is treated as non-generative, and thus an unsafe spread.
307
434
  */
308
- function isGenerativeImport(source, filename) {
435
+ function getGenerativeImportToolkitNames(source, filename, context) {
309
436
  const cleanFilename = cleanAbsoluteFilename(filename);
310
- if (!cleanFilename) return false;
437
+ if (!cleanFilename) return void 0;
311
438
  const resolved = resolveImportedModuleFile(source, cleanFilename);
312
- if (!resolved) return false;
439
+ if (!resolved) return void 0;
440
+ if (context.importedToolkitNamesByFile.has(resolved)) return context.importedToolkitNamesByFile.get(resolved);
441
+ if (context.resolvingImportedToolkitNames.has(resolved)) return null;
442
+ let code;
313
443
  try {
314
- return isGenerativeModule(readFileSync(resolved, "utf8"));
444
+ code = readFileSync(resolved, "utf8");
315
445
  } catch {
316
- return false;
446
+ context.importedToolkitNamesByFile.set(resolved, void 0);
447
+ return;
317
448
  }
449
+ if (!isGenerativeModule(code)) {
450
+ context.importedToolkitNamesByFile.set(resolved, void 0);
451
+ return;
452
+ }
453
+ context.resolvingImportedToolkitNames.add(resolved);
454
+ try {
455
+ const names = getDefaultExportToolkitNames(parse(code, {
456
+ sourceType: "module",
457
+ plugins: [
458
+ "typescript",
459
+ "jsx",
460
+ "explicitResourceManagement"
461
+ ]
462
+ }), resolved, context);
463
+ context.importedToolkitNamesByFile.set(resolved, names);
464
+ return names;
465
+ } catch (error) {
466
+ if (error instanceof GenerativeCompileError) throw error;
467
+ context.importedToolkitNamesByFile.set(resolved, null);
468
+ return null;
469
+ } finally {
470
+ context.resolvingImportedToolkitNames.delete(resolved);
471
+ }
472
+ }
473
+ function getDefaultExportToolkitNames(ast, filename, context) {
474
+ const def = ast.program.body.find((stmt) => t.isExportDefaultDeclaration(stmt));
475
+ if (!def) return null;
476
+ const toolkitCall = unwrapToToolkitCall(def.declaration);
477
+ if (!toolkitCall || !t.isObjectExpression(toolkitCall.arguments[0])) return null;
478
+ const spreadNames = collectToolkitSpreadNames(ast, filename, context);
479
+ return uniqueToolkitNames(collectToolkitObjectNames(toolkitCall.arguments[0], spreadNames));
318
480
  }
319
481
  /** Resolves an import specifier (relative or `tsconfig`-aliased) to a file on disk. */
320
482
  function resolveImportedModuleFile(source, fromFilename) {
@@ -456,13 +618,54 @@ function parseJsonc(text) {
456
618
  function unwrapToToolkitCall(node) {
457
619
  return unwrapToCall(node, TOOLKIT_WRAPPER) ?? unwrapToCall(node, MCP_TOOLKIT_WRAPPER);
458
620
  }
459
- function isSafeToolkitSpread(entry, safeToolkitSpreads) {
460
- if (t.isIdentifier(entry.argument)) return safeToolkitSpreads.has(entry.argument.name);
621
+ function collectToolkitObjectNames(object, toolkitSpreadNames) {
622
+ const names = [];
623
+ for (const entry of object.properties) {
624
+ const entryNames = toolkitEntryNames(entry, toolkitSpreadNames);
625
+ if (!entryNames) return null;
626
+ names.push(...entryNames);
627
+ }
628
+ return names;
629
+ }
630
+ function uniqueToolkitNames(names) {
631
+ return names ? [...new Set(names)] : names;
632
+ }
633
+ function toolkitEntryNames(entry, toolkitSpreadNames) {
634
+ if (t.isSpreadElement(entry)) {
635
+ if (t.isIdentifier(entry.argument)) return toolkitSpreadNames.get(entry.argument.name) ?? null;
636
+ const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
637
+ if (directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0])) return collectToolkitObjectNames(directMcpToolkit.arguments[0], toolkitSpreadNames);
638
+ return null;
639
+ }
640
+ if (t.isObjectProperty(entry) || t.isObjectMethod(entry)) {
641
+ const name = memberName(entry.key, entry.computed);
642
+ return name ? [name] : [];
643
+ }
644
+ return [];
645
+ }
646
+ function warnDuplicateToolkitNames(object, toolkitSpreadNames, filename) {
647
+ const names = collectToolkitObjectNames(object, toolkitSpreadNames);
648
+ if (!names) return;
649
+ const seen = /* @__PURE__ */ new Set();
650
+ const warned = /* @__PURE__ */ new Set();
651
+ for (const name of names) {
652
+ if (seen.has(name)) {
653
+ if (!warned.has(name)) {
654
+ console.warn(new GenerativeCompileError(`Duplicate tool name "${name}" while composing toolkits. JavaScript object spread keeps the last definition.`, filename).message);
655
+ warned.add(name);
656
+ }
657
+ continue;
658
+ }
659
+ seen.add(name);
660
+ }
661
+ }
662
+ function isSafeToolkitSpread(entry, toolkitSpreadNames) {
663
+ if (t.isIdentifier(entry.argument)) return toolkitSpreadNames.has(entry.argument.name);
461
664
  const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
462
665
  return !!directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0]);
463
666
  }
464
667
  /** The `JSONGenerativeUI` methods that produce a split-by-condition tool. */
465
- const GENERATIVE_TOOL_METHODS = new Set(["present", "promptUser"]);
668
+ const GENERATIVE_TOOL_METHODS = /* @__PURE__ */ new Set(["present", "promptUser"]);
466
669
  /**
467
670
  * Whether a toolkit entry's value is a call to a tool-producing method on a
468
671
  * collected `JSONGenerativeUI` instance (`generative.present()`), which passes
@@ -493,12 +696,13 @@ function compileComponents(object, target, flags, filename) {
493
696
  * `generative.present()` passes through, the library having already split it.
494
697
  * Mutates the object in place and records outcomes in {@link TargetFlags}.
495
698
  */
496
- function compileToolkit(object, target, instances, safeToolkitSpreads, flags, filename) {
699
+ function compileToolkit(object, target, instances, interactableToolImports, toolkitSpreadNames, flags, filename) {
700
+ if (target === "client") warnDuplicateToolkitNames(object, toolkitSpreadNames, filename);
497
701
  const nextProperties = [];
498
702
  for (const entry of object.properties) {
499
703
  const value = entryValue(entry);
500
704
  if (!value) {
501
- if (t.isSpreadElement(entry) && isSafeToolkitSpread(entry, safeToolkitSpreads)) {
705
+ if (t.isSpreadElement(entry) && isSafeToolkitSpread(entry, toolkitSpreadNames)) {
502
706
  nextProperties.push(entry);
503
707
  continue;
504
708
  }
@@ -507,7 +711,14 @@ function compileToolkit(object, target, instances, safeToolkitSpreads, flags, fi
507
711
  nextProperties.push(entry);
508
712
  continue;
509
713
  }
510
- throw new GenerativeCompileError("each tool must be an inline object literal (`name: { ... }`) or a compiler-visible toolkit spread / generative tool (e.g. `...defineMcpToolkit(...)`, `...baseToolkit`, or `generative.present()`) so its `execute` can be routed", filename);
714
+ const config = raw && interactableToolConfig(raw, interactableToolImports);
715
+ if (config) {
716
+ if (target === "client") flags.keptRender = true;
717
+ else removeMember(config, "render");
718
+ nextProperties.push(entry);
719
+ continue;
720
+ }
721
+ throw new GenerativeCompileError("each tool must be an inline object literal (`name: { ... }`) or a compiler-visible toolkit spread / generative tool (e.g. `...defineMcpToolkit(...)`, `...baseToolkit`, `generative.present()`, or `unstable_interactableTool(...)`) so its `execute` can be routed", filename);
511
722
  }
512
723
  const execute = findMember(value, "execute");
513
724
  const isStub = execute ? executeIsStubTool(execute) : false;
@@ -517,7 +728,7 @@ function compileToolkit(object, target, instances, safeToolkitSpreads, flags, fi
517
728
  const hasRenderText = !!findMember(value, "renderText");
518
729
  if (type === "frontend" && !hasRender && !hasRenderText) throw new GenerativeCompileError("a frontend tool must declare a `render` or `renderText` (it has no server execute to show otherwise)", filename);
519
730
  if (type === "human" && !hasRender) throw new GenerativeCompileError("a human tool must declare a `render` so it can collect input", filename);
520
- if (type === "provider" && execute) applyProviderToolConfig(value, execute, filename);
731
+ if (type === "provider" && execute) applyProviderToolConfig(value, execute, t.isObjectProperty(entry) ? memberName(entry.key, entry.computed) : void 0, filename);
521
732
  if (isExternal) {
522
733
  if (!hasRender && !hasRenderText) throw new GenerativeCompileError("an external tool must declare a `render` or `renderText` (assistant-ui only renders calls for tools defined elsewhere)", filename);
523
734
  if (target === "server") continue;
@@ -539,7 +750,7 @@ function compileToolkit(object, target, instances, safeToolkitSpreads, flags, fi
539
750
  }
540
751
  object.properties = nextProperties;
541
752
  }
542
- function applyProviderToolConfig(object, execute, filename) {
753
+ function applyProviderToolConfig(object, execute, toolName, filename) {
543
754
  if (!t.isObjectProperty(execute) || !t.isCallExpression(execute.value) || execute.value.arguments.length !== 1 || !t.isObjectExpression(execute.value.arguments[0])) throw new GenerativeCompileError("`providerTool(...)` must receive an inline object literal", filename);
544
755
  const existingNames = new Set(object.properties.flatMap((prop) => {
545
756
  if (!t.isObjectProperty(prop) && !t.isObjectMethod(prop)) return [];
@@ -552,7 +763,7 @@ function applyProviderToolConfig(object, execute, filename) {
552
763
  const name = memberName(prop.key, prop.computed);
553
764
  if (!name) throw new GenerativeCompileError("`providerTool(...)` config can only contain static property names", filename);
554
765
  if (t.isFunctionExpression(prop.value) || t.isArrowFunctionExpression(prop.value)) throw new GenerativeCompileError("`providerTool(...)` config cannot contain function-valued properties", filename);
555
- if (existingNames.has(name) || configNames.has(name)) throw new GenerativeCompileError("`providerTool(...)` config cannot duplicate tool properties", filename);
766
+ if (existingNames.has(name) || configNames.has(name)) throw new GenerativeCompileError(`\`providerTool(...)\` config${toolName ? ` for "${toolName}"` : ""} duplicates "${name}"`, filename);
556
767
  configNames.add(name);
557
768
  object.properties.push(prop);
558
769
  }