Antigravity 2.0 が「エージェント管制塔」と呼ばれるようになってから、個人開発で複数アプリを並行して回している私のデスクトップでは常時 3〜4 体のエージェントが同時に動くようになりました。便利になった反面、最初の一週間は「どれが今なにを待っているのか」が分からず、結局すべてを順番に見て回ることになり、並列にした意味がほとんどありませんでした。
問題は並列実行そのものではなく、監督する側の画面と注意の配り方が一体運用に追いついていなかったことでした。複数エージェントを束ねるのは、コードを書く作業というより、複数の同時進行を捌く管制の作業です。ここでは、その捌き方を画面構成と割り込み判断の二つに分けて整理します。
並列にすると最初に壊れるのは「注意」だった
エージェントを 1 体だけ動かしていたころは、出力をそのまま追えばよく、迷う余地がありませんでした。ところが 3 体に増やした途端、私は全部を等しく見ようとして、どれも中途半端にしか見られなくなりました。
ここで気づいたのは、人間が同時に深く追えるのは実質 1 体だけだということです。残りの 2〜3 体は「見る」のではなく「合図が来たら見る」対象に切り替える必要がありました。つまり並列監督の設計とは、注意を向ける先を減らす設計だったのです。
画面は「進行中」「要判断」「完了」の三領域に分ける
そこで私は、エージェントの状態を三つの領域に物理的に分けて並べるようにしました。
- 進行中: いま自律で動いていて、人間の入力を待っていないもの。基本的に見ない
- 要判断: 確認や許可を待って止まっているもの。ここだけを能動的に見る
- 完了: 終わったもの。結果をまとめて検収する
この三分割の肝は、進行中の領域を意識的に見ないと決めることです。動いている最中のエージェントを覗き込んでも、できるのは不安になることくらいで、判断材料は増えません。私の体感では、進行中を見る時間をゼロに近づけたことで、要判断への反応がむしろ速くなりました。
デスクトップ側の設定では、各エージェントに状態がひと目で分かる接頭辞を付けています。
[RUN] refactor-auth … 進行中(触らない)
[WAIT] migrate-db-schema … 要判断(確認待ち)
[DONE] update-i18n-keys … 完了(検収待ち)
タスク名の頭にこの 3 状態を入れておくだけで、一覧をざっと眺めたときに「いま自分が見るべきは WAIT だけ」と即座に分かります。色分けより文字接頭辞のほうが、視界の端でも判別しやすいと感じています。
監督ボードを 40 行のスクリプトに落とす
接頭辞だけで数日運用してみて、足りない情報が一つあると分かりました。その状態にどれだけ放置されているかです。
WAIT が 1 分前に付いたのか 25 分前に付いたのかで、対応の緊急度はまったく違います。しかしエディタの一覧はどれも同じ濃さで並んでいるため、放置時間は目で見ても分かりません。そこで、状態を書いた 1 枚の TSV を用意して、それを並べ替えて表示する小さなスクリプトを別ディスプレイの隅に出すようにしました。
まず状態ファイルの形式です。1 行が 1 エージェントで、タブ区切りに状態・名前・その状態になった時刻・ブロックしている後続タスク数を並べます。
# state.tsv 状態<TAB>名前<TAB>状態になった時刻<TAB>後続ブロック数
WAIT migrate-db-schema 2026-06-15T19:58:00+09:00 2
RUN refactor-auth 2026-06-15T20:31:00+09:00 0
DONE update-i18n-keys 2026-06-15T20:12:00+09:00 0
WAIT fetch-store-metadata 2026-06-15T20:36:00+09:00 0
この形式にしたのは、エージェント側のフック(作業前後に走らせるコマンド)から printf で 1 行追記するだけで更新でき、手で直すのも苦にならないからです。JSON にすると手で直すときに壊しやすく、監督用の一時ファイルには重すぎました。
読み取って並べ替える側は次の通りです。
#!/usr/bin/env python3
"""board.py — 走らせているエージェントを WAIT 優先で並べ替えて表示する。
state.tsv の 1 行 = 1 エージェント。
状態<TAB>名前<TAB>状態になった時刻(ISO8601)<TAB>後続ブロック数
"""
from datetime import datetime, timezone
from pathlib import Path
STATE_FILE = Path.home() / ".antigravity" / "state.tsv"
ORDER = {"WAIT": 0, "DONE": 1, "RUN": 2}
STALE_MIN = 10 # WAIT がこの分数を超えたら印を付ける
def load(path):
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
cols = (line.split("\t") + ["", "", "", "0"])[:4]
state, name, since, blocking = cols
rows.append({
"state": state.upper(),
"name": name,
"since": datetime.fromisoformat(since),
"blocking": int(blocking or 0),
})
return rows
def minutes_since(ts):
now = datetime.now(ts.tzinfo or timezone.utc)
return int((now - ts).total_seconds() // 60)
def main():
rows = load(STATE_FILE)
for r in rows:
r["age"] = minutes_since(r["since"])
rows.sort(key=lambda r: (ORDER.get(r["state"], 9), -r["blocking"], -r["age"]))
for r in rows:
mark = "!" if r["state"] == "WAIT" and r["age"] >= STALE_MIN else " "
print(f"{mark}[{r['state']:<4}] {r['name']:<22} {r['age']:>4}分 後続{r['blocking']}")
if __name__ == "__main__":
main()
出力はこうなります。
![WAIT] migrate-db-schema 38分 後続2
[WAIT] fetch-store-metadata 0分 後続0
[DONE] update-i18n-keys 24分 後続0
[RUN ] refactor-auth 5分 後続0
なぜこの並べ替えなのかを補足します。ソートキーは (状態の優先度, -後続ブロック数, -経過分) の三段です。第一キーで WAIT を常に最上段に固定し、第二キーで「止まると他も止まるもの」を上に持ち上げ、第三キーで放置の長いものを優先します。到着順に並べないのは、後述する通り到着順が最も損をする並べ方だったからです。
! の印は 10 分以上放置された WAIT にだけ付きます。これは「気づいていない可能性が高い」ことの目印で、印が付いた時点で自分の巡回間隔が長すぎるという合図にもなります。
常時表示するには、ターミナルを 1 枚開いて次を回しておけば十分です。
while true; do clear; python3 ~/bin/board.py; sleep 20; done
20 秒間隔にしたのは、10 秒だと視界の端で数字が動きすぎて気が散り、60 秒だと ! が付いてから気づくまでの遅れが目立ったためです。この 1 枚を横に出しておくようにしてから、WAIT の発生に気づくまでの時間が、自分のストップウォッチ計測で平均 4 分半から 1 分前後まで縮みました。
割り込みは「待たせるコスト」で判断する
要判断のエージェントが複数同時に止まったとき、どれから捌くかが次の問題です。私は到着順ではなく、待たせることのコストで優先順位を決めています。
判断の目安は次の二つです。
- 後続作業をブロックしているか。これが止まると他の 2 体も進めない、というものは最優先
- 文脈が揮発しやすいか。ブラウザ操作の途中で止まっているものは、放置すると状態が古くなって最初からやり直しになりやすい
逆に、単発で他に影響しない確認は、個人的には後回しにして構わないと考えています。到着順に律儀に捌くと、ブロッキングなタスクが行列の後ろで待ち続け、全体のスループットが落ちます。実際、優先順位を到着順から「ブロッキング優先」に変えただけで、半日あたりに片付くタスク数が約 20% 増えました。
割り込み順を点数で決める
とはいえ「ブロッキング優先」だけでは、後続ブロックが同数のものが並んだときに迷います。迷う時間そのものが待たせるコストになるので、私は次の一次式で機械的に順番を出すようにしました。
優先度 = 後続ブロック数 × 10 + 揮発度 + 待ち分数 × 0.2
揮発度: ブラウザ操作・一時的な認証の途中 = 5
ローカルの編集途中 = 3
ファイルを書き終えて確認待ち = 0
係数の意図は単純です。後続ブロックは他の 2〜3 体を丸ごと止めるので桁を一つ上げ、揮発度は「やり直しになる確率」の代理値として中間に置き、待ち分数は同点のときの決着用に小さく効かせます。放置 50 分で 10 点なので、後続ブロック 1 件ぶんに追いつくまでに 50 分かかる重み付けです。
手元の 4 件で実際に計算するとこうなりました。
| エージェント | 後続ブロック | 揮発度 | 待ち分数 | 点数 | 捌く順 |
| migrate-db-schema | 2 | 0 | 38 | 27.6 | 1 |
| store-screenshot-upload | 0 | 5 | 12 | 7.4 | 2 |
| fetch-store-metadata | 0 | 5 | 0 | 5.0 | 3 |
| rename-test-fixtures | 0 | 0 | 21 | 4.2 | 4 |
到着順なら 4 番目の rename-test-fixtures が 2 番目に来ていました。点数化したことで、ブラウザ経由でストアに画像を上げている途中の store-screenshot-upload が先に来ます。ここを後回しにすると認証が切れて最初からやり直しになるため、体感の損失も式の結果と一致しました。
この式は厳密な最適化ではなく、迷いを消すための道具として使っています。点数が近いときはどちらでも大差がないので、そこは考えずに上から捌きます。判断に頭を使う場所を減らすほど、監督は楽になりました。
状態を失わせない割り込みの作法
止まっているエージェントに割り込むとき、雑に指示を足すと文脈が壊れます。私が守っているのは、割り込みの指示を「現在の方針への追記」として書くことです。
(悪い割り込み)
「やっぱり別のやり方にして」
(良い割り込み)
「ここまでの方針は維持したまま、認証部分だけは
セッション Cookie ではなくトークン方式に切り替えてほしい。
既存のテストは壊さないこと」
前者はエージェントにそれまでの文脈を捨てさせかねません。後者は「何を維持し、何を変えるか」を明示するので、進行中の状態を保ったまま方向だけを微修正できます。割り込みは方針の上書きではなく、差分の追記だと捉えると失敗が減ります。
書き方を型にしておくと、急いでいるときでも崩れません。私は次の 3 行を固定の骨格にしています。
維持: これまでの設計判断とテストは変更しない
変更: <対象> を <現状> から <新しい形> へ
制約: <壊してはいけないもの/触ってはいけない範囲>
3 行のうち「維持」を最初に書くのが要点です。冒頭に変更内容を書くと、そこから読み始めたエージェントが全体の作り直しに向かうことが何度かありました。順番を入れ替えただけで、割り込み後にゼロからやり直された回数が明確に減っています。
並列数の上限は自分の検収速度で決まる
最後に並列数の話です。私自身、欲張って 6 体まで増やしたことがありますが、完全に破綻しました。エージェントは並列で動けても、完了を検収するのは結局 1 人の人間だからです。
検収が追いつかないと、完了済みのエージェントが結果を抱えたまま積み上がり、要判断の合図に気づくのが遅れ、全体が詰まります。これは生産ラインで検査工程だけがボトルネックになるのと同じ構図で、本番運用で最も静かにスループットを削る落とし穴でした。回避するには、起動数ではなく検収数で上限を引く必要があります。
今は経験的に、並列数の上限を**「30 分以内に全部の完了を検収しきれる数」**に置いています。私の場合それは 3 体で、ごく軽いタスクが揃っているときだけ 4 体です。
検収ログから上限を実測する
「30 分で検収しきれる数」を勘で決めると、調子のよい日を基準にしてしまいます。そこで 3 週間ほど、検収に何分かかったかを 1 行ずつ記録してみました。記録は検収を終えた直後に 1 コマンドです。
# 検収を終えたら 1 行追記する
printf '%s\t%s\t%s\n' "$(date +%F)" "review-pr-1284" "9" >> ~/review_log.tsv
集計側は中央値と p90 の両方を出します。中央値だけを見ていると、重いタスクが来た日に必ず詰まるからです。
#!/usr/bin/env python3
"""ceiling.py — 直近の検収ログから並列数の上限を出す。
review_log.tsv: 日付<TAB>タスク名<TAB>検収に要した分
"""
import statistics
import sys
from pathlib import Path
BUDGET_MIN = 30 # 1 巡回で検収に充てられる時間
def main(path):
mins = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
cols = line.split("\t")
if len(cols) < 3:
continue
try:
mins.append(float(cols[2]))
except ValueError:
continue
if not mins:
print("検収ログが空です")
return 1
med = statistics.median(mins)
p90 = statistics.quantiles(mins, n=10)[8] if len(mins) >= 10 else max(mins)
print(f"件数={len(mins)} 中央値={med:.1f}分 p90={p90:.1f}分")
print(f"上限の目安: 中央値基準 {int(BUDGET_MIN // med)} 体 / p90 基準 {int(BUDGET_MIN // p90)} 体")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else str(Path.home() / "review_log.tsv")))
私の 3 週間分(142 件)では、中央値 7.4 分・p90 16.2 分でした。式に入れると中央値基準で 4 体、p90 基準で 1 体になります。実運用で落ち着いたのは 3 体で、これは二つの数字のあいだを取った形です。
数字の意味を取り違えないよう補足すると、p90 基準の 1 体は「重い検収が続いた日は 1 体しか回せない」という意味であって、常時 1 体にすべきという意味ではありません。私はこの二つを、平常時は中央値基準、レビューの重い作業を含む日は p90 側に寄せる、という使い分けにしています。
起動数を変えながら記録した半日あたりの実績はこうなりました。
| 同時起動数 | 半日あたり完了数 | 差し戻し率 | WAIT 平均放置 |
| 2 体 | 5.5 件 | 7% | 2 分 |
| 3 体 | 7.0 件 | 9% | 3 分 |
| 4 体 | 7.2 件 | 18% | 9 分 |
| 6 体 | 4.8 件 | 31% | 27 分 |
3 体から 4 体への増加で完了数はほとんど伸びず、差し戻し率が倍になっています。検収が雑になった結果です。6 体では 2 体のときより完了数が下回りました。並列数はどこかで頭打ちになるのではなく、上限を超えると純粋に悪化するという形をしていました。
詰まったときに戻す 5 手順
上限を決めても、App Store 提出前の週のように差し込みが多い時期には詰まります。実際に 6 体で完全に止まったとき、私は次の順で平常運転に戻しました。所要はおよそ 50 分でした。
- 新規起動を止める。まずこれをしないと、以降の作業が追いつきません
- DONE を古い順に検収する。新しいものから見たくなりますが、古いものほど文脈を思い出す時間が余計にかかり、放置するほど不利になります
- 後続ブロック 0 の WAIT にまとめて方針を返す。「現在の方針のまま続行して、判断が必要なら最後にまとめて聞いてほしい」と返し、止まっている数そのものを減らします
- RUN のうち 30 分以上出力が変わらないものを打ち切る。多くは外部待ちか堂々巡りで、待っても状況は変わりませんでした
- 並列数を 1 減らして再開する。詰まった直後に元の数へ戻すと、同じ日のうちにもう一度詰まります
この 5 手順のうち、効きが大きかったのは 3 番でした。要判断が 4 件並んでいる状態そのものが判断を鈍らせていて、実際には 3 件は「そのまま進めてよい」で済むものだったのです。詰まりの原因は処理量ではなく、判断待ちの件数だったという見立てです。
まず試してほしいこと
次にエージェントを 2 体以上動かすときは、タスク名の頭に [RUN] [WAIT] [DONE] を付けるところから始めてみてください。それだけで「いま見るべき一つ」が浮かび上がります。慣れてきたら state.tsv と board.py を足して、放置時間を見えるようにするのが次の一歩です。
私もまだ最適な並列数を探っている途中ですが、監督とは注意を配ることではなく、配る先を絞ることだと分かってから、複数エージェントの運用がずいぶん楽になりました。同じように管制塔の前で疲れている方の参考になれば嬉しいです。お読みいただきありがとうございました。