# Worker Hook システム Workerのフックシステムは、LLMとの対話プロセスの各段階でカスタムロジックを実行し、システムの動作を柔軟に制御できる強力な機能です。 ## 概要 Hooksシステムは、WorkerがLLMとの対話で実行する各フェーズ(メッセージ処理、ツール実行、ターン完了)でユーザー定義の処理を挿入できる機能です。これにより、以下のような高度なカスタマイズが可能になります: - **メッセージの前処理**: ユーザーメッセージにコンテキスト情報を追加 - **ツール実行後の処理**: ファイル操作後の自動コミットやフォーマット - **ターン完了時の処理**: 統計記録や追加情報の提供 - **ストリーミング制御**: リアルタイム応答の変更・拡張 ## アーキテクチャ ### コア型 ```rust // worker-types/src/lib.rs pub trait WorkerHook: Send + Sync { fn name(&self) -> &str; fn hook_type(&self) -> &str; fn matcher(&self) -> &str; async fn execute(&self, context: HookContext) -> (HookContext, HookResult); } pub struct HookManager { hooks: HashMap>>, } pub enum HookEvent { OnMessageSend, PreToolUse, PostToolUse, OnTurnCompleted, } ``` ### Hook実行フロー ``` 1. ユーザーメッセージ受信 2. OnMessageSend hooks 実行 3. プロンプト構築・LLMへ送信 4. ストリーミング応答開始 5. ツール呼び出し検出 ├── PreToolUse hooks 実行 ├── ツール実行 └── PostToolUse hooks 実行 6. 応答完了 7. OnTurnCompleted hooks 実行 8. 結果返却 ``` ## Hook の種類 ### フェーズ別Hook | フェーズ | タイプ名 | 実行タイミング | ストリーミング | |---------|----------|---------------|---------------| | メッセージ送信時 | `OnMessageSend` | ユーザーメッセージをLLMに送信する直前 | × | | ツール実行前 | `PreToolUse` | 特定のツール実行直前 | ○ | | ツール実行後 | `PostToolUse` | 特定のツール実行完了後 | ○ | | ターン完了時 | `OnTurnCompleted` | AIの応答が完了した時点 | ○ | ### マッチャーパターン `PreToolUse`、`PostToolUse`および`OnTurnCompleted`では、`matcher`パラメータで特定のツールに対してのみHookを実行できます: ```rust // Edit または Create ツールの実行後のみ実行 #[hook(hook_type = "PostToolUse", matcher = "Edit|Create")] // すべてのツールで実行(matcher省略時) #[hook(hook_type = "OnTurnCompleted")] // 正規表現マッチング #[hook(hook_type = "PostToolUse", matcher = r"^(Read|Write)File.*")] ``` ## Hook の作成方法 ### 基本構文 ```rust use worker::types::{HookContext, HookResult, Role}; use worker_macros::hook; #[hook(hook_type = "フェーズ名", matcher = "パターン")] pub async fn your_hook_name(mut context: HookContext) -> HookResult { // カスタムロジックをここに実装 HookResult::Continue } ``` ### マクロを使わない実装 ```rust use worker::types::{WorkerHook, HookContext, HookResult}; use async_trait::async_trait; pub struct CustomHook { name: String, } impl CustomHook { pub fn new(name: String) -> Self { Self { name } } } #[async_trait] impl WorkerHook for CustomHook { fn name(&self) -> &str { &self.name } fn hook_type(&self) -> &str { "OnMessageSend" } fn matcher(&self) -> &str { "" } async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { // Hook処理をここに実装 (context, HookResult::Continue) } } ``` ## HookContext API `HookContext`は、Hook内で使用できる情報とメソッドを提供します: ### 利用可能なデータ ```rust pub struct HookContext { pub content: String, // 処理対象のコンテンツ pub workspace_path: String, // 現在のワークスペースパス pub message_history: Vec, // これまでの会話履歴 pub tools: Vec, // 利用可能なツール一覧 pub variables: HashMap, // Hook間で共有可能な変数 pub tool_name: Option, // 実行中のツール名(ツール関連フックのみ) pub tool_args: Option, // ツール実行引数(ツール関連フックのみ) pub tool_result: Option, // ツール実行結果(PostToolUseのみ) } ``` ### 操作メソッド ```rust impl HookContext { // ワークスペースでコマンドを実行 pub async fn run_command(&self, command: &str) -> Result>; // ツールを実行(将来実装予定) pub async fn run_tool(&self, tool_name: &str, args: serde_json::Value) -> Result>; // メッセージを履歴に追加 pub fn add_message(&mut self, content: String, role: Role); // コンテンツを書き換え pub fn set_content(&mut self, content: String); // 変数の設定・取得 pub fn set_variable(&mut self, key: String, value: String); pub fn get_variable(&self, key: &str) -> Option<&String>; // ツール情報の取得 pub fn get_tool(&self, tool_name: &str) -> Option<&DynamicToolDefinition>; pub fn list_tool_names(&self) -> Vec<&String>; } ``` ### ストリーミング用メソッド ```rust impl HookContext { // ストリーミング中にメッセージを送信 pub fn stream_message(&self, content: String, role: Role); // ストリーミング中にシステム通知を送信 pub fn stream_system_message(&self, content: String); // ストリーミング中にデバッグ情報を送信 pub fn stream_debug(&self, title: String, data: serde_json::Value); } ``` ## HookResult の種類 Hook関数は以下のいずれかの結果を返す必要があります: ```rust pub enum HookResult { // 処理を続行 Continue, // コンテンツを変更して続行 ModifyContent(String), // システムメッセージを追加して続行 AddMessage(String, Role), // 複数のメッセージを追加して続行 AddMessages(Vec), // ターンを強制完了 Complete, // エラーでターンを終了 Error(String), // Hook処理をスキップ(デバッグ用) Skip, } ``` ## 実用例 ### 1. タイムスタンプ付きメッセージ ```rust /// メッセージ送信時に現在時刻を追加するHook #[hook(hook_type = "OnMessageSend")] pub async fn add_timestamp_hook(mut context: HookContext) -> HookResult { let timestamp = chrono::Local::now().format("%H:%M:%S").to_string(); let enhanced_content = format!("[{}] {}", timestamp, context.content); HookResult::ModifyContent(enhanced_content) } ``` ### 2. ファイル操作後の自動Git追跡 ```rust /// ファイル編集後にGitステータスを確認するHook #[hook(hook_type = "PostToolUse", matcher = "Edit|Create|Write")] pub async fn file_change_notification_hook(mut context: HookContext) -> HookResult { match context.run_command("git status --porcelain").await { Ok(output) => { if !output.trim().is_empty() { context.add_message( format!("📝 ファイル変更を検出:\n{}", output), Role::System, ); HookResult::Continue } else { HookResult::Continue } } Err(_) => HookResult::Continue, } } ``` ### 3. 長い応答への注意喚起 ```rust /// 長い応答が生成された際に注意を促すHook #[hook(hook_type = "OnTurnCompleted")] pub async fn long_response_warning_hook(context: HookContext) -> HookResult { if context.content.len() > 2000 { HookResult::AddMessage( "⚠️ 長い応答が生成されました。スクロールして全体を確認してください。".to_string(), Role::System, ) } else { HookResult::Continue } } ``` ### 4. ツール実行前のバリデーション ```rust /// 危険なコマンドの実行前に確認を行うHook #[hook(hook_type = "PreToolUse", matcher = "Execute|Shell")] pub async fn dangerous_command_hook(context: HookContext) -> HookResult { if let Some(args) = &context.tool_args { if let Some(command) = args.get("command").and_then(|v| v.as_str()) { let dangerous_commands = ["rm -rf", "format", "dd if="]; for dangerous in &dangerous_commands { if command.contains(dangerous) { return HookResult::Error(format!( "危険なコマンド「{}」の実行が阻止されました: {}", dangerous, command )); } } } } HookResult::Continue } ``` ### 5. 自動読み取りHook (TUI用) ```rust /// ファイル作成・編集後に自動的にファイル内容を読み取るHook #[hook(hook_type = "PostToolUse", matcher = "Edit|Create|Write")] pub async fn auto_read_hook(mut context: HookContext) -> HookResult { // ツール実行結果からファイルパスを抽出 if let Some(tool_result) = &context.tool_result { if let Ok(result_data) = serde_json::from_str::(tool_result) { if let Some(file_path) = result_data.get("file_path").and_then(|v| v.as_str()) { // ファイル内容を読み取って表示 match tokio::fs::read_to_string(file_path).await { Ok(content) => { context.stream_system_message(format!( "📄 **{}** の内容:\n```\n{}\n```", file_path, content )); } Err(e) => { context.stream_system_message(format!( "❌ ファイル読み取りエラー ({}): {}", file_path, e )); } } } } } HookResult::Continue } ``` ## Hook の登録と管理 ### Workerへの登録 ```rust use worker::Worker; // 単一のHookを登録 worker.register_hook(Box::new(YourHook::new())); // 複数のHookを一括登録 let hooks = vec![ Box::new(TimestampHook::new()), Box::new(FileNotificationHook::new()), Box::new(DebugHook::new()), ]; worker.register_hooks(hooks); // TUI用デフォルトHookの登録 let tui_hooks = crate::tui::hooks::get_default_hooks(); worker.register_hooks(tui_hooks); ``` ### Hook の実行順序 同じフェーズで複数のHookが登録されている場合、登録順に実行されます。先に実行されたHookの結果(コンテキストの変更など)は、後続のHookに引き継がれます。 ```rust // 実行順序の例 worker.register_hook(Box::new(TimestampHook)); // 1番目 worker.register_hook(Box::new(ValidationHook)); // 2番目 worker.register_hook(Box::new(LoggingHook)); // 3番目 ``` ### Hook の中断 `HookResult::Complete`または`HookResult::Error`を返すHookがあると、それ以降のHookは実行されず、処理が中断されます。 ### Hook の動的管理 ```rust impl Worker { // Hook一覧を取得 pub fn list_hooks(&self) -> Vec<(&str, &str)>; // (name, hook_type) // 特定のHookを削除 pub fn remove_hook(&mut self, hook_name: &str) -> bool; // フェーズ別Hookを削除 pub fn remove_hooks_by_phase(&mut self, hook_type: &str); // すべてのHookをクリア pub fn clear_hooks(&mut self); } ``` ## ストリーミング処理 ### ストリーミング中のHook実行 ```rust // worker/src/lib.rs の process_with_shared_state より stream! { // ... LLM応答処理中 ... // ツール呼び出し検出時 if let Some(tool_calls) = &response.tool_calls { for tool_call in tool_calls { // PreToolUse hooks 実行 let (context, hook_result) = execute_hooks( HookEvent::PreToolUse, tool_call.name.clone() ).await; match hook_result { HookResult::Error(msg) => { yield Ok(StreamEvent::Error(msg)); continue; } HookResult::Complete => break, _ => {} } // ツール実行 let result = execute_tool(tool_call).await; // PostToolUse hooks 実行(ストリーミング中) let (context, hook_result) = execute_hooks( HookEvent::PostToolUse, tool_call.name.clone() ).await; // Hook結果を即座にストリーミング if let HookResult::AddMessage(msg, role) = hook_result { yield Ok(StreamEvent::HookMessage { hook_name: "PostToolUse".to_string(), content: msg, role, }); } } } } ``` ## ベストプラクティス ### 1. エラーハンドリング ```rust #[hook(hook_type = "OnTurnCompleted")] pub async fn robust_hook(mut context: HookContext) -> HookResult { match context.run_command("potentially_failing_command").await { Ok(output) => { // 成功時の処理 context.add_message(format!("実行完了: {}", output), Role::System); HookResult::Continue } Err(error) => { // エラーログを記録するが、処理は継続 tracing::warn!("Hook実行時にエラー: {}", error); HookResult::Continue } } } ``` ### 2. パフォーマンス配慮 ```rust #[hook(hook_type = "OnTurnCompleted")] pub async fn performance_aware_hook(context: HookContext) -> HookResult { // 重い処理は条件分岐で制限 if context.content.len() > 10000 { // 大きなコンテンツの場合はスキップ return HookResult::Skip; } // 非同期処理は適切にawaitする let result = tokio::time::timeout( Duration::from_secs(5), expensive_operation(&context) ).await; match result { Ok(output) => HookResult::AddMessage(output, Role::System), Err(_) => { tracing::warn!("Hook処理がタイムアウトしました"); HookResult::Continue } } } ``` ### 3. 設定可能なHook ```rust #[hook(hook_type = "OnMessageSend")] pub async fn configurable_hook(mut context: HookContext) -> HookResult { // 環境変数で動作を制御 let enabled = std::env::var("HOOK_ENABLED") .unwrap_or_default() .parse::() .unwrap_or(false); if !enabled { return HookResult::Skip; } // 設定ファイルからオプション読み込み let config_path = format!("{}/.nia/hook_config.json", context.workspace_path); if let Ok(config_content) = tokio::fs::read_to_string(&config_path).await { if let Ok(config) = serde_json::from_str::(&config_content) { // 設定に基づく処理 return process_with_config(&mut context, &config).await; } } HookResult::Continue } ``` ### 4. 条件付きHook ```rust #[hook(hook_type = "PostToolUse", matcher = ".*")] pub async fn conditional_hook(context: HookContext) -> HookResult { // ワークスペースの種類に応じて処理を変更 let is_git_repo = context.run_command("git status").await.is_ok(); let is_rust_project = tokio::fs::metadata( format!("{}/Cargo.toml", context.workspace_path) ).await.is_ok(); match (is_git_repo, is_rust_project) { (true, true) => { // Rustプロジェクト + Git context.run_command("cargo fmt").await.ok(); HookResult::AddMessage("Rustコードをフォーマットしました".to_string(), Role::System) } (true, false) => { // その他のGitプロジェクト HookResult::AddMessage("Gitプロジェクトで作業中です".to_string(), Role::System) } _ => HookResult::Continue } } ``` ## デバッグとテスト ### Hook のテスト ```rust #[cfg(test)] mod tests { use super::*; use worker::types::*; #[tokio::test] async fn test_timestamp_hook() { let mut context = HookContext { content: "Hello, world!".to_string(), workspace_path: "/tmp".to_string(), message_history: vec![], tools: vec![], variables: HashMap::new(), tool_name: None, tool_args: None, tool_result: None, }; let result = add_timestamp_hook(context).await; match result { HookResult::ModifyContent(content) => { assert!(content.contains("Hello, world!")); assert!(content.contains("[")); assert!(content.contains("]")); } _ => panic!("Expected ModifyContent"), } } } ``` ### Hook のデバッグ ```rust #[hook(hook_type = "OnMessageSend")] pub async fn debug_hook(context: HookContext) -> HookResult { tracing::debug!( "Hook実行 - コンテンツ長: {}, ツール数: {}, 履歴数: {}", context.content.len(), context.tools.len(), context.message_history.len() ); // デバッグ情報をストリーミング context.stream_debug( "Hook Debug Info".to_string(), serde_json::json!({ "content_length": context.content.len(), "tool_count": context.tools.len(), "history_count": context.message_history.len(), "workspace": context.workspace_path }) ); HookResult::Continue } ``` ## 高度なHookパターン ### 状態を持つHook ```rust pub struct StatefulHook { counter: Arc>, } impl StatefulHook { pub fn new() -> Self { Self { counter: Arc::new(Mutex::new(0)), } } } #[async_trait] impl WorkerHook for StatefulHook { fn name(&self) -> &str { "stateful_hook" } fn hook_type(&self) -> &str { "OnTurnCompleted" } fn matcher(&self) -> &str { "" } async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { let mut count = self.counter.lock().unwrap(); *count += 1; context.set_variable("turn_count".to_string(), count.to_string()); if *count % 10 == 0 { ( context, HookResult::AddMessage( format!("🎉 {}回目のターンです!", count), Role::System ) ) } else { (context, HookResult::Continue) } } } ``` ### チェーン可能なHook ```rust pub struct HookChain { hooks: Vec>, } impl HookChain { pub fn new() -> Self { Self { hooks: Vec::new() } } pub fn add_hook(mut self, hook: Box) -> Self { self.hooks.push(hook); self } } #[async_trait] impl WorkerHook for HookChain { fn name(&self) -> &str { "hook_chain" } fn hook_type(&self) -> &str { "OnMessageSend" } fn matcher(&self) -> &str { "" } async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { for hook in &self.hooks { let (new_context, result) = hook.execute(context).await; context = new_context; match result { HookResult::Continue | HookResult::Skip => continue, other => return (context, other), } } (context, HookResult::Continue) } } ``` ## よくある質問 ### Q: Hookはどのように読み込まれますか? A: 現在、HookはRustコードとしてコンパイル時に組み込まれます。`#[hook]`マクロを使用するか、`WorkerHook`トレイトを実装して`worker.register_hook()`で登録します。 ### Q: Hook内でファイルシステムにアクセスできますか? A: はい。`run_command`メソッドを使用してシェルコマンドを実行できるほか、Rustの標準ライブラリやtokioを使用した直接的なファイル操作も可能です。 ### Q: Hook間でデータを共有できますか? A: `HookContext`の`variables`を使用して、同一ターン内のHook間でデータを共有できます。永続的なデータ共有には、ファイルやデータベースを使用してください。 ### Q: ストリーミング中にHookの結果を表示できますか? A: はい。`context.stream_message()`や`context.stream_system_message()`を使用することで、ストリーミング中にリアルタイムでメッセージを送信できます。 ### Q: Hookでエラーが発生した場合はどうなりますか? A: `HookResult::Error`を返すと、そのターンは中断されます。継続したい場合は、エラーをログに記録して`HookResult::Continue`を返してください。 ## 関連ドキュメント - [worker.md](worker.md) - Worker全体の文書 - [worker-macro.md](worker-macro.md) - マクロシステム - `worker/src/lib.rs` - Hook実装コード - `worker-types/src/lib.rs` - Hook型定義 - `nia-cli/src/tui/hooks/` - TUI用Hook実装例