Ce qui se passe
Le Copilot agent runtime — le moteur derrière la CLI Copilot, l'app GitHub Copilot, le SDK, et aussi VS Code, Visual Studio, Copilot Code Review, Copilot Cowork, Copilot Studio, Excel, Outlook, Word — était écrit en TypeScript sur Node.js. Il ne l'est plus.
Entre le 12 mai et le 21 août 2026, il a été réécrit en Rust, en 128 pull requests, par essentiellement une seule personne, les agents écrivant la majeure partie du code.
Les volumes
L'estimation initiale, en mai, donnait ~130 000 lignes de TypeScript — exacte au moment de la mesure, et profondément trompeuse : du code remontait de la couche TUI vers le runtime pendant le port, et des dizaines de développeurs continuaient de fusionner des centaines de PR par semaine. Au total, ~430 000 lignes de TypeScript de production sont passées par le port.
| À l'arrivée | Lignes |
|---|---|
| Rust de production | 832 378 |
| Tests unitaires Rust | 468 689 |
| Tests E2E restés en TypeScript | 174 675 |
| Tests E2E du dépôt SDK | ~130 000 |
La méthode, qui est le vrai sujet
Deux approches existaient : le grand soir (branche parallèle, bascule unique) ou le portage en place. GitHub a choisi le portage en place avec remplacement atomique : chaque PR remplace une implémentation TypeScript par une fine couche qui appelle le Rust — via napi-rs — et supprime l'ancien code dans le même changement. L'ordre allait des feuilles vers le centre : helpers purs, utilitaires shell, fichiers de session, puis les sous-systèmes à état, les outils, MCP. L'orchestration de session, la plus couplée, est venue en dernier.
Ce qui rend l'opération possible tient en une phrase : toute la suite de tests de bout en bout, CLI et SDK, tournait contre le nouveau code Rust à chaque étape. Une PR qui cassait un test requis n'était pas fusionnée.
Sur les 14,5 semaines, main a livré 135 versions (100 pré-versions,
35 stables), soit ~1,3 par jour. Les pré-versions ne pesaient que 10,5 % des
téléchargements npm : l'exposition restait faible pendant qu'on surveillait les
retours.
Ce que ça a rapporté
Mesuré via le SDK C#, contre un serveur de complétion déterministe en local — donc sans latence de modèle ni de réseau.
| Scénario | 12 mai | 21 août, hors processus | 21 août, en processus |
|---|---|---|---|
| Client + session + un tour | 5,25 s | 1,33 s (4,0×) | 292 ms (18,0×) |
| Reprise d'une session de 32 tours | 5,64 s | 1,52 s (3,7×) | 264 ms (21,4×) |
| Dix cycles de clients concurrents | 12,34 s | 4,18 s (3,0×) | 742 ms (16,6×) |
| 1 000 cycles de session à un tour | 132,52 s | 22,53 s (5,9×) | 20,93 s (6,3×) |
Sur le test de charge à 100 pipelines concurrents : 7,55 cycles par seconde avant, 57,45 en Rust hors processus, 120,0 en processus. Le CPU agrégé passe de 312 s à ~110 s, et la mémoire résidente ajoutée par dix clients de 1 383 Mo à 247 Mo hors processus et 126 Mo en processus, soit −91 %.
Les régressions, et ce qu'elles disent
Au 14 septembre, des dizaines de régressions connues, toutes corrigées. Aucune n'est spécifique à Rust ; elles viennent de ce que JavaScript laissait implicite :
- Types flous. TypeScript n'a qu'un
number. Des entiers devenusf64sérialisaient42.0, que les SDK Go et C# refusaient ; à l'inverse,timeToFirstTokenMsdéclaréi64recevait5446.712845. Plus fin :event.error || "Unknown error"devenu.unwrap_or("Unknown error")— le||de JavaScript remplace la chaîne vide,unwrap_orla conserve. - Comportements ambiants.
toLocaleDateStringhérite du fuseau de l'hôte, qu'il fallait passer explicitement ; etIntl.DateTimeFormat().resolvedOptions().timeZone, typéstring, peut renvoyerundefined, que napi ne savait pas convertir. - Blocage du thread principal.
/chronicle reindexparsait des centaines de fichiers de session de façon synchrone à travers napi et gelait l'interface près d'une minute. D'où une règle permanente : un export napi qui fait du travail réel doit être asynchrone.
Sur la sûreté : 158 blocs unsafe dans 36 fichiers, tous aux frontières
externes — ABI C, API Windows, POSIX, SQLite, dlopen. Aucune régression
connue n'en impliquait un.
Ce que ça a coûté
~136,3 milliards de tokens — dont ~130,6 Md de lecture de cache et ~600 M de
sortie. Facture : ~120 000 $. En chemin, 5 116 compactions de contexte
automatiques, dont 647 pour la seule PR d'infrastructure de sessions. Et
4 478 cargo check directs, dont 87,1 % propres — le résultat d'éditions
par petits incréments plutôt que d'une écriture en bloc.
À retenir
La leçon que l'auteur tient à corriger : « si ça compile, c'est correct » est
faux, et sa propre liste de régressions le prouve — toutes ont compilé. Le
compilateur ne sait pas qu'un identifiant de dépôt doit être sérialisé en
entier, ni qu'un rebase a silencieusement retiré une garde et son test.
Ce qui a rendu ce portage faisable n'est ni Rust ni l'agent : c'est une suite de tests E2E assez complète pour être rejouée à chaque étape. Avant d'envisager la même chose, c'est cette couverture qu'il faut mesurer.