複数エージェントの境界設計について書かれた記事は、たいてい「責務を分けましょう」で終わります。私自身も最初はそう理解していました。問題は、責務を分けたつもりでも、実行時にそれが守られている保証がどこにもないことでした。
個人開発で Antigravity のエージェントを2つ3つと並走させていたとき、設計上はきれいに領域を分けたはずなのに、朝になると同じ src/lib/ のファイルが両方のエージェントに書き換えられていて、片方の変更がもう片方に静かに飲み込まれている、ということが何度か起きました。設計図は正しかったのです。守らせる仕組みがなかっただけでした。
この記事は、その反省から組み立てた「実行時に効く境界」の運用メモです。概念ではなく、所有権を1ファイルに固定する方法、衝突を git だけで見つける方法、引き継ぎを契約として書く方法という、押せばすぐ使える3点に絞ってお伝えします。
なぜ「設計時の境界」は実行時に溶けるのか
人間のチームなら、担当外のファイルに触れるとき一声かけます。エージェントはかけません。明示されていない境界は、エージェントにとって存在しないのと同じです。さらに厄介なのは、エージェントは自分の担当範囲を「タスクの文脈から推測」してしまう点です。
たとえば「認証まわりを実装して」と指示すると、エージェントは認証に関係しそうな共有ユーティリティまで触りにいきます。本人にとっては合理的な判断です。しかし同時刻に別のエージェントが同じユーティリティを別の理由で触っていれば、そこで境界は溶けます。
つまり境界は、自然言語の指示の中に書いても守られません。機械が読める場所に、機械が弾ける形で置く必要があります。私が行き着いたのが、所有権マニフェストという1枚のファイルでした。
所有権マニフェスト — 編集領域を1ファイルに固定する
考え方は単純です。どのエージェントがどのパスを触ってよいかを、リポジトリ直下の1ファイルに宣言します。エージェントへの指示文ではなく、リポジトリの事実として置くのがポイントです。
// agents.ownership.json
{
"agents": {
"auth-agent": { "owns": ["src/features/auth/**"], "deny": ["src/lib/**"] },
"billing-agent": { "owns": ["src/features/billing/**"], "deny": ["src/lib/**"] },
"shared-agent": { "owns": ["src/lib/**", "src/hooks/**"] }
},
"rule": "共有領域(src/lib, src/hooks)は shared-agent だけが触れる。他はパスをまたいだら停止。"
}
共有ディレクトリを独立した第3の所有者(ここでは shared-agent)に切り出しているのが肝です。私が最初に痛い目を見たのは、この共有領域を「誰でも触ってよい暗黙の場所」にしていたからでした。共有コードは衝突の温床なので、明示的に1つの所有者へ寄せ、変更が必要なら他のエージェントは shared-agent に依頼を出す、という一方通行にします。
各エージェントを起動するときは、この owns に書いたパスだけをタスク文脈として渡します。Antigravity のエージェントに「あなたが触ってよいのは src/features/auth/ 以下だけです。それ以外を変更する必要が出たら、変更せずに理由を報告して止まってください」と先頭で固定するわけです。指示は自然言語ですが、根拠はマニフェストという1つの事実を指しているので、複数エージェント間で食い違いません。
push 前ガード — 領域外の変更を機械的に弾く
マニフェストを置いても、エージェントがうっかり領域外を触る可能性は残ります。そこで、コミット前に「変更されたファイルが、そのエージェントの owns に収まっているか」を機械でチェックします。エージェントの善意に頼らず、最後に機械で止めるのが安全です。
# guard_ownership.py — そのエージェントの変更が所有領域に収まっているか検証
import json, subprocess, sys, fnmatch
agent = sys.argv[1] # 例: auth-agent
manifest = json.load(open("agents.ownership.json"))["agents"][agent]
owns = manifest["owns"]
deny = manifest.get("deny", [])
# ステージ済み + 未ステージの変更ファイルを取得
changed = subprocess.check_output(
["git", "diff", "--name-only", "HEAD"], text=True
).splitlines()
violations = []
for path in changed:
in_owns = any(fnmatch.fnmatch(path, p) for p in owns)
in_deny = any(fnmatch.fnmatch(path, p) for p in deny)
if in_deny or not in_owns:
violations.append(path)
if violations:
print(f"❌ {agent} が所有外を変更: {violations}")
sys.exit(1)
print(f"✅ {agent} の変更はすべて所有領域内")
これを各エージェントの作業完了フックに入れておくと、領域外の変更が混ざった瞬間に exit 1 で止まります。私はこのガードを入れてから、「気づいたら共有ファイルが書き換わっていた」系の手戻りがほぼ消えました。なぜ効くかというと、エラーの検出地点が「朝のレビュー」から「コミット直前」に前倒しされるからです。被害が1ファイルの差分のうちに止まります。
衝突検知 — git diff だけで奪い合いを見つける
領域分割をしていても、フェーズ分離で時間差運用しているときや、共有領域への依頼が重なったときには、複数エージェントが同じファイルに触ることがあります。これを専用ツールなしで、git だけで見つけます。
やり方は、各エージェントを別々の作業ツリー(worktree)か別ブランチで走らせ、最後に「両者が同じファイルを変更していないか」を突き合わせるだけです。
# detect_conflict.sh — 2つのエージェントブランチが同じファイルを触っていないか
A_BRANCH="agent/auth"
B_BRANCH="agent/billing"
BASE="$(git merge-base "$A_BRANCH" "$B_BRANCH")"
A_FILES="$(git diff --name-only "$BASE" "$A_BRANCH")"
B_FILES="$(git diff --name-only "$BASE" "$B_BRANCH")"
OVERLAP="$(comm -12 <(echo "$A_FILES" | sort) <(echo "$B_FILES" | sort))"
if [ -n "$OVERLAP" ]; then
echo "⚠️ 両エージェントが触ったファイル:"
echo "$OVERLAP"
exit 1
fi
echo "✅ 衝突なし。マージ可能。"
comm -12 で両ブランチの変更ファイル集合の積を取るだけです。積が空でなければ、そのファイルは奪い合いが起きています。重要なのは、ここでマージを試みないことです。両方の変更をマージしようとすると、意味的な矛盾(型は通るのに前提が食い違う)がほぼ確実に紛れ込みます。
衝突が出たときの私の復旧手順は、次の3ステップに固定しています。
- 片方のエージェントの成果を
git checkout で確定させる
- もう片方は破棄し、確定後の状態を入力にして最初からやり直させる
- 再実行が終わったら、もう一度
detect_conflict.sh を回して衝突がゼロになったことを確認する
マージではなく片方を捨てて再実行する方針を、私は強く推奨します。一見もったいないようですが、矛盾を抱えたままマージして後から原因を探すより、結局この方が速く、型は通るのに前提が食い違う系の意味的な矛盾を確実に回避できる、というのが半年運用しての実感です。これは本番運用に入れるなら譲れない判断だと考えています。
引き継ぎ契約 — 「確定/依頼/未定」の3区分
フェーズ分離で前段の成果を後段に渡すとき、最も事故が起きるのは「前段が仮で決めたことを、後段が確定事項だと誤解する(あるいは逆に勝手に覆す)」場面でした。これを防ぐには、引き継ぎを文章ではなく契約として書きます。鍵は、項目を3区分に分けることです。
# 引き継ぎ契約: AUTH-014
## 確定(変更禁止)
- useAuth フックのインターフェースは現状維持。これに依存する箇所が他に3つある
- エラーは Result 型で返す。例外を投げない
## 依頼(これを実装してほしい)
- 対象: src/features/auth/loginFlow.ts
- 入力仕様: docs/auth-spec.md セクション3
- 完了条件: 既存テストが全て緑、かつ loginFlow の単体テストを追加
## 未定(あなたが決めてよい・ただし理由を残す)
- エラーメッセージの文言
- ローディング表示の実装方法
「確定」は触ると壊れるもの、「依頼」は今回やってほしいこと、「未定」は委ねるものです。とくに未定をわざわざ明示するのが効きます。未定だと書いていない事項を、後段エージェントは「確定」か「自由」のどちらかに勝手に倒します。その勝手な解釈が、上流の意図とずれた実装を生みます。未定は未定だと宣言し、決めたら理由を残させる。これだけで、後段の暴走がかなり減りました。
監視エージェントを足すかどうかの線引き
「全体を見る監視エージェント」を1つ足すべきか。これはよく聞かれますが、私は条件で割り切っています。
足すのは、同時に動くエージェントが3つ以上で、かつ完了順序に意味がある(前段が終わらないと後段が走れない)ときだけです。このときは監視エージェントに「個別の変更判断は一切しない。進行管理と、上の衝突検知スクリプトの実行だけを担当する」という強い制約を渡します。判断をさせると、監視役自身のコンテキストが重くなって質が落ちるからです。
逆に、エージェントが2つで領域が完全に独立しているなら、監視役は要りません。むしろ層が増えるぶん遅くなります。人間がリアルタイムで差分を見られる規模なら、監視は人間が兼ねるのが一番軽いです。私の手元では、2〜3エージェントに所有権マニフェストと衝突検知スクリプトを組み合わせる構成が、最も手間とのバランスが良いところに落ち着いています。
まとめると、投資先はエージェント数ではない
半年ほど複数エージェントを回して確信したのは、スループットを上げる近道はエージェントを増やすことではなく、境界を実行時に守らせる仕組みに投資することだという点でした。所有権をマニフェスト1枚に固定し、領域外の変更を push 前に弾き、衝突を git で見つけ、引き継ぎを3区分の契約で渡す。この4つが回り始めると、エージェントを増やしても全体が破綻しなくなります。
次に試すなら、まず agents.ownership.json を1枚書いて、今動いている各エージェントの owns を宣言してみてください。書けないエージェントがあれば、それは境界が設計されていないサインです。そこを言語化するだけで、翌週の出力の安定感が変わるはずです。同じ課題に取り組んでいる方の参考になれば幸いです。