Skip to content

UI Generator

read as .md

The 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 deliver the ~100ms path.

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; staged and staged_concurrent are 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) → loop
WorkflowStatusShape
single_passLive — only dispatched workflowOne impl turn.
stagedReserved (registered, not routed)Plan → execute.
staged_concurrentReserved (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:

  1. The LLM receives everything pre-injected — primitives docs, design tokens, the data contract with examples, gadget capability cards.
  2. The LLM writes the component as plain text. No tool calls required.
  3. The system auto-runs self_check and compile_component on the emitted source.
  4. Failures are fed back as a structured diff for the next turn to fix.

This is what keeps healthy generations to 3–5 turns. Turns ≥ 6 is a triad-misalignment signal, not a turn-budget problem.

Every workflow runs the same checks before returning:

  • TypeScript — the emitted source is compiled against the real @ggui-ai/design type definitions via the TypeScript compiler API on a virtual filesystem. Catches wrong prop types, missing required props, invalid imports, and strictNullChecks violations.
  • Render smokeReactDOMServer.renderToString() with contract-derived sample props. Catches undefined.toLowerCase()-class runtime errors before the component reaches the renderer.
  • Per-axis assertions — axis-specific checks derived from the contract (e.g., does an interactive axis include a focusable element? does a submit action 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.

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}}). generation.keySource: 'own' | 'managed' declares whose provider key funds generation — self-hosted deployments always use their own key.

A successful generation returns:

  • A compiled component module (TSX source compiled to JS).
  • A typed data contract — the same PropsSpec / ActionSpec / StreamSpec / ContextSpec that 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 stable blueprintId for 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.