Fable tool files export Zod schemas and AI SDK tool definitions. For example, metric-card installs lib/fable-ui/tools/show-metric-tool.ts with:
export const showMetricInputSchema = z.object({ ... })
export function createShowMetricTool() { ... }
export const showMetric = defineFableComponent({ ... })For the full schema, AI SDK tool, renderer, state, and props contract, read Tool Definitions.
Register Tools#
import { streamText } from "ai"
import { showMetric } from "@/lib/fable-ui/tools/show-metric-tool"
const tools = {
show_metric: showMetric.tool,
}
const result = streamText({
model,
messages,
tools,
toolChoice: "auto",
})Only pass AI SDK tool objects into the route. A Fable definition includes both tool and renderer; the route should map just showMetric.tool, showChart.tool, or showDataBrowser.tool. Keep the full Fable registry in client/UI code where streamed tool parts are rendered.
Heavy renderers are lazy-loaded from their definition files. For example, importing the chart definition lets the route register showChart.tool without eagerly loading the chart component or Recharts. The UI renderer loads those pieces only when it actually renders the tool part.
Render Tool Parts#
Install core or any item that depends on it, then provide the full Fable definition map to the renderer:
import { FableToolPart } from "@/lib/fable-ui/core/tool-renderer"
import { showMetric } from "@/lib/fable-ui/tools/show-metric-tool"
const registry = {
show_metric: showMetric,
}
export function ToolPart({ part }: { part: unknown }) {
return <FableToolPart part={part} registry={registry} />
}Unknown tool parts render a small fallback. Invalid payloads are parsed with the item schema and render the component error state.
collect_input is a UI handoff: it renders from the tool input and waits for the host app to handle user submission. The playground mock mode can stream valid and invalid form tool parts without calling a provider, which is useful for verifying renderer safety.
Wire Interactive Tool Parts#
Display tools can render without handlers. Interactive surfaces need the host chat to pass callbacks into FableToolPart.
For show_next_actions, a click should become a normal user message:
"use client"
import { useCallback } from "react"
import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport } from "ai"
import { FableToolPart } from "@/lib/fable-ui/core/tool-renderer"
import { showNextActions } from "@/lib/fable-ui/tools/show-next-actions-tool"
const registry = {
show_next_actions: showNextActions,
}
export function ChatToolPart({ part }: { part: unknown }) {
const { sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
})
const isBusy = status === "submitted" || status === "streaming"
const onSuggestedAction = useCallback(
(action: { prompt: string }) => {
if (isBusy) {
return
}
void sendMessage({ text: action.prompt })
},
[isBusy, sendMessage]
)
return (
<FableToolPart
part={part}
registry={registry}
handlers={{
onSuggestedAction: isBusy ? undefined : onSuggestedAction,
}}
/>
)
}In a full chat shell, prefer defining this handler next to the composer and passing it down through message rendering. That keeps suggested actions and manual composer sends on the same path, including request options:
await sendMessage(
{ text: action.prompt },
{
body: {
provider,
model,
selectedKeyId,
apiKey,
},
}
)The selected action is user intent, not an automatic side effect. The model receives the prompt, then decides whether to answer in text or call another registered Fable tool.
Add email, confirmation, or form#
The quickstart does not include these tools. Add each one to the server route with its AI SDK tool object:
import { collectInput } from "@/lib/fable-ui/tools/collect-input-tool"
import { requestConfirmation } from "@/lib/fable-ui/tools/request-confirmation-tool"
import { showEmailComposer } from "@/lib/fable-ui/tools/show-email-composer-tool"
const tools = {
show_email_composer: showEmailComposer.tool,
request_confirmation: requestConfirmation.tool,
collect_input: collectInput.tool,
}Map the complete definitions in the client renderer. The email definition needs no host callback: it renders a user-controlled compose handoff and never sends mail.
import type { ToolPartLike, ToolRenderHandlers } from "@/lib/fable-ui/core"
import { FableToolPart } from "@/lib/fable-ui/core/tool-renderer"
import { collectInput } from "@/lib/fable-ui/tools/collect-input-tool"
import { requestConfirmation } from "@/lib/fable-ui/tools/request-confirmation-tool"
import { showEmailComposer } from "@/lib/fable-ui/tools/show-email-composer-tool"
const registry = {
show_email_composer: showEmailComposer,
request_confirmation: requestConfirmation,
collect_input: collectInput,
}
type InteractiveHandlers = Pick<
ToolRenderHandlers,
"onConfirm" | "onCancel" | "onFormSubmit"
>
export function ChatToolPart({
part,
handlers,
}: {
part: ToolPartLike
handlers: InteractiveHandlers
}) {
return <FableToolPart part={part} registry={registry} handlers={handlers} />
}onConfirm and onCancel receive { id, label }; onFormSubmit receives Record<string, string | number | boolean>. Define those callbacks in the host chat layer, where you can associate them with the current conversation and call a server endpoint. On the server, authenticate, authorize, validate the values again, and make side effects idempotent. A confirmation card is a user decision, not authorization; a form submission is untrusted input, not a completed action.
If the host needs to observe a user-edited email draft, render EmailComposerCard directly with its onDraftChange prop. FableToolPart intentionally has no email-delivery callback.
DataBrowser Tools#
show_table and show_data_browser are display tools. show_data_browser should receive a resourceId, not database details. Register resources in host code, then add the safe manifest to your system prompt:
show_chart renders static model-provided chart data only. Fetch private or live data in host code, validate the rows, then pass display-ready data into the chart payload.
import { describeAvailableResources } from "@/lib/fable-ui/core"
const result = streamText({
model,
system: [
"Use Fable UI tools for structured UI.",
describeAvailableResources(),
].join("\n\n"),
messages,
tools,
})The model can select a resource id from that manifest. It must not pass raw SQL, Firestore paths, collection names, REST URLs, secrets, or permission decisions.
Register get_rendered_data alongside those display tools when the model may need to reason about a resource-backed browser after it renders. In AI SDK 7 it is a client tool: its definition has no execute, so the route registers the tool while useChat supplies the output.
import { getRenderedDataTool } from "@/lib/fable-ui/tools/get-rendered-data-tool"
import { showDataBrowser } from "@/lib/fable-ui/tools/show-data-browser-tool"
const tools = {
show_data_browser: showDataBrowser.tool,
get_rendered_data: getRenderedDataTool,
}FableDataProvider must wrap the shared chat and rendered DataBrowser tree. Register resources on the same registry passed to this provider; without it, no rendered snapshot is published and the client tool correctly returns not-rendered.
import type { ReactNode } from "react"
import { FableDataProvider, type DataSourceRegistry } from "@/lib/fable-ui/core"
export function FableDataRoot({
children,
registry,
}: {
children: ReactNode
registry: DataSourceRegistry
}) {
return <FableDataProvider registry={registry}>{children}</FableDataProvider>
}"use client"
import { useChat } from "@ai-sdk/react"
import { useFableDataContext } from "@/lib/fable-ui/core"
import type { FableUIMessage } from "@/lib/fable-ui/tools"
import { shouldContinueAfterRenderedData } from "@/lib/fable-ui/tools/get-rendered-data-tool"
export function useFableChat() {
const dataContext = useFableDataContext()
const { addToolOutput, ...chat } = useChat<FableUIMessage>({
sendAutomaticallyWhen: shouldContinueAfterRenderedData,
onToolCall({ toolCall }) {
if (toolCall.dynamic || toolCall.toolName !== "get_rendered_data") return
const output = dataContext.getRenderedData(toolCall.input.resourceId)
addToolOutput({
tool: "get_rendered_data",
toolCallId: toolCall.toolCallId,
output,
})
},
})
return { addToolOutput, ...chat }
}The provider reads only its current rendered resource page; this client handler must never fetch or refetch. A miss or over-limit snapshot returns unavailable. Keep the continuation predicate narrow so it sends the next request only after the tool output is available. Rows are untrusted model context, not instructions, authorization, or permission to perform a side effect.
Agent-rendered UI#
This guidance is verified against ai@7.0.14 and @ai-sdk/react@4.0.15. Recheck the installed AI SDK docs and types when upgrading because the agent and UI APIs change frequently.
AI SDK generative UI does not let the model write React or HTML. The model selects a registered tool, the SDK streams a typed tool-* message part, and the host maps that trusted tool contract to a React component. Fable follows that pattern: schemas constrain model input, FableUIMessage keeps tool parts typed end to end, and FableToolPart selects an allowlisted renderer.
Keep the current streamText route while a workflow depends on browser-owned state such as get_rendered_data. A client tool has no execute; the server loop pauses at its tool call, useChat supplies the result with addToolOutput, and the narrow sendAutomaticallyWhen predicate starts the next request. Moving this flow into a server-only loop would not give the server access to the provider's browser cache.
Use ToolLoopAgent later when the host has reusable, server-executed tools that need several autonomous steps. At that point, define the tools once on the agent, infer the client message type with InferAgentUIMessage<typeof agent>, and return its stream with createAgentUIStreamResponse. Do not migrate only to rename the current streamText call: the agent abstraction becomes useful when it owns a real multi-step server workflow, lifecycle callbacks, or shared call configuration.
SDK tool approval is also a future server-side boundary. Use it when a tool's execute performs a sensitive action. A rendered confirmation card or client tool output is model context, not authorization; the host must still authenticate, authorize, and validate the action.
Quickstart#
The quickstart installs a working example at /fable-chat and /api/fable-chat. Configure FABLE_AI_PROVIDER, FABLE_AI_MODEL, and FABLE_AI_API_KEY in .env.local; when those variables are missing, the chat responds with setup guidance instead of rendering placeholder data.
Provider mode is opt-in. Keep provider keys, data access, authorization, writes, and validation in your host app.