e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
156 lines
7.3 KiB
Markdown
156 lines
7.3 KiB
Markdown
# `"use generative"` — specification
|
|
|
|
## Problem
|
|
|
|
A tool has three regions of code with three different deployment targets:
|
|
|
|
| region | server (registration + agent loop) | client (browser) |
|
|
| --------------------------- | ---------------------------------- | ---------------- |
|
|
| `description` / `parameters`| needed (→ LLM, parse) | needed (→ parse) |
|
|
| `render` | **must not load** (React/CSS/DOM) | needed |
|
|
| `execute` | depends on kind (see routing) | depends on kind |
|
|
|
|
We want to **colocate all three in one source file** for DX, but keep `render`'s
|
|
client deps out of the server bundle and — more importantly — keep a backend
|
|
`execute`'s server deps (DB handles, API keys, server SDKs) out of the **client**
|
|
bundle. The second direction is a *security* boundary, not just bundle hygiene.
|
|
|
|
`"use client"` cannot express this: it is whole-module, so a `"use client"`
|
|
generative module would also turn `parameters` into a client reference on the server,
|
|
making the zod schema unreadable server-side. We need **sub-module, per-property**
|
|
routing. That is what the `"use generative"` directive provides.
|
|
|
|
## The directive
|
|
|
|
A module opts in with a leading directive and a single default export wrapped in
|
|
`defineToolkit`:
|
|
|
|
```tsx
|
|
"use generative";
|
|
import { z } from "zod";
|
|
import { defineToolkit } from "@assistant-ui/react";
|
|
import { db } from "@/db"; // server-only dependency
|
|
import { Chart } from "@/ui/chart"; // client-only dependency
|
|
|
|
export default defineToolkit({
|
|
weather: {
|
|
description: "Show the weather for a city.",
|
|
parameters: z.object({ city: z.string() }),
|
|
execute: async ({ city }) => db.weather.get(city), // backend (server-only)
|
|
render: (props) => <Chart data={props} />, // client-only
|
|
},
|
|
});
|
|
```
|
|
|
|
The file is imported from both server and client code; the compiler emits a
|
|
different module per build target so each side only loads what it needs.
|
|
|
|
## Routing (by inferred kind)
|
|
|
|
A tool's kind is **not authored** — declaring a `type` field is a type error. The
|
|
compiler infers it from the `execute` and writes the resolved `type` back into
|
|
each emitted tool object (so the runtime keeps it):
|
|
|
|
| how it's authored | inferred kind | `render` | `execute` |
|
|
| ----------------------------------------- | ------------- | -------- | -------------------------------- |
|
|
| `execute` with a `"use client"` directive | `frontend` | client | **client** (bundled with render) |
|
|
| `execute` (plain) | `backend` | client | **server** (`server-only` leaf) |
|
|
| `execute: humanTool()` | `human` | client | — (dropped; the UI resolves it) |
|
|
|
|
Consequences:
|
|
|
|
- For `frontend` entries the server keeps **schema only** — render *and* execute
|
|
are client concerns.
|
|
- `backend` is the only kind that produces the server-only secrets boundary; its
|
|
`execute` leaf imports `server-only`, so any routing mistake that pulls it into
|
|
the client build fails the build instead of leaking secrets.
|
|
- **Server-by-default is the safe default:** a plain `execute` stays server-only,
|
|
so a forgotten marker can't leak — you opt *into* the client with `"use client"`.
|
|
A frontend `execute`'s `"use client"` is stripped from the output (the module
|
|
already carries it).
|
|
|
|
## Compile targets
|
|
|
|
The compiler produces two self-contained rewrites of the source, selected by the
|
|
bundler per build layer. Each rewrite keeps only the relevant regions and prunes
|
|
the imports that became unused, so a dropped region's dependencies disappear.
|
|
|
|
### `client` target
|
|
|
|
- Keep `description`, `parameters`, `render`, and `execute` of `frontend` tools.
|
|
- Drop `execute` of `backend` tools (and its now-unused imports).
|
|
- Prepend `"use client"` when any `render` remains.
|
|
|
|
### `server` target
|
|
|
|
- Keep `description`, `parameters`, and `execute` of `backend` tools.
|
|
- Drop every `render` (and its now-unused imports).
|
|
- Drop `execute` of `frontend` tools.
|
|
- Prepend `import "server-only"` when any backend `execute` remains.
|
|
|
|
The `"use generative"` directive is stripped from both outputs.
|
|
|
|
## Bundler integration
|
|
|
|
Wrap the Next config with `withAui` from `@assistant-ui/next`
|
|
(no filename convention — modules are matched by the `"use generative"` directive,
|
|
and the loader passes non-generative files through untouched). It applies `./loader`,
|
|
a webpack/Turbopack loader.
|
|
|
|
The loader rewrites a **bare** generative import into a facade that delegates the
|
|
build choice to a `react-server`-conditioned package subpath, so a single import
|
|
resolves to the right build per layer — no query, no per-app config (see
|
|
DESIGN.md for the mechanism):
|
|
|
|
- route handler / RSC (`react-server` ON) → **server build** (schema + `execute`,
|
|
guarded by `server-only`)
|
|
- client component, SSR + browser (`react-server` OFF) → **client build**
|
|
(schema + `render`)
|
|
|
|
The concrete compile is keyed off an **internal** `?generative-env=server|client`
|
|
query the facade generates — it is never authored by consumers, so no ambient
|
|
module declaration is needed. (Clear `.next` after changing the loader —
|
|
Turbopack caches loader output aggressively.)
|
|
|
|
Why not infer the target from the build **layer** inside the loader? Turbopack
|
|
compiles one output per resource path and does not give a loader a per-layer
|
|
module instance — so the split must happen at resolve time (the `react-server`
|
|
export condition), which is exactly what the facade routes through.
|
|
|
|
## Consumption
|
|
|
|
Both sides import the module **bare**; the facade resolves each to the right build:
|
|
|
|
- **server:** import `./x.generative` in a route handler — it resolves to the
|
|
server build (schema + `execute`). With the AI SDK,
|
|
`await new AISDKToolkit({ toolkit }).tools({ frontend })` from
|
|
`@assistant-ui/react-ai-sdk` converts it into a `ToolSet` whose `execute` runs
|
|
in the route, merging in the frontend-uploaded tools.
|
|
- **client:** import `./x.generative` in a client component — it resolves to the
|
|
client build (schema + `render`) — and register its tool UI.
|
|
|
|
Neither side ships the other's code, and the schema is never re-uploaded from
|
|
the client per request — the server owns it.
|
|
|
|
## Authoring constraints (enforced, with errors)
|
|
|
|
1. A leading `"use generative"` directive.
|
|
2. A single `export default defineToolkit({ ... })` (the wrapper is required;
|
|
optionally inside `satisfies` / `as`). No other exports.
|
|
3. Every tool must declare an `execute`. Its form determines the kind: `humanTool()`
|
|
→ human; a leading `"use client"` directive → frontend (needs a block body,
|
|
not an expression body); otherwise backend. `type` is never authored.
|
|
4. `render` / `execute` must be inline functions that close over **module
|
|
imports only**, so they can be routed/pruned without dragging local scope.
|
|
|
|
## Known limitations (v1)
|
|
|
|
- Bare side-effect imports (e.g. `import "./styles.css"`) cannot be attributed to
|
|
a region by reference analysis, so they are left untouched in both targets.
|
|
- Output preserves TS/JSX; the loader must run before the bundler's TS/JSX pass
|
|
(the default in Next).
|
|
- Turbopack honors a loader-emitted `"use client"` directive (validated on Next
|
|
16.2.6), but does not give a loader per-layer module instances — hence the
|
|
`?generative-env=server` query rather than layer inference. Clear `.next` after
|
|
changing the loader.
|