yoi/crates/llm-worker/docs/spec/hooks.md
2026-04-04 04:27:46 +09:00

19 KiB
Raw Blame History

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 ToolCallContextToolCall + 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 PostToolCallContextToolCall + 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 StreamChunkContextWorker層のEventを含む)

用途:

  • 監査・ログ記録
  • 低遅延での介入

on_text_delta

呼び出しタイミング: BlockDeltaイベントのうちDeltaContent::Text受信時

入力: &mut TextDeltaContextindex + delta

用途:

  • テキスト出力の監視
  • 特定パターン検出による中断

on_tool_call_delta

呼び出しタイミング: BlockDeltaイベントのうちDeltaContent::InputJson受信時

入力: &mut ToolCallDeltaContextindex + delta_json_fragment

用途:

  • ツール引数JSONの部分監視
  • 危険な引数パターン検出による中断

on_stream_complete

呼び出しタイミング: 1回のストリームが完了した後内側ループ終了後

入力: &mut StreamCompleteContextturn + 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到着時の順序:

  1. Timelineへdispatchcollector/subscriber更新
  2. on_stream_chunk
  3. on_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),    // 内部エラー
}

HookErrorWorkerError::Hookに変換され、finalize_interruptionon_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 要件

HookSend + Syncを要求するため、スレッドセーフな実装が必要。 状態を持つ場合はArc<Mutex<T>>AtomicUsizeなどを使用する。

5. pre_llm_request / on_turn_end のコンテキスト

pre_llm_requeston_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 外部システムへの通知