UI Generator
read as.mdThe UI generator runs when a blueprint match misses and GGUI has to build a component from scratch. It takes a data contract (PropsSpec + ActionSpec + StreamSpec + ContextSpec) and returns a compiled, contract-typed React module — typically a handful of LLM turns; cache hits via blueprints are what avoid this cost entirely.
The harness
Section titled “The harness”A harness is a per-contract execution plan derived from the contract’s risk and axes. It bundles three things:
- Workflow — the topology of LLM phases and tasks. Today the dispatcher always picks
single_pass;stagedandstaged_concurrentare registered as reserved-future topologies for risk-tier routing that has not yet shipped. - Prompt + boilerplate — the system prompt and fragment set that tell the LLM how to author against
@ggui-ai/design, the active gadgets, and the contract. - Check leg — the post-conditions the output must satisfy.
Generation runs the workflow to produce source, runs the check leg, and — if checks fail — derives a revised harness and re-runs, bounded by maxIterations.
workflow → source → check → pass? ─► return │ └─ fail → derive (swap fragments / upgrade workflow / adjust prompt) → loopWorkflows
Section titled “Workflows”| Workflow | Status | Shape |
|---|---|---|
single_pass |
Live — only dispatched workflow | One impl turn. |
staged |
Reserved (registered, not routed) | Plan → execute. |
staged_concurrent |
Reserved (registered, not routed) | Plan → parallel skeleton tasks → integrate. |
pickWorkflow is deliberately conservative — every classification today routes to single_pass. The staged topologies are wired up so a future risk-tier router can dispatch to them without a schema change, but changing the picker is a first-class experiment with its own bench gate. All three feed the same check + derive loop; the workflow only changes how source is produced, not how it’s validated.
Plain-text impl loop (no tool-call ceremony)
Section titled “Plain-text impl loop (no tool-call ceremony)”The impl phase is structured so the LLM doesn’t waste turns on deterministic ceremony:
- The LLM receives everything pre-injected — primitives docs, design tokens, the data contract with examples, gadget capability cards.
- The LLM writes the component as plain text. No tool calls required.
- The system auto-runs
self_checkandcompile_componenton the emitted source. - Failures are fed back as a structured diff for the next turn to fix.
This is what keeps generations short. On the public benchmark, the median generation takes two turns, and about four in five finish in one to three. Turns ≥ 6 is a triad-misalignment signal, not a turn-budget problem.
Check leg
Section titled “Check leg”Every workflow runs the same checks before returning:
- TypeScript — the emitted source is compiled against the real
@ggui-ai/designtype definitions via the TypeScript compiler API on a virtual filesystem. Catches wrong prop types, missing required props, invalid imports, andstrictNullChecksviolations. - Render smoke —
ReactDOMServer.renderToString()with contract-derived sample props. Catchesundefined.toLowerCase()-class runtime errors before the component reaches the renderer. - Per-axis assertions — axis-specific checks derived from the contract (e.g., does an
interactiveaxis include a focusable element? does asubmitaction have a matching form?).
If any leg fails, the harness derives a revised configuration and loops. If the final iteration still fails, generation returns ok: false with reason: "max-iterations" and the last source, compiled output, and check result attached for diagnostics — the caller decides whether to surface a fallback, retry with a different harness, or report the failure.
Multi-provider transport
Section titled “Multi-provider transport”The harness is provider-agnostic. Only the LLM transport differs:
- Claude (Anthropic) — raw API or Claude Agent SDK.
- OpenAI — Responses API or OpenAI Agents SDK.
- Google (Gemini) — GenAI API or Google ADK.
Workflow, check, and derive are identical across providers. Switching providers does not change what gets generated, only the per-turn cost and latency profile.
A deployment pins its model in ggui.json#generation.model (provider:model, e.g. anthropic:claude-haiku-4-5-20251001); the GGUI_GENERATION_MODEL env var overrides the manifest (precedence: GGUI_GENERATION_MODEL env > ggui.json#generation.model > per-provider default), and an agent may override per render via ggui_render({infra: {model}}). The protocol registry is the allowlist of accepted ids: for Anthropic, the current lineup is claude-haiku-4-5-20251001, claude-sonnet-5, claude-opus-5, claude-opus-5-5, and claude-fable-5-1 (the most capable — and roughly ten times Haiku’s per-render cost), with claude-fable-5 and the still-served 4.6–4.8 generation also accepted; for OpenAI, gpt-6-astra, gpt-6-sol and gpt-6-luna sit beside the GPT-5.6 tiers — pick by the cost and capability you want; an id outside the registry is rejected at parse time rather than passed through. Hosted GGUI’s pool default — what a pool-funded render runs when nothing names a model — is openai/gpt-6-luna. generation.keySource: 'own' | 'managed' declares whose provider key funds generation — self-hosted deployments always use their own key.
Output shape
Section titled “Output shape”A successful generation returns:
- A compiled component module (TSX source compiled to JS).
- A typed data contract — the same
PropsSpec/ActionSpec/StreamSpec/ContextSpecthat was the input, now frozen as the runtime wire shape. - A blueprint candidate — the contract (hashed via RFC 8785 to
contractHash) plus its variance (variantKey), which the registry stores under a stableblueprintIdfor the next handshake’s cache match.
The contract is enforced again at the MCP handler boundary so that what the agent renders matches what the component was generated to render.
→ See Architecture overview for where the generator fits in the render pipeline, and the Glossary for harness, blueprint, gadget, contract.