Read OSS

Gemini CLI のアーキテクチャ:モノレポ全体マップ

中級

前提知識

  • TypeScript の基礎知識
  • Node.js プロジェクト構造への慣れ
  • モノレポの概念を理解していること

Gemini CLI のアーキテクチャ:モノレポ全体マップ

Google の Gemini CLI は、Gemini モデルをターミナルから直接使えるようにするオープンソースのエージェント型コーディングアシスタントです。ファイルの編集、シェルコマンドの実行、Web 検索、MCP サーバーとの連携など、多彩な機能を TypeScript で書かれた洗練されたコードベースが束ねています。ただし、Gemini CLI にコントリビュートしたり、コードを深く読み解いたりするには、まずコードベース全体の地図を頭に入れておく必要があります。本記事では、モノレポの構造・起動シーケンス・中心となる設定オブジェクト・2つのイベントシステムを順に案内します。

7 パッケージで構成されるモノレポ

Gemini CLI は npm workspaces を使ってコードを 7 つのパッケージに分割しており、それぞれの責務が明確に定義されています。

パッケージ 役割
packages/core バックエンドロジック全般:API クライアント、ツール、ポリシーエンジン、スケジューラー、フック、MCP、セーフティ
packages/cli React/Ink を使ったターミナル UI(インタラクティブ・非インタラクティブの両モード対応)
packages/sdk Gemini CLI を他アプリケーションに組み込むためのプログラマブル API
packages/a2a-server 実験的な Agent-to-Agent プロトコルサーバー
packages/devtools ブラウザベースのデバッグインスペクター
packages/vscode-ide-companion IDE 連携のための VS Code 拡張機能
packages/test-utils 共有テストインフラ

packages/core/src/index.ts のバレルエクスポートを見ると、core がいかに広大かがよくわかります。config・policy・tools・scheduling・MCP・hooks・agents・telemetry など、270 行を超える re-export が並んでいます。core はコードベースの重力の中心です。

graph TD
    subgraph Consumers
        CLI[packages/cli<br/>Terminal UI]
        SDK[packages/sdk<br/>Programmatic API]
        A2A[packages/a2a-server<br/>Agent-to-Agent]
        DEV[packages/devtools<br/>Debug Inspector]
        VSC[packages/vscode-ide-companion<br/>VS Code Extension]
    end
    
    CORE[packages/core<br/>Backend Logic]
    TEST[packages/test-utils<br/>Test Infrastructure]
    
    CLI --> CORE
    SDK --> CORE
    A2A --> SDK
    DEV --> CORE
    VSC --> CORE
    CLI -.-> TEST
    CORE -.-> TEST

ヒント: Gemini CLI を初めて読むときは packages/core/src/index.ts から始めましょう。import のグループ分け(config・コアロジック・ツール・サービス・フック)が、そのままアーキテクチャの層構造を映しています。

core と cli の分離

Gemini CLI で最も重要なアーキテクチャ上の判断は、ヘッドレスバックエンドである core とターミナル UI である cli をきれいに分離していることです。この分離により、三つの異なる入力インターフェースが同じバックエンドを共有できます。

  1. インタラクティブ CLI — ターミナルに描画される React/Ink アプリケーション
  2. 非インタラクティブ CLI — パイプ入力や CI/CD 向け
  3. SDKGeminiCliAgent を通じたプログラマブルな組み込み

packages/sdk/src/index.ts は驚くほどシンプルで、re-export が 5 つあるだけです。重い処理はすべて core のエージェントセッション・ツールレジストリ・クライアント層が担っており、SDK はそれらを GeminiCliAgent / GeminiCliSession という使いやすい API に包んでいます。

この分離にはパフォーマンス上のメリットもあります。CLI はインタラクティブモードに入るときのみ React/Ink を遅延インポートするため、非インタラクティブなパスを高速に保てます。

起動シーケンス:Shebang からインタラクティブモードまで

Gemini CLI を読み解くうえで、起動フローの理解は欠かせません。エントリーポイントである packages/cli/index.ts は一見シンプルで、グローバルな未捕捉例外ハンドラー(Windows 上での node-pty の既知の競合状態を意図的に抑制しています)をインストールして main() を呼び出すだけです。

本当の複雑さは packages/cli/src/gemini.tsx に集まっています。main() 関数は複数のフェーズに分かれた起動処理を調整します。

flowchart TD
    A[Entry: index.ts] --> B[main&#40;&#41;]
    B --> C[Setup handlers & patch stdio]
    C --> D[Load settings & worktree]
    D --> E[Parse arguments]
    E --> F[Configure DNS & auth type]
    F --> G[Load CLI config &#40;partial&#41;]
    G --> H[Refresh auth / admin settings]
    H --> I{Sandbox needed?}
    I -- Yes --> J[Relaunch in sandbox]
    I -- No --> K[Relaunch in child process]
    J --> L[Exit parent]
    K --> M[Full config load]
    M --> N{Interactive?}
    N -- Yes --> O[startInteractiveUI&#40;&#41;]
    N -- No --> P[runNonInteractive&#40;&#41;]

いくつかの設計判断が際立っています。

二段階の config 読み込み。 config は二回ロードされます。サンドボックス判定の前に一度部分的にロードし(330 行目)、サンドボックスに入った後で完全にロードします(454 行目)。これは、認証をサンドボックス化より先に行う必要があるため(サンドボックスは OAuth リダイレクトをブロックします)であり、一方でエクステンションはサンドボックス化の後まで読み込まないようにするためです。

サンドボックス再起動。 サンドボックスが有効かつ現在サンドボックス内にいない場合(!process.env['SANDBOX'])、プロセスはコンテナまたは macOS の seatbelt 内で自分自身を再起動します。ユーザーには見えませんが、セキュリティ上重要な処理です。

遅延 UI 読み込み。 167〜185 行目startInteractiveUI 関数は、重い interactiveCli.js モジュールを動的にインポートします。これにより、非インタラクティブな実行時には React/Ink が依存グラフに入り込まないようにしています。

Config god オブジェクトと AgentLoopContext

すべての Gemini CLI セッションの中心にあるのが Config クラスです。約 3,700 行にわたるこのクラスは McpContextAgentLoopContext の両インターフェースを実装しており、packages/core/src/config/config.ts#L736 で定義されています。保持している内容は以下の通りです。

  • ツール・プロンプト・リソースのレジストリ
  • MCP および A2A クライアントマネージャー
  • サンドボックスマネージャーとポリシーエンジン
  • モデルルーターサービスとコンテンツジェネレーター
  • フックシステム・スキルマネージャー・ファイル検索サービス
  • セッション状態・テレメトリー設定・IDE モードなど
classDiagram
    class Config {
        +toolRegistry: ToolRegistry
        +mcpClientManager: McpClientManager
        +sandboxManager: SandboxManager
        +modelRouterService: ModelRouterService
        +policyEngine: PolicyEngine
        +hookSystem: HookSystem
        +skillManager: SkillManager
        +contentGenerator: ContentGenerator
        +getMessageBus(): MessageBus
        +getGeminiClient(): GeminiClient
        +initialize(): Promise
    }
    
    class AgentLoopContext {
        <<interface>>
        +config: Config
        +promptId: string
        +parentSessionId?: string
        +toolRegistry: ToolRegistry
        +promptRegistry: PromptRegistry
        +resourceRegistry: ResourceRegistry
        +messageBus: MessageBus
        +geminiClient: GeminiClient
        +sandboxManager: SandboxManager
    }
    
    Config ..|> AgentLoopContext
    Config ..|> McpContext

AgentLoopContext インターフェースは、単一のエージェントターンに絞ったスコープ付きビューを提供します。GeminiClientScheduler などのコンポーネントに Config 全体を渡す代わりに、そのスコープで必要なレジストリ・メッセージバス・サンドボックスマネージャーだけを持つ AgentLoopContext を渡します。サブエージェントでは各エージェントが独自の派生コンテキストを持つため、この設計は特に重要です。

ヒント: AgentLoopContext を受け取るコードを読むときは、context.config で常に完全な Config オブジェクトに戻れることを頭に置いておきましょう。インターフェースはあくまで絞り込みの慣習であり、ハードな境界ではありません。

二つのイベントシステム:coreEvents と MessageBus

Gemini CLI は、用途の異なる二つのイベントシステムを使い分けています。

CoreEventEmitter — グローバルな横断的関心事

coreEvents シングルトン は、グローバルな通知を扱う型付き EventEmitter です。CoreEvent 列挙型には UserFeedbackModelChangedConsoleLogRetryAttemptMcpProgressQuotaChanged などのイベントが定義されています。

注目すべき機能として、CoreEventEmitter はイベントのバックログ機能を実装しています。リスナーが登録される前にイベントが発行された場合(起動中によく起こります)、最大 10,000 件のキューにためておき、リスナーが現れた時点でまとめて流します。これにより、起動初期の警告が失われるのを防いでいます。

MessageBus — ツール確認の Pub/Sub

MessageBus は別のパターンを担います。ツール確認のためのスケジューラーと UI 間のリクエスト・レスポンス通信です。ツールの呼び出しにユーザーの承認が必要な場合、スケジューラーが TOOL_CONFIRMATION_REQUEST を発行します。バスはポリシーをチェックし(allow/deny/ask_user)、自動で解決するか UI に転送するかを判断します。

flowchart LR
    S[Scheduler] -->|TOOL_CONFIRMATION_REQUEST| MB[MessageBus]
    MB -->|PolicyEngine.check| PE{Policy Decision}
    PE -- ALLOW --> AR[Auto-resolve confirmed]
    PE -- DENY --> AD[Auto-resolve denied]
    PE -- ASK_USER --> UI[Forward to UI]
    UI -->|TOOL_CONFIRMATION_RESPONSE| MB
    MB --> S

46〜72 行目derive() メソッドは、サブエージェント用に名前空間を分けた子バスを作成します。派生バスは確認リクエストにサブエージェント名をプレフィックスとして付与し、同じイベントインフラを共有しながらも、親と子のエージェントフローが干渉しないようにします。

コードベースの読み方:どこから始めるか

新しくコントリビューターになる方向けの、実践的な読む順序を紹介します。

ステップ ファイル 読む理由
1 packages/cli/src/gemini.tsx 起動シーケンスを把握する
2 packages/core/src/config/config.ts Config が何を保持しているか確認する
3 packages/core/src/core/client.ts エージェントループのオーケストレーター
4 packages/core/src/core/turn.ts ストリーミングイベントの仕組み
5 packages/core/src/scheduler/scheduler.ts ツールの実行方法
6 packages/core/src/policy/policy-engine.ts セキュリティモデル
7 packages/core/src/tools/definitions/coreTools.ts ツールスキーマの定義

知っておくと役立つ命名規則は以下の通りです。

  • core/ — エージェントループ層(client・turn・chat・prompts)
  • tools/definitions/ — モデルファミリー別に分割されたツールスキーマ
  • scheduler/ — ツール実行のオーケストレーション
  • confirmation-bus/ — MessageBus システム
  • hooks/ — 5 コンポーネント構成のフックシステム
  • mcp/ — MCP クライアントと OAuth
  • policy/ — ルールベースのポリシーエンジン

ツール定義と実行の関係はビルダーパターンに従っています。coreTools.tsBaseDeclarativeTool を使って宣言的なスキーマを定義し、スケジューラーが実際の実行のために ToolInvocation オブジェクトをインスタンス化します。この仕組みは第 3 回の記事で詳しく解説します。

次回の記事では、ユーザーのプロンプトをストリーミングのツール呼び出しとレスポンスに変換する三層アーキテクチャ、すなわち GeminiClientTurnGeminiChat で構成されるエージェントループの内部に深く踏み込みます。