第 44 章:ツールの並列実行

「読み取り専用なら並列に回せる」——読み取り専用と並行安全は同じ性質ではなく、その隙間に事故が住んでいる。


5 分で要点掴む

  1. 並行安全性でバッチを組む。読み取り専用性ではない——別の問いです
  2. 安全性はその呼び出しの性質であって、ツール名の性質ではない
  3. bash が難所:コマンド文字列そのものを分類し、認識できないものは fail closed
  4. シェルのメタ文字は——改行を含めて——すべて失格にする
  5. 並行結果には tool_use_id の対応付けが要る。ないと UI は出力を別の呼び出しに紐づける

10 分パス:44.1-44.3 → 44.5 → Kocoro OSS Lab


公開ソースのスナップショット:実装例は 2026-07-27 に確認した Kocoro 公開 origin/main のコミット 4ec6772 に基づきます。学ぶべき対象は不変条件であり、定数は最新ソースで再確認してください。

44.1 たった 1 語で変わる話

モデルが 1 つの Turn で 4 つの bash 呼び出しを出します。4 つとも読み取りです。分類器は実際にはこう扱います。

bash("git log --oneline -10")              並行バッチ
bash("cat config.yaml")                    並行バッチ
bash("git stash list")                     並行バッチ
bash("git config --global core.editor")    直列、サイズ 1 のバッチ

4 つ目も本当に読み取りです。git config --global core.editor は値を与えなければ設定を問い合わせるだけで、何も変えません。ではなぜ引き剥がされたのか。

分類器がそれを読み取りだと証明できないからです。git config は値を 1 つ足した瞬間に書き込みになります。git config --global user.email me@example.com と上の問い合わせの差はトークン 1 つ。そして分類器は明示的な読み取り専用の形(--get--list とその変種)しか認識しません。config サブコマンドのそれ以外はすべて拒否です。

これは偽陰性です。代償はわずかなレイテンシだけ。逆を考えてみてください。分類器が config 一族に寛容だったら、git config --global user.email xgit stash pop と並んで並行バッチに滑り込み、2 つの git 操作が共有状態を同時に書き換えます。この非対称性が設計のすべてです。 「直列」側に外せばミリ秒、「並行」側に外せばリポジトリを失います。

その下にあるのは、あまり切り分けられない問いです。「読み取り専用」は誤った問いに答えています。読み取り専用が問うのは状態を変えるか。並行安全が問うのは兄弟たちと同時に走って干渉しないか。この 2 つは両方向に分岐します。排他ロックを取る読み取りは読み取り専用ですが並行安全ではありません。まったく別のリソースへの 2 つの書き込みは読み取り専用ではありませんが、一緒に走らせて何の問題もありません。

だからディスパッチャは、本当に気にしている問いでバッチを組むべきです。

44.2 並行安全は「その呼び出し」の性質

公開 Runtime は読み取り専用フラグではなく並行安全チェックでツール呼び出しを分割します。並行 Trait を実装していないツールは読み取り専用の値にフォールバックするので、既存ツールの挙動は変わらず、1 つのツールに Trait を足しても他のどのツールにも影響しません。

このフォールバック設計自体が注目に値します。新しい分類軸は旧来の挙動へ既定で落ちるべきです。そうでなければ、導入は「ツール単位の改善」ではなく「システム全体のリスク」になります。

肝心なのは呼び出しという語です。file_read はどんな引数でも並行安全。bash は引数文字列によって安全でもあり、破滅的でもあります。名前単位の許可リストではこれを表現できません。だからこのチェックはツール名だけでなく引数を受け取ります。

44.3 Bash と、fail closed の規律

任意のシェルを分類するのが難所で、公開実装は自らの姿勢を直接書いています。

It is intentionally conservative: any shell metacharacter, any unknown leading token, or any unrecognized subcommand pattern returns false.

3 つのフィルタ、すべて通過が必要です。

シェルのメタ文字がないこと。 パイプ、リダイレクト、&&、コマンド置換——そして重要なことに、改行とキャリッジリターン。改行はコマンド区切りです。改行を含む「単一コマンド」は実は 2 つのコマンドで、2 つ目はあなたが一度も分類していません。この隙間は、モデルがコマンドを複数行に整形したときにだけ表に出ます。

既知の先頭トークン。 厳密な読み取り専用の許可リスト。git pushnpm installcurlrm——リストにないものは無条件で直列。知らない=安全でない、です。

認識できるサブコマンドの形。 git のような多目的バイナリでは先頭トークンだけでは足りません。グローバルオプションを読み飛ばし、サブコマンドを安全集合と照合し、出力や外部変更を示唆する引数パターンは失格にします。

それ以外はサイズ 1 のバッチで走ります。拒否ではなく、直列なだけです。この区別は重要で、保守的な分類器は作業を止めることはなく、並列化を辞退するだけです。偽陰性のコストはレイテンシ、偽陽性のコストは破損です。 それに合わせて調整してください。

44.4 並行結果には身元が要る

並列が実際に動き出した瞬間に噛みついてくる二次的な問題があります。

直列実行なら結果は呼び出し順に届くので、UI は位置で対応付けできます。4 つを並行で走らせれば完了順は乱れます。git log が終わったのに cat はまだ走っている。イベントストリームが「ツールが 1 つ終わった」としか言わないなら、UI はどのカードを更新すべきか分かりません。

そこでツールのライフサイクルイベントは tool_use_id を運び、クライアントは到着順ではなくその id で対応付けます。Daemon はこれをハンドシェイクで capability トークンとして通知します。第 33 章のパターンそのものです。クライアントが検知しなければならないワイヤ契約の変更にはトークンを鋳造し、バージョン推測は決してしない。

既定で並行 bash を有効にした公開設定コメントの時系列に注目してください。

Phase C: Desktop now consumes tool_use_id on tool_status events, safe to enable concurrent bash batches by default.

機能を作り、クライアントが id で対応付けられるようになり、その後で既定値を反転させています。消費側が結果を帰属できないうちに並列を有効にすると、「正しいデータを誤った呼び出しに表示する」UI ができます。それは並列なしより悪い。遅いのではなく、誤っているからです。

44.5 スナップショットの根拠

観測4ec6772 のソース位置
読み取り専用ではなく並行安全でバッチを決めるpartition.go L41
読み取り専用フォールバックが既存挙動を保つpartition.go L28
分割とバッチ実行partition.go L51
保守的な bash 分類器bash_concurrency.go L61
git サブコマンドの安全判定bash_concurrency.go L166
出力・変更を示す引数パターンは失格bash_concurrency.go L115
クライアントが tool_use_id を消費してから既定で有効化config.go L449

これらは特定時点の実装例であり、普遍的な契約ではありません。

44.6 並列のコスト

並行は無料ではなく、そのコストのうち 3 つは見落とされがちです。

承認の順序。 バッチ内の 2 つの呼び出しが両方承認を要するなら、ユーザーには順序保証のないプロンプトが 2 つ出ます。承認はバッチ実行のに解決すべきで、バッチの中で競争させるものではありません。

結果のサイズ。 4 つの並行ツールは 4 つの大きな結果を同時に返せます。第 36 章の Turn 単位の合計上限が 4 倍の速さで効きます。並列はコンテキスト圧を加速します。

デバッグしやすさ。 直列実行はきれいな因果トレースをくれます。並行実行は 4 つの呼び出しのログを交錯させ、追っている障害はその交錯のどこかにあります。バッチを小さく、分類を保守的に保つ本当の理由はこれです。安全性だけでなく、後から何が起きたか理解できるかどうか。

本章が主張していないことも書いておきます。第 32 章はより古い「受け入れてから並列実行」モデルを描いています。現在の分類はより細粒度で、呼び出し単位です。あの章は、自ら宣言しているとおり歴史的スナップショットとして読んでください。

44.7 よくある落とし穴

読み取り専用を安全の代理指標にする。 2 つの性質は両方向に分岐します。

ツール名で分類する。 bashcat なら安全、git push なら危険。名前単位の判断では表現できません。

改行がメタ文字であることを忘れる。 複数行の「単一コマンド」は複数コマンドで、検査したのは最初の 1 つだけです。

多目的バイナリを丸ごと許可する。 git は 1 つのコマンドではありません。npmdockerkubectl も同様です。

結果の身元より先に並列を出す。 UI が出力を誤った呼び出しに紐づけ、ユーザーは表示全体への信頼を失います。

テストが通れば安全だと考える。 並行バグはタイミング依存です。テストが緑なのは「今回は起きなかった」という意味にすぎません。保守的な分類こそが実際の防御です。

Kocoro OSS Lab(10 分)

  1. IsCommandConcurrencySafe を読み、コマンドが拒否されうる経路をすべて列挙します。そのうち何個が「認識できない」で、何個が「危険だと知っている」かを数えてください。
  2. gitSubcommandSafe を見て、dockerkubectl 向けに同種の関数を書くなら何を覆う必要があるか考えます。
  3. 自分のディスパッチャを見ます。読み取り専用でバッチを組んでいますか、並行安全で組んでいますか。前者なら、2 つの答えが食い違うツールを 1 つ探してください。どのコードベースにも 1 つはあります。

ここだけは押さえる

  1. 並行安全でバッチを組む。読み取り専用ではない。 異なる問いに答え、両方向に分岐します。
  2. 安全性はその呼び出しに属する。 同じツールでも、引数によって安全だったりそうでなかったりします。
  3. 認識できないものは fail closed。 偽陰性はレイテンシ、偽陽性はデータを失います。
  4. 改行はメタ文字。 複数行コマンドは複数コマンドです。
  5. 並列より先に結果の身元を出荷する。 tool_use_id の対応付けがなければ、並行出力は誤ったカードに着地します。

次章:第 45 章は、同じ予算の規律を最も高価な結果——スクリーンショット——に適用します。

この記事を引用する / Cite
Zhang, Wayland (2026). 第 44 章:ツールの並列実行. In AI Agent アーキテクチャ:単体からエンタープライズ級マルチエージェントへ. https://waylandz.com/ai-agent-book-ja/%E7%AC%AC44%E7%AB%A0-%E3%83%84%E3%83%BC%E3%83%AB%E3%81%AE%E4%B8%A6%E5%88%97%E5%AE%9F%E8%A1%8C/
@incollection{zhang2026aiagent_ja_44_-,
  author = {Zhang, Wayland},
  title = {第 44 章:ツールの並列実行},
  booktitle = {AI Agent アーキテクチャ:単体からエンタープライズ級マルチエージェントへ},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book-ja/%E7%AC%AC44%E7%AB%A0-%E3%83%84%E3%83%BC%E3%83%AB%E3%81%AE%E4%B8%A6%E5%88%97%E5%AE%9F%E8%A1%8C/}
}