Reading gemini-cli #2
さて今日は core を眺めようねー。
Code Assist
- CLI 側からよくアクセスされていたのは GeminiClient. こいつは core 側の色々な機能の facade だと思えば良さそうである。
- Facade されているオブジェクトの一つ ContentGenerator インターフェイスを見てみると・・・異なる API endpoints の差分を吸収している。具体的にはなぜなら API キーは GCP を使う勢は普通の Gemini API が使えるが、Google Sign-in 勢は code assist という別の API に依存しているからである!わー大変残念。
- Code Assist というのは VS Code などのプラグインで、全然話題にならなかった。一方
gemini-cli
はそれなりに buzz り、その一因は google sign-in で使える手軽さだったと思うので、統一 API の登場をまたず重複を受け入れる判断はビジネス的には正しかったと言えよう。絵に書いたような tech debt のケーススタディーとして記憶しておいてあげます。がんばった。 - Code Assist API/SDK というのは公式には存在しないので、types.ts を見ると色々なものを社内からコピーした様子が伺えて趣深い。(なお公平を期すために書いておくと、多くの型情報は genai SDKから再利用されている。)
- いやーがんばってるね。
- Code Assist というのは VS Code などのプラグインで、全然話題にならなかった。一方
GeminiChat
- さてゴシップでそれた脇道から復帰し続きを読むと・・・
ContentGenerator
は GenAI SDK の API の subset だということがわかる。つまり Code Assist を公式 API の signature にあわせる bridge だった。
- もう一つのメジャーなクラスは GeminiChat. これは ContentGenerator を wrap してオンメモリの history を保持したりする。ところで JS いつの間にか
yield
が入っていたのだね1。チャットの結果はストリームなので大活躍している。- しかしこんなにコードいっぱい書かないとダメとは随分 low-level だな・・・とおもっていたら、
DISCLAIMER: This is a copied version of ... with the intention of working around a key bug
とのことで、普通は SDK を使えば良い。
- しかしこんなにコードいっぱい書かないとダメとは随分 low-level だな・・・とおもっていたら、
- さて GeminiClient に戻ると、
--full-context
というフラグをつけるとディレクトリのファイルを全部突っ込む機能があり、その処理がある。1M token 贅沢に使ってこうな、という話か。 - あとは built-in のツールを登録したりとか。
prompts.ts
- お、System Prompt など各種 prompt の定義された prompts.ts があった!見どころの一つ(のはず)。
- Mobile App: Compose Multiplatform (Kotlin Multiplatform) or Flutter (Dart) using Material Design libraries and principles, when sharing code between Android and iOS. Jetpack Compose (Kotlin JVM) with Material Design principles or SwiftUI (Swift) for native apps targeted at either Android or iOS, respectively.
- シレっと Flutter 推すのやめてくれませんかね・・・
- Parallelism: Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
-
そんなことできるんだ!たしかにサーチが並列に動いてる時あるよな。実装みておきたいところ。
-
few-shot 的なものは
<example>
タグで囲んでいる。へえ。 -
plan を立てろと序盤に書いてあり、Plan 提案例が
<example>
に入っている。 しかしそこでは plan を箇条書きして許可を求めあとは agentic に動いてしまうので、ステップごとに一旦止まるとか計画をファイルに書くとかしたい場合はGEMINI.md
で上書きしないとダメそうだな。- が、よくみると環境変数
GEMINI_SYSTEM_MD
を使うと.gemini/system.md
or お好みのパスから上書きできるらしい。いいじゃない!自分の Obsidian helper はコード関係の system prompt まったく必要ないからな。
- が、よくみると環境変数
-
そのほかヒストリ圧縮機能のプロンプトもここにある。君はツールじゃないのかい? とおもったが、history のような内部構造はツールからはアクセスできないな。
-
さて GeminiChat に戻ると
generateEmbedding()
なんてのがあるね。何に使うだろうか -> 使われていない。SDK コピペの残骸か。 -
GeminiChat は切り上げ他のファイルも眺めましょう。
-
はて、ところで昨日は gemini-cli はクライアント・サーバだと書いたけれど、全然クライアントサーバじゃないね。UI とロジックが切り分けてあるだけで。まあその方が自然だと思うけれど。
- architecture.md を見直すと backend とは言っているが server とは言っていないな。私の思い違いでした。
logger.ts
- core/src/index.ts をじっとにらみ・・・
- logger.ts いってみっか。
- メッセージをファイルに書きます。次回起動時に続行できます。
- へーと思い
.gemini
にある logs.json を見ると、ユーザ(自分)のメッセージしかない。はて?コードを確認すると、たしかにユーザのログしか書いてない。なんか嬉しいのそれ? - というと、user input の UI から “↑” キーで履歴を戻るためのものらしい。それめっちゃ UI の機能じゃん・・・。
- それとは別に、オンメモリのフル履歴を checkpoint する機能もこのファイルに定義されている。それ全然別物だろ・・・ダメだなこのコードは・・・。
sendMessageStream
- 次に turn.ts. これは Chat へのメッセージ送信を微妙なかんじにラップする
Turn
クラスを定義する。Turn.run()
メソッドが生えている。 - ここでは tool call をハンドル…はせず、tool call request を抜き出して呼び出しもとから使えるようラップする。
Turn
は client.ts のGeminiClient.sendMessageStream()
メソッドから呼ばれている。そしてよく見るとこのsendMessageStream
がいわゆる agentic loop ですね。Turn
を介してリクエストを送る。すると Tool call が返ってきたり来なかったりするので、それを必要に応じて処理する。- このコードからわかることは、ツールの並列呼び出しはモデルがネイティブにサポートしているのだね。複数の tool call request が返ってくる。
- というか、tool call にせよその他の返事にせよ、全部
ServerGeminiStreamEvent
として yield され、呼び出し側にストリームされる。(つまりsendMessageStream()
は generator である。)ServerGeminiStreamEvent
は turn.ts に定義されている。- これが client.ts の呼び出し側から参照されるんだから依存関係はグチャグチャだな・・・。
- したがって tool call をハンドルするのは GeminiClient の利用者である。しかしどうやってサーバに結果返すの?
- というのを調べるべく、長い旅が始まります。
CoreToolScheduler
- Tool call のための入口は
CoreToolScheduler
です。- coreToolScheduler.ts
- そしてついになる UI 側の useToolScheduler.ts というのがある。
- Core が tool call の状態遷移を管理して色々なイベントを発行し、UI 側がそのイベントに応じて仕事をして返す、というかんじ。
- 仕事とは、UI の表示のみならず、実行許可の確認などもある。
- そういう確認がおわるとツールを実s行し、全部のツールの実行が終わるとコールバックを呼びます。
ToolRegistry
and MCP
- ツールは tools.ts にインターフェイスがあり、tool-registory.ts に登録される。built-in tool はどこか最初の方で登録されていた気がする。MCP はどうなのかな?
ToolRegistory
は初期化時に Config を呼んで MCP から tool を登録する。- そのコードは mcp-client.ts で、これは MCP SDK (というのがある) を gemini-cli 内の Tools のインターフェイスにつなぐという仕事をしています。めっちゃ相互依存してて失神しそうなひどいコードだが、これが若さの勢いなんだよ。
- なお MCP 標準 は MPC server が tools 以外にも色々なデータなどを提供できることになっているが、みたかんじ
gemini-cli
は tools しかサポートしていない。まあ本家たる Claude Desktop でも実装されてない機能があるとどこかで読んだので、そういうものなのでしょう。 - まあ MCP まわりにそんなに驚きはないね。あっても困るが。
checkNextSpeaker
さてツールの結果が出揃ったら(あるいはツールが必要なかったら) dMessageStream は checkNextSpeaker
というのを呼びます (nextSpeakerChecker.ts。)
- というのも、LLM からの返信は必ずしもユーザに戻るわけではなく、たとえばツールの結果をうけて LLM が仕事を続行したいかもしれないわけだ。
- で、どう実装されているかというと、Gemini Flash に訊きます!たしかに NLP 得意だよな君たち!
今日はここまで。というかだいたい気が済んだので code reading は終了。これで何らかの疑問が生じた時にコードを眺めて調べるくらいはできることでしょう (React 部分除く。つまり半分はわかってない。Sigh.)
後半感想:
- コードは悪く言えば雑、良く言えば勢いがある。なんらかの巨大な野望に向かって慎重にデザインして巨大建築を作る・・・という感じはゼロで、競合にキャッチアップするためにちゃちゃっと実装してる雰囲気。
- 良くも悪くも伝統的な “Google っぽさ” はゼロ。
- それは、いいんじゃないですかね。野望はモデルの方にまかせておけば。この大企業にジャカジャカ雑コードを書く余地が残っているとは思わなかったですよ。
- JS の async/await/generator がバリバリに使われており、TS でイベントの型をきちんと定義したりもしており、型付非同期言語の良さが生きているなと感心する。
- Ink は差し置いても、これを TS 以外で書くのはそんなに嬉しくない気がする。Rust … がどうかはしらないが、たとえば Python だと辛いんじゃないか。async/await/generator 全部あるけど全部出来が悪いからな。
- まあ、これは Claude Code を作った人たちが偉かったね。Codex は Rust rewrite をしているけど、どうかねー。
- CLI だから作るのは割と簡単なのかなとおもったが、全然そんなことはなくてコードは結構いっぱい書かれていた。というのは、
- CLI というよりはターミナルアプリなので、UI は割と入り組んでいる。(さわればわかりますね。Claude Code も同様。) もちろん「クラウドのサーバとブラウザのJS」という分離はないので、そこはラクなはず。
- モデルの制限とか SDK のバグとかが結構あるが、直るのを待ったりせずテキトーにワークアラウンドしているケースが多い。このへんは現在進行系の分野ですねーというかんじ。
- UI の要件が色々あるので、コアも高尚な「ドメインモデル」というより UI 部分を event stream consumer および callback に追い出したループ、みたいになっている.しかも flat なループではなく、ツールとかの都合で結構ネストしたりして大変。もうちょっとなんとかならなかったのかと思わなくもないが、そこを勢いで押し切れるのが JS+TS の力のような気もする。
- こうした現実を考えると、今の所 model agnostic にいいものを作るのは大変そう。これは前にも思ったことだけれど, まだ色々荒削りなので抽象化をするのは早いってことじゃないかな。cline とか goose とかがんばってるけれど、しばらくは claude code なり gemini-cli なり model 固有のツールを使うほうが現実的に思える。
- もっともツールを作る人たちは今のうちから作っておかないと分野が成熟した時に出遅れてしまうので、開発を進めていくのが悪いとも思わないけれど。
後発ゆえの必然とはいえオープンソースにしたのは偉かったね。ある程度機能が固まってきた暁にはつよつよエンジニアを連れてきて tech debt を返済のうえ素敵な client-side agentic platform に仕立てていっていただきたいものです。
個人的には「エージェントアプリつくるのこういうかんじかー」と大変勉強になりました。個人がおもちゃとして agent をつくるなら、もっと用途を限定してデザインを簡素化する方法を考える必要があることでしょう。
-
10年前からあるらしい。 ↩︎