renga という Rust 製のターミナルマルチプレクサを書いていて、Claude Code や Codex から renga 自体を MCP 経由で操作できるようにしました。やってみたら使い方が結構変わったので書いておきます。具体的にできることの多くは動画で見られます、1分位なのでそっちを見た方がはやいかも。
tool 呼び出しが届く経路はこうなっています。
各 Claude Code / Codex(以下まとめてエージェントと呼びます)は自分専用の mcp-peer を stdio サブプロセスとして持ちます。エージェントが tool を呼ぶと mcp-peer が renga 本体に IPC で渡し、renga が他のペインを操作したり、向こう側の mcp-peer 経由で通知を流したりします。やり取りは双方向で、renga 本体が常にハブを兼ねる構成です。
並べるだけだと結局人間は忙しいまま
エージェントを別ペインで立ち上げて複数動かすところまでは普通のマルチプレクサでも出来ます。
ただ、それだけだと意外と人間に残った仕事が多くてそこまで楽になりません。
- 片方のエージェントが
[y/N]で止まっているのに気付く- auto mode で回避可能だがセンシティブ操作はもっと分かり難く止まる状態になる
- もう1人エージェントのワーカーを生やすか人間が判断する
- エージェントの出力結果を片方からもう片方にコピペする
- タスクが終わったペインを閉じる
上記のような本来人間がやりたくないタスクを全部人間がやる事になり、エージェントよりも自分がボトルネックになって「やっぱり人間いらないのでは」という状況に陥ります。
こういった状況を回避するために人はエージェント同士がメッセージを送れる MCP を利用し始めます。
メッセージングだけの MCP も試したけど足りなかった
エージェント間で会話できれば良いのでは、と思って、最初はピア間メッセージングだけの MCP サーバを試しました(happy-ryo/claude-peers-mcp)。
これでエージェント同士は喋れるようになりましたが、思った以上に仕事は楽にならないし効率化された感も殆どありませんでした。
結果として単なるメッセージ送信が可能になるだけの MCP では、メッセージのコピペ以外の仕事は依然として人間に委ねられていて、現実で言うなら人を採用するのも席を用意するのも、誰にどの仕事をさせるのかも、退職の処理までも全部自分がやるのと変わらない状態です。
renga 自体を tool にした
renga では、ピア間メッセージング系と一緒にペイン操作系の tool も MCP サーバから生やしています。
エージェントから呼べる tool は大体こんな感じ。
list_peers 同じ tab のピアを列挙
send_message ピアにメッセージを送る
check_messages 自分宛のメッセージを取り出す
list_panes tab 内の全ペインを見る
spawn_pane 任意コマンドのペインを生やす
spawn_claude_pane Claude Code のペインを生やす
spawn_codex_pane Codex のペインを生やす
inspect_pane ペインの可視内容を取り出す
send_keys ペインにキー入力を送る
close_pane ペインを閉じる
focus_pane フォーカスを移す
new_tab 新しい tab を開く
poll_events lifecycle イベントを待つ
set_pane_identity ペインに stable な name と role を付ける
set_summary このペインが何をやっているかをユーザに見せる
メッセージング系とペイン操作系が同じ MCP サーバに乗っているのがポイントです。
動かしてみるとこうなる
メインのエージェントに「このリポジトリのテスト書いて、ついでに Codex 立ち上げて lint 走らせて問題あったら修正させといて」と頼むとこんな流れで動きます。
人間がやる事は最初の 1 行を投げるだけになりました。
人間が直接やりとりするエージェント以外のエージェントを立ち上げるのも、他のエージェントが何で止まっているのかも、他のエージェントの動きを承認させるのも全部人間が直接やりとりする1つのエージェントで完結できるようになります。
勿論タスクが終わったら立ち上げた別のエージェントを終了するのもメインのエージェントから実行可能です。
設計で迷ったところ
ピアの discover スコープ
list_peers で誰が見えるかは結構悩みました。候補は cwd / git root / PID ツリー / ターミナルの tab。
- cwd: エージェントが
cdするとピア集合が変わってしまう。 - git root: monorepo / サブモジュールで意図と違う粒度になる。リポジトリ外で動かしている時にそもそも使えない。
- PID ツリー: マルチプレクサ越しだと親プロセスが共通になって全員見える。
- tab: ユーザが「同じ画面に並べた」相手とだいたい一致する。
最終的に tab スコープにしました。ユーザに見えているワークスペースの単位と、エージェントが見えるピアの集合が一致するのが一番事故が少なかったです。
spawn_claude_pane を別 tool にした
最初は汎用の spawn_pane(command="claude --permission-mode acceptEdits ...") で済ませようとしました。
結果的にこの方針は上手く行かず。エージェントが起動コマンドの組み立てを毎回やるので、ピアフラグの付け忘れ・モデル指定の取り違え・引数のクオート崩れがどうしても回避できませんでした。
生成AIに作業させるということは、そういう事なので spawn_claude_pane(model, permission_mode, args[]) のように構造化された引数を取る tool を別に作りました。Claude の起動オプションは MCP 側で吸収する、というやり方です。spawn_codex_pane も同じ理由で別になっています。
サーバから push する経路
ピアからのメッセージを「エージェントが tool を叩きに来た時にだけ届ける」形にすると、相手がアイドル状態の間はなんの反応も得られません。
MCP には JSON-RPC のサーバ→クライアント通知(id を持たないフレーム)があります。これを使って、別ペインから send_message が来た時に宛先ペインの MCP サーバが stdout に通知フレームを書き出すようにしました。
fn channel_notification(body: &str, from_id: &str, from_name: Option<&str>) -> Value {
json!({
"jsonrpc": "2.0",
"method": "notifications/claude/channel",
"params": {
"source": "renga-peers",
"from_id": from_id,
"from_name": from_name,
"body": body,
}
})
}
メッセージを受けるエージェント側はこの通知を自分のコンテキストに取り込んで、アイドルでも反応してくれます。これが無いとピアメッセージングはポーリングになって応答性が出ません。
ペインに id と name の両方
send_message の宛先が pane id だけだと、エージェントが list_panes を毎回叩かないと相手を指定できません。
set_pane_identity で stable な name("reviewer" とか "tester" とか)を付けられるようにして、to_id には name でも id でも渡せるようにしました。エージェントは大抵自分にロールを付けてから仕事を始めるので、id ではなく役割名で送り合う形になります。
inspect_pane の戻り値
生の vt100 エスケープシーケンスを返すとエージェントのコンテキストがすぐ埋まります。renga では vt100 パーサのスクリーン状態から可視文字列だけを抽出して返しています。
[y/N] のようなプロンプト検出はこの可視テキストへの部分文字列マッチで充分通ります。エージェントに渡す情報量は人間が見るより少なくて済む、というのは結構効きました。
補足: localhost ピア通信系の MCP とは思想が違います
ピア間メッセージ交換 MCP として既にあるものは、大体は localhost にサーバを立てて、エージェント同士が TCP / Unix socket で直接喋るタイプです(自分が前に書いた claude-peers-mcp も基本そっち系)。これはエージェントをネットワーク上のノードとして扱う発想で、discover も network レベル(ポートスキャン、PID 共有、cwd 一致)でやります。
renga は方向が違っていて、エージェントは独立したノードではなく、renga というマルチプレクサに収容されているペイン、という扱いです。
図にするとこんな感じです。
どちらもハブ&スポーク型なのは同じで、違うのはハブが何者かです。localhost 系のハブは汎用のメッセージブローカーなので、エージェントが何を画面に出しているか・どんなプロンプトで止まっているかは見えません。お互いに「メッセージを送り合う」事しか出来ないので、エージェント B が [y/N] で止まったら B 自身が気付いて誰かに助けを求めない限り誰にも分かりません。(そしてそんな機能は今の所ない)
renga のハブはマルチプレクサ本体で、各ペインの PTY を握っています。エージェント A から inspect_pane(B) を呼ぶと B の画面を vt100 パーサ越しに可視テキストとして読めるので、[y/N] で止まっている事を A が外から自律的に検知できます。検知したら send_keys(B, "y") で B の PTY にキーイベントを直接撃ち込めます。要は A に「他のエージェントが承認プロンプトで止まっていたらこういう基準で y か n を送れ」みたいな方針さえ仕込んでおけば、B 側の協力を待たずに作業が継続されます。
つまり大きな差は「メッセージを相互配送できる」止まりか、「他のエージェントを自律的に立ち上げてメッセージを相互配送し、エージェント自身が他のエージェントの画面を覗いてキーを撃ち込める」かです。
| localhost ピア通信系 | renga | |
|---|---|---|
| ハブの役割 | メッセージ配送 | 各ペインの PTY 所有 (画面 + 入力) |
| 他エージェントの状態取得 | 相手が send_message で報告した範囲のみ |
inspect_pane で画面内容を直接読める |
| 他エージェントへの干渉 | メッセージ送信のみ |
send_keys でキー入力を注入できる |
| discover の単位 | ポート / PID / cwd | ユーザに見えているターミナル tab |
| ライフサイクル制御 | 範囲外 |
spawn_* / close_pane で生成・終了まで可能 |
エージェントにワーカーを生やしたり片付けたりまでやらせたい場合は、ホストが居る後者の方が今のところ扱いやすいです。
まとめ
- 権限回りのコントロールはハーネスで行うべきなので、その機能は省いている
- renga 上で利用するハーネスのリファレンス実装も用意してある
- ピア間メッセージングだけだとペイン操作が人間に残る
- ペイン操作の tool も MCP に乗せると、エージェントが自分でワーカーを生やしたり詰まりを解いたり片付けたり出来る
- ピアスコープは tab 単位が今の所一番事故が少ない
- 起動コマンドは構造化された tool に閉じ込める
- サーバ→クライアント notification があるとアイドル中でも肩を叩ける
- localhost ピア通信系の MCP とは違って、
renga本体をホストにして全 tool をそこに集約している - あまり詳しく書いてないが Claude Code と Codex のメッセージ交換も良い感じになってます
renga は github.com/suisya-systems/renga にあります。npm install -g @suisya-systems/renga → renga mcp install --client claude で試せます。