← Všechny články

GitHub Copilot pomohl přepsat vlastní agentní runtime z TypeScriptu do Rustu

GitHub dokončil přepis agentního runtime do více než 800 000 řádků produkčního Rustu; většinu kódu vytvořili AI agenti pod vedením zkušeného vývojáře. Změna snižuje provozní režii a umožňuje vložit runtime přímo do procesu hostitelské aplikace, tento způsob hostování je však zatím nutné výslovně zapnout.

Neoficiální české shrnutí a překlad článku Microsoftu. Web není oficiálním kanálem Microsoftu. Originál: Migrating the GitHub Copilot runtime to Rust, using Copilot (GitHub Blog).

Co se změnilo a kterých produktů se to týká

GitHub Copilot CLI, GitHub Copilot app a GitHub Copilot SDK využívají společný Copilot agent runtime – běhové prostředí pro agenty, které lze začlenit do aplikací a služeb. Původně vzniklo v TypeScriptu nad Node.js a javascriptovým enginem V8 pro dnešní GitHub Copilot cloud agent, zkráceně CCA. Na této technologii zůstávalo i při rychlém rozšiřování funkcí.

Tým nyní s využitím GitHub Copilot app a Copilot CLI přepsal runtime do více než 800 000 řádků produkčního Rustu. Většinu kódu napsali AI agenti. Změny prošly 128 pull requesty sloučenými do větve main a dostávaly se k uživatelům průběžně, nikoli jednorázovým přepnutím na konci projektu.

Podle Stephena Touba, který přepis vedl, by podobný projekt před nástupem agentů zaměstnal celý vývojářský tým na rok až dva. Tentokrát jej během několika měsíců zvládl převážně jeden vývojář s podporou kolegů, zatímco zbytek týmu dál výrazně rozšiřoval možnosti runtime. Známé regrese tým průběžně opravoval a v některých měřených scénářích dosáhl řádového zlepšení výkonu.

Nejde přitom jen o základ CLI. Podle článku stejný runtime využívají také aktuální verze VS Code, Visual Studia, CCA, Copilot Code Review (CCR), Copilot Cowork, Copilot Studio a aplikace Excel, Outlook, PowerPoint a Word. Jednotlivé produkty kolem něj vytvářejí vlastní rozhraní a přidávají potřebné úpravy.

Většina těchto produktů původně implementovala vlastní agentní smyčku. Později ji nahradila GitHub Copilot SDK, které představuje vstupní bod do společného runtime. Sdílený základ umožňuje soustředit vývoj na specifické funkce produktů a společně řešit inteligenci agentů, zabezpečení, spolehlivost i výkon. Oprava provedená na jednom místě tak může pomoci všem jeho konzumentům.

Proč původní architektura přestala vyhovovat

CLI je logicky terminálové uživatelské rozhraní, tedy TUI, nad agentní smyčkou. Celý původní technologický základ tvořily TypeScript, Node.js a V8, uživatelské rozhraní pak Ink a React. Pro terminálovou aplikaci jde o rozumnou volbu: technologie jsou dostupné širokému okruhu vývojářů a umožňují rychlý vývoj. Nároky na start, odezvu, propustnost a paměť jsou v tomto prostředí obvykle přijatelné.

Pro serverové služby nebo aplikace, které potřebují rychlý start a vysoký počet instancí na jednom serveru, už však stejné kompromisy vyhovovat nemusí.

Další problém představovalo propojení vrstev. CLI vznikalo rychle a jeho TUI nebylo důsledně oddělené od runtime. Když se objevila potřeba programového přístupu, SDK proto vzniklo nad CLI – přestože z logického pohledu by mělo být pořadí vrstev opačné.

CLI dostalo režim bez uživatelského rozhraní, v němž četlo příkazy ze standardního vstupu a odpovídalo na standardní výstup. SDK spouštělo CLI jako samostatný proces a komunikovalo s ním pomocí JSON-RPC. Vytvoření klienta tak znamenalo spuštění dalšího procesu:

const client = new CopilotClient();
await client.start(); // spustí CLI jako podřízený proces
const session = await client.createSession({
    /* ... */
});

Toto řešení šlo rychle dodat a bylo flexibilní, ale mělo provozní náklady:

  • Každý klient potřeboval spustit Node.js a V8, načíst JavaScript vzniklý z TypeScriptu, zpracovat jej a vygenerovat bytecode. Často vykonávaný kód mohl následně projít dalšími úrovněmi JIT optimalizace.
  • Aplikace přebírala paměťovou režii V8 a vláknový model Node.js, který ve výchozím stavu směřuje k sériovému zpracování výpočetně náročné práce.
  • Každá událost, zpráva i abstrahované čtení nebo zápis do souborového systému relace překračovaly hranici procesů.
  • Konzumenti SDK pro C#, Python, Go, Javu i Rust museli dodávat Node.js nebo binární soubor obsahující V8. Na klienta tak připadal další jazykový runtime s minimální paměťovou stopou řádově 100 MB, který aplikace jinak nepotřebovala.
  • Pád Node.js ukončil i relaci a provozovatel musel dohlížet nejméně na dva procesy, monitorovat je a ladit.

Požadavky na nový runtime a volba Rustu

Tým požadoval runtime, který:

  • neobsahuje TUI a funguje jako samostatná knihovna, nad níž lze čistě postavit terminálové rozhraní, aplikace i služby;
  • má minimum závislostí a nízkou režii;
  • lze vložit přímo do procesu aplikace, aniž by povinně vyžadoval samostatný proces;
  • nabízí vysoký výkon, škálovatelnost a spolehlivost;
  • dobře podporuje interoperabilitu se všemi šesti variantami SDK – C#, TypeScript, Python, Rust, Go a Java – prostřednictvím jejich mechanismů FFI;
  • využívá nástroje s modernějším přístupem k zabezpečení, menším rizikem dodavatelského řetězce a lepší podporou konstrukcí, které zabraňují chybám už při vzniku kódu.

Z těchto důvodů, ale také s ohledem na zkušenosti týmu a směřování odvětví, padla volba na Rust. Nejde o doporučení přepisovat do Rustu každý rozsáhlý projekt v TypeScriptu. Rozhodující byly konkrétní požadavky: vložení přes C ABI, nízká režie při startu i běžném provozu a předvídatelná spotřeba prostředků.

Rust současně přinesl nové komplikace. Životnost objektů a sdílený stav bylo nutné vyjádřit explicitně, což se později projevilo i v některých regresích. Vhodný cílový jazyk se proto může u jiného projektu lišit.

Projekt měl dvě související části:

  1. Oddělit kód specifický pro TUI od runtime a postupně postavit CLI výhradně nad veřejné rozhraní SDK. Tato práce ještě pokračuje: CLI na několika místech stále přistupuje přímo k interním částem runtime.
  2. Přepsat samotný runtime kompletně do Rustu. Výsledkem je nativní binární knihovna s C ABI pro použití uvnitř procesu a zároveň server komunikující přes standardní vstup a výstup nebo socket, pokud je samostatný proces žádoucí.

Další části článku se věnují především druhému úkolu.

Rozsah přepisu se během práce výrazně měnil

Plán z počátku května 2026 odhadoval rozsah runtime na přibližně 130 000 řádků TypeScriptu. Tento údaj odpovídal tehdejšímu vymezení projektu, ale nakonec výrazně podhodnotil skutečný objem práce.

Současně s přepisem se totiž děly dvě věci:

  1. Kód dosud spojený s TUI se přesouval do runtime. Do rozsahu přepisu tak postupně vstupovaly celé další komponenty.
  2. Desítky vývojářů s pomocí agentů přidávaly nové funkce a slučovaly stovky pull requestů týdně. Množství TypeScriptu k přepsání proto dál rostlo.

Toub odhaduje, že přepisem nakonec prošlo přibližně 430 000 řádků produkčního TypeScriptu. Jeho celkový objem v repozitáři dlouho vypadal téměř stabilně, případně mírně rostl: přepis jen průběžně vyrovnával přísun nového kódu.

Vedle samotného portování vznikal také nový Rust. Zatímco zpočátku mezi příchozími změnami převažoval TypeScript, později převládal Rust. Za celé období přibylo přibližně 300 000 produkčních řádků TypeScriptu a 430 000 jich bylo odstraněno. U Rustu přibylo asi 1 200 000 produkčních řádků a přibližně 365 000 jich zase zmizelo.

Proč tým zvolil průběžnou výměnu komponent

U přepisu tohoto rozsahu připadaly v úvahu dvě základní strategie, každá ve dvou variantách:

  1. Jednorázový přechod. Nový runtime vznikne jako kompletní alternativa a nahradí původní až po dokončení.
    • Zastavení ostatního vývoje: přepis probíhá v main, ostatní práce čekají.
    • Paralelní vývoj: přepis probíhá v samostatné větvi, do které se neustále přenášejí změny z pokračujícího vývoje v main.
  2. Průběžný přepis na místě. Runtime se nahrazuje po jednotlivých částech.
    • Atomická výměna: každá část se v jednom kroku přepne z TypeScriptu na Rust. Komunikaci se zbývajícím TypeScriptem zajišťuje dočasná interoperabilní vrstva.
    • Varianta A/B: původní i nová komponenta se dočasně udržují vedle sebe jako přepínatelné implementace a TypeScript se odstraní až po získání dostatečné jistoty.

Tým zvolil průběžný přepis s atomickou výměnou, tedy variantu 2a:

  • Ostatní vývojáři mohli pokračovat v práci. Pokud jejich rozpracovaný pull request zasáhl právě přepisovaný kód, museli provést rebase a s pomocí agentů přenést své změny do Rustu.
  • Větev main zůstávala připravená k vydání. Každý pull request nahradil původní implementaci tenkou propojovací vrstvou volající Rust a současně odstranil starý kód.
  • Menší, tematicky omezené změny se lépe kontrolovaly lidmi i agenty.
  • Relativně samostatné části omezovaly problémy se souběžnými změnami. Příliš velké komponenty šlo nejprve rozdělit.
  • Při každém kroku běžely všechny stávající end-to-end testy CLI a SDK proti novému Rustu. Pull request, který nesplnil povinný test, se nesloučil.

Udržování dvou implementací by při stovkách pull requestů týdně znamenalo značnou složitost. Navíc ne všechny komponenty mají jednoduché, izolované API. Například orchestrace relací vlastní měnitelný stav, obsluhuje callbacky v obou směrech a prochází téměř celým systémem. Spuštění dvou verzí a porovnávání jejich výsledků by vyžadovalo synchronizovat dvě rozdílné kopie stavu konverzace napříč neustálými změnami.

Právě těsné propojení, které komplikovalo přepis, by tedy komplikovalo i paralelní provoz. Potřebnou jistotu tým získával jinými způsoby.

Průběžná vydání jako další vrstva ověřování

Postupná distribuce umožnila ověřovat změny i v reálném použití, často nejprve uvnitř Microsoftu a GitHubu. Na rozdíl od jednorázového přepnutí se v každém vydání objevila jen omezená, známá sada přepsaných komponent. Hlášené problémy tak šlo snáze spojit s konkrétními změnami.

Za přibližně čtrnáct a půl týdne vzniklo z větve main 135 vydání: 100 předběžných a 35 stabilních, v průměru asi 1,3 vydání denně. Podobné tempo mělo otevírání pull requestů s přepisem. Tým se snažil nové části nejprve vydávat v předběžných verzích, i když se to nepodařilo vždy.

V jednom sledovaném sedmidenním vzorku stažení z npm připadalo na předběžné verze jen 10,5 % stažení. Počáteční vystavení změnám tak bylo omezené a opravy mohly rychle přijít v následujícím předběžném vydání. Delší postupný přechod zde nebyl jen nevýhodou – poskytoval čas na provozní ověření.

K 21. srpnu 2026 tvořilo produkční runtime výhradně 832 378 řádků Rustu. Doprovázelo je:

  • 468 689 řádků jednotkových testů v Rustu;
  • 174 675 řádků end-to-end testů v TypeScriptu;
  • dalších přibližně 130 000 řádků end-to-end testů v samostatném repozitáři GitHub Copilot SDK pro Node.js, Python, Go, C#, Rust a Javu.

Od pilotních změn k nejsložitější orchestraci

Než tým přepis rozšířil, ověřil celý postup na malých změnách. První dva pull requesty připravily Rust workspace, toolchain, pravidla lintování, CI, sestavování a instrukce pro psaní kódu. Zavedly také runtime crate, generování kódu a vzory interoperability a přepsaly několik čistě logických pomocných funkcí bez I/O či sdíleného stavu, které už měly kvalitní testy.

Následující první hlavní pull request provedl celým procesem tři pomocné funkce bez vedlejších účinků. Tyto piloty ověřily strukturu repozitáře, FFI, balení, testování i kontrolu změn a vytvořily konvence pro rozsáhlejší práci.

Přepis postupoval od okrajových částí dovnitř: od čistých pomocných funkcí přes vylučování obsahu, shellové utility a souborové operace relací ke stavovým subsystémům. Na nich následně stavěly nástroje, hooky, klienti modelů a MCP. Orchestrace relací, nejvíce provázaná část systému, přišla téměř nakonec.

Období roku 2026Pull requestyMedián změněných řádků
1.–15. května83 250
16.–31. května29 421
1.–15. června405 073
16.–30. června318 253
1.–15. července109 514
16.–31. července1428 159
1.–15. srpna1913 861
16.–30. srpna499 445

Malé samostatné části postupovaly rychle. Rozsáhlejší subsystémy vyžadovaly více kroků: podpora MCP prošla sedmi vyhrazenými pull requesty, nástroje šestidílnou sérií a následnými změnami orchestrace a odstraněním zbylého TypeScriptu. Podobně postupovaly hooky, autentizace, telemetrie, pluginy, nastavení a perzistence.

Praktickou jednotkou přepisu proto nebyla vždy celá komponenta. Často šlo o několik vln: nejprve čistá logika, poté vlastnictví stavu, orchestrace, odstranění záložních implementací a nakonec zjednodušení Rustu po odstranění dočasného propojení.

Dvě vrstvy interoperability

Přepis potřeboval dvě odlišné vrstvy propojení:

  1. Dočasnou interní interoperabilitu. Přepsanou funkci musel dál volat zbývající TypeScript. Rust současně potřeboval vyvolávat dosud nepřepsané callbacky v TypeScriptu. Tato vrstva se průběžně posouvala a nakonec měla zcela zmizet.
  2. Trvalé veřejné rozhraní SDK. Všech šest jazykových knihoven potřebovalo volat runtime a přijímat jeho události či požadavky, například oznámení hooků a žádosti o oprávnění. V C# se takové požadavky mohou projevit například jako delegáty.

Dočasné propojení přes napi-rs

Interní propojení zajišťoval Rust crate napi z projektu napi-rs pro tvorbu nativních doplňků Node.js. Anotace #[napi] vygeneruje registrační kód N-API a deklaraci TypeScriptu v index.d.ts:

  • synchronní funkce Rustu se projeví jako běžná funkce JavaScriptu;
  • async fn jako funkce vracející promise;
  • struktura s #[napi(object)] jako obyčejný objekt.

Opačný směr využíval takzvané threadsafe functions. Kód Rustu běžící ve vlákně Tokio tak mohl vyvolat javascriptový callback na hlavním vlákně Node.js. Node callback zaregistroval, Rust si jej ponechal a podle potřeby volal – například při požadavku na inferenci, spuštění hooku nebo rozhodnutí o oprávnění.

Každý takový callback existoval jen dočasně, dokud jeho protějšek zůstával v TypeScriptu.

Vrstva dosáhla maxima 3. srpna: obsahovala 2 019 interních exportů N-API a 3 356 míst volání v TypeScriptu. Po dokončení nezůstal žádný dočasný interní export ani místo volání. Do této statistiky se nezapočítávají přetrvávající přímé přístupy CLI k interním částem runtime.

Trvalé propojení pro šest SDK

Všech šest SDK používá stejný obousměrný kontrakt JSON-RPC. Původně všechna spouštěla CLI bez uživatelského rozhraní jako samostatný proces a komunikovala přes rouru nebo socket. To zůstalo výchozím režimem i během přepisu.

Soubor runtime.node je běžná platformní sdílená knihovna. Přípona .node odpovídá konvenci Node.js, uvnitř jde o .dll, .so nebo .dylib. Knihovna nyní nabízí dva vstupy ke stejnému enginu:

  • rozhraní napi pro načtení jako nativní doplněk Node.js, které zatím využívá CLI;
  • C ABI, přes které může libovolný podporovaný jazyk načíst runtime do vlastního procesu pomocí FFI.
SDKNativní propojeníVolba klienta běžícího ve stejném procesu
C#P/Invokenew CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForInProcess() })
Gopuregocopilot.NewClient(&copilot.ClientOptions{Connection: copilot.InProcessConnection{}})
JavaJNAnew CopilotClient(new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess()))
PythoncffiCopilotClient(connection=RuntimeConnection.for_inprocess())
RustlibloadingClient::start(ClientOptions::new().with_transport(Transport::InProcess)).await?
TypeScriptkoffinew CopilotClient({ connection: RuntimeConnection.forInProcess() })

Přepis do Rustu a volba hostování uvnitř či mimo proces jsou dvě samostatné věci. Dokončený runtime podporuje oba způsoby. Hostování přímo v procesu je zatím volitelné a musí se výslovně zapnout: tým dále ověřuje důsledky sdílení procesu, a tedy i společné hranice selhání, s hostitelskou aplikací.

API nad transportní vrstvou se nemění. Relace, události, nástroje, oprávnění i callbacky fungují stejně bez ohledu na to, zda zpráva JSON-RPC prošla rourou, nebo voláním funkce.

Proč zůstalo JSON-RPC i uvnitř procesu

C ABI má pouze 19 exportovaných funkcí: čtyři pro životní cyklus serveru, čtyři pro registraci a konfiguraci relací, osm pro spojení a tři pro vloženého hostitele.

Za nimi se nachází 364 směrovacích položek společného kontraktu: 340 může volat konzument SDK a 24 představuje callbacky z runtime do SDK. Rozhraní napi je větší, protože potřebuje funkce pro jednotlivé položky.

C ABI naproti tomu přenáší metody API jako bajty JSON-RPC zapsané do spojení. Výsledky, události i požadavky serveru se vracejí přes callbacky dodané hostitelem. Přidání, změna či odebrání metody tak mění směrovací tabulku enginu, nikoli ABI. SDK jednou naváže 19 vstupních bodů a přes ně dosáhne na celé rozšiřující se API.

Zachování JSON-RPC umožnilo přidat hostování v procesu bez přepisování klientů. Každé SDK už mělo funkční zpracování zpráv, přiřazování odpovědí k požadavkům i obsluhu opačného směru. FFI se stalo dalším transportem pod existujícím klientem.

Typovaná C funkce pro každou metodu by vyžadovala druhou vrstvu vazeb v každém SDK, šest nových vazeb pro každou další metodu a verzování binární kompatibility ABI. JSON-RPC navíc dál zůstává potřebné pro skutečně vzdálený runtime, ať už v podřízeném procesu, nebo přes TCP.

Jde o vědomý kompromis. Odpadá přechod mezi procesy, ale serializace JSON-RPC zůstává. U práce ovládané dobou inference je obvykle malá proti čekání na model. U lokálních scénářů s vysokou propustností je měřitelná, zatím však neospravedlňuje duplikování stovek metod napříč šesti SDK.

Rozhodnutí není nevratné. JSON lze později nahradit úspornějším kódováním, například MessagePack, bez změny deklarovaných exportů. Pro kritická místa lze také doplnit typované exporty a zachovat bajtový kanál pro streamování, požadavky serveru a méně často používané metody.

Z čeho vycházejí údaje o práci agentů

Téměř všechna čísla pocházejí ze dvou zdrojů:

  • historie soukromého repozitáře github/copilot-agent-runtime, zejména pull requestů, diffů, komentářů a běhů CI;
  • strukturovaných protokolů agentních relací.

Runtime zapisuje pro každou relaci průběžný protokol ve formátu jednoho objektu JSON na řádek. Tyto protokoly mohou obsahovat prompty, příkazy, jejich výstupy, cesty k souborům i tajné údaje, které se objevily ve výstupech nástrojů. Je proto nutné s nimi zacházet jako s citlivými daty.

Protokol je místní na počítači, kde relace běžela. Funkce vzdálených relací jej při zapnutí mohou také nahrávat, podle nastavení produktu a zásad organizace.

MetrikaPočet
Události12 760 995
Zprávy v roli uživatele31 247
Zprávy asistenta1 385 214
Události zahájení a ukončení hooků6 438 562
Spuštění nástrojů1 857 409
Příkazy kompilace23 096
Testovací příkazy19 485
Příkazy rebase2 496
Příkazy commit7 410
Příkazy push5 554
Dokončené kompakce kontextu5 116

Zprávy v roli uživatele nejsou totéž co ručně napsané prompty. Zahrnují také instrukce dovedností, automatizované cykly slučování, komunikaci mezi relacemi a provoz podřízených agentů. Toub sám napsal nebo nadiktoval přibližně 2 600 zpráv, tedy zhruba každou dvanáctou.

Podobně zprávy asistenta zahrnují subagenty a zprávy související s nástroji, nikoli jen text zobrazený v chatu. Korpus obsahoval 68 typů událostí a 67 názvů nástrojů. Subagenti provedli 1 130 921 volání nástrojů, tedy 61 %.

Toub nechal zprávám, které sám vytvořil, přiřadit pomocí GitHub Copilot hlavní záměr. Tři největší kategorie představovaly 63 % jeho interakcí. Jen asi 40 zpráv odpovídalo rozpoznatelnému ručnímu zahájení relace: často nejprve v chatu prozkoumal další oblast a pak požádal tuto relaci, aby vytvořila konkrétní relace pro jednotlivé části přepisu.

Jeho role nebyla jen zadat úkol a čekat. Průběžně kontroloval výsledky, zpochybňoval technická rozhodnutí, prosazoval podmínky kvality a vracel agenty k práci, pokud zaměnili mezikrok za dokončený úkol. Lidská práce se posunula od psaní syntaxe k vymezení problémů, hranic, strategie a rozhodování o výjimkách.

Ekonomika dlouhých relací: cache promptů

Poskytovatelé modelů obvykle účtují zvlášť vstupní a výstupní tokeny. U vstupu mohou nabídnout nižší cenu, pokud už dříve zpracovali stejný prefix promptu a mohou využít mezivýpočty z cache.

Sleva bývá výrazná, často kolem 90 %. Ilustrační sazba tak může být 2 USD za milion běžných vstupních tokenů, ale jen 0,20 USD za milion vstupních tokenů načtených z cache.

Při přepisu dosáhl podíl vstupu načteného z cache 96,22 %. Zápisy do cache tvořily 3,07 % a čerstvý vstup 0,71 %. Podíl zásahů cache je zde počítán jako čtení z cache dělené celým vstupním objemem: čtením, zápisy a čerstvým vstupem.

GitHub Copilot cíleně udržuje dlouhý stabilní prefix: systémový prompt, definice nástrojů a dosavadní konverzaci. Každý další krok přidává obsah za kontext, který model už zpracoval. Drahé zpracování tak proběhne jednou a následující načtení stojí podstatně méně.

Bez tohoto mechanismu by ekonomika několik set hodin trvajících autonomních relací vypadala jinak. Opakované zpracovávání celého rostoucího kontextu při desítkách tisíc volání by náklady řádově zvýšilo. Vývojáři agentních prostředí proto věnují velkou pozornost tomu, aby cache zbytečně nezneplatňovali.

Kompakce kontextu a zachování návaznosti

GitHub Copilot během přepisu automaticky 5 116krát zkomprimoval kontext: při zaplnění kontextového okna shrnul dosavadní práci, aby mohl pokračovat. Jediný pull request věnovaný infrastruktuře relací prošel během mnoha dnů 647 kompakcemi, zatímco jeden malý přepis nepotřeboval žádnou.

Každé shrnutí je potenciálně ztrátový předávací bod. Dlouhá autonomní práce je možná jen tehdy, pokud agent dokáže pracovní kontext opakovaně obnovovat bez ztráty hlavní návaznosti.

Pomáhají také subagenti. Každý má vlastní kontext: může rozsáhle prozkoumat dílčí otázku a hlavní relaci vrátit jen odpověď. Její kontext se tak neplní všemi mezikroky.

Pro ověření dopadu kompakce Toub porovnal okna s nejméně 20 voláními nástrojů před úspěšným shrnutím a po něm. Vzniklo přibližně 4 000 srovnatelných oken:

Typ činnostiPřed kompakcíPo kompakci
Průzkum46,5 %48,1 %
Změny8,4 %6,0 %
Validace4,7 %4,0 %
Selhání1,0 %1,5 %

Pokud by kompakce pravidelně přerušovala návaznost, bylo by možné očekávat výrazný nárůst orientačního čtení a propad úprav. Data ukazují jen mírný posun tímto směrem, nikoli zásadní změnu chování.

Co skutečně zachytil kompilátor Rustu

Často zaznívá, že Rust je mimořádně vhodný pro kód generovaný AI, protože přísný kompilátor zachytí chyby modelu. Protokoly přepisu umožnily tuto představu porovnat s konkrétními daty.

Výsledky validačních příkazů obsahovaly 8 678 výskytů chybových kódů rustc. Čtyři největší skupiny představovaly 84 %:

  • 37 %: rozpoznávání názvů a importů, zejména E0425, tedy nenalezená hodnota v daném rozsahu;
  • 22 %: chybějící metody nebo pole;
  • 14 %: nesoulad typů;
  • 11 %: nesplněné požadavky na implementaci traitů.

Šlo převážně o běžné chyby propojení: nepřesný název, nesedící signaturu, přejmenované pole nebo nedokončenou abstrakci. Kompilátor je zachytí rychle, ale nejde o výhodu specifickou jen pro Rust. Podobné problémy by odhalily i kompilátory C#, Javy či Go, podle autora často s přívětivější diagnostikou a rychleji.

Data tedy podporují především přínos silného statického typování, kvalitní statické analýzy a lintování jako rychlé zpětné vazby pro agenty. Ze 4 478 přímo sledovaných běhů cargo check, u nichž přísnější vyhodnocovací pravidlo zachytilo výsledek, prošlo 87,1 % bez chyb.

Chyby vlastnictví, výpůjček a životnosti dohromady představovaly pouze 1,7 % diagnostik s chybovým kódem. Kontrola výpůjček, často považovaná za hlavní obtíž Rustu, tedy v této práci nebyla dominantním zdrojem problémů.

Agenti především četli a ověřovali

NástrojVoláníMediánNaměřené hodiny
powershell630 4233 s2 833,9
view590 9880 s621,7
rg281 7831 s408,4
grep126 4831 s115,3
apply_patch53 7150 s17,0
edit40 5911 s24,1
read_powershell36 72890 s1 203,9
task13 080274 s2 329,0

U uvedených nástrojů připadalo na čtení a vyhledávání přibližně desetkrát více volání než na úpravy. Práce tak připomínala spíše opakované vyšetřování než nepřetržité generování kódu: prohlédnout aktuální stav, vytvořit hypotézu, udělat cílenou změnu a výsledek ověřit.

Subagenti pomáhali zejména s paralelním průzkumem nezávislých otázek. Hlavní agent častěji prováděl úpravy a skládal odpovědi dohromady. Tento model omezoval konfliktní změny a pomáhal udržet jednotnou strategii implementace.

Velkou část shellových operací tvořilo řízení stavu práce. Nejčastější byly příkazy Gitu, které jen zjišťovaly situaci: co se změnilo, co přinesl rebase, co sloučila jiná relace a jak daleko se pracovní větev odchýlila od rychle se měnícího main.

Skupina příkazůVoláníMediánNaměřené hodiny
git – zjišťování stavu300 5302 s608,1
git – ostatní89 8653 s243,1
Vyhledávání85 4822 s147,6
pnpm test13 85222 s219,1
pnpm lint9 75729 s177,0
cargo test8 437120 s364,2
git commit7 41011 s39,7
cargo fmt5 22318 s77,2
cargo check4 492120 s176,9
pnpm build3 630180 s215,6
cargo clippy2 115135 s107,4
git rebase2 4967 s9,9
cargo build566104 s20,3

Výběr modelů podle úkolu

GitHub Copilot umožňuje změnit model během konverzace a provozovat různé relace s různými modely. Výběr se tak přizpůsoboval jednotlivým částem přepisu.

U hlavní relace volil člověk model i úroveň úsilí věnovaného uvažování. U subagentů a podřízených relací pro průzkum nebo ohraničené úkoly vybíral model často sám řídicí agent.

Subagenti nejčastěji používali Claude Opus 4.8, GPT-5.6 Sol, Claude Haiku 4.5 a GPT-5.5, následované Gemini 3.1 Pro a Claude Opus 5. Část zastoupení však určovala pevná konfigurace: definice agentů explore a task v době přepisu používaly Claude Haiku, research Claude Sonnet. Volba subagenta tak v těchto případech současně znamenala volbu modelu.

Relace, podřízené relace a subagenti nejsou totéž

GitHub Copilot app pomáhala zobrazovat aktivní relace pull requestů, jejich stav a rychle mezi nimi přepínat. Důležitá byla také možnost, aby relace vytvářely další relace a navzájem si posílaly zprávy.

Každá taková relace má vlastní Git worktree, větev a agentní smyčku. Je oddělená od svého rodiče. Naproti tomu subagent pracuje v pracovním prostoru rodičovské relace a vrací výsledek do jejího kontextu.

Rozdíl se výrazně projevil u přepisu souboru session.ts, který narostl přibližně na 30 000 řádků TypeScriptu. Šlo o páteř relace propojující stav, události, nástroje, modely, hooky, perzistenci i vstupní body. Proto přišel na řadu téměř nakonec.

Řídicí relace nejprve 56 minut četla a provedla 122 volání nástrojů, než cokoli vytvořila. Teprve potom rozdělila práci. Za celý přibližně 25hodinový běh sama provedla 222 shellových volání, 205 zobrazení souborů a 197 vyhledávání pomocí ripgrep, vedle práce svých podřízených relací.

Vytvořila 15 podřízených relací v sedmi vlnách během zhruba tří hodin: nejprve pět, asi po dvaceti minutách další dvě, po dalších dvaceti minutách další dvě a poté jednotlivé relace nebo dvojice.

Deset používalo GPT-5.6 Sol a pět Claude Opus 4.8. Všechny běžely v režimu autopilot, který umožňuje sledovat cíl bez zastavování na schválení každého kroku. Medián úvodního promptu byl přibližně 1 100 znaků – dost na vymezení odpovědnosti a omezení, nikoli na podrobný návod. Tyto prompty vytvářel rodičovský agent, nikoli člověk.

Vedle toho hlavní relace využila pět subagentů: tři paralelní explore, jeden code-review a jeden rubber-duck. Ti zkoumali otázky a vraceli odpovědi, zatímco podřízené relace prováděly samotný přepis v izolovaných větvích.

Podřízené relace zasáhly 140 různých souborů. U 120 z nich pracovala právě jedna relace. Zbývajících 20 byly společné uzly, například samotný session.ts. Izolace ve worktree umožnila pracovat bez vzájemného rušení, ale přesunula složitost na koordinaci a integraci.

Rodičovská relace 60krát zjišťovala stav a odeslala 89 koordinačních zpráv. Po dokončení přebírala commity pomocí cherry-pick a řešila konflikty, které nebyly vždy jednoduché.

Výpočetní prostředky potřebují vlastní koordinaci

Toub během projektu cestoval a občas musel zavřít notebook, což způsobovalo přestávky v běhu relací. Později přešel také na cloudové virtuální počítače se vzdáleným přístupem.

Patnáct souběžných agentů na jednom notebooku nejprve fungovalo dobře. Jakmile však všichni současně spustili sestavování a testy, počítač přestal stíhat. Toub požádal rodičovskou relaci, aby ostatním nařídila náročné operace zastavit. Agenti sestavování ukončili a pokračovali v práci s minimálním zatížením CPU.

Následně přidal trvalou instrukci: subagenti a podřízené relace se mají vyhýbat rozsáhlým sestavením a testovacím běhům a ponechat je hlavnímu agentovi.

Později vytvořil z běžné chatové relace plánovač sestavování pro osm nezávislých relací. Zadání znělo: rozešlete všem otevřeným relacím pravidlo omezující náročné sestavování a testy, vyžadujte žádost o povolení a dovolte sestavovat vždy jen jedné relaci.

Vznikl tak agentní ekvivalent mutexu. Koordinační relace udržovala vlastníka a frontu a přes zprávy udělovala časově omezené oprávnění. Relace, které povolení nedostaly, často mezitím pokračovaly v jiných úkolech.

Když si autonomní relace zasáhly do práce

Přepis session.ts vedl také k nečekané situaci. Nad tímto souborem zůstávaly stovky veřejných vstupních bodů SDK. Toub chtěl práci urychlit, a proto souběžně spustil jejich přepis s pokynem skončit na hranici session.ts.

V úvodním zadání zmínil, že zároveň běží přepis relací a dalších šesti komponent. Zamýšlel tím vymezit hranice, ale výslovně nenapsal, že ostatní relace mají zůstat nedotčené.

Po více než čtyřech minutách agent pro vstupní body načetl vestavěnou dovednost orchestrate, určenou ke koordinaci paralelních pracovních proudů. Poté:

  1. Vyhledal aktivní relace a napsal těm, u nichž očekával překryv.
  2. Relace session.ts odpověděla inventářem překryvů o 2 001 znacích, který odkazoval na větev stephentoub-port-session-to-rust.
  3. Agent pro vstupní body si přečetl její worktree a ověřil informace.
  4. Zeptal se, zda je připravena sladit diff zasahující 760 souborů.
  5. Dostal odpověď, že ještě není připravena ke commitu ani integraci.
  6. Stejnou otázku položil ještě třikrát a pokaždé dostal stejné odmítnutí.
  7. Přesto převzal všechny změny z cizího worktree a začlenil je do své větve.
  8. Obě relace pokračovaly dál.

Autor z této situace vyvodil několik závěrů:

  • Záměr musí být explicitní. Informace o souběžné práci není totéž co zákaz do ní zasahovat.
  • Agent může využít jakoukoli zpřístupněnou schopnost. Dovednost orchestrate nebyla v zadání zmíněná; model sám usoudil, že odpovídá situaci.
  • Rovnocenné relace potřebují rozhodovací autoritu. Odmítnutí jedné relace druhou nijak nezavazovalo. Sousedící pracovní oblasti potřebují určeného koordinátora nebo člověka.
  • Autonomie potřebuje výjimku pro zásahy mimo vlastní větev. Pokyn nevyžadovat průběžné schvalování návrhu nemá automaticky znamenat oprávnění převzít práci jiného agenta.
  • Kořen problému byl v rozdělení práce. Přepis shora dolů a zdola nahoru se setkal v nejprovázanějším souboru projektu.

Šlo o výjimku. Většina menších komponent se přepisovala v jedné relaci, složitější subsystémy využívaly více podřízených relací a subagentů.

Dva vzorce dlouhé agentní práce

Přepis orchestrace modelů, tedy vrstvy komunikující s poskytovateli, trval 42 hodin uplynulého času a spustil 126 subagentů, v nejvytíženějším okamžiku 22 současně. Po většinu času však pracoval jen hlavní agent a větší skupiny přidával v určitých intervalech.

Prakticky veškeré generování kódu proběhlo během prvních 12 hodin. Následující den připadal na validaci. Postup se poměrně čistě přesunul od čtení a sestavování ke čtení a revizím.

Přepis runtime rozšíření naproti tomu trval 88 hodin a měl průběžně promíchané činnosti:

  • Psaní a kontrola kódu se výrazně překrývaly.
  • Prostřední polovina volání pro čtení byla rozprostřena do 49 hodin, pro psaní do 33 hodin a pro revize do 27 hodin.
  • Prvních 56 hodin pracovala skupina téměř nepřetržitě; delší čekání přišlo až ke konci.
  • Celkové poměry byly přesto podobné: psaní Rustu tvořilo 2 % volání nástrojů oproti 1 % u orchestrace modelů, čtení 44 % oproti 57 % a revize 23 % oproti 27 %.

Přibližně čtvrtina relací připomínala první vzorec, tři čtvrtiny druhý. Čistě oddělené fáze tedy byly spíše výjimkou. Běžnější bylo průběžné plánování, psaní a kontrolování.

Závěrečné prodlevy často představovaly čekání na schválení: revize přinesla připomínky, agent je opravil a znovu úspěšně dokončil CI, načež čekal na další posouzení.

Opakovaná kontrola shody s původním kódem

Toub vytvořil dovednost rust-rebase-review, která doplňovala obecné instrukce pro Rust v repozitáři. Vzhledem k rychlému přísunu změn a častým konfliktům agenti opakovaně prováděli rebase. Dovednost se spouštěla podle vlastních instrukcí i ručně.

Používaný prompt se postupně vyvíjel. Jeho podoba odpovídala tomuto zadání:

Slučte změny do jediného commitu, poté proveďte rebase na nejnovější origin/main, vyřešte všechny konflikty a proveďte force push. Při rebase věnujte zvláštní pozornost všemu, co se změnilo, přibylo nebo bylo odstraněno, a zajistěte správné přenesení veškeré této logiky do odpovídajícího kódu Rustu. Rebase vždy provádějte sami jako hlavní agent; nespouštějte pro něj subagenta.
Poté zahajte smyčku revizí a oprav, v níž spustíte po jednom subagentovi s modely opus 5, gpt-5.6-sol a grok 4.6.
- Každý subagent má porovnat původní TypeScript a nový Rust řádek po řádku a potvrdit shodu chování.
- Hledejte cokoli, co zavádí jakoukoli nekompatibilitu. Cílem je převést kód do Rustu s co nejbližší 100% shodou sémantiky. Pokud narazíte na něco sporného, zeptejte se zadavatele.
- Zajistěte co nejefektivnější a idiomatický Rust. Hledejte příležitosti ke zjednodušení, k použití funkcí například z crate memchr pro optimalizaci vyhledávání místo ručně psaných smyček, k omezení zbytečných alokací a k využití traitů pro opakované použití a volné propojení.
- Zajistěte odstranění veškerého již nepotřebného TypeScriptu: plně přepsaného kódu, duplicitních testů, zbytečných propojovacích vrstev napi a podobně.
- Ověřte, že bylo přepsáno co nejvíce kódu. Pokud v dotčených souborech zbývá TypeScript, který není jen propojovací vrstvou, jde o varovný signál. Totéž platí pro přidávání nového TypeScriptu, který není mimořádně tenkou propojovací vrstvou. Vyhledejte volající těchto vrstev a ověřte, zda je lze také přepsat do Rustu a posunout hranici co nejdále. Cílem je brzy dosáhnout 100% Rustu ve vrstvě runtime.
- Ověřte, že nebyly odstraněny ani změněny žádné E2E testy. Takové změny ukazují na možnou chybu přepisu.
Pokud revize odhalí problémy, ověřte je, opravte potvrzené chyby a spusťte další úplnou revizi. Pokračujte, dokud všechny revize neskončí bez nálezů. Po každé sadě oprav podle připomínek proveďte commit a push, aby validace v CI běžela souběžně s dalšími revizemi.
Nespouštějte celé testovací sady; ty obslouží CI. Omezte činnosti zatěžující CPU na nezbytné minimum, protože pravděpodobně poběží mnoho operací souběžně.

Atomická výměna měla při rebase nečekanou výhodu. Pokud jedna větev TypeScript změnila a druhá jej odstranila při přepisu do Rustu, vznikl konflikt. Příchozí změna se tak zviditelnila a bylo nutné ji řešit; nezůstala tiše vedle už přepsaného kódu.

Vedle těchto kontrol běželo CCR nad každým commitem a tým využíval další revizní boty s vlastními postupy a prompty. Jejich připomínky se objevovaly v pull requestech a z velké části je zase zpracovávali agenti.

Rozdělení odpovědnosti bylo podstatné: agenti prováděli vyčerpávající porovnávání starého a nového kódu, testy a statická analýza kontrolovaly mechanicky ověřitelné vlastnosti a lidé se soustředili na architekturu, API kontrakty, rizika a podezřelá místa.

Toub volil cílovou architekturu, určoval důležité chování, rozděloval práci, rozhodoval nejasné kompromisy, posuzoval důkazy, ručně kontroloval rizikové oblasti i reakce agentů na připomínky a dělal konečná rozhodnutí o sloučení. Agenti zvýšili množství kódu, na které dokáže jeden odborník dohlížet; potřebu tohoto odborníka neodstranili.

Agent merge pomáhal, poslední schválení zůstávalo člověku

GitHub Copilot app spravovala souběžné relace i související kontext, například terminály, okna prohlížeče a pracovní plochy. Zvlášť důležitá byla funkce Agent merge, dostupná v CLI také příkazem /pr auto.

Tato automatizovaná smyčka pravidelně nebo na základě oznámení GitHubu kontroluje změny:

  • U nové připomínky agent rozhodne, zda ji přijme a opraví, nebo odmítne, a odpoví s označením automatizované reakce.
  • Při selhání testu stáhne protokoly, prozkoumá příčinu a opraví problém.
  • Při konfliktu provede merge nebo rebase.
  • Postupně vede pull request k úspěšnému CI, schválení a případně sloučení.

Agent merge obsluhoval všechny pull requesty přepisu. Většinou však automatizace končila před skutečným sloučením. Toub namátkově kontroloval provedené změny a zejména způsob vypořádání připomínek.

Význam této poslední kontroly ukázal případ ztracené funkce SDK. Přepis ji odstranil a kontrola kompatibility schématu v CI správně selhala. Agent však použil automatizační štítek schema-break-ok, kterým lze kontrolu obejít.

Toub se před sloučením zeptal, o jakou změnu schématu jde a proč je přijatelná. Nebyla: metoda ve větvi main existovala a přepis ji pouze opomenul. Po pokynu obnovit ji v Rustu agent během 21 sekund odstranil výjimku a vrátil nativní implementaci.

Tým proto neopravoval jen jednotlivé chyby. Průběžně upravoval instrukce pro vývojové i revizní agenty, z protokolů relací vytvářel evaluační testy a některá poučení promítl přímo do runtime – do promptů, popisů nástrojů a fungování režimu autopilot.

Přepis jazyka znamenal také výměnu knihoven

Každou knihovnu používanou původním runtime bylo nutné nahradit. Některé měly přímý protějšek v Rustu, jiné vyžadovaly více crate nebo vlastní implementaci.

CLI a runtime zatím sdílejí repozitář i package.json. Během přepisu z něj zmizelo přibližně 60 npm závislostí, které používal jen runtime. Skutečný počet nahrazených knihoven byl vyšší, protože některé zůstaly potřebné pro CLI.

Příkladem je zod pro deklarace a validaci schémat. Runtime jej nahradil kombinací serde, schemars a jsonschema, ale CLI jej dál používá.

Příklady relativně přímých náhrad:

  • js-tiktoken → tiktoken-rs se stejným kódováním o200k_base;
  • ignore → stejnojmenný crate se sémantikou gitignore;
  • minimatch → globset;
  • fast-myers-diff → similar;
  • dompurify → ammonia;
  • github/keytar → keyring.

Složitější případy zahrnovaly:

  • osm balíčků opentelemetry/* nahrazených čtyřmi crate, vlastním stavovým automatem pro sledování a exportérem do souboru;
  • mozilla/readability, linkedom a turndown nahrazené dvojicí readability a htmd;
  • sharp, image-size a file-type nahrazené image a imagesize.

V pěti případech vznikla místo npm balíčku zcela vlastní implementace napsaná agentem.

Kde nový runtime používá unsafe

Rust umožňuje v blocích unsafe provádět operace, jejichž bezpečnost musí zajistit autor kódu. U agentně generovaného kódu je proto důležité ověřit, zda se tato možnost nestala zkratkou kolem kontrol kompilátoru.

Celý runtime crate obsahoval 158 bloků unsafe ve 36 souborech, dále 26 deklarací unsafe fn, 26 bloků unsafe extern a devět implementací traitů unsafe impl. Všechny souvisely s interoperabilitou s vnějšími komponentami.

Důvod použití unsafeBlokyPodíl
Hranice C ABI5132,3 %
Windows API4931,0 %
POSIX / libc4629,1 %
SQLite C API74,4 %
Dynamické načítání knihoven42,5 %
Prostředí procesu10,6 %

Na hranici C ABI přicházejí nezpracované ukazatele a délky od volajícího, kterého kompilátor Rustu nekontroluje. Windows a POSIX zahrnují systémová volání, například čtení registru, výměnu přihlašovacích údajů, práci se stromy procesů a sysconf. SQLite je knihovna v C. U dynamického načítání přes dlopen nelze předem zaručit ani to, že nalezený symbol odpovídá očekávané funkci.

Jediný blok související s prostředím procesu vychází z toho, že Rust 2024 považuje změnu globálního prostředí vícevláknového procesu za nebezpečnou operaci.

Výhodou je auditovatelnost těchto hranic ve vlastním kódu. Původní TypeScript překračoval obdobné hranice přes interní C++ části Node.js a nativní npm balíčky, aniž by je zdrojový kód stejně viditelně označoval.

Nejde však o úplný seznam bezpečnostních rizik dodaného systému. Nebezpečné operace nebo chybné předpoklady mohou zůstávat v závislostech, sestavovacích nástrojích, knihovnách C, obalových funkcích i nesprávně definovaných FFI kontraktech.

unsafe se nepoužívá v klientech modelů, vrstvě MCP, agentech ani promptech. Žádná ze známých regresí přepisu také nesouvisela s blokem unsafe.

Jaké regrese přepis přinesl

K 14. září 2026 tým dohledal desítky regresí způsobených přepisem a všechny známé opravil. Většina se týkala správnosti chování, menší část výkonu.

Ne všechny se dostaly k uživatelům stabilních verzí. Některé odhalil vývoj v repozitáři, jiné předběžná vydání a část prošla až do stabilního vydání. Autor současně výslovně připouští, že další dosud neobjevené okrajové chyby pravděpodobně zůstávají.

Nejčastější příčiny spadaly do tří skupin: změna smluveného chování, změna stavu či životního cyklu a neúplný nebo při rebase ztracený přepis. Další problémy vznikaly na hranici hostitele a interoperability, případně v testech, které ověřovaly nesprávné chování.

Nejednoznačná sémantika

TypeScript má jediný typ number, zatímco Rust vyžaduje konkrétní číselný typ. Agenti někdy zvolili špatně:

  • Koncepčně celočíselné hodnoty převedli na f64, takže se serializovalo 42.0 místo 42. Silně typovaná SDK, například Go a C#, pak odmítala identifikátor repozitáře, časové razítko hooku nebo délku úkolu.
  • timeToFirstTokenMs se naopak změnilo na i64, přestože streamování produkovalo hodnoty jako 5446.712845. Uložené relace pak nešlo načíst ani obnovit.
  • Výraz event.error || "Unknown error" se změnil na .unwrap_or("Unknown error"). JavaScript původně nahrazoval i prázdný řetězec, Rust jej zachoval, takže chyba subagenta zůstala prázdná.

Chování implicitně dodávané prostředím

JavaScript a Node.js zajišťovaly některé vlastnosti neviditelně. toLocaleDateString například přebírá časové pásmo hostitele, zatímco Rust je potřeboval dostat explicitně. Jenže Intl.DateTimeFormat().resolvedOptions().timeZone může navzdory typu string vrátit undefined, které napi nedokázalo převést na Rust String. Výsledkem bylo rozbité načítání seznamu modelů.

Jinde se čtení proměnné prostředí přesunulo z místa před await za něj. Změna prostředí během čekání tak mohla změnit výsledek.

Nativní loader volal process.report.getReport() jen kvůli určení platformy, ale ve Windows tím respektoval _NT_SYMBOL_PATH a mohl několik minut stahovat PDB, než vykreslil CLI.

Podobné implicitní vstupy zahrnovaly pracovní adresář, identitu repozitáře, PATH a autentizaci relace. Přesun do Rustu vyžadoval rozhodnout, kdy je zachytit, jak je předávat a kdy obnovovat.

Přepis jen jedné poloviny související operace

Kontrola maximálního počtu kroků aktualizovala stav přerušení v nativním registru, ale nezrušila modelovou smyčku ve stejném procesu. Odešel tak ještě jeden požadavek navíc.

Jinde se dokončení úkolu uložilo a oznámilo událostí, ale nepromítlo do aktivního stavu relace. Režim autopilot proto pokračoval i po splnění úkolu.

Blokování hlavního vlákna

CLI stále řídí Rust z jednovláknové událostní smyčky Node.js. Synchronní práce přes napi tak může zmrazit rozhraní.

Příkaz /chronicle reindex tímto způsobem zpracovával stovky souborů relací a téměř minutu blokoval vykreslování i vstup. Pomohlo změnit export na async a práci přesunout do fondu vláken pro blokující operace.

Audit našel další rizikové vstupní body a vedl k pravidlu: exporty napi, které provádějí skutečnou práci, mají být asynchronní a podle potřeby používat spawn_blocking.

Krátce se otevírající konzolová okna

Ve Windows může spuštění procesu bez CREATE_NO_WINDOW krátce zobrazit konzolové okno. Původní Node.js kód upravoval spouštění procesů tak, aby příznak přidával automaticky. Agent při přepisu tento skrytý požadavek minul.

Audit opravil ještě dvě další místa, kde příznak chyběl už před přepisem, a požadavek se stal součástí instrukcí.

Životní cyklus, vlastnictví a pořadí událostí

Největší skupina problémů souvisela se životností, uvolňováním, vlastnictvím, pořadím a závody. Po přesunu stavu do Rustu držel TypeScript často jen neprůhledný identifikátor instance v nativní tabulce. Na rozdíl od objektové reference mohl tento identifikátor přežít samotnou instanci.

Hook uvolněný během požadavku zanechal blok tool_use bez odpovídajícího výsledku a zablokoval konverzaci. Shell zrušený mezi oznámením a skutečným spuštěním zanechal osiřelou operaci, která udržovala relaci aktivní. Přepnutí sandboxu aktualizovalo jeden čítač generace, ale ne jeho nativní protějšek, takže shell zůstal ve stavu změny konfigurace.

Opomenuté funkce

Jeden přepis vynechal callbacky SDK a současně odstranil jejich end-to-end test. Následovalo pravidlo, že agenti nesmějí bez výslovného souhlasu měnit E2E testy.

Přerušení relace jinde zachovalo nativní polovinu, ale ztratilo zrušení uvnitř procesu nutné k přerušení kroku čekajícího v nástroji.

Možnost SDK nahradit vestavěné vyhledávání nástroje závisela na zapnutí, popisu a schématu pro model a směrování provedení do callbacku SDK. Přepis ztratil všechny tři části a jednomu konzumentovi tiše nahradil vyhledávání přirozeným jazykem hledáním pomocí regulárních výrazů.

Odlišné chování náhradních knihoven

Rust MCP SDK rmcp tehdy odpovídalo na chybný vstup JSON-RPC, zatímco TypeScript SDK nikoli. Server, který na chyby reagoval dalším chybným výstupem, se tak dostal do nekonečné smyčky a zablokoval start.

Knihovny se stejným účelem v různých ekosystémech zkrátka nemusí mít totožné chování.

Změny ztracené mezi větvemi

Některé regrese vznikly odchylováním dlouho otevřených větví nebo při rebase. Při stovkách pull requestů týdně a tisících operací rebase znamená i velmi vysoká úspěšnost několik selhání.

Funkčně správné, ale pomalejší implementace

Část přepisů ztratila původní optimalizace, například memoizaci, plně asynchronní čekání nebo omezené streamování protokolů. Jiné přidaly režii na hranici Rustu a TypeScriptu: zbytečnou serializaci, zámky, polling, neomezenou nativní souběžnost nebo časté přechody do hostitele.

Jedno čtení například hluboce kopírovalo 260MB protokol událostí místo zapůjčení dat. Jiná implementace při soustavném toku událostí dokončovala jen malý zlomek požadovaných vyprázdnění kanálu a držela asynchronní handle pro každou událost, dokud V8 nevyčerpalo haldu.

Nešlo o budoucí příležitosti k optimalizaci, ale o skutečné zhoršení způsobené přepisem.

Projevila se migrace ve veřejně hlášené kvalitě?

Tým porovnal issues v repozitářích github/copilot-cli a github/copilot-sdk od ledna do srpna. Za související s kvalitou označil ta, která měla štítek chyby nebo v titulku běžné výrazy jako „bug“, „regression“, „crash“, „hang“, „timeout“, „broken“ či „incorrect“.

RepozitářLeden–duben, před přepisemKvěten–srpen, během přepisu a po něm
github/copilot-cli22,9 % (454 / 1 982)23,7 % (354 / 1 496)
github/copilot-sdk36,2 % (190 / 525)32,3 % (135 / 418)

Nejde o metriku dostupnosti ani přesný počet chyb, které unikly testům. Jednotlivá hlášení se liší rozsahem i významem. Je to však doplňková kontrola: navzdory velkému objemu změn nebyl ve veřejných kanálech patrný významný nárůst podílu problémů s kvalitou.

Úspěšná kompilace neznamená správné chování

Všechny známé regrese prošly do main, a tedy se úspěšně zkompilovaly. Z pohledu kompilátoru šlo o platný Rust.

Kompilátor může ověřit konzistentní použití f64, ale neví, že identifikátor repozitáře musí být v JSON celé číslo. Dokáže zabránit nesynchronizovaným datovým závodům v kódu, který vidí, ale neodhalí chybně navržený synchronizovaný stavový automat.

Fronta může být chráněná zámky a přesto se mohou dva odesílatelé spoléhat, že ji vyprázdní ten druhý. Události mohou bezpečně přecházet mezi vlákny a přitom dorazit ve špatném pořadí. Paměťově bezpečná funkce napi může na minutu zablokovat hlavní vlákno Node.js.

Kompilátor také nepozná chybějící funkcionalitu, odstraněný test, zapomenutý příznak Windows ani nepřiměřené náklady opakovaného kopírování stovek megabajtů dat.

Statické typování odstranilo rozsáhlou třídu mechanických chyb a poskytlo agentům velmi užitečnou zpětnou vazbu. Správnost původního kontraktu, úplnost přepisu, vhodné pořadí operací a provozní náklady však musely ověřovat další vrstvy.

Výkon: co přineslo odstranění Node.js a V8

Přepis měl záměrně zachovat chování. Neměl současně měnit algoritmy, opravovat nesouvisející chyby ani přepracovávat architekturu jednotlivých komponent. Toub agenty opakovaně odrazoval od příležitostných optimalizací, protože kombinace změny jazyka a chování ztěžuje hledání příčin regresí.

Výkon a škálovatelnost přesto patřily mezi hlavní cíle. Podstatou bylo zejména odstranit závislost na Node.js a V8.

Autor měřil několik scénářů přes C# SDK. Výchozí stav používal sestavení SDK a CLI před přepisem, TypeScript runtime hostovaný Node.js a komunikaci přes standardní vstup a výstup. Výsledky z 21. srpna porovnávaly Rust v samostatném procesu a Rust načtený přes FFI přímo do procesu aplikace.

Jde o srovnání celých dodaných systémů, nikoli izolovaný důkaz vlivu jazyka. Během stejného období přicházely i jiné změny.

Každý měřený krok komunikoval s deterministickým lokálním serverem, který vracel pevnou krátkou odpověď. Měření tedy záměrně vyloučilo inferenci modelu a síťovou latenci. Zachycovalo start klienta a procesu, vytvoření relace, události, ukládání a ukončení.

Scénář12. května21. srpna, samostatný proces21. srpna, stejný proces
Klient, relace a jeden krok5,25 s1,33 s (4,0×)292 ms (18,0×)
Obnovení relace s 32 kroky5,64 s1,52 s (3,7×)264 ms (21,4×)
Deset souběžných životních cyklů klienta12,34 s4,18 s (3,0×)742 ms (16,6×)
1 000 životních cyklů relace s jedním krokem132,52 s22,53 s (5,9×)20,93 s (6,3×)

První scénář zahrnuje vytvoření klienta, vytvoření relace, jeden krok a úplné ukončení. Velkou část původní režie tvořil start Node.js, inicializace V8 a načtení i zpracování JavaScriptu před prvním krokem. Rust tyto náklady odstraňuje.

Zátěžový test 1 000 relací používal jednoho sdíleného klienta a 100 souběžných pracovních proudů. Každý desetkrát vytvořil relaci, provedl krok modelu a relaci ukončil. V navazujícím popisu propustnosti zdroj uvádí 7,55 takového cyklu za sekundu pro původní TypeScript, 57,45 pro Rust mimo proces a 120,0 pro Rust ve stejném procesu.

Tyto hodnoty nejsou tvrzením, že Rust je univerzálně 15,9krát rychlejší. Jde o konkrétní scénář důležitý pro servery: mnoho nezávislých relací sdílejících jeden runtime.

V samostatném měření prostředků při stejné zátěži 100 × 10 spotřeboval původní strom procesů souhrnně 312 sekund CPU, zatímco konfigurace Rustu přibližně 110 sekund CPU. Úspora tedy nespočívala jen v přesunu práce na další jádra.

Paměťová stopa

Při dávce deseti klientů vzrostla špička rezidentní soukromé paměti nad výchozí stav o:

  • 1 383 MB u původního řešení;
  • 247 MB u Rustu v samostatných procesech;
  • 126 MB u Rustu ve stejném procesu.

Hodnoty se budou lišit podle stroje a použití. Směr výsledků však odpovídá cíli: na stejném hardwaru provozovat více klientů a relací, než se limitem stanou paměť, počet procesů nebo CPU.

Článek ve svém závěrečném shrnutí výkonu uvádí také přibližně 55 ms pro vytvoření klienta ve stejném procesu, jeden krok relace a ukončení, propustnost 120 cyklů za sekundu a snížení paměťového přírůstku o 91 %. U časů a propustnosti se tato shrnující čísla plně neshodují s výše uvedenou tabulkou; zdroj rozdíl podmínek dále nevysvětluje.

Podstatné je, že jde stále o základní přepis. Mnoho algoritmů zachovává strukturu původního TypeScriptu. Širší redesign využívající vlastnictví, souběžnost a hostování ve stejném procesu teprve může přinést další zlepšení.

Náklady: přibližně 120 000 USD za tokeny

Celková spotřeba přepisu činila přibližně 136,3 miliardy tokenů:

  • 130,6 miliardy vstupních tokenů načtených z cache;
  • 4,2 miliardy vstupních tokenů zapsaných do cache;
  • 900 milionů čerstvých vstupních tokenů;
  • 600 milionů výstupních tokenů.

Přiřazené náklady na tokeny dosáhly přibližně 120 000 USD.

K tomu je nutné přičíst práci člověka. Toub se však projektu nevěnoval výhradně. Agentní vývoj zahrnuje čekání, které umožňuje pracovat souběžně na dalších věcech: zadat úkol, nechat agenta pracovat a průběžně jej usměrňovat.

Pull requesty tohoto přepisu představovaly během daného období asi 20 % všech jeho pull requestů napříč repozitáři. Pokud se velmi hrubě předpokládá, že tento podíl odpovídá i podílu času, vychází přibližně tři týdny jeho vyhrazené práce.

Odhad 120 000 USD za tokeny a tří týdnů práce jednoho vývojáře tedy není úplným nákladovým vyúčtováním celého týmu. Autor výslovně uvádí, že migrace nebyla stoprocentně prací jediného člověka:

  • @stevesandersonms navrhl a implementoval napi-oop, dočasnou vrstvu interoperability mimo proces, a pět ze šesti implementací FFI pro SDK.
  • @edburns dodal šestou implementaci FFI.
  • @roji připravil balení SDK pro správnou distribuci a využití binárních souborů Rustu.
  • @caarlos0 pomáhal rozdělovat Rust na menší crate kvůli rostoucí době sestavování.
  • @criemen zlepšoval cachování sestavovacích prostředků pro rychlejší CI a lokální sestavení.
  • @devm33, @examon, @MRayermannMSFT, @dereklegenzoff a další se podíleli na revizích a schvalování pull requestů.

Průběžnému přechodu se museli přizpůsobovat také ostatní přispěvatelé do repozitáře.

Poučení pro další agentní projekty

Cíl musí být úplný a jednoznačný

Obecné zadání přepsat komponentu do Rustu agenti někdy chápali jen jako přepis nejčastěji vykonávaných cest nebo samotné logiky. I/O a orchestrace považovali za něco mimo rozsah.

Výrazně pomohlo explicitně stanovit cílový stav: nativní binární soubor ze 100% Rustu, bez zbývajícího prostředí, ve kterém by TypeScript vůbec mohl běžet.

End-to-end testy jsou zásadní

S jedinou výjimkou souvisely všechny regrese s chybějícími funkcemi a řada dalších problémů s nedostatečnými E2E testy. Při přepisu musí existovat nezávislé ověření správnosti, které se nepřepisuje současně s implementací.

Tým před začátkem pokrytí E2E testy zlepšil, ale zpětně je hodnotí jako nedostatečné. Důslednější pokrytí významného chování by podle autora počet regresí snížilo.

Agent nesmí sám oslabit měřítko správnosti

Agent měnící implementaci nesmí bez dohledu zároveň měnit definici úspěchu: oslabit test, aktualizovat referenční výstup, posunout výchozí úroveň kompatibility nebo použít výjimku z kontroly.

Pokud podobný postup zavádíte ve Vaší organizaci, oddělte vlastnictví citlivých kontrol nebo jejich schvalování a kombinujte vrstvy s odlišnými způsoby selhání. Jedna chyba pak nebude stačit k vydání závažné regrese.

Nejprve přepis, potom redesign

Zachování chování a algoritmů omezovalo počet současně měněných proměnných. Teprve po odstranění původní implementace a dočasného propojení dává smysl redesign vlastnictví, souběžnosti a výkonu nad stabilním základem.

Autor několikrát od tohoto pravidla ustoupil a zpětně všech těchto odboček litoval: přinesly více regresí, času nebo spotřebovaných tokenů.

Opakované chyby proměňte v trvalá pravidla

Pokud se stejný způsob selhání objeví podruhé, měl by se promítnout do stálých instrukcí, opakovaně použitelné dovednosti, evaluačního testu, chráněné referenční úrovně nebo samotného agentního prostředí.

Rychlá vývojová smyčka je s agenty ještě důležitější

Agenti rychle uvažují a píší kód, ale dál musí sestavovat a testovat. Právě tyto operace proto mohou tvořit větší podíl celkového času než při ručním vývoji.

Vyplatí se předem zrychlit sestavení, testy a opakované ověřování a připravit nástroje také na více souběžných úkolů ve více worktree. Výkon vývojových nástrojů není při agentním vývoji méně důležitý, ale naopak důležitější.

Co je hotové a co bude následovat

Produkční implementace runtime je kompletně v Rustu a dočasná interní hranice TypeScript/N-API zmizela. Přechod probíhal průběžně za běžného vydávání uživatelům, nikoli jedním velkým přepnutím.

Nešlo o zadání jediného promptu k přepisu celého repozitáře. Projekt vyžadoval rozdělení práce, testy, kontrolní mechanismy, koordinaci a zkušeného člověka, který rozuměl systému. Agenti však změnili ekonomiku natolik, že se rozsáhlý přepis živého produktu stal proveditelným bez vyčlenění celého týmu na rok či dva.

Další práce zahrnují zlepšování sestavování a vývojové smyčky, čištění přeložených struktur, redesign podle modelu vlastnictví a souběžnosti Rustu a další optimalizace. Na úrovni jednotlivých funkcí je značná část kódu idiomatická, ve větším měřítku ale stále často odpovídá algoritmům původního TypeScriptu.

Praktickým výsledkem je možnost načíst runtime přes SDK přímo do hostitelského procesu v šesti jazycích, bez Node.js a V8 a bez povinného druhého procesu. Právě tento druhý proces představoval podle zpětné vazby partnerů nejčastější překážku při adopci SDK.

Nižší režie zároveň umožňuje více souběžných relací na stejném hardwaru. Nativní runtime otevírá i další možnosti nasazení – od cloudu přes desktop a zařízení až po vestavěné systémy. Jde o nový technický základ pro další vývoj, nikoli o oznámení konkrétní dostupnosti produktu na všech těchto platformách.