The usual “chat with your data” architecture is a credit card on fire. The model writes a SQL query, you execute it, the rows come back, the rows go into the next prompt as context, the model summarises. Every interesting question pours actual data through the LLM. Tokens, latency, and a privacy footprint that is hard to explain to anyone in legal.
This demo flips that. Gemini emits a layout, not an answer. The layout contains SQL strings as component props. A DataLoader component on the client picks up each query, runs it through DuckDB-WASM in the browser, and writes the result rows into the surface data model at a JSON-Pointer path. Charts on that path re-render the moment the write lands. The model is blind to a single row.
The DataLoader is the trick
A2UI v0.9 has the right primitive for this: components with Dynamic props that resolve through JSON-Pointer paths into a per-surface data model. We add one custom component on top of the basic catalog.
export const DataLoaderApi = {
name: 'DataLoader',
schema: z.object({
sql: z.string().min(1),
target: z.string().regex(/^\//),
}).strict(),
};
The React impl renders nothing. It runs sql through DuckDB-WASM on mount, then writes { loading, rows } (or { loading, error }) into the data model at target. A LineChart whose data prop is { path: "/data/revenueByMonth/rows" } sees undefined first, paints a shimmer, and swaps in once the loader writes. The agent never had to know any of this.
What the agent does see
Two server-side tools, both metadata only. listTables() returns table names and one-line descriptions, describeTable({name}) returns column types. The agent uses them to write SQL it cannot execute. Output is JSONL, one A2UI envelope per line, validated structurally before each line is forwarded to the client. SQL with a destructive verb is rejected at validation time so a careless DROP TABLE does not even reach the browser.
The trade-off is honest. A blind agent cannot iterate on actual values. If a user’s filter narrows things down to zero rows, the model will not notice and re-plan. The fix is to feed compact result summaries (row counts, value ranges) back through action context on the next turn, which is the obvious follow-up and intentionally not in v1. The privacy punchline is the whole point.