MIDL agent reference

The capability map for an agent building with MIDL: what the system is, every surface you can use, and the validate → repair → render loop that lets you author correctly without memorizing the spec. For a hands-on first run, do AGENT-QUICKSTART.md first; this is the companion reference.

Core principle: don't guess the spec — introspect and round-trip. The vocabulary is machine-readable (describeVocabulary()), and every render/validate call returns structured feedback that names the fix.


1. The model in one minute

  • A MIDL document is plain JSON: { midl: '0.1', ns: { ui: 'https://midl.ai/ui#' }, frames: [...] }.
  • A frame is a component: { id, type, ...fields, children? }. type is namespaced (ui:Hero). children is an array of frame ids (composition is by reference, not nesting).
  • A pack turns the document into HTML. The document never changes; the pack decides the look.
    • @midl/pack-minimal-html — bare semantic tags, zero CSS. Best for a first run.
    • @midl/pack-tailwind-shadcn — Tailwind + shadcn classes + a :root theme via themeCss.
    • Pack ids are literal strings built into @wisepunk/corenot npm packages to install.
  • A theme (optional) is a token map mapped to themeCss (:root { … }), contrast-checked.
import { renderMidl } from '@wisepunk/renderer-html';
const { html, themeCss, validation } = renderMidl(doc, { pack: '@midl/pack-minimal-html', theme });

2. Discover the vocabulary (instead of reading docs)

import { describeVocabulary, EXAMPLE_DOCS } from '@wisepunk/core';

const vocab = describeVocabulary();
// vocab.components[] — one entry per ui:* type:
//   { type, requiredFields, leaf, allowsChildren, container, acceptsText, textProp?,
//     variants?, packs: { <id>: { tag } }, example }

So for any component you get its required fields; whether it accepts text (acceptsText + which textProp — e.g. ui:Buttontext, ui:Herotitle); whether it takes children (allowsChildren = the validator's real rule, container = a declared child container); its variant axes (e.g. ui:Buttonvariant/size, ui:Stackdirection, ui:Sectionwidth, ui:Herosize); the tag each pack renders it as; and a minimal example. allowsChildren and the validator now agree (MID-100) — introspection never forbids a pattern the validator accepts.

EXAMPLE_DOCS is an array of { name, description, doc } — full, valid documents to pattern-match (landing, contact-form, article). Every doc is test-asserted valid against both packs, so they're safe to copy and mutate. E.g. EXAMPLE_DOCS.find(e => e.name === 'landing').doc.

On a live deployment the same data is served (origin-rooted):

Surface What it gives you
GET /llms.txt The discovery index (this, machine-readable)
GET /api/agents/vocabulary.json describeVocabulary() output
GET /api/agents/examples.json EXAMPLE_DOCS
GET /api/agents/openapi.json OpenAPI 3.1 for the public API
GET /.well-known/agent.json Site identity, tools, formats
GET /render/<slug>.midl.json A page's raw MIDL document
GET /render/<slug>.facts.json Extracted facts (RAG/agent view)

3. The authoring loop: validate → repair → render

MIDL is designed so you author by round-tripping, not by getting it right blind.

  1. Write a document from a describeVocabulary() entry or an EXAMPLE_DOCS doc.
  2. Validate it. The result names every problem (missing required field, unknown type, bad child ref):
    import { validateMIDL, validateAgainstManifest, getPack } from '@wisepunk/core';
    validateMIDL(doc);                                   // structural: { valid, errors, warnings }
    validateAgainstManifest(doc, getPack(packId).manifest); // + pack constraints: { valid, errors, warnings }
    
    Or over HTTP: POST /api/validate with the document. Or just read renderMidl(doc).validation. Read warnings, not just errors: a prop a component won't render (e.g. a subtitle on ui:Hero, a hallucinated field) surfaces as a warning — it won't fail validation but it won't render either (MID-100).
  3. Repair structural issues automatically instead of hand-fixing:
    import { repairAgainstManifest, getPack } from '@wisepunk/core';
    const { doc: fixed, repairs } = repairAgainstManifest(doc, getPack(packId).manifest);
    
  4. RenderrenderMidl(doc, { pack }) → HTML. validation.valid === false? Fix what validation.errors say and re-render. Trust the validator over your assumptions.

4. Authoring & serving stored pages (the write API)

To create/update pages on a deployment, use @wisepunk/sdk with a Bearer API key:

import { createClient } from '@wisepunk/sdk';
const midl = createClient({ baseUrl: 'https://your-deployment.workers.dev', apiKey: process.env.MIDL_API_KEY });
const { id, slug } = await midl.frames.create({ content: doc, slug: 'home' }); // validated server-side
const page = await midl.frames.get({ slug: 'home' });                          // read it back (GET is public)

Reads (list, get) are public; writes (create, update, delete) need the key. The full HTTP contract is GET /api/agents/openapi.json. See WRITE-API-MANUAL.md.


5. Theming

Pass options.theme (DESIGN.md tokens) to renderMidl, or let a document theme itself.

  • Token values accept a plain string OR { value }{ 'colors.primary': '#34d399' } works (MID-102).
  • Self-theming documents — a ui:StyleToken frame's tokens are read automatically when you don't pass options.theme. renderMidl(doc, { pack }) themes from the document itself.
  • Foregrounds are auto-derived to pass WCAG AA against their own background — you supply background-side colors; --*-foreground is computed (and contrast-checked). To override a derived foreground (e.g. make --muted-foreground less bright), set the *.text key: colors.muted.text, colors.card.text, colors.primary.text, etc.
  • Tailwind hosts (for @midl/pack-tailwind-shadcn): don't hand-write the var mapping — import { tailwindThemeExtend } from '@wisepunk/core' and spread it into tailwind.config's theme.extend so bg-primary / text-muted-foreground / the radius resolve to the vars themeCss sets.

6. Gotchas

  • Children are ids, not objectschildren: ['hero', 'cta'], and those frames exist in frames.
  • Packs aren't installable — pass the id string to renderMidl; it's bundled in @wisepunk/core.
  • minimal-html needs no CSS; tailwind-shadcn does — start with minimal-html.
  • Express structure in the document, not the host shell: ui:Heading (level 1–6) / ui:Hero (title<h1>, size) for outline; ui:Label (text + for) for inputs; ui:Stack (direction: row|column) / ui:Section (width: content|full) for layout; ui:Button with href renders an <a> (real CTA); ui:Select (options: string[]), ui:Rating (value/max), ui:Accordion (summary<details>).
  • ESM + Node 18+ for the packages.
  • Don't assume — round-trip. If validation.valid is false, the document is wrong, not the renderer; if validation.warnings is non-empty, a prop you set isn't being rendered.