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
+75 -8
View File
@@ -204,26 +204,81 @@ impl EventPlayer {
///
/// 事前に定義されたイベントシーケンスをストリームとして返す。
/// fixtureファイルからロードすることも、直接イベントを渡すこともできる。
///
/// # 複数リクエスト対応
///
/// `with_responses()`を使用して、複数回のリクエストに対して異なるレスポンスを設定できる。
/// リクエスト回数が設定されたレスポンス数を超えた場合は空のストリームを返す。
pub struct MockLlmClient {
events: Vec<Event>,
/// 各リクエストに対するレスポンス(イベントシーケンス)
responses: std::sync::Arc<std::sync::Mutex<Vec<Vec<Event>>>>,
/// 現在のリクエストインデックス
request_index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
#[allow(dead_code)]
impl MockLlmClient {
/// イベントリストから直接作成
/// イベントリストから直接作成(単一レスポンス)
///
/// すべてのリクエストに対して同じイベントシーケンスを返す(従来の動作)
pub fn new(events: Vec<Event>) -> Self {
Self { events }
Self {
responses: std::sync::Arc::new(std::sync::Mutex::new(vec![events])),
request_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
/// fixtureファイルからロード
/// 複数のレスポンスを設定
///
/// 各リクエストに対して順番にイベントシーケンスを返す。
/// N回目のリクエストにはN番目のレスポンスが使用される。
///
/// # Example
/// ```ignore
/// let client = MockLlmClient::with_responses(vec![
/// // 1回目のリクエスト: ツール呼び出し
/// vec![Event::tool_use_start(0, "call_1", "my_tool"), ...],
/// // 2回目のリクエスト: テキストレスポンス
/// vec![Event::text_block_start(0), ...],
/// ]);
/// ```
pub fn with_responses(responses: Vec<Vec<Event>>) -> Self {
Self {
responses: std::sync::Arc::new(std::sync::Mutex::new(responses)),
request_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
/// fixtureファイルからロード(単一レスポンス)
pub fn from_fixture(path: impl AsRef<Path>) -> std::io::Result<Self> {
let player = EventPlayer::load(path)?;
let events = player.parse_events();
Ok(Self { events })
Ok(Self::new(events))
}
/// 保持しているイベント数を取得
/// 保持しているレスポンス数を取得
pub fn response_count(&self) -> usize {
self.responses.lock().unwrap().len()
}
/// 最初のレスポンスのイベント数を取得(後方互換性)
pub fn event_count(&self) -> usize {
self.events.len()
self.responses
.lock()
.unwrap()
.first()
.map(|v| v.len())
.unwrap_or(0)
}
/// 現在のリクエストインデックスを取得
pub fn current_request_index(&self) -> usize {
self.request_index.load(std::sync::atomic::Ordering::SeqCst)
}
/// リクエストインデックスをリセット
pub fn reset(&self) {
self.request_index.store(0, std::sync::atomic::Ordering::SeqCst);
}
}
@@ -233,8 +288,20 @@ impl LlmClient for MockLlmClient {
&self,
_request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<Event, ClientError>> + Send>>, ClientError> {
let events = self.events.clone();
let index = self.request_index.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let events = {
let responses = self.responses.lock().unwrap();
if index < responses.len() {
responses[index].clone()
} else {
// レスポンスが尽きた場合は空のストリーム
Vec::new()
}
};
let stream = futures::stream::iter(events.into_iter().map(Ok));
Ok(Box::pin(stream))
}
}