Vers des bases de code autonomes

Nous sommes ravis des réactions suscitées par nos recherches sur la mise à l’échelle du codage autonome sur de longues durées.
Ce travail a commencé comme une recherche interne visant à repousser les limites des modèles actuels. Dans le cadre de ces recherches, nous avons créé un nouveau harness d’agents pour orchestrer plusieurs milliers d’agents et observer leur comportement. Le mois dernier, notre système était suffisamment stable pour fonctionner en continu pendant une semaine, effectuant la grande majorité des commits de notre projet de recherche (un navigateur web). Ce navigateur n’était pas destiné à un usage externe, et nous nous attendions à ce que le code présente des imperfections.
Cependant, malgré ces particularités, le fait que des milliers d’agents puissent travailler ensemble pour produire un code presque entièrement exécutable sans intervention humaine nous a semblé être une étape importante qui méritait d’être partagée. Depuis, nous avons poursuivi nos recherches, et nous voulions entrer davantage dans le détail de la façon dont le harness a été construit.
Nous ouvrons également une partie de cette recherche à l’essai pour certains utilisateurs.
Contexte
Notre projet de recherche a commencé comme l’un de mes projets personnels.
Un navigateur semblait être un benchmark intéressant. Il était suffisamment complexe pour révéler les limites des modèles de pointe, et il comportait de nombreux sous-systèmes différents qui devaient fonctionner ensemble.
Mon plan initial était de prendre en charge le rendu de pages Web sans JavaScript. J’ai commencé par soumettre un prompt à Opus 4.5, en lui demandant de rédiger un plan détaillé pour créer un moteur de navigateur. Je l’incitais ensuite à plusieurs reprises à « continuer » pour voir jusqu’où il irait dans ce plan.
Cela a rapidement échoué. Le modèle perdait le fil de ce qu’il faisait, s’arrêtait souvent pour proclamer sa réussite alors qu’il en était encore loin, et restait bloqué sur des détails d’implémentation complexes. Mais il montrait des signes de connaissances approfondies et d’intelligence. Il pouvait écrire du bon code par petits morceaux.
Le problème principal était que le navigateur constituait une tâche trop vaste et qu’il fallait la décomposer en sous-tâches. J’ai ensuite demandé à l’agent d’établir un graphe de dépendances des grands chantiers que des agents pourraient prendre en charge en parallèle. Des agents étaient lancés manuellement pour ces tâches, puis 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 ni communiquer entre eux ni fournir de retours sur l’ensemble du projet. Le système devait être plus dynamique.
Pendant ce temps, GPT-5.1 (puis GPT-5.2) a commencé à montrer de meilleurs résultats grâce à sa capacité à suivre précisément les instructions. Cela semblait bien convenir à des agents de longue durée, nous avons donc mis à jour notre harness pour utiliser des modèles OpenAI sur la base de ces expériences.
À ce stade, le harness pouvait créer une version simple du navigateur Web sans JavaScript, mais créer un moteur de navigateur complet avec un seul agent serait prohibitivement lent.
C’est ce qui a lancé notre série de recherches suivante. Pouvions-nous dépenser 10x plus en calcul pour obtenir un débit utile 10x supérieur ?
Du mono-agent au multi-agent
Nous avons démarré un nouveau dépôt avec un harness simple basé sur Rust.
Au lieu de gérer la complexité des systèmes distribués, nous avons exécuté le harness sur une seule grande VM Linux (machine virtuelle) dotée de nombreuses ressources. Pour contrôler le harness, nous nous connections en SSH à la VM et utilisions une interface de terminal simple.
Dès le départ, nous avons consacré plus de temps à mettre en place une observabilité adéquate du système. Nous avons consigné tous les messages des agents, les actions du système et les sorties de commandes, avec des horodatages afin de pouvoir analyser et rejouer les sessions. Cela nous a non seulement aidés à examiner manuellement l’ensemble, mais aussi à réinjecter ces données dans Cursor pour passer au crible de grands volumes de données et repérer rapidement des patterns.
Coordination autonome
Notre première idée de système multi-agent était la plus simple : faire en sorte que des agents aux rôles équivalents utilisent un fichier d’état partagé pour voir sur quoi les autres travaillent, décider de ce sur quoi travailler, puis mettre à jour le fichier.


Nous voulions être aussi peu prescriptifs que possible et laisser les agents trouver eux-mêmes comment se coordonner. Cette approche a rapidement échoué.
Le fichier de coordination a vite créé encore plus de problèmes. Les agents gardaient les verrous trop longtemps, oubliaient de les libérer, essayaient de verrouiller ou de déverrouiller alors qu’ils n’y étaient pas autorisés et, plus généralement, ne comprenaient pas l’importance de détenir un verrou sur le fichier de coordination. Les verrous sont faciles à mal gérer et difficiles à utiliser correctement, et ajouter davantage d’instructions n’a pas aidé.
Les verrous provoquaient aussi trop de contention. 20 agents voyaient leur débit chuter à celui de 1 à 3 agents, l’essentiel du temps étant passé à attendre les verrous. Nous avons essayé de donner aux agents un outil pour attendre explicitement le travail d’un autre agent, mais ils l’utilisaient rarement. Nous avons aussi essayé une approche optimiste de contrôle de concurrence sans verrou, ce qui a réduit la surcharge sans éliminer la confusion.
Le manque de structure entre les agents faisait qu’aucun ne prenait en charge les tâches importantes et complexes. Ils évitaient la contention et les conflits, privilégiant 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 afin de donner aux agents de l’appropriation et de la responsabilité :


Un planificateur définissait d’abord l’approche exacte et les livrables nécessaires pour faire avancer les instructions de l’utilisateur. Cela était ensuite transmis à un exécuteur, qui devenait l’unique agent principal chargé de veiller à ce que le plan soit mené à bien dans son intégralité. L’exécuteur pouvait créer des tâches pour des workers, ce qui permettait une montée en charge linéaire et un meilleur débit.
Pour maintenir la progression et la responsabilité, un juge indépendant intervenait une fois l’exécuteur terminé afin de déterminer s’il avait bien terminé et si une nouvelle itération devait être lancée. Cela a résolu de nombreux problèmes de coordination. Le fait de dédier un rôle unique à la prise en charge et à la supervision de l’exécution permettait aux workers de se concentrer étroitement sur leur tâche, tandis que le système dans son ensemble continuait à produire des résultats.
Observation et ajustements progressifs
Parvenir à cette conception a nécessité une observation attentive du système.
Lorsqu'un problème majeur survenait, il avait tendance à se répéter, sur de nombreux agents et dans de nombreux appels d'outils. Par exemple, nous avons remarqué qu'il y avait trop de contention parce que de nombreux agents exécutaient git restore en même temps. Nous avons utilisé Cursor pour analyser les logs et les comparer à nos requêtes afin de comprendre pourquoi le comportement ne correspondait pas à nos attentes.
Au final, 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 empêchait aussi le système de se réajuster dynamiquement à mesure que de nouveaux problèmes étaient découverts. Certains agents finissaient par partir dans des directions contre-productives, incapables de se corriger d'eux-mêmes avant l'itération suivante 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 créer des tâches. Comme c’était le seul agent, il n’avait pas besoin d’écrire un plan quelque part, de s’en tenir à un plan statique et immuable, ni d’attendre rigidement tous les workers.
Garantir la fraîcheur
Pour éviter que les agents de tous les rôles ne dérivent sur de longues périodes, nous avons introduit des mécanismes pour préserver la fraîcheur :
- Un
scratchpad.mddoit être réécrit fréquemment, plutôt que simplement enrichi par ajouts successifs. - Les agents individuels doivent résumer automatiquement lorsqu’ils atteignent la limite de contexte.
- Nous avons ajouté des rappels d’auto-réflexion et d’alignement aux requêtes système.
- Les agents étaient encouragés à changer de cap et à remettre en question les 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, alterner entre les tâches et tenir continuellement compte des informations les plus récentes. Nous avons constaté que les agents étaient plutôt efficaces pour suivre les instructions jusqu’au bout, donc le juge a été retiré 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 en veille de façon aléatoire, arrêtait de lancer des agents, faisait lui-même le travail, refusait de planifier et de créer plus de quelques tâches très ciblées, n’intégrait pas correctement les modifications des workers et déclarait la tâche terminée prématurément.
Nous avons constaté qu’on lui attribuait trop de rôles et d’objectifs simultanément, notamment : planifier, explorer, faire des recherches, créer des tâches, surveiller les workers, examiner le code, effectuer des modifications, fusionner les résultats et déterminer si la boucle est terminée. Avec le recul, il est logique qu’il ait été dépassé.
L’architecture finale du système
L’architecture finale intègre tous les enseignements que nous avons tirés :
- Un planificateur racine prend en charge l’ensemble du périmètre des instructions de l’utilisateur. Il est chargé de comprendre l’état actuel et de fournir des tâches précises et ciblées qui font progresser l’objectif. Il ne code pas 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 entièrement en charge la portion restreinte qui leur est déléguée, avec le même niveau de responsabilité, mais uniquement pour cette portion. Ce processus est récursif.
- Les workers prennent en charge les tâches et sont les seuls responsables de les mener à bien. Ils n’ont aucune visibilité sur le système dans son ensemble. Ils ne communiquent avec aucun autre planificateur ni worker. Ils travaillent sur leur propre copie du dépôt et, une fois le travail terminé, rédigent un unique compte rendu de passation que le système transmet au planificateur qui a demandé la tâche.
Fait intéressant, cela reflète bien la manière dont certaines équipes logicielles fonctionnent aujourd’hui.


Les sous-planificateurs augmentent le débit en répartissant rapidement le travail entre les workers, tout en garantissant que l’ensemble du système reste intégralement pris en charge et sous la responsabilité d’un agent. Cela a aussi aidé sur les grands projets et les tâches où, autrement, un seul planificateur serait débordé et développerait une vision en tunnel.
Le compte rendu de passation contient non seulement ce qui a été fait, mais aussi des notes importantes, des préoccupations, des écarts, des résultats, des réflexions et des retours. Le planificateur le reçoit sous la forme d’un message de suivi. Cela maintient le système en mouvement continu : même si un planificateur a « 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 les décisions suivantes.
Tous les agents disposent de ce mécanisme, ce qui permet au système de rester incroyablement dynamique et auto-convergent, en faisant remonter l’information le long de la chaîne jusqu’aux responsables ayant une vision de plus en plus globale, sans le surcoût d’une synchronisation globale ou de communications croisées.
Suppression de l’intégrateur
Nous avons initialement ajouté un intégrateur pour assurer un contrôle qualité central, avec une vision globale, et pour éviter les conflits dus au trop grand nombre de workers essayant de push, rebase, résoudre des conflits et 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 (c.-à-d. de la « paperasserie ») par lequel tout le travail devait passer. Nous avons essayé de changer les requêtes, mais avons finalement décidé que c’était inutile et qu’il pouvait être supprimé afin de simplifier le système.
Débit et compromis
Le système a atteint un pic d’environ 1 000 commits par heure, pour 10 M d’appels d’outil sur une semaine. Une fois le système démarré, il n’a nécessité aucune intervention de notre part.
Des compromis intentionnels ont été nécessaires pour atteindre ce débit.
Fiabilité des commits
Lorsque nous exigions une fiabilité de 100 % avant chaque commit, cela entraînait une forte sérialisation et ralentissait considérablement le débit réel. La moindre petite erreur, comme un changement d'API ou une faute de frappe, suffisait à quasiment bloquer tout le système. Les workers dépassaient leur périmètre et commençaient à corriger des éléments sans rapport. De nombreux agents s'empilaient et se marchaient dessus en essayant de corriger la même issue.
Ce comportement n'était ni utile ni nécessaire. Laisser une certaine marge signifie que les agents peuvent partir du principe que les autres issues seront bientôt corrigées par d'autres agents, ce qui est bien le cas puisque le système répartit efficacement la responsabilité et la délégation sur l'ensemble de la base de code. Des erreurs apparaissent, puis sont rapidement corrigées. Le taux d'erreur reste faible et constant : il est peut-être rarement nul, mais il demeure stable et gérable, sans exploser ni se dégrader.
Cela peut indiquer que le système efficace idéal accepte un certain taux d'erreur, mais qu'une branche finale « verte » reste nécessaire, dans laquelle un agent prend régulièrement des instantanés et effectue une rapide passe de correction avant la publication.
Surcoût de synchronisation
Il arrive que plusieurs agents modifient le même fichier ou refactorisent le même code. Au lieu d’essayer d’éliminer complètement ces situations ou de concevoir une solution excessivement complexe, nous acceptons certains moments de turbulence et laissons le système converger et se stabiliser naturellement en peu de temps.
Cela consomme quelques tokens supplémentaires et crée une contention locale, mais rend le système 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 s’exécutait sur sa propre machine puissante, avec d’importantes ressources système, afin d’éviter une complexité prématurée liée aux systèmes distribués. C’était un bon choix, car la plupart des exécutions atteignaient un pic de plusieurs centaines d’agents, ce qui saturait généralement ces machines sans pour autant les pousser au-delà de leurs capacités. Cette architecture facilitait l’observation des métriques système, ainsi que le partage et la duplication de l’état lorsque nécessaire.
Après avoir limité l’utilisation de la RAM par les agents, le disque est devenu le principal goulot d’étranglement. En particulier dans un projet monolithique, des centaines d’agents compilant simultanément entraînaient des lectures et écritures de plusieurs Go/s d’artefacts de build. Cela a eu un impact significatif sur le débit global du harness, avec un enseignement intéressant à la clé : la structure du projet, les choix d’architecture et l’expérience développeur peuvent affecter le débit des tokens et des commits, simplement parce que le travail sur la base de code (par ex. la compilation) prend plus de temps qu’idéalement la réflexion et le codage.
Il y avait aussi des contraintes et des inefficacités dans l’environnement de développement général : des choses qui ont du sens ou sont négligeables dans un workspace utilisé par une seule personne peuvent devenir très visibles lorsque des centaines d’agents font la même chose sur une seule machine. Une solution simple consiste à donner à chaque agent sa propre machine. Mais il existe aussi des pistes intéressantes, faciles à exploiter, pour obtenir de gros gains d’efficacité simplement en repensant et en redesignant certaines de ces primitives et de ces outils.
Par exemple, de nombreux outils comme Git et Cargo utilisent des verrous partagés, essentiellement comme mécanisme simple de contrôle de concurrence. L’intégration de mécanismes éprouvés issus des systèmes concurrents, comme les bases de données, pourrait-elle leur permettre de fonctionner tout aussi bien dans des systèmes multi-agents ? Tous les agents ont leur propre copie du dépôt, mais la plupart des fichiers et des artefacts sont identiques ; l’ajout de fonctionnalités simples de copy-on-write et de déduplication, qu’on retrouve dans des systèmes de stockage de production plus sophistiqués, pourrait-il apporter des gains similaires, faciles à obtenir, à un système généralement « mono-utilisateur » sans créer d’infrastructure distincte ?
Préciser l’intention aux agents
Les instructions données à ce système multi-agents étaient très importantes.
Au départ, nous n’en avions pas fait notre objectif principal, préférant viser un harness stable et efficace. Mais l’importance des instructions est vite devenue évidente. Nous interagissions essentiellement avec un agent de codage classique, sauf qu’il disposait d’ordres de grandeur supplémentaires en temps et en calcul. Cela amplifie tout, y compris les instructions imparfaites et peu claires.
Consacrer plus de temps aux instructions initiales est logique. Au final, les agents restent des agents : entraînés à suivre strictement vos instructions, à s’engager dans ces directions, à ne pas les modifier ni les contourner, même si elles sont mauvaises.
Nous voulions obtenir de bons résultats dans nos projets de recherche, nous avons donc modifié nos instructions initiales à mesure que le projet et le harness évoluaient. Nous apprenions à créer un navigateur tout en apprenant à faire fonctionner ce nouveau système multi-agents, et nous pouvions voir des spécifications médiocres ou insuffisamment précises se refléter dans la qualité des sorties, sans que cela soit dû au harness lui-même. Le harness ne faisait que suivre exactement nos instructions.
Quelques exemples tirés du projet de navigateur :
- Au départ, les instructions mettaient l’accent sur l’implémentation des spécifications et la correction des bugs. Des instructions comme
spec implementationétaient suffisamment vagues pour amener les agents à s’enfoncer dans des fonctionnalités obscures et rarement utilisées, au lieu d’établir intelligemment des priorités. - Nous supposions implicitement qu’il y avait des attentes en matière de performance, dans des limites acceptables pour l’utilisateur. Mais il a fallu des instructions explicites et des timeouts imposés pour forcer les agents à équilibrer les performances avec les autres objectifs.
- Pour les parties complexes du système, les agents peuvent écrire du code qui provoque des fuites de mémoire ou des interblocages. Les humains le remarqueraient, mais ce n’était pas toujours évident pour les agents. Des outils explicites de gestion des ressources fondés sur les processus étaient requis pour permettre au système de se rétablir proprement et d’adopter un comportement plus défensif.
Notre première version du navigateur simple sans JavaScript a convergé vers une architecture impropre à évoluer en navigateur complet. C’était un échec de la spécification initiale.
De même, bien qu’il ait été indiqué aux agents que le projet consistait à créer un navigateur from scratch, ils ont quand même ajouté certaines dépendances qu’ils auraient pu implémenter eux-mêmes, ou utiliser comme échafaudage temporaire pendant que la bonne implémentation était en cours. C’était une omission dans les instructions. Une exécution ultérieure a explicitement défini la philosophie en matière de dépendances et les bibliothèques à ne pas utiliser, ce qui a corrigé cela.
Cette exécution ultérieure a également opéré une restructuration majeure en de nombreuses crates autonomes, s’éloignant d’un monolithe. Le dépôt était dans un état très dégradé, mais le système multi-agents a convergé vers un code fonctionnel en quelques jours. Cela a montré que le système avait une forte capacité à travailler de manière collaborative et intelligente, même dans des états totalement cassés, au lieu de se dégrader davantage ou de rester bloqué. Cette exécution a aussi passé bien moins de temps à attendre la compilation, avec un débit plusieurs fois supérieur à celui d’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 le bon équilibre entre des métriques trop étroites et une liberté non structurée était délicat, tout comme savoir ce qui allait de soi et ce qui nécessitait une mention explicite.
Tout cela montre l’importance de faire émerger, de préciser et de comprendre l’intention, ce qui devient encore plus important à cette échelle. Le contrôle et l’observabilité seront des axes de recherche intéressants à continuer d’explorer.
Optimiser les requêtes
La formulation des requêtes a joué un rôle important dans le processus d'évolution.
Nous avons constaté qu'il valait mieux ne pas donner d'instructions pour ce que le modèle sait déjà faire, mais seulement pour ce qu'il ne connaît pas (par ex. la collaboration multi-agent) ou ce qui est spécifique au domaine concerné (par ex. comment exécuter les tests, votre pipeline de déploiement). Traitez le modèle comme une brillante nouvelle recrue qui maîtrise l'ingénierie, mais ne connaît pas encore votre base de code ni vos processus.
Les contraintes sont plus efficaces que les instructions. « Pas de TODO, pas d'implémentations partielles » fonctionne mieux que « n'oubliez pas de terminer les implémentations ». Les modèles font généralement ce qu'il faut par défaut. Les contraintes servent à en définir les limites.
Évitez une logique de case à cocher pour les tâches plus complexes ou de plus haut niveau. Donnez des indications détaillées sur votre intention, mais gardez à l'esprit qu'énumérer des choses précises à faire pousse le modèle à se concentrer dessus plutôt que sur le périmètre global. Cela relègue aussi implicitement au second plan ce qui n'est pas listé. En général, il vaut mieux laisser le modèle faire appel à son jugement et à son autonomie.
Nous avons toutefois constaté qu'il était utile de donner des chiffres concrets et des fourchettes lorsqu'il est question de l'ampleur du périmètre. Des instructions comme « générer de nombreuses tâches » tendent à produire un petit nombre de tâches : valeur prudente par défaut, approche conservatrice, tout en respectant techniquement l'instruction. « Générer 20 à 100 tâches » indique que l'intention porte sur un périmètre plus large, qu'il faut voir grand, et nous avons observé un comportement global très différent.
Enseignements sur la conception du système
Nous avons tiré de nos recherches quelques principes :
- Le système doit être antifragile. À mesure que nous augmentons le nombre d’agents exécutés simultanément, la probabilité de défaillance augmente elle aussi. Notre système doit pouvoir résister à la défaillance d’agents individuels, afin de permettre aux autres de se rétablir ou d’essayer d’autres approches.
- Privilégier l’empirisme aux hypothèses. Nous voulions nous appuyer sur les données et l’observation pour ajuster le système, plutôt que de partir d’hypothèses sur son fonctionnement inspirées d’organisations humaines ou de conceptions de systèmes existantes.
- Concevoir explicitement pour le débit. Cela impliquait d’accepter des compromis sur d’autres aspects du codage, par exemple tolérer un taux d’erreurs faible mais stable nécessitant une passe finale de réconciliation, plutôt que d’exiger un code parfaitement fonctionnel dans 100 % des cas, ce qui aurait considérablement ralenti le système.
Ces systèmes tendent à être d’une simplicité élégante lorsqu’ils sont bien conçus, mais il n’était pas clair quelle approche simple fonctionnerait avant d’en avoir exploré de nombreuses. La conception actuelle du système fonctionne avec une surcharge minimale et offre une mise à l’échelle linéaire utile du débit de tokens. Aucune autre itération majeure n’a été nécessaire sur le harness.
Conclusion
Si les humains apportaient le goût, le jugement et l’orientation, l’IA a été un puissant levier pour itérer rapidement et explorer cette recherche.
Cela ressemble quelque peu à la boucle « vertueuse » de l’IA, où l’IA est utilisée pour développer l’IA, et à mesure que les modèles, les agents et les harnesses s’améliorent, la boucle s’auto-entretient et s’accélère de plus en plus. Nous façonnons les outils qui nous façonnent.
Cette recherche présente une ressemblance poétique avec 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, après tout, de la bonne façon de structurer les projets logiciels.
Nous poursuivrons nos recherches sur des agents de très longue durée, et nos résultats éclaireront l’avenir de notre produit.