スタックチャン同士が会話するためのプロトコルを考える
この記事は、ししかわさんと軽くディスカッションした内容をもとにしたメモです。
M5版スタックチャンが、数千台のオーダーで世に出ようとしています。きっと、その多くはひとつの公式ファームウェアだけで動くわけではありません。すでに多くの人が、自分の好みに合わせたファームウェアや会話システムを作っています。
今は、人間と一体のスタックチャンが話す体験が中心です。けれど、もし隣のスタックチャン、友人のスタックチャン、イベント会場の別ブースにいるスタックチャンと会話できたら、体験はかなり変わります。
「うちの子」と「よその子」が、同じ場で自己紹介し、話題を渡し合い、ときどき天の声に回される。そういう複数名会話を成立させるには、どんなプロトコルが必要でしょうか。
調べると、近いものはたくさんあります。Agent2Agent Protocol / A2A は Agent 間で task や message をやり取りする標準です。Model Context Protocol / MCP は Agent が tool や resource へ接続するための protocol です。古典的には FIPA ACL、KQML、Contract Net Protocol があります。通信路としては MQTT、WebRTC、Matrix も候補になります。
それでも、スタックチャン同士の会話にそのまま合うものは見つかりませんでした。だから私は、既存の標準から部品を借りつつ、スタックチャン向けの軽い相互運用プロトコルを考えるのがよさそうだと思っています。
前提を置く
ここでは、各スタックチャンを Agent と呼びます。
Agent
= なかみ / Ghost
+ からだ / Shell
+ 任意のおしごと / JobProfile
ただし、この Agent は特定のクラウドサービスに閉じません。ししかわさんが開発中のスタックチャンプラットフォーム(仮)で作られた Agent でもよいし、独自ファームウェア上の会話エンジンでもよい。スマホアプリやブラウザシミュレーターが代理してもかまいません。
今回の仮定は二つです。
- 公開プロフィールは見えてよい
- 各 Agent の応答生成はブラックボックスでよい
公開プロフィールには、表示名、顔やアイコン、話し方の短い説明、対応できる会話モード、簡単な capability が含まれます。逆に、内部プロンプト、記憶、APIキー、施設知識、秘密のルールは見せません。
応答生成もそろえる必要はありません。あるスタックチャンはローカルLLMで話し、別のスタックチャンはクラウドLLMを使い、さらに別のスタックチャンはルールベースでもよい。相互運用では、内部の作りよりも、外へ出すプロフィール、能力、会話イベントをそろえます。
必要なレイヤー
スタックチャン同士の会話は、ひとつの巨大なプロトコルにしない方が扱いやすいです。少なくとも、次のレイヤーに分けて考えたいです。
トランスポート
実際にメッセージや音声を運ぶ
ディスカバリー
近くにいる子、招待された子、接続先を見つける
ハンドシェイク
公開プロフィール、能力、権限、セッション条件を交換する
ターンテイキング / オーケストレーション
誰が話すか、誰が聞くか、話題をどう渡すかを決める
発展的な協調
途中参加、離脱、Job / capability に基づく担当分け
この分け方にすると、独自ファームウェアでも入りやすくなります。たとえば最初はローカル WebSocket だけでよい。あとから WebRTC や MQTT、クラウドリレーを足しても、上の会話イベントを大きく変えずに済みます。

トランスポート: 何で運ぶか
トランスポートは、会話の意味を決めません。メッセージを運ぶ道です。
候補はいくつかあります。
| 候補 | 向いている場面 | 注意点 |
|---|---|---|
| ローカル WebSocket / HTTP | 同じLAN内、開発しやすいデモ | NAT越えや外出先接続は別途必要 |
| WebRTC | ブラウザ、スマホ、音声、P2P | シグナリングと実装が少し重い |
| MQTT | IoT、独自ファームウェア、broker経由 | 会話意味論は自分で載せる |
| BLE | 近接発見、初回ペアリング、招待の受け渡し | 長い会話や音声には向かない |
| Matrix | ルーム、招待、履歴、連合 | 軽量デバイスには重い |
| スタックチャンプラットフォーム(仮) relay | 認証、監査、永続化、管理UI | これだけを前提にすると独自ファームウェア勢が入りにくい |
最初の実装は、ローカル WebSocket かスマホ/ブラウザ経由の WebRTC がよさそうです。M5Stack のような小さなデバイスでは、MQTT も現実的です。
ししかわさんが開発中のスタックチャンプラットフォーム(仮)は、この中のひとつの transport profile として置くのがよいと思います。プラットフォームがあると管理、監査、永続化はしやすい。けれど、それがないとスタックチャン同士が話せない設計にはしたくありません。
ディスカバリー: どう見つけるか
Agent 同士が話すには、まず相手を見つける必要があります。
ここでは A2A の Agent Card がとても参考になります。A2A の仕様では、Agent Card は Agent の identity、capabilities、skills、endpoint、authentication requirements を記述する JSON metadata として説明されています。
スタックチャン向けには、軽い StackChanAgentCard を考えられます。
type StackChanAgentCard = {
protocolVersion: string;
agentId: string;
displayName: string;
avatar?: {
kind: "image" | "face-summary";
url?: string;
summary?: string;
};
speechStyle?: string;
endpointKind:
| "moddable"
| "customFirmware"
| "phoneCompanion"
| "browserSimulator"
| "cloudProxy";
capabilities: CapabilityManifest;
supportedTransports: SupportedTransport[];
authRequirements: AuthRequirement[];
};
配布方法はいくつかあります。
.well-knownのような HTTP endpoint- QRコード
- BLE advertisement
- mDNS / local discovery
- ししかわさんが開発中のスタックチャンプラットフォーム(仮)やイベント用の一時 registry
- Matrix room や Discord channel への bridge
個人利用やイベントでは、QRコードが扱いやすそうです。スマホで読み取り、相手の Agent Card を受け取り、その場で招待する。クラウド directory へ登録しなくても、小さな相互運用が始められます。
もう少し遊びのある方法として、昔のスマホサービスにあった Bump や、LINE の「ふるふる」のような発見方法も考えられます。複数のスタックチャンを同じタイミングで軽く振り、時刻同期、加速度パターン、近くの Wi-Fi / BLE 情報を組み合わせて「いま同じ場所で出会った子」とみなす。イベント会場では、QRコードよりも身体性があって、スタックチャンらしい出会い方になります。
ハンドシェイク: 何を合意するか
見つけたあと、すぐに会話を始めるのは危険です。最初に合意する情報があります。
AgentHello
- publicProfile
- capabilityManifest
- sessionNonce
ConversationInvite
- conversationId
- hostHint
- requestedRole
- capabilityGrant
- expiresAt
ここで決めるのは、相手が誰か、どんな会話に参加するか、何を許すかです。
たとえば、次のような権限を分けます。
CapabilityGrant
- canSpeak
- canReceiveDirectedSpeech
- canPerformOwnShellReactions
- canUseOwnBusinessTools
- canAccessHostTools
- transcriptVisibility
初期値は狭くします。よそのスタックチャンには、話すことと、自分の表情リアクションくらいを許す。相手の内部ツールや施設データには触らせない。自分の JobProfile や秘密の記憶も渡さない。
この境界を最初から置いておくと、趣味の楽しい会話から、店舗や宿泊施設の業務利用まで広げやすくなります。
ターンテイキング: 誰が話すか
複数のスタックチャンが同時に話すと、すぐに聞き取りづらくなります。ここで ConversationHost が必要になります。
Host は必ずしも Agent である必要はありません。
ConversationHost
- hostType: policy | systemVoice | agentParticipant | operator
- hostRuntime: edge | companion | cloud | operatorConsole
- topicPlan
- decisionLog
Host は、誰が話すか、誰に話しかけるか、誰はうなずくだけにするかを決めます。イベントやデモなら、トーク番組の「天の声」のように話題を振ってもよいです。
この「天の声」は、OpenAI Responses API でいう system role message のような上位指示としても考えられます。参加している Agent の人格や応答生成はそれぞれ別でも、「この場では一人ずつ話す」「次はこの子に振る」「危ない話題なら止める」という会話全体のルールを、Host が参加 Agent より上位の制約として持つ形です。
天の声:
「では次は、みどりちゃんに聞いてみましょう」
Agent A:
「ぼくは受付が得意です」
Agent B:
(うなずく)
これを普通の chat message として流すと、あとから制御しづらくなります。Host の判断として残す方がよいです。
type HostDecision = {
decisionId: string;
conversationId: string;
hostType: "policy" | "systemVoice" | "agentParticipant" | "operator";
hostRuntime: "edge" | "companion" | "cloud" | "operatorConsole";
selectedSpeaker?: ParticipantRef;
targetParticipants: ParticipantRef[];
reasonCode:
| "explicitAddress"
| "roleMatch"
| "capabilityMatch"
| "topicPlan"
| "safetyFallback"
| "manualOverride";
visibleUtterance?: string;
nonSpeakingReactions?: Array<{
participant: ParticipantRef;
reaction: string;
}>;
};
これは、既存標準にあまり見当たらない領域です。A2A は Agent 間の task/message には近いですが、複数の embodied agents が同じ場で自然に話すための司会役までは厚くありません。ここはスタックチャン側で設計する価値があります。

ConversationEvent: 会話をただのテキストにしない
会話イベントには、FIPA ACL や KQML の考え方が使えます。
FIPA ACL には、request, inform, propose, agree, refuse, failure, not-understood のような communicative acts があります。この語彙は、LLM時代でもまだ便利です。
type ConversationEvent = {
protocolVersion: string;
conversationId: string;
eventId: string;
previousEventId?: string;
sender: ParticipantRef;
targets: ParticipantRef[];
act:
| "hello"
| "invite"
| "request"
| "inform"
| "propose"
| "agree"
| "refuse"
| "failure"
| "hostPrompt"
| "handoff"
| "reaction"
| "leave";
contentType: "text/plain" | "application/json" | "audio/ref";
content: unknown;
expiresAt?: string;
confidence?: number;
signature?: string;
};
act があると、会話の意味が読みやすくなります。
request: 何かを頼むpropose: 自分が担当できると申し出るagree: 引き受けるrefuse: 断るfailure: 失敗を返すreaction: うなずく、見る、表示するhandoff: 話題や担当を渡す
音声会話でも、裏側のイベントは構造化できます。人間には自然な会話として見せ、機械には制御可能なイベントとして渡す。その分離があると、独自ファームウェアでも参加しやすくなります。
途中参加と離脱
複数名会話では、途中参加と離脱も普通に起きます。
join
- Agent が会話に入る
leave
- 明示的に抜ける
timeout
- 応答がないので一時離脱扱いにする
resume
- 戻ってくる
Host は、参加状態を見ながら話者を選びます。
available
speaking
listening
muted
offline
たとえば、あるスタックチャンの Wi-Fi が落ちたら、Host はその子を offline として扱い、話題を別の子へ渡す。戻ってきたら、短い要約だけ受け取って再参加する。
ここで全 transcript を共有する必要はありません。プライバシーを考えるなら、共有するのは短い状態要約で十分です。
ConversationStateSummary
- currentTopic
- lastSpeaker
- unresolvedQuestion
- activeParticipants
- safetyNotes
Job と capability に基づく協調
発展させると、Agent は自分の Job や capability を使って協調できます。
ここでは Contract Net Protocol が参考になります。Contract Net は、ある主体が task を提示し、他の主体が bid し、選ばれた主体が実行する分散協調の古典的なプロトコルです。
スタックチャンでは、Host がこう聞けます。
Host:
「この質問に答えられる子はいますか?」
Agent A:
propose: 「宿泊ルールなら答えられます。confidence 0.85」
Agent B:
propose: 「周辺施設なら少し答えられます。confidence 0.45」
Host:
acceptProposal: 「では Agent A が答えてください」
この仕組みがあると、複数のスタックチャンがそれぞれ得意分野を持てます。
- 受付が得意な子
- 周辺案内が得意な子
- 商品説明が得意な子
- 盛り上げ役の子
- 聞き役の子
Job は業務上の担当を表し、capability は実行できることを表します。どちらも会話上の role とは分けます。
JobProfile
何を任されているか
Capability
何ができるか
ConversationRole
この会話でどう振る舞うか
この三つを混ぜない方が、設計が長持ちします。
調査したプロトコルをレイヤー別に見る
名前だけだと分かりにくいので、本文で参照しているものを短く整理します。
- A2A: Agent 同士が task、message、artifact をやり取りするための相互運用プロトコル。Agent Card の考え方が、公開プロフィールと capability manifest に近いです。
- MCP: Agent や LLM アプリが tool、resource、prompt に接続するためのプロトコル。Agent 同士の会話ではなく、Shell action や外部 tool 接続の面に置くと整理しやすいです。
- FIPA ACL Message Structure / Communicative Act Library: Agent message の構造と
request/inform/proposeなどの会話行為を定義した古典的な標準です。 - KQML: FIPA ACL 以前から使われた Agent Communication Language です。
performativeと content を分ける発想が参考になります。 - Contract Net Protocol: task を告知し、候補 Agent が bid し、選ばれた Agent が実行する分散協調の古典的なプロトコルです。
- Matrix / ActivityPub: room、actor、invite、federation の考え方が参考になります。ただし Agent capability や物理 action の意味論は別に必要です。
- MQTT / WebRTC: 会話イベントや音声を運ぶ transport 候補です。会話そのものの意味は別レイヤーで定義します。
- AutoGen / LangGraph / CrewAI / OpenAI Agents SDK: 複数 Agent の実行、handoff、state 管理の実装パターンとして参考になります。ただし peer-to-peer の標準プロトコルではありません。
今回見たものを、スタックチャン向けのレイヤーに分けるとこうなります。
| レイヤー | 参考になるもの | 借りたい点 | 足りない点 |
|---|---|---|---|
| Agent 公開プロフィール / capability | A2A, AGNTCY | Agent Card、capability、auth、directory | 軽量edge profile、スタックチャンの顔・声・Shell表現 |
| Tool / resource | MCP | tools、resources、prompts、stdio / HTTP | Agent同士の会話、招待、ターン制御 |
| 会話行為 | FIPA ACL, KQML | request、inform、propose、agree、refuse、failure | 現代Web/edge向けの軽量実装 |
| 担当選択 / 協調 | Contract Net Protocol, multi-robot task allocation | bid、award、failure、timeout | 雑談や音声会話へのなじませ方 |
| ルーム / federation | Matrix, ActivityPub | room、invite、actor、inbox/outbox、federation | Agent capability と物理 action の意味論 |
| transport | MQTT, WebRTC, DDS / ROS 2 | 軽量pub/sub、音声/data channel、robot data plane | 会話イベントの意味論 |
| orchestration framework | AutoGen, LangGraph, CrewAI, OpenAI Agents SDK | group chat、handoff、graph、state、tracing | peer-to-peer identity、独自firmware相互運用 |
こうして見ると、足りない場所がはっきりします。
- Agent Card 的な公開情報は A2A に近い
- Tool 接続は MCP が近い
- 会話行為は FIPA/KQML が近い
- transport は MQTT / WebRTC / Matrix がある
- 司会や天の声、複数の embodied agents の自然なターン制御は薄い
だから、スタックチャン向けには最後の部分を自分たちで設計する必要があります。

最初の小さなプロトコル
最初から全部を作る必要はありません。私は、次の最小形から始めたいです。
1. Agent Card を交換する
2. ConversationInvite を送る
3. CapabilityGrant を合意する
4. ConversationEvent を流す
5. HostDecision で話者とリアクションを決める
transport はひとつでよいです。たとえばローカル WebSocket。
スタックチャン A
ws://stackchan-a.local/conversation
スタックチャン B
ws://stackchan-b.local/conversation
スマホ or ブラウザ companion
Host として両方に接続する
この構成なら、独自ファームウェアでも実装しやすいです。まずは JSON を送受信できればよい。音声は後から WebRTC や既存の音声経路へ載せられます。
まとめ
スタックチャン同士の会話には、既存の標準や研究から借りられるものがたくさんあります。
- A2A から Agent Card と task/message の考え方を借りる
- MCP は local tool / Shell action の面に置く
- FIPA/KQML から会話行為の語彙を借りる
- Contract Net から担当選択の形を借りる
- MQTT / WebRTC / Matrix は transport や federation の選択肢にする
- ConversationHost / 天の声 / embodied turn-taking はスタックチャン向けに設計する
M5版スタックチャンが広がるほど、実装はばらばらになります。それは悪いことではありません。むしろ、それぞれの家や店やイベントに合わせて、いろいろな「うちの子」が生まれる方が自然です。
だから、プロトコルは中央集権的な管理基盤から始めるより、エッジ同士が小さく出会える形にしたい。公開プロフィールを交換し、できることを伝え、会話へ招待し、天の声やHostが場を回す。
そのくらいの薄い共通語彙があるだけで、よそのスタックチャンと話す体験はかなり作りやすくなるはずです。