@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.
@@ -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/frontend tools", () => {
785
+ it("requires a render for human tools", () => {
222
786
  expect(() =>
223
787
  compileGenerative(
224
- `"use generative";\nimport { defineToolkit, hitl } from "@assistant-ui/react";\nexport default defineToolkit({ ask: { execute: hitl() } });`,
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: 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 } });`;
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("hitl"); // sentinel + its import pruned
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("hitl");
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;