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

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,UA2CK,cAAA;EA3CL;EA6CV,MAAA,EAAQ,MAAM;;EAEd,QAAA;EA/CkB;EAiDlB,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
@@ -28,16 +28,25 @@ const DISTRIBUTION_PACKAGES = [
28
28
  "@assistant-ui/react-native",
29
29
  "@assistant-ui/react-ink"
30
30
  ];
31
+ /** Package that exports the generative UI runtime split by export condition. */
32
+ const GENERATIVE_UI_PACKAGE = "@assistant-ui/react-generative-ui";
31
33
  /**
32
34
  * The class whose instances expose split-by-condition tools (`present()`,
33
35
  * `promptUser()`). A toolkit entry that calls a method on one of these passes
34
36
  * through untouched — the library, not this compiler, routes its halves.
35
37
  */
36
38
  const GENERATIVE_FACTORY = "JSONGenerativeUI";
39
+ /**
40
+ * The factory producing an interactable's complete tool entry. Unlike
41
+ * `JSONGenerativeUI`, its package has no per-target builds, so this compiler
42
+ * splits the inline config: the client keeps `render`, the server drops it.
43
+ * The factory's own `execute` is internal and client-safe (frontend tool).
44
+ */
45
+ const INTERACTABLE_TOOL_FACTORY = "unstable_interactableTool";
37
46
  /** Thrown when a `"use generative"` file violates an authoring constraint. */
38
47
  var GenerativeCompileError = class extends Error {
39
48
  constructor(message, filename) {
40
- super(`[assistant-ui/next]${filename ? ` ${filename}:` : ""} ${message}`);
49
+ super(`[assistant-ui/use-generative]${filename ? ` ${filename}:` : ""} ${message}`);
41
50
  this.name = "GenerativeCompileError";
42
51
  }
43
52
  };
@@ -56,7 +65,89 @@ function isGenerativeModule(code) {
56
65
  i = end + 2;
57
66
  } else break;
58
67
  }
59
- return code.startsWith(`"use generative"`, i) || code.startsWith(`'use generative'`, i);
68
+ const quote = code[i];
69
+ if (quote !== "\"" && quote !== "'") return false;
70
+ const directiveStart = i + 1;
71
+ if (!code.startsWith("use generative", directiveStart)) return false;
72
+ const directiveEnd = directiveStart + DIRECTIVE.length;
73
+ if (code[directiveEnd] !== quote) return false;
74
+ return hasDirectiveTerminator(code, directiveEnd + 1);
75
+ }
76
+ function hasDirectiveTerminator(code, start) {
77
+ let i = start;
78
+ let sawLineTerminator = false;
79
+ for (;;) {
80
+ if (i >= code.length) return true;
81
+ const char = code.charCodeAt(i);
82
+ if (isSemicolon(char)) return true;
83
+ if (isLineTerminator(char)) {
84
+ sawLineTerminator = true;
85
+ i++;
86
+ continue;
87
+ }
88
+ if (code.startsWith("//", i)) {
89
+ const lineEnd = nextLineTerminatorIndex(code, i + 2);
90
+ if (lineEnd === -1) return true;
91
+ sawLineTerminator = true;
92
+ i = lineEnd + 1;
93
+ continue;
94
+ }
95
+ if (code.startsWith("/*", i)) {
96
+ const end = code.indexOf("*/", i + 2);
97
+ if (end === -1) return false;
98
+ sawLineTerminator ||= containsLineTerminator(code, i + 2, end);
99
+ i = end + 2;
100
+ continue;
101
+ }
102
+ if (/\s/.test(code[i])) {
103
+ i++;
104
+ continue;
105
+ }
106
+ return sawLineTerminator && !startsExpressionContinuation(code, i);
107
+ }
108
+ }
109
+ function nextLineTerminatorIndex(code, start) {
110
+ for (let i = start; i < code.length; i++) if (isLineTerminator(code.charCodeAt(i))) return i;
111
+ return -1;
112
+ }
113
+ function containsLineTerminator(code, start, end) {
114
+ for (let i = start; i < end; i++) if (isLineTerminator(code.charCodeAt(i))) return true;
115
+ return false;
116
+ }
117
+ function startsExpressionContinuation(code, start) {
118
+ switch (code[start]) {
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 "[":
136
+ case "(":
137
+ case "`": return true;
138
+ }
139
+ return startsKeywordContinuation(code, start, "as") || startsKeywordContinuation(code, start, "in") || startsKeywordContinuation(code, start, "instanceof") || startsKeywordContinuation(code, start, "satisfies");
140
+ }
141
+ function startsKeywordContinuation(code, start, keyword) {
142
+ if (!code.startsWith(keyword, start)) return false;
143
+ const next = code[start + keyword.length];
144
+ return next === void 0 || !/[\p{ID_Continue}$]/u.test(next);
145
+ }
146
+ function isSemicolon(char) {
147
+ return char === 59;
148
+ }
149
+ function isLineTerminator(char) {
150
+ return char === 10 || char === 13 || char === 8232 || char === 8233;
60
151
  }
61
152
  /**
62
153
  * Rewrites a `"use generative"` module for a single build target, keeping only the
@@ -76,7 +167,8 @@ function compileGenerative(code, options) {
76
167
  ensureCompilerCompatibleWithCore(ast, filename);
77
168
  ensureDefaultExport(ast, filename);
78
169
  const generativeInstances = collectGenerativeInstances(ast);
79
- const safeToolkitSpreads = collectSafeToolkitSpreads(ast, filename);
170
+ const interactableToolImports = collectInteractableToolImports(ast);
171
+ const toolkitSpreadNames = collectToolkitSpreadNames(ast, filename, createToolkitNameContext());
80
172
  const flags = {
81
173
  keptRender: false,
82
174
  keptBackendExecute: false
@@ -93,7 +185,7 @@ function compileGenerative(code, options) {
93
185
  }
94
186
  if (t.isIdentifier(callee, { name: TOOLKIT_WRAPPER })) {
95
187
  if (!object) throw new GenerativeCompileError(`${TOOLKIT_WRAPPER}() takes an inline object literal of tools`, filename);
96
- compileToolkit(object, target, generativeInstances, safeToolkitSpreads, flags, filename);
188
+ compileToolkit(object, target, generativeInstances, interactableToolImports, toolkitSpreadNames, flags, filename);
97
189
  path.replaceWith(object);
98
190
  path.skip();
99
191
  }
@@ -234,17 +326,56 @@ function unwrapToCall(node, name) {
234
326
  */
235
327
  function collectGenerativeInstances(ast) {
236
328
  const names = /* @__PURE__ */ new Set();
329
+ const generativeFactories = collectGenerativeFactoryImports(ast);
237
330
  for (const statement of ast.program.body) {
238
- if (!t.isVariableDeclaration(statement)) continue;
239
- for (const declaration of statement.declarations) {
331
+ const variableDeclaration = t.isVariableDeclaration(statement) ? statement : t.isExportNamedDeclaration(statement) && t.isVariableDeclaration(statement.declaration) ? statement.declaration : null;
332
+ if (!variableDeclaration) continue;
333
+ for (const declaration of variableDeclaration.declarations) {
240
334
  const { id, init } = declaration;
241
- if (t.isIdentifier(id) && t.isNewExpression(init) && t.isIdentifier(init.callee, { name: GENERATIVE_FACTORY })) names.add(id.name);
335
+ if (t.isIdentifier(id) && t.isNewExpression(init) && t.isIdentifier(init.callee) && generativeFactories.has(init.callee.name)) names.add(id.name);
242
336
  }
243
337
  }
244
338
  return names;
245
339
  }
340
+ function collectGenerativeFactoryImports(ast) {
341
+ const names = /* @__PURE__ */ new Set();
342
+ for (const statement of ast.program.body) {
343
+ if (!t.isImportDeclaration(statement) || statement.source.value !== GENERATIVE_UI_PACKAGE) continue;
344
+ for (const specifier of statement.specifiers) if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported, { name: GENERATIVE_FACTORY })) names.add(specifier.local.name);
345
+ }
346
+ return names;
347
+ }
348
+ /**
349
+ * Collects the local names `unstable_interactableTool` is imported under from a
350
+ * distribution package, so toolkit entries calling it can be recognized
351
+ * (and a same-named local function can't smuggle an arbitrary call through).
352
+ */
353
+ function collectInteractableToolImports(ast) {
354
+ const names = /* @__PURE__ */ new Set();
355
+ for (const statement of ast.program.body) {
356
+ if (!t.isImportDeclaration(statement)) continue;
357
+ if (!packageNameFromSpecifier(statement.source.value)) continue;
358
+ for (const specifier of statement.specifiers) if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported, { name: INTERACTABLE_TOOL_FACTORY })) names.add(specifier.local.name);
359
+ }
360
+ return names;
361
+ }
362
+ function createToolkitNameContext() {
363
+ return {
364
+ importedToolkitNamesByFile: /* @__PURE__ */ new Map(),
365
+ resolvingImportedToolkitNames: /* @__PURE__ */ new Set()
366
+ };
367
+ }
246
368
  /**
247
- * Toolkit identifiers that are safe to spread into a `defineToolkit({ ... })`.
369
+ * The inline config of an `unstable_interactableTool({ ... })` toolkit entry, or `null`
370
+ * when the entry is some other expression.
371
+ */
372
+ function interactableToolConfig(value, imports) {
373
+ return t.isCallExpression(value) && t.isIdentifier(value.callee) && imports.has(value.callee.name) && t.isObjectExpression(value.arguments[0]) ? value.arguments[0] : null;
374
+ }
375
+ /**
376
+ * Toolkit identifiers that are safe to spread into a `defineToolkit({ ... })`,
377
+ * paired with the static tool names they contain. A `null` name list means the
378
+ * spread is safe, but its names are not statically known for duplicate checks.
248
379
  *
249
380
  * Two kinds qualify:
250
381
  *
@@ -257,30 +388,38 @@ function collectGenerativeInstances(ast) {
257
388
  * export crosses the generative-module boundary, so named imports don't
258
389
  * qualify — they would be `undefined` once that module is build-split.
259
390
  */
260
- function collectSafeToolkitSpreads(ast, filename) {
261
- const names = /* @__PURE__ */ new Set();
262
- const generativeBySource = /* @__PURE__ */ new Map();
391
+ function collectToolkitSpreadNames(ast, filename, context) {
392
+ const spreadNames = /* @__PURE__ */ new Map();
393
+ const localToolkitCalls = /* @__PURE__ */ new Map();
263
394
  for (const statement of ast.program.body) {
264
395
  if (t.isVariableDeclaration(statement)) {
265
396
  for (const declaration of statement.declarations) {
266
397
  const { id, init } = declaration;
267
- if (t.isIdentifier(id) && init && unwrapToToolkitCall(init)) names.add(id.name);
398
+ if (t.isIdentifier(id) && init) {
399
+ const toolkitCall = unwrapToToolkitCall(init);
400
+ if (toolkitCall) localToolkitCalls.set(id.name, toolkitCall);
401
+ }
268
402
  }
269
403
  continue;
270
404
  }
271
405
  if (t.isImportDeclaration(statement)) {
272
406
  const defaultSpecifier = statement.specifiers.find((specifier) => t.isImportDefaultSpecifier(specifier));
273
407
  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);
408
+ const names = getGenerativeImportToolkitNames(statement.source.value, filename, context);
409
+ if (names !== void 0) spreadNames.set(defaultSpecifier.local.name, names);
281
410
  }
282
411
  }
283
- return names;
412
+ const resolveLocal = (name) => {
413
+ if (spreadNames.has(name)) return spreadNames.get(name);
414
+ const call = localToolkitCalls.get(name);
415
+ if (!call) return void 0;
416
+ const object = t.isObjectExpression(call.arguments[0]) ? call.arguments[0] : null;
417
+ const publicNames = uniqueToolkitNames(object ? collectToolkitObjectNames(object, spreadNames) : null);
418
+ spreadNames.set(name, publicNames);
419
+ return publicNames;
420
+ };
421
+ for (const name of localToolkitCalls.keys()) resolveLocal(name);
422
+ return spreadNames;
284
423
  }
285
424
  const MODULE_EXTENSIONS = [
286
425
  ".ts",
@@ -293,29 +432,64 @@ const MODULE_EXTENSIONS = [
293
432
  ".cjs"
294
433
  ];
295
434
  /** Extensions a specifier may carry that actually map to a TS source file. */
296
- const REWRITABLE_JS_EXTENSIONS = new Set([
435
+ const REWRITABLE_JS_EXTENSIONS = /* @__PURE__ */ new Set([
297
436
  ".js",
298
437
  ".jsx",
299
438
  ".mjs",
300
439
  ".cjs"
301
440
  ]);
302
441
  /**
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.
442
+ * Reads the static tool names from the default export of an imported
443
+ * `"use generative"` module. Relative specifiers and `tsconfig` path aliases
444
+ * (e.g. `@/tools`) are resolved; anything else (a bare package, an unresolvable
445
+ * alias) is treated as non-generative, and thus an unsafe spread.
307
446
  */
308
- function isGenerativeImport(source, filename) {
447
+ function getGenerativeImportToolkitNames(source, filename, context) {
309
448
  const cleanFilename = cleanAbsoluteFilename(filename);
310
- if (!cleanFilename) return false;
449
+ if (!cleanFilename) return void 0;
311
450
  const resolved = resolveImportedModuleFile(source, cleanFilename);
312
- if (!resolved) return false;
451
+ if (!resolved) return void 0;
452
+ if (context.importedToolkitNamesByFile.has(resolved)) return context.importedToolkitNamesByFile.get(resolved);
453
+ if (context.resolvingImportedToolkitNames.has(resolved)) return null;
454
+ let code;
313
455
  try {
314
- return isGenerativeModule(readFileSync(resolved, "utf8"));
456
+ code = readFileSync(resolved, "utf8");
315
457
  } catch {
316
- return false;
458
+ context.importedToolkitNamesByFile.set(resolved, void 0);
459
+ return;
460
+ }
461
+ if (!isGenerativeModule(code)) {
462
+ context.importedToolkitNamesByFile.set(resolved, void 0);
463
+ return;
464
+ }
465
+ context.resolvingImportedToolkitNames.add(resolved);
466
+ try {
467
+ const names = getDefaultExportToolkitNames(parse(code, {
468
+ sourceType: "module",
469
+ plugins: [
470
+ "typescript",
471
+ "jsx",
472
+ "explicitResourceManagement"
473
+ ]
474
+ }), resolved, context);
475
+ context.importedToolkitNamesByFile.set(resolved, names);
476
+ return names;
477
+ } catch (error) {
478
+ if (error instanceof GenerativeCompileError) throw error;
479
+ context.importedToolkitNamesByFile.set(resolved, null);
480
+ return null;
481
+ } finally {
482
+ context.resolvingImportedToolkitNames.delete(resolved);
317
483
  }
318
484
  }
485
+ function getDefaultExportToolkitNames(ast, filename, context) {
486
+ const def = ast.program.body.find((stmt) => t.isExportDefaultDeclaration(stmt));
487
+ if (!def) return null;
488
+ const toolkitCall = unwrapToToolkitCall(def.declaration);
489
+ if (!toolkitCall || !t.isObjectExpression(toolkitCall.arguments[0])) return null;
490
+ const spreadNames = collectToolkitSpreadNames(ast, filename, context);
491
+ return uniqueToolkitNames(collectToolkitObjectNames(toolkitCall.arguments[0], spreadNames));
492
+ }
319
493
  /** Resolves an import specifier (relative or `tsconfig`-aliased) to a file on disk. */
320
494
  function resolveImportedModuleFile(source, fromFilename) {
321
495
  if (source.startsWith(".")) return resolveModuleFileAtPath(nodePath.resolve(nodePath.dirname(fromFilename), source));
@@ -456,13 +630,64 @@ function parseJsonc(text) {
456
630
  function unwrapToToolkitCall(node) {
457
631
  return unwrapToCall(node, TOOLKIT_WRAPPER) ?? unwrapToCall(node, MCP_TOOLKIT_WRAPPER);
458
632
  }
459
- function isSafeToolkitSpread(entry, safeToolkitSpreads) {
460
- if (t.isIdentifier(entry.argument)) return safeToolkitSpreads.has(entry.argument.name);
633
+ function collectToolkitObjectNames(object, toolkitSpreadNames) {
634
+ const names = [];
635
+ for (const entry of object.properties) {
636
+ const entryNames = toolkitEntryNames(entry, toolkitSpreadNames);
637
+ if (!entryNames) return null;
638
+ names.push(...entryNames);
639
+ }
640
+ return names;
641
+ }
642
+ function uniqueToolkitNames(names) {
643
+ return names ? [...new Set(names)] : names;
644
+ }
645
+ function toolkitEntryNames(entry, toolkitSpreadNames) {
646
+ if (t.isSpreadElement(entry)) {
647
+ if (t.isIdentifier(entry.argument)) return toolkitSpreadNames.get(entry.argument.name) ?? null;
648
+ const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
649
+ if (directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0])) return collectToolkitObjectNames(directMcpToolkit.arguments[0], toolkitSpreadNames);
650
+ return null;
651
+ }
652
+ if (t.isObjectProperty(entry) || t.isObjectMethod(entry)) {
653
+ const name = memberName(entry.key, entry.computed);
654
+ return name ? [name] : [];
655
+ }
656
+ return [];
657
+ }
658
+ function warnDuplicateToolkitNames(object, toolkitSpreadNames, filename) {
659
+ const names = collectToolkitObjectNames(object, toolkitSpreadNames);
660
+ if (!names) return;
661
+ const seen = /* @__PURE__ */ new Set();
662
+ const warned = /* @__PURE__ */ new Set();
663
+ for (const name of names) {
664
+ if (seen.has(name)) {
665
+ if (!warned.has(name)) {
666
+ console.warn(new GenerativeCompileError(`Duplicate tool name "${name}" while composing toolkits. JavaScript object spread keeps the last definition.`, filename).message);
667
+ warned.add(name);
668
+ }
669
+ continue;
670
+ }
671
+ seen.add(name);
672
+ }
673
+ }
674
+ function isSafeToolkitSpread(entry, toolkitSpreadNames) {
675
+ if (t.isIdentifier(entry.argument)) return toolkitSpreadNames.has(entry.argument.name);
461
676
  const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
462
677
  return !!directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0]);
463
678
  }
679
+ function genericUnsafeToolkitEntryMessage() {
680
+ return "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";
681
+ }
682
+ function describeUnsafeToolkitEntry(entry) {
683
+ if (!t.isObjectProperty(entry)) return genericUnsafeToolkitEntryMessage();
684
+ const toolName = memberName(entry.key, entry.computed);
685
+ const raw = entryRawValue(entry);
686
+ if (!toolName || !raw) return genericUnsafeToolkitEntryMessage();
687
+ return `tool "${toolName}" cannot be \`${generate(raw).code}\`; use an inline object literal (\`${toolName}: { ... }\`) or a compiler-visible toolkit spread / generative tool so its \`execute\` can be routed`;
688
+ }
464
689
  /** The `JSONGenerativeUI` methods that produce a split-by-condition tool. */
465
- const GENERATIVE_TOOL_METHODS = new Set(["present", "promptUser"]);
690
+ const GENERATIVE_TOOL_METHODS = /* @__PURE__ */ new Set(["present", "promptUser"]);
466
691
  /**
467
692
  * Whether a toolkit entry's value is a call to a tool-producing method on a
468
693
  * collected `JSONGenerativeUI` instance (`generative.present()`), which passes
@@ -493,12 +718,13 @@ function compileComponents(object, target, flags, filename) {
493
718
  * `generative.present()` passes through, the library having already split it.
494
719
  * Mutates the object in place and records outcomes in {@link TargetFlags}.
495
720
  */
496
- function compileToolkit(object, target, instances, safeToolkitSpreads, flags, filename) {
721
+ function compileToolkit(object, target, instances, interactableToolImports, toolkitSpreadNames, flags, filename) {
722
+ if (target === "client") warnDuplicateToolkitNames(object, toolkitSpreadNames, filename);
497
723
  const nextProperties = [];
498
724
  for (const entry of object.properties) {
499
725
  const value = entryValue(entry);
500
726
  if (!value) {
501
- if (t.isSpreadElement(entry) && isSafeToolkitSpread(entry, safeToolkitSpreads)) {
727
+ if (t.isSpreadElement(entry) && isSafeToolkitSpread(entry, toolkitSpreadNames)) {
502
728
  nextProperties.push(entry);
503
729
  continue;
504
730
  }
@@ -507,19 +733,27 @@ function compileToolkit(object, target, instances, safeToolkitSpreads, flags, fi
507
733
  nextProperties.push(entry);
508
734
  continue;
509
735
  }
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);
736
+ const config = raw && interactableToolConfig(raw, interactableToolImports);
737
+ if (config) {
738
+ if (target === "client") flags.keptRender = true;
739
+ else removeMember(config, "render");
740
+ nextProperties.push(entry);
741
+ continue;
742
+ }
743
+ throw new GenerativeCompileError(describeUnsafeToolkitEntry(entry), filename);
511
744
  }
745
+ const toolName = t.isObjectProperty(entry) ? memberName(entry.key, entry.computed) : void 0;
512
746
  const execute = findMember(value, "execute");
513
747
  const isStub = execute ? executeIsStubTool(execute) : false;
514
748
  const isExternal = execute ? executeIsExternalTool(execute) : false;
515
- const type = inferToolType(value, filename);
749
+ const type = inferToolType(value, toolName, filename);
516
750
  const hasRender = !!findMember(value, "render");
517
751
  const hasRenderText = !!findMember(value, "renderText");
518
- 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
- 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);
752
+ if (type === "frontend" && !hasRender && !hasRenderText) throw new GenerativeCompileError(`${typedToolSubject("frontend", toolName)} must declare a \`render\` or \`renderText\` (it has no server execute to show otherwise)`, filename);
753
+ if (type === "human" && !hasRender) throw new GenerativeCompileError(`${typedToolSubject("human", toolName)} must declare a \`render\` so it can collect input`, filename);
754
+ if (type === "provider" && execute) applyProviderToolConfig(value, execute, toolName, filename);
521
755
  if (isExternal) {
522
- 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);
756
+ if (!hasRender && !hasRenderText) throw new GenerativeCompileError(`${typedToolSubject("external", toolName)} must declare a \`render\` or \`renderText\` (assistant-ui only renders calls for tools defined elsewhere)`, filename);
523
757
  if (target === "server") continue;
524
758
  stripExternalToolMetadata(value);
525
759
  }
@@ -539,7 +773,7 @@ function compileToolkit(object, target, instances, safeToolkitSpreads, flags, fi
539
773
  }
540
774
  object.properties = nextProperties;
541
775
  }
542
- function applyProviderToolConfig(object, execute, filename) {
776
+ function applyProviderToolConfig(object, execute, toolName, filename) {
543
777
  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
778
  const existingNames = new Set(object.properties.flatMap((prop) => {
545
779
  if (!t.isObjectProperty(prop) && !t.isObjectMethod(prop)) return [];
@@ -552,7 +786,7 @@ function applyProviderToolConfig(object, execute, filename) {
552
786
  const name = memberName(prop.key, prop.computed);
553
787
  if (!name) throw new GenerativeCompileError("`providerTool(...)` config can only contain static property names", filename);
554
788
  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);
789
+ if (existingNames.has(name) || configNames.has(name)) throw new GenerativeCompileError(`\`providerTool(...)\` config${toolName ? ` for "${toolName}"` : ""} duplicates "${name}"`, filename);
556
790
  configNames.add(name);
557
791
  object.properties.push(prop);
558
792
  }
@@ -615,15 +849,22 @@ function stripUseClient(member) {
615
849
  * The loader writes the result back as a `type` field (see {@link setToolType})
616
850
  * so the runtime keeps it.
617
851
  */
618
- function inferToolType(object, filename) {
852
+ function inferToolType(object, toolName, filename) {
619
853
  const execute = findMember(object, "execute");
620
- if (!execute) throw new GenerativeCompileError("every tool must declare an `execute`; use `humanTool()` for a human-in-the-loop tool", filename);
854
+ if (!execute) throw new GenerativeCompileError(`${toolSubject(toolName)} must declare an \`execute\`; use \`humanTool()\` for a human-in-the-loop tool`, filename);
621
855
  if (executeIsHumanTool(execute)) return "human";
622
856
  if (executeIsProviderTool(execute)) return "provider";
623
857
  if (executeIsStubTool(execute)) return "frontend";
624
858
  if (executeIsExternalTool(execute)) return "backend";
625
859
  return executeIsClient(execute) ? "frontend" : "backend";
626
860
  }
861
+ function toolSubject(toolName) {
862
+ return toolName ? `tool "${toolName}"` : "every tool";
863
+ }
864
+ function typedToolSubject(type, toolName) {
865
+ if (toolName) return `${type} tool "${toolName}"`;
866
+ return `${/^[aeiou]/i.test(type) ? "an" : "a"} ${type} tool`;
867
+ }
627
868
  function stripExternalToolMetadata(object) {
628
869
  removeMember(object, "description");
629
870
  removeMember(object, "parameters");