feat: #[tool_registry] and #[tool] macros

This commit is contained in:
2026-01-06 22:42:24 +09:00
parent e82e0a3ed9
commit ddd80908c0
11 changed files with 1262 additions and 125 deletions
+206 -1
View File
@@ -228,4 +228,209 @@ pub enum TurnResult {
* ただし、リアルタイム性を重視する場合(ストリーミング中にToolを実行開始等)は将来的な拡張とするが、現状は「結果が揃うのを待って」という要件に従い、収集フェーズと実行フェーズを分ける。
3. **worker-macros**:
* `syn`, `quote` を用いて、関数定義から `Tool` トレイト実装と `InputInputSchema` (schemars利用) を生成。
* `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<TextBlockKind>
worker.on_tool_use_block(my_tool_handler); // Handler<ToolUseBlockKind>
// 単発イベント(スコープ = ())
worker.on_usage(my_usage_handler); // Handler<UsageKind>
worker.on_status(my_status_handler); // Handler<StatusKind>
// 累積イベント(Worker層で追加、スコープ = ())
worker.on_text_complete(my_complete_handler); // Handler<TextCompleteKind>
worker.on_tool_call_complete(my_tool_complete); // Handler<ToolCallCompleteKind>
```
#### 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<TextCompleteKind> 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<C: LlmClient> Worker<C> {
pub fn subscribe<S: WorkerSubscriber + 'static>(&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()`も引き続き使用可能