Generative UI
Define typed components on the server, let the agent invoke them as tools, render them as React components on the client.
agent-webkit's generative UI lets the model render rich UI to the user instead of always replying in prose. You define the components as typed schemas server-side; the model gets to "call" them like tools; the frontend matches incoming tool_use events to renderers and mounts the corresponding React component with validated props.
The whole feature is an opt-in extra — nothing changes about agent-webkit's existing protocol if you don't use it.
How it works
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ pydantic.Model │ ───→ │ in-process MCP │ ───→ │ tool_use event │
│ (server) │ │ server tool │ │ on the wire │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ GenUIStream │
│ matches + │
│ dispatches │
└────────┬────────┘
▼
┌─────────────────┐
│ React renderer │
└─────────────────┘- You declare components with
pydantic.BaseModeland register them withGenUIRegistry. - The registry mounts an in-process MCP server (no IPC) where each component becomes a tool whose input schema is the component's props.
create_app(genui=...)exposesGET /genui/schemaand auto-allows the render tools (they're side-effect-free).- On the client,
useGenerativeUIfetches the schema, taps the wire-event stream, and dispatches each matchingtool_useto a React renderer keyed byshort_name.
Server: registering components
from pydantic import BaseModel, Field
from agent_webkit_server.adapters.fastapi import create_app
from agent_webkit_server.extras.genui import GenUIRegistry
class WeatherCard(BaseModel):
"""Display the current weather for a location."""
location: str
temperature_f: float
condition: str | None = Field(default=None, description="e.g. 'sunny', 'cloudy'")
class PricingPlan(BaseModel):
name: str
price: float
features: list[str] = Field(default_factory=list)
class PricingTable(BaseModel):
"""Side-by-side pricing comparison."""
plans: list[PricingPlan]
registry = GenUIRegistry()
registry.register(WeatherCard)
registry.register(PricingTable)
app = create_app(genui=registry)That's it on the server. create_app will:
- Mount
GET /genui/schema(no auth — the schema is a public client contract). - Stand up an in-process MCP server named
genuiexposing one tool per component. - Add the qualified tool names (e.g.
mcp__genui__render_weather_card) toallowed_toolsso they don't trigger permission prompts. - Append a system-prompt nudge so the model knows it can call these to render UI.
Customizing the registry
registry = GenUIRegistry(
server_name="ui", # changes mcp__ui__... in the qualified name
prefix="show_", # tool names become show_<short_name>
system_prompt="...", # override the bundled nudge entirely; "" disables it
)The pydantic docstring becomes the tool description by default. Pass description= explicitly to override:
registry.register(WeatherCard, description="Render a weather card for the user.")Client: rendering components
import { useAgentSession, useGenerativeUI } from "@agent-webkit/react";
function App() {
const ui = useGenerativeUI({
schemaUrl: "/genui/schema",
renderers: {
weather_card: (props) => <WeatherCard {...props} />,
pricing_table: (props) => <PricingTable {...props} />,
},
});
const session = useAgentSession({
baseUrl: "/",
onEvent: ui.onEvent, // pipe wire events into the GenUI dispatcher
});
return (
<>
<MessageList messages={session.messages} />
<div>{ui.updates.map((u) => <div key={u.toolUseId}>{ui.render(u)}</div>)}</div>
<Composer onSend={session.send} />
</>
);
}Renderer functions receive the validated props plus a meta object with the raw update:
weather_card: (props, { complete, partial }) => (
<WeatherCard {...(props as WeatherProps)} loading={!complete} />
),Streaming partial updates
While the model is still emitting the tool input, useGenerativeUI surfaces incremental updates with partial: true. The L1 partial-JSON parser is conservative — it only emits a key once its value is fully delimited — so you'll never see 7 flicker into 72 mid-render.
If you want skeleton states, render against props directly: as fields land they become defined; before that, your renderer just won't have those keys yet.
Without a server schema
If you don't want to run loadSchema(), pass serverName / prefix directly. The hook will then dispatch any tool_use whose name matches mcp__<serverName>__<prefix><short_name>:
useGenerativeUI({
serverName: "genui",
prefix: "render_",
renderers: { weather_card: ... },
});Type-checking is up to you in this mode — the schema is what carries it.
Lower-level building blocks
If you're not on React, @agent-webkit/core/genui gives you the same machinery without any framework dependency:
import { GenUIStream } from "@agent-webkit/core/genui";
const ui = new GenUIStream({ schemaUrl: "/genui/schema" });
await ui.loadSchema();
for await (const ev of session.events()) {
const update = ui.feed(ev);
if (update) {
// dispatch to your own renderer / template engine / Svelte store / ...
}
}GenUIStream.feed() returns null for any event it doesn't care about, so it's safe to pipe every wire event through it.
Caveats
- The render tools are deliberately stub handlers — they return
"rendered:<name>"server-side. The actual UI work is client-side. Don't put logic in those handlers assuming it will run for every render; the agent may invoke them, the client may receive the event, but the server-side handler is just there to satisfy the SDK. - Auto-allowing render tools means there's no permission prompt. That's deliberate — they have no side effects — but if you also register a registry with side-effecting tools, don't reuse the same
GenUIRegistryfor them. Use the regularcan_use_toolpath for anything that mutates state. - The schema endpoint is unauthenticated by design. It carries no session data; only your component contracts. If those contracts are themselves sensitive (e.g. proprietary product names), gate the route at your reverse proxy.