Vers des bases de code autonomes

Nous sommes ravis de la réaction qu’a suscitée notre recherche sur la mise à l’échelle du codage autonome de longue durée.
Ce travail a commencé comme une recherche interne visant à repousser les limites des modèles actuels. Dans le cadre de cette recherche, nous avons créé un nouveau cadre d’orchestration d’agents pour coordonner plusieurs milliers d’agents et observer leur comportement. Le mois dernier, notre système était suffisamment stable pour fonctionner en continu pendant une semaine, en réalisant la grande majorité des commits sur notre projet de recherche (un navigateur web). Ce navigateur n’était pas destiné à être utilisé en externe et nous nous attendions à ce que le code présente des imperfections.
Cependant, même avec ses bizarreries, le fait que des milliers d’agents puissent collaborer pour produire un travail presque entièrement exécutable sans intervention humaine nous a semblé constituer une étape importante à partager. Depuis, nous avons poursuivi nos recherches et nous voulions détailler davantage la façon dont ce cadre a été construit.
Nous mettons également une partie de ces travaux de recherche à disposition pour qu’un nombre limité d’utilisateurs puisse l’essayer.
Contexte
Notre projet de recherche a commencé comme un projet personnel que je menais en parallèle.
Un navigateur me semblait être un benchmark intéressant. C’était suffisamment complexe pour révéler les limites des modèles de pointe, et il y a de nombreux sous-systèmes différents qui doivent fonctionner ensemble.
Mon plan initial était de prendre en charge le rendu de pages web sans prise en charge de JavaScript. J’ai commencé par solliciter Opus 4.5, en lui demandant d’écrire un plan détaillé pour construire un moteur de navigateur. Je le relançais régulièrement pour lui dire de « continuer » afin de voir jusqu’où il irait dans le plan.
Cela a rapidement échoué. Le modèle perdait le fil de ce qu’il faisait, s’arrêtait fréquemment pour déclarer qu’il avait terminé avec succès alors que c’était loin d’être le cas, et se retrouvait bloqué sur des détails d’implémentation complexes. Mais il montrait aussi des signes de connaissances approfondies et d’intelligence. Il savait écrire du bon code sur de petits segments.
Le problème central était que le navigateur représentait une tâche trop écrasante et devait être décomposé en sous-tâches. Ensuite, j’ai demandé à l’agent d’élaborer un graphe de dépendances des principales tâches que des agents pouvaient prendre en charge en parallèle. Des agents étaient lancés manuellement pour les tâches et relancés lorsqu’ils s’arrêtaient. Cela a augmenté le débit, mais les résultats n’étaient pas beaucoup meilleurs. Les agents ne pouvaient pas communiquer entre eux ni donner de retour sur le projet dans son ensemble. Le système devait être plus dynamique.
Entre-temps, GPT-5.1 (puis GPT-5.2) a commencé à donner de meilleurs résultats en matière de suivi précis des instructions. Cela semblait bien convenir à des agents de longue durée, nous avons donc mis à jour notre infrastructure pour utiliser des modèles OpenAI à partir de ces expériences.
À ce stade, l’infrastructure pouvait construire une version simple du navigateur web sans JavaScript, mais construire un moteur de navigateur complet avec un seul agent aurait été beaucoup trop lent.
C’est ce qui a lancé notre cycle de recherche suivant. Pouvions-nous dépenser 10× plus en ressources de calcul pour obtenir un débit réellement 10× plus élevé ?
Du mono-agent au multi-agent
Nous avons démarré un nouveau dépôt avec une simple infrastructure écrite en Rust.
Plutôt que de gérer la complexité des systèmes distribués, nous avons fait tourner cette infrastructure sur une seule grande VM (Virtual Machine) Linux avec beaucoup de ressources. Pour la contrôler, nous nous connections en SSH à la VM et utilisions une simple interface en ligne de commande.
Nous avons passé plus de temps dès le départ à mettre en place une bonne observabilité du système. Nous enregistrions tous les messages des agents, les actions du système et les sorties des commandes, avec des horodatages afin de pouvoir analyser et rejouer les sessions. Cela nous a été utile non seulement pour les revues manuelles, mais aussi pour les réinjecter dans Cursor afin de filtrer de grandes quantités de données et de trouver rapidement des schémas.
Auto-coordination
Notre première idée multi-agents était la plus simple : faire en sorte que des agents ayant des rôles équivalents utilisent un fichier d'état partagé pour voir sur quoi les autres travaillent, décider sur quoi travailler et mettre le fichier à jour.


Nous donnerions le moins de directives possible sur ce qu'il faut faire et laisserions plutôt les agents découvrir comment s'auto-coordonner. Cette approche a rapidement échoué.
Le fichier de coordination a très vite créé davantage de problèmes. Les agents gardaient les verrous trop longtemps, oubliaient de les libérer, essayaient de verrouiller ou déverrouiller alors que ce n'était pas autorisé et, de manière générale, ne comprenaient pas l'importance de détenir un verrou sur le fichier de coordination. La gestion des verrous est facile à rater et difficile à bien réussir, et davantage de prompting n'a pas aidé.
Le verrouillage provoquait également trop de contention. Vingt agents se retrouvaient ralentis à un débit équivalent à celui de un à trois, la plupart du temps étant passée à attendre des verrous. Nous avons essayé de donner aux agents un outil pour attendre explicitement le travail d'un autre agent, mais ils l'ont rarement utilisé. Nous avons aussi essayé une approche de contrôle de concurrence optimiste sans verrou, ce qui a réduit la surcharge mais n'a pas supprimé la confusion.
L'absence de structure entre les agents signifiait qu'aucun agent ne prenait en charge de grandes tâches complexes. Ils évitaient la contention et les conflits, préférant des changements plus petits et plus sûrs plutôt que d'assumer la responsabilité du projet dans son ensemble.
Ajout de structure et de rôles
Ensuite, nous avons séparé les rôles pour donner aux agents un véritable périmètre de responsabilité et de redevabilité :


Un Planner commençait par définir précisément l’approche et les livrables pour faire progresser l’exécution des instructions de l’utilisateur. Cela était transmis à un Executor, qui devenait l’agent principal unique chargé de s’assurer que le plan était mené à bien dans son intégralité. L’Executor pouvait générer des tâches pour des Workers, ce qui permettait une mise à l’échelle linéaire et un débit accru.
Pour assurer une progression continue et la redevabilité, un Judge indépendant intervenait une fois l’Executor terminé afin de déterminer si la tâche était achevée et si une autre itération devait être lancée. Cela a permis de résoudre de nombreux problèmes de coordination. Le fait d’avoir un rôle unique dédié à la responsabilité et à la supervision de l’exécution permettait aux Workers de se concentrer strictement sur leur propre tâche tout en garantissant que le système global continue à produire des résultats.
Observation et recherche locale
Aboutir à ce design a nécessité une observation attentive du système.
S'il y avait un problème majeur, il avait tendance à se reproduire fréquemment et sur de nombreux agents et appels d'outils. Par exemple, nous avons constaté qu'il y avait trop de contention, car de nombreux agents exécutaient git restore en même temps. Nous avons utilisé Cursor pour analyser les logs et les comparer à nos prompts afin de comprendre pourquoi le comportement ne correspondait pas à nos attentes.
En fin de compte, nous avons constaté que ce système était limité par le worker le plus lent. Il était trop rigide.
Faire toute la planification en amont rendait également difficile le réajustement dynamique du système à mesure que de nouveaux problèmes étaient découverts. Certains agents finissaient par partir dans des directions contre-productives, incapables de se corriger eux-mêmes avant la prochaine itération de la boucle.
Exécuteur continu
La version suivante a supprimé le planificateur indépendant.
L’exécuteur pouvait désormais aussi planifier la manière d’atteindre l’objectif, en plus de lancer des tâches. Comme il était l’unique agent, il n’avait pas besoin d’écrire un plan quelque part, de s’en tenir à un plan statique et immuable, ni d’attendre strictement que tous les workers aient terminé.
Assurer l’actualisation
Pour éviter que les agents, quel que soit leur rôle, ne dérivent sur de longues périodes, nous avons introduit des mécanismes d’actualisation :
- Un
scratchpad.mddoit être fréquemment réécrit plutôt qu’être simplement enrichi. - Les agents individuels doivent produire automatiquement un résumé lorsqu’ils atteignent les limites de contexte.
- Nous avons ajouté de l’auto-réflexion et des rappels d’alignement aux invites système.
- Les agents étaient encouragés à changer de cap et à remettre en question leurs hypothèses à tout moment.
Le système était désormais très dynamique et flexible : il pouvait explorer le code de manière proactive, reconsidérer des décisions, gérer des workers, entrelacer des tâches et refléter en continu les informations les plus récentes. Nous avons constaté que les agents suivaient raisonnablement bien les instructions jusqu’à leur terme, nous avons donc supprimé le juge pour garder le système simple.


Comportements pathologiques
Malgré ces améliorations, l'exécuteur continu a commencé à présenter des comportements pathologiques. Il se mettait à dormir de façon aléatoire, arrêtait d'exécuter des agents, faisait le travail lui‑même, refusait de planifier et de lancer plus que quelques tâches très ciblées, ne fusionnait pas correctement les changements des workers et déclarait la fin de l'exécution de manière prématurée.
Nous avons découvert qu'on lui assignait trop de rôles et d'objectifs à la fois, notamment : planifier, explorer, faire de la recherche, lancer des tâches, vérifier l'état des workers, relire le code, effectuer des modifications, fusionner les résultats et juger si la boucle est terminée. Avec le recul, il est logique qu'il ait été submergé.
La conception finale du système
La conception finale intègre l'ensemble des enseignements que nous avons tirés :
- Un planificateur racine possède l'intégralité du périmètre des instructions de l'utilisateur. Il est responsable de la compréhension de l'état actuel et de la définition de tâches spécifiques et ciblées qui font progresser vers l'objectif. Il ne code jamais lui‑même. Il ne sait pas si ses tâches sont prises en charge, ni par qui.
- Lorsqu'un planificateur estime que son périmètre peut être subdivisé, il crée des sous‑planificateurs qui prennent pleinement possession de la partie étroite déléguée, avec le même niveau de responsabilité mais uniquement pour cette partie. Ce comportement est récursif.
- Les workers récupèrent des tâches et sont seuls responsables de les mener à bien. Ils n'ont aucune connaissance du système global. Ils ne communiquent avec aucun autre planificateur ni worker. Ils travaillent sur leur propre copie du dépôt et, une fois leur travail terminé, rédigent un unique compte‑rendu de passation que le système soumet au planificateur qui a demandé la tâche.
Fait intéressant, cela reflète la façon dont certaines équipes de développement fonctionnent aujourd'hui.


Les sous‑planificateurs augmentent la capacité de traitement en déployant rapidement de nombreux workers, tout en garantissant qu'un agent conserve la responsabilité et la maîtrise complètes de l'ensemble du système. Cela a également aidé pour les grands projets et les tâches où un seul planificateur serait autrement submergé et développerait une vision trop étroite.
La passation contient non seulement ce qui a été fait, mais aussi des notes importantes, des préoccupations, des écarts, des découvertes, des réflexions et des retours. Le planificateur la reçoit comme un message de suivi. Cela maintient le système en mouvement continu : même si un planificateur est « terminé », il continue de recevoir des mises à jour, récupère la dernière version du dépôt et peut continuer à planifier et à prendre des décisions ultérieures.
Tous les agents disposent de ce mécanisme, qui permet au système de rester incroyablement dynamique et auto‑convergent, en propageant l'information vers le haut de la chaîne jusqu'aux responsables ayant des vues de plus en plus globales, sans le surcoût d'une synchronisation globale ou de discussions croisées.
Suppression de l’intégrateur
À l’origine, nous avions ajouté un intégrateur pour disposer d’un contrôle qualité centralisé, avec une vue globale, et pour éviter la concurrence entre trop de workers essayant de push, de rebaser, de résoudre des conflits et de fusionner simultanément.
Il est rapidement devenu un goulet d’étranglement évident. Il y avait des centaines de workers et un seul point de passage (autrement dit de la « paperasserie ») par lequel tout le travail devait passer. Nous avons essayé de modifier les prompts, mais avons finalement décidé qu’il était superflu et qu’on pouvait le supprimer pour simplifier le système.
Débit et compromis
Le système a atteint un pic d’environ 1 000 commits par heure, avec 10 millions d’appels d’outils sur une période d’une semaine. Une fois lancé, il n’a plus nécessité aucune intervention de notre part.
Des choix délibérés ont été faits pour atteindre ce niveau de débit.
Fiabilité des commits
Quand nous exigions une exactitude à 100 % avant chaque commit, cela provoquait une forte sérialisation et un ralentissement important du débit effectif. Même une petite erreur isolée, comme un changement d'API ou une faute de frappe, suffisait à quasiment arrêter le système. Les workers sortaient de leur périmètre et commençaient à corriger des éléments sans rapport. De nombreux agents se marchaient dessus en essayant de corriger le même problème.
Ce comportement n'était ni utile ni nécessaire. Laisser un peu de marge permet aux agents de faire confiance au fait que d'autres problèmes seront bientôt corrigés par d'autres agents, ce qui est vrai puisque le système dispose d'une véritable prise de responsabilité et d'une délégation efficaces sur l'ensemble de la base de code. Des erreurs apparaissent puis sont corrigées rapidement. Le taux d'erreur reste faible et stable, rarement totalement nul, mais régulier et gérable, sans explosion ni dérive.
Cela peut indiquer que le système efficace idéal accepte un certain taux d'erreur, mais qu'une branche finale « verte » est nécessaire, où un agent prend régulièrement des snapshots et effectue un passage rapide de corrections avant la mise en production.
Surcharge de synchronisation
Parfois, plusieurs agents touchent le même fichier ou refactorisent le même code. Plutôt que d’essayer d’éliminer complètement ces situations ou de recourir à une solution de sur‑ingénierie, nous acceptons quelques moments de turbulence et laissons le système converger et se stabiliser naturellement sur une courte période.
Cela consomme quelques jetons supplémentaires et crée de la contention locale, mais le système reste globalement plus simple : il est plus facile d’aligner les modèles sans les submerger, plus facile à gérer et à observer, avec moins de friction et une meilleure productivité globale. Cela évite aussi les approches inutilement complexes.
Enseignements sur l'infrastructure
Chaque exécution multi-agents tournait sur sa propre machine puissante avec des ressources système abondantes, afin d’éviter de complexifier trop tôt la gestion de systèmes distribués. C’était un bon choix, car la plupart des exécutions culminaient à plusieurs centaines d’agents, ce qui saturait généralement ces machines sans les surprovisionner. Cette architecture facilitait l’observation des métriques système, ainsi que le partage et la copie d’état lorsque nécessaire.
Après avoir limité l’utilisation de la RAM par les agents, le disque est devenu le principal goulet d’étranglement. En particulier avec un projet monolithique, des centaines d’agents compilant simultanément entraînaient plusieurs Go/s de lectures et d’écritures d’artefacts de build. Cela avait un impact significatif sur le débit global du harness, ce qui a été une leçon intéressante : la structure du projet, les choix d’architecture et l’expérience développeur peuvent affecter le débit en tokens et en commits, simplement parce que le travail sur la base de code (par ex. la compilation) occupe l’essentiel du temps, au lieu de permettre, idéalement, de consacrer ce temps à la réflexion et au codage.
Il existait aussi des contraintes et des inefficacités dans l’environnement de développement général : des choses qui ont du sens ou restent négligeables dans l’espace de travail d’un seul utilisateur deviennent très visibles quand des centaines d’agents font la même chose sur une seule machine. Une façon triviale de résoudre cela serait de donner à chaque agent sa propre machine. Mais il existe des opportunités intéressantes et faciles d’accès pour de gros gains d’efficacité, simplement en repensant et en reconcevant certains de ces primitifs et outils.
Par exemple, de nombreux outils comme Git et Cargo utilisent des verrous partagés, principalement comme mécanisme simple de contrôle de la concurrence. Le fait d’apporter des mécanismes bien établis issus de systèmes concurrents comme les bases de données pourrait-il les rendre tout aussi efficaces dans des systèmes multi-agents ? Tous les agents ont leur propre copie du dépôt, mais la plupart des fichiers et artefacts sont identiques ; l’ajout de fonctionnalités simples de copy-on-write et de déduplication, que l’on trouve dans des systèmes de stockage de production plus sophistiqués, pourrait-il offrir les mêmes gains faciles à un système typiquement « mono-utilisateur » sans construire une infrastructure séparée ?
Spécifier l’intention aux agents
Les instructions données à ce système multi‑agents étaient très importantes.
Au départ, nous n’en avons pas fait notre objectif principal ; nous visions plutôt un harnais stable et efficace. Mais l’importance des instructions est rapidement devenue évidente. Nous interagissions essentiellement avec un agent de programmation classique, sauf qu’il disposait de plusieurs ordres de grandeur supplémentaires en temps et en capacité de calcul. Cela amplifie tout, y compris les instructions sous‑optimales et peu claires.
Passer plus de temps sur les instructions initiales a du sens. En fin de compte, les agents restent des agents : ils sont entraînés à suivre vos instructions strictement, à emprunter ces chemins, sans les modifier ni les outrepasser, même si elles sont mauvaises.
Nous voulions voir des résultats dans nos projets de recherche, nous avons donc modifié nos instructions initiales à mesure que le projet et le harnais évoluaient. Nous apprenions à construire un navigateur en même temps que nous apprenions à faire fonctionner ce nouveau système multi‑agents, et nous pouvions voir des spécifications médiocres ou insuffisamment détaillées se refléter dans la qualité des outputs, ce qui n’était pas dû au harnais lui‑même. Le harnais ne faisait que suivre nos instructions à la lettre.
Quelques exemples issus du projet de navigateur :
- Au départ, les instructions étaient centrées sur l’implémentation des spécifications et la correction des bugs. Des instructions telles que « spec implementation » étaient suffisamment vagues pour que les agents s’enfoncent dans des fonctionnalités obscures et rarement utilisées plutôt que de hiérarchiser intelligemment les priorités.
- Nous supposions implicitement qu’il existait des attentes de performance dans des limites acceptables pour les utilisateurs. Mais il a fallu des instructions explicites et des timeouts stricts pour forcer les agents à équilibrer les performances avec les autres objectifs.
- Pour des parties complexes du système, les agents peuvent écrire du code qui présente des fuites mémoire ou provoque des blocages (deadlocks). Les humains le remarqueraient, mais ce n’était pas toujours évident pour les agents. Des outils explicites de gestion des ressources basés sur les processus étaient nécessaires pour permettre au système de se rétablir proprement et d’être plus robuste.
Notre première version du navigateur simple sans JavaScript a convergé vers une architecture inapte à évoluer vers un navigateur complet. C’était un échec de la spécification initiale.
De même, bien que les agents aient été informés que le projet était un navigateur « from scratch », ils ont tout de même importé certaines dépendances qu’ils auraient pu implémenter eux‑mêmes, ou utiliser comme échafaudage temporaire pendant que la véritable implémentation était en cours. C’était une lacune dans les instructions. Une exécution ultérieure a explicitement exposé la philosophie en matière de dépendances et les bibliothèques à ne pas utiliser, ce qui a corrigé ce problème.
Cette exécution ultérieure a également opéré une importante refonte en de nombreux crates autonomes, en s’éloignant d’un monolithe. Le dépôt était dans un état extrêmement dégradé, mais le système multi‑agents a convergé vers un code fonctionnel en quelques jours. Cela a montré que le système a une forte capacité à travailler de manière collaborative et intelligente, en tenant bon même à partir d’états totalement cassés au lieu de se dégrader davantage ou de se retrouver bloqué. Cette exécution a également passé beaucoup moins de temps en attente de compilation, avec un débit multiplié par plusieurs par rapport à auparavant.
L’architecture et les instructions comptent. Les agents ont d’immenses compétences d’ingénierie mais suivront les instructions jusqu’au bout, qu’elles soient bonnes ou mauvaises. Trouver l’équilibre entre des métriques excessivement étroites et une liberté non structurée était délicat, tout comme savoir ce qui était évident par rapport à ce qui nécessitait une mention explicite.
Tout cela souligne l’importance de faire émerger, de spécifier et de comprendre l’intention, qui devient encore plus cruciale à cette échelle. La pilotabilité et l’observabilité seront des domaines de recherche intéressants à continuer d’explorer.
Optimiser les prompts
Le prompting a été une part importante du processus d'évolution.
Nous avons constaté qu'il valait mieux ne pas donner d'instructions pour les choses que le modèle sait déjà faire, mais seulement pour ce qu'il ne sait pas encore faire (par exemple, la collaboration multi-agents) ou pour ce qui est spécifique au domaine concerné (par exemple, comment exécuter les tests, votre pipeline de déploiement). Traitez le modèle comme une nouvelle recrue brillante qui connaît très bien l'ingénierie, mais pas encore votre base de code ni vos processus spécifiques.
Les contraintes sont plus efficaces que les instructions. « Pas de TODO, pas d'implémentations partielles » fonctionne mieux que « pense à terminer les implémentations ». Les modèles font généralement de bonnes choses par défaut. Les contraintes en définissent les limites.
Évitez la mentalité de liste à cocher pour les tâches de plus haut niveau ou plus approfondies. Donnez des instructions détaillées sur votre intention, mais gardez en tête que donner des actions trop spécifiques tend à pousser le modèle à se concentrer sur celles-ci plutôt que sur le périmètre plus large. Vous dépriorisez aussi implicitement ce qui n'est pas listé. En général, il vaut mieux laisser le modèle utiliser son jugement et sa marge de manœuvre.
Nous avons en revanche trouvé utile de donner des chiffres et des plages concrètes lorsqu'on parle de quantité ou d'étendue du périmètre. Des instructions comme « génère beaucoup de tâches » produisent généralement un petit nombre : un comportement par défaut conservateur, prudent, qui respecte techniquement la consigne. « Génère entre 20 et 100 tâches » indique qu'il s'agit d'un périmètre plus large, que le modèle doit être ambitieux, et nous avons observé un comportement global très différent.
Enseignements tirés de la conception du système
Nous avons dégagé quelques principes de nos recherches :
- Le système doit être anti-fragile. À mesure que nous augmentons le nombre d’agents s’exécutant simultanément, nous augmentons aussi la probabilité de défaillance. Notre système doit tolérer la défaillance d’agents individuels, en permettant aux autres de se rétablir ou d’essayer des approches alternatives.
- L’empirique plutôt que les suppositions. Nous voulions utiliser les données et l’observation pour effectuer des ajustements, plutôt que d’arriver avec des hypothèses sur la façon dont il devrait fonctionner en nous basant sur des organisations humaines ou sur des conceptions de systèmes existants.
- Concevoir explicitement pour le débit. Cela signifiait faire des compromis sur d’autres aspects du codage, par exemple accepter un taux d’erreurs faible mais stable nécessitant un passage final de validation, plutôt que du code parfaitement fonctionnel 100 % du temps qui ralentirait considérablement le système.
Ces systèmes ont tendance à être d’une élégante simplicité lorsqu’ils sont bien conçus, mais il n’était pas clair quelle approche simple fonctionnerait avant que nous en explorions de nombreuses. La conception actuelle du système fonctionne avec un surcoût minimal et offre une montée en charge linéaire du débit de jetons de manière utile. Aucune itération majeure supplémentaire n’a été nécessaire sur l’infrastructure de pilotage.
Conclusion
Même si le goût, le jugement et l’orientation venaient des humains, l’IA a été un formidable multiplicateur de force pour itérer et explorer rapidement cette recherche.
Cela rappelle en partie la boucle d’IA « vertueuse », où l’IA est utilisée pour développer l’IA, et à mesure que les modèles, les agents et leurs environnements d’exécution s’améliorent, le processus se nourrit de lui‑même et s’accélère de plus en plus. Nous façonnons les outils qui nous façonnent.
Il y a un écho poétique entre cette recherche et la façon dont certaines équipes logicielles fonctionnent aujourd’hui. Ces modèles n’ont pas été explicitement entraînés de cette manière, ce qui suggère qu’il s’agit d’un comportement émergent et peut‑être de la bonne façon de structurer les projets logiciels, en fin de compte.
Nous allons continuer à étudier des agents s’exécutant sur des périodes extrêmement longues, et nos conclusions guideront l’évolution future de notre produit.