From b538c2f1ea50a1fd7820641c2cff8ff648192f86 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 18 Apr 2026 17:27:22 +0900 Subject: [PATCH] =?UTF-8?q?Interceptor=E3=81=AE=E8=B2=AC=E5=8B=99=E5=88=86?= =?UTF-8?q?=E9=9B=A2=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 - tickets/hook-interceptor-separation.md | 154 ------------------ tickets/hook-interceptor-separation.review.md | 72 -------- 3 files changed, 227 deletions(-) delete mode 100644 tickets/hook-interceptor-separation.md delete mode 100644 tickets/hook-interceptor-separation.review.md diff --git a/TODO.md b/TODO.md index f815e03f..3c68af4b 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,6 @@ - [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) -- [ ] Hook と Interceptor の責務分離 → [tickets/hook-interceptor-separation.md](tickets/hook-interceptor-separation.md) - [ ] Method::Notify: システム起点のコンテキスト注入 → [tickets/method-notify.md](tickets/method-notify.md) - [ ] Pod オーケストレーション: LLM によるマルチエージェント分業 → [tickets/pod-orchestration.md](tickets/pod-orchestration.md) - [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md) diff --git a/tickets/hook-interceptor-separation.md b/tickets/hook-interceptor-separation.md deleted file mode 100644 index 2c0d54c1..00000000 --- a/tickets/hook-interceptor-separation.md +++ /dev/null @@ -1,154 +0,0 @@ -# Hook と Interceptor の責務分離 - -## レビュー状態 - -初回レビュー実施済み。[hook-interceptor-separation.review.md](hook-interceptor-separation.review.md) を参照。 -要件全項目達成。**無条件で受け入れ可**。 - -## 背景 - -Worker の実行ループへの介入手段として `Interceptor` trait (llm-worker crate) と `Hook` trait (pod crate) が存在するが、現状では Hook が Interceptor と同じ権限を持っており、責務の分離が成立していない。 - -### 現状の構造 - -``` -Worker - └── Interceptor trait (llm-worker) - ↑ implements - HookInterceptor (pod) - └── HookRegistry - └── Vec>> -``` - -- `Interceptor` は `&mut Vec` (context)、`&mut ToolResultInfo` (tool result) 等を直接受け取る。context の書き換え、history への干渉、tool result の改変が自由にできる -- `HookInterceptor` は `Interceptor` を実装し、各メソッドで Hook を順に呼ぶブリッジ -- `Hook` の `call(&self, input: &mut E::Input)` が受け取る `E::Input` は `Vec` や `ToolCallInfo` **そのもの** -- **結果として Hook からも Interceptor と同じ全権限で context を操作できてしまっている** - -### 問題 - -- Hook を将来スクリプト言語(Rhai, Wasm 等)に公開したとき、ユーザースクリプトが context を自由に書き換えられてしまう -- 内部機構(compaction トリガー、notification 注入、tool output truncation)と外部公開 hook が同じ権限レベルで動いており、何が安全に公開できるかが型で表現されていない -- Interceptor のドキュメントには「upper layers (e.g. Pod) implement」とあるが、Pod が直接実装するのではなく Hook 経由で間接実装しているため、設計意図と実態がずれている - -## ゴール - -Interceptor と Hook の責務を明確に分離し、型レベルで権限の境界を表現する。 - -## 方針 - -### Interceptor(内部実装用) - -- **llm-worker crate に留まる**。Pod / Worker の内部機構が直接実装する -- context / history の**直接操作が可能**(`&mut Vec` 等を受け取る) -- 外部に公開しない。`pub(crate)` または Pod crate 内でのみ利用 -- 用途: - - compaction トリガー(`PreLlmRequest` で context サイズを見て `Yield` を返す) - - notification 注入(`PreLlmRequest` で pending notifications を context に flush) - - tool output truncation(`PostToolCall` で content を書き換え) - - 将来の内部機構 - -### Hook(公開 API 用) - -- **Pod crate で定義**。将来スクリプト言語やプラグインに公開する前提 -- イベントの**観測**と**制御フロー判断**(continue / skip / abort / pause)のみ -- context の直接操作は**できない**。受け取るのは: - - **読み取り専用の情報**(ツール名、引数の概要、ターン番号、etc.) - - **制御フロー判断の返却値**(continue / skip / abort) -- 安全に sandbox 可能 - -### 実行順序 - -同じ決定点(例: pre_tool_call)で Interceptor と Hook の両方が登録されている場合: - -``` -[Interceptor: 内部機構の処理(context 操作等)] - ↓ -[Hook: 外部の判断(observe + continue/skip/abort)] - ↓ -Worker が次のステップに進む -``` - -Interceptor が先に走って context を整え、Hook はその結果を観測する。Hook の判断が Interceptor の操作を覆すことはない(abort 等で中断は可能)。 - -## 必要な変更 - -### Hook の Input 型を制限する - -現状の `Hook` の Input は `Vec` だが、これを read-only のサマリ型に変更: - -```rust -// Before: Hook が context を直接操作できる -impl HookEventKind for PreLlmRequest { - type Input = Vec; // &mut Vec が渡される - type Output = PreRequestAction; -} - -// After: Hook は read-only の情報だけ受け取る -impl HookEventKind for PreLlmRequest { - type Input = PreRequestInfo; // context のサマリ(item 数、token 推定等) - type Output = PreRequestAction; -} -``` - -同様に各イベントの Input を「観測に必要な最小限の read-only 情報」に絞る。 - -### `ToolCallInfo` / `ToolResultInfo` の分離 - -現状は Interceptor と Hook で同じ `ToolCallInfo` を共有している。分離: - -- **Interceptor 用**: `ToolCallInfo { call: &mut ToolCall, meta: ToolMeta, tool: Arc }` — call の書き換え可能 -- **Hook 用**: `ToolCallSummary { name: String, arguments_preview: String }` — 読み取りのみ、tool instance へのアクセスなし - -### HookInterceptor の解体 - -現在の `HookInterceptor`(Hook を Interceptor にブリッジする層)は不要になる。代わりに: - -- Worker が Interceptor を呼ぶ(内部機構の処理) -- Worker が Hook を呼ぶ(外部判断の問い合わせ) -- これらは Worker の実行ループ内で**別々のステップ**として呼ばれる - -Worker が Hook を直接知るか、Pod 層が Worker に Hook callback を注入するかは設計時に判断。 - -### llm-worker 側の変更 - -- `Interceptor` trait はそのまま(内部実装用として健全) -- Worker の実行ループに「Hook 呼び出しポイント」を追加(Interceptor とは別系統) -- Hook の呼び出しインターフェースは `Fn` ベースのコールバックか、新しい trait か - -### pod 側の変更 - -- `Hook` の `E::Input` を read-only サマリ型に置き換え -- `HookInterceptor` を削除 -- 内部機構(compaction 等)は Interceptor を直接実装する形に移行 -- HookRegistry は外部公開 API として残り、将来のスクリプト公開の基盤になる - -## 設計で決めること - -- **Hook の Input 型の具体設計**: 各イベントで何を read-only として渡すか(ツール名? 引数の先頭 N 文字? token 数?) -- **Worker が Hook をどう呼ぶか**: Interceptor と同じ trait 方式か、コールバック登録方式か、別の trait か -- **Interceptor の複数登録**: 現状 Worker は1つの Interceptor しか持てない。内部機構が複数(compaction + notification + truncation)になると、Chain of Responsibility 的に複数の Interceptor を合成する仕組みが要る -- **Hook の実行順序保証**: 複数の Hook が登録されている場合の評価順序と short-circuit 規則(現行と同じ「登録順 + 最初の non-Continue で打ち切り」を維持するか) -- **既存の Pod 層 hook ユーザーの移行**: compact_interceptor 等が現在 HookInterceptor 経由で動いている場合の移行パス - -## 完了条件 - -- Interceptor は context / history の直接操作が可能な内部 API として存続し、compaction / notification 注入 / tool output truncation 等が Interceptor を直接利用する -- Hook は read-only のサマリ情報のみを受け取り、制御フロー判断(continue / skip / abort / pause)を返す公開 API になる -- Hook から `&mut Vec` や `&mut ToolCallInfo` 等の context 直接操作パスが**型レベルで存在しない** -- HookInterceptor ブリッジは削除される -- Worker の実行ループ内で Interceptor → Hook の順序で呼ばれ、それぞれの責務が分離されている -- 既存の Pod 層 hook(compaction トリガー等)が Interceptor 直接実装に移行し、動作が壊れていない -- 単体テストで Hook が context を操作できないことが検証される - -## 他チケットとの関係 - -- **tickets/method-notify.md**: notification の context 注入は Interceptor の責務。Hook ではない -- **tickets/pod-orchestration.md**: spawned Pod のツール実行制御(permission)は Hook として公開しうる -- **tickets/permission-extension-point.md**: パーミッション制御は Hook の代表的ユースケース。ツール実行の approve/deny を外部から制御する -- **tickets/bash-tool.md**: Bash ツールの Permission 層は Hook (pre_tool_call) で実装される想定 - -## 範囲外 - -- **スクリプト言語バインディングの実装**: Hook を Rhai / Wasm に公開する仕組み自体は別チケット。本チケットは公開可能な型境界を作るところまで -- **Hook の動的ロード / アンロード**: 起動時に登録して freeze する現行モデルを維持。動的な Hook 管理は別チケット diff --git a/tickets/hook-interceptor-separation.review.md b/tickets/hook-interceptor-separation.review.md deleted file mode 100644 index da5b3278..00000000 --- a/tickets/hook-interceptor-separation.review.md +++ /dev/null @@ -1,72 +0,0 @@ -# レビュー: Hook と Interceptor の責務分離 - -対象差分: `crates/pod/src/{hook,pod,lib,pod_interceptor}.rs`、削除: `compact_interceptor.rs` / `hook_interceptor.rs`(staged、未コミット) - -## 要件達成状況 - -| 要件 | 状態 | -|---|---| -| Hook の Input が read-only サマリ型になる | ✅ `PromptSubmitInfo` / `PreRequestInfo` / `ToolCallSummary` / `ToolResultSummary` / `TurnEndInfo` / `AbortInfo` を新設。`Hook::call(&self, input: &E::Input)` で `&mut` が消えた | -| Hook から context / history の直接操作パスが型レベルで存在しない | ✅ `Vec` / `ToolCallInfo` / `ToolResultInfo` が Hook の Input から消え、サマリ型に置換 | -| Interceptor は context / history の直接操作が可能なまま | ✅ `PodInterceptor` が `Interceptor` を実装し `&mut Vec` / `&mut ToolCallInfo` 等を受け取る | -| Interceptor → Hook の実行順序(内部が先、公開が後) | ✅ `PodInterceptor` の各メソッドが: 内部ロジック(compaction check 等)→ サマリ構築 → Hook 呼び出し の順 | -| HookInterceptor ブリッジの削除 | ✅ `hook_interceptor.rs` deleted | -| CompactInterceptor の統合 | ✅ `compact_interceptor.rs` deleted。compaction check は `PodInterceptor::pre_llm_request` に統合 | -| 既存の compaction 動作が壊れない | ✅ `compact_state` を `PodInterceptor` に渡し、`exceeds_turn()` で Yield する同じロジック | -| 単体テストで Hook が context を操作できないことが検証される | ✅ テストの Hook が `&PreRequestInfo`(read-only)を受け取る形で書かれている。`&mut` パスは型レベルで不可能 | - -## アーキテクチャ - -### 良い点 - -**`PodInterceptor` が single composite interceptor として全責務を統合**: -- compaction check (内部) → サマリ構築 → Hook 呼び出しを1つの `pre_llm_request` 内で制御 -- Worker 側は単一の `set_interceptor` で完結(`Vec>` 不要) -- `compact_interceptor.rs` の decorator パターン(inner HookInterceptor をラップ)が消え、フラットな構造に - -**Hook の `call` シグネチャが `&self, input: &E::Input` に**: -- `&mut E::Input` → `&E::Input` への変更で、Hook 実装者が context を書き換える経路が型レベルで消えた -- 将来スクリプトに公開するとき、`&T` だけ渡せば良い(sandbox の粒度が明確) - -**サマリ型の設計**: -- `ToolCallSummary::arguments` が `serde_json::Value` clone — 構造的アクセスが可能で permission 判断に使える -- `ToolResultSummary::output` が `ToolOutput` clone — summary + content 両方にアクセス可能 -- `PreRequestInfo` が item_count / estimated_tokens / turn_index / tool_calls_this_turn を集約 — compaction 判断に必要十分 -- `TurnEndInfo::final_text_preview` が 512 byte 制限 + UTF-8 boundary — Pod orchestration で spawned Pod の結果要約に使える - -**ターン追跡の統合**: -- `next_turn_index` / `tool_calls_this_turn` が `PodInterceptor` 内の `AtomicUsize` で管理 -- `on_prompt_submit` でリセット、`pre_tool_call` でインクリメント -- Hook が受け取るサマリにこの情報が反映される - -### テスト - -4 ケース: -- `pre_llm_request_yields_and_skips_hooks_when_compact_threshold_exceeded` — 内部機構(compaction)が Hook より先に short-circuit することを検証 -- `pre_llm_request_runs_hooks_when_under_threshold` — 通常時は Hook が走ることを検証 -- `pre_llm_request_runs_hooks_when_no_compact_state` — compaction 無効時も Hook が走ることを検証 -- `pre_llm_request_short_circuits_on_first_non_continue` — 複数 Hook の短絡評価を検証 - -特に 1 つ目が**設計の核心(Interceptor が先、Hook が後。内部が short-circuit したら Hook は走らない)**を lock-in している。 - -## 指摘事項 - -### 1. 🟢 `pod.rs` の `ensure_interceptor_installed` が少しすっきりした - -旧: `CompactInterceptor::new(hook_interceptor, state)` で decorator をネスト → 新: `PodInterceptor::new(registry, compact_state)` でフラットに構築。`compact_state` の有無で分岐していた `set_interceptor` 呼び出しが1箇所に統合。 - -### 2. 🟢 `HookEventKind::Input` に `Send + Sync` バウンドが追加 - -```rust -type Input: Send + Sync; -``` - -`&E::Input` を async メソッドで受け取るために必要。正しい追加。 - -### 3. 🟢 `extract_message_text` / `preview` がユーティリティとして分離 - -`PodInterceptor` 内の private function として切り出し。`preview` は UTF-8 boundary を考慮した切断で、`truncate_content`(tool output truncation)と同じパターン。 - -## 結論 - -**無条件で受け入れ可**。チケットの要件を完全に達成。特に「型レベルで Hook から context 操作が不可能」という核心的な保証が `&E::Input` (not `&mut`) + サマリ型で実現されている。テストも設計意図を正確にカバー。