Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

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.