@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.
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +286 -45
- package/dist/compile.js.map +1 -1
- package/dist/package.js +1 -1
- package/package.json +9 -9
- package/src/compile.test.ts +477 -9
- package/src/compile.ts +474 -47
package/src/compile.ts
CHANGED
|
@@ -37,12 +37,21 @@ const DISTRIBUTION_PACKAGES = [
|
|
|
37
37
|
"@assistant-ui/react-native",
|
|
38
38
|
"@assistant-ui/react-ink",
|
|
39
39
|
] as const;
|
|
40
|
+
/** Package that exports the generative UI runtime split by export condition. */
|
|
41
|
+
const GENERATIVE_UI_PACKAGE = "@assistant-ui/react-generative-ui";
|
|
40
42
|
/**
|
|
41
43
|
* The class whose instances expose split-by-condition tools (`present()`,
|
|
42
44
|
* `promptUser()`). A toolkit entry that calls a method on one of these passes
|
|
43
45
|
* through untouched — the library, not this compiler, routes its halves.
|
|
44
46
|
*/
|
|
45
47
|
const GENERATIVE_FACTORY = "JSONGenerativeUI";
|
|
48
|
+
/**
|
|
49
|
+
* The factory producing an interactable's complete tool entry. Unlike
|
|
50
|
+
* `JSONGenerativeUI`, its package has no per-target builds, so this compiler
|
|
51
|
+
* splits the inline config: the client keeps `render`, the server drops it.
|
|
52
|
+
* The factory's own `execute` is internal and client-safe (frontend tool).
|
|
53
|
+
*/
|
|
54
|
+
const INTERACTABLE_TOOL_FACTORY = "unstable_interactableTool";
|
|
46
55
|
|
|
47
56
|
/** Mutable per-build outcomes the toolkit pass reports back for directive/guard injection. */
|
|
48
57
|
interface TargetFlags {
|
|
@@ -78,7 +87,9 @@ export interface CompileResult {
|
|
|
78
87
|
/** Thrown when a `"use generative"` file violates an authoring constraint. */
|
|
79
88
|
export class GenerativeCompileError extends Error {
|
|
80
89
|
constructor(message: string, filename?: string) {
|
|
81
|
-
super(
|
|
90
|
+
super(
|
|
91
|
+
`[assistant-ui/use-generative]${filename ? ` ${filename}:` : ""} ${message}`,
|
|
92
|
+
);
|
|
82
93
|
this.name = "GenerativeCompileError";
|
|
83
94
|
}
|
|
84
95
|
}
|
|
@@ -103,11 +114,126 @@ export function isGenerativeModule(code: string): boolean {
|
|
|
103
114
|
break;
|
|
104
115
|
}
|
|
105
116
|
}
|
|
117
|
+
|
|
118
|
+
const quote = code[i];
|
|
119
|
+
if (quote !== '"' && quote !== "'") return false;
|
|
120
|
+
|
|
121
|
+
const directiveStart = i + 1;
|
|
122
|
+
if (!code.startsWith(DIRECTIVE, directiveStart)) return false;
|
|
123
|
+
|
|
124
|
+
const directiveEnd = directiveStart + DIRECTIVE.length;
|
|
125
|
+
if (code[directiveEnd] !== quote) return false;
|
|
126
|
+
|
|
127
|
+
return hasDirectiveTerminator(code, directiveEnd + 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasDirectiveTerminator(code: string, start: number): boolean {
|
|
131
|
+
let i = start;
|
|
132
|
+
let sawLineTerminator = false;
|
|
133
|
+
for (;;) {
|
|
134
|
+
if (i >= code.length) return true;
|
|
135
|
+
|
|
136
|
+
const char = code.charCodeAt(i);
|
|
137
|
+
if (isSemicolon(char)) return true;
|
|
138
|
+
if (isLineTerminator(char)) {
|
|
139
|
+
sawLineTerminator = true;
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (code.startsWith("//", i)) {
|
|
145
|
+
const lineEnd = nextLineTerminatorIndex(code, i + 2);
|
|
146
|
+
if (lineEnd === -1) return true;
|
|
147
|
+
sawLineTerminator = true;
|
|
148
|
+
i = lineEnd + 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (code.startsWith("/*", i)) {
|
|
153
|
+
const end = code.indexOf("*/", i + 2);
|
|
154
|
+
if (end === -1) return false;
|
|
155
|
+
sawLineTerminator ||= containsLineTerminator(code, i + 2, end);
|
|
156
|
+
i = end + 2;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (/\s/.test(code[i]!)) {
|
|
161
|
+
i++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return sawLineTerminator && !startsExpressionContinuation(code, i);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nextLineTerminatorIndex(code: string, start: number): number {
|
|
170
|
+
for (let i = start; i < code.length; i++) {
|
|
171
|
+
if (isLineTerminator(code.charCodeAt(i))) return i;
|
|
172
|
+
}
|
|
173
|
+
return -1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function containsLineTerminator(
|
|
177
|
+
code: string,
|
|
178
|
+
start: number,
|
|
179
|
+
end: number,
|
|
180
|
+
): boolean {
|
|
181
|
+
for (let i = start; i < end; i++) {
|
|
182
|
+
if (isLineTerminator(code.charCodeAt(i))) return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function startsExpressionContinuation(code: string, start: number): boolean {
|
|
188
|
+
switch (code[start]) {
|
|
189
|
+
case ".":
|
|
190
|
+
case "+":
|
|
191
|
+
case "-":
|
|
192
|
+
case "*":
|
|
193
|
+
case "/":
|
|
194
|
+
case "%":
|
|
195
|
+
case "<":
|
|
196
|
+
case ">":
|
|
197
|
+
case "=":
|
|
198
|
+
case "!":
|
|
199
|
+
case "&":
|
|
200
|
+
case "|":
|
|
201
|
+
case "^":
|
|
202
|
+
case "?":
|
|
203
|
+
case ":":
|
|
204
|
+
case ",":
|
|
205
|
+
case "[":
|
|
206
|
+
case "(":
|
|
207
|
+
case "`":
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
106
211
|
return (
|
|
107
|
-
code
|
|
212
|
+
startsKeywordContinuation(code, start, "as") ||
|
|
213
|
+
startsKeywordContinuation(code, start, "in") ||
|
|
214
|
+
startsKeywordContinuation(code, start, "instanceof") ||
|
|
215
|
+
startsKeywordContinuation(code, start, "satisfies")
|
|
108
216
|
);
|
|
109
217
|
}
|
|
110
218
|
|
|
219
|
+
function startsKeywordContinuation(
|
|
220
|
+
code: string,
|
|
221
|
+
start: number,
|
|
222
|
+
keyword: string,
|
|
223
|
+
): boolean {
|
|
224
|
+
if (!code.startsWith(keyword, start)) return false;
|
|
225
|
+
const next = code[start + keyword.length];
|
|
226
|
+
return next === undefined || !/[\p{ID_Continue}$]/u.test(next);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isSemicolon(char: number): boolean {
|
|
230
|
+
return char === 59;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isLineTerminator(char: number): boolean {
|
|
234
|
+
return char === 10 || char === 13 || char === 0x2028 || char === 0x2029;
|
|
235
|
+
}
|
|
236
|
+
|
|
111
237
|
/**
|
|
112
238
|
* Rewrites a `"use generative"` module for a single build target, keeping only the
|
|
113
239
|
* regions that target needs and pruning the imports the dropped regions used.
|
|
@@ -140,7 +266,12 @@ export function compileGenerative(
|
|
|
140
266
|
// such entries pass through.
|
|
141
267
|
ensureDefaultExport(ast, filename);
|
|
142
268
|
const generativeInstances = collectGenerativeInstances(ast);
|
|
143
|
-
const
|
|
269
|
+
const interactableToolImports = collectInteractableToolImports(ast);
|
|
270
|
+
const toolkitSpreadNames = collectToolkitSpreadNames(
|
|
271
|
+
ast,
|
|
272
|
+
filename,
|
|
273
|
+
createToolkitNameContext(),
|
|
274
|
+
);
|
|
144
275
|
|
|
145
276
|
const flags: TargetFlags = { keptRender: false, keptBackendExecute: false };
|
|
146
277
|
|
|
@@ -177,7 +308,8 @@ export function compileGenerative(
|
|
|
177
308
|
object,
|
|
178
309
|
target,
|
|
179
310
|
generativeInstances,
|
|
180
|
-
|
|
311
|
+
interactableToolImports,
|
|
312
|
+
toolkitSpreadNames,
|
|
181
313
|
flags,
|
|
182
314
|
filename,
|
|
183
315
|
);
|
|
@@ -470,14 +602,22 @@ function unwrapToCall(node: t.Node, name: string): t.CallExpression | null {
|
|
|
470
602
|
*/
|
|
471
603
|
function collectGenerativeInstances(ast: t.File): Set<string> {
|
|
472
604
|
const names = new Set<string>();
|
|
605
|
+
const generativeFactories = collectGenerativeFactoryImports(ast);
|
|
473
606
|
for (const statement of ast.program.body) {
|
|
474
|
-
|
|
475
|
-
|
|
607
|
+
const variableDeclaration = t.isVariableDeclaration(statement)
|
|
608
|
+
? statement
|
|
609
|
+
: t.isExportNamedDeclaration(statement) &&
|
|
610
|
+
t.isVariableDeclaration(statement.declaration)
|
|
611
|
+
? statement.declaration
|
|
612
|
+
: null;
|
|
613
|
+
if (!variableDeclaration) continue;
|
|
614
|
+
for (const declaration of variableDeclaration.declarations) {
|
|
476
615
|
const { id, init } = declaration;
|
|
477
616
|
if (
|
|
478
617
|
t.isIdentifier(id) &&
|
|
479
618
|
t.isNewExpression(init) &&
|
|
480
|
-
t.isIdentifier(init.callee
|
|
619
|
+
t.isIdentifier(init.callee) &&
|
|
620
|
+
generativeFactories.has(init.callee.name)
|
|
481
621
|
) {
|
|
482
622
|
names.add(id.name);
|
|
483
623
|
}
|
|
@@ -486,8 +626,85 @@ function collectGenerativeInstances(ast: t.File): Set<string> {
|
|
|
486
626
|
return names;
|
|
487
627
|
}
|
|
488
628
|
|
|
629
|
+
function collectGenerativeFactoryImports(ast: t.File): Set<string> {
|
|
630
|
+
const names = new Set<string>();
|
|
631
|
+
for (const statement of ast.program.body) {
|
|
632
|
+
if (
|
|
633
|
+
!t.isImportDeclaration(statement) ||
|
|
634
|
+
statement.source.value !== GENERATIVE_UI_PACKAGE
|
|
635
|
+
) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
for (const specifier of statement.specifiers) {
|
|
640
|
+
if (
|
|
641
|
+
t.isImportSpecifier(specifier) &&
|
|
642
|
+
t.isIdentifier(specifier.imported, { name: GENERATIVE_FACTORY })
|
|
643
|
+
) {
|
|
644
|
+
names.add(specifier.local.name);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return names;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
type ToolkitStaticNames = readonly string[] | null;
|
|
652
|
+
type ToolkitSpreadNames = Map<string, ToolkitStaticNames>;
|
|
653
|
+
|
|
654
|
+
interface ToolkitNameContext {
|
|
655
|
+
importedToolkitNamesByFile: Map<string, ToolkitStaticNames | undefined>;
|
|
656
|
+
resolvingImportedToolkitNames: Set<string>;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Collects the local names `unstable_interactableTool` is imported under from a
|
|
661
|
+
* distribution package, so toolkit entries calling it can be recognized
|
|
662
|
+
* (and a same-named local function can't smuggle an arbitrary call through).
|
|
663
|
+
*/
|
|
664
|
+
function collectInteractableToolImports(ast: t.File): Set<string> {
|
|
665
|
+
const names = new Set<string>();
|
|
666
|
+
for (const statement of ast.program.body) {
|
|
667
|
+
if (!t.isImportDeclaration(statement)) continue;
|
|
668
|
+
if (!packageNameFromSpecifier(statement.source.value)) continue;
|
|
669
|
+
for (const specifier of statement.specifiers) {
|
|
670
|
+
if (
|
|
671
|
+
t.isImportSpecifier(specifier) &&
|
|
672
|
+
t.isIdentifier(specifier.imported, { name: INTERACTABLE_TOOL_FACTORY })
|
|
673
|
+
) {
|
|
674
|
+
names.add(specifier.local.name);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return names;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function createToolkitNameContext(): ToolkitNameContext {
|
|
682
|
+
return {
|
|
683
|
+
importedToolkitNamesByFile: new Map(),
|
|
684
|
+
resolvingImportedToolkitNames: new Set(),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
489
688
|
/**
|
|
490
|
-
*
|
|
689
|
+
* The inline config of an `unstable_interactableTool({ ... })` toolkit entry, or `null`
|
|
690
|
+
* when the entry is some other expression.
|
|
691
|
+
*/
|
|
692
|
+
function interactableToolConfig(
|
|
693
|
+
value: t.Node,
|
|
694
|
+
imports: Set<string>,
|
|
695
|
+
): t.ObjectExpression | null {
|
|
696
|
+
return t.isCallExpression(value) &&
|
|
697
|
+
t.isIdentifier(value.callee) &&
|
|
698
|
+
imports.has(value.callee.name) &&
|
|
699
|
+
t.isObjectExpression(value.arguments[0])
|
|
700
|
+
? value.arguments[0]
|
|
701
|
+
: null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Toolkit identifiers that are safe to spread into a `defineToolkit({ ... })`,
|
|
706
|
+
* paired with the static tool names they contain. A `null` name list means the
|
|
707
|
+
* spread is safe, but its names are not statically known for duplicate checks.
|
|
491
708
|
*
|
|
492
709
|
* Two kinds qualify:
|
|
493
710
|
*
|
|
@@ -500,19 +717,21 @@ function collectGenerativeInstances(ast: t.File): Set<string> {
|
|
|
500
717
|
* export crosses the generative-module boundary, so named imports don't
|
|
501
718
|
* qualify — they would be `undefined` once that module is build-split.
|
|
502
719
|
*/
|
|
503
|
-
function
|
|
720
|
+
function collectToolkitSpreadNames(
|
|
504
721
|
ast: t.File,
|
|
505
722
|
filename: string | undefined,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
723
|
+
context: ToolkitNameContext,
|
|
724
|
+
): ToolkitSpreadNames {
|
|
725
|
+
const spreadNames: ToolkitSpreadNames = new Map();
|
|
726
|
+
const localToolkitCalls = new Map<string, t.CallExpression>();
|
|
509
727
|
|
|
510
728
|
for (const statement of ast.program.body) {
|
|
511
729
|
if (t.isVariableDeclaration(statement)) {
|
|
512
730
|
for (const declaration of statement.declarations) {
|
|
513
731
|
const { id, init } = declaration;
|
|
514
|
-
if (t.isIdentifier(id) && init
|
|
515
|
-
|
|
732
|
+
if (t.isIdentifier(id) && init) {
|
|
733
|
+
const toolkitCall = unwrapToToolkitCall(init);
|
|
734
|
+
if (toolkitCall) localToolkitCalls.set(id.name, toolkitCall);
|
|
516
735
|
}
|
|
517
736
|
}
|
|
518
737
|
continue;
|
|
@@ -525,17 +744,40 @@ function collectSafeToolkitSpreads(
|
|
|
525
744
|
);
|
|
526
745
|
if (!defaultSpecifier) continue;
|
|
527
746
|
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
747
|
+
const names = getGenerativeImportToolkitNames(
|
|
748
|
+
statement.source.value,
|
|
749
|
+
filename,
|
|
750
|
+
context,
|
|
751
|
+
);
|
|
752
|
+
if (names !== undefined) {
|
|
753
|
+
spreadNames.set(defaultSpecifier.local.name, names);
|
|
533
754
|
}
|
|
534
|
-
if (isGenerative) names.add(defaultSpecifier.local.name);
|
|
535
755
|
}
|
|
536
756
|
}
|
|
537
757
|
|
|
538
|
-
|
|
758
|
+
const resolveLocal = (name: string): ToolkitStaticNames | undefined => {
|
|
759
|
+
if (spreadNames.has(name)) return spreadNames.get(name);
|
|
760
|
+
|
|
761
|
+
const call = localToolkitCalls.get(name);
|
|
762
|
+
if (!call) return undefined;
|
|
763
|
+
|
|
764
|
+
const object = t.isObjectExpression(call.arguments[0])
|
|
765
|
+
? call.arguments[0]
|
|
766
|
+
: null;
|
|
767
|
+
const names = object
|
|
768
|
+
? collectToolkitObjectNames(object, spreadNames)
|
|
769
|
+
: null;
|
|
770
|
+
const publicNames = uniqueToolkitNames(names);
|
|
771
|
+
|
|
772
|
+
spreadNames.set(name, publicNames);
|
|
773
|
+
return publicNames;
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
for (const name of localToolkitCalls.keys()) {
|
|
777
|
+
resolveLocal(name);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return spreadNames;
|
|
539
781
|
}
|
|
540
782
|
|
|
541
783
|
const MODULE_EXTENSIONS = [
|
|
@@ -553,26 +795,78 @@ const MODULE_EXTENSIONS = [
|
|
|
553
795
|
const REWRITABLE_JS_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs"]);
|
|
554
796
|
|
|
555
797
|
/**
|
|
556
|
-
*
|
|
557
|
-
* Relative specifiers and `tsconfig` path aliases
|
|
558
|
-
* anything else (a bare package, an unresolvable
|
|
559
|
-
* non-generative, and thus an unsafe spread.
|
|
798
|
+
* Reads the static tool names from the default export of an imported
|
|
799
|
+
* `"use generative"` module. Relative specifiers and `tsconfig` path aliases
|
|
800
|
+
* (e.g. `@/tools`) are resolved; anything else (a bare package, an unresolvable
|
|
801
|
+
* alias) is treated as non-generative, and thus an unsafe spread.
|
|
560
802
|
*/
|
|
561
|
-
function
|
|
803
|
+
function getGenerativeImportToolkitNames(
|
|
562
804
|
source: string,
|
|
563
805
|
filename: string | undefined,
|
|
564
|
-
|
|
806
|
+
context: ToolkitNameContext,
|
|
807
|
+
): ToolkitStaticNames | undefined {
|
|
565
808
|
const cleanFilename = cleanAbsoluteFilename(filename);
|
|
566
|
-
if (!cleanFilename) return
|
|
809
|
+
if (!cleanFilename) return undefined;
|
|
567
810
|
|
|
568
811
|
const resolved = resolveImportedModuleFile(source, cleanFilename);
|
|
569
|
-
if (!resolved) return
|
|
812
|
+
if (!resolved) return undefined;
|
|
813
|
+
|
|
814
|
+
if (context.importedToolkitNamesByFile.has(resolved)) {
|
|
815
|
+
return context.importedToolkitNamesByFile.get(resolved);
|
|
816
|
+
}
|
|
817
|
+
if (context.resolvingImportedToolkitNames.has(resolved)) return null;
|
|
570
818
|
|
|
819
|
+
let code: string;
|
|
571
820
|
try {
|
|
572
|
-
|
|
821
|
+
code = readFileSync(resolved, "utf8");
|
|
573
822
|
} catch {
|
|
574
|
-
|
|
823
|
+
context.importedToolkitNamesByFile.set(resolved, undefined);
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!isGenerativeModule(code)) {
|
|
828
|
+
context.importedToolkitNamesByFile.set(resolved, undefined);
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
context.resolvingImportedToolkitNames.add(resolved);
|
|
833
|
+
try {
|
|
834
|
+
const importedAst = parse(code, {
|
|
835
|
+
sourceType: "module",
|
|
836
|
+
plugins: ["typescript", "jsx", "explicitResourceManagement"],
|
|
837
|
+
});
|
|
838
|
+
const names = getDefaultExportToolkitNames(importedAst, resolved, context);
|
|
839
|
+
context.importedToolkitNamesByFile.set(resolved, names);
|
|
840
|
+
return names;
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (error instanceof GenerativeCompileError) throw error;
|
|
843
|
+
context.importedToolkitNamesByFile.set(resolved, null);
|
|
844
|
+
return null;
|
|
845
|
+
} finally {
|
|
846
|
+
context.resolvingImportedToolkitNames.delete(resolved);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function getDefaultExportToolkitNames(
|
|
851
|
+
ast: t.File,
|
|
852
|
+
filename: string | undefined,
|
|
853
|
+
context: ToolkitNameContext,
|
|
854
|
+
): ToolkitStaticNames {
|
|
855
|
+
const def = ast.program.body.find(
|
|
856
|
+
(stmt): stmt is t.ExportDefaultDeclaration =>
|
|
857
|
+
t.isExportDefaultDeclaration(stmt),
|
|
858
|
+
);
|
|
859
|
+
if (!def) return null;
|
|
860
|
+
|
|
861
|
+
const toolkitCall = unwrapToToolkitCall(def.declaration);
|
|
862
|
+
if (!toolkitCall || !t.isObjectExpression(toolkitCall.arguments[0])) {
|
|
863
|
+
return null;
|
|
575
864
|
}
|
|
865
|
+
|
|
866
|
+
const spreadNames = collectToolkitSpreadNames(ast, filename, context);
|
|
867
|
+
return uniqueToolkitNames(
|
|
868
|
+
collectToolkitObjectNames(toolkitCall.arguments[0], spreadNames),
|
|
869
|
+
);
|
|
576
870
|
}
|
|
577
871
|
|
|
578
872
|
/** Resolves an import specifier (relative or `tsconfig`-aliased) to a file on disk. */
|
|
@@ -797,12 +1091,90 @@ function unwrapToToolkitCall(node: t.Node): t.CallExpression | null {
|
|
|
797
1091
|
);
|
|
798
1092
|
}
|
|
799
1093
|
|
|
1094
|
+
function collectToolkitObjectNames(
|
|
1095
|
+
object: t.ObjectExpression,
|
|
1096
|
+
toolkitSpreadNames: ToolkitSpreadNames,
|
|
1097
|
+
): ToolkitStaticNames {
|
|
1098
|
+
const names: string[] = [];
|
|
1099
|
+
|
|
1100
|
+
for (const entry of object.properties) {
|
|
1101
|
+
const entryNames = toolkitEntryNames(entry, toolkitSpreadNames);
|
|
1102
|
+
if (!entryNames) return null;
|
|
1103
|
+
names.push(...entryNames);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return names;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function uniqueToolkitNames(names: ToolkitStaticNames): ToolkitStaticNames {
|
|
1110
|
+
return names ? [...new Set(names)] : names;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function toolkitEntryNames(
|
|
1114
|
+
entry: t.ObjectExpression["properties"][number],
|
|
1115
|
+
toolkitSpreadNames: ToolkitSpreadNames,
|
|
1116
|
+
): ToolkitStaticNames {
|
|
1117
|
+
if (t.isSpreadElement(entry)) {
|
|
1118
|
+
if (t.isIdentifier(entry.argument)) {
|
|
1119
|
+
return toolkitSpreadNames.get(entry.argument.name) ?? null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
|
|
1123
|
+
if (
|
|
1124
|
+
directMcpToolkit &&
|
|
1125
|
+
t.isObjectExpression(directMcpToolkit.arguments[0])
|
|
1126
|
+
) {
|
|
1127
|
+
return collectToolkitObjectNames(
|
|
1128
|
+
directMcpToolkit.arguments[0],
|
|
1129
|
+
toolkitSpreadNames,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (t.isObjectProperty(entry) || t.isObjectMethod(entry)) {
|
|
1137
|
+
const name = memberName(entry.key, entry.computed);
|
|
1138
|
+
return name ? [name] : [];
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return [];
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function warnDuplicateToolkitNames(
|
|
1145
|
+
object: t.ObjectExpression,
|
|
1146
|
+
toolkitSpreadNames: ToolkitSpreadNames,
|
|
1147
|
+
filename: string | undefined,
|
|
1148
|
+
): void {
|
|
1149
|
+
const names = collectToolkitObjectNames(object, toolkitSpreadNames);
|
|
1150
|
+
if (!names) return;
|
|
1151
|
+
|
|
1152
|
+
const seen = new Set<string>();
|
|
1153
|
+
const warned = new Set<string>();
|
|
1154
|
+
for (const name of names) {
|
|
1155
|
+
if (seen.has(name)) {
|
|
1156
|
+
if (!warned.has(name)) {
|
|
1157
|
+
console.warn(
|
|
1158
|
+
new GenerativeCompileError(
|
|
1159
|
+
`Duplicate tool name "${name}" while composing toolkits. ` +
|
|
1160
|
+
"JavaScript object spread keeps the last definition.",
|
|
1161
|
+
filename,
|
|
1162
|
+
).message,
|
|
1163
|
+
);
|
|
1164
|
+
warned.add(name);
|
|
1165
|
+
}
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
seen.add(name);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
800
1172
|
function isSafeToolkitSpread(
|
|
801
1173
|
entry: t.SpreadElement,
|
|
802
|
-
|
|
1174
|
+
toolkitSpreadNames: ToolkitSpreadNames,
|
|
803
1175
|
): boolean {
|
|
804
1176
|
if (t.isIdentifier(entry.argument)) {
|
|
805
|
-
return
|
|
1177
|
+
return toolkitSpreadNames.has(entry.argument.name);
|
|
806
1178
|
}
|
|
807
1179
|
|
|
808
1180
|
const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
|
|
@@ -811,6 +1183,30 @@ function isSafeToolkitSpread(
|
|
|
811
1183
|
);
|
|
812
1184
|
}
|
|
813
1185
|
|
|
1186
|
+
function genericUnsafeToolkitEntryMessage(): string {
|
|
1187
|
+
return (
|
|
1188
|
+
"each tool must be an inline object literal (`name: { ... }`) or a " +
|
|
1189
|
+
"compiler-visible toolkit spread / generative tool (e.g. " +
|
|
1190
|
+
"`...defineMcpToolkit(...)`, `...baseToolkit`, " +
|
|
1191
|
+
"`generative.present()`, or `unstable_interactableTool(...)`) so its " +
|
|
1192
|
+
"`execute` can be routed"
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function describeUnsafeToolkitEntry(entry: Entry): string {
|
|
1197
|
+
if (!t.isObjectProperty(entry)) return genericUnsafeToolkitEntryMessage();
|
|
1198
|
+
|
|
1199
|
+
const toolName = memberName(entry.key, entry.computed);
|
|
1200
|
+
const raw = entryRawValue(entry);
|
|
1201
|
+
if (!toolName || !raw) return genericUnsafeToolkitEntryMessage();
|
|
1202
|
+
|
|
1203
|
+
return (
|
|
1204
|
+
`tool "${toolName}" cannot be \`${generate(raw).code}\`; use an inline ` +
|
|
1205
|
+
`object literal (\`${toolName}: { ... }\`) or a compiler-visible toolkit ` +
|
|
1206
|
+
"spread / generative tool so its `execute` can be routed"
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
814
1210
|
/** The `JSONGenerativeUI` methods that produce a split-by-condition tool. */
|
|
815
1211
|
const GENERATIVE_TOOL_METHODS = new Set(["present", "promptUser"]);
|
|
816
1212
|
|
|
@@ -871,10 +1267,17 @@ function compileToolkit(
|
|
|
871
1267
|
object: t.ObjectExpression,
|
|
872
1268
|
target: Target,
|
|
873
1269
|
instances: Set<string>,
|
|
874
|
-
|
|
1270
|
+
interactableToolImports: Set<string>,
|
|
1271
|
+
toolkitSpreadNames: ToolkitSpreadNames,
|
|
875
1272
|
flags: TargetFlags,
|
|
876
1273
|
filename: string | undefined,
|
|
877
1274
|
): void {
|
|
1275
|
+
// Split builds compile both targets; emit target-independent warnings from
|
|
1276
|
+
// the client pass so each duplicate is logged once.
|
|
1277
|
+
if (target === "client") {
|
|
1278
|
+
warnDuplicateToolkitNames(object, toolkitSpreadNames, filename);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
878
1281
|
const nextProperties: t.ObjectExpression["properties"] = [];
|
|
879
1282
|
|
|
880
1283
|
for (const entry of object.properties) {
|
|
@@ -882,7 +1285,7 @@ function compileToolkit(
|
|
|
882
1285
|
if (!value) {
|
|
883
1286
|
if (
|
|
884
1287
|
t.isSpreadElement(entry) &&
|
|
885
|
-
isSafeToolkitSpread(entry,
|
|
1288
|
+
isSafeToolkitSpread(entry, toolkitSpreadNames)
|
|
886
1289
|
) {
|
|
887
1290
|
nextProperties.push(entry);
|
|
888
1291
|
continue;
|
|
@@ -896,27 +1299,36 @@ function compileToolkit(
|
|
|
896
1299
|
nextProperties.push(entry);
|
|
897
1300
|
continue;
|
|
898
1301
|
}
|
|
1302
|
+
const config =
|
|
1303
|
+
raw && interactableToolConfig(raw, interactableToolImports);
|
|
1304
|
+
if (config) {
|
|
1305
|
+
if (target === "client") flags.keptRender = true;
|
|
1306
|
+
else removeMember(config, "render");
|
|
1307
|
+
nextProperties.push(entry);
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
899
1310
|
throw new GenerativeCompileError(
|
|
900
|
-
|
|
901
|
-
"compiler-visible toolkit spread / generative tool (e.g. " +
|
|
902
|
-
"`...defineMcpToolkit(...)`, `...baseToolkit`, or " +
|
|
903
|
-
"`generative.present()`) so its `execute` can be routed",
|
|
1311
|
+
describeUnsafeToolkitEntry(entry),
|
|
904
1312
|
filename,
|
|
905
1313
|
);
|
|
906
1314
|
}
|
|
907
1315
|
|
|
908
1316
|
// Nature is inferred from `execute` (see inferToolType), not an authored
|
|
909
1317
|
// `type`. The resolved type is written back below so the runtime keeps it.
|
|
1318
|
+
const toolName = t.isObjectProperty(entry)
|
|
1319
|
+
? memberName(entry.key, entry.computed)
|
|
1320
|
+
: undefined;
|
|
910
1321
|
const execute = findMember(value, "execute");
|
|
911
1322
|
const isStub = execute ? executeIsStubTool(execute) : false;
|
|
912
1323
|
const isExternal = execute ? executeIsExternalTool(execute) : false;
|
|
913
|
-
const type = inferToolType(value, filename);
|
|
1324
|
+
const type = inferToolType(value, toolName, filename);
|
|
914
1325
|
const hasRender = !!findMember(value, "render");
|
|
915
1326
|
const hasRenderText = !!findMember(value, "renderText");
|
|
916
1327
|
|
|
917
1328
|
if (type === "frontend" && !hasRender && !hasRenderText) {
|
|
918
1329
|
throw new GenerativeCompileError(
|
|
919
|
-
"
|
|
1330
|
+
`${typedToolSubject("frontend", toolName)} must declare a ` +
|
|
1331
|
+
"`render` or `renderText` " +
|
|
920
1332
|
"(it has no server execute to show otherwise)",
|
|
921
1333
|
filename,
|
|
922
1334
|
);
|
|
@@ -924,19 +1336,21 @@ function compileToolkit(
|
|
|
924
1336
|
|
|
925
1337
|
if (type === "human" && !hasRender) {
|
|
926
1338
|
throw new GenerativeCompileError(
|
|
927
|
-
"
|
|
1339
|
+
`${typedToolSubject("human", toolName)} must declare a ` +
|
|
1340
|
+
"`render` so it can collect input",
|
|
928
1341
|
filename,
|
|
929
1342
|
);
|
|
930
1343
|
}
|
|
931
1344
|
|
|
932
1345
|
if (type === "provider" && execute) {
|
|
933
|
-
applyProviderToolConfig(value, execute, filename);
|
|
1346
|
+
applyProviderToolConfig(value, execute, toolName, filename);
|
|
934
1347
|
}
|
|
935
1348
|
|
|
936
1349
|
if (isExternal) {
|
|
937
1350
|
if (!hasRender && !hasRenderText) {
|
|
938
1351
|
throw new GenerativeCompileError(
|
|
939
|
-
"
|
|
1352
|
+
`${typedToolSubject("external", toolName)} must declare a ` +
|
|
1353
|
+
"`render` or `renderText` " +
|
|
940
1354
|
"(assistant-ui only renders calls for tools defined elsewhere)",
|
|
941
1355
|
filename,
|
|
942
1356
|
);
|
|
@@ -972,6 +1386,7 @@ function compileToolkit(
|
|
|
972
1386
|
function applyProviderToolConfig(
|
|
973
1387
|
object: t.ObjectExpression,
|
|
974
1388
|
execute: t.ObjectProperty | t.ObjectMethod,
|
|
1389
|
+
toolName: string | undefined,
|
|
975
1390
|
filename: string | undefined,
|
|
976
1391
|
): void {
|
|
977
1392
|
if (
|
|
@@ -1019,8 +1434,9 @@ function applyProviderToolConfig(
|
|
|
1019
1434
|
);
|
|
1020
1435
|
}
|
|
1021
1436
|
if (existingNames.has(name) || configNames.has(name)) {
|
|
1437
|
+
const toolLabel = toolName ? ` for "${toolName}"` : "";
|
|
1022
1438
|
throw new GenerativeCompileError(
|
|
1023
|
-
|
|
1439
|
+
`\`providerTool(...)\` config${toolLabel} duplicates "${name}"`,
|
|
1024
1440
|
filename,
|
|
1025
1441
|
);
|
|
1026
1442
|
}
|
|
@@ -1151,13 +1567,14 @@ function stripUseClient(member: t.ObjectProperty | t.ObjectMethod): void {
|
|
|
1151
1567
|
*/
|
|
1152
1568
|
function inferToolType(
|
|
1153
1569
|
object: t.ObjectExpression,
|
|
1570
|
+
toolName: string | undefined,
|
|
1154
1571
|
filename: string | undefined,
|
|
1155
1572
|
): ToolType {
|
|
1156
1573
|
const execute = findMember(object, "execute");
|
|
1157
1574
|
if (!execute) {
|
|
1158
1575
|
throw new GenerativeCompileError(
|
|
1159
|
-
|
|
1160
|
-
"human-in-the-loop tool",
|
|
1576
|
+
`${toolSubject(toolName)} must declare an \`execute\`; use ` +
|
|
1577
|
+
"`humanTool()` for a human-in-the-loop tool",
|
|
1161
1578
|
filename,
|
|
1162
1579
|
);
|
|
1163
1580
|
}
|
|
@@ -1168,6 +1585,16 @@ function inferToolType(
|
|
|
1168
1585
|
return executeIsClient(execute) ? "frontend" : "backend";
|
|
1169
1586
|
}
|
|
1170
1587
|
|
|
1588
|
+
function toolSubject(toolName: string | undefined): string {
|
|
1589
|
+
return toolName ? `tool "${toolName}"` : "every tool";
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function typedToolSubject(type: string, toolName: string | undefined): string {
|
|
1593
|
+
if (toolName) return `${type} tool "${toolName}"`;
|
|
1594
|
+
const article = /^[aeiou]/i.test(type) ? "an" : "a";
|
|
1595
|
+
return `${article} ${type} tool`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1171
1598
|
function stripExternalToolMetadata(object: t.ObjectExpression): void {
|
|
1172
1599
|
// Mirror BackendTool's forbidden metadata fields: execute is stripped by the
|
|
1173
1600
|
// main routing loop, while streamCall is also removed because there is no
|