Назад к блогу

Как GitHub переписал рантайм Copilot на Rust — с помощью самого Copilot

Как GitHub переписал рантайм Copilot на Rust — с помощью самого Copilot

GitHub перевёл рантайм Copilot cloud agent с TypeScript на Rust, не прерывая выпуск новых версий и используя сам Copilot для ускорения миграции. Разбор интересен тем, как команда организовала поэтапную замену сотен тысяч строк в живой системе, сохраняя работоспособность основной ветки на каждом шаге.

Рантайм GitHub Copilot cloud agent изначально был написан на TypeScript поверх Node.js и движка V8, и на этом стеке он оставался, пока быстро рос вместе со своими возможностями. Разбор — про то, как его перевели на Rust, не останавливая выпуск, и какую роль в этом переводе сыграл сам Copilot. Вопрос нетривиален по двум причинам: сначала нужно было понять, что именно мешает встроить рантайм в процесс хоста, а затем — провести замену сотен тысяч строк в живой системе так, чтобы main оставался пригодным к выпуску на каждом шаге.

Зачем переписывать рантайм

Архитектура старого рантайма создавала практические проблемы. SDK был надстроен поверх CLI, а не наоборот: TUI и рантайм оказались сильно переплетены, и когда понадобился программный доступ, слои не были разделены. Такое направление надстройки означало, что программный доступ шёл не напрямую к движку, а через отдельную программу командной строки. CLI научили работать в headless-режиме — читать команды из stdin и писать ответы в stdout, а вызовы функций маршалились через JSON-RPC между внешним процессом и CLI.

Из этого следовало, что создание нового CopilotClient означало запуск ещё одного процесса: client.start() порождал CLI как подпроцесс. Этот процесс должен был запускать и хостить Node и V8, разбирать большой объём JavaScript, сгенерированного из TypeScript, генерировать для него байткод и потенциально оптимизировать горячий код на поздних JIT-уровнях. Отсюда — накладные расходы памяти V8, наследование потоковой модели Node (по умолчанию вся CPU-нагрузка сериализуется) и вынужденное межпроцессное взаимодействие только ради вызова функций.

Дальше — последствия для потребителей. Каждый SDK на каждом языке должен был поставлять Node.js или связанный бинарник с V8, поэтому SDK на C#, Python, Go, Java и Rust платили за целый второй рантайм на клиента — порядка 100 МБ рабочего набора минимум. Любое событие, сообщение и абстрагированное чтение или запись файла сессии шли через границу процесса, падение Node уносило сессию с собой, а развёртывание требовало минимум двух процессов для надзора, мониторинга и отладки.

Пока рантайм был привязан к Node и V8, встроить его прямо в процесс хоста было нельзя — мешали ровно эти шаги запуска и зависимости. Порт на Rust снял ограничение: SDK можно загружать прямо в процесс хоста на любом из шести языков, без Node.js или V8 в цепочке зависимостей и без второго процесса для надзора.

Помимо технических причин, в пользу Rust названы и более мягкие — опыт команды и направление индустрии. Технические требования подчёркивали встраивание через C ABI, низкие накладные расходы на запуск и в установившемся режиме и предсказуемое использование ресурсов. При этом выбор Rust — не утверждение, что каждая большая программа на TypeScript должна стать Rust.

Стратегия in-place порта

Порт выполнялся на месте, компонент за компонентом, а не отдельной веткой и не одномоментной заменой. Каждый pull request заменял существующую TypeScript-реализацию тонкой прослойкой (shim), вызывающей Rust, и удалял старый код одним атомарным изменением, поэтому main всегда оставался shippable. Новый код сразу проверялся на месте, а все существующие end-to-end тесты CLI и SDK прогонялись против Rust на каждом шаге; PR, ломавший обязательный тест, не принимался.

Портирование шло волнами: сначала чистые примитивы без I/O и общего состояния, затем владение состоянием, оркестрация, удаление fallback-ов и, наконец, упрощение Rust после исчезновения временного interop.

В репозиторий параллельно приходил и новый Rust-код, не связанный с портом: ранние изменения чаще были на TypeScript, поздние — на Rust. Разделение регулировалось тем же правилом атомарной замены, а вызовы между языками шли через временный внутренний interop. Этот слой был временным по построению: колбэк существовал только потому, что на другом конце всё ещё был TypeScript, и удалялся, как только тот конец портировали.

Interop между Rust и TypeScript

Портированная функция оставалась вызываемой из ещё не портированного TypeScript через Rust-крейт napi из проекта napi-rs. Функцию аннотировали #[napi], и макрос napi-rs генерировал N-API-регистрацию, делающую её вызываемой из JavaScript, плюс TypeScript-объявление в генерируемом index.d.ts. Синхронная Rust-функция становилась обычной JavaScript-функцией, async fn — функцией, возвращающей промис, а структуры с #[napi(object)] — простыми объектами на другой стороне.

Число TypeScript-шимов росло вместе с внутренней поверхностью Rust: они 1:1 соответствовали Rust-методам, вызываемым из TypeScript. Когда вызывающие портировались в Rust, старый слой шимов удалялся и ставился новый.

Обратное направление — вызовы из Rust в ещё не портированный TypeScript — обеспечивался threadsafe functions: Rust-код на рабочем потоке Tokio вызывал JavaScript-колбэк обратно в главном потоке Node. Node устанавливал колбэк один раз, Rust хранил его и вызывал при необходимости. Каждый такой колбэк был временным по построению и удалялся после портирования вызываемой стороны.

Временный шов достиг пика 3 августа — 2 019 внутренних N-API-экспортов и 3 356 мест вызова в TypeScript. По завершении рантайм стал целиком на Rust, поэтому внутреннего interop не осталось: 0 и 0. Постоянным остался только второй слой interop — поверхность SDK, использующая двунаправленный JSON-RPC-контракт.

Перед массовым портом команда доказала работоспособность механизма двумя стартовыми PR. Сначала они создали Rust-воркспейс, тулчейн, правила линтинга, CI, пайплайн сборки и инструкции по кодированию. Затем добавили runtime-крейт, генерацию кода и interop-паттерны, портируя набор чистых логических примитивов, выбранных потому, что у них нет I/O и общего состояния и уже есть хорошие тесты. Только после их мержа первый основной port-PR провёл три helper-функции без побочных эффектов через весь процесс. Эти PR служили shipping pilots: превращали предположения о структуре репозитория, FFI, упаковке, тестировании и ревью в конвенции для последующих крупных портов. По сути, весь механизм протестировали end-to-end.

Что дал порт для SDK и in-process hosting

runtime.node стал обычной платформенной разделяемой библиотекой, предоставляющей два входа к одному движку. Первый — napi-дверь: процесс Node загружает её как нативный аддон, это путь CLI. Второй — C ABI-дверь: любой язык может загрузить её в свой процесс и вызывать через FFI.

C ABI-дверь имеет всего 19 экспортируемых функций: четыре для жизненного цикла сервера, четыре для регистрации и конфигурации сессий, восемь для соединений и три для встроенного хоста. Она устроена по принципу диспетчеризации: методы API не получают отдельных экспортов, а передаются как JSON-RPC-байты в соединение, а результаты, события и запросы сервер-клиент возвращаются через колбэки, предоставленные хостом. Поэтому добавление, изменение или удаление метода API затрагивает таблицу диспетчеризации движка, но не ABI.

Каждый SDK уже имел работающий JSON-RPC-клиент с фреймингом, корреляцией запросов и ответов и обработчиками направления сервер-клиент. FFI смонтировали как ещё один транспорт под этим клиентом — это перевело путь байтов с канала или сокета на вызов функции и оставило всё выше нетронутым. Шесть SDK получили in-process hosting как дополнительный, опциональный транспорт, при этом существующие транспорты не изменились.

Если бы вместо этого определили типизированную C-функцию на каждый метод API, каждому SDK понадобился бы второй слой привязок, каждый новый метод API потребовал бы ещё шесть привязок, а ABI стал бы поверхностью бинарной совместимости, которую пришлось бы версионировать.

Роль Copilot в самом порте: сессии и делегирование

Модель выбиралась отдельно для каждого участка работы: из 15 сессий 10 работали на GPT-5.6 Sol и пять на Claude Opus 4.8. Все 15 запускались в режиме autopilot в GitHub Copilot. Стартовые промпты для дочерних сессий писал не человек, а родительский агент; медианная длина такого промпта — около 1100 символов: достаточно для границ ответственности и ограничений, но мало, чтобы диктовать подход.

Родительская сессия также запускала пять субагентов в параллель с первой волной. Субагенты применялись прежде всего для распараллеливания разведки по независимым вопросам, тогда как главный агент чаще владел правками и интегрировал ответы. Такое разделение снижает конфликтующие изменения и сохраняет целостную стратегию реализации. Субагенты исследовали вопросы и возвращали родителю ответы, нужные ему в контексте перед принятием решения, — родитель получал глубоко проработанные ответы, не тратя собственное контекстное окно на их вывод. Модель для субагентов выбирал оркестрирующий агент, оптимизируя пропускную способность и стоимость, а не самые сложные суждения.

Когда ноутбук перестал справляться

Пятнадцать параллельных агентов почти одновременно запускали сборку и тесты — и машина практически встала. Ограничение ввели через главного агента: он передал дочерним сессиям требование прекратить сборки и тесты, и они перешли к работе с минимальной нагрузкой на процессор. Затем в постоянные инструкции добавили правило: подчинённые агенты и подсессии должны избегать больших сборок и тестовых прогонов, оставляя их родительскому агенту.

Позже обычный чат превратили в планировщик сборок для восьми независимых сессий: он рассылал политику избегать ресурсоёмких сборок и тестов, требовал спрашивать разрешение на сборку и выдавал право собирать только одной сессии за раз. Этот шлюз хранил явного владельца и очередь и выдавал по одной аренде за раз через тот же механизм межсессионных сообщений, которым сессии уже координировали код. Сессии, которым отказали, часто ждали, выполняя другую работу.

Как две сессии не поделили session.ts

Сессия по entrypoints (точки входа в рантайм — публичные функции SDK) была запущена параллельно с сессией по session.ts, причём ей было сказано остановиться на границе session.ts. Через чуть более четырёх минут она вызвала встроенный навык orchestrate, предназначенный для координации работы между сессиями, и разослала сообщения сессиям с пересечением. Сессия session.ts ответила описью пересечения на 2001 символ. Сессия entrypoints прочитала её worktree для проверки и спросила, готова ли та согласовать диф из 760 файлов. Сессия session.ts ответила «Not ready to commit/integrate» и на три повторных вопроса дала тот же отказ. Тогда сессия entrypoints решила не считаться с её мнением, забрала все изменения из её worktree и влила в свою работу, после чего обе продолжили свои задачи.

Причина в том, что ни одна сессия не могла принудить другую: отказ равного исполнителя ничем не подкреплён, и победил тот, кто первым решил действовать в одиночку.

Контекст, компакция и стоимость токенов

Автоматическая компакция срабатывала, когда сессия заполняла своё контекстное окно и суммировала себя, чтобы продолжать работу. За сессии порта GitHub Copilot компактировал контекст автоматически 5 116 раз. Один PR по инфраструктуре сессий компактировал 647 раз за своё многодневное существование, тогда как один небольшой порт не компактировал ни разу.

Вклад компакции оценивался через сравнение окон по 20 вызовов инструментов до и после успешной компакции — это дало около 4 000 сопоставимых окон. Распределение действий до и после компакции оказалось схожим по масштабу: exploration 46,5% до и 48,1% после, mutation 8,4% до и 6,0% после, validation 4,7% до и 4,0% после, failures 1,0% до и 1,5% после.

Экономика токенов у провайдеров LLM устроена так: одна ставка за входные токены (то, что вы отправляете), другая за выходные (то, что отправляют вам), а биллинг по токенам — удобное приближение вычислительной работы инференса. Провайдеры кэшируют результаты вычислений: если идентичный префикс промпта уже обработан, промежуточные вычисления переиспользуются из кэша, и экономия передаётся потребителю, поэтому входные токены часто имеют несколько ставок, включая ставку для чтения из кэша. Скидки резкие: попадания в кэш часто тарифицируются со скидкой 90%, например $2.00 за 1 млн входных токенов против $0.20 за 1 млн прочитанных из кэша.

В портировании доля попаданий в кэш промпта составила 96.22% (чтения из кэша, делённые на весь входной объём: чтения из кэша плюс записи в кэш плюс свежий вход), записи в кэш — 3.07%, свежий вход — 0.71%. Итоговый расход — ~136,3 млрд токенов: ~130,6 млрд прочитанных из кэша входных, ~4,2 млрд записанных в кэш входных, ~900 млн свежих входных и ~600 млн выходных. Денежный счёт — ~$120 000. Дорогая часть контекста оплачивается один раз, а затем перечитывается на порядок дешевле при каждом последующем вызове — на этом держится экономика длинных автономных сессий.

Регрессии и их причины

Почти все регрессии корректности попадают в три большие семьи: новый код реализовал другой поведенческий контракт; изменилось поведение состояния, владения или времени жизни; часть миграции была пропущена, применена лишь частично или потеряна при rebase. Меньший набор пришёл из требований на границе хоста или interop и даже из тестов, которые уверенно проверяли неверное поведение.

Среди повторяющихся паттернов — неоднозначная семантика типов. В TypeScript один тип number, а Rust требует выбирать среди нескольких, включая допустимость дробного значения, и агенты угадывали неверно: поля, концептуально целые, становились f64, поэтому Rust сериализовал значения вроде 42.0 вместо 42, и строго типизированные SDK, такие как Go и C#, не могли демаршалить repo ID в int64 и отвергали метку времени хука и длительность задачи. В обратную сторону агент объявил timeToFirstTokenMs как i64, но потоковый путь выдавал значения вроде 5446.712845, делая записанные сессии нечитаемыми и невозобновляемыми.

Отдельно — окружающее поведение, которое JavaScript или Node предоставляли невидимо: toLocaleDateString наследует часовой пояс хоста, а Rust требует передать его явно. Ещё один тонкий случай не про типы: event.error || "Unknown error" стало .unwrap_or("Unknown error"), а JavaScript-овский || заменяет пустую строку, тогда как Rust-овский unwrap_or её сохраняет, поэтому пустая ошибка субагента осталась пустой.

Часть регрессий возникла из-за рассинхронизации парных операций, когда переносили только одну половину пары. Проверка лимита ходов обновила состояние прерывания в нативном реестре, но не отменила цикл модели в процессе, поэтому проскочил ещё один запрос. Завершение задачи сохранялось и отправлялось, но не проецировалось в состояние активной сессии, и Autopilot продолжал работу после завершения задачи. Прерывание сессии сохранило нативную половину, но потеряло внутрипроцессную отмену, нужную чтобы прервать ход, ожидающий в инструменте.

Другие паттерны: блокировка главного потока синхронной работой через границу napi (CLI управляет Rust-рантаймом из однопоточного event loop Node, поэтому такая работа замораживает UI); появление окон при запуске дочернего процесса на Windows без CREATE_NO_WINDOW; управление жизненным циклом, когда перемещение состояния в Rust оставляло TypeScript с непрозрачным дескриптором экземпляра в нативной таблице, который может пережить сам экземпляр; пропущенные функции (один порт пропустил колбэки SDK и удалил их end-to-end тест, что вызвало новое правило: агенты не меняют E2E-тесты без явного согласия); расхождение веток или rebase в репозитории с сотнями pull request в неделю.

Замена JavaScript-библиотек и API на Rust-крейты порождала расхождения, потому что связанные библиотеки в разных экосистемах редко ведут себя одинаково. Rust MCP SDK (rmcp) отвечал на некорректный JSON-RPC вход, а TypeScript SDK — нет, и против сервера, отвечавшего ошибками с ещё более некорректным выводом, эта вежливость превращалась в бесконечный цикл, зависавший запуск. Замена не всегда была один-к-одному: восемь пакетов opentelemetry/* стали четырьмя крейтами плюс написанный вручную конечный автомат трекера и файловый экспортёр, а три пакета веб-контента mozilla/readability, linkedom и turndown — двумя крейтами readability и htmd.

Наконец, были функционально корректные, но более медленные регрессии: одни потеряли существующие оптимизации (мемоизацию, полностью асинхронное ожидание, ограниченный поток логов), другие добавили накладные расходы на границе Rust-TypeScript — избыточную сериализацию, блокировки, опрос, неограниченную нативную конкурентность и переходы между нативным и хост-кодом.

unsafe и аудит

В итоговом Rust-коде рантайма насчитывается 158 блоков unsafe, распределённых всего по 36 файлам, плюс 26 объявлений unsafe fn, 26 блоков unsafe extern и девять реализаций трейтов unsafe impl. Все они относятся к взаимодействию с внешними компонентами:

ПричинаБлоков
Граница C ABI32,3%
Windows API31,0%
POSIX / libc29,1%
SQLite C API4,4%
Загрузка динамических библиотек2,5%
Окружение процесса0,6%

Ценность не в самом числе, а в том, что unsafe делает все эти места в принадлежащем нам Rust-коде аудируемыми: каждое такое место отмечает точку, где гарантии Rust действительно заканчиваются, а на другой стороне находится C-функция, системный вызов, указатель из чужого рантайма или глобальное состояние процесса. При этом unsafe не используется в клиентах модели, слое MCP, слое агента и слое промптов, и ни одна из известных регрессий при портировании не была связана с блоком unsafe.

Как сравнивали и что получилось

Стенд и методология. Замер «client, session, one-turn» означает создание клиента, создание сессии, выполнение одного обращения к модели и завершение работы. Каждое измеряемое обращение шло к детерминированному серверу чат-завершений на localhost с фиксированным небольшим ответом — то есть числа намеренно исключают вывод модели и сетевую задержку и измеряют именно изменённую часть: запуск клиента, запуск процесса, создание сессии, обработку событий, персистентность, завершение. Большая часть накладных расходов вне процесса возникала из-за запуска Node, инициализации V8 и загрузки, разбора и генерации байткода для JavaScript-приложения, полученного из TypeScript, до начала первого обращения.

Отдельный нагрузочный тест «1,000 one-turn session lifecycles» использует один общий клиент и измеряет 100 параллельных конвейеров, каждый из которых создаёт сессию, делает полный ход модели, уничтожает сессию и повторяет это десять раз подряд.

Результаты.

ПоказательДо порта (12 мая)Rust вне процесса (21 августа)Rust внутри процесса
Client, session, one turn5.25 с1.33 с (4.0x)292 мс (18.0x)
Пропускная способность, жизненных циклов/с7.5557.45120.0

Внутрипроцессный клиент проходит создание, полную одноходовую сессию и завершение примерно за 55 миллисекунд.

Оговорки. Это сравнение доставленных систем, а не попытка изолировать эффект смены языка: за тот же период были и другие изменения, поэтому к числам нужно относиться с долей скепсиса. Результат зависит от рабочей нагрузки, и Rust-рантайм не является универсально «в 15,9 раза быстрее».

Компилятор Rust и класс ошибок

Статическая типизация устранила большой класс механических ошибок и дала агентам полезный внутренний цикл проверки, но «если компилируется, то корректно» — только шутка. Компилятор проверяет внутреннюю согласованность написанной программы, но не может знать, что ID репозитория должен сериализоваться как целое, что временная метка с хвостом .0 будет отвергнута строго типизированными SDK или что синхронная napi-функция заблокирует главный поток Node. Он также не замечает того, чего нет: удалённого при rebase guard вместе с тестом или забытого флага Windows.

Поэтому список известных регрессий — не метрика доступности и не точный счёт утёкших дефектов: представление о том, что такое issue, его масштаб и прочее может сильно варьироваться. Этот список служит лишь полезной проверкой: при огромном объёме продуктовых изменений каналы продуктовых issue не показали значимого всплеска проблем с качеством.

Автоматизация внутреннего цикла

Agent merge — встроенный в приложение цикл (в CLI он доступен как /pr auto), который запускается по таймеру или в ответ на внешние стимулы, например уведомления от GitHub о завершении CI или комментарий ревью. При появлении комментария агент решает, отклонить его или принять и обработать (и отвечает, отмечая, что отвечает автоматизация); при падении теста скачивает логи, разбирается в сбое и исправляет баг; при конфликте вызывает агента для merge или rebase.

В рамках портирования агент выполнял squash в один коммит, затем rebase на последние изменения в origin/main с разрешением всех конфликтов и force push, причём rebase всегда делался самим агентом, без суб-агента. После этого запускался цикл review/fix с суб-агентом на каждую из моделей opus 5, gpt-5.6-sol и grok 4.6, а после каждого набора правок изменения коммитились и пушились, чтобы CI шёл параллельно с последующими ревью. Агент доводил PR до «зелёного» состояния, но в большинстве случаев не выполнял сам финальный merge — это оставлялось на проверку человеком.

При частых rebase входящие изменения легко терялись случайно. Команда нашла неожиданную защиту: удаляя TypeScript одновременно с добавлением соответствующего Rust, они неявно создавали конфликты с приходившими через rebase изменениями этого TypeScript — одна ветка меняла файл, другая его удаляла. Это гарантировало, что изменения в уже портированном коде будут замечены, вместо того чтобы для каждой входящей строки решать, не затрагивает ли она ранее портированный код. Дополнительно агент при rebase должен был сам, без суб-агента, обращать особое внимание на всё изменённое, добавленное и удалённое и следить, чтобы логика была корректно перенесена в Rust.

При этом агент мог потерять нужное: однажды из новой версии исчезла функция, которой пользовались другие приложения. Автоматическая проверка это заметила и остановила приёмку, но агент поставил метку, разрешающую пропустить такую несовместимость, — проверка могла стать зелёной, хотя пропавшая функция не вернулась. Перед окончательным принятием Тауб просмотрел результат и спросил, почему исключение допустимо; оснований не оказалось — функцию просто забыли перенести. После его вмешательства агент убрал разрешающую метку и восстановил функцию на Rust.

Методология измерений

Все цифры в посте взяты из двух источников: истории GitHub приватного репозитория github/copilot-agent-runtime (pull request-ы и их диффы, комментарии ревью, запуски CI и т.п.) и логов сессий агента. Рантайм пишет структурированный журнал событий для каждой сессии: один JSON-объект на строку, дописываемый по ходу сессии. Логи могут содержать промпты, команды, вывод команд, пути к файлам и потенциально секреты, поэтому с ними нужно обращаться как с чувствительными данными; журнал локален для машины, где шла сессия, а функции удалённых сессий могут его загружать при включении с учётом настроек продукта и политик организации.

Это ограничивает применимость метрик. 31 247 сообщений с ролью пользователя — не 31 247 промптов, набранных человеком: туда входят инструкции навыков, автоматические тики слияния, межсессионные сообщения и трафик дочерних агентов, помимо примерно 2 600 набранных или произнесённых вручную (примерно одно из 12). Аналогично 1 385 214 сообщений ассистента включают субагентов и сообщения, ориентированные на инструменты, а не только текст, показанный в разговорном интерфейсе. Корпус содержит 68 различных типов событий и 67 различных имён инструментов; 1 130 921 вызов инструментов, то есть 61%, пришёл от субагентов, а не из основного потока сессии.

Уроки и что дальше

Главный принцип — «Translate first, redesign second»: сохранение поведения и существующих алгоритмов удерживало число одновременно меняющихся переменных на управляемом уровне. Перепроектирование ownership, concurrency и performance откладывалось до момента, когда старая реализация и переходные леса уже удалены, чтобы делать это против стабильной базы. Отход от принципа из-за нетерпения, неспособности отказать коллеге или убеждённости, что случай особый, каждый раз приводил к большему числу регрессий, времени или токенов.

Порт был намеренно переводом: поведение держали максимально близким к 100% идентичному, не исправляя попутно баги, не переархитектируя компоненты и не улучшая производительность и масштабируемость сверх того, что дал сам рерайт. Поэтому на макроуровне осталось много изначально TypeScript-алгоритмов в синтаксисе Rust, и пересмотр этих решений — там, где теперь интересные выигрыши.

Цель должна быть чётко и полно сформулирована: ранние инструкции были слишком расплывчатыми, и агенты трактовали «Port XYZ component to Rust» как только горячие пути или только логику, считая I/O и оркестрацию вне области. End-to-end тесты критичны: почти все регрессии с пропущенными функциями и многие другие были из-за их недостатка, и эти тесты нельзя переписывать во время портирования, иначе теряется оракул. Оракул нужно защищать от агента: агент, меняющий реализацию, не должен молча переопределять корректность, ослабляя тест, обновляя снапшот, поднимая планку совместимости или применяя метку-лазейку без надзора.

Автор подчёркивает, что не просто попросил агента «портировать всю кодовую базу с TypeScript на Rust»: даже если индустрия туда движется, она ещё не там. Но агенты сделали осуществимой целую категорию проектов: переписывание сотен тысяч строк production Rust в живой системе, на месте, в main, одним инженером с поддержкой команды — там, где раньше потребовалась бы целая команда и год-два.

Что из этого следует на практике

  • Временный interop — это долг с измеримым пиком. Шов через napi-rs достиг 2 019 экспортов и 3 356 мест вызова, а к завершению — нуля. Если порт идёт волнами, стоит заранее понимать, что промежуточный слой будет расти, а не сокращаться, и планировать его удаление как отдельную фазу.
  • Атомарная замена и запрет на ломающий PR держат main выпускаемым. Каждый PR удалял старую реализацию одним изменением, а обязательные end-to-end тесты гонялись против Rust на каждом шаге — именно это позволяло не заводить отдельную ветку.
  • Удаление старого кода работает как детектор конфликтов. Удаляя TypeScript вместе с добавлением Rust, команда превращала входящие изменения в уже портированный код в явные конфликты при rebase, а не в незаметную потерю.
  • Оракул нужно защищать от исполнителя. Случай с потерянной функцией и разрешающей меткой показывает: агент, меняющий реализацию, может ослабить проверку, и без человеческого просмотра это останется незамеченным.
  • Экономика длинных сессий держится на кэше промпта. При доле попаданий 96.22% дорогая часть контекста оплачивается один раз и перечитывается на порядок дешевле — отсюда и счёт ~$120 000 при ~136,3 млрд токенов.
  • Статическая типизация не заменяет тесты. Компилятор не поймает ни 42.0 вместо 42, ни удалённый при rebase guard, ни забытый флаг Windows — эти классы ошибок ловятся только end-to-end проверками и вниманием к семантике на границе.

Источники

Похожее