私はイースターエッグが大好きだ。
VSCode で GitHub Copilot を使っていると偶に遭遇する「ハムスターに賄賂を渡しています」というメッセージ。
ソースコードを見てみると、抽選ロジックが書かれていることが分かる。というわけで、この記事ではこの一文がどこから湧いて出るのか、そして当選確率はいくつなのかなどを調べていく。
イースターエッグの定義
chatThinkingContentPart.ts の中に、以下のような配列がある。
/** Easter-egg loading messages, used ~1 in {@link FUN_WORKING_MESSAGE_RATE} picks. */
const funWorkingMessages = [
// Generic
localize('chat.working.fun.1', "Bribing the hamster"),
localize('chat.working.fun.2', "Reticulating splines"),
localize('chat.working.fun.3', "Untangling the spaghetti"),
localize('chat.working.fun.4', "Communing with the codebase"),
// Minecraft
localize('chat.working.fun.minecraft.1', "Mining diamonds"),
// Microsoft
localize('chat.working.fun.ms.1', "Summoning Clippy"),
];
由来が分からないものがあったので Copilot に調べさせてみた。
| ID | 原文 | 日本語訳(直訳) | 意味と由来 |
|---|---|---|---|
chat.working.fun.1 |
Bribing the hamster | ハムスターに賄賂を渡しています | 「コンピューターの中ではハムスターが回し車を回して動力を作っている」という古くからある冗談を踏まえたものと思われます(推測) |
chat.working.fun.2 |
Reticulating splines | スプラインを網状化しています | 1993 年の『シムシティ 2000』のロード画面に出た架空の処理名。ロード画面ジョークの代表例として知られます |
chat.working.fun.3 |
Untangling the spaghetti | スパゲッティをほぐしています | 制御フローが絡み合って追跡困難なコード、いわゆるスパゲッティコードを指した表現 |
chat.working.fun.4 |
Communing with the codebase | コードベースと交信しています | commune with は自然や霊的な対象と心を通わせる、という意味合いの語 |
chat.working.fun.minecraft.1 |
Mining diamonds | ダイヤモンドを採掘しています | Minecraft のダイヤモンド鉱石は地下深くにあり入手に手間がかかる。 |
chat.working.fun.ms.1 |
Summoning Clippy | Clippy を召喚しています | Clippy は Microsoft Office 97 から 2003 に搭載された Office アシスタント Clippit の通称 |
しかも localize() を通しているので、翻訳対象となっている。
抽選確率は 1/50
$$P(\text{当選}) = \frac{1}{50} = 0.02$$
当選したら、6 候補から一様ランダムで 1 つ。つまり「ハムスターに賄賂」単体の確率は
$$P(\text{ハムスター}) = \frac{1}{50} \times \frac{1}{6} = \frac{1}{300} \approx 0.33%$$
300 回に 1 回。それなりにレアなのに、なぜかよく見る気がするのは何故?
外れた 98% の場合は、真面目なプールから抽出される。
export const defaultThinkingMessages = ['Thinking', 'Reasoning', 'Considering', 'Analyzing', 'Evaluating', 'Working'];
const terminalMessages = ['Executing', 'Running', 'Processing'];
const toolMessages = ['Processing', 'Preparing', 'Loading', 'Analyzing', 'Evaluating'];
しかもこの通常プールからの抽出は splice を使った非復元抽出になっている。
const index = Math.floor(Math.random() * pool.length);
return pool.splice(index, 1)[0]; // 引いたら山札から抜く
つまり「分析中…分析中…分析中…」と同じ言葉が連続しないよう、山札を使い切るまで重複させない設計なのだ。ここだけ妙に真面目である。
抽選は「1 応答に 1 回」ではない
この抽選は、表示する作業ラベルを選び直すたびに走る。ツール呼び出し回数と一対一ということでもない。
抽選の入口 getRandomWorkingMessage() を呼ぶのは、chatThinkingContentPart.ts の中の次の 4 箇所だ。
| # | 呼び出し元 | いつ | カテゴリ |
|---|---|---|---|
| 1 | initContent() |
思考パートの DOM を初期化するとき | Thinking |
| 2 | setupThinkingContainer() |
新しい思考ブロックが始まったとき | Thinking |
| 3 | appendItem() |
ツール呼び出しや編集項目が追加されるたび |
Terminal または Tool
|
| 4 | materializeLazyItem() |
折りたたまれていた項目が展開で実体化されたとき |
Terminal または Tool
|
そして、別ファイルにもう 1 箇所ある。 chatProgressContentPart.ts の pickWorkingLabel()
function pickWorkingLabel(elementId, configurationService) {
// ...1.2 秒のデバウンス処理...
const fun = maybePickFunWorkingMessage(configurationService);
const label = fun ?? (() => {
const pool = buildPhrasePool(defaultThinkingMessages, configurationService);
return pool[Math.floor(Math.random() * pool.length)];
})();
// ...
}
こちらは応答開始直後に出る「作業中」行(ChatWorkingProgressContentPart)を担当している。
-
WORKING_LABEL_MIN_DWELL_MS = 1200というデバウンスがある。ストリーミング中の再描画でラベルがチカチカしないよう、1.2 秒は同じ文言を維持する - 通常プールの取り方が
spliceではなく単純な添字参照(非復元抽出ではない)
つまり ハムスターの生息地は計 5 箇所(イースターエッグ抽選を起動し得る呼び出し経路)ということになる。初期化や新しい思考ブロックでも抽選され、遅延項目は追加時と実体化時の両方で抽選される場合がある。一方、スピナーが存在しない場合や、pickWorkingLabel() が直前のラベルを再利用する場合は抽選されない。このため、実際の抽選回数をツール呼び出し回数だけから決めることはできない。
ハムスターを封印する
VSCode v1.110 以降、推論中またはツール呼び出し中に表示される読み込みテキストをカスタマイズできるようになった。定義済みのカスタムフレーズを使用して、既存のデフォルトフレーズを補完(replace )したり、カスタムフレーズを既存のデフォルトフレーズに追加(append)したりできる 。
設定 ID は chat.agent.thinking.phrases となっている。settings.json に以下のように書けば封印完了だ。
{
"chat.agent.thinking.phrases": {
"mode": "replace",
"phrases": ["処理中", "解析中", "検証中"]
}
}
逆に自分でハムスターを飼う
{
"chat.agent.thinking.phrases": {
"mode": "replace",
"phrases": ["ハムスターに賄賂を渡しています"]
}
}
この場合、100% ハムスターを召喚できる。
天才: 待ち時間を「ダッシュボード」に変えてしまった人がいる
ここまでは「ハムスターをどう出すか」という、我ながら生産性のない話だったが、世の中にはこの設定項目の使い道を根本から考え直した人がいた。発想の転換である。
Copilot が考えている間、深淵を覗き込みながら「エージェントに仕事を奪われるのでは」と物思いにふけるかわりに、その無駄な時間でもっと有益なことができるのでは。
(拙訳)
chat.agent.thinking.phrases は結局ただの文字列配列である。ならば何を入れてもいい。ライブデータを入れればダッシュボードになる。
何が流れてくるのか
| ソース | 表示例(README より) |
|---|---|
| RSS / Atom | Copilot code review now accounts for over 20% of all code reviews on GitHub... — The GitHub Blog (2d ago) |
| 株価(Yahoo Finance) | BTC - USD $66,978.23 ▼ 2.02% |
| Hacker News | (記事タイトル)— Hacker News @ravenical 378 pts (17h ago) |
| 地震(USGS) | M4.2 — 12 km NE of Ridgecrest, CA — USGS (38m ago) |
| 天気(NOAA/NWS) | 77°F, Mostly Cloudy, Wind E 50 mph — Fort Lauderdale, FL |
| GitHub 活動 | fix devtools entrypoint — vscode +1/-1 @deepak1556 (4h ago) |
| 任意の JSON API | フィールドマッピングを指定すれば何でも |
静的なフレーズパックも同梱されている。
| パック | 件数 |
|---|---|
| VS Code Tips(mac / Windows / Linux 別の文面を持つ) | 80 |
| TypeScript Tips | 124 |
| JavaScript Tips | 100 |
| Ruby Tips | 108 |
| League of Legends ロード画面 Tips | 100 |
| World of Warcraft ロード画面 Tips | 109 |
| 名言集 | 1,614 |
FPS ゲームなどでは、ロード画面にゲームのヒントや名言が表示されることがあるが、まさにコレと同じアイデアである。
どうやってやっているかというと、動的に chat.agent.thinking.phrases を書き換えていた(笑
ついでに見つけた真面目な実装
イースターエッグを追いかけていたら、同じファイルに「思考セクションのタイトルを LLM で要約生成する」処理も見つけた。generateTitleViaLLM() である。
- モデルは
copilot-utility-smallを指定(軽量モデルで要約) - タイムアウトは 5 秒(
CancellationTokenSourceでキャンセル) - プロンプトに「ツール名を絶対に出力するな」というルールが明記されている
- 「Edited X and used Replace String in File」→「Modified X」に直せ
- 生成結果は
StorageScope.PROFILEに 7 日間・最大 1000 件キャッシュされる - 失敗したら
setFallbackTitle()で「Finished with N steps」に落とす
あの「〇〇を更新しました」という折りたたみタイトルは、裏で小さい LLM がもう 1 回走って要約している。1 文・10 語未満・過去形の動詞で始めること、という指定まで入っている。
