code
My agent got less agentic every time I fixed it
A day building a field-data copilot on Gemma 4 at Hacktoberfest, and the agent harness I should have borrowed.
I spent a Saturday at FlytBase’s office for the AI Hackers Collective’s Hacktoberfest. The brief: build an AI system that answers questions about a dataset and draws visualizations to go with its answers. Open-weight models were encouraged, so I stuck with Gemma 4 all day.
I went in confident. I build agents, retrieval systems and evals at work. I left with two prizes, and with the sense that another participant had solved the same problem far more simply.
The brief on the big screen: an oil field, and the records that go with it.
Tools for everything
The dataset was an oil field’s records (wells, inspections, repairs, operator notes, manuals) in directories of JSONL files. Rather than a RAG pipeline, I wanted an agent that could query the data, decide which operations it needed, and answer with a visual.
I built it in LangGraph as a ReAct loop: for each kind of question, I broke my manual approach into small deterministic operations and exposed each as a tool. Evals came along too: questions with expected answers, scored with DeepEval.
My notes for two sample questions, each broken into steps small enough to become tools.
Working around a small model
Gemma 4 was capable, but I couldn’t trust it with arithmetic or dates, and in a multi-step workflow one slip spoils the answer.
So I kept moving reasoning out of the model and into tools. One final question asked whether every gas report got a response in time: within 24 hours before 1 January 2027, and 12 hours after. A gas_reports tool did the matching, the hours and the deadlines. The model only presented the rows: 26 reports, 9 missed.
Sometimes the fix was to take a capability away. When I gave a tool a switch to count workover-rig waits separately, the model flipped it and gave the wrong breakdown. With a small model, every option you expose is another way to be wrong.
I also stopped trusting the final answer. A plain function, not a second model, checked that every ID and number in it came from a tool result, and sent it back once if not:
# verify.py, trimmed
unsupported = [v for v in values if not _matches(v, evidence_numbers)]
if unsupported:
problems.append(f"these numbers are in no tool result: {unsupported}. "
"Take every number from a tool result; "
"do not compute sums or averages yourself")
The model picks tools and writes. Code does the arithmetic and the checking.
Less agent, more space monkey
The evals made progress measurable. A retrieval failure meant the fact wasn’t in any tool result, so I fixed the tool. A generation failure meant the fact was there but missing from the answer, so I fixed the prompt. Most failures were retrieval. Over the afternoon the pass rate went from 4 to 15 of 23 cases. But look at where the arrows go:
Every path back to the agent went through me.
The more failures I dug into, the more I over-engineered the tools, until almost every final question had a tool shaped exactly like it: gas_reports, bypasses, regulator_letter_spills, status_conflicts, recalled_pumps. I was making my agent less agentic. It wasn’t driving any more. It was a space monkey,1 working the one lever it had been trained to work. The answers were right, but it wasn’t really an agent.
The UI
The other half of the brief was visualization, and I treated it the same way: as something to make correct rather than something to explore.
I started with a Streamlit app at two in the afternoon and replaced it 40 minutes later with a Next.js chat built on assistant-ui, talking to the LangGraph server. A “Talking as” picker set the user, so the copilot knew who was asking and kept each person’s threads and memory separate.
Visuals came from a fixed catalog of seven: status card, table, line, bar and stacked bar charts, map and heatmap. The model never sent data. It called show_visual with a type, a title, column names and the ID of a tool result, and the server filled in the rows. A bad spec came back to the model as an error to fix, and the UI showed “Visual not shown” rather than a broken chart.
Q1 from my notebook, answered with a status card filled from tool result r_3900. The model never typed a number into it.
Every chart matched the database. But it was a rendering protocol, not a dynamic interface: the model could only pick from components I’d built. The same space-monkey pattern again.
How the day went
Scores and times from the eval history and the git log.
The test dataset landed at about 4:15. By 5 I was in panic mode, building tools for data the copilot had never seen. It finished with 10 of 11 final questions fully correct.
The harness I should have borrowed
Another participant started from Pi, an existing open-source agent harness, and adapted it to the problem. That was my aha moment of the day. I’d spent hours on plumbing (the loop, data access, file and database tools) that his harness already had. An extensible harness also lets an agent’s capabilities grow without someone hand-designing every operation. It freed him to work on the interesting half: AG-UI and dynamic interfaces.
The irony is that I’d already drawn this line well for my own work. I built the copilot with about 20 parallel coding-agent workspaces: 28 pull requests between noon and 6 pm. I was happy to borrow infrastructure to write the code. I just didn’t think to do it for the agent.
Next time
I came away runner-up in the overall competition, with a special prize for the best use of open-source AI. Probably because nobody was more troubled by Gemma 4’s capabilities than I was. The haul: two Hacktoberfest T-shirts, two Arduino Unos, loads of stickers and a wireless keyboard.
Part of the haul. The stickers didn’t make the photo.
Next time I’d start from an extensible harness and spend the day on evals, model limits and the interface. I still think handing unreliable reasoning to deterministic tools is sound. What I’d change is where I draw the line between infrastructure I build and infrastructure I borrow.
Footnotes
-
In Fight Club, Tyler Durden’s recruits are “space monkeys”: each one does the small job he’s trained to do, without needing to understand any of it. ↩