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 は単一の構造化応答を返す。
{
"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 呼び出しは行わない。preprocessor.md 参照。
Postprocessor steps(同一 step types)は LLM の finish 出力の後、artifact が呼び出し元に返される前に決定論的に実行される。LLM 呼び出しではない。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がチャットルーターで利用可能。mcpop を使用する Phase はcontrol_irにサーバー名とツール名を静的に宣言しなければならない。このギャップは MCP 閲覧がルーターのインタラクティブな「MCP で何ができるか」ユースケースのために追加され、Phase 側mcpop への対応する検索機構なしに実装されたことで生じた。
これらはギャップであり、失敗ではない。閉じるかどうかはセクション 6 の doctrine 問題が決定する。
Type D — LLM 呼び出し前の決定論的ステップ¶
Preprocessor および postprocessor ステップは LLM invocation ではないが、「Skill オーサーが使えるもの」として機能比較の議論に登場する。区別が重要:
pythonpreprocessor ステップはサンドボックス化された Python コードを実行する — LLM 呼び出しなしrun_skillpreprocessor ステップは 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 からのメモリ書き込み: 新しい
memoryop kind(またはupdate_memoryのような stdlib skill)により、Phase がチャット層を経由せずに永続的な事実を書き込める - Phase からのカタログ閲覧: stdlib skill(例:
recall_skill_catalog)により Phase がrun_skill経由でライブカタログを mid-phase クエリできる(OS にカタログ知識を埋め込まずに) -
Phase からの MCP 検索:
mcpop を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. 関連ドキュメント¶
- principles.md — P3、P4、P7
- architecture.md — コンポーネント全体の階層化とランタイムループ
- phase-vs-skill-vs-os.md — Phase・Skill・OS 間の責任境界
- care-boundary.md — Reyn が担うこと・担わないこと; downstream tooling セクションは上記マトリックスを補完する
- preprocessor.md — LLM 呼び出し前の決定論的ステップ(= 第3の invocation kind ではない理由)
- postprocessor.md — LLM 呼び出し後の決定論的ステップ(同理由)
- ../reference/runtime/control-ir.md — Phase 側 op の語彙とセマンティクス
- ../reference/cli/chat.md — チャットで使用可能なスラッシュコマンド(router tools と混同されることがあるが別物)
- ../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.pyToolRegistry—src/reyn/tools/registry.pyinvoke_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 に追加して解決:
op_context_factory: Callable | None— RouterLoop がhost.make_router_op_contextを bind し、 file / mcp / web handlers が operator-declared PermissionDecl + Workspace を受信。 legacy router branch と同等。host: Any— MCP handlers が session-level MCPClient cache を保持するための duck-typed RouterHostAdapter 参照。- 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_KINDsidecar dict 撤去、get_dispatch_kind()が registry のToolDefinition.dispatch_kindを直接参照。 - Phase 4 step 2 — coarse-name
FILE_OP/MCP_OP/RUN_SKILL_OPToolDefinitions をgates(phase="allow")で registry 登録。 phase Control IRkind値は 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 (= linterALL_OP_KINDS、OP_PURITYcoverage) として残存; dispatch time には参照されない。op_runtime/<kind>.pyhandlers は registry handlers が委譲する shared implementation として残存。 is_op_allowedhelper — legacy coarse-nameallowed_opsdeclarations が将来 fine-grainedop.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。