Discovery
A question over the Guild's archive, answered by a statement anybody can read — an agent with one tool behind Chat, and the rows drawn as a real table rather than a block of JSON.
Somebody asks the archive a question in their own words, and gets back the statement that was written for it, the rows it returned, and a sentence quoting numbers it did not choose. The screen is a text-to-SQL panel — the shape every product grows once its data outlives the reports somebody built for it — rebuilt as what it is: an agent with one tool.
It runs without a network, and the split that makes that honest is the same one the
Workspace's Ask dock makes: the language is canned, the answer
is not. The model is the AI SDK's mock, phrasing five recipes; the statement it writes is executed
by the query tool over the running example world, so the table under the answer is a real result
set and the prose reads it back. Everything between the two — the agent loop, the tool call, its
states, the streaming — is the real ToolLoopAgent and useAgentChat.
Worth pressing in the frame:
- The five questions above the composer are the ones this agent can answer. Pressing one asks it.
- Open the thinking above a call: why the agent chose that relation, streamed before it wrote anything.
- Ask what each member was paid last winter. The agent writes a statement and the call goes
red: there is no
paymentsrelation, and the tool fails instead of joining two things that both hold money. A refusal is a state of the call, not an error banner over the page. - Ask something outside the five. There is no call to make, so the answer carries the refusal on its own.
- Press Stop mid-answer. The stream is cancelled at the model, not hidden on screen.
The host draws one thing
Chat draws every part of the AI SDK's message as it arrives — text as markdown, reasoning folded
away, each tool call in its frame with the SDK's own state. The one thing it cannot know is what a
result means, so a host passes a renderer per tool name and it draws inside the frame, in place of
its input and output, at every state of the call:
<Chat
chat={useAgentChat(agent)}
proposals={RECIPES.map((recipe) => ({ text: recipe.question }))}
tools={{ query: (part) => (part.state === "output-available" ? <ResultTable {...(part.output as ResultSet)} /> : <ToolInput />) }}
/>The columns of a result are not known until the statement has been written, so the output is a real
DataTableRoot built from the result's own shape. A JSON rendering is what the frame falls back to
for a tool with no renderer — never the one thing on offer.
The table comes from the @kanzo-tech/ui/table subpath, never the root barrel — TanStack is an
optional peer, and a reader who only wanted a transcript does not pay for it.
The agent is the swap
import { Chat, ToolInput, useAgentChat } from "@kanzo-tech/ai";
import { ToolLoopAgent, jsonSchema, tool } from "ai";ToolLoopAgent takes a model and the tools; useAgentChat runs it in the page. A product swaps one
thing and nothing else on the screen changes: the model becomes
localModel, in the page.
Chat— the transcript, the composer and the stream, whole. The pin, send/stop/retry and the tool states are its business, not this file's.DataTableRoot/DataTableContent/DataTablePagination/useDataTable— the result set, six rows to a page, columns derived from the projection.ShellRoot/ShellHeader/ShellBody/ShellMain/ShellAside— the frame. The schema rail is the<aside>; the transcript is the single<main>.EmptyRoot,Badge,Separator— the rest.
Where the domain lives
Nothing above knows what a contract is. The archive is the docs' own running example — the Guild's
board and its roster — and the two relations the rail lists are those two files described as a
schema, row counts included. data.ts holds the five recipes, each with the statement the model
writes, the function that computes it, and the sentence that reads the rows back; default.tsx
holds the agent and the arrangement.
docs/showcases/discovery/
├── default.tsx — the shell, the agent and its query tool, Chat
├── data.ts — the schema rail's relations, the five recipes, and the statement runner
└── result-table.tsx — a result set as the real table, columns built from the projectionThe agent refuses a sixth question rather than guessing a join, and that refusal is written in
data.ts beside the statement it would have sent. A screen that answers everything is the one
nobody can check.