# Worker & Tool/Hook 設計 ## 概要 `Worker`はアプリケーションの「ターン」を制御する高レベルコンポーネントです。 `LlmClient`と`Timeline`を内包し、ユーザー定義の`Tool`と`Hook`を用いて自律的なインタラクションを行います。 ## アーキテクチャ ```mermaid graph TD User[Application / User] -->|1. Run| Worker Worker -->|2. Event Loop| Timeline Timeline -->|3. Dispatch| Handler[Handlers (inc. ToolExecutor)] subgraph "Worker Layer" Worker Hook[Hooks] end subgraph "Core Layer" Timeline LlmClient end Worker -.->|Intervene| Hook Handler -.->|Execute| Tool[User Defined Tools] ``` ## ライフサイクル (ターン制御) Workerは以下のループ(ターン)を実行します。 1. **Start Turn**: `Worker::run(messages)` 呼び出し 2. **Hook: OnMessageSend**: * ユーザーメッセージの改変、バリデーション、キャンセルが可能。 * コンテキストへのシステムプロンプト注入などもここで行う想定。 3. **Request & Stream**: * LLMへリクエスト送信。イベントストリーム開始。 * `Timeline`によるイベント処理。 4. **Tool Handling (Parallel)**: * レスポンス内に含まれる全てのTool Callを収集。 * 各Toolに対して **Hook: BeforeToolCall** を実行(実行可否、引数改変)。 * 許可されたToolを**並列実行 (`join_all`)**。 * 各Tool実行後に **Hook: AfterToolCall** を実行(結果の確認、加工)。 5. **Next Request Decision**: * Tool実行結果がある場合 -> 結果をMessageとしてContextに追加し、**Step 3へ戻る** (自動ループ)。 * Tool実行がない場合 -> Step 6へ。 6. **Hook: OnTurnEnd**: * 最終的な応答に対するチェック(Lint/Fmt)。 * エラーがある場合、エラーメッセージをContextに追加して **Step 3へ戻る** ことで自己修正を促せる。 * 問題なければターン終了。 ## Tool 設計 ### アーキテクチャ概要 Rustの静的型付けシステムとLLMの動的なツール呼び出し(文字列による指定)を、**Trait Object** と **動的ディスパッチ** を用いて接続します。 1. **共通インターフェース (`Tool` Trait)**: 全てのツールが実装すべき共通の振る舞い(メタデータ取得と実行)を定義します。 2. **ラッパー生成 (`#[tool]` Macro)**: ユーザー定義のメソッドをラップし、`Tool` Traitを実装した構造体を自動生成します。 3. **レジストリ (`HashMap`)**: Workerは動的ディスパッチ用に `HashMap>` でツールを管理します。 この仕組みにより、「名前からツールを探し、JSON引数を型変換して関数を実行する」フローを安全に実現します。 ### 1. Tool Trait 定義 ツールが最低限持つべきインターフェースです。`Send + Sync` を必須とし、マルチスレッド(並列実行)に対応します。 ```rust #[async_trait] pub trait Tool: Send + Sync { /// ツール名 (LLMが識別に使用) fn name(&self) -> &str; /// ツールの説明 (LLMへのプロンプトに含まれる) fn description(&self) -> &str; /// 引数のJSON Schema (schemars等で生成) fn input_schema(&self) -> serde_json::Value; /// 実行関数 /// JSON文字列を受け取り、デシリアライズして元のメソッドを実行し、結果を返す async fn execute(&self, input_json: &str) -> Result; } ``` ### 2. マクロと実装モデル ユーザーは「状態を持つ構造体」とその「メソッド」としてツールを定義します。 **ユーザーコード:** ```rust #[derive(Clone)] // 状態はClone (Arc推奨) で共有される想定 struct MyApp { db: Arc, } impl MyApp { /// ユーザー情報を取得する /// 指定されたIDのユーザーをDBから検索します。 #[tool] async fn get_user( &self, #[description = "取得したいユーザーのID"] user_id: String ) -> Result { let user = self.db.find(&user_id).await?; Ok(user) } } ``` **マクロ展開後のイメージ (擬似コード):** マクロは、元のメソッドに対応する**ラッパー構造体**を生成します。このラッパーが `Tool` Trait を実装します。 ```rust // 1. 引数をデシリアライズ用の中間構造体に変換 #[derive(serde::Deserialize, schemars::JsonSchema)] struct GetUserArgs { /// 取得したいユーザーのID user_id: String, } // 2. ラッパー構造体 (元のコンテキストを持つ) struct GetUserTool { ctx: MyApp, // コンテキストを保持 (Clone) } #[async_trait] impl Tool for GetUserTool { fn name(&self) -> &str { "get_user" } fn description(&self) -> &str { "ユーザー情報を取得する\n指定されたIDのユーザーをDBから検索します。" } fn input_schema(&self) -> serde_json::Value { schemars::schema_for!(GetUserArgs) } async fn execute(&self, input_json: &str) -> Result { // A. JSONを引数構造体に変換 let args: GetUserArgs = serde_json::from_str(input_json) .map_err(|e| ToolError::InvalidArgument(e.to_string()))?; // B. 元のメソッド呼び出し (self.ctx 経由) let result = self.ctx.get_user(args.user_id).await .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?; // C. 結果を文字列化 Ok(format!("{:?}", result)) // または serde_json::to_string(&result) } } ``` ### 3. Workerによる実行フロー Workerは生成されたラッパー構造体を `Box` として保持し、以下のフローで実行します。 1. **登録**: アプリケーション開始時、コンテキスト(`MyApp`)から各ツールのラッパー(`GetUserTool`)を生成し、WorkerのMapに登録。 2. **解決**: LLMからのレスポンスに含まれる `ToolUse { name: "get_user", ... }` を受け取る。 3. **検索**: `name` をキーに Map から `Box` を取得。 4. **実行**: * `tool.execute(json)` を呼び出す。 * 内部で `serde_json` による型変換とメソッド実行が行われる。 * 結果が返る。 これにより、型安全性を保ちつつ、動的なツール実行が可能になります。 ## Hook 設計 ### コンセプト * **制御の介入**: ターンの進行、メッセージの内容、ツールの実行に対して介入します。 * **Contextへのアクセス**: メッセージ履歴(Context)を読み書きできます。 ### Hook Trait ```rust #[async_trait] pub trait WorkerHook: Send + Sync { /// メッセージ送信前。 /// リクエストに含まれるメッセージリストを改変できる。 async fn on_message_send(&self, context: &mut Vec) -> Result { Ok(ControlFlow::Continue) } /// ツール実行前。 /// 実行をキャンセルしたり、引数を書き換えることができる。 async fn before_tool_call(&self, tool_call: &mut ToolCall) -> Result { Ok(ControlFlow::Continue) } /// ツール実行後。 /// 結果を書き換えたり、隠蔽したりできる。 async fn after_tool_call(&self, tool_result: &mut ToolResult) -> Result { Ok(ControlFlow::Continue) } /// ターン終了時。 /// 生成されたメッセージを検査し、必要ならリトライ(ContinueWithMessages)を指示できる。 async fn on_turn_end(&self, messages: &[Message]) -> Result { Ok(TurnResult::Finish) } } pub enum ControlFlow { Continue, Skip, // Tool実行などをスキップ Abort(String), // 処理中断 } pub enum TurnResult { Finish, ContinueWithMessages(Vec), // メッセージを追加してターン継続(自己修正など) } ``` ## 実装方針 1. **Worker Struct**: * `Timeline`を所有。 * `Handler`として「ToolCallCollector」をTimelineに登録。 * `stream`終了後に収集したToolCallを処理するロジックを持つ。 2. **Tool Executor Handler**: * Timeline上ではツール実行を行わず、あくまで「ToolCallブロックの収集」に徹する(Toolの実行は非同期かつ並列で、ストリーム終了後あるいはブロック確定後に行うため)。 * ただし、リアルタイム性を重視する場合(ストリーミング中にToolを実行開始等)は将来的な拡張とするが、現状は「結果が揃うのを待って」という要件に従い、収集フェーズと実行フェーズを分ける。 3. **worker-macros**: * `syn`, `quote` を用いて、関数定義から `Tool` トレイト実装と `InputSchema` (schemars利用) を生成。 ## Worker Event API 設計 ### 背景と目的 Workerは内部でイベントを処理し結果を返しますが、UIへのストリーミング表示やリアルタイムフィードバックには、イベントを外部に公開する仕組みが必要です。 **要件**: 1. テキストデルタをリアルタイムでUIに表示 2. ツール呼び出しの進行状況を表示 3. ブロック完了時に累積結果を受け取る ### 設計思想 Worker APIは **Timeline層のHandler機構の薄いラッパー** として設計します。 | 層 | 目的 | 提供するもの | |---|------|-------------| | **Handler (Timeline層)** | 内部実装、役割分離 | スコープ管理 + Deltaイベント | | **Worker Event API** | ユーザー向け利便性 | Handler露出 + Completeイベント追加 | Handlerのスコープ管理パターン(Start→Delta→End)をそのまま活かしつつ、累積済みのCompleteイベントも追加提供します。 ### APIパターン #### 1. 個別登録: `worker.on_*(handler)` Timelineの`on_*`メソッドを直接露出。必要なイベントだけを個別に登録可能にする。 ```rust // ブロックイベント(スコープ管理あり) worker.on_text_block(my_text_handler); // Handler worker.on_tool_use_block(my_tool_handler); // Handler // 単発イベント(スコープ = ()) worker.on_usage(my_usage_handler); // Handler worker.on_status(my_status_handler); // Handler // 累積イベント(Worker層で追加、スコープ = ()) worker.on_text_complete(my_complete_handler); // Handler worker.on_tool_call_complete(my_tool_complete); // Handler ``` #### 2. 一括登録: `worker.subscribe(subscriber)` `WorkerSubscriber`トレイトを実装し、全ハンドラをまとめて登録。 ```rust /// 統合Subscriberトレイト pub trait WorkerSubscriber: Send { // スコープ型(ブロックイベント用) type TextBlockScope: Default + Send; type ToolUseBlockScope: Default + Send; // === ブロックイベント(スコープ管理あり)=== fn on_text_block( &mut self, _scope: &mut Self::TextBlockScope, _event: &TextBlockEvent, ) {} fn on_tool_use_block( &mut self, _scope: &mut Self::ToolUseBlockScope, _event: &ToolUseBlockEvent, ) {} // === 単発イベント === fn on_usage(&mut self, _event: &UsageEvent) {} fn on_status(&mut self, _event: &StatusEvent) {} fn on_error(&mut self, _event: &ErrorEvent) {} // === 累積イベント(Worker層で追加)=== fn on_text_complete(&mut self, _text: &str) {} fn on_tool_call_complete(&mut self, _call: &ToolCall) {} // === ターン制御 === fn on_turn_start(&mut self, _turn: usize) {} fn on_turn_end(&mut self, _turn: usize) {} } ``` ### 使用例: WorkerSubscriber ```rust struct MyUI { chat_view: ChatView, } impl WorkerSubscriber for MyUI { type TextBlockScope = TextComponent; type ToolUseBlockScope = ToolComponent; fn on_text_block(&mut self, comp: &mut TextComponent, event: &TextBlockEvent) { match event { TextBlockEvent::Start(_) => { // スコープ開始時にコンポーネント初期化(Defaultで自動生成) } TextBlockEvent::Delta(text) => { comp.append(text); self.chat_view.update(comp); } TextBlockEvent::Stop(_) => { comp.set_immutable(); // スコープ終了後に自動破棄 } } } fn on_text_complete(&mut self, text: &str) { // 累積済みテキストを履歴に保存 self.chat_view.add_to_history(text); } fn on_tool_use_block(&mut self, comp: &mut ToolComponent, event: &ToolUseBlockEvent) { match event { ToolUseBlockEvent::Start(start) => { comp.set_name(&start.name); self.chat_view.show_tool_indicator(comp); } ToolUseBlockEvent::InputJsonDelta(delta) => { comp.append_input(delta); } ToolUseBlockEvent::Stop(_) => { comp.finalize(); } } } fn on_tool_call_complete(&mut self, call: &ToolCall) { self.chat_view.update_tool_result(&call.name, &call.input); } } // Worker に登録 let mut worker = Worker::new(client); worker.subscribe(MyUI::new()); let result = worker.run(messages).await?; ``` ### 使用例: 個別登録 ```rust // シンプルなクロージャベース(将来的な糖衣構文として検討) worker.on_text_complete(|text: &str| { println!("Complete: {}", text); }); // または Handler実装 struct TextLogger; impl Handler for TextLogger { type Scope = (); fn on_event(&mut self, _: &mut (), text: &String) { println!("Complete: {}", text); } } worker.on_text_complete(TextLogger); ``` ### 累積イベント用Kind定義 ```rust /// テキスト完了イベント用Kind pub struct TextCompleteKind; impl Kind for TextCompleteKind { type Event = String; // 累積済みテキスト } /// ツール呼び出し完了イベント用Kind pub struct ToolCallCompleteKind; impl Kind for ToolCallCompleteKind { type Event = ToolCall; // 完全なToolCall } ``` ### 内部実装 WorkerはSubscriberを内部で分解し、各Kindに対応するHandlerとしてTimelineに登録します。 累積イベント(TextComplete等)はWorker層で処理し、ブロック終了時に累積結果を渡します。 ```rust impl Worker { pub fn subscribe(&mut self, subscriber: S) { let subscriber = Arc::new(Mutex::new(subscriber)); // TextBlock用ハンドラを登録 self.timeline.on_text_block(TextBlockAdapter { subscriber: subscriber.clone(), }); // 累積イベント用の内部ハンドラも登録 // (TextBlockCollectorのStop時にon_text_completeを呼ぶ) } } ``` ### 設計上のポイント 1. **Handlerの再利用**: 既存のHandler traitをそのまま活用 2. **スコープ管理の維持**: ブロックイベントはStart→Delta→Endのライフサイクルを保持 3. **選択的購読**: on_*で必要なイベントだけ、またはSubscriberで一括 4. **累積イベントの追加**: Worker層でComplete系イベントを追加提供 5. **後方互換性**: 従来の`run()`も引き続き使用可能