@assistant-ui/x-generative-compiler 0.0.2 → 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 +15 -13
- package/src/compile.test.ts +664 -6
- package/src/compile.ts +676 -76
- package/LICENSE +0 -21
package/src/compile.test.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import * as nodePath from "node:path";
|
|
1
4
|
import { describe, it, expect } from "vitest";
|
|
2
5
|
import {
|
|
3
6
|
compileGenerative,
|
|
@@ -5,6 +8,71 @@ import {
|
|
|
5
8
|
GenerativeCompileError,
|
|
6
9
|
} from "./compile";
|
|
7
10
|
|
|
11
|
+
const minimalSource = `"use generative";
|
|
12
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
13
|
+
export default defineToolkit({
|
|
14
|
+
weather: {
|
|
15
|
+
execute: async () => 1,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
function createPackage(
|
|
21
|
+
root: string,
|
|
22
|
+
name: string,
|
|
23
|
+
packageJson: Record<string, unknown> = {},
|
|
24
|
+
): string {
|
|
25
|
+
const packageRoot = nodePath.join(root, "node_modules", ...name.split("/"));
|
|
26
|
+
const distRoot = nodePath.join(packageRoot, "dist");
|
|
27
|
+
mkdirSync(distRoot, { recursive: true });
|
|
28
|
+
writeFileSync(nodePath.join(distRoot, "index.js"), "export {};\n");
|
|
29
|
+
writeFileSync(
|
|
30
|
+
nodePath.join(packageRoot, "package.json"),
|
|
31
|
+
JSON.stringify(
|
|
32
|
+
{
|
|
33
|
+
name,
|
|
34
|
+
version: "0.0.0",
|
|
35
|
+
type: "module",
|
|
36
|
+
exports: {
|
|
37
|
+
".": "./dist/index.js",
|
|
38
|
+
},
|
|
39
|
+
...packageJson,
|
|
40
|
+
},
|
|
41
|
+
null,
|
|
42
|
+
2,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
return packageRoot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createCompatibilityFixture(range: string | null): string {
|
|
49
|
+
const appRoot = mkdtempSync(
|
|
50
|
+
nodePath.join(tmpdir(), "aui-generative-compiler-"),
|
|
51
|
+
);
|
|
52
|
+
const srcRoot = nodePath.join(appRoot, "src");
|
|
53
|
+
mkdirSync(srcRoot, { recursive: true });
|
|
54
|
+
const filename = nodePath.join(srcRoot, "toolkit.tsx");
|
|
55
|
+
writeFileSync(filename, minimalSource);
|
|
56
|
+
|
|
57
|
+
const reactRoot = createPackage(appRoot, "@assistant-ui/react", {
|
|
58
|
+
dependencies: {
|
|
59
|
+
"@assistant-ui/core": "0.0.0",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
createPackage(reactRoot, "@assistant-ui/core", {
|
|
63
|
+
version: "0.2.10",
|
|
64
|
+
...(range
|
|
65
|
+
? {
|
|
66
|
+
optionalDevDependencies: {
|
|
67
|
+
"@assistant-ui/x-generative-compiler": range,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return filename;
|
|
74
|
+
}
|
|
75
|
+
|
|
8
76
|
const source = `"use generative";
|
|
9
77
|
import { z } from "zod";
|
|
10
78
|
import { db } from "@/db";
|
|
@@ -34,6 +102,49 @@ export default defineToolkit({
|
|
|
34
102
|
const server = () => compileGenerative(source, { target: "server" }).code;
|
|
35
103
|
const client = () => compileGenerative(source, { target: "client" }).code;
|
|
36
104
|
|
|
105
|
+
describe("compileGenerative — core/compiler compatibility", () => {
|
|
106
|
+
it("allows a compiler version that satisfies core's optionalDevDependencies range", () => {
|
|
107
|
+
const filename = createCompatibilityFixture(">=0.0.0");
|
|
108
|
+
|
|
109
|
+
expect(() =>
|
|
110
|
+
compileGenerative(minimalSource, { target: "server", filename }),
|
|
111
|
+
).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("throws when core requires a different compiler range", () => {
|
|
115
|
+
const filename = createCompatibilityFixture("<0.0.3");
|
|
116
|
+
|
|
117
|
+
expect(() =>
|
|
118
|
+
compileGenerative(minimalSource, { target: "server", filename }),
|
|
119
|
+
).toThrow(/requires @assistant-ui\/x-generative-compiler <0\.0\.3/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("skips the check for older core packages without compatibility metadata", () => {
|
|
123
|
+
const filename = createCompatibilityFixture(null);
|
|
124
|
+
|
|
125
|
+
expect(() =>
|
|
126
|
+
compileGenerative(minimalSource, { target: "server", filename }),
|
|
127
|
+
).not.toThrow();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("wraps malformed package metadata in a compile error", () => {
|
|
131
|
+
const filename = createCompatibilityFixture(">=0.0.0");
|
|
132
|
+
const packageJsonPath = nodePath.join(
|
|
133
|
+
nodePath.dirname(filename),
|
|
134
|
+
"..",
|
|
135
|
+
"node_modules",
|
|
136
|
+
"@assistant-ui",
|
|
137
|
+
"react",
|
|
138
|
+
"package.json",
|
|
139
|
+
);
|
|
140
|
+
writeFileSync(packageJsonPath, "{");
|
|
141
|
+
|
|
142
|
+
expect(() =>
|
|
143
|
+
compileGenerative(minimalSource, { target: "server", filename }),
|
|
144
|
+
).toThrow(GenerativeCompileError);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
37
148
|
describe("compileGenerative — server target", () => {
|
|
38
149
|
const code = server();
|
|
39
150
|
|
|
@@ -54,6 +165,10 @@ describe("compileGenerative — server target", () => {
|
|
|
54
165
|
expect(code).toContain('type: "frontend"'); // toast: execute + "use client"
|
|
55
166
|
});
|
|
56
167
|
|
|
168
|
+
it("does not mark server tools with backend defaults", () => {
|
|
169
|
+
expect(code).not.toContain("unstable_backendDefault");
|
|
170
|
+
});
|
|
171
|
+
|
|
57
172
|
it("drops all render and its client imports", () => {
|
|
58
173
|
expect(code).not.toContain("Chart");
|
|
59
174
|
expect(code).not.toContain("<div");
|
|
@@ -98,6 +213,29 @@ describe("compileGenerative — client target", () => {
|
|
|
98
213
|
expect(code).toContain('type: "frontend"'); // toast
|
|
99
214
|
});
|
|
100
215
|
|
|
216
|
+
it("marks generated frontend tools with backend parameter defaults", () => {
|
|
217
|
+
expect(code).toContain("unstable_backendDefault: {");
|
|
218
|
+
expect(code).toContain("parameters: true");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("marks generated human tools with backend parameter defaults", () => {
|
|
222
|
+
const src = `"use generative";
|
|
223
|
+
import { defineToolkit, humanTool } from "@assistant-ui/react";
|
|
224
|
+
export default defineToolkit({
|
|
225
|
+
ask: {
|
|
226
|
+
parameters: { type: "object", properties: {} },
|
|
227
|
+
execute: humanTool(),
|
|
228
|
+
render: () => null,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const code = compileGenerative(src, { target: "client" }).code;
|
|
234
|
+
expect(code).toContain('type: "human"');
|
|
235
|
+
expect(code).toContain("unstable_backendDefault: {");
|
|
236
|
+
expect(code).toContain("parameters: true");
|
|
237
|
+
});
|
|
238
|
+
|
|
101
239
|
it("drops a backend execute and its server-only imports", () => {
|
|
102
240
|
expect(code).not.toContain("db.weather.get");
|
|
103
241
|
expect(code).not.toContain("@/db");
|
|
@@ -105,6 +243,420 @@ describe("compileGenerative — client target", () => {
|
|
|
105
243
|
});
|
|
106
244
|
});
|
|
107
245
|
|
|
246
|
+
describe("compileGenerative — generative UI library", () => {
|
|
247
|
+
const generative = `"use generative";
|
|
248
|
+
import { z } from "zod";
|
|
249
|
+
import { Chart } from "@/ui/chart";
|
|
250
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
251
|
+
import {
|
|
252
|
+
JSONGenerativeUI,
|
|
253
|
+
defineGenerativeComponents,
|
|
254
|
+
} from "@assistant-ui/react-generative-ui";
|
|
255
|
+
|
|
256
|
+
const generative = new JSONGenerativeUI({
|
|
257
|
+
library: defineGenerativeComponents({
|
|
258
|
+
Chart: {
|
|
259
|
+
description: "A chart.",
|
|
260
|
+
properties: z.object({ data: z.array(z.number()) }),
|
|
261
|
+
render: (props) => <Chart {...props} />,
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export default defineToolkit({
|
|
267
|
+
present: generative.present(),
|
|
268
|
+
prompt_user: generative.promptUser(),
|
|
269
|
+
});
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
it("keeps the schema and the JSONGenerativeUI instance on the server, drops render", () => {
|
|
273
|
+
const code = compileGenerative(generative, { target: "server" }).code;
|
|
274
|
+
expect(code).toContain("new JSONGenerativeUI");
|
|
275
|
+
expect(code).toContain("generative.present()");
|
|
276
|
+
expect(code).toContain("generative.promptUser()");
|
|
277
|
+
expect(code).toContain("z.object");
|
|
278
|
+
expect(code).toContain('description: "A chart."');
|
|
279
|
+
// render and its client-only import are gone on the server
|
|
280
|
+
expect(code).not.toMatch(/render\s*:/);
|
|
281
|
+
expect(code).not.toContain("Chart }"); // the @/ui/chart import specifier
|
|
282
|
+
expect(code).not.toContain("@/ui/chart");
|
|
283
|
+
expect(code).not.toContain("<Chart");
|
|
284
|
+
// the markers themselves are unwrapped and their import pruned
|
|
285
|
+
expect(code).not.toContain("defineToolkit");
|
|
286
|
+
expect(code).not.toContain("defineGenerativeComponents");
|
|
287
|
+
// no backend execute anywhere → no server-only guard, no use client
|
|
288
|
+
expect(code).not.toContain("server-only");
|
|
289
|
+
expect(code).not.toContain("use client");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("keeps render (and marks use client) on the client", () => {
|
|
293
|
+
const code = compileGenerative(generative, { target: "client" }).code;
|
|
294
|
+
expect(code.trimStart().startsWith('"use client"')).toBe(true);
|
|
295
|
+
expect(code).toContain("<Chart");
|
|
296
|
+
expect(code).toContain('import { Chart } from "@/ui/chart"');
|
|
297
|
+
expect(code).toContain("new JSONGenerativeUI");
|
|
298
|
+
expect(code).toContain("generative.present()");
|
|
299
|
+
// the JSONGenerativeUI import survives on both builds
|
|
300
|
+
expect(code).toContain("@assistant-ui/react-generative-ui");
|
|
301
|
+
expect(code).not.toContain("defineGenerativeComponents");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("rejects an unknown method on a JSONGenerativeUI instance", () => {
|
|
305
|
+
const src = `"use generative";
|
|
306
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
307
|
+
import { JSONGenerativeUI } from "@assistant-ui/react-generative-ui";
|
|
308
|
+
const ui = new JSONGenerativeUI({ library: {} });
|
|
309
|
+
export default defineToolkit({ present: ui.notARealMethod() });`;
|
|
310
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
311
|
+
/inline object literal/,
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("rejects a method call on an unknown (non-JSONGenerativeUI) object", () => {
|
|
316
|
+
const src = `"use generative";
|
|
317
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
318
|
+
import { other } from "@/other";
|
|
319
|
+
export default defineToolkit({ present: other.present() });`;
|
|
320
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
321
|
+
/inline object literal/,
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("does not treat nested JSONGenerativeUI instances as module-scope instances", () => {
|
|
326
|
+
const src = `"use generative";
|
|
327
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
328
|
+
import { JSONGenerativeUI } from "@assistant-ui/react-generative-ui";
|
|
329
|
+
function create() {
|
|
330
|
+
const ui = new JSONGenerativeUI({ library: {} });
|
|
331
|
+
return ui;
|
|
332
|
+
}
|
|
333
|
+
export default defineToolkit({ present: ui.present() });`;
|
|
334
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
335
|
+
/inline object literal/,
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("allows a generative tool alongside an inline tool", () => {
|
|
340
|
+
const src = `"use generative";
|
|
341
|
+
import { z } from "zod";
|
|
342
|
+
import { db } from "@/db";
|
|
343
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
344
|
+
import { JSONGenerativeUI, defineGenerativeComponents } from "@assistant-ui/react-generative-ui";
|
|
345
|
+
const ui = new JSONGenerativeUI({ library: defineGenerativeComponents({ Box: { description: "b", properties: z.object({}), render: () => null } }) });
|
|
346
|
+
export default defineToolkit({
|
|
347
|
+
present: ui.present(),
|
|
348
|
+
weather: {
|
|
349
|
+
description: "w",
|
|
350
|
+
parameters: z.object({ city: z.string() }),
|
|
351
|
+
execute: async ({ city }) => db.get(city),
|
|
352
|
+
render: () => null,
|
|
353
|
+
},
|
|
354
|
+
});`;
|
|
355
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
356
|
+
expect(server).toContain("ui.present()");
|
|
357
|
+
expect(server).toContain("db.get");
|
|
358
|
+
expect(server).toContain('type: "backend"');
|
|
359
|
+
expect(server).toContain("server-only");
|
|
360
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
361
|
+
expect(client).toContain("ui.present()");
|
|
362
|
+
expect(client).not.toContain("db.get");
|
|
363
|
+
expect(client.trimStart().startsWith('"use client"')).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("routes provider tool config", () => {
|
|
367
|
+
const src = `"use generative";
|
|
368
|
+
import { defineToolkit, providerTool } from "@assistant-ui/react";
|
|
369
|
+
export default defineToolkit({
|
|
370
|
+
web_search: {
|
|
371
|
+
execute: providerTool({
|
|
372
|
+
providerId: "openai.web_search_preview",
|
|
373
|
+
args: { searchContextSize: "low" },
|
|
374
|
+
providerOptions: { openai: { rankingOptions: { scoreThreshold: 0.5 } } },
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
});`;
|
|
378
|
+
|
|
379
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
380
|
+
expect(server).toContain('type: "provider"');
|
|
381
|
+
expect(server).toContain("openai.web_search_preview");
|
|
382
|
+
expect(server).toContain("searchContextSize");
|
|
383
|
+
expect(server).toContain("providerOptions");
|
|
384
|
+
expect(server).not.toContain("providerTool");
|
|
385
|
+
|
|
386
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
387
|
+
expect(client).toContain('type: "provider"');
|
|
388
|
+
expect(client).toContain("openai.web_search_preview");
|
|
389
|
+
expect(client).toContain("providerOptions");
|
|
390
|
+
expect(client).not.toContain("providerTool");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("routes flat toolkit tools and preserves spread MCP config", () => {
|
|
394
|
+
const src = `"use generative";
|
|
395
|
+
import { z } from "zod";
|
|
396
|
+
import { db } from "@/db";
|
|
397
|
+
import { defineMcpToolkit, defineToolkit } from "@assistant-ui/react";
|
|
398
|
+
const mcp = defineMcpToolkit({
|
|
399
|
+
docs: { type: "http", url: "http://localhost:3001/mcp" },
|
|
400
|
+
});
|
|
401
|
+
export default defineToolkit({
|
|
402
|
+
...mcp,
|
|
403
|
+
weather: {
|
|
404
|
+
parameters: z.object({ city: z.string() }),
|
|
405
|
+
execute: async ({ city }) => db.get(city),
|
|
406
|
+
render: () => null,
|
|
407
|
+
},
|
|
408
|
+
});`;
|
|
409
|
+
|
|
410
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
411
|
+
expect(server).toContain("...mcp");
|
|
412
|
+
expect(server).toContain("db.get");
|
|
413
|
+
expect(server).not.toContain("render");
|
|
414
|
+
|
|
415
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
416
|
+
expect(client).toContain("...mcp");
|
|
417
|
+
expect(client).toContain("render");
|
|
418
|
+
expect(client).not.toContain("db.get");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("allows spreading a compiler-visible local toolkit", () => {
|
|
422
|
+
const src = `"use generative";
|
|
423
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
424
|
+
import { db } from "@/db";
|
|
425
|
+
const base = defineToolkit({
|
|
426
|
+
get_weather: {
|
|
427
|
+
execute: async () => db.getWeather(),
|
|
428
|
+
render: () => null,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
export default defineToolkit({
|
|
432
|
+
...base,
|
|
433
|
+
get_time: {
|
|
434
|
+
execute: async () => db.getTime(),
|
|
435
|
+
render: () => null,
|
|
436
|
+
},
|
|
437
|
+
});`;
|
|
438
|
+
|
|
439
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
440
|
+
expect(server).toContain("...base");
|
|
441
|
+
expect(server).toContain("db.getWeather");
|
|
442
|
+
expect(server).toContain("db.getTime");
|
|
443
|
+
expect(server).not.toContain("render");
|
|
444
|
+
|
|
445
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
446
|
+
expect(client).toContain("...base");
|
|
447
|
+
expect(client).not.toContain("db.getWeather");
|
|
448
|
+
expect(client).not.toContain("db.getTime");
|
|
449
|
+
expect(client).toContain("render");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("allows directly spreading an MCP toolkit fragment", () => {
|
|
453
|
+
const src = `"use generative";
|
|
454
|
+
import { defineMcpToolkit, defineToolkit } from "@assistant-ui/react";
|
|
455
|
+
export default defineToolkit({
|
|
456
|
+
...defineMcpToolkit({
|
|
457
|
+
docs: { type: "http", url: "http://localhost:3001/mcp" },
|
|
458
|
+
}),
|
|
459
|
+
});`;
|
|
460
|
+
|
|
461
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
462
|
+
expect(server).toContain("defineMcpToolkit");
|
|
463
|
+
expect(server).toContain("docs");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("rejects opaque toolkit spreads", () => {
|
|
467
|
+
const src = `"use generative";
|
|
468
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
469
|
+
import { backendTools } from "@/backend-tools";
|
|
470
|
+
export default defineToolkit({
|
|
471
|
+
...backendTools,
|
|
472
|
+
});`;
|
|
473
|
+
|
|
474
|
+
expect(() => compileGenerative(src, { target: "client" })).toThrow(
|
|
475
|
+
/compiler-visible toolkit spread/,
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("does not treat nested toolkit declarations as compiler-visible spreads", () => {
|
|
480
|
+
const src = `"use generative";
|
|
481
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
482
|
+
function createToolkit() {
|
|
483
|
+
const base = defineToolkit({
|
|
484
|
+
get_weather: {
|
|
485
|
+
execute: async () => 1,
|
|
486
|
+
render: () => null,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
return base;
|
|
490
|
+
}
|
|
491
|
+
export default defineToolkit({
|
|
492
|
+
...base,
|
|
493
|
+
});`;
|
|
494
|
+
|
|
495
|
+
expect(() => compileGenerative(src, { target: "client" })).toThrow(
|
|
496
|
+
/compiler-visible toolkit spread/,
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("still allows a flat toolkit tool named tools", () => {
|
|
501
|
+
const src = `"use generative";
|
|
502
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
503
|
+
import { db } from "@/db";
|
|
504
|
+
export default defineToolkit({
|
|
505
|
+
tools: {
|
|
506
|
+
execute: async () => db.get(),
|
|
507
|
+
render: () => null,
|
|
508
|
+
},
|
|
509
|
+
});`;
|
|
510
|
+
|
|
511
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
512
|
+
expect(server).toContain("db.get");
|
|
513
|
+
expect(server).toContain('type: "backend"');
|
|
514
|
+
|
|
515
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
516
|
+
expect(client).toContain("render");
|
|
517
|
+
expect(client).not.toContain("db.get");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("routes renderText like a lightweight tool renderer", () => {
|
|
521
|
+
const src = `"use generative";
|
|
522
|
+
import { z } from "zod";
|
|
523
|
+
import { db } from "@/db";
|
|
524
|
+
import { formatResult } from "@/ui/format";
|
|
525
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
526
|
+
export default defineToolkit({
|
|
527
|
+
search: {
|
|
528
|
+
parameters: z.object({ query: z.string() }),
|
|
529
|
+
execute: async ({ query }) => db.search(query),
|
|
530
|
+
renderText: {
|
|
531
|
+
running: ({ args }) => \`Searching \${args.query}...\`,
|
|
532
|
+
complete: ({ args, result }) => formatResult(args.query, result),
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
});`;
|
|
536
|
+
|
|
537
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
538
|
+
expect(server).toContain("db.search");
|
|
539
|
+
expect(server).toContain('type: "backend"');
|
|
540
|
+
expect(server).not.toContain("renderText");
|
|
541
|
+
expect(server).not.toContain("@/ui/format");
|
|
542
|
+
|
|
543
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
544
|
+
expect(client.trimStart().startsWith('"use client"')).toBe(true);
|
|
545
|
+
expect(client).toContain("renderText");
|
|
546
|
+
expect(client).toContain("formatResult");
|
|
547
|
+
expect(client).toContain("@/ui/format");
|
|
548
|
+
expect(client).not.toContain("db.search");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("allows frontend tools to use renderText instead of render", () => {
|
|
552
|
+
const src = `"use generative";
|
|
553
|
+
import { z } from "zod";
|
|
554
|
+
import { track } from "@/analytics";
|
|
555
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
556
|
+
export default defineToolkit({
|
|
557
|
+
toast: {
|
|
558
|
+
parameters: z.object({ msg: z.string() }),
|
|
559
|
+
execute: async ({ msg }) => {
|
|
560
|
+
"use client";
|
|
561
|
+
return track(msg);
|
|
562
|
+
},
|
|
563
|
+
renderText: {
|
|
564
|
+
running: "Showing toast...",
|
|
565
|
+
complete: "Toast shown",
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
});`;
|
|
569
|
+
|
|
570
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
571
|
+
expect(client).toContain("track(msg)");
|
|
572
|
+
expect(client).toContain("renderText");
|
|
573
|
+
expect(client).toContain('type: "frontend"');
|
|
574
|
+
|
|
575
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
576
|
+
expect(server).not.toContain("track");
|
|
577
|
+
expect(server).not.toContain("renderText");
|
|
578
|
+
expect(server).toContain('type: "frontend"');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("infers `frontend` from execute: stubTool() and strips the executor", () => {
|
|
582
|
+
const src = `"use generative";
|
|
583
|
+
import { z } from "zod";
|
|
584
|
+
import { defineToolkit, stubTool } from "@assistant-ui/react";
|
|
585
|
+
export default defineToolkit({
|
|
586
|
+
add_task: {
|
|
587
|
+
description: "Add a task.",
|
|
588
|
+
parameters: z.object({ title: z.string() }),
|
|
589
|
+
execute: stubTool(),
|
|
590
|
+
renderText: {
|
|
591
|
+
running: "Adding task",
|
|
592
|
+
complete: "Task added",
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
});`;
|
|
596
|
+
|
|
597
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
598
|
+
expect(server).toContain('type: "frontend"');
|
|
599
|
+
expect(server).not.toContain("disabled");
|
|
600
|
+
expect(server).not.toContain("stubTool");
|
|
601
|
+
expect(server).not.toContain("execute");
|
|
602
|
+
expect(server).not.toContain("renderText");
|
|
603
|
+
|
|
604
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
605
|
+
expect(client).toContain('type: "frontend"');
|
|
606
|
+
expect(client).not.toContain("disabled");
|
|
607
|
+
expect(client).toContain("renderText");
|
|
608
|
+
expect(client).toContain("unstable_backendDefault");
|
|
609
|
+
expect(client).not.toContain("stubTool");
|
|
610
|
+
expect(client).not.toContain("execute");
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("infers `backend` from execute: externalTool(), renders on the client, and omits from the server", () => {
|
|
614
|
+
const src = `"use generative";
|
|
615
|
+
import { z } from "zod";
|
|
616
|
+
import { SearchResults } from "@/ui/search-results";
|
|
617
|
+
import { defineToolkit, externalTool } from "@assistant-ui/react";
|
|
618
|
+
export default defineToolkit({
|
|
619
|
+
web_search: {
|
|
620
|
+
description: "Search the web.",
|
|
621
|
+
parameters: z.object({ query: z.string() }),
|
|
622
|
+
execute: externalTool(),
|
|
623
|
+
render: ({ args, result }) => <SearchResults query={args.query} results={result} />,
|
|
624
|
+
},
|
|
625
|
+
});`;
|
|
626
|
+
|
|
627
|
+
const server = compileGenerative(src, { target: "server" }).code;
|
|
628
|
+
expect(server).not.toContain("web_search");
|
|
629
|
+
expect(server).not.toContain('type: "backend"');
|
|
630
|
+
expect(server).not.toContain("server-only");
|
|
631
|
+
expect(server).not.toContain("description");
|
|
632
|
+
expect(server).not.toContain("parameters");
|
|
633
|
+
expect(server).not.toContain("execute");
|
|
634
|
+
expect(server).not.toContain("externalTool");
|
|
635
|
+
expect(server).not.toContain("SearchResults");
|
|
636
|
+
expect(server).not.toContain('from "zod"');
|
|
637
|
+
|
|
638
|
+
const client = compileGenerative(src, { target: "client" }).code;
|
|
639
|
+
expect(client.trimStart().startsWith('"use client"')).toBe(true);
|
|
640
|
+
expect(client).toContain('type: "backend"');
|
|
641
|
+
expect(client).toContain("render");
|
|
642
|
+
expect(client).toContain("SearchResults");
|
|
643
|
+
expect(client).not.toContain("description");
|
|
644
|
+
expect(client).not.toContain("parameters");
|
|
645
|
+
expect(client).not.toContain("execute");
|
|
646
|
+
expect(client).not.toContain("externalTool");
|
|
647
|
+
expect(client).not.toContain('from "zod"');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("requires a renderer for external tools", () => {
|
|
651
|
+
expect(() =>
|
|
652
|
+
compileGenerative(
|
|
653
|
+
`"use generative";\nimport { defineToolkit, externalTool } from "@assistant-ui/react";\nexport default defineToolkit({ search: { execute: externalTool() } });`,
|
|
654
|
+
{ target: "client" },
|
|
655
|
+
),
|
|
656
|
+
).toThrow(/external tool must declare a `render` or `renderText`/);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
108
660
|
describe("compileGenerative — local dead-code elimination", () => {
|
|
109
661
|
const withHelpers = `"use generative";
|
|
110
662
|
import { z } from "zod";
|
|
@@ -209,6 +761,18 @@ export default defineToolkit({
|
|
|
209
761
|
).toThrow(/defineToolkit/);
|
|
210
762
|
});
|
|
211
763
|
|
|
764
|
+
it("rejects a raw default export even when a defineToolkit exists elsewhere", () => {
|
|
765
|
+
// The default export is what the runtime registers, so it must itself be
|
|
766
|
+
// wrapped — an unrelated defineToolkit() must not let a bare object through.
|
|
767
|
+
const src = `"use generative";
|
|
768
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
769
|
+
const unused = defineToolkit({ x: { execute: async () => 1, render: () => null } });
|
|
770
|
+
export default { weather: { execute: async () => 1, render: () => null } };`;
|
|
771
|
+
expect(() => compileGenerative(src, { target: "client" })).toThrow(
|
|
772
|
+
/default export must be defineToolkit/,
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
|
|
212
776
|
it("rejects a tool that isn't an inline object literal", () => {
|
|
213
777
|
expect(() =>
|
|
214
778
|
compileGenerative(
|
|
@@ -218,15 +782,24 @@ export default defineToolkit({
|
|
|
218
782
|
).toThrow(/inline object literal/);
|
|
219
783
|
});
|
|
220
784
|
|
|
221
|
-
it("requires a render for human
|
|
785
|
+
it("requires a render for human tools", () => {
|
|
222
786
|
expect(() =>
|
|
223
787
|
compileGenerative(
|
|
224
|
-
`"use generative";\nimport { defineToolkit,
|
|
788
|
+
`"use generative";\nimport { defineToolkit, humanTool } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: humanTool() } });`,
|
|
225
789
|
{ target: "client" },
|
|
226
790
|
),
|
|
227
791
|
).toThrow(/must declare a `render`/);
|
|
228
792
|
});
|
|
229
793
|
|
|
794
|
+
it("requires a render or renderText for frontend tools", () => {
|
|
795
|
+
expect(() =>
|
|
796
|
+
compileGenerative(
|
|
797
|
+
`"use generative";\nimport { defineToolkit } from "@assistant-ui/react";\nexport default defineToolkit({ toast: { execute: async () => { "use client"; return 1; } } });`,
|
|
798
|
+
{ target: "client" },
|
|
799
|
+
),
|
|
800
|
+
).toThrow(/must declare a `render` or `renderText`/);
|
|
801
|
+
});
|
|
802
|
+
|
|
230
803
|
it("requires every tool to declare an execute", () => {
|
|
231
804
|
expect(() =>
|
|
232
805
|
compileGenerative(
|
|
@@ -236,17 +809,102 @@ export default defineToolkit({
|
|
|
236
809
|
).toThrow(/must declare an `execute`/);
|
|
237
810
|
});
|
|
238
811
|
|
|
239
|
-
it("infers `human` from execute:
|
|
240
|
-
const src = `"use generative";\nimport { defineToolkit,
|
|
812
|
+
it("infers `human` from execute: humanTool() and drops it on both builds", () => {
|
|
813
|
+
const src = `"use generative";\nimport { defineToolkit, humanTool } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: humanTool(), render: () => null } });`;
|
|
241
814
|
const server = compileGenerative(src, { target: "server" }).code;
|
|
242
815
|
expect(server).toContain('type: "human"');
|
|
243
|
-
expect(server).not.toContain("
|
|
816
|
+
expect(server).not.toContain("humanTool"); // sentinel + its import pruned
|
|
244
817
|
const client = compileGenerative(src, { target: "client" }).code;
|
|
245
818
|
expect(client).toContain('type: "human"');
|
|
246
|
-
expect(client).not.toContain("
|
|
819
|
+
expect(client).not.toContain("humanTool");
|
|
247
820
|
expect(client).toContain("render");
|
|
248
821
|
});
|
|
249
822
|
|
|
823
|
+
it("keeps deprecated human-tool sentinels working", () => {
|
|
824
|
+
for (const sentinel of ["hitl", "hitlTool"]) {
|
|
825
|
+
const src = `"use generative";\nimport { defineToolkit, ${sentinel} } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: ${sentinel}(), render: () => null } });`;
|
|
826
|
+
for (const target of ["client", "server"] as const) {
|
|
827
|
+
const code = compileGenerative(src, { target }).code;
|
|
828
|
+
expect(code).toContain('type: "human"');
|
|
829
|
+
expect(code).not.toContain(sentinel);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("rejects spread properties in providerTool config", () => {
|
|
835
|
+
const src = `"use generative";
|
|
836
|
+
import { defineToolkit, providerTool } from "@assistant-ui/react";
|
|
837
|
+
const config = { providerId: "openai.web_search_preview", args: {} };
|
|
838
|
+
export default defineToolkit({
|
|
839
|
+
web_search: {
|
|
840
|
+
execute: providerTool({
|
|
841
|
+
...config,
|
|
842
|
+
}),
|
|
843
|
+
},
|
|
844
|
+
});`;
|
|
845
|
+
|
|
846
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
847
|
+
/can only contain object properties/,
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("rejects object methods in providerTool config", () => {
|
|
852
|
+
const src = `"use generative";
|
|
853
|
+
import { defineToolkit, providerTool } from "@assistant-ui/react";
|
|
854
|
+
export default defineToolkit({
|
|
855
|
+
web_search: {
|
|
856
|
+
execute: providerTool({
|
|
857
|
+
providerId: "openai.web_search_preview",
|
|
858
|
+
args: {},
|
|
859
|
+
get providerOptions() {
|
|
860
|
+
return {};
|
|
861
|
+
},
|
|
862
|
+
}),
|
|
863
|
+
},
|
|
864
|
+
});`;
|
|
865
|
+
|
|
866
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
867
|
+
/can only contain object properties/,
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it("rejects function-valued properties in providerTool config", () => {
|
|
872
|
+
const src = `"use generative";
|
|
873
|
+
import { defineToolkit, providerTool } from "@assistant-ui/react";
|
|
874
|
+
export default defineToolkit({
|
|
875
|
+
web_search: {
|
|
876
|
+
execute: providerTool({
|
|
877
|
+
providerId: "openai.web_search_preview",
|
|
878
|
+
args: {},
|
|
879
|
+
providerOptions: () => ({}),
|
|
880
|
+
}),
|
|
881
|
+
},
|
|
882
|
+
});`;
|
|
883
|
+
|
|
884
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
885
|
+
/cannot contain function-valued properties/,
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it("rejects providerTool config properties that duplicate tool properties", () => {
|
|
890
|
+
const src = `"use generative";
|
|
891
|
+
import { defineToolkit, providerTool } from "@assistant-ui/react";
|
|
892
|
+
export default defineToolkit({
|
|
893
|
+
web_search: {
|
|
894
|
+
render: () => null,
|
|
895
|
+
execute: providerTool({
|
|
896
|
+
providerId: "openai.web_search_preview",
|
|
897
|
+
args: {},
|
|
898
|
+
render: "duplicate",
|
|
899
|
+
}),
|
|
900
|
+
},
|
|
901
|
+
});`;
|
|
902
|
+
|
|
903
|
+
expect(() => compileGenerative(src, { target: "server" })).toThrow(
|
|
904
|
+
/cannot duplicate tool properties/,
|
|
905
|
+
);
|
|
906
|
+
});
|
|
907
|
+
|
|
250
908
|
it("infers `frontend` from a `use client` execute and keeps it client-side", () => {
|
|
251
909
|
const src = `"use generative";\nimport { defineToolkit } from "@assistant-ui/react";\nimport { track } from "@/a";\nexport default defineToolkit({ t: { execute: async () => { "use client"; return track(); }, render: () => null } });`;
|
|
252
910
|
const server = compileGenerative(src, { target: "server" }).code;
|