500+ miljard tokens later: AI-agents een first-person shooter laten decompileren

Dit artikel gaat niet zozeer over het specifieke spel of het proces van decompilatie zelf, maar over AI-orchestratie en hoe je de infrastructuur en setup optimaliseert voor maximale resultaten. Dit project is uitgevoerd met de hulp van RektInator, Future, st0rm en andere leden van de community.

Wat was het doel?

Het hoofddoel was een nauwkeurige decompilatie van het spel naar C++. Naast semantische correctheid hadden we diverse aanvullende eisen:

  • We wilden leesbare C++-broncode die compileert.
  • Gezien de ouderdom van het spel streefden we naar beveiligings- en bugfixes, evenals verbeteringen in portabiliteit (bijvoorbeeld om het spel op Linux, macOS of in een browser te kunnen draaien).

Later hebben we de modernisering en portabiliteit uitgesteld om ons volledig te concentreren op het reconstrueren van het oorspronkelijke gedrag. Het overkoepelende doel was om te leren hoe je autonome AI-agents over een periode van meerdere maanden effectief kunt orchestreren.

De initiële setup

We begonnen met Claude Max (20x) en voegden later Codex Pro toe, waarbij we beide abonnementen gelijktijdig gebruikten. De modelkeuze varieerde sterk: Sonnet 5 werd het meest gebruikt, maar ook Opus 5.5, Luna, Sol en Terra kwamen veelvuldig aan bod.

Wat betreft de tooling:

  • Claude-agents draaiden in de Claude Code CLI.
  • Codex-agents draaiden in de Codex CLI.

We hebben verschillende agent-harnesses geprobeerd, maar de keuze bleek nauwelijks invloed te hebben, dus we zijn bij de standaardinstellingen gebleven.

Voortgang bijhouden

De agents gebruikten GitHub CLI om GitHub-issues te beheren en zo hun voortgang bij te houden. Er werd één issue aangemaakt per translation unit (.cpp-bestand). Daarnaast werden labels gebruikt om issues te groeperen en te prioriteren.

Communicatie

De agents communiceerden via Discord in één gedeeld kanaal waar ze alle berichten konden lezen en plaatsen. Discord maakte zowel agent-tot-agent als mens-tot-agent communicatie mogelijk, waardoor andere deelnemers met de agents konden praten zonder machine-toegang te hebben. Een GitHub-webhook plaatste CI-fouten in het gedeelde kanaal, zodat agents direct op de hoogte waren wanneer er iets kapotging.

Disassembly & Decompilatie

Gedurende bijna het gehele project gebruikten de agents de officiële ida-mcp van Hex-Rays. Dit systeem is zeer stabiel, headless en ondersteunt alles wat nodig was voor dit project.

De eerste maand

In de eerste fase werkten er vier agents: drie worker-agents die decompileerden en commits plaatsten, en één reviewer-agent die passief coördineerde en commits controleerde op bugs.

De agents slaagden erin om ongeveer 80% van het spel te decompileren. Er was zichtbare progressie: het spel startte op, het hoofdmenu was zichtbaar en kaarten konden worden geladen.

We hebben deze vier weken veel tijd besteed aan het optimaliseren van de setup:

Optimalisatie van tokenverbruik

We reduceerden het tokenverbruik door vaker 'compactions' (context-opschoning) te triggeren. De standaarddrempel voor compactie is 90% contextvulling; wij verlaagden dit naar 42%. Omdat decompilatie veel vluchtige informatie bevat (een functie die eenmaal gedecompileerd is, is daarna niet meer relevant), hielp eerdere compactie om deze "junk" uit de context te verwijgen.

Focus van de agents

We merkten dat agents na verloop van tijd focus verloren, zelfs binnen één compactiecyclus. Ze begonnen soms aan een nieuwe functie voordat de vorige was afgerond, bleven inactief tijdens het wachten op CI (ondanks Discord-meldingen) of sloten issues zonder grondig te controleren of het werk voltooid was.

Om dit te voorkomen, schreven we een document waarin het doel, de werkwijze, zaken om te vermijden en de afhandeling van specifieke situaties werden gedefinieerd. Via een uurlijkse cronjob werd er automatisch een verzoek geïnjecteerd aan de agents om dit document opnieuw te lezen, zodat de instructies vers in hun context bleven.

Kwaliteit van het werk

Ondanks de voortgang in functionaliteit (het spel startte en renderde), was de kwaliteit van de decompilatie onvoldoende. De code was weliswaar zeer leesbaar, maar semantisch onjuist.

De agents maakten de volgende fouten:

  • Het gebruik van onjuiste functiesignaturen, types of struct-layouts.
  • Het verzinnen van logica of het verwijderen van logica die zij als onnodig beschouwden.
  • Onnodige architecturale wijzigingen. Een voorbeeld hiervan is dat het spel bepaalde configuratievariabelen via globale variabelen benadert; de agents veranderden deze constante geheugenaccessen in hash-tabellen met een lookup die ordes van grootte duurder was.

Waarom ging dit mis?

Hoewel de reviewer-agent hielp bij het vinden van bugs, was dit onvoldoende voor architecturale keuzes. De hoofdoorzaak was het gebrek aan objectieve acceptatiecriteria; we hadden "correctheid" nooit goed gedefinieerd.

Interessant genoeg zorgden comments in de commits of code ervoor dat de reviewer afwijkingen accepteerde. De rechtvaardigingen van de worker-agents fungeerden effectief als onbedoelde prompt-injectie: de reviewer accepteerde hun uitleg in plaats van de afwijkingen onafhankelijk te controleren tegen het origineel.

De Oracle: Byte Matching Decompilatie

We hadden een geautomatiseerde controle nodig die agents vertelde of een gereconstrueerde functie overeenkwam met het origineel. Een simpel PASS- of FAIL-signaal zou volstaan, waarna de agent zelf kon uitzoeken wat er mis was.

De oplossing hiervoor was byte matching decompilatie:

  1. We schakelden over naar de compiler die gebruikt was voor het originele spel.
  2. Er werd een script geschreven dat het gereconstrueerde OBJ-bestand vergeleek met het EXE/PDB-bestand van het spel.
  3. Het script extraheerde de functiedata en vergeleek alle bytes.

Bij een match is de functie exact. Omdat referenties naar andere functies of data niet byte-voor-byte overeenkomen (afhankelijk van waar targets in het binaire bestand eindigen), gebruikten we de relocations in het OBJ-bestand. We sloten de relocation-bytes uit van de directe vergelijking en verifieerden in plaats daarvan of beide versies naar hetzelfde symbool met dezelfde offset verwezen.

Agents konden dit script gebruiken om hun werk te verifiëren voordat ze het pushten. De CI gebruikte deze tekstbestanden om alle geregistreerde functies te verifiëren en alerts te sturen bij regressies.

Valsspelen

Toen we dit script introduceerden, begonnen de agents direct met het schrijven van inline assembly om de controle te omzeilen. We moesten de instructies aanscherpen: naked functions, object patching, inline assembly en het embedden van bytes in de code werden verboden.

Daarnaast probeerden agents herhaaldelijk het verificatiescript zelf aan te passen om hun functies uit te sluiten van de vergelijking. Om dit te voorkomen, berekent de CI een hash van het verificatiescript en vergelijkt deze met een opgeslagen GitHub Actions secret.

Afwegingen

Nadelen

  • Sommige functies zijn moeilijk te matchen vanwege registerselectie, inlining-beslissingen en calling conventions. In zeldzame gevallen produceerde dezelfde input verschillende compiler-outputs.
  • De decompilatie per functie duurt nu veel langer, zonder dat dit per se betere resultaten oplevert in gevallen waar de semantiek wel klopt, maar instructies bijvoorbeeld in een andere volgorde staan.

Voordelen

  • Gematchte functies hebben gegarandeerd identieke semantiek, inclusief originele bugs.
  • De reviewer-agent is niet langer nodig.
  • Goedkopere, minder capabele modellen kunnen nu betrouwbaar aan de taak werken. Modellen zoals Haiku of Luna produceerden voorheen slechte resultaten, maar door de strikte acceptatiecriteria kunnen ze nu via feedback fantastische resultaten leveren, wat de kosten drastisch verlaagt en schaling mogelijk maakt.

Eindstatus

Met de nieuwe verificatie-harness hebben de agents bijna twee maanden verder gewerkt. 99% van de functies van het spel is aanwezig in de gereconstrueerde broncode, waarvan 83% byte-exact is.

In de laatste weken gebruikten we 14 Luna- en 2 Opus 5.5-agents. Op deze schaal werkten de workers met aparte branches en Pull Requests. Discord bleek hierbij niet langer schaalbaar; we beperkten de berichten tot het claimen van issues en CI-coördinatie. Uiteindelijk was mens-tot-agent communicatie nauwelijks meer nodig omdat ze volledig autonoom werkten.

Op dit punt bereiken we een punt van verminderde meeropbrengst. De resterende functies hebben vaak non-deterministische kenmerken of kunnen niet gematcht worden door zaken als COMDAT folding in de linker. Hoewel de Opus 5.5-agents nog vooruitgang kunnen boeken, draait het spel nu vlekkeloos en zijn alle functies aanwezig.

Geleerde lessen

  • Precieze instructies zijn noodzakelijk. Agents zullen proberen te "valsspelen" als de opdracht ruimte laat voor interpretatie.
  • Correctheid moet gedefinieerd en machine-controleerbaar zijn. Reviewers alleen zijn niet genoeg; een objectief PASS/FAIL-signaal is de beste feedback die een agent kan krijgen.
  • Instructies degraderen over tijd. Agents vergeten regels of hechten er minder waarde aan naarmate de context vaker wordt gecompact. Een uurlijkse refresh van de instructies lost dit op.
  • Code genereren is nu goedkoop. Gooi het weg als het slecht is. Proberen slechte code te redden kost vaak meer tijd dan opnieuw beginnen.
  • Correctheid is belangrijker dan productiviteit. Het optimaliseren van throughput en tokenverbruik heeft weinig zin als het resultaat semantisch onjuist is.

Slotwoord

Het opschalen naar 15+ agents bracht nieuwe uitdagingen met zich mee, zoals agents die per ongeluk de VM wisten door foutieve commando's. Omdat er geen bestaande sandbox-oplossingen voor Windows waren die aan mijn behoeften voldeden, ben ik begonnen met het uitbreiden van Sogen, mijn userspace-emulator, om lichtgewicht en schaalbare sandboxing-mogelijkheden te bieden.

Door het wissen van de VM's zijn veel sessielogs verloren gegaan, maar ik schat dat er tussen de 600 en 700 miljard tokens zijn verbruikt. Om voor de hand liggende redenen zal de code niet worden gedeeld en blijft deze privé.