5つのAIコーディング・エージェントがいます。何に取り組むべきか、すでに誰かが取っているものは何か、そして何が終わっているのかを把握する必要があります。どうやって調整(オーケストレーション)しますか?
よくある答えはメッセージバスです。エージェントがパブリッシュし、サブスクライブします。タスクを交渉し、コンテキストを共有し、状況(ステータス)をブロードキャストします。これは、CrewAI、AutoGen、あるいは「multi-agent」をタグラインに掲げるあらゆるフレームワークで見られるようなアーキテクチャです。
私はそのアプローチを試しました。次に、それをディレクトリ内のマークダムファイルの一覧に置き換えました。つまり、ブローカーの代わりにファイルシステムを使い、AIエージェント向けにカンバン駆動でタスクをディスパッチする方式です。もう6か月経ちましたが、振り返っていません。
The O(n²) problem with message buses
メッセージでエージェント同士を連携させると、各エージェントが他のほぼ全てのエージェントに話しかける可能性があります。エージェントAがタスクを終えて「task 27 done(タスク27完了)」をブロードキャストします。エージェントB、C、D、Eは全員それを受け取ります。次にエージェントBが次のタスクを主張し、「I'm taking task 28(タスク28を取ります)」とブロードキャストします。すると、他の全員がそれを聞き取って、状態を更新し、同じタスクを取りに行かないようにする必要があります。
5つのエージェントならそれでも管理可能です。10になると、起こり得るメッセージの組み合わせは90です。20なら380になります。通信オーバーヘッドはO(n²)で増えていき、メッセージを送るたびに競合状態(レースコンディション)、古い状態、更新の取りこぼしが起きるチャンスになります。
さらに悪い点があります。何が起きているか見えません。連携の状態は「飛んでいる最中」に存在します——メッセージキュー、メモリ上のバッファ、エージェントのコンテキストウィンドウの中です。何かがうまくいかないと、見えない状態をデバッグすることになります。
The O(1) alternative: read a file
その代わりに、Battyはタスクをどのようにディスパッチします。すべてのタスクは、ディレクトリ内のマークダムファイルです:
.batty/board/tasks/
├── 027-add-jwt-auth.md # status: in-progress, claimed_by: eng-1
├── 028-user-registration.md # status: todo
├── 029-add-rate-limiting.md # status: backlog
└── 030-fix-dashboard-css.md # status: done
各ファイルには、機械が読み取れるフィールドのためのYAMLフロントマターと、タスク説明のためのマークダム本文があります:
id: 28
title: User registration endpoint
status: todo
priority: high
depends_on: [27]
claimed_by:
tags: [api, auth]
# User registration endpoint
Add POST /api/register with email validation,
password hashing, and duplicate detection.
## Done when
- Endpoint returns 201 with user object
- Duplicate email returns 409
- Tests cover happy path and validation errors
エージェントはトピックにサブスクライブしたり、ピアと交渉したりしません。ファイルを読みます。1ファイル、1読み取り、1タスク。O(1)です。
How kanban-driven dispatch actually works
Battyのデーモンはポーリングループを回します——10秒ごとにボードを読み取り、判断します:
1. タスクディレクトリをスキャン
2. 空いているエージェントを見つける(アクティブなタスクがない)
3. 各空きエージェントについて、次の条件を満たす「優先度が最も高い」タスクを探す:
- status: backlog または todo
- 誰にもclaimedされていない
- ブロックされていない
- 依存関係が解決済み(depends_onで指定されたタスクがすべてdone)
4. タスクファイルを更新する:status → in-progress、claimed_by → eng-1
5. タスクのコンテキスト付きでエージェントを起動する
これがディスパッチのアルゴリズム全部です。優先度の並び替えは決定的です:criticalはhighより先、highはmediumより先にディスパッチされます。同順位のときはタスクIDで決まり、最も古くてブロックされていないタスクが勝ちます。デーモンはエージェントを起動する前にファイルを更新するので、ボードは常に一貫します——起動が失敗した場合、そのタスクはclaimedされたままになり、デーモンは次のサイクルで再試行します。
メッセージブローカーはありません。pub/subもありません。合意プロトコルもありません。ファイルシステムが連携のレイヤーであり、grepが監視ツールです:
# 今進行中なのは?
grep -rl "status: in-progress" .batty/board/tasks/
# 誰が何をやってる?
grep -rn "claimed_by:" .batty/board/tasks/*.md
# 各ステータスにあるタスク数は?
grep -rh "^status:" .batty/board/tasks/ | sort | uniq -c
これをメッセージバスでやろうとしてみてください。
Why agents understand markdown natively
この方式全体が機能する鍵になる洞察があります。LLMはすでにマークダウンを理解しています。これは学習データの中で支配的な形式です——READMEファイル、GitHubのIssue、ドキュメント、Stack Overflowの投稿などです。AIコーディング・エージェントにマークダウンのタスクファイルを渡すと、タイトルを読み、受け入れ基準を解析し、そのまま作業を始めます。教え込むべきシリアライズ形式はありません。設定すべきAPIクライアントもありません。
これを、連携用バスからエージェントにメッセージを渡すことと比べてみましょう:
{"type": "task_assignment", "payload": {"id": 28, "title": "User registration endpoint", "priority": "high", "context": {"depends_on": [27], "tags": ["api", "auth"]}, "description": "Add POST /api/register..."}}
エージェントはこれを解析できますが、そのフォーマットにはタスクに関する情報があまりありません。オーバーヘッドです。マークダウン版はタスク説明そのもの——エージェントは、人間の開発者がチケットを読むのと同じように読み取ります。
What happens when things go wrong
メッセージバスには高度なエラー処理が必要です。メッセージが失われたらどうしますか?2つのエージェントが同じタスクをclaimedしたらどうしますか?エージェントがタスクの途中でクラッシュしたらどうしますか?
ファイルベースのディスパッチなら答えはシンプルです:
更新の取りこぼし(Lost updates):起こり得ません。ファイルはディスク上にあります。デーモンが書き込みの途中でクラッシュしても、ファイルは更新されるか、されないかのどちらかです。再起動後、デーモンはボードを読み直して、中断したところから再開します。
二重請求: デーモンは、ディスパッチ操作における唯一のライターです。エージェントを起動する前にタスクを請求します(ファイル内のclaimed_byを更新します)。起動に失敗した場合でも、請求はすでに盤上にあります — デーモンは再試行するか、エスカレーションします。同じタスクを巡って2つのエージェントが競合することはありません。
クラッシュしたエージェント: デーモンは、常に各ポーリングサイクルで整合(reconcile)します。エージェントがアイドル状態でも進行中のタスクを持っていれば、デーモンがそれを再割り当てします。存在しなくなったエージェントによってタスクが請求されている場合、そのタスクは請求解除され、キューに戻されます。盤(ボード)が常に唯一の真実のソースであるため、孤立した状態は不可能です。
依存関係違反: タスク28をディスパッチする前に、デーモンはdepends_on: [27]に含まれるすべてのタスクがstatus: doneになっていることを確認します。タスク27がまだ進行中なら、タスク28はキューに留まります。気にするべきメッセージ順序はありません — 単なるフィールドの比較です。
人間ができて、メッセージバスができないこと
あなたのカンバンボードはテキストファイルのディレクトリです。つまり:
動的に優先度を変更できます。 029-add-rate-limiting.mdを開き、priority: mediumをpriority: criticalに変更します。次のディスパッチサイクルで、キューの中で前に飛びます。API呼び出しも、管理パネルも、「カードをドラッグ」する必要もありません。
タスクの途中で文脈を追加できます。 エージェントがタスク28に取り組んでいて、「追加の文脈が必要だ」と気づいたとします。マークダウンファイルを編集し、注記を追加したり、受け入れ基準を明確にしたり、コードスニペットを貼り付けたりします。エージェントは、次に参照するときに更新されたファイルを読み取ります。
catでデバッグできます。 23時に何かがうまくいかないとき、「ファイルを開く」のと「モニタリングダッシュボードに接続してメッセージの流れを復元する」の違いは、問題を直して床につけるかどうかの違いです。
タスクを即座にブロックできます。 フロントマターにblocked: "waiting on API key from vendor"を追加します。デーモンは、あなたがそのフィールドを削除するまで、毎回のディスパッチサイクルでそれをスキップします。「一時停止」ボタンを探したり、トリガーするワークフローを作ったりする必要はありません。
すべてをバージョン管理できます。 git log -- .batty/board/tasks/は、すべてのタスク作成、ステータス変更、優先度の切り替えを表示します。git diffは、何がどう変わったかを正確に示します。git blameは、誰がそれを変更したかを示します。プロジェクト管理の履歴は、コードと同じリポジトリにあり、同じツールで扱えます。
うまくいかない場合
正直な制約:
リアルタイム協調。 エージェント同士で中間結果を共有する必要がある場合 — たとえば「APIスキーマを今変えたから、みんな型を更新して」 — 10秒間隔でのファイルポーリングでは速さが足りません。プッシュ型の仕組みが必要です。
エージェント数が多い場合。 50以上のエージェントでは、10秒ごとにタスクファイルのディレクトリをスキャンすることの意味が出てきます。Battyは、群れ(スウォーム)ではなく、3〜10人程度のチーム向けに作られています。
プロジェクトをまたいだ連携。 エージェントが複数のリポジトリやマシンにまたがる場合、共有ファイルシステムは利用できません。ネットワーク越しの連携レイヤーが必要になります。
典型的なユースケース — 1つのプロジェクトで開発者が3〜8個のAIコーディングエージェントを動かす — なら、マークダウンファイルのディレクトリは、私が試したどのメッセージバスよりもディスパッチに向いています。運用がより簡単で、デバッグも簡単で、エージェントはREADMEを読むのと同じくらい自然にそれを読み取れます。
試してみる
cargo install batty-cli
batty init
batty up
タスクはマークダウンファイルとして定義します。Battyはそれらをエージェントにディスパッチし、テストでゲートし、ボード上で順に進めます。メッセージバスは不要です。
あなたのマルチエージェント構成では、タスクの連携をどのように扱っていますか? ほかの誰かがファイルベースのアプローチに着地しているのか、それとも実際にデバッグが簡単なメッセージバス構成があるのか、気になっています。

