Mar 26, 2026DSPyGEPAGeminiagents3 min
Let the compiler write your prompt
Gridweave generates React components with Gemini. The interesting part isn't the generation — it's using real esbuild errors as the fitness signal for evolving the system prompt.
Every AI component generator has the same shape: prompt a model, get some TSX back, render it. The part nobody talks about is what happens when the output doesn't compile — which, for anything non-trivial, is most of the time on the first attempt.
The usual fix is to keep editing the system prompt by hand. You add a line like "always import icons from lucide-react", the failures shift somewhere else, you add another line, and six weeks later you have a 2,000-token prompt nobody understands and no idea which parts are still earning their place.
Gridweave takes a different route: it treats the prompt as something to optimize against a signal, and the signal is the bundler.
The loop
The core of it (run_evolution in gepa_evolver.py) is five steps:
- Generate component code via Gemini, keeping the thinking trace
- Bundle it with esbuild — real bundling, not a syntax check
- On failure, reflect on the error plus past failures pulled from Qdrant, and mutate the system prompt
- Repeat up to N generations, breaking early on success
- Store the run in Qdrant so future runs can recall it
Step 2 is the one that matters. An LLM asked "is this code correct?" will cheerfully tell you
yes. esbuild will tell you Could not resolve "lucide-react/icons/Sparkle", and that error is
specific, honest, and free. It's a fitness function you don't have to write.
Why bundle instead of lint
I tried a cheaper version first — parse the TSX, check the imports resolve, move on. It passed code that then exploded in the browser, because the failures that actually matter aren't syntax. They're a named export that doesn't exist in the installed version of a library, or a Tailwind class that silently does nothing, or a hook called conditionally.
Real bundling catches the first two. The third still gets through, which is a limitation I haven't solved.
Failures are worth keeping
The Qdrant store started as debugging convenience — somewhere to look after a run to see what happened. It turned into the more useful half of the system.
When generation 4 fails, the reflection step doesn't just see generation 4's error. It sees the errors from every prior run on similar tasks. That changes the mutation from "fix this bug" to "this class of thing keeps breaking, address it in the prompt." Those are different edits, and the second one generalizes.
The service around it
Everything streams. All the GEPA endpoints emit Server-Sent Events, one JSON object per data:
field, because watching a prompt evolve generation by generation is genuinely more useful than
getting a final answer — you can see the moment it stops making a mistake.
FastAPI (main.py)
├── /gepa → GEPA sub-app
│ ├── POST /render → esbuild-bundles TSX, returns preview_id
│ ├── GET /preview/{id} → serves bundled HTML for iframe
│ ├── GET /stream-evolution → SSE: single task evolution stream
│ ├── POST /run-experiment → SSE: full control (task, generations, seed_prompt)
│ └── GET /run-all → SSE: run all experiments sequentially
├── /viewer → Vite SPA — live preview UI
├── /agent → DSPy ReAct agent endpoint
└── /v2/stream/generate → DSPy stream
Previews render into an iframe from a bundled HTML artifact, keyed by preview_id. That
indirection exists so the preview is a real page load rather than something injected into the
host document — generated code is untrusted code, and it should be behind a boundary.
What I'd tell you before you build this
The eval is the product. The GEPA loop is maybe 200 lines. Getting a signal worth optimizing against — one that's cheap, deterministic, and correlated with what you actually want — took far longer and is the only reason any of it works.
Early stopping matters more than you'd think. Without a break-on-success, the loop keeps mutating a prompt that's already working and makes it worse. Prompt evolution has no natural stopping point; you have to impose one.
A local maximum is invisible from the inside. The prompt I'd hand-tuned over weeks was beaten within a handful of generations. Not because the machine is smarter, but because it will try edits I'd already decided wouldn't work.
Gridweave runs locally and on trusted networks only — it executes model-generated code, so there's no hosted demo, by design.