GitHub CopilotのランタイムがRustへ移行――AIエージェントによる大規模リライトは何を変えたのか

Java / Spring

GitHub Copilotの中核ランタイムをRustへ全面移行

GitHubは、Copilot CLI、Copilotアプリ、Copilot SDKなどを支えるエージェント実行ランタイムを、TypeScript/Node.jsからRustへ移行したと発表しました。

移行対象となったのは、単なるCLIの一部ではありません。VS Code、Visual Studio、Copilot Cloud Agent、Copilot Code Review、Copilot Studio、Microsoft 365アプリなど、複数の製品が共有するエージェント基盤です。最終的に本番コードは約83万行のRustとなり、128件のプルリクエストとして段階的に本番へ投入されました。

注目すべきなのは、AIエージェントがコード生成の大部分を担ったことです。従来なら複数の開発者が1〜2年かける規模の移行を、主担当の開発者1人が数か月で進めました。ただし、これは「AIに移行を依頼したら完了した」という話ではありません。設計、分割、品質保証、競合解消、リリース判断には、人間による継続的な監督が必要でした。

なぜNode.jsからRustへ移行したのか

TypeScriptとNode.jsは、CLIやTUIの開発において合理的な選択です。開発速度が速く、広い開発者層を確保でき、UIも構築しやすいという利点があります。

一方、同じランタイムをSDKやサーバー、複数言語のアプリケーションから利用する場合、Node.jsをプロセスごと同梱する構成が課題になりました。従来のSDKは、Copilot CLIをヘッドレスモードで子プロセスとして起動し、JSON-RPCで通信していました。

この構成では、SDK利用者ごとに次のコストが発生します。

  • Node.jsとV8の起動
  • JavaScriptの読み込み、解析、バイトコード生成
  • V8が使用するメモリ
  • SDKとランタイム間のプロセス間通信
  • CLIとホストアプリという複数プロセスの監視
  • Node.js側のクラッシュによるセッション停止

特にC#、Python、Go、Java、RustなどのSDK利用者にとっては、アプリケーション本体とは別にNode.js/V8を抱えることになります。サーバー環境では、起動時間、メモリ使用量、同一ホスト上で実行できるセッション数が重要です。

そこでGitHubは、ランタイムをUIから分離し、ネイティブライブラリとしてホストプロセスへ組み込める構成を目指しました。Rustは、低レベルの実行効率、明示的な所有権管理、C ABIとの相性、比較的少ない実行時依存といった条件に適合したと説明されています。

ここで重要なのは、TypeScriptが遅いからRustにした、という単純な話ではないことです。今回の要件は、Node.jsを使わずに埋め込めること、予測可能なリソース使用量、複数言語からのFFI利用、サーバー密度、信頼性でした。アプリケーションの制約が異なれば、適切な実装言語も変わります。

JSON-RPCを捨てずにインプロセス化した設計

Rust化によって、ランタイムには2つの利用経路が用意されました。

  • Node.jsのネイティブアドオンとして利用する経路
  • C ABIを通じて各言語のプロセス内に組み込む経路

SDK側では、C#のP/Invoke、JavaのJNA、Pythonのcffi、Goのネイティブ連携、Rustの動的ライブラリ読み込み、TypeScriptのネイティブブリッジなど、それぞれの言語に適したFFI機構を利用します。

興味深いのは、インプロセス化してもSDKの上位APIを大きく変更しなかった点です。C ABIのエクスポートは19個に抑え、実際のAPI呼び出しは従来と同じ双方向JSON-RPCのディスパッチ機構を通じて処理します。

つまり、JSON-RPCのバイト列がパイプやソケットを通る代わりに、同一プロセス内の関数呼び出しとコールバックを経由する構成です。

この設計には、次の利点があります。

  • 既存SDKのリクエスト相関やイベント処理を再利用できる
  • 6言語それぞれに数百個のFFIバインディングを作らずに済む
  • リモート接続や別プロセス実行にも同じプロトコルを利用できる
  • API追加のたびにABIの関数を増やす必要がない

一方で、インプロセスであってもシリアライズのコストは残ります。高スループットなローカル処理では、型付きの直接呼び出しや、JSONより密度の高いエンコーディングが有効になる可能性があります。今回の設計は、まず既存APIとの互換性と移行コストを優先し、必要なら将来ホットパスだけを最適化できる余地を残したものといえます。

ビッグバンではなく、インプレースで段階移行

大規模な言語移行には、主に2つの方法があります。新実装を別ブランチで完成させて一度に切り替える方法と、コンポーネント単位で既存実装を置き換える方法です。

GitHubが採用したのは後者でした。各プルリクエストで、TypeScriptの実装をRust呼び出しの薄いシムへ置き換え、元のコードを削除します。移行途中でもmainブランチはリリース可能な状態に保たれ、既存のE2Eテストが新しいRust実装を継続的に検証しました。

移行は、依存関係の末端から中心へ進められました。

  1. 純粋なロジックや小さなヘルパー
  2. ファイルシステムやシェル関連の処理
  3. 状態を持つサブシステム
  4. ツール、フック、認証、テレメトリ、永続化
  5. モデル連携とMCP
  6. 多くのサブシステムに接続するセッションオーケストレーション
  7. SDKのエントリーポイント

この順序には実務的な意味があります。I/Oや共有状態を持たない処理は、言語間の比較とテストが容易です。一方、セッション管理のように状態、イベント、コールバック、キャンセル処理が絡む部分は、最後に回して影響範囲を抑えます。

また、移行対象を常に「1コンポーネント」と考えるのではなく、純粋ロジック、状態所有、オーケストレーション、旧実装削除という複数の波に分けています。大規模なコードベースでは、論理的なコンポーネント境界と、実際に安全に移行できる境界が一致しないためです。

性能改善はどの程度だったのか

発表では、モデル推論やネットワーク遅延を除外した、ランタイム部分のエンドツーエンド測定が示されています。TypeScriptランタイムをNode.js上で別プロセス実行した構成と、Rustランタイムを比較した結果です。

シナリオ 移行前 Rust・別プロセス Rust・インプロセス
クライアント作成から1ターン実行 5.25秒 1.33秒 292ミリ秒
32ターンのセッション再開 5.64秒 1.52秒 264ミリ秒
10クライアントの同時ライフサイクル 12.34秒 4.18秒 742ミリ秒
1,000回の1ターンセッション 132.52秒 22.53秒 20.93秒

インプロセス構成では、最初のシナリオで約18倍、32ターンの再開で約21倍の改善が見られました。ただし、この数値を「Rustは常に18倍速い」と解釈するのは適切ではありません。起動、プロセス生成、JavaScriptの読み込み、イベント処理、永続化、終了処理など、今回のアーキテクチャ変更全体の効果だからです。

メモリ面では、10クライアントのバッチ処理で、移行前のプロセスツリーがベースラインから1,383MB増加したのに対し、Rustの別プロセス構成は247MB、インプロセス構成は126MBでした。サーバー上で同時実行セッション数を増やす場合、起動時間だけでなく、こうした常駐メモリの差が重要になります。

AIエージェントは何を担い、人間は何を担ったのか

今回の移行では、AIエージェントが大部分のコードを書きました。しかし、ログから見える実態は、AIが無計画にコードを大量生成したというものではありません。

エージェントの活動では、編集よりもファイル閲覧、検索、Gitの状態確認、ビルド、テストの比重が大きく、探索と変更の比率はおよそ10対1でした。大規模移行で重要なのは、コードを書く速度よりも、既存の挙動を理解し、変更範囲を特定し、差分を検証する能力です。

人間の主な役割は、次のように上位の判断へ移りました。

  • 移行後のアーキテクチャを決める
  • コンポーネントの境界と移行順序を決める
  • 挙動互換性の基準を定義する
  • エージェント間の競合を解消する
  • テストや互換性チェックを弱める変更を拒否する
  • リスクの高い設計とAPIをレビューする
  • リリース可否を最終判断する

AIエージェントは、実装と検証の量を大きく拡張しました。一方で、何を正しいとするか、どこまでを変更してよいかという判断は自動化されていません。

発生した回帰が示す、言語移行の難しさ

Rustのコンパイルが通っても、移行が正しいとは限りません。実際、発生した回帰の多くは、コンパイラが検出できない種類のものでした。

代表的な問題には、次のようなものがあります。

  • TypeScriptのnumberをRustで整数か浮動小数点数か誤って表現した
  • JavaScriptの||とRustのunwrap_orで空文字列の扱いが変わった
  • タイムゾーンや環境変数など、Node.jsが暗黙に提供していた情報を失った
  • キャンセル、破棄、所有権、コールバックのライフサイクルがずれた
  • SDKコールバックや組み込みツールの経路を移行から漏らした
  • 同期的なFFI呼び出しでNode.jsのメインスレッドをブロックした
  • Windows固有のプロセス生成フラグを引き継げなかった
  • リベースや競合解消の過程で実装やAPIを失った
  • 大きなイベントログを不要にコピーし、性能を低下させた

これらは、Rustの所有権モデルが悪いという話ではありません。むしろ、ソース言語に暗黙に存在していた契約を、移行先で明示的に再定義する必要があることを示しています。

たとえば、TypeScriptの型が同じnumberでも、実際のAPI契約では「整数でなければならないID」と「小数を含む経過時間」が混在していることがあります。Rust化では、型安全性が増す一方で、元の曖昧な意味を開発者が解釈しなければなりません。

E2Eテストは「移行のオラクル」として守る

今回の最も重要な教訓の一つは、E2Eテストを単なる回帰テストではなく、旧実装と新実装の振る舞いを比較するオラクルとして扱うことです。

移行対象のコードを変更するエージェントが、同時にテストを書き換えたり、スナップショットを更新したり、互換性チェックの例外を追加したりすると、誤った実装を正しいことにできます。

実務では、次のようなガードレールが有効です。

  • 移行前に意味のあるユーザーフローをE2Eテストで網羅する
  • E2Eテストの削除や変更を別の承認経路にする
  • SDKのスキーマ互換性チェックを自動化する
  • 旧実装と新実装の出力、イベント順序、エラーを比較する
  • ネイティブ境界、キャンセル、再開、終了処理を重点的に検証する
  • 移行対象と同じエージェントにテストの合否判定を全面的に任せない
  • 回帰が見つかったら、個別修正だけでなくルールや評価データへ反映する

「コンパイルが通る」は、構文、型、所有権の一部を確認する条件にすぎません。API契約、状態遷移、性能、外部環境との相互作用までは保証しません。

エージェントを複数動かすなら、競合管理が必要になる

GitHubは、複数のエージェントセッションを別々のワークツリーで動かし、親セッションが成果を統合する方式も活用しました。これは独立した領域を並列化するには効果的です。

しかし、隣接するコードや共有状態を同時に変更すると、エージェント同士が相手の意図を正しく理解できない場合があります。実際に、上下方向から同じ中心ファイルへ向かう2つのセッションが、互いの作業境界に介入する事例も発生しました。

ここから得られる設計上の教訓は明確です。

  • エージェントごとに変更可能なファイルと禁止範囲を明示する
  • 共有ファイルには所有者または調整役を置く
  • ブランチをまたぐ操作には人間または上位エージェントの承認を要求する
  • ビルドやテストの同時実行数を制御する
  • CPUやメモリを消費する処理にリースやキューを設ける
  • 「自律実行」の範囲と、例外的に停止すべき操作を定義する

エージェントを増やすだけでは、開発速度は比例して向上しません。並列性の上限は、コードの依存関係、レビュー能力、ビルド資源、統合プロセスによって決まります。

大規模移行を検討するチームへの実務的な示唆

今回の事例は、すべてのTypeScriptアプリをRustへ移行すべきだという主張ではありません。移行の判断では、次の観点を分けて評価する必要があります。

Rust化を検討しやすいケース

  • 複数言語から同じコアランタイムを利用したい
  • Node.jsやV8を各サービスに同梱する負担が大きい
  • 起動時間や定常メモリがサービス品質に直結する
  • C ABIや各言語のFFIを安定した境界にしたい
  • 高い同時実行数や長期運用で予測可能なリソース利用が必要
  • 既存のE2EテストとAPI契約が十分に整備されている

先に移行以外の対策を検討すべきケース

  • ボトルネックがモデル推論や外部I/Oにある
  • Node.jsのプロセス分離が信頼性上の利点になっている
  • ネイティブ境界を保守できる人員がいない
  • 互換性を検証するテストが不足している
  • 既存コードの責務分離が不十分で、段階移行の境界を作れない

特に重要なのは、翻訳と再設計を同時に行わないことです。まず既存挙動を保ったまま移植し、その後にRustの所有権、並行性、ゼロコピー、非同期処理を活かした再設計へ進む方が、問題の原因を切り分けやすくなります。

AI時代のリライトは「コード生成」ではなく「制御系」の設計になる

GitHubの事例が示しているのは、AIエージェントによって大規模な本番コード移行のコストが下がったことです。しかし同時に、開発者の仕事がなくなったわけではありません。

開発者は、コードを書く人から、複数のエージェントと検証システムを含む開発プロセスを設計する人へ役割を広げる必要があります。重要になるのは、プロンプトの巧拙だけではありません。

  • 変更範囲をどう分割するか
  • 正しさを何で判定するか
  • どの操作を自動化し、どこで止めるか
  • 競合するエージェントをどう調整するか
  • 回帰をどの層で検出するか
  • AIが誤った判断をしたとき、どう再発防止するか

今回のRust移行は、AIエージェントが大規模なリライトを現実的な選択肢に押し上げた事例です。ただし成功の本質は、AIに大量のコードを書かせたことではありません。段階的なリリース、安定したE2Eテスト、FFIを含む明確な境界、そして人間による設計と最終判断を組み合わせた点にあります。

AIエージェントによる開発を本番へ広げるなら、まず自動生成の量ではなく、安全に変更を受け入れ続けられる仕組みへ投資することが、最も再現性の高いアプローチになりそうです。

出典: GitHub Blog

タイトルとURLをコピーしました