UnderTheC

5. The LLM loop up close

Who did what in our agent loop (orchestrator, analysts, strategists, workers), what a brief looked like, how verdicts were judged, and the rules we added each time it went wrong.

From 09-28 to 10-09 almost all of our bot code was written by Claude Code subagents, coordinated by one long-running Claude Code session that we called the orchestrator. The paper trail is in syeyoung-tokens: 50 generation reports (league/generations/g*.md), 545 filled-in briefs, 78 strategist proposal files, and ORCHESTRATOR.md, the instruction file a fresh orchestrator session reads before it does anything. This post is about how that loop worked and how it broke.

The roles

There were four kinds of agent by the end.

  • The orchestrator launched the other agents, verified every number they claimed, promoted drafts, kept the generation reports and league/KNOWLEDGE.md (735 lines by the end) up to date, and handled submissions. It was not allowed to design strategy.
  • Analysts (Opus, read-only) diagnosed server and arena losses from replays and wrote a ranked REPORT.md per round. Only they looked at images (rendered filmstrips).
  • Strategists (2-3 per round, Opus) read the analysis and KNOWLEDGE.md and each proposed ranked single changes, each with its evidence, its base bot and how to tell whether it worked. They wrote no code.
  • Workers (Opus 5.5) each made one change to one bot, tested it, submitted it to the arena and reported back.

The agent loop

One round, from 10-04 on.

None of this was the original design. On 09-29 the orchestrator was writing every task focus itself, and we pointed out that all the strategy was coming from it. Strategists followed, and the orchestrator was limited to coordinating. On 09-30, when it started digging into a new server battle itself, it was told that analysis wasn’t its job either. The orchestrator’s notes record why: a single view narrows the search and carries its mistakes through whole generations. The swapped-sides misread in post 2 was one such mistake: it drove three tasks.

Workers started on a mix of models. On 09-29 we moved them all to Opus 5.5 because the Sonnet and GPT workers often shipped only part of the brief, misreported results against a parent, or shipped a no-op as an improvement. Strategists ran on a mix of models until our routes to the GPT models stopped working on 09-30; from strategist round 11 we made them Opus-only too.

A brief, a worktree, one change

Every worker started with no context. Its prompt was a template with the blanks filled in, written to a file so it survived restarts. The late template, league/BRIEF-ARENA.md, opens like this:

You make **one** change to one of our bots for UNSW Battlecode "dragons" and get it judged
by our team arena. You start with no context, so this brief is everything. Read it in full,
then follow it.
...
- **Base:** {base} (arena id `{base_id}`, line `{line}`)
- **Change:** {change}
- **Evidence it answers:** {evidence}
- **How to tell if it worked:** {success}
...
2. Make the ONE change. Don't sweep parameters or bundle a second idea.

The rest covered light arena duels, submitting with --parent set to the bot the code was built on, checking the draft’s own arena games for the targeted weak spot, and a 12-line report. Nearly every rule in it came from a correction.

scripts/new-task.sh gave each task its own git worktree and branch, with the base bot copied into variants/<name>/. Workers could write only that directory. The orchestrator checked that each diff touched nothing else before merging. Six workers ran at once on one laptop.

One change per task was the point. The cleanest signal we ever had was a child against its parent, and a verdict on two changes at once tells you nothing about either.

Verdicts

How a change was judged moved through five stages: league Elo with a hidden holdout, then judge-sandbox duels, then round-robins on held-out seeds, then the arena’s beats_parent plus the eval P(win) from the metrics engine, and finally server per-map rating-adjusted Elo.

In the arena era, a draft played its parent and the top 3 on several map suites, and beats_parent was simply true when it won more games against its parent than it lost. Promotion required it. That rule alone was too loose: 78 games is about ±9 wins, so the orchestrator was told to treat 38-40 or 41-37 as a tie and ask for more games. Round 9 showed what happens otherwise: a line can climb by three promotions that are all inside noise.

How the method changed

  • g1-g25 (09-28 to 09-29): local league, generations. Six tasks per generation, each tagged as a mutate, crossover, fresh or tweak operator, rated by a local Bradley-Terry league. It soon became continuous at our request: a very short loop, tasks of about 20 minutes, a free slot refilled the moment a result came in. The gather-split-trade line climbed this way to gst131.
  • Rounds 1-8 (g26-g33, 09-29 to 10-02): long-running teams. Six Opus teams (search, swarm, giant, hunter, territory, free), mostly from scratch. We suspected that losing context between tasks limited how far an agent could improve a bot, and long-running teams kept theirs. arena-search-7 topped our round-robin at 62.2% and became v9, then went 0-5 against the server’s #1, having lost the corridor farming gst had built up. The rule that came out of it: build on gst131 and add one radical behaviour; no from-scratch bots.
  • Round 9 (g34, 10-02 to 10-04): three long-running teams on the new arena, each owning a line. Their contexts grew long, they drifted, and about 15 verdicts landed inside noise.
  • Round 10 onward (g35, from 10-04): short fresh workers. Server and arena losses go to an analyst, the analysis goes to 2-3 strategists, and about six fresh one-change workers build on the line head. ring-plus went 1 → 4 → 7 → 9 → 16 → 19 → 21 this way, and the ladder went from #35 at 1832 on 10-04 to #16 at 2139 on 10-06.
  • Per-round merges, 10-05 to 10-06. For a day each round’s winning changes were bundled into one submission, so that each round’s improvements would stack. On 10-06 we reversed it: one change at a time on the head, with ports of other winners as ordinary single-change tasks.
  • After 10-06: stall. A deep analysis, 3-4-agent rounds, planner stages and numeric bars; the ladder sat around 2100.
  • The reset (10-08): we told the orchestrator to drop every instruction given since 10-06 and work the way it had up to then. ORCHESTRATOR.md and the brief template went back to their end-of-Oct-6 text. On 10-09 we stopped all workers, mid-task, with four drafts unfinished.

In parallel, from 10-02 to 10-04, we ran Codex on separate branches, to get more tokens working on the problem. It made disciplined single-change queen tweaks and rejected its own failures honestly; codex-queen-value-1 reached #3 in the arena at 1543.

Rules from corrections

Each of these is in the memory notes with the date it started. The ones that changed the most:

  • No map hardcoding (09-30). A worker’s bot picked its play mode from a table of known map sizes, and the orchestrator had accepted it, asking only for a fallback. We called it what it was, a hardcoded map. From then every brief banned map names, exact sizes and lookup tables, and every new threshold had to name the observed cause it responds to.
  • Full brief at launch, no steering (10-05). The orchestrator had been messaging running analysts with its own conclusions (“our queen never splits on Colosseum”), which they then dutifully confirmed. Mid-run messages became limited to new evidence or a rule change from the user, and strategists started only after the analyses finished.
  • No unrequested builds (10-05). Asked to watch how games played out, the orchestrator built a text replay watcher, extended it, and rewrote the briefs to require it, none of which had been asked for.
  • Orchestrate without asking (10-04). The orchestrator kept asking permission for routine steps such as starting an analysis; managing agents was its job, so it stopped asking.
  • Line names describe the mechanism (10-05): ring-contest, not ring-next, and not a name borrowed from the user’s example when the code did something else.
  • Give an idea 2-3 tries if its target moves (10-05), because single noisy pass/fail verdicts were throwing away partial successes.

Failures and fixes

What went wrong What fixed it
From-scratch v9 lost behaviours the gst line had Build on gst131, add one behaviour
Round-9 teams drifted inside noise Short fresh workers, one change each
The arena overrated ring-plus-4, mostly on its 2-seed server suite Read verdicts split by suite
A centre rush won on known maps and lost on generated anti-centre maps Test openings on tools/mapgen families before trusting arena gains
The cloud session running 10-06/07 stopped at the weekly usage limit A “State at stop” section in HANDOFF.md for the next session