llm-worker-rs/docs/hooks.md

718 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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を実行できます
```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<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のみ
}
```
### 操作メソッド
```rust
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>;
}
```
### ストリーミング用メソッド
```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<Message>),
// ターンを強制完了
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::<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への登録
```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::<bool>()
.unwrap_or(false);
if !enabled {
return HookResult::Skip;
}
// 設定ファイルからオプション読み込み
let config_path = format!("{}/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
```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<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
```rust
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.md) - Worker全体の文書
- [worker-macro.md](worker-macro.md) - マクロシステム
- `worker/src/lib.rs` - Hook実装コード
- `worker-types/src/lib.rs` - Hook型定義