---
type: concept
topic: architecture
audience: [human, agent]
---

# LLM invocation surfaces — Router-style vs Phase-style

## 1. なぜこれが重要か

Reyn は LLM を2つの異なる文脈で呼び出す。チャットルーター（および plan-mode バリアント）と、Skill 内部の Phase executor である。各文脈はそれぞれ固有の機能語彙を持つ — ルーターには function calling tools、Phase には Control IR ops。この2つのセットは大きく重なるが、完全には一致しない。この乖離を明文化しなければ、貢献者は便宜上どちらかの surface に新機能を追加し続け、差異は静かに広がる。本ドキュメントは2つの invocation kind を命名し、乖離を整理し、どの非対称性に原則的根拠があるか・どれが convention drift かを識別する。これにより、将来の追加が正しい場所に着地し、意図しない非対称性が積み重なる前に表面化するようになる。

---

## 2. 2つの invocation kind

### 2.1 Router-style（チャット・planner）

**使用箇所:** `RouterLoop`（インタラクティブチャットセッション）および `PlanRuntime`（plan-mode ステップ実行）。両者は単一実装を共有する — コンテキストごとにカタログを絞り込む `RouterLoopHost` facade を用いた `RouterLoop`。

**仕組み:** litellm 経由の `call_llm_tools` によるネイティブ LLM function calling。ツール定義は OpenAI `tools` 配列形式に従い、モデルはアシスタントメッセージ内の `tool_calls` で応答する。OS は各呼び出しをディスパッチし、`tool_result` を追記して、モデルが通常テキストを返すまで LLM を再呼び出しする。

**ツール surface:** `src/reyn/chat/router_tools.py` の `build_tools()` がツールリストを組み立てる。実際の数はオペレーター設定に依存する。

- **常時存在（14 tools）:** `list_skills`, `describe_skill`, `list_agents`, `describe_agent`, `list_memory`, `read_memory_body`, `delegate_to_agent`, `remember_shared`, `remember_agent`, `forget_memory`, `web_search`, `plan`, `reyn_src_list`, `reyn_src_read`
- **条件付き（+0〜+9 tools）:** `invoke_skill`（Skill 登録時）、`list_directory` + `read_file`（file read スコープ設定時）、`write_file` + `delete_file`（file write スコープ設定時）、`web_fetch`（オペレーターのオプトイン）、`list_mcp_servers` + `list_mcp_tools` + `call_mcp_tool`（MCP servers 設定時）
- **実測レンジ: 14–23 tools**（`router_tools.py` のコメントに記載の「11–18」は `web_search`、`plan`、`reyn_src_list`、`reyn_src_read` 追加前の記述であり、現在は stale）

**Plan-mode は同一 surface から `plan` 自身を除いたもの:** `PlanRuntime` は `execute_plan` をラップし、`exclude_tools={"plan"}` を指定した `RouterLoop` を内部で使用する。これにより再帰的な plan 分解を防ぐ。親セッションで使用可能な他のすべての router tool は各 plan step で利用可能であり、step の `tools` リストでさらに絞り込まれる。

**役割:** オーケストレーション — 次のサブコンポーネント（Skill、Agent、plan、メモリ操作、直接テキスト応答）を選択する。

### 2.2 Phase-style（Skill 実行）

**使用箇所:** `OSRuntime` が駆動する、Skill 内すべての Phase 呼び出し。

**仕組み:** JSON output contract。LLM は単一の構造化応答を返す。

```json
{
  "control": {"type": "transition|finish|abort", "decision": "continue|finish|abort",
               "next_phase": "<name> or null", "confidence": 0.0, "reason": {}},
  "artifact": {"type": "<schema_name>", "data": {}},
  "control_ir": []
}
```

ネイティブ function calling はない。LLM は意図するサイドエフェクトを型付き op オブジェクトとして `control_ir` に宣言し、OS がそれらをディスパッチする。

**Op surface:** `src/reyn/op_runtime/registry.py` の `OP_KIND_MODEL_MAP` で定義された 8 種類の Control IR op kind。

| Op kind | 目的 |
|---------|------|
| `file` | ファイルの読み取り・書き込み・glob・grep・編集・削除 |
| `mcp` | 設定済み MCP server のツールを呼び出す |
| `run_skill` | 別の Skill をネストされたワークフローとして呼び出す |
| `shell` | シェルコマンドを実行する |
| `lint` | Skill ディレクトリに DSL linter を実行する |
| `ask_user` | Phase を一時停止してユーザーに入力を求める |
| `web_fetch` | 単一 URL を取得する |
| `web_search` | 公開ウェブを検索する |

各 Phase はさらに Phase 宣言の `allowed_ops: list[str]`（デフォルト: `["file", "ask_user"]`）によってこのセットを絞り込む。OS は defense-in-depth として `allowed_ops` をディスパッチ時に強制する。

**役割:** ドメイン作業 — 次の Phase または Skill の最終出力として artifact を生成する。

### 2.3 第3の invocation kind ではないもの

2つのコンストラクトは同じ Phase 実行コンテキストに登場するため、LLM invocation kind と混同されることがある。

**Preprocessor steps**（`run_skill` / `iterate` / `validate` / `lint_plan` / `python`）は Phase LLM 呼び出しの前に決定論的に実行される。それ自体は LLM を呼び出さない。`python` ステップはサンドボックス化された Python 関数を実行する。`run_skill` ステップは sub-skill を再帰的にディスパッチし、その sub-skill には Phase-style で LLM を呼び出す Phase が含まれるが、preprocessor ステップ自体は同期的な OS 制御であり、preprocessing 層から LLM 呼び出しは行わない。[../skills/preprocessor.md](../skills/preprocessor.md) 参照。

**Postprocessor steps**（同一 step types）は LLM の `finish` 出力の後、artifact が呼び出し元に返される前に決定論的に実行される。LLM 呼び出しではない。[../skills/postprocessor.md](../skills/postprocessor.md) 参照。

両者はOS が実行する決定論的パイプラインであり、LLM invocation ではない。

---

## 3. 機能比較マトリックス

| 機能 | Router-style surface | Phase-style surface | 状態 |
|------|---------------------|---------------------|------|
| ファイル読み取り | `read_file`（file read 権限付与時） | `file` op (op=read) | Symmetric |
| ファイル書き込み・削除 | `write_file`, `delete_file`（file write 権限付与時） | `file` op (op=write/delete) | Symmetric |
| ディレクトリ一覧 | `list_directory` | `file` op (op=glob) | Symmetric |
| Web 検索 | `web_search`（常時） | `web_search` op | Symmetric |
| Web フェッチ | `web_fetch`（オペレーターのオプトイン） | `web_fetch` op | Symmetric |
| MCP call_tool | `call_mcp_tool`（mcp_servers 設定時） | `mcp` op | Symmetric |
| MCP 検索（サーバー・ツール一覧） | `list_mcp_servers`, `list_mcp_tools` | 利用不可 | Gap (Type C) |
| Shell | 利用不可 | `shell` op | Role-separated (Type B) |
| Lint | 利用不可 | `lint` op | Role-separated (Type B) |
| Skill の実行・呼び出し | `invoke_skill`（Skill 登録時） | `run_skill` op | Symmetric |
| Agent 間委任 | `delegate_to_agent` | 利用不可 | Role-separated (Type B) |
| ユーザーへの質問 | ツールとしては存在しない（router はテキスト応答で終了） | `ask_user` op | Role-separated (Type B) |
| メモリ読み取り | `list_memory`, `read_memory_body` | context_builder による注入のみ（Phase 開始時のスナップショット） | Gap (Type C) |
| メモリ書き込み | `remember_shared`, `remember_agent`, `forget_memory` | 利用不可 | Gap (Type C) |
| カタログ閲覧 | `list_skills`, `describe_skill`, `list_agents`, `describe_agent` | ContextFrame の `op_catalog` 注入のみ（mid-phase クエリ不可） | Gap (Type C) |
| Plan 呼び出し | `plan` | 利用不可（Phase 内分解には `run_skill` を使用） | Role-separated (Type B) |
| Reyn ソース読み取り | `reyn_src_list`, `reyn_src_read` | 利用不可 | Router-only |

---

## 4. 4つの乖離タイプ

### Type A — 健全な対称性

両側に同一のセマンティクスで存在するが、呼び出し形式が異なる機能（function calling vs Control IR JSON）。これらは問題ではなく、2つの API スタイルから生じる自然な結果である。

**例:** file ops（`read_file` ↔ `file/read`）、web ops（`web_search` / `web_fetch` ↔ `web_search` / `web_fetch` ops）、MCP 呼び出し（`call_mcp_tool` ↔ `mcp` op）、Skill 呼び出し（`invoke_skill` ↔ `run_skill` op）

router LLM は `invoke_skill("name", input={...})` を呼び出し、Phase LLM は `{"kind": "run_skill", "skill": "name", "input": {...}}` を emit する。OS は両方をディスパッチする。対称性は実在しており、surface 形式が異なるのは2つの invocation kind が異なるプロトコルを使用しているためである。

### Type B — 意図的な役割分離

原則的な理由で存在し、非対称のままであるべき非対称性。

- **`delegate_to_agent` は router-only。** Phase は Skill スコープ内で動作する。ピア Agent へのリクエストルーティングはチャットセッションに属するオーケストレーション上の決定であり、Phase 実行途中のものではない。Phase 内部から agent 委任を許可することは、オーケストレーション層（セッション）とドメイン作業層（Phase）を混同させる。

- **`plan` は router-only。** Phase には in-phase 分解のための `run_skill` がすでにある。`plan` ツールはルータターンをまたぐ multi-source 合成のためのチャットセッション機構であり、Phase は定義済みの input/output コントラクトを持つため Phase 内に対応物はない。

- **`shell` は Phase-only。** `shell` をチャットルーターに直接公開すると、スキーマ境界なしの自由形式の会話コンテキストで LLM が任意コマンドを実行できてしまう。Phase モデルはこれを制約する — `shell` は Skill ごとにオプトイン、`allowed_ops` でゲート、Phase の input schema がコマンドに到達するデータを絞り込む。

- **`lint` は Phase-only。** Lint は Phase 中に LLM の Skill オーサリング出力を検証する。Skill artifact を生成しないチャットルーターには用途がない。

- **`ask_user` は Phase-only の明示的 op。** router LLM はプレーンテキスト応答を emit することでユーザーに質問する — `RouterLoop` はそのテキストで終了する。Phase LLM は mid-phase で終了できないため、`control_ir` 内の `ask_user` を使用して一時停止し OS に質問を表面化する必要がある。

### Type C — Convention drift

原則なしに時間とともに生まれた非対称性であり、役割ベースの強い根拠がないもの。

- **メモリ I/O が router-only。** `list_memory`、`read_memory_body`、`remember_shared`、`remember_agent`、`forget_memory` がチャットルーターで利用可能。Phase は Phase 開始時に context_builder 経由で注入されたメモリを受け取る（読み取り専用スナップショット）が、mid-phase でメモリの照会・更新はできない。Phase がメモリを書き込めない原則的なアーキテクチャ上の理由はない — このギャップはメモリツールが直接ユーザーインタラクション用にルーターへ追加され、対応する Phase 機能が設計されなかったことで生じた。

- **カタログ閲覧が router-only。** `list_skills`、`describe_skill`、`list_agents`、`describe_agent` がチャットルーターで利用可能。Skill やエージェントのカタログデータが必要な Phase（例: `eval_builder` や `skill_improver`）は ContextFrame データ（`op_catalog`）として注入されたカタログを受け取るが、mid-phase カタログクエリは発行できない。このギャップはカタログ閲覧が主にルーターの「どの Skill を呼び出すか」決定に有用だったため生じた。

- **MCP 検索が router-only。** `list_mcp_servers`、`list_mcp_tools` がチャットルーターで利用可能。`mcp` op を使用する Phase は `control_ir` にサーバー名とツール名を静的に宣言しなければならない。このギャップは MCP 閲覧がルーターのインタラクティブな「MCP で何ができるか」ユースケースのために追加され、Phase 側 `mcp` op への対応する検索機構なしに実装されたことで生じた。

これらはギャップであり、失敗ではない。閉じるかどうかはセクション 6 の doctrine 問題が決定する。

### Type D — LLM 呼び出し前の決定論的ステップ

Preprocessor および postprocessor ステップは LLM invocation ではないが、「Skill オーサーが使えるもの」として機能比較の議論に登場する。区別が重要:

- `python` preprocessor ステップはサンドボックス化された Python コードを実行する — LLM 呼び出しなし
- `run_skill` preprocessor ステップは sub-skill を呼び出し、その Phase が Phase-style で LLM を呼び出す — ただし preprocessor ディスパッチ自体は同期的であり OS 制御であり、同一ターンでの LLM 呼び出しではない
- `validate` ステップは JSON Schema チェックを実行する — LLM 呼び出しなし

Preprocessor と postprocessor ステップは LLM 呼び出しの前後に Phase が計算できることを拡張するが、第3の invocation kind を構成しない。

---

## 5. なぜ乖離が生まれたか — 歴史的パターン

チャットルーターは新機能が追加されるたびにツール追加で機能を蓄積してきた — メモリ I/O、カタログ閲覧、web ops、plan モード、Reyn ソースアクセス。各追加はそのコンテキストで自然だった（チャットユーザーがメモリについて直接質問したい、インタラクティブにカタログを閲覧したい、会話ターンでウェブを検索したい）。Phase Control IR op セットはより保守的に成長した（最大 23 router tools に対して 8 op kind）— Reyn の Phase モデルが制約された candidate set（P4）と Skill オーサーの意図を重視するためである。Phase は何を行うことが許可されているかを宣言し、それ以上ではない。結果として router はインタラクティブ探索機能を蓄積し、Phase surface はドメイン作業に焦点を絞ったまま残った。これは役割分離の理由が妥当な場合（Type B）は適切だが、そうでない場合（Type C）は convention drift である。

---

## 6. Doctrine オプション

問い: **Convention drift のギャップ（Type C）は閉じるべきか？** このセクションは3つのオプションを提示する。選択は別の決定事項であり、本ドキュメントはフレームワークを確立する。

### Option 1 — 完全な対称性

すべての機能を両 surface で適切な呼び出し形式で利用可能にする。Type B の例外（shell、lint、ask_user、plan、delegate_to_agent）は文書化された例外として保持する。

- **Pros:** クリーンな doctrine; 明示的な機能別選択以外の非対称性なし; 貢献者はシンプルなデフォルトルール（「理由がない限り両方に追加する」）を持てる
- **Cons:** 一部の機能は両側に自然にフィットしない（Phase 実行途中にピア agent へ委任することはオーケストレーションとドメイン作業を混同する）; surface area が増大; 新機能ごとに2-surface 実装が必要

### Option 2 — 役割ベースの非対称性（現状を追認）

現在の非対称性を doctrine として文書化する。Router はオーケストレーション、Phase はドメイン作業、機能は役割のどちらか一方にのみ属する。Type C ギャップはそのまま受け入れる。

- **Pros:** 変更最小; 既存の動作を明文化; 貢献者は明確なルール（「これはオーケストレーションかドメイン作業か？」）を持てる; 実装コストなし
- **Cons:** Type C ギャップを再検討せずにゴム印を押す; Phase からのメモリ書き込みは正当なニーズだがこのオプションでは未解決; より複雑な Skill がより豊富な Phase 側機能を必要とするにつれて doctrine が陳腐化する可能性

### Option 3 — ハイブリッド: Type C のみ閉じる

Type B には Option 2 の役割分離を採用しつつ、3つの Type C convention drift ギャップを明示的に閉じる。

- **Phase からのメモリ書き込み:** 新しい `memory` op kind（または `update_memory` のような stdlib skill）により、Phase がチャット層を経由せずに永続的な事実を書き込める
- **Phase からのカタログ閲覧:** stdlib skill（例: `recall_skill_catalog`）により Phase が `run_skill` 経由でライブカタログを mid-phase クエリできる（OS にカタログ知識を埋め込まずに）
- **Phase からの MCP 検索:** `mcp` op を `action=list_servers` および `action=list_tools` バリアントで拡張し、Phase が実行時に利用可能な MCP 機能を探索できる

- **Pros:** 原則的 — 役割分離が実在する場合は役割ベース（Type B）、ギャップが意図しないものだった場合は対称（Type C）; doctrine に技術的負債が蓄積されない; 新機能が最初から両 surface を念頭に設計される
- **Cons:** 中程度の実装コスト（3つの新機能）; 順序が重要（phase op 拡張より前に stdlib skills）; 次の追加バッチで drift を再生成しないよう規律が必要

---

## 7. 既存の原則との接続

**P3（OS が実行を制御する）** — 両 invocation kind は OS が仲介する。router LLM はツールを呼び出し、OS がディスパッチする。Phase LLM は `control_ir` を emit し、OS がそれらの op をディスパッチする。どちらの surface も LLM が直接実行することは許可しない。Doctrine の問いは OS が各 kind にどの機能を公開するかについてであり、誰が実行を制御するかではない。

**P4（LLM は制約された決定エンジン）** — 両 invocation kind は厳選された candidate set を提示する。router LLM は `build_tools()` で組み立てられた固定ツールリストを見る。Phase LLM は Phase の `allowed_ops` から構築された `available_control_ops` を見る。Doctrine は各 kind がどの候補を見るかについてであり、P4 は両側に等しく適用される。

**P7（OS は Skill に依存しない）** — どちらの surface も Skill 固有の知識を埋め込むべきではない。stdlib skills を通じて Type C ギャップを閉じる（Option 3 パス）ことで P7 を保全する — OS は汎用の `memory` op や `run_skill` 機構を公開し、Skill オーサーがそれを使用するかどうかを決める。Skill 固有のメモリキーやカタログパスを OS 層に埋め込むことは P7 違反となる。

---

## 8. 関連ドキュメント

- [../architecture/principles.md](../architecture/principles.md) — P3、P4、P7
- [../architecture/architecture.md](../architecture/architecture.md) — コンポーネント全体の階層化とランタイムループ
- [../architecture/phase-vs-skill-vs-os.md](../architecture/phase-vs-skill-vs-os.md) — Phase・Skill・OS 間の責任境界
- [../architecture/care-boundary.md](../architecture/care-boundary.md) — Reyn が担うこと・担わないこと; downstream tooling セクションは上記マトリックスを補完する
- [../skills/preprocessor.md](../skills/preprocessor.md) — LLM 呼び出し前の決定論的ステップ（= 第3の invocation kind ではない理由）
- [../skills/postprocessor.md](../skills/postprocessor.md) — LLM 呼び出し後の決定論的ステップ（同理由）
- [../../reference/runtime/control-ir.md](../../reference/runtime/control-ir.md) — Phase 側 op の語彙とセマンティクス
- [../../reference/cli/chat.md](../../reference/cli/chat.md) — チャットで使用可能なスラッシュコマンド（router tools と混同されることがあるが別物）
- [../../reference/cli/mcp.md](../../reference/cli/mcp.md) — MCP サーバー側（Reyn-as-MCP-server は外部クライアントが Reyn を呼び出す第3の surface を公開するが、Reyn 内部の LLM invocation kind ではないため本ドキュメントでは扱わない）

---

## 9. 実装: 統合 tool registry（ADR-0026 Accepted — M4 完了、 router/phase 両 surface が registry を消費）

本ドキュメントで説明した二重実装アーキテクチャ（`router_tools.py` / `OP_KIND_MODEL_MAP` の 2 つのカタログ）は歴史的ベースラインである。
ADR-0026（ステータス: Proposed）は、1 つの `ToolDefinition` に 2 つの render メソッドを持たせることで構造的なドリフトを解消する。

**M1（着地済み — commit `edd4c1b`）:** インフラモジュール `src/reyn/tools/` が存在する:

- `ToolDefinition`, `ToolGates`, `ToolContext`, `ToolHandler`, `ToolResult` — `src/reyn/tools/types.py`
- `ToolRegistry` — `src/reyn/tools/registry.py`
- `invoke_tool`, `ToolNotFound`, `ToolGateRefused` — `src/reyn/tools/dispatch.py`

**M2 POC（着地済み — commit `367b41c`）:** `web_search` が統合 registry に移行された最初のケーパビリティである。`build_tools()` は `render_for_router()` 経由で registry から `web_search` を導出し、従来の `ToolSpec` リテラルとバイト同一の出力を生成する（LLMReplay フィクスチャは変更なし）。すべての M2 検証ゲートが通過: byte-identity GREEN、drift test GREEN、フルスイート 1500 passed / 2 xfailed、mkdocs strict エラーなし。

**M3 Wave 1（着地済み — commit `ba4c5fe`）:** 7 ケーパビリティを移行 — `web_fetch`、`shell`、`lint`、`ask_user`、`delegate_to_agent`、`plan`、`reyn_src_list`、`reyn_src_read`。`ToolDefinition` に `dispatch_kind` フィールドを追加。Tier 2 invariant +99。

**M3 Wave 2（着地済み — commit `66435d1`）:** 17 ケーパビリティを移行 — file ops × 4 / MCP ops × 3 / memory ops × 5 / catalog ops × 4 / `invoke_skill`。§4 で識別した Type C convention-drift の 3 つのギャップをすべて `gates(router=allow, phase=allow)` で宣言的にクローズ（memory write phase-side、catalog browse phase-side、MCP discover phase-side）。Tier 2 invariant +127。全移行を通じて LLMReplay fixtures を保持。`reyn web` A2A エンドポイントのサニティチェックにより実 LLM リグレッションなしを確認。

13 ケーパビリティクラスター（= 26 ToolDefinitions）すべてが unified ToolRegistry に登録済みである。§4 で識別した Type C convention-drift のギャップは `gates(router=allow, phase=allow)` で宣言的にクローズされている。Phase-side Control IR dispatch が registry を消費するように配線する作業は M4 cleanup の範囲である。

**M4 Phase 2（着地済み）:** ToolContext の型拡張 — `router_state` と `phase_state` が loose `Any` から型付き sub-object（`RouterCallerState` / `PhaseCallerState`）に変わり、ADR-0026 Open Question #3 を解決。全フィールドはデフォルト `None` で段階的移行に対応。Tier 2 invariant +7。

**M4 Phase 3 step 1（着地済み）:** ハンドラ活性化 + per-call schema enrichment hook。6 つの design-revisit `NotImplementedError` stub（catalog 4 件 + `delegate_to_agent` + `plan`）が型付き `RouterCallerState` の callable フィールド経由で delegate するよう活性化された。`RouterCallerState` に 4 つの新規 callable フィールド（`list_skills_fn` / `describe_skill_fn` / `list_agents_fn` / `describe_agent_fn`）を追加。`ToolDefinition` に optional `schema_enricher` hook を追加し、`render_for_router(state=...)` が per-session 動的データを inject するために起動する（正準用途: `invoke_skill.name` / `delegate_to_agent.to` enums）。`router_tools.py` 内の残り 2 件のインライン `ToolSpec` リテラル（= `invoke_skill` + `delegate_to_agent`）を新 hook 経由で registry consumption に移行、byte-identity を保持。mis-wiring 契約: dispatcher が必要な callable を populate しない場合、ハンドラは記述的メッセージで `RuntimeError` を raise する。Tier 2 invariant +29。1754 passed / 2 xfailed。

**M4 Phase 3 step 2（着地済み — commit `649a426`）:** `RouterLoop._invoke_router_tool` が活性化済 6 tools (catalog ×4 + `delegate_to_agent` + `plan`) を if/elif tree ではなく `invoke_tool(get_default_registry(), ...)` 経由で dispatch するように切替。`RouterLoop._build_router_caller_state` が bound callbacks つき `RouterCallerState` を構築。catalog list-handler の戻り値 shape を bare list に緩和（= LLMReplay byte-identity 保持）。`_invoke_router_tool` 内の A1–A4 / B2 / G レガシー分岐を削除。

**M4 Phase 4 step 1（着地済み）:** `_DISPATCH_KIND` sidecar dict / `_TOOL_SPECS_STATIC_ASYNC` を `router_tools.py` から削除。`get_dispatch_kind(name)` は registry の `ToolDefinition.dispatch_kind` を直接参照。registry が schema render と dispatch posture 分類の両方の canonical source になった。

**M4 Phase 3.5（着地済み — 5 commits `0093667` / `2b1fe8d` / `3378051` / `a58c685` / `7482b33`）:** router-side cluster activations 完了。 残り 18 tools (file ×4 / mcp ×3 / memory ×5 / web ×2 / reyn_src ×2 / `invoke_skill`) も全て `invoke_tool(get_default_registry(), ...)` 経由 dispatch するようになった。 migration audit で識別した per-tool 設計課題は 3 つの bridge pattern を `RouterCallerState` に追加して解決:

1. **`op_context_factory: Callable | None`** — RouterLoop が `host.make_router_op_context` を bind し、 file / mcp / web handlers が operator-declared PermissionDecl + Workspace を受信。 legacy router branch と同等。
2. **`host: Any`** — MCP handlers が session-level MCPClient cache を保持するための duck-typed RouterHostAdapter 参照。
3. **Per-tool callable bridges** (`run_skill_fn` / `list_memory_fn` / `read_memory_body_fn` / `remember_fn` / `forget_fn`) — RouterLoop の private helper に bind されており、 chain_id propagation (`invoke_skill`) と agent-aware memory paths (memory cluster) を保持。

`RouterLoop._invoke_router_tool` は registry dispatch top-branch + 将来 cluster 用の placeholder コメントだけの薄い実装に。 `_normalise_router_tool_result` が handler 戻り値 shape (= op_runtime synthesis 由来の dict envelope) を legacy router branch が emit していた bare-string / bare-list shape に正規化し、 LLMReplay byte-identity を 5 cluster migration を通じて end-to-end で保持。

**M4 Phase 4（着地済み）:** phase-side migration 完了で architectural goal 達成。

- **Phase 4 step 1 (commit `ebe5786`)** — `_DISPATCH_KIND` sidecar dict 撤去、 `get_dispatch_kind()` が registry の `ToolDefinition.dispatch_kind` を直接参照。
- **Phase 4 step 2** — coarse-name `FILE_OP` / `MCP_OP` / `RUN_SKILL_OP` ToolDefinitions を `gates(phase="allow")` で registry 登録。 phase Control IR `kind` 値は registry entry に 1:1 マッピング。 `ControlIRExecutor.execute()` が `invoke_tool(get_default_registry(), op.kind, ...)` 経由 dispatch、 catalog building (`_build_phase_tool_catalog`) は registry から schema を読む。
- **Phase 4 step 3** — `OP_KIND_MODEL_MAP` は coarse-kind reference (= linter `ALL_OP_KINDS`、 `OP_PURITY` coverage) として残存; dispatch time には参照されない。 `op_runtime/<kind>.py` handlers は registry handlers が委譲する shared implementation として残存。
- **`is_op_allowed` helper** — legacy coarse-name `allowed_ops` declarations が将来 fine-grained `op.kind` にマッチするための prefix-wildcard membership。 forward-looking: phase Control IR は今日も coarse kinds を emit。

**tool 追加コスト** (steady state): `src/reyn/tools/<name>.py` 1 file + `__init__.py` の register 呼出 1 行 = router-or-phase tool で **2 touch points**。 新規 phase-side coarse op kind は加えて `OP_KIND_MODEL_MAP` entry (linter / purity coverage) + `schemas/models.py` の Pydantic `IROp` model = **3 touch points** が phase-eligible 新 kind の予算。 これが今後の tool-scope 拡大が amortise する base line。

ADR-0026 は **Accepted**。

**参照:** [../../deep-dives/decisions/0026-unified-tool-registry.md](../../deep-dives/decisions/0026-unified-tool-registry.md)
