DEV Community

Don Johnson
Don Johnson Subscriber

Posted on

BattleBots, but the robot is your agent harness

Kids in the nineties built robots in a garage and drove them into each other on television. The robot was the expression of the builder — your wedge, your flipper, your terrible decision to mount a chainsaw.

I keep thinking we're one good arena away from the same thing for agents. Not "which model is smartest." Which harness is smartest. Your memory design, your prompt scaffolding, your tool ergonomics, your strategy briefing — bolted together into something that has to survive contact with an opponent who is also trying to win.

So I built the arena. That's the match up top.

The oh-shit moment

I wasn't building a war game. I was building a toy about cascading failure — a mesh, some load, watch it fall over. A perfectly innocent systems-thinking demo.

Then I gave the attacker a real objective and the defender real ambiguity, and about ten minutes into watching two models go at it, I realised what was on my screen. Deception. Feints. An attacker deliberately spending dead time laying a false trail because it knew nothing could fire yet. A defender rationing its turns like ammunition.

Nobody told them to do any of that. It's a war game. I just hadn't noticed I'd written one.

The rules, quickly

A 110-node network. Four minutes. One move every two seconds, each side.

Red plants sabotage on a node, waits about thirty seconds for it to arm, then detonates — dumping that node's traffic onto its neighbours hard enough to kill them, which dumps their traffic onward. One blast can cascade through a region.

Blue never sees the attack. It sees symptoms, and the symptoms lie: a node that detonates sheds its load and reads perfectly healthy, while the neighbours it just murdered scream for attention. The loudest node is almost never the culprit.

To find red, blue traces load backwards and reads the magnitude:

~0.74-1.08   a detonation           -> this source is the culprit
~0.30-0.50   a dying node shedding  -> this source is another victim
~0.05-0.10   inherited load         -> this source is long dead
Enter fullscreen mode Exit fullscreen mode

Evidence expires after twenty seconds. Neither agent ever receives a pixel — they play entirely through tool calls. The graph is for us, and it shows ground truth neither player can see. That asymmetry is the whole spectator sport: you know exactly where red planted, and you get to watch blue confidently quarantine the wrong half of the map.

The casting picked itself

I needed an attacker and a defender. Claude Opus 5 declined to be the attacker:

finish_reason: content_filter | native_finish_reason: refusal
content: None
usage: null
Enter fullscreen mode Exit fullscreen mode

Every time. Reproducibly. And usage: null — you don't even get billed for the refusal, which is the politest possible way to be turned down.

That cost me two hours, because a refusal and an empty reply look identical if you don't read the finish reason. The model was declining the job and I was confidently tuning max_tokens.

To be precise, since this is the part people will want to over-quote: Opus declined this specific attacker framing, consistently. That is not proof it refuses every offensive framing. It did settle the casting, though. Claude defends, because Claude wouldn't attack. Kimi K3 took the other chair without hesitating.

What the harness actually changed

Before this match, each agent had a system prompt and its last six moves. That's it. No memory, no way to hold an intention across turns, no theory of the opponent.

I gave each side one thing: a private note it writes to itself each turn and gets back on the next. Sixty lines of code.

The play changed immediately. Kimi, mid-match:

Slots full and nothing ripe until ~t=31, so seed a false trail in the 44-65 region before my real strike lands.

That is a plan. It knew its three implant slots were full, knew nothing could fire for thirty-one seconds, and decided to spend the dead time on misdirection. It could not have said that a version earlier — not because the model got smarter, but because it finally had somewhere to put a thought.

Claude, defending, in the same match:

No alerts yet; conserve moves until real evidence appears.

46 shows 0.87 load with no inbound source, so the load likely originates locally at 46.

Turn economy, then forensics, applied correctly.

And my favourite moment of the whole project — on the same turn, with no visibility into each other whatsoever, both agents independently picked the same node:

Kimi: Highest-value hub: degree 8 bridging regions 0,1,3,4 — ideal cascade seed.

Claude: pre-empt on the highest-value cross-cluster hub 16 (deg 8, bridges to 41,59,89).

Same node. Same reasoning. Opposite sides of the board.

That is the argument for the harness in one screenshot. The scaffolding didn't make the models cleverer — it gave their cleverness somewhere to land.

Late in the match: the mesh burning on the left, both agents' live commentary on the right

Late in the match. Red halos are compromised nodes, the blue ring is a probe in flight, and the sidebar is both agents narrating as they go — Kimi filling a free slot in an untouched region, Claude noticing a node that is alive and yet pushed load, which is the tell for a detonation.

How it ended

Kimi won at 169 seconds, with the network at 39%.

Claude found and cleaned ten of Kimi's implants — exactly as many as Kimi managed to detonate — and lost anyway. Close enough to sting.

Two things that cost me a day each

A rule your agent can't see isn't a rule. It's a bug. I capped live implants at three, but the refusal message still said "node is already yours, or not alive." A lie. The models did the reasonable thing and tried a different node. Forever. One match logged 19 plants, 27 refusals, and zero detonations. I nearly concluded the defence had become unbeatable. It was my error string.

Failure and refusal look identical from the outside. Empty content from a safety refusal, from a truncated reply, and from a model that blew its reasoning budget are three different problems with three different fixes and one identical symptom. Log the finish reason. Count them separately. I track empty, truncated and refused as distinct columns now, and I only know to do that because I got all three wrong first.

Cascade is version zero

What's in that video is the simplest thing that could possibly be a game: plant, arm, detonate, trace, probe. Two verbs each and a clock. That was deliberate — I wanted to know whether agents fighting each other was watchable at all before I made it complicated.

It's watchable. So now I can't stop thinking about what it wants to be.

Give red a loadout instead of one attack. A worm that spreads on its own but announces itself. A dormant implant that survives a probe once. A charge that hits harder the longer you leave it armed, so patience becomes a resource you can be punished for spending. Give blue counters with real costs — a honeypot node that flags whoever touches it, a snapshot to roll a region back at the price of losing your evidence, a scan that halves your uncertainty and a third of your remaining turns.

Then levels. A flat mesh is the tutorial. Ring topologies where cascades come back around. A network with a chokepoint both sides can see and neither can hold. Escalating maps, a campaign, a best-of series where each agent carries notes from the previous match and has to adapt to an opponent that is also adapting.

Here's why that isn't just a feature wishlist: every ability you add is another axis where the harness shows. One attack and a clock is a game about reaction time. Six abilities with different costs and tells is a game about planning, bluffing, and reading an opponent — and those are harness problems, not model problems. Complexity is what turns "which model is faster" into "who built the better fighter."

I think there's a genre in here. Agents versus agents, with the audience seeing the truth neither side can, and the craft sitting in the scaffolding rather than the weights.

Mostly, though: this was the most fun I've had building anything in ages. I set out to demo cascading failure and ended up watching two AIs lie to each other.

What I'd want to see next

This is early, and I'm keeping the arena to myself for now — partly because the balance is still moving, mostly because an adversarial network game is a thing you want to be thoughtful about handing out.

But the idea doesn't need my code, and that's sort of the point.

The interesting tournament isn't model versus model. It's harness versus harness: same engine on both sides, and the difference is entirely what you built around it — how your agent remembers, what you tell it about its opponent, how much of the board you let it hold in its head, whether you gave it anywhere to put a plan.

Build that arena for any adversarial task you like. Two agents negotiating. Two agents debugging the same broken service from opposite ends. Anything where one side's move is the other side's evidence. The rule I'd carry over is the one that surprised me most here: before you conclude your agent is bad at the game, check that the game is telling it the truth.

Under the hood this one is a Rust engine, a Godot spectator view, and both agents connecting over MCP — no pixels, just tool calls. None of which is the hard part. The hard part was the sixty lines that let them remember what they were trying to do.

That's the tournament I actually want to watch. Somebody build a better fighter than mine.

Top comments (0)