DEV Community

Cover image for The Builder agent — the code-modifier that fought me
Sunitha Eswaraiah
Sunitha Eswaraiah

Posted on

The Builder agent — the code-modifier that fought me

Post 5 of 8 in the game-factory series.

Three hours and no result

I was watching the terminal scroll when I noticed the agent had been running for over an hour. Build attempts, compiler rejections, patch attempts. Same file, same error pattern, a new proposed fix each time.

By the time I stopped it, the call count was 868. Three hours had elapsed. The file it had been working on had two export default statements, two return (...) blocks — one of them orphaned in JSX that didn't close — and a mix of correctly-themed and original-themed strings sitting next to each other like the model had started several attempts and stopped mid-thought. The React compiler had been rejecting it for the last hundred calls at least.

At list rates for the model, that run cost about $138.

The Builder hadn't done anything wrong by its own rules. There were no rules that said stop.

What the Builder is

The Builder is the fourth agent in the pipeline. By the time it runs, the Designer has produced a spec, Image-Gen has generated themed icons, and Background-Gen has produced a background image. The Builder's job is to take all of that and wire it into the actual code.

The Builder runs a flat while loop. Call the model, dispatch the tool it chooses, feed the result back, repeat until the model calls mark_build_complete or something goes wrong enough to abort. The loop itself is eight lines. What makes it interesting is the toolset inside it.

The Builder has more tools than any other agent in the pipeline. It can read a file, write a file, list the files in a directory, copy a file, patch a file (find an exact string and replace it), and call the terminal tool. Six operations, each corresponding to a real step in the job. The variety matters: most of the build is copy operations, some of it is writes, a small slice is targeted patches.

There are two human gates. Before the agent touches any code, it proposes a plan and waits for approval. When the build finishes, it presents the output and waits again.

In the version that burned $138, there was no mandatory compile gate. The model could see compiler output — it was running npm run build itself via the terminal tool, reading the errors, and attempting patches — but nothing stopped the loop when compilation failed. The model just kept trying. I added a hard compile gate later: compilation runs after the model signals done, errors feed back for a bounded number of fix rounds, and if it doesn't converge, the build rolls back. But that structure didn't exist during the failure I'm about to describe.

What it did

The casino codebase is a React application backed by serverless AWS functions. Every symbol in the game, every color, every win message is baked into the source code. The Builder's job was to fork it into a themed variant: copy the structural files unchanged, swap the colors and fonts and API endpoints to match the spec, and patch the main game component so the reels, win messages, and tutorial reflected the theme.

For an ancient Egypt variant: gold and lapis tones replacing the original palette, hieroglyph names replacing cloud service names in the symbol configuration, the win message changed from something about AWS architecture to something about pharaohs. Same code structure, different content surface.

The copy-and-patch pattern worked well for small, isolated files. The CSS config, the sound file manifest, the theme metadata — all clean boundaries. The Builder handled them without complaint.

The main game component was a different case entirely.

What went wrong

The file was too big to edit safely.

SlotGame.js was about 1,900 lines. It held the spin logic, the win-calculation state machine, the reel animation handling — and scattered throughout all of that: theme-specific strings. Win messages embedded in conditional branches. Tutorial copy baked into JSX. Symbol references in a dozen different places, each with slightly different surrounding syntax.

I had already moved the color swap, the font swap, and the static string substitutions into plain Python — deterministic passes that ran before the model got involved. That reduced the patch surface significantly. It wasn't enough.

The remaining patches still ran into the problem the patch tool created: to change a line, the model had to reproduce the surrounding block exactly. Every bracket, every indentation level, every blank line before and after the target. On a 1,900-line file, the model didn't have a reliable mental model of every whitespace detail. It guessed. Sometimes correctly. Often close enough to fail.

Themed names broke the parser.

Symbol names in a spec sometimes contained apostrophes. A symbol called Chef's Trio sounds reasonable in a design document. Dropped into a single-quoted JavaScript string, it breaks the parse. The model didn't consistently escape these. The compiler would reject the file, the agent would read the error, and in fixing the apostrophe it would shift an indentation level, which broke a different thing.

Each failed patch made the file worse.

Every edit left the file slightly different from what the model remembered. The model patched based on what it had seen from a prior read — but after each change, the file no longer matched that snapshot. So the model was proposing patches against a version that no longer existed on disk. The exact-string match would fail because the surrounding context had changed, and the model — not knowing what the file currently looked like — would guess at what the block might be now and try again, often wrong.

There was a way to bound this: force a full re-read of the current file before every patch. I hadn't done that. The token cost of re-reading 1,900 lines on every turn seemed wasteful on the runs that were going well. On the runs that weren't, it would have caught the drift early.

Duplicate structures were the signature failure. When the exact-match patch tool rejected the model's target string, the model would fall back to rewriting larger sections of the file via the write tool — sometimes inserting the replacement alongside the original instead of replacing it, producing two copies of the same block. Duplicate export default statements. Two return (...) calls. Orphaned JSX floating between structural blocks that no longer connected to anything.

The loop had no ceiling.

868 calls. Three hours. Over 413 million cached input tokens as the model re-read the same growing conversation on every call. About $138 at list rates for Sonnet — and that was with prompt caching enabled. Bedrock's caching meant each call charged a fraction of the full input cost. Without it, the bill would have been an order of magnitude worse. Caching made the Builder viable on normal runs; it also made the worst-case run cheap enough per-call that I didn't notice it was spiraling until an hour in.

I knew abstractly that an unbounded loop backed by a paid API was a risk. Knowing it abstractly and watching it happen are different experiences.

What the fix looked like, in outline

The eventual solution wasn't about making the model more careful. It was about removing the reason to edit that file at all.

The component was fragile to patch because it mixed structural code with content. Theme-specific strings were not isolated — they were woven into the spin logic and the win-calculation branches. The fix was to separate them: move the theme-specific text into a config file the component reads at runtime, so the Builder's job for that component becomes generating a JSON file rather than patching 1,900 lines of JSX.

Generating a JSON config from a spec is ordinary Python — a function reads the spec fields and serializes them into the shape the component expects. No model involvement, no string matching, no file editing. The component reads the config at runtime. The model never touches the component at all.

I'm not going to explain that fix in full here — the full redesign and what it teaches about which parts of a pipeline should involve a model at all is the subject of the final post in this series.

What to take from this

Two things, both short.

A code-modifying agent is only as safe as the size and shape of what you ask it to edit. Small files with a clean boundary between structure and content are workable targets. Large files with interleaved concerns are not. If your agent is patching a big file and behaving badly, examine the file before you examine the model.

Any model loop backed by a paid API needs a hard turn cap before you trust it with a real run. Not added later as a safety measure — as a condition of running at all. The cap doesn't improve the agent's reasoning. It makes the worst-case run affordable rather than surprising.


Previous: the image agents. Next: the Tester agent. The full redesign and the general lesson — about what should and shouldn't be an agent — is the final post in this series.

Top comments (0)