441 lines
14 KiB
Markdown
441 lines
14 KiB
Markdown
# Hooks 設計
|
||
|
||
## 概要
|
||
|
||
HookはWorker層でのターン制御に介入するためのメカニズムです。
|
||
|
||
メッセージ送信・ツール実行・ターン終了等の各ポイントで処理を差し込むことができます。
|
||
|
||
## コンセプト
|
||
|
||
- **制御の介入**: ターンの進行、メッセージの内容、ツールの実行に対して介入
|
||
- **Contextへのアクセス**: メッセージ履歴を読み書き可能
|
||
- **非破壊的チェーン**: 複数のHookを登録順に実行、後続Hookへの影響を制御
|
||
|
||
## Hook一覧
|
||
|
||
| Hook | タイミング | 主な用途 | 戻り値 |
|
||
| ------------------ | -------------------------- | -------------------------- | ---------------------- |
|
||
| `on_prompt_submit` | `run()` 呼び出し時 | ユーザーメッセージの前処理 | `OnPromptSubmitResult` |
|
||
| `pre_llm_request` | 各ターンのLLM送信前 | コンテキスト改変/検証 | `PreLlmRequestResult` |
|
||
| `pre_tool_call` | ツール実行前 | 実行許可/引数改変 | `PreToolCallResult` |
|
||
| `post_tool_call` | ツール実行後 | 結果加工/マスキング | `PostToolCallResult` |
|
||
| `on_turn_end` | ツールなしでターン終了直前 | 検証/リトライ指示 | `OnTurnEndResult` |
|
||
| `on_abort` | 中断時 | クリーンアップ/通知 | `()` |
|
||
|
||
## Hook Trait
|
||
|
||
```rust
|
||
#[async_trait]
|
||
pub trait Hook<E: HookEventKind>: Send + Sync {
|
||
async fn call(&self, input: &mut E::Input) -> Result<E::Output, HookError>;
|
||
}
|
||
```
|
||
|
||
## 制御フロー型
|
||
|
||
### HookEventKind / Result
|
||
|
||
Hookイベントごとに入力/出力型を分離し、意味のない制御フローを排除する。
|
||
|
||
```rust
|
||
pub trait HookEventKind {
|
||
type Input;
|
||
type Output;
|
||
}
|
||
|
||
pub struct OnPromptSubmit;
|
||
pub struct PreLlmRequest;
|
||
pub struct PreToolCall;
|
||
pub struct PostToolCall;
|
||
pub struct OnTurnEnd;
|
||
pub struct OnAbort;
|
||
|
||
pub enum OnPromptSubmitResult {
|
||
Continue,
|
||
Cancel(String),
|
||
}
|
||
|
||
pub enum PreLlmRequestResult {
|
||
Continue,
|
||
Cancel(String),
|
||
}
|
||
|
||
pub enum PreToolCallResult {
|
||
Continue,
|
||
Skip,
|
||
Abort(String),
|
||
Pause,
|
||
}
|
||
|
||
pub enum PostToolCallResult {
|
||
Continue,
|
||
Abort(String),
|
||
}
|
||
|
||
pub enum OnTurnEndResult {
|
||
Finish,
|
||
ContinueWithMessages(Vec<Message>),
|
||
Paused,
|
||
}
|
||
```
|
||
|
||
### Tool Call Context
|
||
|
||
`pre_tool_call` / `post_tool_call` は、ツール実行の文脈を含む入力を受け取る。
|
||
|
||
```rust
|
||
pub struct ToolCallContext {
|
||
pub call: ToolCall,
|
||
pub meta: ToolMeta, // 不変メタデータ
|
||
pub tool: Arc<dyn Tool>, // 状態アクセス用
|
||
}
|
||
|
||
pub struct PostToolCallContext {
|
||
pub call: ToolCall,
|
||
pub result: ToolResult,
|
||
pub meta: ToolMeta,
|
||
pub tool: Arc<dyn Tool>,
|
||
}
|
||
```
|
||
|
||
## 呼び出しタイミング
|
||
|
||
```
|
||
Worker::run(user_input)
|
||
│
|
||
├─▶ on_prompt_submit ───────────────────────────┐
|
||
│ ユーザーメッセージの前処理・検証 │
|
||
│ (最初の1回のみ) │
|
||
│ │
|
||
└─▶ loop {
|
||
│
|
||
├─▶ pre_llm_request ──────────────────────│
|
||
│ コンテキストの改変、バリデーション、 │
|
||
│ システムプロンプト注入などが可能 │
|
||
│ (毎ターン実行) │
|
||
│ │
|
||
├─▶ LLMリクエスト送信 & ストリーム処理 │
|
||
│ │
|
||
├─▶ ツール呼び出しがある場合: │
|
||
│ │ │
|
||
│ ├─▶ pre_tool_call (各ツールごと・逐次) │
|
||
│ │ 実行可否の判定、引数の改変 │
|
||
│ │ │
|
||
│ ├─▶ ツール並列実行 (join_all) │
|
||
│ │ │
|
||
│ └─▶ post_tool_call (各結果ごと・逐次) │
|
||
│ 結果の確認、加工、ログ出力 │
|
||
│ │
|
||
├─▶ ツール結果をコンテキストに追加 │
|
||
│ → ループ先頭へ │
|
||
│ │
|
||
└─▶ ツールなしの場合: │
|
||
│ │
|
||
└─▶ on_turn_end ───────────────────┘
|
||
最終応答のチェック(Lint/Fmt等)
|
||
エラーがあればContinueWithMessagesでリトライ
|
||
}
|
||
|
||
※ 中断時は on_abort が呼ばれる
|
||
```
|
||
|
||
## 各Hookの詳細
|
||
|
||
### on_prompt_submit
|
||
|
||
**呼び出しタイミング**: `run()`
|
||
でユーザーメッセージを受け取った直後(最初の1回のみ)
|
||
|
||
**用途**:
|
||
|
||
- ユーザー入力のバリデーション
|
||
- 入力のサニタイズ・フィルタリング
|
||
- ログ出力
|
||
- `OnPromptSubmitResult::Cancel` による実行キャンセル
|
||
|
||
**入力**: `&mut Message` - ユーザーメッセージ(改変可能)
|
||
|
||
**例**: 入力のバリデーション
|
||
|
||
```rust
|
||
struct InputValidator;
|
||
|
||
#[async_trait]
|
||
impl Hook<OnPromptSubmit> for InputValidator {
|
||
async fn call(
|
||
&self,
|
||
message: &mut Message,
|
||
) -> Result<OnPromptSubmitResult, HookError> {
|
||
if let MessageContent::Text(text) = &message.content {
|
||
if text.trim().is_empty() {
|
||
return Ok(OnPromptSubmitResult::Cancel("Empty input".to_string()));
|
||
}
|
||
}
|
||
Ok(OnPromptSubmitResult::Continue)
|
||
}
|
||
}
|
||
```
|
||
|
||
### pre_llm_request
|
||
|
||
**呼び出しタイミング**: 各ターンのLLMリクエスト送信前(ループの毎回)
|
||
|
||
**用途**:
|
||
|
||
- コンテキストへのシステムメッセージ注入
|
||
- メッセージのバリデーション
|
||
- 機密情報のフィルタリング
|
||
- リクエスト内容のログ出力
|
||
- `PreLlmRequestResult::Cancel` による送信キャンセル
|
||
|
||
**入力**: `&mut Vec<Message>` - コンテキスト全体(改変可能)
|
||
|
||
**例**: メッセージにタイムスタンプを追加
|
||
|
||
```rust
|
||
struct TimestampHook;
|
||
|
||
#[async_trait]
|
||
impl Hook<PreLlmRequest> for TimestampHook {
|
||
async fn call(
|
||
&self,
|
||
context: &mut Vec<Message>,
|
||
) -> Result<PreLlmRequestResult, HookError> {
|
||
let timestamp = chrono::Local::now().to_rfc3339();
|
||
context.insert(0, Message::user(format!("[{}]", timestamp)));
|
||
Ok(PreLlmRequestResult::Continue)
|
||
}
|
||
}
|
||
```
|
||
|
||
### pre_tool_call
|
||
|
||
**呼び出しタイミング**: 各ツール実行前(並列実行フェーズの前)
|
||
|
||
**用途**:
|
||
|
||
- 危険なツールのブロック
|
||
- 引数のサニタイズ
|
||
- 確認プロンプトの表示(UIとの連携)
|
||
- 実行ログの記録
|
||
- `PreToolCallResult::Pause` による一時停止
|
||
|
||
**入力**:
|
||
|
||
- `ToolCallContext`(`ToolCall` + `ToolMeta` + `Arc<dyn Tool>`)
|
||
|
||
**例**: 特定ツールをブロック
|
||
|
||
```rust
|
||
struct ToolBlocker {
|
||
blocked_tools: HashSet<String>,
|
||
}
|
||
|
||
#[async_trait]
|
||
impl Hook<PreToolCall> for ToolBlocker {
|
||
async fn call(
|
||
&self,
|
||
ctx: &mut ToolCallContext,
|
||
) -> Result<PreToolCallResult, HookError> {
|
||
if self.blocked_tools.contains(&ctx.call.name) {
|
||
println!("Blocked tool: {}", ctx.call.name);
|
||
Ok(PreToolCallResult::Skip)
|
||
} else {
|
||
Ok(PreToolCallResult::Continue)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### post_tool_call
|
||
|
||
**呼び出しタイミング**: 各ツール実行後(並列実行フェーズの後)
|
||
|
||
**用途**:
|
||
|
||
- 結果の加工・フォーマット
|
||
- 機密情報のマスキング
|
||
- 結果のキャッシュ
|
||
- 実行結果のログ出力
|
||
|
||
**入力**:
|
||
|
||
- `PostToolCallContext`(`ToolCall` + `ToolResult` + `ToolMeta` +
|
||
`Arc<dyn Tool>`)
|
||
|
||
**例**: 結果にプレフィックスを追加
|
||
|
||
```rust
|
||
struct ResultFormatter;
|
||
|
||
#[async_trait]
|
||
impl Hook<PostToolCall> for ResultFormatter {
|
||
async fn call(
|
||
&self,
|
||
ctx: &mut PostToolCallContext,
|
||
) -> Result<PostToolCallResult, HookError> {
|
||
if !ctx.result.is_error {
|
||
ctx.result.content = format!("[OK] {}", ctx.result.content);
|
||
}
|
||
Ok(PostToolCallResult::Continue)
|
||
}
|
||
}
|
||
```
|
||
|
||
### on_turn_end
|
||
|
||
**呼び出しタイミング**: ツール呼び出しなしでターンが終了する直前
|
||
|
||
**用途**:
|
||
|
||
- 生成されたコードのLint/Fmt
|
||
- 出力形式のバリデーション
|
||
- 自己修正のためのリトライ指示
|
||
- 最終結果のログ出力
|
||
- `OnTurnEndResult::Paused` による一時停止
|
||
|
||
### on_abort
|
||
|
||
**呼び出しタイミング**: キャンセル/エラー/AbortなどでWorkerが中断された時
|
||
|
||
**用途**:
|
||
|
||
- クリーンアップ処理
|
||
- 中断理由のログ出力
|
||
- 外部システムへの通知
|
||
|
||
**例**: JSON形式のバリデーション
|
||
|
||
```rust
|
||
struct JsonValidator;
|
||
|
||
#[async_trait]
|
||
impl Hook<OnTurnEnd> for JsonValidator {
|
||
async fn call(
|
||
&self,
|
||
messages: &mut Vec<Message>,
|
||
) -> Result<OnTurnEndResult, HookError> {
|
||
// 最後のアシスタントメッセージを取得
|
||
let last = messages.iter().rev()
|
||
.find(|m| m.role == Role::Assistant);
|
||
|
||
if let Some(msg) = last {
|
||
if let MessageContent::Text(text) = &msg.content {
|
||
// JSONとしてパースを試みる
|
||
if serde_json::from_str::<serde_json::Value>(text).is_err() {
|
||
// 失敗したらリトライ指示
|
||
return Ok(OnTurnEndResult::ContinueWithMessages(vec![
|
||
Message::user("Invalid JSON. Please fix and try again.")
|
||
]));
|
||
}
|
||
}
|
||
}
|
||
Ok(OnTurnEndResult::Finish)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 複数Hookの実行順序
|
||
|
||
Hookは**イベントごとに登録順**に実行されます。
|
||
|
||
```rust
|
||
worker.add_pre_tool_call_hook(HookA); // 1番目に実行
|
||
worker.add_pre_tool_call_hook(HookB); // 2番目に実行
|
||
worker.add_pre_tool_call_hook(HookC); // 3番目に実行
|
||
```
|
||
|
||
### 制御フローの伝播
|
||
|
||
- `Continue`: 後続Hookも実行
|
||
- `Skip`: 現在の処理をスキップし、後続Hookは実行しない
|
||
- `Abort`: 即座にエラーを返し、処理全体を中断
|
||
- `Pause`: Workerを一時停止(再開は`resume`)
|
||
|
||
```
|
||
Hook A: Continue → Hook B: Skip → (Hook Cは実行されない)
|
||
↓
|
||
処理をスキップ
|
||
|
||
Hook A: Continue → Hook B: Abort("reason")
|
||
↓
|
||
WorkerError::Aborted
|
||
|
||
Hook A: Continue → Hook B: Pause
|
||
↓
|
||
WorkerResult::Paused
|
||
```
|
||
|
||
## 設計上のポイント
|
||
|
||
### 1. イベントごとの実装
|
||
|
||
必要なイベントのみ `Hook<Event>` を実装する。
|
||
|
||
### 2. 可変参照による改変
|
||
|
||
`&mut`で引数を受け取るため、直接改変が可能。
|
||
|
||
```rust
|
||
async fn call(&self, ctx: &mut ToolCallContext) -> ... {
|
||
// 引数を直接書き換え
|
||
ctx.call.input["sanitized"] = json!(true);
|
||
Ok(PreToolCallResult::Continue)
|
||
}
|
||
```
|
||
|
||
### 3. 並列実行との統合
|
||
|
||
- `pre_tool_call`: 並列実行**前**に逐次実行(許可判定のため)
|
||
- ツール実行: `join_all`で**並列**実行
|
||
- `post_tool_call`: 並列実行**後**に逐次実行(結果加工のため)
|
||
|
||
### 4. Send + Sync 要件
|
||
|
||
`Hook`は`Send + Sync`を要求するため、スレッドセーフな実装が必要。
|
||
状態を持つ場合は`Arc<Mutex<T>>`や`AtomicUsize`などを使用する。
|
||
|
||
```rust
|
||
struct CountingHook {
|
||
count: Arc<AtomicUsize>,
|
||
}
|
||
|
||
#[async_trait]
|
||
impl Hook<PreToolCall> for CountingHook {
|
||
async fn call(&self, _: &mut ToolCallContext) -> Result<PreToolCallResult, HookError> {
|
||
self.count.fetch_add(1, Ordering::SeqCst);
|
||
Ok(PreToolCallResult::Continue)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 典型的なユースケース
|
||
|
||
| ユースケース | 使用Hook | 処理内容 |
|
||
| ------------------ | -------------------- | -------------------------- |
|
||
| ツール許可制御 | `pre_tool_call` | 危険なツールをSkip |
|
||
| 実行ログ | `pre/post_tool_call` | 呼び出しと結果を記録 |
|
||
| 出力バリデーション | `on_turn_end` | 形式チェック、リトライ指示 |
|
||
| コンテキスト注入 | `on_message_send` | システムメッセージ追加 |
|
||
| 結果のサニタイズ | `post_tool_call` | 機密情報のマスキング |
|
||
| レート制限 | `pre_tool_call` | 呼び出し頻度の制御 |
|
||
|
||
## TODO
|
||
|
||
### Hooks仕様の厳密な再定義
|
||
|
||
現在のHooks実装は基本的なユースケースをカバーしているが、以下の点について将来的に厳密な仕様を定義する必要がある:
|
||
|
||
- **エラーハンドリングの明確化**:
|
||
`HookError`発生時のリカバリー戦略、部分的な失敗の扱い
|
||
- **Hook間の依存関係**: 複数Hookの実行順序が結果に影響する場合のセマンティクス
|
||
- **非同期キャンセル**: Hook実行中のキャンセル(タイムアウト等)の振る舞い
|
||
- **状態の一貫性**:
|
||
`on_message_send`で改変されたコンテキストが後続処理で期待通りに反映される保証
|
||
- **リトライ制限**:
|
||
`on_turn_end`での`ContinueWithMessages`による無限ループ防止策
|
||
- **Hook優先度**: 登録順以外の優先度指定メカニズムの必要性
|
||
- **条件付きHook**: 特定条件でのみ有効化されるHookパターン
|
||
- **テスト容易性**: Hookのモック/スタブ作成のためのユーティリティ
|