From a4e2795e563eca0206c88c5a6616670e53f0925c Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 6 Jan 2026 13:52:32 +0900 Subject: [PATCH] feat: Add Tool trait definition and tool_registry macro skeleton --- Cargo.lock | 65 +++++++++++ docs/spec/basis.md | 6 +- docs/spec/worker_design.md | 231 +++++++++++++++++++++++++++++++++++++ worker-macros/src/lib.rs | 133 ++++++++++++++++----- worker-types/Cargo.toml | 3 + worker-types/src/lib.rs | 2 + worker-types/src/tool.rs | 33 ++++++ 7 files changed, 440 insertions(+), 33 deletions(-) create mode 100644 docs/spec/worker_design.md create mode 100644 worker-types/src/tool.rs diff --git a/Cargo.lock b/Cargo.lock index b02a9bf..bc5c1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -872,6 +878,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "reqwest" version = "0.13.1" @@ -1047,6 +1073,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "security-framework" version = "3.5.1" @@ -1100,6 +1151,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.148" @@ -1869,8 +1931,11 @@ dependencies = [ name = "worker-types" version = "0.1.0" dependencies = [ + "async-trait", + "schemars", "serde", "serde_json", + "thiserror 2.0.17", ] [[package]] diff --git a/docs/spec/basis.md b/docs/spec/basis.md index 65b77cd..903658e 100644 --- a/docs/spec/basis.md +++ b/docs/spec/basis.md @@ -24,9 +24,9 @@ worker │ ├── scheme │ │ └── openai, anthropic, gemini // APIスキーマ │ ├── events -│ ├── providers -│ │ └── anthropic, googleai, ollama, etc... // プロバイダ -│ └── timeline +│ └── providers +│ └── anthropic, googleai, ollama, etc... // プロバイダ +└── timeline ``` OpenAI互換のプロバイダでスキーマを使い回せるよう、schemeとプロバイダモジュールは分離されている diff --git a/docs/spec/worker_design.md b/docs/spec/worker_design.md new file mode 100644 index 0000000..59ac333 --- /dev/null +++ b/docs/spec/worker_design.md @@ -0,0 +1,231 @@ +# 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` トレイト実装と `InputInputSchema` (schemars利用) を生成。 diff --git a/worker-macros/src/lib.rs b/worker-macros/src/lib.rs index 7ae263a..921d201 100644 --- a/worker-macros/src/lib.rs +++ b/worker-macros/src/lib.rs @@ -1,41 +1,114 @@ -//! worker-macros - LLMワーカー用のProcedural Macros -//! -//! このクレートはTools/Hooksを定義するためのマクロを提供する予定です。 -//! -//! TODO: Tool定義マクロの実装 -//! TODO: Hook定義マクロの実装 - use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ImplItem, ItemImpl}; -/// ツール定義マクロ(未実装) +/// `impl` ブロックに付与し、内部の `#[tool]` 属性がついたメソッドからツールを生成するマクロ。 /// /// # Example /// ```ignore -/// #[tool( -/// name = "get_weather", -/// description = "Get weather information for a city" -/// )] -/// fn get_weather(city: String) -> Result { -/// // ... +/// #[tool_registry] +/// impl MyApp { +/// #[tool] +/// async fn my_function(&self, arg: String) -> Result { ... } /// } /// ``` #[proc_macro_attribute] +pub fn tool_registry(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut impl_block = parse_macro_input!(item as ItemImpl); + let self_ty = &impl_block.self_ty; + + let mut generated_items = Vec::new(); + + for item in &mut impl_block.items { + if let ImplItem::Fn(method) = item { + // #[tool] 属性を探す + let mut is_tool = false; + let mut _description = String::new(); + + // 属性を走査してtoolがあるか確認し、削除する + // 同時にドキュメントコメントから説明を取得 + method.attrs.retain(|attr| { + if attr.path().is_ident("tool") { + is_tool = true; + false // 属性を削除 + } else if attr.path().is_ident("doc") { + // TODO: docコメントのパース + true + } else { + true + } + }); + + if is_tool { + let sig = &method.sig; + let method_name = &sig.ident; + let tool_name = method_name.to_string(); + let tool_struct_name = syn::Ident::new( + &format!("Tool_{}", method_name), + method_name.span(), + ); + + let factory_name = syn::Ident::new( + &format!("{}_tool", method_name), + method_name.span(), + ); + + // TODO: 引数の解析とArgs構造体の生成 + // TODO: descriptionの取得 + + // 仮の実装: Contextを抱えるTool構造体を作成 + let tool_impl = quote! { + #[derive(Clone)] + pub struct #tool_struct_name { + ctx: #self_ty, + } + + #[async_trait::async_trait] + impl worker_types::Tool for #tool_struct_name { + fn name(&self) -> &str { + #tool_name + } + + fn description(&self) -> &str { + "TODO: description from doc comments" + } + + fn input_schema(&self) -> serde_json::Value { + serde_json::json!({}) // TODO: schemars + } + + async fn execute(&self, input_json: &str) -> Result { + // TODO: Deserialize args and call check + // self.ctx.#method_name(...) + Ok("Not implemented yet".to_string()) + } + } + + impl #self_ty { + pub fn #factory_name(&self) -> #tool_struct_name { + #tool_struct_name { + ctx: self.clone() + } + } + } + }; + + generated_items.push(tool_impl); + } + } + } + + let expanded = quote! { + #impl_block + + #(#generated_items)* + }; + + TokenStream::from(expanded) +} + +/// マーカー属性。`tool_registry` によって処理されるため、ここでは何もしない。 +#[proc_macro_attribute] pub fn tool(_attr: TokenStream, item: TokenStream) -> TokenStream { - // TODO: 実装 - item -} - -/// フック定義マクロ(未実装) -/// -/// # Example -/// ```ignore -/// #[hook(on = "before_tool_call")] -/// fn log_tool_call(tool_name: &str) { -/// println!("Calling tool: {}", tool_name); -/// } -/// ``` -#[proc_macro_attribute] -pub fn hook(_attr: TokenStream, item: TokenStream) -> TokenStream { - // TODO: 実装 item } diff --git a/worker-types/Cargo.toml b/worker-types/Cargo.toml index 9ba1934..d878a53 100644 --- a/worker-types/Cargo.toml +++ b/worker-types/Cargo.toml @@ -4,5 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +async-trait = "0.1.89" +schemars = "1.2.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "2.0.17" diff --git a/worker-types/src/lib.rs b/worker-types/src/lib.rs index 96566c4..72b3291 100644 --- a/worker-types/src/lib.rs +++ b/worker-types/src/lib.rs @@ -7,6 +7,8 @@ mod event; mod handler; +mod tool; pub use event::*; pub use handler::*; +pub use tool::*; diff --git a/worker-types/src/tool.rs b/worker-types/src/tool.rs new file mode 100644 index 0000000..9ac4cdd --- /dev/null +++ b/worker-types/src/tool.rs @@ -0,0 +1,33 @@ +use async_trait::async_trait; +use serde_json::Value; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ToolError { + #[error("Invalid argument: {0}")] + InvalidArgument(String), + #[error("Execution failed: {0}")] + ExecutionFailed(String), + #[error("Internal error: {0}")] + Internal(String), +} + +/// ツール定義トレイト +/// +/// ユーザー定義のツールはこれを実装し、Workerに登録される。 +/// 通常は `#[tool]` マクロによって自動生成される。 +#[async_trait] +pub trait Tool: Send + Sync { + /// ツール名 (LLMが識別に使用) + fn name(&self) -> &str; + + /// ツールの説明 (LLMへのプロンプトに含まれる) + fn description(&self) -> &str; + + /// 引数のJSON Schema + fn input_schema(&self) -> Value; + + /// 実行関数 + /// JSON文字列を受け取り、デシリアライズして元のメソッドを実行し、結果を返す + async fn execute(&self, input_json: &str) -> Result; +}