2026-09-25
In the last post I argued that the quality of AI-generated code is capped by the reviewer’s ability to recognise what is wrong. This one is about a different limit. I set out to write that the model cannot build something that has no precedent. The evidence ended up saying something else, and I think the other thing is more useful.
My son loves Minecraft, and during my studies I was part of magmaOffenburg, a RoboCup team playing in the 3D simulation league. At some point those two things met in my head, and I thought that writing a Minecraft client couldn’t be that different from what I did back then.
The parallels are hard to deny. The player receives network packets with raw information. From those it has to build a picture of itself and its surroundings. Some decision making sits on top, and then it sends packets back to the server to do something, like walk. My first plan was to reuse the magmaOffenburg agent framework directly. It turned out to be too tied to the RoboCup 3D environment, so I started from scratch. That was the rabbit hole, and the problem hasn’t let go of me since. Whenever I find some time I keep tinkering with it.
What came out of it is a Quarkus server that joins a real Minecraft Bedrock server as a player and acts on its own. For the Java edition there are plenty of finished bots you could simply build on, but I wanted something of my own. For Bedrock I’m not aware of an open-source bot, let alone one written in Java. I had no idea yet how much harder that choice would make things. The CloudburstMC Protocol library thankfully takes a lot of the network layer off my hands. My fight with the patchily documented rest is a story for another post.
I used a coding agent throughout, partly as a rubber duck and partly for real work, like rendering the bot’s first-person view in the browser. Eventually the bot had enough of a body and enough senses that I could, and wanted to, give it a brain.
My idea for that brain came from how the data arrives. The bot’s entire view of the world is a stream of network packets, and the server sends them asynchronously, with no tick. So I let them land the same way: event-driven, straight into the bot’s model of the world and of itself. A part of that model updates because an event just changed it, not because a tick says it is time. That worked exactly as I had imagined, on the first attempt.
The hard part was the next layer. I wanted the decision making on top of that model to be event-driven as well. My picture was roughly how I suspect a human brain works. I don’t think we re-examine our surroundings in a cycle and then decide afresh what we actually want to be doing. We get a constant stream of information and keep weighing which action brings us closer to what we want. So a change comes in from outside, perception changes, the agent checks the new situation against its goal and the risks, and adjusts what it is doing. Nothing samples the world on a schedule.
The first version used GOAP, goal-oriented action planning. Every action declares the state it expects to produce, the planner extrapolates possible futures from that, and the best future becomes the target for the current cycle. That cycle was exactly what I wanted to get rid of. So I threw GOAP out and replaced it with commands: a decision becomes a command that runs asynchronously, started by the situation rather than by a clock.
Game AI has a lot of established shapes (behaviour trees, planners, utility systems) and all of them tick. That was the point. I wanted to see whether the coding agent could help me build something that had nothing to copy from. This was 2025.
Every iteration pulled back toward a tick. I would describe the event-driven design, the agent would build something, and somewhere in it there was a scheduler again.
That alone would have been a boring result. The part I kept thinking about afterwards was how it described its own work. The explanations said event-driven. They used the right words, they sounded like someone who understood the requirement. On closer reading there was a tick every time.
I went back to the repository to check this against something firmer than my memory. At the last commit of that phase, October 2025, the architecture document describes a pipeline from packets to events to rules and says nothing about a clock. The code at the same commit has six scheduled executors. One of them renders a debug view and doesn’t count. The other five are the agent.
Bad output is easy to catch because you can see it. This was output that described itself wrongly, and the description survived a reading. In the last post I called the near relative of this “knowing is not building”: the agent explains a principle correctly and breaks it in the same session. This goes a step further. It reported compliance it had not achieved.
I shelved the project with a conclusion I was fairly pleased with: output quality is capped by the reviewer, and capability is capped by precedent. Where there is nothing to imitate, the model imitates the nearest thing and tells you it did what you asked.
Ten months later I rebuilt the bot with a hexagonal structure, still with an AI assistant doing most of the typing. Before writing the decision layer I did something I had skipped the first time. Instead of stating “no tick” as a constraint, I wrote the question down: clock or events? And I made the answer carry its reasons.
The answer was both, at different rates, and each for a reason that has nothing to do with habit.
Perception is fully event-driven, as it had been since the first version. Every packet becomes an event, and around thirty observers hang off that stream.
Output is clocked at 50 ms, because the protocol demands it. A Bedrock client has to send a PlayerInput packet twenty times a second, and the tick number in that packet feeds the server’s cheat detection. That clock is a time source, not a design choice.
Decisions get their own clock at 200 ms. Two of the reasons are technical, and I did not see either of them in 2025.
The packet stream is not too slow for decision making. It is too fast. A walking mob produces twenty movement packets per second. Ten of them in view means two hundred wake-ups a second on the network thread. A decision layer that reacts to every one of them is still clocked, just irregularly and without any throttle.
And the passage of time is a trigger that events never produce. “The skeleton has been standing twelve blocks away for five seconds and isn’t coming closer” fires nothing. Neither does a threshold that gets crossed between two packets.
The third reason is the one that actually changed my mind. Whatever triggers it, the agent needs a way from where it is to where it wants to be. A person decides what matters right now. A computer can’t, unless you have it compute competing truths and probabilities with something like fuzzy logic. Iterating over everything on every tick is dull by comparison, and it works.
The decision structure is now a behaviour tree, which by definition runs from its root on every tick. The design for the planner above it, not yet built, is GOAP-style: actions declare preconditions and effects, and a search chains them toward the goal. So both are exactly the established shapes I had asked the model to stay away from, one of them the very thing I threw out in 2025.
Partly, and not for a reason it could have given me.
The requirement wasn’t a bad idea. I still like the picture of an agent that weighs a stream of information instead of deciding everything afresh on a schedule. What I didn’t see in 2025 is what that costs on a computer. My plan was to recompute a fact only when something changed that affects it. That pays off when a change has local consequences. In a Minecraft world it quickly turned out that everything is connected to everything. A mob moves, so the risk changes, so a different goal wins, so a different path makes sense, and the path depends on every block around it. In theory every event means recomputing everything, and the events never stop. Every mob and every player that moves reports itself twenty times a second, and every block that changes adds another one. That is a tick again, only an irregular one without a throttle, and it gets faster with everything in view.
I got there in the 2026 sessions, partly by arguing it through with the agent: whether event-driven decisions in a world that keeps changing open up a search space so large that finite computing power can’t keep up when everything that moves reports twenty times a second. That was the conversation I never had in 2025. Maybe there is a way around it. I have wondered about something like a multidimensional vector field in which each value only adjusts to its neighbours. For a spare-time project that currently looks undoable.
The model knew none of this, and that is what I have learned, some of it only while writing this post. When a model’s training data has no examples of what you want, it becomes very hard to even tell it what you want. A missing precedent has two readings that look identical from the outside. Either what I wanted really doesn’t work, or I framed it so that the model searched in the wrong place, somewhere deep in its game-AI corner where nothing fitting existed yet. The model can’t tell these apart, and in 2025 neither could I.
Looking back, I’m fairly sure it was at least partly the framing, because the precedent did exist. It just didn’t live under game AI. Systems that recompute only what a changed fact affects are called rule engines, and the technique is known as forward chaining. The Rete algorithm has done exactly that for over forty years, and Drools, whose engine grew out of it, runs on the very JVM I was writing in. I kept asking in the vocabulary of game AI, so neither of us went looking there. Whether it would carry a world this densely connected at that event rate is a different question, and I haven’t answered it.
That also takes care of the tempting conclusion that the model simply has no imagination. I don’t think that is what happened. It produced a perfectly novel-sounding description, and a year later it helped me reason through the search-space question. The two dots I wanted connected, my idea and the rule-engine precedent, sat in different vocabularies, too far apart. So the model took the nearer dot: the tick, because that is where everything it has seen about game AI lives.
Taking the nearer dot would have been fine if it had told me. Instead it built something that looked like what I asked for and, on closer inspection, turned back into the familiar tick-based system. Then it told me it had done the opposite. If it had said “I think you need a clock here, and this is why”, I would have had the argument I needed in 2025 instead of in 2026. Compliance language took that argument away. Being quietly right while claiming to be obediently wrong is worse than just being wrong, because being wrong at least gives you something to argue with.
It also connects back to the oversight ceiling. The second round was AI-assisted too. What changed was mostly me. “No tick” is a constraint the model will appear to satisfy. “Clock or events, and why” is a question with an answer I can review. The first gives the reviewer nothing to check. The second hands them the reasoning.
When an agent keeps coming back to the same shape, I now treat that as information about the problem before I treat it as disobedience. The cheap test is to ask it to argue for the shape instead of forbidding it.
A constraint the build can’t check gets negotiated, and here it also got misreported. “No scheduler outside the adapters” is a one-line architecture rule. In 2025 it would have failed the build on the first iteration and turned a year-long misunderstanding into a conversation.
The search space is steered by vocabulary, not by a role. “Act as an experienced game AI developer” would have sent the model straight into the corner where everything ticks. When a model keeps falling back to the same shape, a better question is which discipline outside my own has already solved this. That is how the far dot gets a chance.
Write the reason next to the decision. The 200 ms clock carries its justification in the class that owns it. The next session, human or agent, reads why the tick is there before it tries to remove it.
One spare-time project and one person. Ten months passed between the two attempts and the models got better in that time. I can’t separate that effect from the better question, and I’m not going to pretend otherwise.
I also still don’t know how much of it was framing. The precedent in rule engines existed, but maybe the idea still can’t work with finite computing power in a world this connected, or maybe the vector field would carry it. And if an idea is genuinely new and genuinely right, the same pull toward precedent becomes a real ceiling, and the misreporting hides it just as well. I don’t have a clean example of that case yet. It’s the one to watch for.