@assistant-ui/x-generative-compiler 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,115 @@ 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
+
76
+ /**
77
+ * Writes a child module to disk next to a (not-yet-written) `toolkit.tsx` and
78
+ * returns the parent's filename, so a parent compiled from a string can resolve
79
+ * `./tools/child` relative to it. Used to exercise cross-file toolkit spreads.
80
+ */
81
+ function createMergeFixture(childSource: string): string {
82
+ const appRoot = mkdtempSync(nodePath.join(tmpdir(), "aui-generative-merge-"));
83
+ const toolsRoot = nodePath.join(appRoot, "src", "tools");
84
+ mkdirSync(toolsRoot, { recursive: true });
85
+ writeFileSync(nodePath.join(toolsRoot, "weather.tsx"), childSource);
86
+ return nodePath.join(appRoot, "src", "toolkit.tsx");
87
+ }
88
+
89
+ /**
90
+ * Like {@link createMergeFixture}, but also writes a (JSONC) `tsconfig.json`
91
+ * with a `@/*` path alias so alias resolution can be exercised. The child lives
92
+ * at `src/tools/weather.tsx` and `@/*` maps to `./src/*`.
93
+ */
94
+ function createAliasMergeFixture(childSource: string): string {
95
+ const filename = createMergeFixture(childSource);
96
+ const appRoot = nodePath.dirname(nodePath.dirname(filename));
97
+ writeFileSync(
98
+ nodePath.join(appRoot, "tsconfig.json"),
99
+ `{
100
+ // path aliases
101
+ "compilerOptions": {
102
+ "baseUrl": ".",
103
+ "paths": { "@/*": ["./src/*"] },
104
+ }
105
+ }`,
106
+ );
107
+ return filename;
108
+ }
109
+
110
+ const generativeChild = `"use generative";
111
+ import { defineToolkit } from "@assistant-ui/react";
112
+ export default defineToolkit({
113
+ get_weather: {
114
+ execute: async () => 1,
115
+ render: () => null,
116
+ },
117
+ });
118
+ `;
119
+
8
120
  const source = `"use generative";
9
121
  import { z } from "zod";
10
122
  import { db } from "@/db";
@@ -34,6 +146,49 @@ export default defineToolkit({
34
146
  const server = () => compileGenerative(source, { target: "server" }).code;
35
147
  const client = () => compileGenerative(source, { target: "client" }).code;
36
148
 
149
+ describe("compileGenerative — core/compiler compatibility", () => {
150
+ it("allows a compiler version that satisfies core's optionalDevDependencies range", () => {
151
+ const filename = createCompatibilityFixture(">=0.0.0");
152
+
153
+ expect(() =>
154
+ compileGenerative(minimalSource, { target: "server", filename }),
155
+ ).not.toThrow();
156
+ });
157
+
158
+ it("throws when core requires a different compiler range", () => {
159
+ const filename = createCompatibilityFixture("<0.0.3");
160
+
161
+ expect(() =>
162
+ compileGenerative(minimalSource, { target: "server", filename }),
163
+ ).toThrow(/requires @assistant-ui\/x-generative-compiler <0\.0\.3/);
164
+ });
165
+
166
+ it("skips the check for older core packages without compatibility metadata", () => {
167
+ const filename = createCompatibilityFixture(null);
168
+
169
+ expect(() =>
170
+ compileGenerative(minimalSource, { target: "server", filename }),
171
+ ).not.toThrow();
172
+ });
173
+
174
+ it("wraps malformed package metadata in a compile error", () => {
175
+ const filename = createCompatibilityFixture(">=0.0.0");
176
+ const packageJsonPath = nodePath.join(
177
+ nodePath.dirname(filename),
178
+ "..",
179
+ "node_modules",
180
+ "@assistant-ui",
181
+ "react",
182
+ "package.json",
183
+ );
184
+ writeFileSync(packageJsonPath, "{");
185
+
186
+ expect(() =>
187
+ compileGenerative(minimalSource, { target: "server", filename }),
188
+ ).toThrow(GenerativeCompileError);
189
+ });
190
+ });
191
+
37
192
  describe("compileGenerative — server target", () => {
38
193
  const code = server();
39
194
 
@@ -54,6 +209,10 @@ describe("compileGenerative — server target", () => {
54
209
  expect(code).toContain('type: "frontend"'); // toast: execute + "use client"
55
210
  });
56
211
 
212
+ it("does not mark server tools with backend defaults", () => {
213
+ expect(code).not.toContain("unstable_backendDefault");
214
+ });
215
+
57
216
  it("drops all render and its client imports", () => {
58
217
  expect(code).not.toContain("Chart");
59
218
  expect(code).not.toContain("<div");
@@ -98,6 +257,29 @@ describe("compileGenerative — client target", () => {
98
257
  expect(code).toContain('type: "frontend"'); // toast
99
258
  });
100
259
 
260
+ it("marks generated frontend tools with backend parameter defaults", () => {
261
+ expect(code).toContain("unstable_backendDefault: {");
262
+ expect(code).toContain("parameters: true");
263
+ });
264
+
265
+ it("marks generated human tools with backend parameter defaults", () => {
266
+ const src = `"use generative";
267
+ import { defineToolkit, humanTool } from "@assistant-ui/react";
268
+ export default defineToolkit({
269
+ ask: {
270
+ parameters: { type: "object", properties: {} },
271
+ execute: humanTool(),
272
+ render: () => null,
273
+ },
274
+ });
275
+ `;
276
+
277
+ const code = compileGenerative(src, { target: "client" }).code;
278
+ expect(code).toContain('type: "human"');
279
+ expect(code).toContain("unstable_backendDefault: {");
280
+ expect(code).toContain("parameters: true");
281
+ });
282
+
101
283
  it("drops a backend execute and its server-only imports", () => {
102
284
  expect(code).not.toContain("db.weather.get");
103
285
  expect(code).not.toContain("@/db");
@@ -105,6 +287,553 @@ describe("compileGenerative — client target", () => {
105
287
  });
106
288
  });
107
289
 
290
+ describe("compileGenerative — generative UI library", () => {
291
+ const generative = `"use generative";
292
+ import { z } from "zod";
293
+ import { Chart } from "@/ui/chart";
294
+ import { defineToolkit } from "@assistant-ui/react";
295
+ import {
296
+ JSONGenerativeUI,
297
+ defineGenerativeComponents,
298
+ } from "@assistant-ui/react-generative-ui";
299
+
300
+ const generative = new JSONGenerativeUI({
301
+ library: defineGenerativeComponents({
302
+ Chart: {
303
+ description: "A chart.",
304
+ properties: z.object({ data: z.array(z.number()) }),
305
+ render: (props) => <Chart {...props} />,
306
+ },
307
+ }),
308
+ });
309
+
310
+ export default defineToolkit({
311
+ present: generative.present(),
312
+ prompt_user: generative.promptUser(),
313
+ });
314
+ `;
315
+
316
+ it("keeps the schema and the JSONGenerativeUI instance on the server, drops render", () => {
317
+ const code = compileGenerative(generative, { target: "server" }).code;
318
+ expect(code).toContain("new JSONGenerativeUI");
319
+ expect(code).toContain("generative.present()");
320
+ expect(code).toContain("generative.promptUser()");
321
+ expect(code).toContain("z.object");
322
+ expect(code).toContain('description: "A chart."');
323
+ // render and its client-only import are gone on the server
324
+ expect(code).not.toMatch(/render\s*:/);
325
+ expect(code).not.toContain("Chart }"); // the @/ui/chart import specifier
326
+ expect(code).not.toContain("@/ui/chart");
327
+ expect(code).not.toContain("<Chart");
328
+ // the markers themselves are unwrapped and their import pruned
329
+ expect(code).not.toContain("defineToolkit");
330
+ expect(code).not.toContain("defineGenerativeComponents");
331
+ // no backend execute anywhere → no server-only guard, no use client
332
+ expect(code).not.toContain("server-only");
333
+ expect(code).not.toContain("use client");
334
+ });
335
+
336
+ it("keeps render (and marks use client) on the client", () => {
337
+ const code = compileGenerative(generative, { target: "client" }).code;
338
+ expect(code.trimStart().startsWith('"use client"')).toBe(true);
339
+ expect(code).toContain("<Chart");
340
+ expect(code).toContain('import { Chart } from "@/ui/chart"');
341
+ expect(code).toContain("new JSONGenerativeUI");
342
+ expect(code).toContain("generative.present()");
343
+ // the JSONGenerativeUI import survives on both builds
344
+ expect(code).toContain("@assistant-ui/react-generative-ui");
345
+ expect(code).not.toContain("defineGenerativeComponents");
346
+ });
347
+
348
+ it("rejects an unknown method on a JSONGenerativeUI instance", () => {
349
+ const src = `"use generative";
350
+ import { defineToolkit } from "@assistant-ui/react";
351
+ import { JSONGenerativeUI } from "@assistant-ui/react-generative-ui";
352
+ const ui = new JSONGenerativeUI({ library: {} });
353
+ export default defineToolkit({ present: ui.notARealMethod() });`;
354
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
355
+ /inline object literal/,
356
+ );
357
+ });
358
+
359
+ it("rejects a method call on an unknown (non-JSONGenerativeUI) object", () => {
360
+ const src = `"use generative";
361
+ import { defineToolkit } from "@assistant-ui/react";
362
+ import { other } from "@/other";
363
+ export default defineToolkit({ present: other.present() });`;
364
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
365
+ /inline object literal/,
366
+ );
367
+ });
368
+
369
+ it("does not treat nested JSONGenerativeUI instances as module-scope instances", () => {
370
+ const src = `"use generative";
371
+ import { defineToolkit } from "@assistant-ui/react";
372
+ import { JSONGenerativeUI } from "@assistant-ui/react-generative-ui";
373
+ function create() {
374
+ const ui = new JSONGenerativeUI({ library: {} });
375
+ return ui;
376
+ }
377
+ export default defineToolkit({ present: ui.present() });`;
378
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
379
+ /inline object literal/,
380
+ );
381
+ });
382
+
383
+ it("allows a generative tool alongside an inline tool", () => {
384
+ const src = `"use generative";
385
+ import { z } from "zod";
386
+ import { db } from "@/db";
387
+ import { defineToolkit } from "@assistant-ui/react";
388
+ import { JSONGenerativeUI, defineGenerativeComponents } from "@assistant-ui/react-generative-ui";
389
+ const ui = new JSONGenerativeUI({ library: defineGenerativeComponents({ Box: { description: "b", properties: z.object({}), render: () => null } }) });
390
+ export default defineToolkit({
391
+ present: ui.present(),
392
+ weather: {
393
+ description: "w",
394
+ parameters: z.object({ city: z.string() }),
395
+ execute: async ({ city }) => db.get(city),
396
+ render: () => null,
397
+ },
398
+ });`;
399
+ const server = compileGenerative(src, { target: "server" }).code;
400
+ expect(server).toContain("ui.present()");
401
+ expect(server).toContain("db.get");
402
+ expect(server).toContain('type: "backend"');
403
+ expect(server).toContain("server-only");
404
+ const client = compileGenerative(src, { target: "client" }).code;
405
+ expect(client).toContain("ui.present()");
406
+ expect(client).not.toContain("db.get");
407
+ expect(client.trimStart().startsWith('"use client"')).toBe(true);
408
+ });
409
+
410
+ it("routes provider tool config", () => {
411
+ const src = `"use generative";
412
+ import { defineToolkit, providerTool } from "@assistant-ui/react";
413
+ export default defineToolkit({
414
+ web_search: {
415
+ execute: providerTool({
416
+ providerId: "openai.web_search_preview",
417
+ args: { searchContextSize: "low" },
418
+ providerOptions: { openai: { rankingOptions: { scoreThreshold: 0.5 } } },
419
+ }),
420
+ },
421
+ });`;
422
+
423
+ const server = compileGenerative(src, { target: "server" }).code;
424
+ expect(server).toContain('type: "provider"');
425
+ expect(server).toContain("openai.web_search_preview");
426
+ expect(server).toContain("searchContextSize");
427
+ expect(server).toContain("providerOptions");
428
+ expect(server).not.toContain("providerTool");
429
+
430
+ const client = compileGenerative(src, { target: "client" }).code;
431
+ expect(client).toContain('type: "provider"');
432
+ expect(client).toContain("openai.web_search_preview");
433
+ expect(client).toContain("providerOptions");
434
+ expect(client).not.toContain("providerTool");
435
+ });
436
+
437
+ it("routes flat toolkit tools and preserves spread MCP config", () => {
438
+ const src = `"use generative";
439
+ import { z } from "zod";
440
+ import { db } from "@/db";
441
+ import { defineMcpToolkit, defineToolkit } from "@assistant-ui/react";
442
+ const mcp = defineMcpToolkit({
443
+ docs: { type: "http", url: "http://localhost:3001/mcp" },
444
+ });
445
+ export default defineToolkit({
446
+ ...mcp,
447
+ weather: {
448
+ parameters: z.object({ city: z.string() }),
449
+ execute: async ({ city }) => db.get(city),
450
+ render: () => null,
451
+ },
452
+ });`;
453
+
454
+ const server = compileGenerative(src, { target: "server" }).code;
455
+ expect(server).toContain("...mcp");
456
+ expect(server).toContain("db.get");
457
+ expect(server).not.toContain("render");
458
+
459
+ const client = compileGenerative(src, { target: "client" }).code;
460
+ expect(client).toContain("...mcp");
461
+ expect(client).toContain("render");
462
+ expect(client).not.toContain("db.get");
463
+ });
464
+
465
+ it("allows spreading a compiler-visible local toolkit", () => {
466
+ const src = `"use generative";
467
+ import { defineToolkit } from "@assistant-ui/react";
468
+ import { db } from "@/db";
469
+ const base = defineToolkit({
470
+ get_weather: {
471
+ execute: async () => db.getWeather(),
472
+ render: () => null,
473
+ },
474
+ });
475
+ export default defineToolkit({
476
+ ...base,
477
+ get_time: {
478
+ execute: async () => db.getTime(),
479
+ render: () => null,
480
+ },
481
+ });`;
482
+
483
+ const server = compileGenerative(src, { target: "server" }).code;
484
+ expect(server).toContain("...base");
485
+ expect(server).toContain("db.getWeather");
486
+ expect(server).toContain("db.getTime");
487
+ expect(server).not.toContain("render");
488
+
489
+ const client = compileGenerative(src, { target: "client" }).code;
490
+ expect(client).toContain("...base");
491
+ expect(client).not.toContain("db.getWeather");
492
+ expect(client).not.toContain("db.getTime");
493
+ expect(client).toContain("render");
494
+ });
495
+
496
+ it("allows directly spreading an MCP toolkit fragment", () => {
497
+ const src = `"use generative";
498
+ import { defineMcpToolkit, defineToolkit } from "@assistant-ui/react";
499
+ export default defineToolkit({
500
+ ...defineMcpToolkit({
501
+ docs: { type: "http", url: "http://localhost:3001/mcp" },
502
+ }),
503
+ });`;
504
+
505
+ const server = compileGenerative(src, { target: "server" }).code;
506
+ expect(server).toContain("defineMcpToolkit");
507
+ expect(server).toContain("docs");
508
+ });
509
+
510
+ it("rejects opaque toolkit spreads", () => {
511
+ const src = `"use generative";
512
+ import { defineToolkit } from "@assistant-ui/react";
513
+ import { backendTools } from "@/backend-tools";
514
+ export default defineToolkit({
515
+ ...backendTools,
516
+ });`;
517
+
518
+ expect(() => compileGenerative(src, { target: "client" })).toThrow(
519
+ /compiler-visible toolkit spread/,
520
+ );
521
+ });
522
+
523
+ it("does not treat nested toolkit declarations as compiler-visible spreads", () => {
524
+ const src = `"use generative";
525
+ import { defineToolkit } from "@assistant-ui/react";
526
+ function createToolkit() {
527
+ const base = defineToolkit({
528
+ get_weather: {
529
+ execute: async () => 1,
530
+ render: () => null,
531
+ },
532
+ });
533
+ return base;
534
+ }
535
+ export default defineToolkit({
536
+ ...base,
537
+ });`;
538
+
539
+ expect(() => compileGenerative(src, { target: "client" })).toThrow(
540
+ /compiler-visible toolkit spread/,
541
+ );
542
+ });
543
+
544
+ it("allows spreading the default import of a generative module", () => {
545
+ const filename = createMergeFixture(generativeChild);
546
+ const src = `"use generative";
547
+ import { defineToolkit } from "@assistant-ui/react";
548
+ import weatherTools from "./tools/weather";
549
+ export default defineToolkit({
550
+ ...weatherTools,
551
+ });`;
552
+
553
+ const server = compileGenerative(src, { target: "server", filename }).code;
554
+ expect(server).toContain("...weatherTools");
555
+ expect(server).toContain('from "./tools/weather"');
556
+
557
+ const client = compileGenerative(src, { target: "client", filename }).code;
558
+ expect(client).toContain("...weatherTools");
559
+ expect(client).toContain('from "./tools/weather"');
560
+ });
561
+
562
+ it("rejects spreading the default import of a non-generative module", () => {
563
+ const filename = createMergeFixture(
564
+ `export default { get_weather: { execute: async () => 1 } };\n`,
565
+ );
566
+ const src = `"use generative";
567
+ import { defineToolkit } from "@assistant-ui/react";
568
+ import weatherTools from "./tools/weather";
569
+ export default defineToolkit({
570
+ ...weatherTools,
571
+ });`;
572
+
573
+ expect(() =>
574
+ compileGenerative(src, { target: "client", filename }),
575
+ ).toThrow(/compiler-visible toolkit spread/);
576
+ });
577
+
578
+ it("rejects spreading a named import even from a generative module", () => {
579
+ const filename = createMergeFixture(generativeChild);
580
+ const src = `"use generative";
581
+ import { defineToolkit } from "@assistant-ui/react";
582
+ import { weatherTools } from "./tools/weather";
583
+ export default defineToolkit({
584
+ ...weatherTools,
585
+ });`;
586
+
587
+ expect(() =>
588
+ compileGenerative(src, { target: "client", filename }),
589
+ ).toThrow(/compiler-visible toolkit spread/);
590
+ });
591
+
592
+ it("resolves a tsconfig path alias when spreading a generative module", () => {
593
+ const filename = createAliasMergeFixture(generativeChild);
594
+ const src = `"use generative";
595
+ import { defineToolkit } from "@assistant-ui/react";
596
+ import weatherTools from "@/tools/weather";
597
+ export default defineToolkit({
598
+ ...weatherTools,
599
+ });`;
600
+
601
+ const client = compileGenerative(src, { target: "client", filename }).code;
602
+ expect(client).toContain("...weatherTools");
603
+ expect(client).toContain('from "@/tools/weather"');
604
+ });
605
+
606
+ it("prefers the most specific tsconfig path alias", () => {
607
+ const appRoot = mkdtempSync(
608
+ nodePath.join(tmpdir(), "aui-generative-alias-spec-"),
609
+ );
610
+ mkdirSync(nodePath.join(appRoot, "src", "tools"), { recursive: true });
611
+ mkdirSync(nodePath.join(appRoot, "generated"), { recursive: true });
612
+ // The broader `@/*` alias would resolve here — a non-generative module.
613
+ writeFileSync(
614
+ nodePath.join(appRoot, "src", "tools", "weather.tsx"),
615
+ `export default { get_weather: { execute: async () => 1 } };\n`,
616
+ );
617
+ // The more specific `@/tools/*` alias resolves to the generative module.
618
+ writeFileSync(
619
+ nodePath.join(appRoot, "generated", "weather.tsx"),
620
+ generativeChild,
621
+ );
622
+ writeFileSync(
623
+ nodePath.join(appRoot, "tsconfig.json"),
624
+ `{
625
+ "compilerOptions": {
626
+ "baseUrl": ".",
627
+ "paths": {
628
+ "@/*": ["./src/*"],
629
+ "@/tools/*": ["./generated/*"]
630
+ }
631
+ }
632
+ }`,
633
+ );
634
+ const filename = nodePath.join(appRoot, "src", "toolkit.tsx");
635
+ const src = `"use generative";
636
+ import { defineToolkit } from "@assistant-ui/react";
637
+ import weatherTools from "@/tools/weather";
638
+ export default defineToolkit({
639
+ ...weatherTools,
640
+ });`;
641
+
642
+ // `@/tools/*` is more specific than `@/*`, so it wins and the spread is the
643
+ // generative module (allowed) rather than the non-generative `@/*` target.
644
+ const client = compileGenerative(src, { target: "client", filename }).code;
645
+ expect(client).toContain("...weatherTools");
646
+ });
647
+
648
+ it("resolves a .js specifier to its .tsx source when spreading", () => {
649
+ const filename = createMergeFixture(generativeChild);
650
+ const src = `"use generative";
651
+ import { defineToolkit } from "@assistant-ui/react";
652
+ import weatherTools from "./tools/weather.js";
653
+ export default defineToolkit({
654
+ ...weatherTools,
655
+ });`;
656
+
657
+ const client = compileGenerative(src, { target: "client", filename }).code;
658
+ expect(client).toContain("...weatherTools");
659
+ });
660
+
661
+ it("allows defineMcpToolkit as the default export", () => {
662
+ const src = `"use generative";
663
+ import { defineMcpToolkit } from "@assistant-ui/react";
664
+ export default defineMcpToolkit({
665
+ docs: { type: "http", url: "https://mcp.example.com/mcp" },
666
+ });`;
667
+
668
+ const server = compileGenerative(src, { target: "server" }).code;
669
+ expect(server).toContain("defineMcpToolkit");
670
+ expect(server).toContain("docs");
671
+
672
+ const client = compileGenerative(src, { target: "client" }).code;
673
+ expect(client).toContain("defineMcpToolkit");
674
+ expect(client).toContain("docs");
675
+ });
676
+
677
+ it("still allows a flat toolkit tool named tools", () => {
678
+ const src = `"use generative";
679
+ import { defineToolkit } from "@assistant-ui/react";
680
+ import { db } from "@/db";
681
+ export default defineToolkit({
682
+ tools: {
683
+ execute: async () => db.get(),
684
+ render: () => null,
685
+ },
686
+ });`;
687
+
688
+ const server = compileGenerative(src, { target: "server" }).code;
689
+ expect(server).toContain("db.get");
690
+ expect(server).toContain('type: "backend"');
691
+
692
+ const client = compileGenerative(src, { target: "client" }).code;
693
+ expect(client).toContain("render");
694
+ expect(client).not.toContain("db.get");
695
+ });
696
+
697
+ it("routes renderText like a lightweight tool renderer", () => {
698
+ const src = `"use generative";
699
+ import { z } from "zod";
700
+ import { db } from "@/db";
701
+ import { formatResult } from "@/ui/format";
702
+ import { defineToolkit } from "@assistant-ui/react";
703
+ export default defineToolkit({
704
+ search: {
705
+ parameters: z.object({ query: z.string() }),
706
+ execute: async ({ query }) => db.search(query),
707
+ renderText: {
708
+ running: ({ args }) => \`Searching \${args.query}...\`,
709
+ complete: ({ args, result }) => formatResult(args.query, result),
710
+ },
711
+ },
712
+ });`;
713
+
714
+ const server = compileGenerative(src, { target: "server" }).code;
715
+ expect(server).toContain("db.search");
716
+ expect(server).toContain('type: "backend"');
717
+ expect(server).not.toContain("renderText");
718
+ expect(server).not.toContain("@/ui/format");
719
+
720
+ const client = compileGenerative(src, { target: "client" }).code;
721
+ expect(client.trimStart().startsWith('"use client"')).toBe(true);
722
+ expect(client).toContain("renderText");
723
+ expect(client).toContain("formatResult");
724
+ expect(client).toContain("@/ui/format");
725
+ expect(client).not.toContain("db.search");
726
+ });
727
+
728
+ it("allows frontend tools to use renderText instead of render", () => {
729
+ const src = `"use generative";
730
+ import { z } from "zod";
731
+ import { track } from "@/analytics";
732
+ import { defineToolkit } from "@assistant-ui/react";
733
+ export default defineToolkit({
734
+ toast: {
735
+ parameters: z.object({ msg: z.string() }),
736
+ execute: async ({ msg }) => {
737
+ "use client";
738
+ return track(msg);
739
+ },
740
+ renderText: {
741
+ running: "Showing toast...",
742
+ complete: "Toast shown",
743
+ },
744
+ },
745
+ });`;
746
+
747
+ const client = compileGenerative(src, { target: "client" }).code;
748
+ expect(client).toContain("track(msg)");
749
+ expect(client).toContain("renderText");
750
+ expect(client).toContain('type: "frontend"');
751
+
752
+ const server = compileGenerative(src, { target: "server" }).code;
753
+ expect(server).not.toContain("track");
754
+ expect(server).not.toContain("renderText");
755
+ expect(server).toContain('type: "frontend"');
756
+ });
757
+
758
+ it("infers `frontend` from execute: stubTool() and strips the executor", () => {
759
+ const src = `"use generative";
760
+ import { z } from "zod";
761
+ import { defineToolkit, stubTool } from "@assistant-ui/react";
762
+ export default defineToolkit({
763
+ add_task: {
764
+ description: "Add a task.",
765
+ parameters: z.object({ title: z.string() }),
766
+ execute: stubTool(),
767
+ renderText: {
768
+ running: "Adding task",
769
+ complete: "Task added",
770
+ },
771
+ },
772
+ });`;
773
+
774
+ const server = compileGenerative(src, { target: "server" }).code;
775
+ expect(server).toContain('type: "frontend"');
776
+ expect(server).not.toContain("disabled");
777
+ expect(server).not.toContain("stubTool");
778
+ expect(server).not.toContain("execute");
779
+ expect(server).not.toContain("renderText");
780
+
781
+ const client = compileGenerative(src, { target: "client" }).code;
782
+ expect(client).toContain('type: "frontend"');
783
+ expect(client).not.toContain("disabled");
784
+ expect(client).toContain("renderText");
785
+ expect(client).toContain("unstable_backendDefault");
786
+ expect(client).not.toContain("stubTool");
787
+ expect(client).not.toContain("execute");
788
+ });
789
+
790
+ it("infers `backend` from execute: externalTool(), renders on the client, and omits from the server", () => {
791
+ const src = `"use generative";
792
+ import { z } from "zod";
793
+ import { SearchResults } from "@/ui/search-results";
794
+ import { defineToolkit, externalTool } from "@assistant-ui/react";
795
+ export default defineToolkit({
796
+ web_search: {
797
+ description: "Search the web.",
798
+ parameters: z.object({ query: z.string() }),
799
+ execute: externalTool(),
800
+ render: ({ args, result }) => <SearchResults query={args.query} results={result} />,
801
+ },
802
+ });`;
803
+
804
+ const server = compileGenerative(src, { target: "server" }).code;
805
+ expect(server).not.toContain("web_search");
806
+ expect(server).not.toContain('type: "backend"');
807
+ expect(server).not.toContain("server-only");
808
+ expect(server).not.toContain("description");
809
+ expect(server).not.toContain("parameters");
810
+ expect(server).not.toContain("execute");
811
+ expect(server).not.toContain("externalTool");
812
+ expect(server).not.toContain("SearchResults");
813
+ expect(server).not.toContain('from "zod"');
814
+
815
+ const client = compileGenerative(src, { target: "client" }).code;
816
+ expect(client.trimStart().startsWith('"use client"')).toBe(true);
817
+ expect(client).toContain('type: "backend"');
818
+ expect(client).toContain("render");
819
+ expect(client).toContain("SearchResults");
820
+ expect(client).not.toContain("description");
821
+ expect(client).not.toContain("parameters");
822
+ expect(client).not.toContain("execute");
823
+ expect(client).not.toContain("externalTool");
824
+ expect(client).not.toContain('from "zod"');
825
+ });
826
+
827
+ it("requires a renderer for external tools", () => {
828
+ expect(() =>
829
+ compileGenerative(
830
+ `"use generative";\nimport { defineToolkit, externalTool } from "@assistant-ui/react";\nexport default defineToolkit({ search: { execute: externalTool() } });`,
831
+ { target: "client" },
832
+ ),
833
+ ).toThrow(/external tool must declare a `render` or `renderText`/);
834
+ });
835
+ });
836
+
108
837
  describe("compileGenerative — local dead-code elimination", () => {
109
838
  const withHelpers = `"use generative";
110
839
  import { z } from "zod";
@@ -209,6 +938,18 @@ export default defineToolkit({
209
938
  ).toThrow(/defineToolkit/);
210
939
  });
211
940
 
941
+ it("rejects a raw default export even when a defineToolkit exists elsewhere", () => {
942
+ // The default export is what the runtime registers, so it must itself be
943
+ // wrapped — an unrelated defineToolkit() must not let a bare object through.
944
+ const src = `"use generative";
945
+ import { defineToolkit } from "@assistant-ui/react";
946
+ const unused = defineToolkit({ x: { execute: async () => 1, render: () => null } });
947
+ export default { weather: { execute: async () => 1, render: () => null } };`;
948
+ expect(() => compileGenerative(src, { target: "client" })).toThrow(
949
+ /default export must be defineToolkit/,
950
+ );
951
+ });
952
+
212
953
  it("rejects a tool that isn't an inline object literal", () => {
213
954
  expect(() =>
214
955
  compileGenerative(
@@ -218,15 +959,24 @@ export default defineToolkit({
218
959
  ).toThrow(/inline object literal/);
219
960
  });
220
961
 
221
- it("requires a render for human/frontend tools", () => {
962
+ it("requires a render for human tools", () => {
222
963
  expect(() =>
223
964
  compileGenerative(
224
- `"use generative";\nimport { defineToolkit, hitl } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: hitl() } });`,
965
+ `"use generative";\nimport { defineToolkit, humanTool } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: humanTool() } });`,
225
966
  { target: "client" },
226
967
  ),
227
968
  ).toThrow(/must declare a `render`/);
228
969
  });
229
970
 
971
+ it("requires a render or renderText for frontend tools", () => {
972
+ expect(() =>
973
+ compileGenerative(
974
+ `"use generative";\nimport { defineToolkit } from "@assistant-ui/react";\nexport default defineToolkit({ toast: { execute: async () => { "use client"; return 1; } } });`,
975
+ { target: "client" },
976
+ ),
977
+ ).toThrow(/must declare a `render` or `renderText`/);
978
+ });
979
+
230
980
  it("requires every tool to declare an execute", () => {
231
981
  expect(() =>
232
982
  compileGenerative(
@@ -236,17 +986,102 @@ export default defineToolkit({
236
986
  ).toThrow(/must declare an `execute`/);
237
987
  });
238
988
 
239
- it("infers `human` from execute: hitl() and drops it on both builds", () => {
240
- const src = `"use generative";\nimport { defineToolkit, hitl } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: hitl(), render: () => null } });`;
989
+ it("infers `human` from execute: humanTool() and drops it on both builds", () => {
990
+ const src = `"use generative";\nimport { defineToolkit, humanTool } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: humanTool(), render: () => null } });`;
241
991
  const server = compileGenerative(src, { target: "server" }).code;
242
992
  expect(server).toContain('type: "human"');
243
- expect(server).not.toContain("hitl"); // sentinel + its import pruned
993
+ expect(server).not.toContain("humanTool"); // sentinel + its import pruned
244
994
  const client = compileGenerative(src, { target: "client" }).code;
245
995
  expect(client).toContain('type: "human"');
246
- expect(client).not.toContain("hitl");
996
+ expect(client).not.toContain("humanTool");
247
997
  expect(client).toContain("render");
248
998
  });
249
999
 
1000
+ it("keeps deprecated human-tool sentinels working", () => {
1001
+ for (const sentinel of ["hitl", "hitlTool"]) {
1002
+ const src = `"use generative";\nimport { defineToolkit, ${sentinel} } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: ${sentinel}(), render: () => null } });`;
1003
+ for (const target of ["client", "server"] as const) {
1004
+ const code = compileGenerative(src, { target }).code;
1005
+ expect(code).toContain('type: "human"');
1006
+ expect(code).not.toContain(sentinel);
1007
+ }
1008
+ }
1009
+ });
1010
+
1011
+ it("rejects spread properties in providerTool config", () => {
1012
+ const src = `"use generative";
1013
+ import { defineToolkit, providerTool } from "@assistant-ui/react";
1014
+ const config = { providerId: "openai.web_search_preview", args: {} };
1015
+ export default defineToolkit({
1016
+ web_search: {
1017
+ execute: providerTool({
1018
+ ...config,
1019
+ }),
1020
+ },
1021
+ });`;
1022
+
1023
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
1024
+ /can only contain object properties/,
1025
+ );
1026
+ });
1027
+
1028
+ it("rejects object methods in providerTool config", () => {
1029
+ const src = `"use generative";
1030
+ import { defineToolkit, providerTool } from "@assistant-ui/react";
1031
+ export default defineToolkit({
1032
+ web_search: {
1033
+ execute: providerTool({
1034
+ providerId: "openai.web_search_preview",
1035
+ args: {},
1036
+ get providerOptions() {
1037
+ return {};
1038
+ },
1039
+ }),
1040
+ },
1041
+ });`;
1042
+
1043
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
1044
+ /can only contain object properties/,
1045
+ );
1046
+ });
1047
+
1048
+ it("rejects function-valued properties in providerTool config", () => {
1049
+ const src = `"use generative";
1050
+ import { defineToolkit, providerTool } from "@assistant-ui/react";
1051
+ export default defineToolkit({
1052
+ web_search: {
1053
+ execute: providerTool({
1054
+ providerId: "openai.web_search_preview",
1055
+ args: {},
1056
+ providerOptions: () => ({}),
1057
+ }),
1058
+ },
1059
+ });`;
1060
+
1061
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
1062
+ /cannot contain function-valued properties/,
1063
+ );
1064
+ });
1065
+
1066
+ it("rejects providerTool config properties that duplicate tool properties", () => {
1067
+ const src = `"use generative";
1068
+ import { defineToolkit, providerTool } from "@assistant-ui/react";
1069
+ export default defineToolkit({
1070
+ web_search: {
1071
+ render: () => null,
1072
+ execute: providerTool({
1073
+ providerId: "openai.web_search_preview",
1074
+ args: {},
1075
+ render: "duplicate",
1076
+ }),
1077
+ },
1078
+ });`;
1079
+
1080
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
1081
+ /cannot duplicate tool properties/,
1082
+ );
1083
+ });
1084
+
250
1085
  it("infers `frontend` from a `use client` execute and keeps it client-side", () => {
251
1086
  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
1087
  const server = compileGenerative(src, { target: "server" }).code;