@assistant-ui/x-generative-compiler 0.0.1 → 0.0.3
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/README.md +7 -5
- package/dist/compile.d.ts +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +352 -47
- package/dist/compile.js.map +1 -1
- package/package.json +7 -5
- package/src/compile.test.ts +664 -6
- package/src/compile.ts +676 -76
package/src/compile.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as nodePath from "node:path";
|
|
1
4
|
import { parse } from "@babel/parser";
|
|
2
5
|
import _traverse, { type NodePath } from "@babel/traverse";
|
|
3
6
|
import _generate from "@babel/generator";
|
|
4
7
|
import * as t from "@babel/types";
|
|
8
|
+
import { satisfies } from "semver";
|
|
5
9
|
import { DIRECTIVE, type Target } from "./constants";
|
|
6
10
|
|
|
7
11
|
// @babel/traverse and @babel/generator are CJS; their default export is the
|
|
@@ -13,7 +17,39 @@ const generate = (
|
|
|
13
17
|
typeof _generate === "function" ? _generate : (_generate as any).default
|
|
14
18
|
) as typeof _generate;
|
|
15
19
|
|
|
16
|
-
export type ToolType = "frontend" | "backend" | "human";
|
|
20
|
+
export type ToolType = "frontend" | "backend" | "human" | "provider";
|
|
21
|
+
|
|
22
|
+
/** The required wrapper around a toolkit's tools (stripped at build time). */
|
|
23
|
+
const TOOLKIT_WRAPPER = "defineToolkit";
|
|
24
|
+
/** The helper that produces MCP-only toolkit fragments. */
|
|
25
|
+
const MCP_TOOLKIT_WRAPPER = "defineMcpToolkit";
|
|
26
|
+
/** The required wrapper around a generative-UI library (stripped at build time). */
|
|
27
|
+
const COMPONENTS_WRAPPER = "defineGenerativeComponents";
|
|
28
|
+
/** The core package whose metadata declares supported compiler versions. */
|
|
29
|
+
const CORE_PACKAGE = "@assistant-ui/core";
|
|
30
|
+
/** This package, checked against core's compatibility range. */
|
|
31
|
+
const COMPILER_PACKAGE = "@assistant-ui/x-generative-compiler";
|
|
32
|
+
/** Packages that re-export core's generative markers. */
|
|
33
|
+
const DISTRIBUTION_PACKAGES = [
|
|
34
|
+
CORE_PACKAGE,
|
|
35
|
+
"@assistant-ui/react",
|
|
36
|
+
"@assistant-ui/react-native",
|
|
37
|
+
"@assistant-ui/react-ink",
|
|
38
|
+
] as const;
|
|
39
|
+
/**
|
|
40
|
+
* The class whose instances expose split-by-condition tools (`present()`,
|
|
41
|
+
* `promptUser()`). A toolkit entry that calls a method on one of these passes
|
|
42
|
+
* through untouched — the library, not this compiler, routes its halves.
|
|
43
|
+
*/
|
|
44
|
+
const GENERATIVE_FACTORY = "JSONGenerativeUI";
|
|
45
|
+
|
|
46
|
+
/** Mutable per-build outcomes the toolkit pass reports back for directive/guard injection. */
|
|
47
|
+
interface TargetFlags {
|
|
48
|
+
/** A `render` survived on a client build (→ emit `"use client"`). */
|
|
49
|
+
keptRender: boolean;
|
|
50
|
+
/** A backend `execute` survived on a server build (→ emit `import "server-only"`). */
|
|
51
|
+
keptBackendExecute: boolean;
|
|
52
|
+
}
|
|
17
53
|
|
|
18
54
|
export interface CompileOptions {
|
|
19
55
|
/** Which build target to emit. */
|
|
@@ -93,52 +129,64 @@ export function compileGenerative(
|
|
|
93
129
|
);
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
|
|
132
|
+
ensureCompilerCompatibleWithCore(ast, filename);
|
|
97
133
|
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
// A module may hold several `defineToolkit(...)` / `defineGenerativeComponents(...)`
|
|
135
|
+
// calls anywhere (e.g. a library built inside `new JSONGenerativeUI(...)` plus
|
|
136
|
+
// the toolkit that exposes it). Tools may reference a `JSONGenerativeUI`
|
|
137
|
+
// instance's `present()`/`promptUser()`, which the library — not this compiler —
|
|
138
|
+
// splits across builds via export conditions; collect those instance names so
|
|
139
|
+
// such entries pass through.
|
|
140
|
+
ensureDefaultExport(ast, filename);
|
|
141
|
+
const generativeInstances = collectGenerativeInstances(ast);
|
|
142
|
+
const safeToolkitSpreads = collectSafeToolkitSpreads(ast);
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
const value = entryValue(entry);
|
|
103
|
-
if (!value) {
|
|
104
|
-
// A non-inline tool (spread, method, or a call like `makeTool()`) can't be
|
|
105
|
-
// analyzed, so its `execute` would pass through to the client unstripped.
|
|
106
|
-
throw new GenerativeCompileError(
|
|
107
|
-
"each tool must be an inline object literal (`name: { ... }`) so its " +
|
|
108
|
-
"`execute` can be routed",
|
|
109
|
-
filename,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
144
|
+
const flags: TargetFlags = { keptRender: false, keptBackendExecute: false };
|
|
112
145
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
traverse(ast, {
|
|
147
|
+
CallExpression(path: NodePath<t.CallExpression>) {
|
|
148
|
+
const callee = path.node.callee;
|
|
149
|
+
const object = t.isObjectExpression(path.node.arguments[0])
|
|
150
|
+
? path.node.arguments[0]
|
|
151
|
+
: null;
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
if (t.isIdentifier(callee, { name: COMPONENTS_WRAPPER })) {
|
|
154
|
+
if (!object) {
|
|
155
|
+
throw new GenerativeCompileError(
|
|
156
|
+
`${COMPONENTS_WRAPPER}() takes an inline object literal of components`,
|
|
157
|
+
filename,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
compileComponents(object, target, flags, filename);
|
|
161
|
+
// Unwrap the authoring helper to the bare library object so its import
|
|
162
|
+
// can be pruned.
|
|
163
|
+
path.replaceWith(object);
|
|
164
|
+
path.skip();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
125
167
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
if (t.isIdentifier(callee, { name: TOOLKIT_WRAPPER })) {
|
|
169
|
+
if (!object) {
|
|
170
|
+
throw new GenerativeCompileError(
|
|
171
|
+
`${TOOLKIT_WRAPPER}() takes an inline object literal of tools`,
|
|
172
|
+
filename,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
compileToolkit(
|
|
176
|
+
object,
|
|
177
|
+
target,
|
|
178
|
+
generativeInstances,
|
|
179
|
+
safeToolkitSpreads,
|
|
180
|
+
flags,
|
|
181
|
+
filename,
|
|
182
|
+
);
|
|
183
|
+
path.replaceWith(object);
|
|
184
|
+
path.skip();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
139
188
|
|
|
140
|
-
|
|
141
|
-
}
|
|
189
|
+
const { keptRender, keptBackendExecute } = flags;
|
|
142
190
|
|
|
143
191
|
pruneUnused(ast);
|
|
144
192
|
|
|
@@ -174,61 +222,543 @@ export function compileGenerative(
|
|
|
174
222
|
return { code: result.code, map: result.map };
|
|
175
223
|
}
|
|
176
224
|
|
|
177
|
-
|
|
225
|
+
interface PackageJson {
|
|
226
|
+
name?: string;
|
|
227
|
+
version?: string;
|
|
228
|
+
optionalDevDependencies?: Record<string, string>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const checkedCorePackageJsonPaths = new Set<string>();
|
|
232
|
+
let compilerPackageVersion: string | undefined;
|
|
233
|
+
|
|
234
|
+
function ensureCompilerCompatibleWithCore(
|
|
178
235
|
ast: t.File,
|
|
179
236
|
filename: string | undefined,
|
|
180
|
-
):
|
|
181
|
-
|
|
182
|
-
|
|
237
|
+
): void {
|
|
238
|
+
const corePackageJsonPath = resolveCorePackageJson(ast, filename);
|
|
239
|
+
if (
|
|
240
|
+
!corePackageJsonPath ||
|
|
241
|
+
checkedCorePackageJsonPaths.has(corePackageJsonPath)
|
|
242
|
+
) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
183
245
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// (and the import it pulled).
|
|
190
|
-
if (object) stmt.declaration = object;
|
|
246
|
+
const corePackageJson = readPackageJson(corePackageJsonPath);
|
|
247
|
+
const range = corePackageJson?.optionalDevDependencies?.[COMPILER_PACKAGE];
|
|
248
|
+
if (!range) {
|
|
249
|
+
checkedCorePackageJsonPaths.add(corePackageJsonPath);
|
|
250
|
+
return;
|
|
191
251
|
}
|
|
192
252
|
|
|
193
|
-
|
|
253
|
+
const compilerVersion = getCompilerPackageVersion();
|
|
254
|
+
let compatible = false;
|
|
255
|
+
try {
|
|
256
|
+
compatible = satisfies(compilerVersion, range, {
|
|
257
|
+
includePrerelease: true,
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
throw new GenerativeCompileError(
|
|
261
|
+
`${CORE_PACKAGE}@${corePackageJson.version ?? "unknown"} declares an ` +
|
|
262
|
+
`invalid optionalDevDependencies range for ${COMPILER_PACKAGE}: ` +
|
|
263
|
+
JSON.stringify(range),
|
|
264
|
+
filename,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!compatible) {
|
|
269
|
+
throw new GenerativeCompileError(
|
|
270
|
+
`${CORE_PACKAGE}@${corePackageJson.version ?? "unknown"} requires ` +
|
|
271
|
+
`${COMPILER_PACKAGE} ${range}, but the current compiler is ` +
|
|
272
|
+
`${compilerVersion}. Update @assistant-ui/next or @assistant-ui/vite ` +
|
|
273
|
+
"so their compiler satisfies the core package's " +
|
|
274
|
+
"optionalDevDependencies range.",
|
|
275
|
+
filename,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
checkedCorePackageJsonPaths.add(corePackageJsonPath);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getCompilerPackageVersion(): string {
|
|
283
|
+
if (compilerPackageVersion) return compilerPackageVersion;
|
|
284
|
+
|
|
285
|
+
const packageJsonPath = findPackageJson(import.meta.url, COMPILER_PACKAGE);
|
|
286
|
+
const packageJson = packageJsonPath ? readPackageJson(packageJsonPath) : null;
|
|
287
|
+
if (!packageJson?.version) {
|
|
288
|
+
throw new GenerativeCompileError(
|
|
289
|
+
`could not determine ${COMPILER_PACKAGE}'s package version`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
compilerPackageVersion = packageJson.version;
|
|
293
|
+
return compilerPackageVersion;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveCorePackageJson(
|
|
297
|
+
ast: t.File,
|
|
298
|
+
filename: string | undefined,
|
|
299
|
+
): string | null {
|
|
300
|
+
for (const packageName of collectImportedDistributionPackages(ast)) {
|
|
301
|
+
const packageJsonPath = resolvePackageJson(packageName, filename);
|
|
302
|
+
if (!packageJsonPath) continue;
|
|
303
|
+
if (packageName === CORE_PACKAGE) return packageJsonPath;
|
|
304
|
+
|
|
305
|
+
const corePackageJsonPath = resolvePackageJson(
|
|
306
|
+
CORE_PACKAGE,
|
|
307
|
+
packageJsonPath,
|
|
308
|
+
);
|
|
309
|
+
if (corePackageJsonPath) return corePackageJsonPath;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function collectImportedDistributionPackages(ast: t.File): Set<string> {
|
|
316
|
+
const packages = new Set<string>();
|
|
317
|
+
|
|
318
|
+
for (const statement of ast.program.body) {
|
|
319
|
+
const source =
|
|
320
|
+
(t.isImportDeclaration(statement) ||
|
|
321
|
+
t.isExportNamedDeclaration(statement) ||
|
|
322
|
+
t.isExportAllDeclaration(statement)) &&
|
|
323
|
+
statement.source
|
|
324
|
+
? statement.source.value
|
|
325
|
+
: null;
|
|
326
|
+
if (!source) continue;
|
|
327
|
+
|
|
328
|
+
const packageName = packageNameFromSpecifier(source);
|
|
329
|
+
if (packageName) packages.add(packageName);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return packages;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function packageNameFromSpecifier(specifier: string): string | null {
|
|
336
|
+
for (const packageName of DISTRIBUTION_PACKAGES) {
|
|
337
|
+
if (specifier === packageName || specifier.startsWith(`${packageName}/`)) {
|
|
338
|
+
return packageName;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function resolvePackageJson(
|
|
346
|
+
packageName: string,
|
|
347
|
+
filename: string | undefined,
|
|
348
|
+
): string | null {
|
|
349
|
+
const requirePath = normalizeRequirePath(filename);
|
|
350
|
+
const require = createRequire(requirePath);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
return findPackageJson(require.resolve(packageName), packageName);
|
|
354
|
+
} catch {
|
|
355
|
+
return findPackageJsonFromNodeModules(packageName, requirePath);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeRequirePath(filename: string | undefined): string {
|
|
360
|
+
if (filename) {
|
|
361
|
+
const cleanFilename = filename.split(/[?#]/, 1)[0]!;
|
|
362
|
+
if (nodePath.isAbsolute(cleanFilename)) return cleanFilename;
|
|
363
|
+
}
|
|
364
|
+
return import.meta.url;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function findPackageJson(
|
|
368
|
+
fromPathOrUrl: string,
|
|
369
|
+
packageName: string,
|
|
370
|
+
): string | null {
|
|
371
|
+
let current = nodePath.dirname(
|
|
372
|
+
fromPathOrUrl.startsWith("file:")
|
|
373
|
+
? new URL(fromPathOrUrl).pathname
|
|
374
|
+
: nodePath.resolve(fromPathOrUrl),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
for (;;) {
|
|
378
|
+
const packageJsonPath = nodePath.join(current, "package.json");
|
|
379
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
380
|
+
if (packageJson?.name === packageName) return packageJsonPath;
|
|
381
|
+
|
|
382
|
+
const parent = nodePath.dirname(current);
|
|
383
|
+
if (parent === current) return null;
|
|
384
|
+
current = parent;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function findPackageJsonFromNodeModules(
|
|
389
|
+
packageName: string,
|
|
390
|
+
fromPathOrUrl: string,
|
|
391
|
+
): string | null {
|
|
392
|
+
const parts = packageName.split("/");
|
|
393
|
+
let current = nodePath.dirname(
|
|
394
|
+
fromPathOrUrl.startsWith("file:")
|
|
395
|
+
? new URL(fromPathOrUrl).pathname
|
|
396
|
+
: nodePath.resolve(fromPathOrUrl),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
for (;;) {
|
|
400
|
+
const packageJsonPath = nodePath.join(
|
|
401
|
+
current,
|
|
402
|
+
"node_modules",
|
|
403
|
+
...parts,
|
|
404
|
+
"package.json",
|
|
405
|
+
);
|
|
406
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
407
|
+
if (packageJson?.name === packageName) return packageJsonPath;
|
|
408
|
+
|
|
409
|
+
const parent = nodePath.dirname(current);
|
|
410
|
+
if (parent === current) return null;
|
|
411
|
+
current = parent;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function readPackageJson(packageJsonPath: string): PackageJson | null {
|
|
416
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
417
|
+
try {
|
|
418
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf8")) as PackageJson;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw new GenerativeCompileError(
|
|
421
|
+
`could not parse package metadata at ${packageJsonPath}: ${
|
|
422
|
+
error instanceof Error ? error.message : String(error)
|
|
423
|
+
}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Errors unless the module's default export is the toolkit — a `defineToolkit(...)`
|
|
430
|
+
* call (through `satisfies`/`as`/parens). This is the security boundary: the
|
|
431
|
+
* default export is what the runtime registers, so it must be wrapped (and thus
|
|
432
|
+
* split). A bare `export default { ... }` would ship a backend `execute` to the
|
|
433
|
+
* client even if some *other* `defineToolkit(...)` exists elsewhere in the file.
|
|
434
|
+
*/
|
|
435
|
+
function ensureDefaultExport(ast: t.File, filename: string | undefined): void {
|
|
436
|
+
const def = ast.program.body.find(
|
|
437
|
+
(stmt): stmt is t.ExportDefaultDeclaration =>
|
|
438
|
+
t.isExportDefaultDeclaration(stmt),
|
|
439
|
+
);
|
|
440
|
+
if (!def) {
|
|
194
441
|
throw new GenerativeCompileError("missing a default export", filename);
|
|
195
442
|
}
|
|
196
|
-
if (!
|
|
443
|
+
if (!unwrapToCall(def.declaration, TOOLKIT_WRAPPER)) {
|
|
197
444
|
throw new GenerativeCompileError(
|
|
198
|
-
|
|
445
|
+
`the default export must be ${TOOLKIT_WRAPPER}({ ... }) (imported from ` +
|
|
199
446
|
'"@assistant-ui/react"); wrapping is required so a backend `execute` ' +
|
|
200
447
|
"can't be authored in a way that reaches the client",
|
|
201
448
|
filename,
|
|
202
449
|
);
|
|
203
450
|
}
|
|
204
|
-
return object;
|
|
205
451
|
}
|
|
206
452
|
|
|
207
453
|
/**
|
|
208
|
-
* Unwraps
|
|
209
|
-
*
|
|
210
|
-
* `satisfies Toolkit` without the wrapper, some other call — yields `null` so the
|
|
211
|
-
* caller errors.
|
|
454
|
+
* Unwraps a node through `satisfies`/`as`/parens to a call of the named function,
|
|
455
|
+
* or returns `null`.
|
|
212
456
|
*/
|
|
213
|
-
function
|
|
457
|
+
function unwrapToCall(node: t.Node, name: string): t.CallExpression | null {
|
|
214
458
|
if (t.isTSSatisfiesExpression(node) || t.isTSAsExpression(node)) {
|
|
215
|
-
return
|
|
459
|
+
return unwrapToCall(node.expression, name);
|
|
216
460
|
}
|
|
217
461
|
if (t.isParenthesizedExpression(node)) {
|
|
218
|
-
return
|
|
462
|
+
return unwrapToCall(node.expression, name);
|
|
463
|
+
}
|
|
464
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name })) {
|
|
465
|
+
return node;
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Collects the names bound to `new JSONGenerativeUI(...)` (e.g.
|
|
472
|
+
* `const generative = new JSONGenerativeUI({ library })`). A toolkit entry that
|
|
473
|
+
* calls a method on one of these is a generative tool whose halves the library
|
|
474
|
+
* routes by export condition, so it passes through the toolkit pass untouched.
|
|
475
|
+
*/
|
|
476
|
+
function collectGenerativeInstances(ast: t.File): Set<string> {
|
|
477
|
+
const names = new Set<string>();
|
|
478
|
+
for (const statement of ast.program.body) {
|
|
479
|
+
if (!t.isVariableDeclaration(statement)) continue;
|
|
480
|
+
for (const declaration of statement.declarations) {
|
|
481
|
+
const { id, init } = declaration;
|
|
482
|
+
if (
|
|
483
|
+
t.isIdentifier(id) &&
|
|
484
|
+
t.isNewExpression(init) &&
|
|
485
|
+
t.isIdentifier(init.callee, { name: GENERATIVE_FACTORY })
|
|
486
|
+
) {
|
|
487
|
+
names.add(id.name);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return names;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Local toolkit variables whose initializer is visible to this compiler pass
|
|
496
|
+
* are safe to spread. `defineToolkit(...)` initializers are compiled in-place
|
|
497
|
+
* before a later spread reads them; `defineMcpToolkit(...)` entries cannot
|
|
498
|
+
* contain executable code.
|
|
499
|
+
*/
|
|
500
|
+
function collectSafeToolkitSpreads(ast: t.File): Set<string> {
|
|
501
|
+
const names = new Set<string>();
|
|
502
|
+
for (const statement of ast.program.body) {
|
|
503
|
+
if (!t.isVariableDeclaration(statement)) continue;
|
|
504
|
+
for (const declaration of statement.declarations) {
|
|
505
|
+
const { id, init } = declaration;
|
|
506
|
+
if (!t.isIdentifier(id) || !init) continue;
|
|
507
|
+
if (unwrapToToolkitCall(init)) names.add(id.name);
|
|
508
|
+
}
|
|
219
509
|
}
|
|
510
|
+
return names;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function unwrapToToolkitCall(node: t.Node): t.CallExpression | null {
|
|
514
|
+
return (
|
|
515
|
+
unwrapToCall(node, TOOLKIT_WRAPPER) ??
|
|
516
|
+
unwrapToCall(node, MCP_TOOLKIT_WRAPPER)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function isSafeToolkitSpread(
|
|
521
|
+
entry: t.SpreadElement,
|
|
522
|
+
safeToolkitSpreads: Set<string>,
|
|
523
|
+
): boolean {
|
|
524
|
+
if (t.isIdentifier(entry.argument)) {
|
|
525
|
+
return safeToolkitSpreads.has(entry.argument.name);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
|
|
529
|
+
return (
|
|
530
|
+
!!directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0])
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** The `JSONGenerativeUI` methods that produce a split-by-condition tool. */
|
|
535
|
+
const GENERATIVE_TOOL_METHODS = new Set(["present", "promptUser"]);
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Whether a toolkit entry's value is a call to a tool-producing method on a
|
|
539
|
+
* collected `JSONGenerativeUI` instance (`generative.present()`), which passes
|
|
540
|
+
* through. The method name is checked too, so a typo like `generative.presnt()`
|
|
541
|
+
* is a compile error here rather than a pass-through that fails at runtime.
|
|
542
|
+
*/
|
|
543
|
+
function isGenerativeToolEntry(value: t.Node, instances: Set<string>): boolean {
|
|
544
|
+
return (
|
|
545
|
+
t.isCallExpression(value) &&
|
|
546
|
+
t.isMemberExpression(value.callee) &&
|
|
547
|
+
!value.callee.computed &&
|
|
548
|
+
t.isIdentifier(value.callee.object) &&
|
|
549
|
+
instances.has(value.callee.object.name) &&
|
|
550
|
+
t.isIdentifier(value.callee.property) &&
|
|
551
|
+
GENERATIVE_TOOL_METHODS.has(value.callee.property.name)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Splits a `defineGenerativeComponents({ ... })` library for a build target:
|
|
557
|
+
* a component's `render` (and the client imports it alone uses) is dropped on
|
|
558
|
+
* the server; `properties`/`description` stay on both, since they drive the tool
|
|
559
|
+
* schema either way. Mutates the object in place.
|
|
560
|
+
*/
|
|
561
|
+
function compileComponents(
|
|
562
|
+
object: t.ObjectExpression,
|
|
563
|
+
target: Target,
|
|
564
|
+
flags: TargetFlags,
|
|
565
|
+
filename: string | undefined,
|
|
566
|
+
): void {
|
|
567
|
+
for (const entry of object.properties) {
|
|
568
|
+
const value = entryValue(entry);
|
|
569
|
+
if (!value) {
|
|
570
|
+
throw new GenerativeCompileError(
|
|
571
|
+
`each component in ${COMPONENTS_WRAPPER}() must be an inline object ` +
|
|
572
|
+
"literal (`name: { ... }`) so its `render` can be routed",
|
|
573
|
+
filename,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (!findMember(value, "render")) continue;
|
|
577
|
+
// The client keeps `render` (and so needs the module marked `"use client"`);
|
|
578
|
+
// the server drops it, since only the schema reaches the model there.
|
|
579
|
+
if (target === "client") flags.keptRender = true;
|
|
580
|
+
else removeMember(value, "render");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Splits a `defineToolkit({ ... })` for a build target. Each inline tool is
|
|
586
|
+
* routed by inferred type (see the per-entry logic); a generative entry like
|
|
587
|
+
* `generative.present()` passes through, the library having already split it.
|
|
588
|
+
* Mutates the object in place and records outcomes in {@link TargetFlags}.
|
|
589
|
+
*/
|
|
590
|
+
function compileToolkit(
|
|
591
|
+
object: t.ObjectExpression,
|
|
592
|
+
target: Target,
|
|
593
|
+
instances: Set<string>,
|
|
594
|
+
safeToolkitSpreads: Set<string>,
|
|
595
|
+
flags: TargetFlags,
|
|
596
|
+
filename: string | undefined,
|
|
597
|
+
): void {
|
|
598
|
+
const nextProperties: t.ObjectExpression["properties"] = [];
|
|
599
|
+
|
|
600
|
+
for (const entry of object.properties) {
|
|
601
|
+
const value = entryValue(entry);
|
|
602
|
+
if (!value) {
|
|
603
|
+
if (
|
|
604
|
+
t.isSpreadElement(entry) &&
|
|
605
|
+
isSafeToolkitSpread(entry, safeToolkitSpreads)
|
|
606
|
+
) {
|
|
607
|
+
nextProperties.push(entry);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
// A generative tool (`generative.present()`) is split by the library's
|
|
611
|
+
// export conditions, so it is safe to keep verbatim. Anything else — a
|
|
612
|
+
// method or opaque call like `makeTool()` — can't be analyzed, so its
|
|
613
|
+
// `execute` could reach the client unstripped.
|
|
614
|
+
const raw = entryRawValue(entry);
|
|
615
|
+
if (raw && isGenerativeToolEntry(raw, instances)) {
|
|
616
|
+
nextProperties.push(entry);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
throw new GenerativeCompileError(
|
|
620
|
+
"each tool must be an inline object literal (`name: { ... }`) or a " +
|
|
621
|
+
"compiler-visible toolkit spread / generative tool (e.g. " +
|
|
622
|
+
"`...defineMcpToolkit(...)`, `...baseToolkit`, or " +
|
|
623
|
+
"`generative.present()`) so its `execute` can be routed",
|
|
624
|
+
filename,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Nature is inferred from `execute` (see inferToolType), not an authored
|
|
629
|
+
// `type`. The resolved type is written back below so the runtime keeps it.
|
|
630
|
+
const execute = findMember(value, "execute");
|
|
631
|
+
const isStub = execute ? executeIsStubTool(execute) : false;
|
|
632
|
+
const isExternal = execute ? executeIsExternalTool(execute) : false;
|
|
633
|
+
const type = inferToolType(value, filename);
|
|
634
|
+
const hasRender = !!findMember(value, "render");
|
|
635
|
+
const hasRenderText = !!findMember(value, "renderText");
|
|
636
|
+
|
|
637
|
+
if (type === "frontend" && !hasRender && !hasRenderText) {
|
|
638
|
+
throw new GenerativeCompileError(
|
|
639
|
+
"a frontend tool must declare a `render` or `renderText` " +
|
|
640
|
+
"(it has no server execute to show otherwise)",
|
|
641
|
+
filename,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (type === "human" && !hasRender) {
|
|
646
|
+
throw new GenerativeCompileError(
|
|
647
|
+
"a human tool must declare a `render` so it can collect input",
|
|
648
|
+
filename,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (type === "provider" && execute) {
|
|
653
|
+
applyProviderToolConfig(value, execute, filename);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (isExternal) {
|
|
657
|
+
if (!hasRender && !hasRenderText) {
|
|
658
|
+
throw new GenerativeCompileError(
|
|
659
|
+
"an external tool must declare a `render` or `renderText` " +
|
|
660
|
+
"(assistant-ui only renders calls for tools defined elsewhere)",
|
|
661
|
+
filename,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (target === "server") continue;
|
|
665
|
+
stripExternalToolMetadata(value);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (target === "client") {
|
|
669
|
+
// A non-stub frontend execute stays (its `"use client"` marker is no longer needed
|
|
670
|
+
// once the module is client); backend, sentinel, and stub executes are dropped.
|
|
671
|
+
if (execute && type === "frontend" && !isStub) stripUseClient(execute);
|
|
672
|
+
else if (execute) removeMember(value, "execute");
|
|
673
|
+
if (hasRender || hasRenderText) flags.keptRender = true;
|
|
674
|
+
} else {
|
|
675
|
+
// server: render is never needed; only a non-external backend execute survives.
|
|
676
|
+
if (hasRender) removeMember(value, "render");
|
|
677
|
+
if (hasRenderText) removeMember(value, "renderText");
|
|
678
|
+
if (execute) {
|
|
679
|
+
if (type === "backend" && !isExternal) flags.keptBackendExecute = true;
|
|
680
|
+
else removeMember(value, "execute");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
setToolType(value, type);
|
|
685
|
+
setBackendDefault(value, target, type);
|
|
686
|
+
nextProperties.push(entry);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
object.properties = nextProperties;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function applyProviderToolConfig(
|
|
693
|
+
object: t.ObjectExpression,
|
|
694
|
+
execute: t.ObjectProperty | t.ObjectMethod,
|
|
695
|
+
filename: string | undefined,
|
|
696
|
+
): void {
|
|
220
697
|
if (
|
|
221
|
-
t.
|
|
222
|
-
t.
|
|
223
|
-
|
|
698
|
+
!t.isObjectProperty(execute) ||
|
|
699
|
+
!t.isCallExpression(execute.value) ||
|
|
700
|
+
execute.value.arguments.length !== 1 ||
|
|
701
|
+
!t.isObjectExpression(execute.value.arguments[0])
|
|
224
702
|
) {
|
|
225
|
-
|
|
703
|
+
throw new GenerativeCompileError(
|
|
704
|
+
"`providerTool(...)` must receive an inline object literal",
|
|
705
|
+
filename,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const existingNames = new Set(
|
|
710
|
+
object.properties.flatMap((prop) => {
|
|
711
|
+
if (!t.isObjectProperty(prop) && !t.isObjectMethod(prop)) return [];
|
|
712
|
+
const name = memberName(prop.key, prop.computed);
|
|
713
|
+
return name ? [name] : [];
|
|
714
|
+
}),
|
|
715
|
+
);
|
|
716
|
+
const configNames = new Set<string>();
|
|
717
|
+
|
|
718
|
+
for (const prop of execute.value.arguments[0].properties) {
|
|
719
|
+
if (!t.isObjectProperty(prop)) {
|
|
720
|
+
throw new GenerativeCompileError(
|
|
721
|
+
"`providerTool(...)` config can only contain object properties",
|
|
722
|
+
filename,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const name = memberName(prop.key, prop.computed);
|
|
726
|
+
if (!name) {
|
|
727
|
+
throw new GenerativeCompileError(
|
|
728
|
+
"`providerTool(...)` config can only contain static property names",
|
|
729
|
+
filename,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (
|
|
733
|
+
t.isFunctionExpression(prop.value) ||
|
|
734
|
+
t.isArrowFunctionExpression(prop.value)
|
|
735
|
+
) {
|
|
736
|
+
throw new GenerativeCompileError(
|
|
737
|
+
"`providerTool(...)` config cannot contain function-valued properties",
|
|
738
|
+
filename,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
if (existingNames.has(name) || configNames.has(name)) {
|
|
742
|
+
throw new GenerativeCompileError(
|
|
743
|
+
"`providerTool(...)` config cannot duplicate tool properties",
|
|
744
|
+
filename,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
configNames.add(name);
|
|
748
|
+
object.properties.push(prop);
|
|
226
749
|
}
|
|
227
|
-
return null;
|
|
228
750
|
}
|
|
229
751
|
|
|
230
752
|
type Entry = t.ObjectExpression["properties"][number];
|
|
231
753
|
|
|
754
|
+
/** The raw AST value of an entry (any expression), or null for spreads/methods. */
|
|
755
|
+
function entryRawValue(entry: Entry): t.Expression | null {
|
|
756
|
+
if (t.isObjectProperty(entry) && t.isExpression(entry.value)) {
|
|
757
|
+
return entry.value;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
232
762
|
function entryValue(entry: Entry): t.ObjectExpression | null {
|
|
233
763
|
if (t.isObjectProperty(entry) && t.isObjectExpression(entry.value)) {
|
|
234
764
|
return entry.value;
|
|
@@ -280,15 +810,47 @@ function executeIsClient(member: t.ObjectProperty | t.ObjectMethod): boolean {
|
|
|
280
810
|
);
|
|
281
811
|
}
|
|
282
812
|
|
|
283
|
-
|
|
284
|
-
|
|
813
|
+
function executeIsSentinel(
|
|
814
|
+
member: t.ObjectProperty | t.ObjectMethod,
|
|
815
|
+
name: string,
|
|
816
|
+
): boolean {
|
|
285
817
|
return (
|
|
286
818
|
t.isObjectProperty(member) &&
|
|
287
819
|
t.isCallExpression(member.value) &&
|
|
288
|
-
t.isIdentifier(member.value.callee, { name
|
|
820
|
+
t.isIdentifier(member.value.callee, { name })
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Whether an `execute` is the human-in-the-loop sentinel. */
|
|
825
|
+
function executeIsHumanTool(
|
|
826
|
+
member: t.ObjectProperty | t.ObjectMethod,
|
|
827
|
+
): boolean {
|
|
828
|
+
return (
|
|
829
|
+
executeIsSentinel(member, "humanTool") ||
|
|
830
|
+
executeIsSentinel(member, "hitlTool") ||
|
|
831
|
+
executeIsSentinel(member, "hitl")
|
|
289
832
|
);
|
|
290
833
|
}
|
|
291
834
|
|
|
835
|
+
/** Whether an `execute` is the provider-tool sentinel. */
|
|
836
|
+
function executeIsProviderTool(
|
|
837
|
+
member: t.ObjectProperty | t.ObjectMethod,
|
|
838
|
+
): boolean {
|
|
839
|
+
return executeIsSentinel(member, "providerTool");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Whether an `execute` is the local override sentinel. */
|
|
843
|
+
function executeIsStubTool(member: t.ObjectProperty | t.ObjectMethod): boolean {
|
|
844
|
+
return executeIsSentinel(member, "stubTool");
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Whether an `execute` is the externally-defined backend tool sentinel. */
|
|
848
|
+
function executeIsExternalTool(
|
|
849
|
+
member: t.ObjectProperty | t.ObjectMethod,
|
|
850
|
+
): boolean {
|
|
851
|
+
return executeIsSentinel(member, "externalTool");
|
|
852
|
+
}
|
|
853
|
+
|
|
292
854
|
/** Drops the `"use client"` directive from an `execute` body (kept frontend). */
|
|
293
855
|
function stripUseClient(member: t.ObjectProperty | t.ObjectMethod): void {
|
|
294
856
|
const body = executeBody(member);
|
|
@@ -301,9 +863,11 @@ function stripUseClient(member: t.ObjectProperty | t.ObjectMethod): void {
|
|
|
301
863
|
|
|
302
864
|
/**
|
|
303
865
|
* The tool's nature, inferred from its (mandatory) `execute` rather than an
|
|
304
|
-
* authored `type`: `
|
|
305
|
-
* `
|
|
306
|
-
*
|
|
866
|
+
* authored `type`: `humanTool()` → `human`; `providerTool(...)` → `provider`;
|
|
867
|
+
* `stubTool()` → `frontend`; `externalTool()` → `backend`; `"use client"` →
|
|
868
|
+
* `frontend`; otherwise `backend`.
|
|
869
|
+
* The loader writes the result back as a `type` field (see {@link setToolType})
|
|
870
|
+
* so the runtime keeps it.
|
|
307
871
|
*/
|
|
308
872
|
function inferToolType(
|
|
309
873
|
object: t.ObjectExpression,
|
|
@@ -312,15 +876,31 @@ function inferToolType(
|
|
|
312
876
|
const execute = findMember(object, "execute");
|
|
313
877
|
if (!execute) {
|
|
314
878
|
throw new GenerativeCompileError(
|
|
315
|
-
"every tool must declare an `execute`; use `
|
|
879
|
+
"every tool must declare an `execute`; use `humanTool()` for a " +
|
|
316
880
|
"human-in-the-loop tool",
|
|
317
881
|
filename,
|
|
318
882
|
);
|
|
319
883
|
}
|
|
320
|
-
if (
|
|
884
|
+
if (executeIsHumanTool(execute)) return "human";
|
|
885
|
+
if (executeIsProviderTool(execute)) return "provider";
|
|
886
|
+
if (executeIsStubTool(execute)) return "frontend";
|
|
887
|
+
if (executeIsExternalTool(execute)) return "backend";
|
|
321
888
|
return executeIsClient(execute) ? "frontend" : "backend";
|
|
322
889
|
}
|
|
323
890
|
|
|
891
|
+
function stripExternalToolMetadata(object: t.ObjectExpression): void {
|
|
892
|
+
// Mirror BackendTool's forbidden metadata fields: execute is stripped by the
|
|
893
|
+
// main routing loop, while streamCall is also removed because there is no
|
|
894
|
+
// assistant-ui executor to stream from.
|
|
895
|
+
removeMember(object, "description");
|
|
896
|
+
removeMember(object, "parameters");
|
|
897
|
+
removeMember(object, "disabled");
|
|
898
|
+
removeMember(object, "toModelOutput");
|
|
899
|
+
removeMember(object, "experimental_onSchemaValidationError");
|
|
900
|
+
removeMember(object, "providerOptions");
|
|
901
|
+
removeMember(object, "streamCall");
|
|
902
|
+
}
|
|
903
|
+
|
|
324
904
|
/** Writes the resolved `type` back onto the tool object (replacing any author's). */
|
|
325
905
|
function setToolType(object: t.ObjectExpression, type: ToolType): void {
|
|
326
906
|
removeMember(object, "type");
|
|
@@ -330,6 +910,26 @@ function setToolType(object: t.ObjectExpression, type: ToolType): void {
|
|
|
330
910
|
);
|
|
331
911
|
}
|
|
332
912
|
|
|
913
|
+
function setBackendDefault(
|
|
914
|
+
object: t.ObjectExpression,
|
|
915
|
+
target: Target,
|
|
916
|
+
type: ToolType,
|
|
917
|
+
): void {
|
|
918
|
+
// Always strip any hand-authored marker first; only re-add it for client
|
|
919
|
+
// frontend/human tools whose schema is already known by the backend.
|
|
920
|
+
removeMember(object, "unstable_backendDefault");
|
|
921
|
+
if (target !== "client" || (type !== "frontend" && type !== "human")) return;
|
|
922
|
+
|
|
923
|
+
object.properties.push(
|
|
924
|
+
t.objectProperty(
|
|
925
|
+
t.identifier("unstable_backendDefault"),
|
|
926
|
+
t.objectExpression([
|
|
927
|
+
t.objectProperty(t.identifier("parameters"), t.booleanLiteral(true)),
|
|
928
|
+
]),
|
|
929
|
+
),
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
333
933
|
function memberName(
|
|
334
934
|
key: t.Node,
|
|
335
935
|
computed: boolean | undefined,
|