Before you pick AG-UI, A2UI, or MCP Apps, you need an answer to a more basic question: what shape is the model allowed to return?
That shape is the output contract. It is the lowest layer in the LLM interface protocol stack. Get it wrong and every layer above it — streaming, events, generative UI — has nothing reliable to render.
Key idea
Structured outputs and tool calling are complementary contracts. Structured outputs constrain what the model says; tool calling constrains what the model asks the system to do. Both are schema-driven interfaces on model behavior.
The problem: valid text is not a valid interface
Models naturally produce prose. Prose is flexible and terrible as a machine-to-UI boundary.
Teams tried JSON in the prompt for years. It worked often enough to ship, failed often enough to hurt: trailing commas, markdown fences, hallucinated fields, partial objects mid-stream. The fix was not “prompt harder.” It was constraining generation and validating at the boundary.
Two mechanisms dominate production systems in 2026:
- Structured outputs — the model’s visible response must match a JSON Schema.
- Tool / function calling — the model emits a structured call to a named function with typed arguments; your runtime executes it and may render the result.
They overlap in syntax but serve different jobs in the interface stack.
Structured outputs: schema as protocol
OpenAI structured outputs
In August 2024, OpenAI introduced Structured Outputs with response_format: { type: "json_schema", ... } on supported models. The critical guarantee: model output adheres to the supplied JSON Schema — not “usually valid JSON,” but schema-constrained decoding.
That shifts frontend work. You can treat the response as a typed object and map it to components without a repair loop. OpenAI documents two entry points:
- Response format — when the user-facing result is structured data.
- Function calling with
strict: true— when the structure is a tool invocation.
Anthropic tool use
Anthropic’s tool use returns structured tool_use blocks with input matching your tool schema. Claude is trained to pick tools and populate arguments reliably. For UI purposes, a tool whose output you render is often indistinguishable from a structured response — the difference is who executes the next step (runtime vs. direct render).
Vercel AI SDK: Zod as the contract language
The Vercel AI SDK exposes generateObject and streamObject with Zod schemas. Zod is not an open wire protocol, but it is a practical contract layer thousands of TypeScript frontends already use:
import { z } from "zod";
import { generateObject } from "ai";
const Comparison = z.object({
kind: z.literal("comparison"),
items: z.array(
z.object({
name: z.string(),
pros: z.array(z.string()),
cons: z.array(z.string()),
score: z.number().min(0).max(10),
}),
),
recommendation: z.string(),
});
const { object } = await generateObject({
model: "anthropic/claude-sonnet-4.5",
schema: Comparison,
prompt: "Compare three deployment options for a Vue SPA.",
});
The SDK validates and types the result. Your component catalog keys off object.kind or distinct schemas per surface.
flowchart LR P["Product decision"] --> Z["Zod / JSON Schema"] Z --> M["Model generation"] M --> V["Validation"] V --> C["Component render"]
Tool calling: actions as structured output
Tool calling is how models request side effects — query a database, send a draft, update state — without embedding imperative code in the response.
A minimal tool definition (OpenAI-style):
{
"type": "function",
"function": {
"name": "show_recommendations",
"description": "Present ranked options to the user",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": { "type": "string" },
"confidence": { "enum": ["low", "medium", "high"] }
},
"required": ["title", "confidence"]
}
}
},
"required": ["items"]
}
}
}
The interface pattern:
- Model returns
tool_callwith structured arguments. - Runtime does not blindly execute UI tools — it routes to a renderer or asks for approval.
- Frontend maps
show_recommendations→RecommendationListcomponent.
This is the bridge between “LLM output” and trust boundaries. Tool schemas name intentions; your action layer decides execution.
| Mechanism | Best for | Frontend receives |
|---|---|---|
| Structured output | User-visible typed content (cards, tables, plans) | JSON object → component |
| Tool calling | Actions, lookups, multi-step agent loops | Tool name + args → handler / UI |
| Both combined | Agents that explain and act | Text stream + parallel tool events |
How this connects to generative UI protocols
Output contracts are necessary but not sufficient for cross-app generative UI.
- A2UI defines a schema for declarative component trees (spec v0.9.1, v1.0 candidate) — a specialized output contract for UI intent.
- AG-UI carries tool and generative UI events between agent and frontend (docs) — transport and lifecycle above the schema.
- MCP Apps attach UI metadata to tools (
_meta.ui.resourceUri) — another contract shape for the tool layer.
Your app’s own Zod types are a fourth contract — perfectly fine when you control both ends.
The structured outputs foundation post made the product case: do not flatten typed objects back into paragraphs. At the protocol level, the same rule applies: pick a schema before you pick a streaming library.
Design rules that survive vendor changes
- Design from the user decision backward. Schema fields should map to UI affordances and actions, not to whatever the model finds easy to emit.
- Keep schemas small. Large nested schemas are harder for models to fill and harder for components to render mid-stream.
- Separate “display” from “execute.” A
proposed_emailobject is display;send_emailis execution. Mixing them in one schema blurs trust boundaries. - Version your contracts. Treat breaking schema changes like API migrations.
- Prefer enums over free strings for routing (
kind,confidence,status) so component maps stay exhaustive.
Product implication
Structured outputs turned reliable JSON from a prompting exercise into a platform feature. That lowers the cost of typed interfaces — and raises the cost of ignoring them.
If your copilot still renders every result as markdown inside a bubble, you are not “avoiding complexity.” You are paying for structured generation on the backend and throwing away the interface value on the frontend.
Tool calling, used well, is how you keep actions legible: named operations with typed arguments, previewable in UI, gateable with approval flows.
What to watch next
Output contracts define what arrives. The next layer defines how it arrives while still generating: Streaming protocols: from tokens to UI messages.
References: