19 KiB
Hooks 仕様
概要
HookはWorker層でのターン制御に介入するためのメカニズムである。
メッセージ送信・ツール実行・ストリーム処理・ターン終了等の各ポイントで処理を差し込むことができる。
コンセプト
- 制御の介入: ターンの進行、メッセージの内容、ツールの実行に対して介入
- Contextへのアクセス: Item履歴を読み書き可能
- 非破壊的チェーン: 複数のHookを登録順に実行、後続Hookへの影響を制御
Hook一覧
| Hook | タイミング | 主な用途 | 戻り値 |
|---|---|---|---|
on_prompt_submit |
run() 呼び出し時 |
ユーザーItemの前処理 | OnPromptSubmitResult |
pre_llm_request |
各ターンのLLM送信前 | コンテキスト改変/検証 | PreLlmRequestResult |
pre_tool_call |
ツール実行前 | 実行許可/引数改変 | PreToolCallResult |
post_tool_call |
ツール実行後 | 結果加工/マスキング | PostToolCallResult |
on_stream_chunk |
ストリーム各イベント受信後 | 監査/低遅延介入 | StreamHookResult |
on_text_delta |
Text delta受信時 | 出力監視/中断 | StreamHookResult |
on_tool_call_delta |
Tool JSON delta受信時 | 引数監視/中断 | StreamHookResult |
on_stream_complete |
1回のstream完了時 | サマリ/完了検証 | StreamHookResult |
on_turn_end |
ツールなしでターン終了直前 | 検証/リトライ指示 | OnTurnEndResult |
on_abort |
中断時 | クリーンアップ/通知 | () |
Hook Trait
#[async_trait]
pub trait Hook<E: HookEventKind>: Send + Sync {
async fn call(&self, input: &mut E::Input) -> Result<E::Output, HookError>;
}
HookEventKind / 制御フロー型
Hookイベントごとに入力型(Input)と出力型(Output)を分離し、意味のない制御フローを排除する。
pub trait HookEventKind: Send + Sync + 'static {
type Input;
type Output;
}
イベント種別一覧
pub struct OnPromptSubmit; // Input: Item, Output: OnPromptSubmitResult
pub struct PreLlmRequest; // Input: Vec<Item>, Output: PreLlmRequestResult
pub struct PreToolCall; // Input: ToolCallContext, Output: PreToolCallResult
pub struct PostToolCall; // Input: PostToolCallContext, Output: PostToolCallResult
pub struct OnTurnEnd; // Input: Vec<Item>, Output: OnTurnEndResult
pub struct OnAbort; // Input: String, Output: ()
pub struct OnTextDelta; // Input: TextDeltaContext, Output: StreamHookResult
pub struct OnToolCallDelta; // Input: ToolCallDeltaContext, Output: StreamHookResult
pub struct OnStreamChunk; // Input: StreamChunkContext, Output: StreamHookResult
pub struct OnStreamComplete; // Input: StreamCompleteContext, Output: StreamHookResult
制御フロー Result 型
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<Item>),
Paused,
}
pub enum StreamHookResult {
Continue,
Abort(String),
Pause,
}
コンテキスト型
ToolCallContext (PreToolCall用)
pub struct ToolCallContext {
pub call: ToolCall, // ツール呼び出し情報(改変可能)
pub meta: ToolMeta, // 不変メタデータ
pub tool: Arc<dyn Tool>, // 状態アクセス用
}
PostToolCallContext (PostToolCall用)
pub struct PostToolCallContext {
pub call: ToolCall, // ツール呼び出し情報
pub result: ToolResult, // 実行結果(改変可能)
pub meta: ToolMeta, // 不変メタデータ
pub tool: Arc<dyn Tool>, // 状態アクセス用
}
TextDeltaContext (OnTextDelta用)
pub struct TextDeltaContext {
pub index: usize, // ブロックインデックス
pub delta: String, // テキストデルタ内容
}
ToolCallDeltaContext (OnToolCallDelta用)
pub struct ToolCallDeltaContext {
pub index: usize, // ブロックインデックス
pub delta_json_fragment: String, // 部分JSONフラグメント
}
StreamChunkContext (OnStreamChunk用)
pub struct StreamChunkContext {
pub event: crate::event::Event, // Worker層の公開イベント
}
StreamCompleteContext (OnStreamComplete用)
pub struct StreamCompleteContext {
pub turn: usize, // 現在のターン番号
pub event_count: usize, // このリクエストでのストリームイベント数
}
呼び出しタイミング
Worker::run(user_input)
│
├── on_prompt_submit ─────────────────────────┐
│ ユーザーItemの前処理・検証 │
│ (最初の1回のみ) │
│ │
└── loop {
│
├── pre_llm_request ──────────────────│
│ history の clone に対して改変・検証 │
│ (毎ターン実行) │
│ │
├── LLMリクエスト送信 & ストリーム処理 │
│ │ │
│ ├── on_stream_chunk (各イベント) │
│ ├── on_text_delta (テキストデルタ) │
│ └── on_tool_call_delta (JSONデルタ) │
│ │
├── on_stream_complete │
│ │
├── ツール呼び出しがある場合: │
│ │ │
│ ├── pre_tool_call (各ツールごと・逐次) │
│ │ 実行可否の判定、引数の改変 │
│ │ │
│ ├── ツール並列実行 (join_all) │
│ │ │
│ └── post_tool_call (各結果ごと・逐次) │
│ 結果の確認、加工、ログ出力 │
│ │
├── ツール結果をhistoryに追加 │
│ → ループ先頭へ │
│ │
└── ツールなしの場合: │
│ │
└── on_turn_end ───────────────┘
最終応答のチェック(Lint/Fmt等)
エラーがあればContinueWithMessagesでリトライ
}
※ 中断時は on_abort が呼ばれる(finalize_interruption経由)
各Hookの詳細
on_prompt_submit
呼び出しタイミング: run() でユーザーメッセージを受け取った直後(最初の1回のみ)
入力: &mut Item - ユーザーItem(改変可能)
用途:
- ユーザー入力のバリデーション
- 入力のサニタイズ・フィルタリング
- ログ出力
OnPromptSubmitResult::Cancelによる実行キャンセル(WorkerError::Abortedになる)
例: 入力のバリデーション
struct InputValidator;
#[async_trait]
impl Hook<OnPromptSubmit> for InputValidator {
async fn call(
&self,
item: &mut Item,
) -> Result<OnPromptSubmitResult, HookError> {
if let Item::Message { content, .. } = item {
if content.trim().is_empty() {
return Ok(OnPromptSubmitResult::Cancel("Empty input".to_string()));
}
}
Ok(OnPromptSubmitResult::Continue)
}
}
pre_llm_request
呼び出しタイミング: 各ターンのLLMリクエスト送信前(ループの毎回)
入力: &mut Vec<Item> - historyのclone(改変可能、元のhistoryは変更されない)
用途:
- コンテキストへのシステムメッセージ注入
- Itemのバリデーション
- 機密情報のフィルタリング
- リクエスト内容のログ出力
PreLlmRequestResult::Cancelによる送信キャンセル(WorkerError::Abortedになる)
pre_tool_call
呼び出しタイミング: 各ツール実行前(並列実行フェーズの前に逐次実行)
入力: &mut ToolCallContext(ToolCall + ToolMeta + Arc<dyn Tool>)
用途:
- 危険なツールのブロック(
Skip) - 引数のサニタイズ(
context.call.inputの直接改変) - 確認プロンプトの表示(UIとの連携)
- 実行ログの記録
PreToolCallResult::Pauseによる一時停止(WorkerResult::Pausedを返す)PreToolCallResult::Abortによる処理全体の中断
備考: 未登録ツール(ToolServerに存在しないツール名)の場合、Hookは適用されずそのまま実行される(実行時にエラーとなる)。
例: 特定ツールをブロック
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) {
Ok(PreToolCallResult::Skip)
} else {
Ok(PreToolCallResult::Continue)
}
}
}
post_tool_call
呼び出しタイミング: 各ツール実行後(並列実行フェーズの後に逐次実行)
入力: &mut PostToolCallContext(ToolCall + ToolResult + ToolMeta + Arc<dyn Tool>)
用途:
- 結果の加工・フォーマット(
context.resultの直接改変) - 機密情報のマスキング
- 結果のキャッシュ
- 実行結果のログ出力
PostToolCallResult::Abortによる処理全体の中断
例: 結果にプレフィックスを追加
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_stream_chunk
呼び出しタイミング: Timeline dispatchの後、各ストリームイベント受信時
入力: &mut StreamChunkContext(Worker層のEventを含む)
用途:
- 監査・ログ記録
- 低遅延での介入
on_text_delta
呼び出しタイミング: BlockDeltaイベントのうちDeltaContent::Text受信時
入力: &mut TextDeltaContext(index + delta)
用途:
- テキスト出力の監視
- 特定パターン検出による中断
on_tool_call_delta
呼び出しタイミング: BlockDeltaイベントのうちDeltaContent::InputJson受信時
入力: &mut ToolCallDeltaContext(index + delta_json_fragment)
用途:
- ツール引数JSONの部分監視
- 危険な引数パターン検出による中断
on_stream_complete
呼び出しタイミング: 1回のストリームが完了した後(内側ループ終了後)
入力: &mut StreamCompleteContext(turn + event_count)
用途:
- ストリーム完了の検証
- イベント数の記録
on_turn_end
呼び出しタイミング: ツール呼び出しなしでターンが終了する直前
入力: &mut Vec<Item> - historyのclone(改変可能)
用途:
- 生成されたコードのLint/Fmt
- 出力形式のバリデーション
- 自己修正のためのリトライ指示(
ContinueWithMessagesでItemを追加して再ループ) - 最終結果のログ出力
OnTurnEndResult::Pausedによる一時停止
例: JSON形式のバリデーション
struct JsonValidator;
#[async_trait]
impl Hook<OnTurnEnd> for JsonValidator {
async fn call(
&self,
items: &mut Vec<Item>,
) -> Result<OnTurnEndResult, HookError> {
// 最後のアシスタントメッセージを検査
let last_text = items.iter().rev().find_map(|item| {
if let Item::Message { role, content, .. } = item {
if role == "assistant" { Some(content.as_str()) } else { None }
} else { None }
});
if let Some(text) = last_text {
if serde_json::from_str::<serde_json::Value>(text).is_err() {
return Ok(OnTurnEndResult::ContinueWithMessages(vec![
Item::user_message("Invalid JSON. Please fix and try again.")
]));
}
}
Ok(OnTurnEndResult::Finish)
}
}
on_abort
呼び出しタイミング: Worker実行が中断された時(finalize_interruption経由)
入力: &mut String - 中断理由
用途:
- クリーンアップ処理
- 中断理由のログ出力
- 外部システムへの通知
発火条件: run()やresume()の結果がErrの場合に必ず発火する。具体的には以下のケース:
WorkerError::Cancelled-- reason:"Cancelled"WorkerError::Aborted(reason)-- reason: フックやキャンセルが指定した理由- その他のエラー(Client, Tool, Hook等) -- reason: エラーの表示文字列
ストリームイベントの処理順序
BlockDelta到着時の順序:
- Timelineへdispatch(collector/subscriber更新)
on_stream_chunkon_text_deltaまたはon_tool_call_delta(デルタ種別による)
※ DeltaContent::Thinking に対するHookは現在存在しない。
複数Hookの実行順序
Hookはイベントごとに登録順に実行される。
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/Finish: 後続Hookも実行Skip: 現在の処理をスキップし、後続Hookは実行しないAbort: 即座にWorkerError::Abortedを返し、処理全体を中断Pause: Workerを一時停止(WorkerResult::Pausedを返す。resume()で再開可能)Cancel:WorkerError::Abortedを返し、処理を中断
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
注: stream系HookのPauseは現状 WorkerError::Aborted("Paused by stream hook") として扱われる。
Hook登録API
Workerは各Hookイベントに対応する登録メソッドを提供する:
worker.add_on_prompt_submit_hook(hook);
worker.add_pre_llm_request_hook(hook);
worker.add_pre_tool_call_hook(hook);
worker.add_post_tool_call_hook(hook);
worker.add_on_turn_end_hook(hook);
worker.add_on_abort_hook(hook);
worker.add_on_text_delta_hook(hook);
worker.add_on_tool_call_delta_hook(hook);
worker.add_on_stream_chunk_hook(hook);
worker.add_on_stream_complete_hook(hook);
これらのメソッドはWorkerの状態(Mutable/CacheLocked)に関係なく利用可能。
HookRegistry
全HookはHookRegistry構造体で内部管理される。各フィールドはVec<Box<dyn Hook<E>>>であり、Workerが初期化時に空のレジストリを作成する。
HookError
pub enum HookError {
Aborted(String), // 処理の中断
Internal(String), // 内部エラー
}
HookErrorはWorkerError::Hookに変換され、finalize_interruptionでon_abortフックが発火する。
設計上のポイント
1. イベントごとの実装
必要なイベントのみ Hook<Event> を実装する。1つの構造体で複数イベントのHookを実装可能。
2. 可変参照による改変
&mutで引数を受け取るため、直接改変が可能。
async fn call(&self, ctx: &mut ToolCallContext) -> ... {
ctx.call.input["sanitized"] = json!(true);
Ok(PreToolCallResult::Continue)
}
3. 並列実行との統合
pre_tool_call: 並列実行前に逐次実行(許可判定のため)- ツール実行:
join_allで並列実行(tokio::select!によりキャンセル可能) post_tool_call: 並列実行後に逐次実行(結果加工のため)
4. Send + Sync 要件
HookはSend + Syncを要求するため、スレッドセーフな実装が必要。
状態を持つ場合はArc<Mutex<T>>やAtomicUsizeなどを使用する。
5. pre_llm_request / on_turn_end のコンテキスト
pre_llm_requestとon_turn_endはhistoryのcloneに対して操作する。Hookによる改変はリクエスト構築時またはリトライ用メッセージ追加時のみ反映され、Worker内部のhistoryは直接変更されない。
典型的なユースケース
| ユースケース | 使用Hook | 処理内容 |
|---|---|---|
| ツール許可制御 | pre_tool_call |
危険なツールをSkip |
| 実行ログ | pre/post_tool_call |
呼び出しと結果を記録 |
| 出力バリデーション | on_turn_end |
形式チェック、リトライ指示 |
| コンテキスト注入 | pre_llm_request |
システムメッセージ追加 |
| 結果のサニタイズ | post_tool_call |
機密情報のマスキング |
| レート制限 | pre_tool_call |
呼び出し頻度の制御 |
| ストリーム監視 | on_text_delta |
出力内容のリアルタイム監視 |
| 中断時通知 | on_abort |
外部システムへの通知 |