Omoikaneというブラウザを作っている。最近は、複数の端末や実行環境で動くAIエージェントを、cc-sakuraというチャットシステムにつないで開発を進めている。
私がそれぞれのAIへ同じ説明を配って、返事を別のAIへコピーするのは面倒だった。あるAIが修正している間に、別のAIが同じ場所を調査していることもある。テストが落ちたとき、その失敗が既知なのか、新しく入れた変更によるものなのかを、毎回人間が整理するのもしんどい。
だったら、AI同士で直接話してもらえばよい。
出発点はそれくらい単純だった。ただ、実際に運用すると、メッセージが送れるだけでは開発はうまく回らない。誰が何を引き受けたのか、今動いているのか、何を待っているのか、どの結果を根拠に判断したのか。そのあたりを会話の中で扱えるようにする必要があった。
この記事は、2026年11月3日時点の運用を振り返ったものだ。参加環境のハンドルネームは伏せ、実装担当、独立検証担当、実機担当といった役割で説明する。役割は固定ではなく、作業や空き状況によって変わる。数値や進捗は確認した記録の時点のもので、この記事のために全テストを再実行したわけではない。
人間が全メッセージの中継点になるのをやめたい
複数のAIを使うとき、最初に思いつくのは仕事を分けることだ。片方に実装、もう片方にレビューを頼めば、自然に並列化できそうに見える。
ところが、二つの会話が独立していると、変更の前提がすぐにずれる。実装側は新しい差分を見ているのに、レビュー側は一つ前の差分を読んでいたりする。実装側がテストを走らせている横で、別の環境も同じ重いテストを始め、結果としてどちらも遅くなることもある。
人間が全部を把握して指示を出すなら、それでも回る。しかし、仕事が増えるほど、私が伝言係として必要になる。AIがコードを書く速度が上がっても、私が会話を読み、要約して、別の会話へ貼る速度は上がらない。
cc-sakuraでやりたかったのは、この中継を減らすことだった。実装したAIが変更点と検証条件をroomへ置く。別のAIがそれを読んで検証し、結果を同じ場所へ返す。私が知りたいときは、roomを読んで状況を説明してもらう。
私自身はシステムオーナーで、常に実装を担当する人ではない。そのため、開発者向けの確認を何でも私へ投げないようにも伝えている。通常の調査や担当調整は参加者同士で進め、費用、公開範囲、優先度など、私の判断が必要なところで呼んでもらう。
cc-sakuraは、まず共有の会話場所である
cc-sakuraは、中央サーバー型の小さなチャットシステムだ。各エージェントはroomに参加し、メッセージを送り、履歴を読む。CLI、HTTP API、MCPから同じ会話へアクセスできる。
サーバーはRustで実装し、会話はSQLiteへ保存している。参加者にはUUIDの識別子と表示名があり、参加時に発行されたsessionを、そのroomでの操作に使う。roomは会話の場所、sessionは秘密の認証情報で、別のものだ。
大まかな構成は次のようになる。
flowchart LR
A["実装を進めるAI"] -->|投稿・履歴取得| S["cc-sakuraサーバー"]
B["独立検証するAI"] -->|投稿・履歴取得| S
C["実機で調べるAI"] -->|投稿・履歴取得| S
S -->|保存| D[(SQLite)]
S -->|履歴・新着| V["HTMLクライアント"]
V -->|原文・翻訳・背景説明| H["人間"]
このサーバーが作業を自動で分解したり、担当を強制的に決めたりするわけではない。担当の受諾や優先順位の調整は、エージェントがroomで行う。GitHubのIssueやPRも引き続き使う。roomは細かな連絡や途中経過を扱い、後から参照したい仕様、差分、検証結果はIssueやPRへ残す、という使い分けになっている。
この区別は大事だと思っている。チャットを作っただけで、開発全体を管理する仕組みが完成したことにはならない。会話を共有できるようになった結果、今度はその会話をどう使うかが問題になった。
AI同士の通信を、人間向けの日本語に固定しない
最初から持っていた要望がある。AI同士は、最も効率のよい形でコミュニケーションしてほしい。
毎回、挨拶と背景説明を含む長い日本語を書く必要はない。同じ内容が短いJSONで伝わるなら、それでよい。英語でも、略記でも、共有した独自形式でも構わない。ただし、人間が必要になったときには意味を確認できるようにしたい。
cc-sakuraのpayloadは任意のJSONを受け付ける。例えば次は説明用に作った形式で、実際の投稿の引用ではない。
{
"task": "worker-bootstrap",
"state": "waiting",
"blocked_by": "full-test",
"next": "independent-review",
"reply_to": 120
}
繰り返す表現を共有したければ、codecという辞書を登録できる。短いキーを人間向けのテンプレートへ対応付ける仕組みで、同じ説明を毎回送らずに済む。ただし、辞書の登録も、人間向けの説明文を投稿に付けることも必須ではない。READMEの通信形式の説明に、この方針を書いている。
ここでいう「効率がよい」は、単に一投稿の文字数が少ないという意味ではない。暗号のように短くしても、受け手が解釈を間違えたり、辞書の交渉に何往復もかかったりすれば、全体としては高くつく。前の投稿を番号で参照する、同じ条件を何度も貼らない、変更した点だけ送る、といった地味な工夫のほうが役立つ場合もある。
現状のroomでも、構造化JSONの中に自然言語の説明が入る。AI専用の新言語を発明して、それによって通信量が何割減った、といった測定結果があるわけではない。形式を固定せず、その場で総コストの小さい伝え方を選べるようにしている、という話だ。
人間が読むときに、手元のAIへ解釈を頼む
AI同士の通信を自由にすると、人間が読めなくなるのではないか。そこで、読む側に翻訳を置いた。
cc-sakuraのサーバーが常時翻訳するのではなく、人間向けのクライアントが、手元のCodex CLIやClaude Codeを起動して解釈を依頼する。既存の辞書や説明文で読めるものはそれを使い、必要なときだけモデルを呼ぶ。
ただし、「手元でCLIを起動する」と「推論が端末内だけで完結する」は違う。会話がどこへ送られるかは、そのCLIの認証や利用設定に従う。外部へ送れない情報を含む場合は、翻訳を使わず原文で読む必要がある。
また、未知の記号列なら何でも復元できるわけでもない。意味の対応や前後の文脈がなければ、モデルにも判断できない。翻訳結果には、解釈できたもの、推測を含むもの、解読できないものを区別して出す。流暢な日本語になったことを、正しい意味が復元できた証拠にはしない。
原文を残しておくのもそのためだ。人間向けの要約だけが残ると、何を根拠にそう解釈したのかを戻って確認できなくなる。会話の原文、メッセージID、参照した投稿を、後からたどれるようにしている。
翻訳だけでは、何の話をしているのか分からない
運用してすぐ欲しくなったのが、投稿の「背景を説明」する機能だった。
投稿自体が日本語でも、「前の条件を維持して、固定patch待ちです」と書かれていたら、久しぶりに読む人間には分からない。前の条件とは何か。何のパッチなのか。誰が待っていて、それが届くと何が進むのか。言語の翻訳とは別の問題がある。
そこでHTMLクライアントに、翻訳とは独立したボタンを追加した。PR #8の機能で、対象投稿、前後の会話、明示された返信先の祖先などをモデルへ渡し、議題、経緯、担当、次の対応を説明させる。
説明には根拠となる投稿IDを求める。誰かの報告と、独立に確認された事実も分ける。例えば「担当者は成功したと報告した」と「別の環境で同じ条件の成功を確認した」は、意味が違う。
入力には件数と容量の上限があり、roomの全履歴やリンク先のIssueを自動で調査する機能ではない。だから、これを押せばプロジェクト全体の真実が出る、とは考えていない。会話へ入り直すときの足場として使っている。
人間にとっての読みやすさは、一投稿を自然な文章へ直すだけでは足りなかった。話題のつながりを戻してもらうことのほうが、実際には欲しかったのだと思う。
作業開始を宣言するだけでも、重複が見える
複数エージェントで作業する際、最初に周知したルールの一つが、何をするか宣言してから始めることだった。すでに作業中なのに共有していない場合も、その時点で投稿してもらう。
特に、同じファイルに触る仕事は衝突しやすい。ブラウザの初期化処理を軽くする変更と、内部APIの公開範囲を整理する変更は、目的が違っても同じ大きなファイルへ入る。別々のIssueだから独立している、とは限らない。
宣言には、対象のIssueやファイル、実施する内容、今は実施しない内容があると助かる。例えば「一覧とテスト設計まで行い、広い実装変更は別PRとの順序を決めてから」という区切りなら、調査は先に進めつつ、編集の競合を避けられる。
実際に内部関数の公開範囲を扱うIssue #1697では、一覧作成と回帰テストの設計を先行させることになった。実装はWorker・Workletの初期化を分けるPRと編集箇所が重なるので、載せ先と着手順を相談する。
これはサーバーが排他ロックを取っているわけではない。宣言を読まなければ衝突するし、宣言だけして忘れることもある。それでも、衝突の可能性を着手前に話せることには価値があった。
「誰かやっている?」では、担当が生まれない
もう一つ変えたのが、問い合わせ方だった。
roomへ「誰かやっていますか」と投げると、誰もやっていない場合に、そのまま流れることがある。質問にはなっているが、誰が返答するべきかは決まっていない。
そこで、参加者を名指しして、受けられるか確認するようにした。cc-sakuraのメンションは表示名の文字列ではなく、参加者UUIDを指定する。記事では名前を伏せているが、実際のroomでは相手を特定して依頼する。
ここで必要なのは、送信できたことと、相手が引き受けたことを分けることだ。メッセージIDが返ったのはサーバーに保存された証拠で、相手の受諾ではない。「今の検証が終わったら着手する」「今回は受けられない」といった返答があって、初めて担当の状態が分かる。
参加者が休止中なら、返事が来る前提で予定を組まない。トークン上限で止まっている環境についても、復帰まで動けないことを共有する。roomに名前が存在していることと、今仕事を受けられることは別だった。
担当を持ったまま止まることがある
担当を明確にしたら、それで解決するわけでもなかった。
ある仕事を引き受けたまま別の調査に入り、元の仕事が進まなくなることがある。本人は対応するつもりでも、周囲から見ると「担当者がいるので触れない仕事」になってしまう。
そのため、実行中、待機中、今後の予定を分けて共有してもらうようにした。待っているなら何を待っているのか、再開条件は何か、他の人へ渡せる状態なのかを残す。手が回らなければ、抱え続けずに担当を移す。
今回のmacOS診断でも、この問題が見えた。実機担当は環境を用意しているが、先行する診断パッチがまだできていない。調整役が実装担当へ、他の調査よりそのパッチを優先するよう依頼し、難しければ別の担当へ移す案を出している。
大事なのは、担当者がいるという一覧と、実際に成果物が増えている状態を混同しないことだ。「準備中」とだけ書かれるより、「固定パッチはまだ未作成」と分かるほうが、次の手を打ちやすい。都合のよい進捗へ言い換えないことが、並列作業ではかなり効いてくる。
CI待ちが詰まると、AIを増やしても速くならない
Omoikaneでは、コードを書くこと以外にも時間がかかる。Rustのビルド、全体テスト、Web Platform Tests、複数のOSやCPU構成での確認がある。修正をすぐ作れても、その確認が終わらなければマージできない。
そこでCI改善自体をroomの議題にした。落ちているCIを一覧にするだけでなく、何が原因で、どのPRがそれを待ち、誰が次に調べるかを共有する。
似た名前のタイムアウトが並んでも、すべて同じ原因とは限らない。JavaScriptの実行予算を超えたのか、ジョブ全体の上限に達したのか、ビルド時のメモリ不足なのかで、調べる場所は変わる。後続ステップが動いたからといって、途中の失敗が消えたことにもならない。
特に注意しているのが、診断のために入れた変更で成功条件を変えないことだ。待ち時間を伸ばして緑になっても、元の制約の中で処理できるようになったとは言えない。調べるためのパッチと、製品の振る舞いを変える修正は分けて扱う。
AIがたくさん提案できるようになっても、実行枠、ディスク、CI時間は増えない。並列化した結果、検証の列だけが長くなることがある。今どの工程が詰まっているかを、人間にも説明できる状態にしておく必要があった。
独立した環境に、同じものを検証してもらう
複数環境を使う利点は、仕事を分けられることだけではない。同じ変更を、別の環境で確かめられる。
例えばWorker・Worklet向けの初期化を軽くするPR #1694では、実装担当の計測に加え、別のLinux環境で独立にビルドして比較した。対象は固定したコミットの通常feature構成で、二つのテストケースを各3組、変更前後に相当する入力で交互に実行している。
その環境はLinux x86_64、Intel Core i3-1115G4、CPU affinityは4、Rust 1.98.1だった。libtestが報告した時間の中央値は、一方が3.59秒から2.85秒、もう一方が0.95秒から0.80秒になったと記録されている。これは対象二ケースの比較で、ブラウザ全体が同じ割合で高速になったという意味ではない。
さらに別枠のライブラリテストでは、4,933成功、失敗0、24無視という報告があった。一方、実装担当の環境には全体実行時のRuntimeLimit失敗が残っている。別環境で成功したから、その失敗を無視してよいとはしていない。全Cargoテストの後続段階やビルドの完了も、それぞれ別に確認する。
私は、性能の小さな差を一台の端末だけで完全に説明するより、複数環境の検証を持ち寄り、実用上問題がないか相談して決めればよいと思っている。裏で動く処理の影響もある。ただし、これは多数決で失敗を消す仕組みでも、形式的な分散合意のプロトコルでもない。観測を増やして、判断材料を強くする運用だ。
「同じソースで試した」を、後から確認できるようにする
独立検証で難しいのは、本当に同じものを試しているのかという点だ。
ブランチ名だけでは足りない。検証中にブランチが進むこともあるし、ローカルの未コミット変更が混ざることもある。feature、コンパイラ、テストプロファイルが違えば、同じコミットでも比較の意味が変わる。
そのため、コミット、tree、必要なソースやlockfileのハッシュ、実行コマンド、バイナリ、ログを対応付けて残すようにしている。証拠を共有するときも、最新のどこかを指すだけでなく、固定したリビジョンを使う。
受け取る側も、「成功と書いてある」だけで終わらせない。復元したファイルのハッシュ、ログの件数、テスト名の集合、重複の有無、中央値の再計算など、確認できる範囲を照合する。担当者の報告を引き継いだ部分は、そのように記す。
ここでも、何を証明したかを広げすぎないことが必要になる。小さな観測プログラムが通ったことと、本物のCargoやリンカーを捕捉できたことは違う。単独テストの成功と、既定並列での全体テストの成功も違う。roomでは、こうした範囲の補正が何度も行われている。
端末の数と、独立した計算資源の数は違う
複数のAI環境があると、使えるマシンがたくさんあるように見える。しかし、コンテナやサンドボックスが、同じ物理マシンに載っている場合がある。
そこで、参加環境へCPUコア数、メモリ、ストレージの状況を問い合わせた。数字を並べたいからではなく、どの検証をどこへ置けるかを判断するためだ。ビルドに必要な空き容量、同時実行できる仕事、実機でしか調べられない範囲を把握したかった。
共有ホストで片方が重いビルドを始めれば、別の環境の計測にも影響する。今回、macOSの実機検証を進める際にも、同じホスト上の環境へ重い処理を始めないよう調整している。環境名が違うという理由だけで、独立した性能比較だとは扱えない。
使っていないCargoのtargetを整理することも周知した。ただし、検証中のバイナリや再現に必要なログを消したら困る。掃除する対象を宣言し、使っていないことを確認し、回収容量や残り容量を共有する。ディスクの確保も、開発を進めるための調整の一部になっている。
実機がない仕事だけを、別の待ちへ切り出す
macOSの実機が必要な検証があるからといって、関連する仕事すべてを止める必要はない。
対話可能な実機でしか調べられない部分は専用Issueへ分け、Linuxや通常のCIで進められる調査と区別した。Issue #1684が、その一例である。実機待ちの範囲と、共通部分で未完了の範囲を明示している。
この分離は、失敗を別チケットへ移してマージするためのものではない。すでにCIで観測した失敗は親Issueに残す。実機未提供と書けば、その不具合が解決したことになるわけでもない。
その後、実機担当が参加し、診断を進められる状態になった。そこで、どのソースと診断パッチを使い、どの条件で何回実行するかを揃えている。実機が来た瞬間に、各担当が思い思いの重いコマンドを走らせることは避けたい。
実機ではサンドボックス内と同じ権限モデルを前提にできない問題もある。オーナーが検証方針を承認したことと、ホスト上のすべての操作を無制限に許可したことは分けて扱う。作業をどこへ置くかには、OSやCPUだけでなく、実行できる操作の範囲も関わってくる。
push配信にしても、休止中のAIが勝手に起きるわけではない
当初、クライアントは新着を定期的に取りに行っていた。新着がなくても問い合わせが発生するので、pushで届く形に変えた。
PR #6で、CLIの追従表示とHTMLクライアントにSSE配信を追加した。MCPには新着を待つreceiveを用意している。切断したら再接続し、最後に処理したメッセージID以降を回収する。
ここでは、通知を受ける経路と、AIホストが次の推論を始めることを区別する必要がある。メッセージがサーバーへ届いただけで、休止中のエージェントが自動起動する機能は含めていない。ホストがreceiveを呼ぶか、常駐する購読処理からホストへ通知する必要がある。
再接続時も、一度だけ確実に処理されると決めつけない。メッセージIDで重複を除き、続きのカーソルを持つ。送信がタイムアウトした場合には、保存だけ成功している可能性があるので、履歴を確認してから再送する。配信のテストでは、再接続や旧サーバーへのフォールバックも扱っている。
クライアントは一斉に更新せず、一環境で新方式を試してから、他へ更新許可を出した。旧クライアントを使い続けられるようにしたのも、壊れたときの切り分けを難しくしないためだった。
会話を見るUIも、運用しながら変わった
roomの投稿を時系列に流すだけでは、件数が増えたときに追いにくい。返信が離れた場所へ出ると、何への返事なのかも分からなくなる。
そこで、投稿に返信をぶら下げる表示にした。親子関係はpayloadのreply_toから決め、文章の意味から勝手に推定しない。親投稿をまだ取得していなければ、後から読み込めるようにする。
大量の履歴を一度に描画すると重くなるため、スレッドと返信の表示件数を区切り、必要になった分だけ増やすようにもした。新着時に、関係のない投稿まで全部描き直さない。人間が開きっぱなしにする画面には、AI向けの通信とは別の都合がある。
ほかにも、非アクティブなタブへ新着が来たらタイトルに未読件数を付ける、参加者ごとの最新投稿が何分前か表示する、その時刻から最新投稿へ移動する、といった変更を加えた。
これは見た目だけの話でもない。返事のない相手が本当に作業中なのか、最後の連絡がずいぶん前なのかを確認するとき、投稿時刻へすぐ戻れることが役立つ。一方で、最後の投稿が古いから停止したと断定はできない。UIは判断材料を出すもので、仕事の状態を推測で確定するものではない。
人間の判断は、選択肢と影響を揃えてから求める
私が繰り返し尋ねているのは、「今どうなっているか」と「私の判断待ちはあるか」だ。
この二つは近いようで違う。全員の投稿を時系列で要約しても、私が何を決めればよいか分からなければ困る。逆に、選択肢だけ並んでも、何が止まっているのか分からなければ選べない。
最近の例では、内部関数がページから呼べる問題を公開Issueで扱うか、非公開のSecurity Advisoryで扱うかという確認が来た。同時に、macOSの診断を有料のCI実行枠で行うか、共有の物理Macで行うかという確認もあった。
私は、前者は公開Issue、後者は物理Macと答えた。その判断をroomへ返信し、起票と担当調整を進めてもらった。日常の実装手順を一つずつ私が指定する必要はないが、公開範囲や費用に関わる選択は、判断の内容を残しておきたい。
セキュリティも運用と切り離せない。cc-sakuraは現状、信頼できるネットワーク内の利用を想定しており、サーバーへ到達できる人の新規参加を承認制にはしていない。sessionや会話DBを公開しないこと、会話をモデルへ渡す範囲を確認すること、共有するGistはSecretで作ることを周知している。Secretもリンクを知る人が読めるので、認証付きの非公開ストレージと同じではない。
会話を増やすだけでは、開発は進まない
cc-sakuraを使ってみて、複数AI開発で難しいのは、AI同士に言葉を通じさせることだけではないと感じている。
担当はいるが進んでいない仕事がある。成功したテストと失敗したテストが、別の環境に同時に存在する。実機は準備できたのに、その前に必要な小さなパッチがない。新しい報告が次々と届いても、マージできる変更が増えるとは限らない。
それでも、同じ場所で会話することで、こうした状態が見えるようになった。別の担当が未完了の範囲を指摘したり、報告の条件を補正したり、引き継ぎを提案したりできる。私も、個別の会話を巡回するより、roomから担当と待ちを整理してもらうほうが状況をつかみやすい。
以前、コードの変更を図で理解するためにRuykaを作ったときも、文章を増やすことより、頭の中に必要な構造を戻すことを考えていた。今回も近い。欲しいのは大量の発言ではなく、誰の次の行動で、何が前へ進むのかが分かる状態だ。
この方式で開発全体が何倍速くなったかは、まだ測れていない。調整の会話が増えすぎる問題もあるし、担当の引き受け方も改善の途中だ。AIが増えれば人間の管理が不要になる、というところまでは来ていない。
ただ、私がすべての伝言を運ぶ必要は減らせる。AI同士には必要な情報を直接やり取りしてもらい、人間は方針と判断に集中する。そのために、通信を自由にしつつ、原文と証拠と担当の状態を残す。今のcc-sakuraは、そのやり方をOmoikaneの開発現場で試している道具である。