22 KiB
Worker Hook システム
Workerのフックシステムは、LLMとの対話プロセスの各段階でカスタムロジックを実行し、システムの動作を柔軟に制御できる強力な機能です。
概要
Hooksシステムは、WorkerがLLMとの対話で実行する各フェーズ(メッセージ処理、ツール実行、ターン完了)でユーザー定義の処理を挿入できる機能です。これにより、以下のような高度なカスタマイズが可能になります:
- メッセージの前処理: ユーザーメッセージにコンテキスト情報を追加
- ツール実行後の処理: ファイル操作後の自動コミットやフォーマット
- ターン完了時の処理: 統計記録や追加情報の提供
- ストリーミング制御: リアルタイム応答の変更・拡張
アーキテクチャ
コア型
// 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<HookEvent, Vec<Box<dyn WorkerHook>>>,
}
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を実行できます:
// Edit または Create ツールの実行後のみ実行
#[hook(hook_type = "PostToolUse", matcher = "Edit|Create")]
// すべてのツールで実行(matcher省略時)
#[hook(hook_type = "OnTurnCompleted")]
// 正規表現マッチング
#[hook(hook_type = "PostToolUse", matcher = r"^(Read|Write)File.*")]
Hook の作成方法
基本構文
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
}
マクロを使わない実装
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内で使用できる情報とメソッドを提供します:
利用可能なデータ
pub struct HookContext {
pub content: String, // 処理対象のコンテンツ
pub workspace_path: String, // 現在のワークスペースパス
pub message_history: Vec<Message>, // これまでの会話履歴
pub tools: Vec<DynamicToolDefinition>, // 利用可能なツール一覧
pub variables: HashMap<String, String>, // Hook間で共有可能な変数
pub tool_name: Option<String>, // 実行中のツール名(ツール関連フックのみ)
pub tool_args: Option<serde_json::Value>, // ツール実行引数(ツール関連フックのみ)
pub tool_result: Option<String>, // ツール実行結果(PostToolUseのみ)
}
操作メソッド
impl HookContext {
// ワークスペースでコマンドを実行
pub async fn run_command(&self, command: &str) -> Result<String, Box<dyn std::error::Error>>;
// ツールを実行(将来実装予定)
pub async fn run_tool(&self, tool_name: &str, args: serde_json::Value) -> Result<String, Box<dyn std::error::Error>>;
// メッセージを履歴に追加
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>;
}
ストリーミング用メソッド
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関数は以下のいずれかの結果を返す必要があります:
pub enum HookResult {
// 処理を続行
Continue,
// コンテンツを変更して続行
ModifyContent(String),
// システムメッセージを追加して続行
AddMessage(String, Role),
// 複数のメッセージを追加して続行
AddMessages(Vec<Message>),
// ターンを強制完了
Complete,
// エラーでターンを終了
Error(String),
// Hook処理をスキップ(デバッグ用)
Skip,
}
実用例
1. タイムスタンプ付きメッセージ
/// メッセージ送信時に現在時刻を追加する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追跡
/// ファイル編集後に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. 長い応答への注意喚起
/// 長い応答が生成された際に注意を促す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. ツール実行前のバリデーション
/// 危険なコマンドの実行前に確認を行う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用)
/// ファイル作成・編集後に自動的にファイル内容を読み取る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::<serde_json::Value>(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への登録
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に引き継がれます。
// 実行順序の例
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 の動的管理
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実行
// 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. エラーハンドリング
#[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. パフォーマンス配慮
#[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
#[hook(hook_type = "OnMessageSend")]
pub async fn configurable_hook(mut context: HookContext) -> HookResult {
// 環境変数で動作を制御
let enabled = std::env::var("HOOK_ENABLED")
.unwrap_or_default()
.parse::<bool>()
.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::<HookConfig>(&config_content) {
// 設定に基づく処理
return process_with_config(&mut context, &config).await;
}
}
HookResult::Continue
}
4. 条件付きHook
#[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 のテスト
#[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 のデバッグ
#[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
pub struct StatefulHook {
counter: Arc<Mutex<u32>>,
}
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
pub struct HookChain {
hooks: Vec<Box<dyn WorkerHook>>,
}
impl HookChain {
pub fn new() -> Self {
Self { hooks: Vec::new() }
}
pub fn add_hook(mut self, hook: Box<dyn WorkerHook>) -> 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全体の文書
- worker-macro.md - マクロシステム
worker/src/lib.rs- Hook実装コードworker-types/src/lib.rs- Hook型定義nia-cli/src/tui/hooks/- TUI用Hook実装例