# Tool 設計 ## 概要 `llm-worker`のツールシステムは、LLMが外部リソースにアクセスしたり計算を実行するための仕組みを提供する。 メタ情報の不変性とセッションスコープの状態管理を両立させる設計となっている。 ツールの管理は`ToolServer` / `ToolServerHandle`に分離されており、 Workerから独立したツール管理・実行が可能。 ## 主要な型 ``` type ToolDefinition Fn() -> (ToolMeta, Arc) Worker.register_tool() → ToolServerHandle.register_tool() で呼び出し | v - struct ToolMeta (name, desc, schema) 不変・登録時固定 - trait Tool (execute) 登録時生成・セッション中再利用 - struct ToolServer / ToolServerHandle ツールの管理・実行を担当 ``` ### ToolMeta ツールのメタ情報を保持する不変構造体。登録時に固定され、Worker内で変更されない。 ```rust #[derive(Debug, Clone, PartialEq, Eq)] pub struct ToolMeta { pub name: String, pub description: String, pub input_schema: Value, } impl ToolMeta { pub fn new(name: impl Into) -> Self; pub fn description(mut self, desc: impl Into) -> Self; pub fn input_schema(mut self, schema: Value) -> Self; } ``` **目的:** - LLM へのツール定義として送信 - Hook からの参照(読み取り専用) - 登録後の不変性を保証 ### Tool trait ツールの実行ロジックのみを定義するトレイト。 ```rust #[async_trait] pub trait Tool: Send + Sync { async fn execute(&self, input_json: &str) -> Result; } ``` **設計方針:** - メタ情報(name, description, schema)は含まない - 状態を持つことが可能(セッション中のカウンターなど) - `Send + Sync` で並列実行に対応 **インスタンスのライフサイクル:** 1. `register_tool()` 呼び出し時にファクトリが実行され、インスタンスが生成される 2. LLM がツールを呼び出すと、既存インスタンスの `execute()` が実行される 3. 同じセッション中は同一インスタンスが再利用される ※ 「最初に呼ばれたとき」の遅延初期化ではなく、**登録時の即時初期化**である。 ### ToolDefinition メタ情報とツールインスタンスを生成するファクトリ。 ```rust pub type ToolDefinition = Arc (ToolMeta, Arc) + Send + Sync>; ``` **なぜファクトリか:** - Worker への登録時に一度だけ呼び出される - メタ情報とインスタンスを同時に生成し、整合性を保証 - クロージャでコンテキスト(`self.clone()`)をキャプチャ可能 ### ToolError ```rust #[derive(Debug, Error)] pub enum ToolError { #[error("Invalid argument: {0}")] InvalidArgument(String), #[error("Execution failed: {0}")] ExecutionFailed(String), #[error("Internal error: {0}")] Internal(String), } ``` ## ToolServer / ToolServerHandle ツール管理がWorkerから分離された専用コンポーネント。 `ToolServer`がツールマップを所有し、`ToolServerHandle`がそのクローン可能な参照を提供する。 ### ToolServer ```rust #[derive(Clone, Default)] pub struct ToolServer { tools: Arc>, // HashMap)> } impl ToolServer { pub fn new() -> Self; pub fn handle(&self) -> ToolServerHandle; } ``` ### ToolServerHandle ```rust #[derive(Clone, Default)] pub struct ToolServerHandle { tools: Arc>, } impl ToolServerHandle { // 登録(pub(crate) - Worker経由でのみ呼び出し可能) pub(crate) fn register_tool(&self, factory: WorkerToolDefinition) -> Result<(), ToolServerError>; pub(crate) fn register_tools(&self, factories: impl IntoIterator) -> Result<(), ToolServerError>; // 検索(Hookからのアクセス用) pub fn get_tool(&self, name: &str) -> Option<(ToolMeta, Arc)>; // 実行 pub async fn call_tool(&self, name: &str, input_json: &str) -> Result; // LLMリクエスト用定義生成(名前順ソート済み) pub fn tool_definitions_sorted(&self) -> Vec; } ``` ### ToolServerError ```rust #[derive(Debug, Error, PartialEq, Eq)] pub enum ToolServerError { #[error("Tool with name '{0}' already registered")] DuplicateName(String), #[error("Tool '{0}' not found")] ToolNotFound(String), #[error("Tool execution failed: {0}")] ToolExecution(String), } ``` ### 設計上のポイント - `ToolServer`は`Clone`可能で、`Arc>`により複数箇所で共有可能 - `register_tool` / `register_tools` は `pub(crate)` でWorker経由のみ - `get_tool`, `call_tool` は `pub` でHookやアプリケーションから直接利用可能 - `tool_definitions_sorted()` は名前でソートされた決定的な定義リストを返す - Worker側では `tool_server_handle()` で `ToolServerHandle` を取得可能 ## Worker でのツール管理 Worker は `ToolServerHandle` を内部に持ち、ツール登録のAPIを提供する。 登録は `Mutable` 状態でのみ可能。 ```rust // Worker API (Mutable状態のみ) pub fn register_tool(&mut self, factory: WorkerToolDefinition) -> Result<(), ToolRegistryError>; pub fn register_tools(&mut self, factories: impl IntoIterator) -> Result<(), ToolRegistryError>; // ハンドルの取得(全状態) pub fn tool_server_handle(&self) -> ToolServerHandle; ``` 登録時の処理: 1. ファクトリを呼び出し `(meta, instance)` を取得 2. 同名ツールが既に登録されていればエラー(`ToolRegistryError::DuplicateName`) 3. HashMap に `(meta, instance)` を保存 ## マクロによる自動生成 (`llm-worker-macros`) `#[tool_registry]` マクロと `#[tool]` マクロにより、メソッドから自動的に `Tool` トレイト実装と `ToolDefinition` ファクトリを生成する。 ### マクロ一覧 | マクロ | 種類 | 用途 | | --- | --- | --- | | `#[tool_registry]` | proc_macro_attribute | `impl`ブロックに適用。`#[tool]`メソッドを検出し、コード生成 | | `#[tool]` | proc_macro_attribute | メソッドに適用。マーカー属性(`tool_registry`が処理) | | `#[description]` | proc_macro_attribute | 引数に適用。schemarsの`description`に変換 | ### 使用例 ```rust #[tool_registry] impl MyApp { /// 検索を実行する /// 指定されたクエリでデータベースを検索します。 #[tool] async fn search( &self, #[description = "検索クエリ文字列"] query: String, ) -> String { format!("Results for: {}", query) } } // 生成されるもの: // - SearchArgs 構造体 // - ToolSearch 構造体 (Tool trait実装) // - MyApp::search_definition() メソッド ``` ### 生成されるコード詳細 マクロは以下を自動生成します。 **1. 引数構造体(`{PascalCase}Args`)** ```rust #[derive(serde::Deserialize, schemars::JsonSchema)] struct SearchArgs { #[schemars(description = "検索クエリ文字列")] pub query: String, } ``` - `#[description = "..."]` 属性は `#[schemars(description = "...")]` に変換される - 引数がない場合は空の構造体が生成される **2. ラッパー構造体(`Tool{PascalCase}`)** ```rust #[derive(Clone)] pub struct ToolSearch { ctx: MyApp, } #[async_trait] impl Tool for ToolSearch { async fn execute(&self, input_json: &str) -> Result { let args: SearchArgs = serde_json::from_str(input_json) .map_err(|e| ToolError::InvalidArgument(e.to_string()))?; let result = self.ctx.search(args.query).await; Ok(format!("{:?}", result)) } } ``` - 戻り値が `Result` の場合、`Err` は `ToolError::ExecutionFailed` に変換される - 戻り値が `Result` でない場合、`format!("{:?}", result)` で文字列化される - async/非asyncの両方をサポート **3. ファクトリメソッド(`{method_name}_definition`)** ```rust impl MyApp { pub fn search_definition(&self) -> ToolDefinition { let ctx = self.clone(); Arc::new(move || { let schema = schemars::schema_for!(SearchArgs); let meta = ToolMeta::new("search") .description("検索を実行する\n指定されたクエリでデータベースを検索します。") .input_schema(serde_json::to_value(schema).unwrap_or(json!({}))); let tool: Arc = Arc::new(ToolSearch { ctx: ctx.clone() }); (meta, tool) }) } } ``` - ツール名はメソッド名(snake_case)がそのまま使われる - ツールの説明はメソッドのdocコメント全体が使われる(docコメントがない場合は `"Tool: {name}"` がフォールバック) - `input_schema` は `schemars::schema_for!` で生成される ### 命名規則 | 元のメソッド名 | 引数構造体 | ラッパー構造体 | ファクトリメソッド | | --- | --- | --- | --- | | `search` | `SearchArgs` | `ToolSearch` | `search_definition()` | | `get_user` | `GetUserArgs` | `ToolGetUser` | `get_user_definition()` | PascalCaseへの変換は `to_pascal_case()` 関数で行われる(snake_caseの各セグメントを先頭大文字に変換)。 ## Hook との連携 Hook は `ToolCallContext` / `PostToolCallContext` を通じてメタ情報とインスタンスにアクセスできる。 ```rust pub struct ToolCallContext { pub call: ToolCall, // 呼び出し情報(改変可能) pub meta: ToolMeta, // メタ情報(読み取り専用) pub tool: Arc, // インスタンス(状態アクセス用) } pub struct PostToolCallContext { pub call: ToolCall, // 呼び出し情報 pub result: ToolResult, // 実行結果(改変可能) pub meta: ToolMeta, // メタ情報(読み取り専用) pub tool: Arc, // インスタンス(状態アクセス用) } ``` **用途:** - `meta` で名前やスキーマを確認 - `tool` でツールの内部状態を読み取り(ダウンキャスト必要) - `call` の引数を改変してツールに渡す(PreToolCall) - `result` の内容を改変(PostToolCall) ## 使用例 ### 手動実装 ```rust struct Counter { count: AtomicUsize } #[async_trait] impl Tool for Counter { async fn execute(&self, _: &str) -> Result { let n = self.count.fetch_add(1, Ordering::SeqCst); Ok(format!("count: {}", n)) } } let def: ToolDefinition = Arc::new(|| { let meta = ToolMeta::new("counter") .description("カウンターを増加") .input_schema(json!({"type": "object"})); (meta, Arc::new(Counter { count: AtomicUsize::new(0) })) }); worker.register_tool(def)?; ``` ### マクロ使用(推奨) ```rust #[derive(Clone)] struct App; #[tool_registry] impl App { /// 挨拶する #[tool] async fn greet(&self, #[description = "名前"] name: String) -> String { format!("Hello, {}!", name) } } let app = App; worker.register_tool(app.greet_definition())?; ``` ### 複数ツールの一括登録 ```rust worker.register_tools([ app.search_definition(), app.get_user_definition(), app.greet_definition(), ])?; ``` ## 設計上の決定 | 問題 | 決定 | 理由 | | --- | --- | --- | | メタ情報の変更可能性 | ToolMeta を分離・不変化 | 登録後の整合性を保証 | | 状態管理 | 登録時にインスタンス生成 | セッション中の状態保持、同一インスタンス再利用 | | Factory vs Instance | Factory + 登録時即時呼び出し | コンテキストキャプチャと登録時検証 | | Hook からのアクセス | Context に meta と tool を含む | 柔軟な介入を可能に | | ツール管理の分離 | ToolServer / ToolServerHandle | Worker外からのツール検索・実行を可能に | | 登録の可視性 | `register_tool` は `pub(crate)` | Worker経由でのみ登録を許可し、整合性を維持 | | 定義リストの順序 | 名前順ソート | 決定的なリクエスト生成を保証 |