GitHub перевёл Copilot agent runtimeпрограммная оболочка вокруг нейросети: передаёт ей запросы, даёт доступ к инструментам и следит за ходом работы с TypeScript на Rust. Это не смена модели ИИ — переписан слой, который используется в GitHub Copilot CLI, GitHub Copilot app, GitHub Copilot SDK, а также в свежих релизах VS Code, Visual Studio, CCA, Copilot Code Review, Copilot Cowork, Copilot Studio, Excel, Outlook, PowerPoint и Word. О работе рассказали в блоге GitHub 16 сентября 2026 года (обновление 23 сентября), на Хабре — 5 октября.
Перенос шёл с мая по август. За это время выпустили 135 версий, включая предварительные, а новый код на Rust составил 832 378 строк без учёта тестов — при том что через перенос прошло около 430 тысяч строк старого кода, хотя изначально речь шла примерно о 130 тысячах.
Что именно изменилось
Рантайм стал на 100% Rust — чистым нативным бинарником. Он предоставляет C ABIдвоичный интерфейс, по которому одна программа вызывает функции другой так, как это принято в языке C для внутрипроцессного использования и stdin/stdout- или socket-сервер для внепроцессного режима. Поставляемый runtime.node — обычная платформенная разделяемая библиотека: под расширением .node лежит .dll, .so или .dylib. У неё две «двери» к одному движку: napiинтерфейс, через который Node.js загружает нативные аддоны и вызывает их функции из JavaScript, которую Node-процесс загружает как нативный аддон (это путь CLI), и C ABI-дверь, которую любой язык может загрузить в свой процесс и вызывать через FFIинтерфейс вызова функций одного языка из кода на другом языке. Обе двери ведут к одному движку, но napi-дверь рассчитана на загрузку из Node, а C ABI-дверь — на загрузку из любого языка.
Через C ABI-дверь SDK выбирает in-process рантайм своим механизмом нативной интероперации: C# — через P/Invoke, Go — через purego, Python — через cffi, Rust — через libloading. C ABI-дверь имеет всего 19 экспортируемых функций (четыре для жизненного цикла сервера, четыре для регистрации и конфигурации сессий, восемь для соединений и три для встроенного хоста). За ними стоит общий контракт из 364 маршрута диспетчеризацииадресуемых операций, которые хост может вызвать у рантайма или рантайм у хоста, — каждая передаётся как отдельное сообщение, а не как отдельная экспортируемая функция: 340 вызываются потребителями SDK, 24 — обратные вызовы рантайма к SDK. Методы API не получают отдельных экспортов, а передаются как JSON-RPC байты в соединение; результаты, события и запросы сервер-клиент возвращаются через предоставленные хостом колбэки.
In-process хостинг стал «drop-in» вместо переписывания: у каждого SDK уже был работающий JSON-RPC клиент с фреймингом, корреляцией запросов и ответов и обработчиками сервер-клиент направления. Подключение FFI как ещё одного транспорта под этим клиентом переносит байтовый путь с пайпа или сокета на вызов функции, оставляя всё выше нетронутым. SDK теперь можно загружать прямо в хост-процесс на любом из шести языков без Node.js или V8 в цепочке зависимостей и без второго процесса для надзора.
Как было раньше
Рантайм изначально написали на TypeScript поверх Node.js и движка V8 — для того, что теперь называется GitHub Copilot cloud agent (CCA). Запуск требовал поднять и разместить Node и V8, разобрать значительный объём JavaScript, сгенерированного из TypeScript в CLI, и сгенерировать для него байткод. Это давало накладные расходы по памяти, связанные с V8, и навязывало модель потоков Node, при которой вся работа, ограниченная CPU, по умолчанию сериализуется.
Каждый потребитель SDK на любом языке был вынужден поставлять Node.js или собранный бинарник с V8, платя за второй языковой рантайм минимум порядка 100 МБ рабочего набора. Любое событие, сообщение и абстрагированное чтение/запись файловой системы сессии передавались через границу процесса, а падение Node уносило с собой сессию. Архитектура CLI и рантайма была переплетена, а SDK надстроили над CLI: подключение к рантайму из SDK означало порождение ещё одного процесса.
Почему это сделали
Старая архитектура возникла из-за скорости индустрии: решения принимались ради скорости поставки и охвата рынка. Из-за быстрой поставки TUI и рантайм оказались переплетены, а SDK положили поверх CLI, а не наоборот. Чтобы дать программный доступ, CLI научили работать headless и общаться по JSON-RPC, а SDK стал запускать процесс CLI — это плохо влияло на производительность (запуск, память, пропускная способность) и надёжность потребляющих приложений.
Хотели рантайм без TUI, с минимальными зависимостями, встраиваемый в процесс, пригодный для FFI всех шести SDK. Требования подчёркивали встраивание через C ABI, низкие накладные расходы на запуск и в установившемся режиме, а также предсказуемое использование ресурсов. Rust сделал эти цели достижимыми — ценой других осложнений, например необходимости явно представлять времена жизни и разделяемое состояние. Помимо технических причин названы «мягкие» — опыт команды и направление индустрии. Авторы оговаривают, что это не утверждение, будто каждая большая программа на TypeScript должна стать Rust.
Стратегия переноса: in-place porting
In-place portingпокомпонентный перенос, при котором рантайм переписывается по частям, а не весь сразу означал, что каждый кусок переписывается по отдельности. Выбран был вариант atomic replacement: каждый PR заменяет существующую TypeScript-реализацию тонкой прослойкой (shim), которая вызывает Rust, и удаляет старый код одним атомарным изменением. Поэтому main всегда оставался shippable: каждый PR портирует один компонент или срез, все существующие end-to-end тесты CLI и SDK прогоняются против нового Rust-кода на каждом шаге, и если PR ломал обязательный тест, он не попадал в main.
За время переноса main выпустил 135 релизов (100 pre-release и 35 stable), в среднем ~1,3 релиза в день и ~1,3 port-PRpull request, который портирует один компонент рантайма с TypeScript на Rust в день. Временный внутренний interopслой взаимодействия между Rust и TypeScript, существующий только пока часть рантайма ещё не портирована был двусторонним: любую портированную в Rust функцию нужно было вызывать из TypeScript, а Rust-функциям нужно было вызывать TypeScript-колбэки. Вызов Rust из TypeScript реализовали через крейт napi из проекта napi-rs: функция помечается #[napi], и макрос генерирует N-API-регистрацию плюс TypeScript-декларацию в index.d.ts. Обратное направление делали через threadsafe functionsмеханизм napi-rs, позволяющий Rust-коду на рабочем потоке Tokio вызывать JavaScript-колбэк в главном потоке Node napi-rs.
Каждый такой колбэк временный по построению: он существует только потому, что другая сторона ещё TypeScript, и удаляется при её портировании. Пик временного шва пришёлся на 3 августа — 2 019 внутренних N-API-экспортов и 3 356 мест вызова в TypeScript; в завершении осталось 0 и 0.
Как работали ИИ-агенты
Параллельно работали 15 дочерних сессийотдельные агентские сессии, которые родительская сессия создаёт как самостоятельные ветки со своим рабочим пространством и своим циклом работы, созданных родительской сессией в семь волн примерно за три часа: первая волна — пять, вторая — ещё две примерно через двадцать минут, затем ещё пара через двадцать минут, дальше одиночные и пары в течение следующих двух часов. Каждая сессия — отдельная ветка со своим worktreeотдельная рабочая копия репозитория, привязанная к ветке и своим агентом. Выбор модели делался на каждый слайс: 10 из 15 работали на GPT-5.6 Sol и пять на Claude Opus 4.8, все 15 запускались в autopilotрежим, в котором сессия преследует цель, не останавливаясь на одобрение каждого шага.
Стартовые промпты дочерним сессиям писал родительский агент, а не человек; медианный промпт был около 1 100 символов. Помимо 15 подсессий та же родительская сессия использовала пять субагентов: три exploreсубагента, которые исследуют кодовую базу и возвращают находки в контекст родителя, запускались параллельно с первой волной, один code-reviewсубагент, который проверяет изменения и ищет проблемы, и один rubber-duckсубагент, с которым можно обсудить решение, чтобы найти слабые места. Субагент работает внутри рабочего пространства родителя и возвращает ответ в его контекст, тогда как дочерняя сессия имеет собственный worktree, ветку и цикл агента.
Порт оркестрации моделей — слоя, который общается с провайдерами, — имел основную сессию длительностью 42 часа настенных часов и запустил 126 субагентов, из которых в пике одновременно работали 22. Порт extension-runtime занял 88 часов и имел иную структуру: написание и ревью значительно перекрывались, а простой откладывался на конец. Сессия entrypoints решила не считаться с мнением сессии session.ts и забрала все её изменения из worktree, объединив их в свою работу.
Позже обычная чат-сессия была превращена в планировщик сборок для восьми независимых сессий-портов, выдающий по одной арендуразрешению на сборку, которое выдаётся одной сессии за раз на сборку за раз: сессии обязывались избегать ресурсоёмких сборок и тестов и запрашивать разрешение, когда сборка необходима.
Agent merge и автоматизация inner loop
Agent mergeвстроенный в приложение цикл, который автоматизирует доведение pull request до зелёного состояния, одобрения и слияния по таймеру или в ответ на внешние стимулы (уведомления от GitHub о завершении CI, комментарий ревью) смотрит, что изменилось, и автоматизирует действия разработчика. В CLI тот же цикл доступен как /pr auto. При появлении комментария ревью агент решает, отклонить его или принять и исправить, и отвечает, отмечая, что отвечает автоматизация. При падении теста он скачивает логи, исследует сбой и исправляет баг; при конфликте — выполняет merge или rebase.
В рамках миграции агент выполнял squash в один коммит, затем rebase на последний origin/main с разрешением всех конфликтов и force push, причём rebase всегда делался самим агентом, без суб-агента. После этого запускался цикл review/fix с суб-агентами (по одному на opus 5, gpt-5.6-sol и grok 4.6), которые построчно сравнивали старый TypeScript и новый Rust, искали несовместимости, возможности упрощения и идиоматичности, требовали удаления отжившего TypeScript и проверяли, что E2E-тесты не удалены и не изменены. При обнаружении проблем их валидировали, исправляли и повторяли полный цикл ревью до чистого результата, а после каждого набора изменений коммитили и пушили, чтобы CI-валидация шла параллельно с последующими ревью.
Что это меняет на практике
Сценарий «клиент, сессия, один ход» ускорился с 5.25 с до 1.33 с (в 4.0 раза) вне процесса и до 292 мс (в 18.0 раз) при загрузке в процесс через FFI. Возобновление 32-ходовой сессии — с 5.64 с до 1.52 с (3.7x) вне процесса и до 264 мс (21.4x) в процессе. Десять параллельных жизненных циклов клиента — с 12.34 с до 4.18 с (3.0x) и 742 мс (16.6x) соответственно. Один цикл клиента в процессе занимает около 55 мс.
Основная часть накладных расходов вне процесса приходилась на запуск Node, инициализацию V8 и загрузку, разбор и генерацию байткода JavaScript — Rust-рантайм эти затраты устраняет. На нагрузке из 1000 одноходовых жизненных циклов сессий старый TypeScript CLI выполнял 7.55 цикла в секунду, Rust вне процесса — 57.45, Rust в процессе — 120.0, при этом агрегированное CPU-время упало с 312 с примерно до 110 с. Пиковый прирост резидентной памяти на десяти клиентах составил 1 383 МБ до переноса, 247 МБ для Rust вне процесса и 126 МБ для Rust в процессе.
Регрессии и unsafe-код
Регрессии делились на несколько групп. Парные операции рассинхронизировались: проверка лимита ходов обновляла состояние прерывания в нативном реестре, но не отменяла внутрипроцессный цикл модели, позволяя уйти ещё одному запросу; завершение задачи сохранялось и публиковалось, но не проецировалось в состояние активной сессии, поэтому Autopilot продолжал работу после завершения задачи. Замены библиотек давали разное поведение: Rust MCP SDK (rmcp) отвечал на некорректный JSON-RPC, а TypeScript SDK — нет, и против сервера, отвечавшего ошибками ещё более некорректным выводом, это превращалось в бесконечный цикл, вешавший запуск.
Часть регрессий возникала из-за дрейфа веток и ребейзов: сессия, открытая на дни, накапливала изменения и конфликты, порт требовал тысяч ребейзов, и даже очень высокий процент успеха оставлял сбои. Были и «медленные» регрессии: терялась мемоизация, полностью асинхронное ожидание или ограниченная потоковая передача логов, добавлялись накладные расходы на границе Rust-TypeScript — избыточная сериализация, блокировки, опрос, неограниченная нативная конкурентность и переходы между нативным и хост-кодом. К 14 сентября 2026 года проследили десятки известных регрессий порта, все исправлены. Исправляли точечно: например, экспорт /chronicle reindex сделали async и перенесли работу в поток блокирующего пула, а также ввели постоянное правило, что napi-экспорты, делающие реальную работу, должны быть асинхронными и при необходимости использовать spawn_blocking.
В рантайме насчитывается 158 блоков unsafeучастков кода, где программист берёт на себя ответственность за гарантии безопасности, которые Rust обычно проверяет сам в 36 файлах, вместе с ними 26 объявлений unsafe fn, 26 блоков unsafe extern и девять реализаций трейтов unsafe impl. Все они связаны исключительно с взаимодействием с внешними компонентами: граница C ABI (51 блок), Windows API (49), POSIX/libc (46), SQLite C API (7), динамическая загрузка библиотек (4) и окружение процесса (1). Эти блоки отмечают места, где гарантии Rust действительно заканчиваются: по ту сторону находится C-функция, системный вызов, указатель из чужого рантайма или глобальное состояние процесса. unsafe не используется в клиентах моделей, слое MCP, слое агентов и слое промптов, и ни одна из известных регрессий при портировании не была связана с блоком unsafe.
Цифры
По логам сессий зафиксировано 12 760 995 событий, 31 247 пользовательских сообщений (из них примерно 2 600 набраны или произнесены лично автором, около одного из 12) и 1 385 214 сообщений ассистента. Дочерние сессии затронули 140 различных файлов, 120 из которых — ровно одна сессия; родитель опросил их состояние 60 раз и отправил 89 координационных сообщений.
Расход токенов составил ~136,3 млрд всего, включая ~130,6 млрд кэшированных входных чтений, ~4,2 млрд кэшированных входных записей, ~900 млн свежих входных и ~600 млн выходных токенов. Денежный счёт — ~$120 000.
Ограничения и открытые вопросы
Авторы признают, что цель должна быть сформулирована чётко и полно: ранние инструкции были слишком расплывчатыми, из-за чего агенты считали ввод-вывод и оркестрацию вне области работ. Сквозные тесты критичны, и их собственный план требовал значительно улучшить тестовую базу до начала переноса, но этого сделали недостаточно. Также признаётся необходимость защищать «оракул» от агента: нельзя позволять агенту ослаблять тесты, обновлять снимки или применять метку-исключение без надзора.
Автор сожалеет об отступлениях от принципа «сначала перевод, потом редизайн» — каждое такое отступление стоило больше регрессий, времени или токенов. Остаются открытыми вопросы дальнейшего улучшения сборки и внутреннего цикла разработчика, очистки переведённых структур, редизайна под владение и конкурентность Rust и дальнейших оптимизаций производительности. Автор прямо допускает, что обнаружатся новые ошибки: весь набор редких ситуаций заранее не перебрали.