Designing for AI agents

We live in a world where software is co-authored. We prompt, and AI agents write the code.

But if you've used agents to build UI, you've probably noticed a pattern: they generate correct, functional layouts on some libraries, and complete garbage on others. They hallucinate non-existent props, fail to import the right sub-components, or attempt to refactor core library wrapper files just to change a hover state.

It's easy to blame the LLM. But the root cause is usually the library's design.

Most modern component libraries were designed for human IDE autocomplete, not AI context windows. They prioritize deep nesting and implicit namespaces, which look tidy in a sidebar but are opaque to a model.

If you want an AI agent to write clean, maintainable UI, you need to design your component API as an instruction set. Here is how Lumi UI's architecture does that.


Flat semantic exports

Nested namespaces are popular for a reason:

tsx
// Nested namespaces - AI struggles
import { Combobox } from "@base-ui/react/combobox";

For a human, this is great. You import Combobox and let the IDE suggest .Trigger, .Content, or .Item.

But for an AI agent, this is a black box. Unless the agent consumes its token budget reading the entire type definition for Combobox, it doesn't actually know what properties live on that namespace. It has to guess: Is it Combobox.Input? Combobox.Item? Or Combobox.ListItem?

If it guesses wrong, you get a compilation error.

Lumi flattens exports so every piece is explicitly named and accessible at the root:

tsx
// AI-friendly
import {
  ComboboxInputGroupContent,
  ComboboxItem,
  ComboboxPortal,
} from "@/components/ui/combobox";

This simple change does three things for an agent:

  1. Reduces token usage: The agent doesn't need to load large nested type trees to figure out what's available.
  2. Explicit relationships: The import statement itself acts as a map of the component's structure.
  3. No ambiguity: Autocomplete suggestions are literal and unique, reducing the chance of hallucinated props or child components.

Composites as living examples

AI agents learn best by example. When you ask an agent to build a new feature, it scans your workspace to understand how components are typically assembled.

If your library hides its implementation inside a pre-compiled node_modules package, the agent has no reference point. It has to rely entirely on its training weights — which are often outdated or generic.

In Lumi, composite components live inside your local codebase (@/components/ui/...). They serve as executable documentation.

When an agent reads your files, it sees exactly how the primitives compose:

tsx
// This teaches AI agents: "This is how you use ComboboxPortal + ComboboxPositioner + ComboboxPopup"
function ComboboxContent({
  className,
  children,
  sideOffset = 6,
  align = "start",
  matchAnchorWidth = true,
  positionerAnchor,
  ...props
}: BaseCombobox.Popup.Props & {
  sideOffset?: BaseCombobox.Positioner.Props["sideOffset"];
  align?: BaseCombobox.Positioner.Props["align"];
  matchAnchorWidth?: boolean;
  positionerAnchor?: React.RefObject<HTMLDivElement | null>;
}) {
  return (
    <BaseCombobox.Portal data-slot="combobox-portal">
      <BaseCombobox.Positioner
        data-slot="combobox-positioner"
        sideOffset={sideOffset}
        align={align}
        anchor={positionerAnchor}
      >
        <BaseCombobox.Popup
          data-slot="combobox-content"
          className={cn(
            "bg-popover text-popover-foreground rounded-sm shadow-md",
            "outline outline-border dark:-outline-offset-1",
            "overflow-hidden overflow-y-auto",
            "max-w-(--available-width) max-h-[min(23rem,var(--available-height))]",
            "animate-popup",
            matchAnchorWidth && "w-(--anchor-width)",
            className,
          )}
          {...props}
        >
          {children}
        </BaseCombobox.Popup>
      </BaseCombobox.Positioner>
    </BaseCombobox.Portal>
  );
}

By reading this composite, the agent learns the relationship between BaseCombobox.Portal, BaseCombobox.Positioner, and BaseCombobox.Popup. When you ask it to build a custom combobox for a dense dashboard, it doesn't invent a new pattern. It copies the existing structure and tweaks the styles.

It learns by example, within your project context, leading to a much higher first-try success rate.


Immutable logic blocks

When an agent runs into a layout limitation, its first instinct is often to modify the component library itself. It will open @/components/ui/dialog.tsx and start adding parameters and if statements to support its new layout.

This is how component codebases rot.

Lumi avoids this by separating the concerns into two layers:

  1. Primitives: Stable, thin wrappers around Base UI that handle state, accessibility, keyboard navigation, and custom data-slot hooks.
  2. Composites: Pre-assembled visual components.

The boundaries are clear. We treat the primitive wrapper layer as immutable. The agent shouldn't modify the logic inside @/components/ui/select.tsx. Instead, if the composite doesn't fit, the agent should import the primitives directly into the application file and build the layout there.

Because the primitives handle all the heavy lifting (like focus management and keyboard handling), the agent only needs to focus on DOM structure and styling:

tsx
// The agent writes this composition in your app file, leaving core select logic untouched
<Select>
  <SelectTrigger render={<Button />}>
    <SelectValue />
  </SelectTrigger>
  <SelectPortal>
    <SelectPositioner>
      <SelectPopup className="my-custom-layout">
        {/* custom list composition */}
      </SelectPopup>
    </SelectPositioner>
  </SelectPortal>
</Select>

This clear contract makes AI assistance predictable. The agent knows exactly where its responsibility starts (composition and styling) and where it ends (state management and accessibility).


Architecture is the prompt

In the age of AI engineering, we spend a lot of time optimization-tuning our prompts. We write system instructions, outline strict rules, and build few-shot examples.

But the cleanest prompt you can write is a solid, self-documenting architecture.

If your code patterns are consistent, your exports are flat and descriptive, and your layout boundaries are clear, the agent doesn't need a 2,000-word prompt to do the right thing. It just reads the code, understands the system, and writes software that looks like you wrote it yourself.


In this series:

  1. Why I built Lumi UI — the problem and the idea behind it
  2. One file, one source of truth — the dual-layer architecture in practice
  3. Designing for AI agents — how the architecture becomes an instruction set
  4. The details that matter — animations, hit areas, and type safety