6 Commits
Author SHA1 Message Date
Hare 33f1c218f2 feat: pause and resume 2026-01-09 14:50:34 +09:00
Hare 81107c6f5c feat: Implement RequestConfig validation 2026-01-09 01:19:16 +09:00
Hare 3b5c7e2d46 update: worker -> llm-worker 2026-01-09 00:42:33 +09:00
Hare 1d890395cc chore: Update package license, repository, etc 2026-01-09 00:14:27 +09:00
Hare 16afdd799d chore: Update dependencies version 2026-01-08 22:47:07 +09:00
Hare 7d398fa6de update: Merge worker-types crate into worker crate 2026-01-08 22:15:11 +09:00
78 changed files with 2008 additions and 1424 deletions
-109
View File
@@ -1,109 +0,0 @@
---
description: ドキュメントコメントの書き方ガイドライン
---
# ドキュメントコメント スタイルガイド
## 基本原則
1. **利用者視点で書く**: 「何をするものか」「どう使うか」を先に、「なぜそう実装したか」は後に
2. **型パラメータはバッククォートで囲む**: `Handler<K>` ✓ / Handler<K> ✗
3. **Examplesは`worker::`パスで書く**: re-export先のパスを使用
## 構造テンプレート
```rust
/// [1行目: 何をするものか - 利用者が最初に知りたいこと]
///
/// [詳細説明: いつ使うか、なぜ使うか、注意点など]
///
/// # Examples
///
/// ```
/// use worker::SomeType;
///
/// let instance = SomeType::new();
/// instance.do_something();
/// ```
///
/// # Notes (オプション)
///
/// 実装上の注意事項や制限があれば記載
pub struct SomeType { ... }
```
## 良い例・悪い例
### 構造体/Trait
```rust
// ❌ 悪い例(実装視点)
/// Handler<K>からErasedHandler<K>へのラッパー
/// 各Handlerは独自のScope型を持つため、Timelineで保持するには型消去が必要
// ✅ 良い例(利用者視点)
/// `Handler<K>`を`ErasedHandler<K>`として扱うためのラッパー
///
/// 通常は直接使用せず、`Timeline::on_text_block()`などのメソッド経由で
/// 自動的にラップされます。
```
### メソッド
```rust
// ❌ 悪い例(処理内容の説明のみ)
/// ツールを登録する
// ✅ 良い例(何が起きるか、どう使うか)
/// ツールを登録する
///
/// 登録されたツールはLLMからの呼び出しで自動的に実行されます。
/// 同名のツールを登録した場合、後から登録したものが優先されます。
///
/// # Examples
///
/// ```
/// use worker::{Worker, Tool};
///
/// worker.register_tool(MyTool::new());
/// ```
```
### 型パラメータ
```rust
// ❌ HTMLタグとして解釈されてしまう
/// Handler<K>を保持するフィールド
// ✅ バッククォートで囲む
/// `Handler<K>`を保持するフィールド
```
## ドキュメントの配置
| 項目 | 配置場所 |
|-----|---------|
| 型/trait/関数のdoc | 定義元のクレート(worker-types等) |
| モジュールdoc (`//!`) | 各クレートのlib.rsに書く |
| 実装詳細 | 実装コメント (`//`) を使用 |
| 利用者向けでない内部型 | `#[doc(hidden)]`または`pub(crate)` |
## Examplesのuseパス
re-exportされる型のExamplesでは、最終的な公開パスを使用:
```rust
// worker-types/src/tool.rs でも
/// # Examples
/// ```
/// use worker::Tool; // ✓ worker_types::Tool ではなく
/// ```
```
## チェックリスト
- [ ] 1行目は「何をするものか」を利用者視点で説明しているか
- [ ] 型パラメータ (`<T>`, `<K>` 等) はバッククォートで囲んでいるか
- [ ] 主要なpub APIにはExamplesがあるか
- [ ] Examplesの`use`パスは`worker::`になっているか
- [ ] `cargo doc --no-deps`で警告が出ないか
-29
View File
@@ -4,32 +4,3 @@
- クレートに依存関係を追加・更新する際は必ず - クレートに依存関係を追加・更新する際は必ず
`cargo`コマンドを使い、`Cargo.toml`を直接手で書き換えず、必ずコマンド経由で管理すること。 `cargo`コマンドを使い、`Cargo.toml`を直接手で書き換えず、必ずコマンド経由で管理すること。
## worker-types
`worker-types` クレートには次の条件を満たす型だけを置く。
1. **共有セマンティクスの源泉**
- ランタイム(`worker`)、proc-macro(`worker-macros`)、外部利用者のすべてで同じ定義を共有したい値型。
- 例: `BlockId`, `ProviderEvent`, `ToolArgumentsDelta` などイベント/DTO群。
2. **シリアライズ境界を越えるもの**
- serde経由でプロセス外へ渡したり、APIレスポンスとして公開するもの。
- ロジックを持たない純粋なデータキャリアに限定する。
3. **依存の最小化が必要な型**
- `serde`, `serde_json` 程度の軽量依存で収まる。
4. **マクロが直接参照する型**
- 属性/derive/proc-macro が型に対してコード生成する場合は `worker-macros` ->
`worker-types` の単方向依存を維持するため、対象型を `worker-types` に置く。
5. **副作用を伴わないこと**
- `worker-types` 内では I/O・状態保持・スレッド操作などの副作用を禁止。
- 振る舞いを持つ場合でも `impl`
は純粋な計算か軽量ユーティリティのみに留める。
この基準に当てはまらない型(例えばクライアント状態管理、エラー型で追加依存が必要、プロバイダ固有ロジックなど)は
`worker` クレート側に配置し、どうしても公開が必要なら `worker`
経由で再エクスポートする。 何にせよ、`worker` ->
`worker-types`の片方向依存を維持すること。
Generated
+151 -765
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -1,7 +1,12 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"worker", "llm-worker",
"worker-types", "llm-worker-macros",
"worker-macros",
] ]
[workspace.package]
publish = true
edition = "2024"
license = "MIT"
repository = "https://gitea.hareworks.net/Hare/llm_worker_rs"
+8
View File
@@ -0,0 +1,8 @@
Copyright 2026 Hare
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+36 -1
View File
@@ -1 +1,36 @@
# llm-worker-rs # llm-worker
Rusty, Efficient, and Agentic LLM Client Library
`llm-worker` is a Rust library for building autonomous LLM-powered systems. Define tools, register hooks, and let the Worker handle the agentic loop — tool calls are executed automatically until the task completes.
## Features
- Autonomous Execution: The `Worker` manages the full request-response-tool cycle. You provide tools and input; it loops until done.
- Multi-Provider Support: Unified interface for Anthropic, Gemini, OpenAI, and Ollama.
- Tool System: Define tools as async functions. The Worker automatically parses LLM tool calls, executes them in parallel, and feeds results back.
- Hook System: Intercept execution flow with `before_tool_call`, `after_tool_call`, and `on_turn_end` hooks for validation, logging, or self-correction.
- Event-Driven Streaming: Subscribe to real-time events (text deltas, tool calls, usage) for responsive UIs.
- Cache-Aware State Management: Type-state pattern (`Mutable``Locked`) ensures KV cache efficiency by protecting the conversation prefix.
## Quick Start
```rust
use llm_worker::{Worker, Message};
// Create a Worker with your LLM client
let mut worker = Worker::new(client)
.system_prompt("You are a helpful assistant.");
// Register tools (optional)
worker.register_tool(SearchTool::new());
worker.register_tool(CalculatorTool::new());
// Run — the Worker handles tool calls automatically
let history = worker.run("What is 2+2?").await?;
```
## License
MIT
+7
View File
@@ -0,0 +1,7 @@
[licenses]
allow = [
"MIT",
"Apache-2.0",
"Unicode-3.0",
]
confidence-threshold = 0.8
@@ -1,8 +1,10 @@
[package] [package]
name = "worker-macros" name = "llm-worker-macros"
version = "0.1.0" version = "0.1.0"
edition = "2024" publish.workspace = true
publish = false edition.workspace = true
license.workspace = true
repository.workspace = true
[lib] [lib]
proc-macro = true proc-macro = true
@@ -11,4 +13,3 @@ proc-macro = true
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2", features = ["full"] } syn = { version = "2", features = ["full"] }
worker-types = { path = "../worker-types" }
@@ -1,4 +1,4 @@
//! worker-macros - Tool生成用手続きマクロ //! llm-worker-macros - Tool生成用手続きマクロ
//! //!
//! `#[tool_registry]` と `#[tool]` マクロを提供し、 //! `#[tool_registry]` と `#[tool]` マクロを提供し、
//! ユーザー定義のメソッドから `Tool` トレイト実装を自動生成する。 //! ユーザー定義のメソッドから `Tool` トレイト実装を自動生成する。
@@ -193,7 +193,7 @@ fn generate_tool_impl(self_ty: &Type, method: &syn::ImplItemFn) -> proc_macro2::
quote! { quote! {
match result { match result {
Ok(val) => Ok(format!("{:?}", val)), Ok(val) => Ok(format!("{:?}", val)),
Err(e) => Err(worker_types::ToolError::ExecutionFailed(format!("{}", e))), Err(e) => Err(::llm_worker::tool::ToolError::ExecutionFailed(format!("{}", e))),
} }
} }
} else { } else {
@@ -230,7 +230,7 @@ fn generate_tool_impl(self_ty: &Type, method: &syn::ImplItemFn) -> proc_macro2::
} else { } else {
quote! { quote! {
let args: #args_struct_name = serde_json::from_str(input_json) let args: #args_struct_name = serde_json::from_str(input_json)
.map_err(|e| worker_types::ToolError::InvalidArgument(e.to_string()))?; .map_err(|e| ::llm_worker::tool::ToolError::InvalidArgument(e.to_string()))?;
let result = self.ctx.#method_name(#(#arg_names),*)#awaiter; let result = self.ctx.#method_name(#(#arg_names),*)#awaiter;
#result_handling #result_handling
@@ -246,7 +246,7 @@ fn generate_tool_impl(self_ty: &Type, method: &syn::ImplItemFn) -> proc_macro2::
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl worker_types::Tool for #tool_struct_name { impl ::llm_worker::tool::Tool for #tool_struct_name {
fn name(&self) -> &str { fn name(&self) -> &str {
#tool_name #tool_name
} }
@@ -260,7 +260,7 @@ fn generate_tool_impl(self_ty: &Type, method: &syn::ImplItemFn) -> proc_macro2::
serde_json::to_value(schema).unwrap_or(serde_json::json!({})) serde_json::to_value(schema).unwrap_or(serde_json::json!({}))
} }
async fn execute(&self, input_json: &str) -> Result<String, worker_types::ToolError> { async fn execute(&self, input_json: &str) -> Result<String, ::llm_worker::tool::ToolError> {
#execute_body #execute_body
} }
} }
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "llm-worker"
description = ""
version = "0.1.0"
publish.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
tracing = "0.1"
async-trait = "0.1"
futures = "0.3"
tokio = { version = "1.49", features = ["macros", "rt-multi-thread"] }
reqwest = { version = "0.13.1", default-features = false, features = ["stream", "json", "native-tls"] }
eventsource-stream = "0.2"
llm-worker-macros = { path = "../llm-worker-macros", version = "0.1" }
[dev-dependencies]
clap = { version = "4.5", features = ["derive", "env"] }
schemars = "1.2"
tempfile = "3.24"
dotenv = "0.15"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -20,9 +20,9 @@ mod recorder;
mod scenarios; mod scenarios;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use worker::llm_client::providers::anthropic::AnthropicClient; use llm_worker::llm_client::providers::anthropic::AnthropicClient;
use worker::llm_client::providers::gemini::GeminiClient; use llm_worker::llm_client::providers::gemini::GeminiClient;
use worker::llm_client::providers::openai::OpenAIClient; use llm_worker::llm_client::providers::openai::OpenAIClient;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@@ -101,7 +101,7 @@ async fn run_scenario_with_ollama(
subdir: &str, subdir: &str,
model: Option<String>, model: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
use worker::llm_client::providers::ollama::OllamaClient; use llm_worker::llm_client::providers::ollama::OllamaClient;
// Ollama typically runs local, no key needed or placeholder // Ollama typically runs local, no key needed or placeholder
let model = model.as_deref().unwrap_or("llama3"); // default example let model = model.as_deref().unwrap_or("llama3"); // default example
let client = OllamaClient::new(model); // base_url placeholder, handled by client default let client = OllamaClient::new(model); // base_url placeholder, handled by client default
@@ -8,7 +8,7 @@ use std::path::Path;
use std::time::{Instant, SystemTime, UNIX_EPOCH}; use std::time::{Instant, SystemTime, UNIX_EPOCH};
use futures::StreamExt; use futures::StreamExt;
use worker::llm_client::{LlmClient, Request}; use llm_worker::llm_client::{LlmClient, Request};
/// 記録されたイベント /// 記録されたイベント
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
@@ -2,7 +2,7 @@
//! //!
//! 各シナリオのリクエストと出力ファイル名を定義 //! 各シナリオのリクエストと出力ファイル名を定義
use worker::llm_client::{Request, ToolDefinition}; use llm_worker::llm_client::{Request, ToolDefinition};
/// テストシナリオ /// テストシナリオ
pub struct TestScenario { pub struct TestScenario {
@@ -39,7 +39,7 @@ use tracing::info;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use worker::{ use llm_worker::{
Worker, Worker,
hook::{ControlFlow, HookError, ToolResult, WorkerHook}, hook::{ControlFlow, HookError, ToolResult, WorkerHook},
llm_client::{ llm_client::{
@@ -51,7 +51,7 @@ use worker::{
}, },
timeline::{Handler, TextBlockEvent, TextBlockKind, ToolUseBlockEvent, ToolUseBlockKind}, timeline::{Handler, TextBlockEvent, TextBlockKind, ToolUseBlockEvent, ToolUseBlockKind},
}; };
use worker_macros::tool_registry; use llm_worker_macros::tool_registry;
// 必要なマクロ展開用インポート // 必要なマクロ展開用インポート
use schemars; use schemars;
+446
View File
@@ -0,0 +1,446 @@
//! Worker層の公開イベント型
//!
//! 外部利用者に公開するためのイベント表現。
use serde::{Deserialize, Serialize};
// =============================================================================
// Core Event Types (from llm_client layer)
// =============================================================================
/// LLMからのストリーミングイベント
///
/// 各LLMプロバイダからのレスポンスは、この`Event`のストリームとして
/// 統一的に処理されます。
///
/// # イベントの種類
///
/// - **メタイベント**: `Ping`, `Usage`, `Status`, `Error`
/// - **ブロックイベント**: `BlockStart`, `BlockDelta`, `BlockStop`, `BlockAbort`
///
/// # ブロックのライフサイクル
///
/// テキストやツール呼び出しは、`BlockStart` → `BlockDelta`(複数) → `BlockStop`
/// の順序でイベントが発生します。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Event {
/// ハートビート
Ping(PingEvent),
/// トークン使用量
Usage(UsageEvent),
/// ストリームのステータス変化
Status(StatusEvent),
/// エラー発生
Error(ErrorEvent),
/// ブロック開始(テキスト、ツール使用等)
BlockStart(BlockStart),
/// ブロックの差分データ
BlockDelta(BlockDelta),
/// ブロック正常終了
BlockStop(BlockStop),
/// ブロック中断
BlockAbort(BlockAbort),
}
// =============================================================================
// Meta Events
// =============================================================================
/// Pingイベント(ハートビート)
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct PingEvent {
pub timestamp: Option<u64>,
}
/// 使用量イベント
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct UsageEvent {
/// 入力トークン数
pub input_tokens: Option<u64>,
/// 出力トークン数
pub output_tokens: Option<u64>,
/// 合計トークン数
pub total_tokens: Option<u64>,
/// キャッシュ読み込みトークン数
pub cache_read_input_tokens: Option<u64>,
/// キャッシュ作成トークン数
pub cache_creation_input_tokens: Option<u64>,
}
/// ステータスイベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatusEvent {
pub status: ResponseStatus,
}
/// レスポンスステータス
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ResponseStatus {
/// ストリーム開始
Started,
/// 正常完了
Completed,
/// キャンセルされた
Cancelled,
/// エラー発生
Failed,
}
/// エラーイベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorEvent {
pub code: Option<String>,
pub message: String,
}
// =============================================================================
// Block Types
// =============================================================================
/// ブロックの種別
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BlockType {
/// テキスト生成
Text,
/// 思考 (Claude Extended Thinking等)
Thinking,
/// ツール呼び出し
ToolUse,
/// ツール結果
ToolResult,
}
/// ブロック開始イベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockStart {
/// ブロックのインデックス
pub index: usize,
/// ブロックの種別
pub block_type: BlockType,
/// ブロック固有のメタデータ
pub metadata: BlockMetadata,
}
impl BlockStart {
pub fn block_type(&self) -> BlockType {
self.block_type
}
}
/// ブロックのメタデータ
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BlockMetadata {
Text,
Thinking,
ToolUse { id: String, name: String },
ToolResult { tool_use_id: String },
}
/// ブロックデルタイベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockDelta {
/// ブロックのインデックス
pub index: usize,
/// デルタの内容
pub delta: DeltaContent,
}
/// デルタの内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DeltaContent {
/// テキストデルタ
Text(String),
/// 思考デルタ
Thinking(String),
/// ツール引数のJSON部分文字列
InputJson(String),
}
impl DeltaContent {
/// デルタのブロック種別を取得
pub fn block_type(&self) -> BlockType {
match self {
DeltaContent::Text(_) => BlockType::Text,
DeltaContent::Thinking(_) => BlockType::Thinking,
DeltaContent::InputJson(_) => BlockType::ToolUse,
}
}
}
/// ブロック停止イベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockStop {
/// ブロックのインデックス
pub index: usize,
/// ブロックの種別
pub block_type: BlockType,
/// 停止理由
pub stop_reason: Option<StopReason>,
}
impl BlockStop {
pub fn block_type(&self) -> BlockType {
self.block_type
}
}
/// ブロック中断イベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockAbort {
/// ブロックのインデックス
pub index: usize,
/// ブロックの種別
pub block_type: BlockType,
/// 中断理由
pub reason: String,
}
impl BlockAbort {
pub fn block_type(&self) -> BlockType {
self.block_type
}
}
/// 停止理由
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StopReason {
/// 自然終了
EndTurn,
/// 最大トークン数到達
MaxTokens,
/// ストップシーケンス到達
StopSequence,
/// ツール使用
ToolUse,
}
// =============================================================================
// Builder / Factory helpers
// =============================================================================
impl Event {
/// テキストブロック開始イベントを作成
pub fn text_block_start(index: usize) -> Self {
Event::BlockStart(BlockStart {
index,
block_type: BlockType::Text,
metadata: BlockMetadata::Text,
})
}
/// テキストデルタイベントを作成
pub fn text_delta(index: usize, text: impl Into<String>) -> Self {
Event::BlockDelta(BlockDelta {
index,
delta: DeltaContent::Text(text.into()),
})
}
/// テキストブロック停止イベントを作成
pub fn text_block_stop(index: usize, stop_reason: Option<StopReason>) -> Self {
Event::BlockStop(BlockStop {
index,
block_type: BlockType::Text,
stop_reason,
})
}
/// ツール使用ブロック開始イベントを作成
pub fn tool_use_start(index: usize, id: impl Into<String>, name: impl Into<String>) -> Self {
Event::BlockStart(BlockStart {
index,
block_type: BlockType::ToolUse,
metadata: BlockMetadata::ToolUse {
id: id.into(),
name: name.into(),
},
})
}
/// ツール引数デルタイベントを作成
pub fn tool_input_delta(index: usize, json: impl Into<String>) -> Self {
Event::BlockDelta(BlockDelta {
index,
delta: DeltaContent::InputJson(json.into()),
})
}
/// ツール使用ブロック停止イベントを作成
pub fn tool_use_stop(index: usize) -> Self {
Event::BlockStop(BlockStop {
index,
block_type: BlockType::ToolUse,
stop_reason: Some(StopReason::ToolUse),
})
}
/// 使用量イベントを作成
pub fn usage(input_tokens: u64, output_tokens: u64) -> Self {
Event::Usage(UsageEvent {
input_tokens: Some(input_tokens),
output_tokens: Some(output_tokens),
total_tokens: Some(input_tokens + output_tokens),
cache_read_input_tokens: None,
cache_creation_input_tokens: None,
})
}
/// Pingイベントを作成
pub fn ping() -> Self {
Event::Ping(PingEvent { timestamp: None })
}
}
// =============================================================================
// Conversions: timeline::event -> worker::event
// =============================================================================
impl From<crate::timeline::event::ResponseStatus> for ResponseStatus {
fn from(value: crate::timeline::event::ResponseStatus) -> Self {
match value {
crate::timeline::event::ResponseStatus::Started => ResponseStatus::Started,
crate::timeline::event::ResponseStatus::Completed => ResponseStatus::Completed,
crate::timeline::event::ResponseStatus::Cancelled => ResponseStatus::Cancelled,
crate::timeline::event::ResponseStatus::Failed => ResponseStatus::Failed,
}
}
}
impl From<crate::timeline::event::BlockType> for BlockType {
fn from(value: crate::timeline::event::BlockType) -> Self {
match value {
crate::timeline::event::BlockType::Text => BlockType::Text,
crate::timeline::event::BlockType::Thinking => BlockType::Thinking,
crate::timeline::event::BlockType::ToolUse => BlockType::ToolUse,
crate::timeline::event::BlockType::ToolResult => BlockType::ToolResult,
}
}
}
impl From<crate::timeline::event::BlockMetadata> for BlockMetadata {
fn from(value: crate::timeline::event::BlockMetadata) -> Self {
match value {
crate::timeline::event::BlockMetadata::Text => BlockMetadata::Text,
crate::timeline::event::BlockMetadata::Thinking => BlockMetadata::Thinking,
crate::timeline::event::BlockMetadata::ToolUse { id, name } => {
BlockMetadata::ToolUse { id, name }
}
crate::timeline::event::BlockMetadata::ToolResult { tool_use_id } => {
BlockMetadata::ToolResult { tool_use_id }
}
}
}
}
impl From<crate::timeline::event::DeltaContent> for DeltaContent {
fn from(value: crate::timeline::event::DeltaContent) -> Self {
match value {
crate::timeline::event::DeltaContent::Text(text) => DeltaContent::Text(text),
crate::timeline::event::DeltaContent::Thinking(text) => DeltaContent::Thinking(text),
crate::timeline::event::DeltaContent::InputJson(json) => DeltaContent::InputJson(json),
}
}
}
impl From<crate::timeline::event::StopReason> for StopReason {
fn from(value: crate::timeline::event::StopReason) -> Self {
match value {
crate::timeline::event::StopReason::EndTurn => StopReason::EndTurn,
crate::timeline::event::StopReason::MaxTokens => StopReason::MaxTokens,
crate::timeline::event::StopReason::StopSequence => StopReason::StopSequence,
crate::timeline::event::StopReason::ToolUse => StopReason::ToolUse,
}
}
}
impl From<crate::timeline::event::PingEvent> for PingEvent {
fn from(value: crate::timeline::event::PingEvent) -> Self {
PingEvent {
timestamp: value.timestamp,
}
}
}
impl From<crate::timeline::event::UsageEvent> for UsageEvent {
fn from(value: crate::timeline::event::UsageEvent) -> Self {
UsageEvent {
input_tokens: value.input_tokens,
output_tokens: value.output_tokens,
total_tokens: value.total_tokens,
cache_read_input_tokens: value.cache_read_input_tokens,
cache_creation_input_tokens: value.cache_creation_input_tokens,
}
}
}
impl From<crate::timeline::event::StatusEvent> for StatusEvent {
fn from(value: crate::timeline::event::StatusEvent) -> Self {
StatusEvent {
status: value.status.into(),
}
}
}
impl From<crate::timeline::event::ErrorEvent> for ErrorEvent {
fn from(value: crate::timeline::event::ErrorEvent) -> Self {
ErrorEvent {
code: value.code,
message: value.message,
}
}
}
impl From<crate::timeline::event::BlockStart> for BlockStart {
fn from(value: crate::timeline::event::BlockStart) -> Self {
BlockStart {
index: value.index,
block_type: value.block_type.into(),
metadata: value.metadata.into(),
}
}
}
impl From<crate::timeline::event::BlockDelta> for BlockDelta {
fn from(value: crate::timeline::event::BlockDelta) -> Self {
BlockDelta {
index: value.index,
delta: value.delta.into(),
}
}
}
impl From<crate::timeline::event::BlockStop> for BlockStop {
fn from(value: crate::timeline::event::BlockStop) -> Self {
BlockStop {
index: value.index,
block_type: value.block_type.into(),
stop_reason: value.stop_reason.map(Into::into),
}
}
}
impl From<crate::timeline::event::BlockAbort> for BlockAbort {
fn from(value: crate::timeline::event::BlockAbort) -> Self {
BlockAbort {
index: value.index,
block_type: value.block_type.into(),
reason: value.reason,
}
}
}
impl From<crate::timeline::event::Event> for Event {
fn from(value: crate::timeline::event::Event) -> Self {
match value {
crate::timeline::event::Event::Ping(p) => Event::Ping(p.into()),
crate::timeline::event::Event::Usage(u) => Event::Usage(u.into()),
crate::timeline::event::Event::Status(s) => Event::Status(s.into()),
crate::timeline::event::Event::Error(e) => Event::Error(e.into()),
crate::timeline::event::Event::BlockStart(s) => Event::BlockStart(s.into()),
crate::timeline::event::Event::BlockDelta(d) => Event::BlockDelta(d.into()),
crate::timeline::event::Event::BlockStop(s) => Event::BlockStop(s.into()),
crate::timeline::event::Event::BlockAbort(a) => Event::BlockAbort(a.into()),
}
}
}
@@ -4,7 +4,7 @@
//! カスタムハンドラを実装してTimelineに登録することで、 //! カスタムハンドラを実装してTimelineに登録することで、
//! ストリームイベントを受信できます。 //! ストリームイベントを受信できます。
use crate::event::*; use crate::timeline::event::*;
// ============================================================================= // =============================================================================
// Kind Trait // Kind Trait
@@ -32,7 +32,7 @@ pub trait Kind {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::{Handler, TextBlockKind, TextBlockEvent}; /// use llm_worker::timeline::{Handler, TextBlockEvent, TextBlockKind};
/// ///
/// struct TextCollector { /// struct TextCollector {
/// texts: Vec<String>, /// texts: Vec<String>,
@@ -20,6 +20,8 @@ pub enum ControlFlow {
Skip, Skip,
/// 処理を中断 /// 処理を中断
Abort(String), Abort(String),
/// 処理を一時停止(再開可能)
Pause,
} }
/// ターン終了時の判定結果 /// ターン終了時の判定結果
@@ -29,6 +31,8 @@ pub enum TurnResult {
Finish, Finish,
/// メッセージを追加してターン継続(自己修正など) /// メッセージを追加してターン継続(自己修正など)
ContinueWithMessages(Vec<crate::Message>), ContinueWithMessages(Vec<crate::Message>),
/// ターンを一時停止
Paused,
} }
// ============================================================================= // =============================================================================
@@ -109,7 +113,8 @@ pub enum HookError {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::{WorkerHook, ControlFlow, HookError, ToolCall, TurnResult, Message}; /// use llm_worker::hook::{ControlFlow, HookError, ToolCall, TurnResult, WorkerHook};
/// use llm_worker::Message;
/// ///
/// struct ValidationHook; /// struct ValidationHook;
/// ///
+56
View File
@@ -0,0 +1,56 @@
//! llm-worker - LLMワーカーライブラリ
//!
//! LLMとの対話を管理するコンポーネントを提供します。
//!
//! # 主要なコンポーネント
//!
//! - [`Worker`] - LLMとの対話を管理する中心コンポーネント
//! - [`tool::Tool`] - LLMから呼び出し可能なツール
//! - [`hook::WorkerHook`] - ターン進行への介入
//! - [`subscriber::WorkerSubscriber`] - ストリーミングイベントの購読
//!
//! # Quick Start
//!
//! ```ignore
//! use llm_worker::{Worker, Message};
//!
//! // Workerを作成
//! let mut worker = Worker::new(client)
//! .system_prompt("You are a helpful assistant.");
//!
//! // ツールを登録(オプション)
//! use llm_worker::tool::Tool;
//! worker.register_tool(my_tool);
//!
//! // 対話を実行
//! let history = worker.run("Hello!").await?;
//! ```
//!
//! # キャッシュ保護
//!
//! KVキャッシュのヒット率を最大化するには、[`Worker::lock()`]で
//! ロック状態に遷移してから実行してください。
//!
//! ```ignore
//! let mut locked = worker.lock();
//! locked.run("user input").await?;
//! ```
mod handler;
mod message;
mod worker;
pub mod event;
pub mod hook;
pub mod llm_client;
pub mod state;
pub mod subscriber;
pub mod timeline;
pub mod tool;
// =============================================================================
// トップレベル公開(最も頻繁に使う型)
// =============================================================================
pub use message::{ContentPart, Message, MessageContent, Role};
pub use worker::{Worker, WorkerConfig, WorkerError};
+86
View File
@@ -0,0 +1,86 @@
//! LLMクライアント共通trait定義
use std::pin::Pin;
use crate::llm_client::{ClientError, Request, RequestConfig, event::Event};
use async_trait::async_trait;
use futures::Stream;
/// 設定に関する警告
///
/// プロバイダがサポートしていない設定を使用した場合に返される。
#[derive(Debug, Clone)]
pub struct ConfigWarning {
/// 設定オプション名
pub option_name: &'static str,
/// 警告メッセージ
pub message: String,
}
impl ConfigWarning {
/// 新しい警告を作成
pub fn unsupported(option_name: &'static str, provider_name: &str) -> Self {
Self {
option_name,
message: format!(
"'{}' is not supported by {} and will be ignored",
option_name, provider_name
),
}
}
}
impl std::fmt::Display for ConfigWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.option_name, self.message)
}
}
/// LLMクライアントのtrait
///
/// 各プロバイダはこのtraitを実装し、統一されたインターフェースを提供する。
#[async_trait]
pub trait LlmClient: Send + Sync {
/// ストリーミングリクエストを送信し、Eventストリームを返す
///
/// # Arguments
/// * `request` - リクエスト情報
///
/// # Returns
/// * `Ok(Stream)` - イベントストリーム
/// * `Err(ClientError)` - エラー
async fn stream(
&self,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<Event, ClientError>> + Send>>, ClientError>;
/// 設定をバリデーションし、未サポートの設定があれば警告を返す
///
/// # Arguments
/// * `config` - バリデーション対象の設定
///
/// # Returns
/// サポートされていない設定に対する警告のリスト
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
// デフォルト実装: 全ての設定をサポート
let _ = config;
Vec::new()
}
}
/// `Box<dyn LlmClient>` に対する `LlmClient` の実装
///
/// これにより、動的ディスパッチを使用するクライアントも `Worker` で利用可能になる。
#[async_trait]
impl LlmClient for Box<dyn LlmClient> {
async fn stream(
&self,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<Event, ClientError>> + Send>>, ClientError> {
(**self).stream(request).await
}
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
(**self).validate_config(config)
}
}
@@ -1,7 +1,6 @@
//! イベント型 //! LLMクライアント層のイベント型
//! //!
//! LLMからのストリーミングレスポンスを表現するイベント型。 //! LLMプロバイダからのストリーミングレスポンスを表現するイベント型。
//! Timeline層がこのイベントを受信し、ハンドラにディスパッチします。
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -1,6 +1,7 @@
//! LLMクライアント層 //! LLMクライアント層
//! //!
//! 各LLMプロバイダと通信し、統一された[`Event`](crate::event::Event)ストリームを出力します。 //! 各LLMプロバイダと通信し、統一された[`Event`](crate::llm_client::event::Event)
//! ストリームを出力します。
//! //!
//! # サポートするプロバイダ //! # サポートするプロバイダ
//! //!
@@ -17,6 +18,7 @@
pub mod client; pub mod client;
pub mod error; pub mod error;
pub mod event;
pub mod types; pub mod types;
pub mod providers; pub mod providers;
@@ -24,4 +26,5 @@ pub mod scheme;
pub use client::*; pub use client::*;
pub use error::*; pub use error::*;
pub use event::*;
pub use types::*; pub use types::*;
@@ -4,13 +4,13 @@
use std::pin::Pin; use std::pin::Pin;
use crate::llm_client::{
ClientError, LlmClient, Request, event::Event, scheme::anthropic::AnthropicScheme,
};
use async_trait::async_trait; use async_trait::async_trait;
use eventsource_stream::Eventsource; use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt, TryStreamExt, future::ready}; use futures::{Stream, StreamExt, TryStreamExt, future::ready};
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use worker_types::Event;
use crate::llm_client::{ClientError, LlmClient, Request, scheme::anthropic::AnthropicScheme};
/// Anthropic クライアント /// Anthropic クライアント
pub struct AnthropicClient { pub struct AnthropicClient {
@@ -156,7 +156,8 @@ impl LlmClient for AnthropicClient {
if let Some(block_type) = current_block_type.take() { if let Some(block_type) = current_block_type.take() {
// 正しいブロックタイプで上書き // 正しいブロックタイプで上書き
// (Event::BlockStopの中身を置換) // (Event::BlockStopの中身を置換)
evt = Event::BlockStop(worker_types::BlockStop { evt =
Event::BlockStop(crate::llm_client::event::BlockStop {
block_type, block_type,
..stop.clone() ..stop.clone()
}); });
@@ -4,13 +4,13 @@
use std::pin::Pin; use std::pin::Pin;
use crate::llm_client::{
ClientError, LlmClient, Request, event::Event, scheme::gemini::GeminiScheme,
};
use async_trait::async_trait; use async_trait::async_trait;
use eventsource_stream::Eventsource; use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt, TryStreamExt}; use futures::{Stream, StreamExt, TryStreamExt};
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use worker_types::Event;
use crate::llm_client::{ClientError, LlmClient, Request, scheme::gemini::GeminiScheme};
/// Gemini クライアント /// Gemini クライアント
pub struct GeminiClient { pub struct GeminiClient {
@@ -5,13 +5,12 @@
use std::pin::Pin; use std::pin::Pin;
use crate::llm_client::{
ClientError, LlmClient, Request, event::Event, providers::openai::OpenAIClient,
scheme::openai::OpenAIScheme,
};
use async_trait::async_trait; use async_trait::async_trait;
use futures::Stream; use futures::Stream;
use worker_types::Event;
use crate::llm_client::{
ClientError, LlmClient, Request, providers::openai::OpenAIClient, scheme::openai::OpenAIScheme,
};
/// Ollama クライアント /// Ollama クライアント
/// ///
@@ -4,13 +4,14 @@
use std::pin::Pin; use std::pin::Pin;
use crate::llm_client::{
ClientError, ConfigWarning, LlmClient, Request, RequestConfig, event::Event,
scheme::openai::OpenAIScheme,
};
use async_trait::async_trait; use async_trait::async_trait;
use eventsource_stream::Eventsource; use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt, TryStreamExt}; use futures::{Stream, StreamExt, TryStreamExt};
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use worker_types::Event;
use crate::llm_client::{ClientError, LlmClient, Request, scheme::openai::OpenAIScheme};
/// OpenAI クライアント /// OpenAI クライアント
pub struct OpenAIClient { pub struct OpenAIClient {
@@ -197,4 +198,15 @@ impl LlmClient for OpenAIClient {
Ok(Box::pin(stream)) Ok(Box::pin(stream))
} }
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
// OpenAI does not support top_k
if config.top_k.is_some() {
warnings.push(ConfigWarning::unsupported("top_k", "OpenAI"));
}
warnings
}
} }
@@ -2,13 +2,14 @@
//! //!
//! Anthropic Messages APIのSSEイベントをパースし、統一Event型に変換 //! Anthropic Messages APIのSSEイベントをパースし、統一Event型に変換
use serde::Deserialize; use crate::llm_client::{
use worker_types::{ ClientError,
BlockDelta, BlockMetadata, BlockStart, BlockStop, BlockType, DeltaContent, ErrorEvent, Event, event::{
PingEvent, ResponseStatus, StatusEvent, UsageEvent, BlockDelta, BlockMetadata, BlockStart, BlockStop, BlockType, DeltaContent, ErrorEvent,
Event, PingEvent, ResponseStatus, StatusEvent, UsageEvent,
},
}; };
use serde::Deserialize;
use crate::llm_client::ClientError;
use super::AnthropicScheme; use super::AnthropicScheme;
@@ -23,6 +23,8 @@ pub(crate) struct AnthropicRequest {
pub temperature: Option<f32>, pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>, pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>, pub stop_sequences: Vec<String>,
pub stream: bool, pub stream: bool,
@@ -90,6 +92,7 @@ impl AnthropicScheme {
tools, tools,
temperature: request.config.temperature, temperature: request.config.temperature,
top_p: request.config.top_p, top_p: request.config.top_p,
top_k: request.config.top_k,
stop_sequences: request.config.stop_sequences.clone(), stop_sequences: request.config.stop_sequences.clone(),
stream: true, stream: true,
} }
@@ -2,12 +2,11 @@
//! //!
//! Google Gemini APIのSSEイベントをパースし、統一Event型に変換 //! Google Gemini APIのSSEイベントをパースし、統一Event型に変換
use serde::Deserialize; use crate::llm_client::{
use worker_types::{ ClientError,
BlockMetadata, BlockStart, BlockStop, BlockType, Event, StopReason, UsageEvent, event::{BlockMetadata, BlockStart, BlockStop, BlockType, Event, StopReason, UsageEvent},
}; };
use serde::Deserialize;
use crate::llm_client::ClientError;
use super::GeminiScheme; use super::GeminiScheme;
@@ -231,7 +230,7 @@ impl GeminiScheme {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use worker_types::DeltaContent; use crate::llm_client::event::DeltaContent;
#[test] #[test]
fn test_parse_text_response() { fn test_parse_text_response() {
@@ -133,6 +133,9 @@ pub(crate) struct GeminiGenerationConfig {
/// Top P /// Top P
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>, pub top_p: Option<f32>,
/// Top K
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
/// ストップシーケンス /// ストップシーケンス
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>, pub stop_sequences: Vec<String>,
@@ -183,6 +186,7 @@ impl GeminiScheme {
max_output_tokens: request.config.max_tokens, max_output_tokens: request.config.max_tokens,
temperature: request.config.temperature, temperature: request.config.temperature,
top_p: request.config.top_p, top_p: request.config.top_p,
top_k: request.config.top_k,
stop_sequences: request.config.stop_sequences.clone(), stop_sequences: request.config.stop_sequences.clone(),
}); });
@@ -1,9 +1,10 @@
//! OpenAI SSEイベントパース //! OpenAI SSEイベントパース
use crate::llm_client::{
ClientError,
event::{Event, StopReason, UsageEvent},
};
use serde::Deserialize; use serde::Deserialize;
use worker_types::{Event, StopReason, UsageEvent};
use crate::llm_client::ClientError;
use super::OpenAIScheme; use super::OpenAIScheme;
@@ -155,7 +156,7 @@ impl OpenAIScheme {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use worker_types::DeltaContent; use crate::llm_client::event::DeltaContent;
#[test] #[test]
fn test_parse_text_delta() { fn test_parse_text_delta() {
@@ -188,7 +189,7 @@ mod tests {
assert_eq!(events.len(), 1); assert_eq!(events.len(), 1);
if let Event::BlockStart(start) = &events[0] { if let Event::BlockStart(start) = &events[0] {
assert_eq!(start.index, 0); assert_eq!(start.index, 0);
if let worker_types::BlockMetadata::ToolUse { id, name } = &start.metadata { if let crate::llm_client::event::BlockMetadata::ToolUse { id, name } = &start.metadata {
assert_eq!(id, "call_abc"); assert_eq!(id, "call_abc");
assert_eq!(name, "get_weather"); assert_eq!(name, "get_weather");
} else { } else {
@@ -62,6 +62,30 @@ impl Request {
self.config.max_tokens = Some(max_tokens); self.config.max_tokens = Some(max_tokens);
self self
} }
/// temperatureを設定
pub fn temperature(mut self, temperature: f32) -> Self {
self.config.temperature = Some(temperature);
self
}
/// top_pを設定
pub fn top_p(mut self, top_p: f32) -> Self {
self.config.top_p = Some(top_p);
self
}
/// top_kを設定
pub fn top_k(mut self, top_k: u32) -> Self {
self.config.top_k = Some(top_k);
self
}
/// ストップシーケンスを追加
pub fn stop_sequence(mut self, sequence: impl Into<String>) -> Self {
self.config.stop_sequences.push(sequence.into());
self
}
} }
/// メッセージ /// メッセージ
@@ -191,8 +215,47 @@ pub struct RequestConfig {
pub max_tokens: Option<u32>, pub max_tokens: Option<u32>,
/// Temperature /// Temperature
pub temperature: Option<f32>, pub temperature: Option<f32>,
/// Top P /// Top P (nucleus sampling)
pub top_p: Option<f32>, pub top_p: Option<f32>,
/// Top K
pub top_k: Option<u32>,
/// ストップシーケンス /// ストップシーケンス
pub stop_sequences: Vec<String>, pub stop_sequences: Vec<String>,
} }
impl RequestConfig {
/// 新しいデフォルト設定を作成
pub fn new() -> Self {
Self::default()
}
/// 最大トークン数を設定
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
/// temperatureを設定
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
/// top_pを設定
pub fn with_top_p(mut self, top_p: f32) -> Self {
self.top_p = Some(top_p);
self
}
/// top_kを設定
pub fn with_top_k(mut self, top_k: u32) -> Self {
self.top_k = Some(top_k);
self
}
/// ストップシーケンスを追加
pub fn with_stop_sequence(mut self, sequence: impl Into<String>) -> Self {
self.stop_sequences.push(sequence.into());
self
}
}
@@ -20,7 +20,7 @@ pub enum Role {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::Message; /// use llm_worker::Message;
/// ///
/// // ユーザーメッセージ /// // ユーザーメッセージ
/// let user_msg = Message::user("Hello!"); /// let user_msg = Message::user("Hello!");
@@ -79,7 +79,7 @@ impl Message {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::Message; /// use llm_worker::Message;
/// let msg = Message::user("こんにちは"); /// let msg = Message::user("こんにちは");
/// ``` /// ```
pub fn user(content: impl Into<String>) -> Self { pub fn user(content: impl Into<String>) -> Self {
@@ -24,7 +24,7 @@ mod private {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::Worker; /// use llm_worker::Worker;
/// ///
/// let mut worker = Worker::new(client) /// let mut worker = Worker::new(client)
/// .system_prompt("You are helpful."); /// .system_prompt("You are helpful.");
@@ -1,14 +1,145 @@
//! WorkerSubscriber統合 //! イベント購読
//! //!
//! WorkerSubscriberをTimeline層のHandlerとしてブリッジする実装 //! LLMからのストリーミングイベントをリアルタイムで受信するためのトレイト。
//! UIへのストリーム表示やプログレス表示に使用します。
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use worker_types::{ use crate::{
ErrorEvent, ErrorKind, Handler, StatusEvent, StatusKind, TextBlockEvent, TextBlockKind, handler::{
ToolCall, ToolUseBlockEvent, ToolUseBlockKind, UsageEvent, UsageKind, WorkerSubscriber, ErrorKind, Handler, StatusKind, TextBlockEvent, TextBlockKind, ToolUseBlockEvent,
ToolUseBlockKind, UsageKind,
},
hook::ToolCall,
timeline::event::{ErrorEvent, StatusEvent, UsageEvent},
}; };
// =============================================================================
// WorkerSubscriber Trait
// =============================================================================
/// LLMからのストリーミングイベントを購読するトレイト
///
/// Workerに登録すると、テキスト生成やツール呼び出しのイベントを
/// リアルタイムで受信できます。UIへのストリーム表示に最適です。
///
/// # 受信できるイベント
///
/// - **ブロックイベント**: テキスト、ツール使用(スコープ付き)
/// - **メタイベント**: 使用量、ステータス、エラー
/// - **完了イベント**: テキスト完了、ツール呼び出し完了
/// - **ターン制御**: ターン開始、ターン終了
///
/// # Examples
///
/// ```ignore
/// use llm_worker::subscriber::WorkerSubscriber;
/// use llm_worker::timeline::TextBlockEvent;
///
/// struct StreamPrinter;
///
/// impl WorkerSubscriber for StreamPrinter {
/// type TextBlockScope = ();
/// type ToolUseBlockScope = ();
///
/// fn on_text_block(&mut self, _: &mut (), event: &TextBlockEvent) {
/// if let TextBlockEvent::Delta(text) = event {
/// print!("{}", text); // リアルタイム出力
/// }
/// }
///
/// fn on_text_complete(&mut self, text: &str) {
/// println!("\n--- Complete: {} chars ---", text.len());
/// }
/// }
///
/// // Workerに登録
/// worker.subscribe(StreamPrinter);
/// ```
pub trait WorkerSubscriber: Send {
// =========================================================================
// スコープ型(ブロックイベント用)
// =========================================================================
/// テキストブロック処理用のスコープ型
///
/// ブロック開始時にDefault::default()で生成され、
/// ブロック終了時に破棄される。
type TextBlockScope: Default + Send;
/// ツール使用ブロック処理用のスコープ型
type ToolUseBlockScope: Default + Send;
// =========================================================================
// ブロックイベント(スコープ管理あり)
// =========================================================================
/// テキストブロックイベント
///
/// Start/Delta/Stopのライフサイクルを持つ。
/// scopeはブロック開始時に生成され、終了時に破棄される。
#[allow(unused_variables)]
fn on_text_block(&mut self, scope: &mut Self::TextBlockScope, event: &TextBlockEvent) {}
/// ツール使用ブロックイベント
///
/// Start/InputJsonDelta/Stopのライフサイクルを持つ。
#[allow(unused_variables)]
fn on_tool_use_block(
&mut self,
scope: &mut Self::ToolUseBlockScope,
event: &ToolUseBlockEvent,
) {
}
// =========================================================================
// 単発イベント(スコープ不要)
// =========================================================================
/// 使用量イベント
#[allow(unused_variables)]
fn on_usage(&mut self, event: &UsageEvent) {}
/// ステータスイベント
#[allow(unused_variables)]
fn on_status(&mut self, event: &StatusEvent) {}
/// エラーイベント
#[allow(unused_variables)]
fn on_error(&mut self, event: &ErrorEvent) {}
// =========================================================================
// 累積イベント(Worker層で追加)
// =========================================================================
/// テキスト完了イベント
///
/// テキストブロックが完了した時点で、累積されたテキスト全体が渡される。
/// ブロック処理後の最終結果を受け取るのに便利。
#[allow(unused_variables)]
fn on_text_complete(&mut self, text: &str) {}
/// ツール呼び出し完了イベント
///
/// ツール使用ブロックが完了した時点で、完全なToolCallが渡される。
#[allow(unused_variables)]
fn on_tool_call_complete(&mut self, call: &ToolCall) {}
// =========================================================================
// ターン制御
// =========================================================================
/// ターン開始時
///
/// `turn`は0から始まるターン番号。
#[allow(unused_variables)]
fn on_turn_start(&mut self, turn: usize) {}
/// ターン終了時
#[allow(unused_variables)]
fn on_turn_end(&mut self, turn: usize) {}
}
// ============================================================================= // =============================================================================
// SubscriberAdapter - WorkerSubscriberをTimelineハンドラにブリッジ // SubscriberAdapter - WorkerSubscriberをTimelineハンドラにブリッジ
// ============================================================================= // =============================================================================
+448
View File
@@ -0,0 +1,448 @@
//! Timeline層のイベント型
//!
//! Timelineが受け取り、各Handlerへディスパッチするイベント表現。
use serde::{Deserialize, Serialize};
// =============================================================================
// Core Event Types (from llm_client layer)
// =============================================================================
/// LLMからのストリーミングイベント
///
/// 各LLMプロバイダからのレスポンスは、この`Event`のストリームとして
/// 統一的に処理されます。
///
/// # イベントの種類
///
/// - **メタイベント**: `Ping`, `Usage`, `Status`, `Error`
/// - **ブロックイベント**: `BlockStart`, `BlockDelta`, `BlockStop`, `BlockAbort`
///
/// # ブロックのライフサイクル
///
/// テキストやツール呼び出しは、`BlockStart` → `BlockDelta`(複数) → `BlockStop`
/// の順序でイベントが発生します。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Event {
/// ハートビート
Ping(PingEvent),
/// トークン使用量
Usage(UsageEvent),
/// ストリームのステータス変化
Status(StatusEvent),
/// エラー発生
Error(ErrorEvent),
/// ブロック開始(テキスト、ツール使用等)
BlockStart(BlockStart),
/// ブロックの差分データ
BlockDelta(BlockDelta),
/// ブロック正常終了
BlockStop(BlockStop),
/// ブロック中断
BlockAbort(BlockAbort),
}
// =============================================================================
// Meta Events
// =============================================================================
/// Pingイベント(ハートビート)
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct PingEvent {
pub timestamp: Option<u64>,
}
/// 使用量イベント
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct UsageEvent {
/// 入力トークン数
pub input_tokens: Option<u64>,
/// 出力トークン数
pub output_tokens: Option<u64>,
/// 合計トークン数
pub total_tokens: Option<u64>,
/// キャッシュ読み込みトークン数
pub cache_read_input_tokens: Option<u64>,
/// キャッシュ作成トークン数
pub cache_creation_input_tokens: Option<u64>,
}
/// ステータスイベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatusEvent {
pub status: ResponseStatus,
}
/// レスポンスステータス
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ResponseStatus {
/// ストリーム開始
Started,
/// 正常完了
Completed,
/// キャンセルされた
Cancelled,
/// エラー発生
Failed,
}
/// エラーイベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorEvent {
pub code: Option<String>,
pub message: String,
}
// =============================================================================
// Block Types
// =============================================================================
/// ブロックの種別
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BlockType {
/// テキスト生成
Text,
/// 思考 (Claude Extended Thinking等)
Thinking,
/// ツール呼び出し
ToolUse,
/// ツール結果
ToolResult,
}
/// ブロック開始イベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockStart {
/// ブロックのインデックス
pub index: usize,
/// ブロックの種別
pub block_type: BlockType,
/// ブロック固有のメタデータ
pub metadata: BlockMetadata,
}
impl BlockStart {
pub fn block_type(&self) -> BlockType {
self.block_type
}
}
/// ブロックのメタデータ
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BlockMetadata {
Text,
Thinking,
ToolUse { id: String, name: String },
ToolResult { tool_use_id: String },
}
/// ブロックデルタイベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockDelta {
/// ブロックのインデックス
pub index: usize,
/// デルタの内容
pub delta: DeltaContent,
}
/// デルタの内容
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DeltaContent {
/// テキストデルタ
Text(String),
/// 思考デルタ
Thinking(String),
/// ツール引数のJSON部分文字列
InputJson(String),
}
impl DeltaContent {
/// デルタのブロック種別を取得
pub fn block_type(&self) -> BlockType {
match self {
DeltaContent::Text(_) => BlockType::Text,
DeltaContent::Thinking(_) => BlockType::Thinking,
DeltaContent::InputJson(_) => BlockType::ToolUse,
}
}
}
/// ブロック停止イベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockStop {
/// ブロックのインデックス
pub index: usize,
/// ブロックの種別
pub block_type: BlockType,
/// 停止理由
pub stop_reason: Option<StopReason>,
}
impl BlockStop {
pub fn block_type(&self) -> BlockType {
self.block_type
}
}
/// ブロック中断イベント
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockAbort {
/// ブロックのインデックス
pub index: usize,
/// ブロックの種別
pub block_type: BlockType,
/// 中断理由
pub reason: String,
}
impl BlockAbort {
pub fn block_type(&self) -> BlockType {
self.block_type
}
}
/// 停止理由
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StopReason {
/// 自然終了
EndTurn,
/// 最大トークン数到達
MaxTokens,
/// ストップシーケンス到達
StopSequence,
/// ツール使用
ToolUse,
}
// =============================================================================
// Builder / Factory helpers
// =============================================================================
impl Event {
/// テキストブロック開始イベントを作成
pub fn text_block_start(index: usize) -> Self {
Event::BlockStart(BlockStart {
index,
block_type: BlockType::Text,
metadata: BlockMetadata::Text,
})
}
/// テキストデルタイベントを作成
pub fn text_delta(index: usize, text: impl Into<String>) -> Self {
Event::BlockDelta(BlockDelta {
index,
delta: DeltaContent::Text(text.into()),
})
}
/// テキストブロック停止イベントを作成
pub fn text_block_stop(index: usize, stop_reason: Option<StopReason>) -> Self {
Event::BlockStop(BlockStop {
index,
block_type: BlockType::Text,
stop_reason,
})
}
/// ツール使用ブロック開始イベントを作成
pub fn tool_use_start(index: usize, id: impl Into<String>, name: impl Into<String>) -> Self {
Event::BlockStart(BlockStart {
index,
block_type: BlockType::ToolUse,
metadata: BlockMetadata::ToolUse {
id: id.into(),
name: name.into(),
},
})
}
/// ツール引数デルタイベントを作成
pub fn tool_input_delta(index: usize, json: impl Into<String>) -> Self {
Event::BlockDelta(BlockDelta {
index,
delta: DeltaContent::InputJson(json.into()),
})
}
/// ツール使用ブロック停止イベントを作成
pub fn tool_use_stop(index: usize) -> Self {
Event::BlockStop(BlockStop {
index,
block_type: BlockType::ToolUse,
stop_reason: Some(StopReason::ToolUse),
})
}
/// 使用量イベントを作成
pub fn usage(input_tokens: u64, output_tokens: u64) -> Self {
Event::Usage(UsageEvent {
input_tokens: Some(input_tokens),
output_tokens: Some(output_tokens),
total_tokens: Some(input_tokens + output_tokens),
cache_read_input_tokens: None,
cache_creation_input_tokens: None,
})
}
/// Pingイベントを作成
pub fn ping() -> Self {
Event::Ping(PingEvent { timestamp: None })
}
}
// =============================================================================
// Conversions: llm_client::event -> timeline::event
// =============================================================================
impl From<crate::llm_client::event::ResponseStatus> for ResponseStatus {
fn from(value: crate::llm_client::event::ResponseStatus) -> Self {
match value {
crate::llm_client::event::ResponseStatus::Started => ResponseStatus::Started,
crate::llm_client::event::ResponseStatus::Completed => ResponseStatus::Completed,
crate::llm_client::event::ResponseStatus::Cancelled => ResponseStatus::Cancelled,
crate::llm_client::event::ResponseStatus::Failed => ResponseStatus::Failed,
}
}
}
impl From<crate::llm_client::event::BlockType> for BlockType {
fn from(value: crate::llm_client::event::BlockType) -> Self {
match value {
crate::llm_client::event::BlockType::Text => BlockType::Text,
crate::llm_client::event::BlockType::Thinking => BlockType::Thinking,
crate::llm_client::event::BlockType::ToolUse => BlockType::ToolUse,
crate::llm_client::event::BlockType::ToolResult => BlockType::ToolResult,
}
}
}
impl From<crate::llm_client::event::BlockMetadata> for BlockMetadata {
fn from(value: crate::llm_client::event::BlockMetadata) -> Self {
match value {
crate::llm_client::event::BlockMetadata::Text => BlockMetadata::Text,
crate::llm_client::event::BlockMetadata::Thinking => BlockMetadata::Thinking,
crate::llm_client::event::BlockMetadata::ToolUse { id, name } => {
BlockMetadata::ToolUse { id, name }
}
crate::llm_client::event::BlockMetadata::ToolResult { tool_use_id } => {
BlockMetadata::ToolResult { tool_use_id }
}
}
}
}
impl From<crate::llm_client::event::DeltaContent> for DeltaContent {
fn from(value: crate::llm_client::event::DeltaContent) -> Self {
match value {
crate::llm_client::event::DeltaContent::Text(text) => DeltaContent::Text(text),
crate::llm_client::event::DeltaContent::Thinking(text) => DeltaContent::Thinking(text),
crate::llm_client::event::DeltaContent::InputJson(json) => {
DeltaContent::InputJson(json)
}
}
}
}
impl From<crate::llm_client::event::StopReason> for StopReason {
fn from(value: crate::llm_client::event::StopReason) -> Self {
match value {
crate::llm_client::event::StopReason::EndTurn => StopReason::EndTurn,
crate::llm_client::event::StopReason::MaxTokens => StopReason::MaxTokens,
crate::llm_client::event::StopReason::StopSequence => StopReason::StopSequence,
crate::llm_client::event::StopReason::ToolUse => StopReason::ToolUse,
}
}
}
impl From<crate::llm_client::event::PingEvent> for PingEvent {
fn from(value: crate::llm_client::event::PingEvent) -> Self {
PingEvent {
timestamp: value.timestamp,
}
}
}
impl From<crate::llm_client::event::UsageEvent> for UsageEvent {
fn from(value: crate::llm_client::event::UsageEvent) -> Self {
UsageEvent {
input_tokens: value.input_tokens,
output_tokens: value.output_tokens,
total_tokens: value.total_tokens,
cache_read_input_tokens: value.cache_read_input_tokens,
cache_creation_input_tokens: value.cache_creation_input_tokens,
}
}
}
impl From<crate::llm_client::event::StatusEvent> for StatusEvent {
fn from(value: crate::llm_client::event::StatusEvent) -> Self {
StatusEvent {
status: value.status.into(),
}
}
}
impl From<crate::llm_client::event::ErrorEvent> for ErrorEvent {
fn from(value: crate::llm_client::event::ErrorEvent) -> Self {
ErrorEvent {
code: value.code,
message: value.message,
}
}
}
impl From<crate::llm_client::event::BlockStart> for BlockStart {
fn from(value: crate::llm_client::event::BlockStart) -> Self {
BlockStart {
index: value.index,
block_type: value.block_type.into(),
metadata: value.metadata.into(),
}
}
}
impl From<crate::llm_client::event::BlockDelta> for BlockDelta {
fn from(value: crate::llm_client::event::BlockDelta) -> Self {
BlockDelta {
index: value.index,
delta: value.delta.into(),
}
}
}
impl From<crate::llm_client::event::BlockStop> for BlockStop {
fn from(value: crate::llm_client::event::BlockStop) -> Self {
BlockStop {
index: value.index,
block_type: value.block_type.into(),
stop_reason: value.stop_reason.map(Into::into),
}
}
}
impl From<crate::llm_client::event::BlockAbort> for BlockAbort {
fn from(value: crate::llm_client::event::BlockAbort) -> Self {
BlockAbort {
index: value.index,
block_type: value.block_type.into(),
reason: value.reason,
}
}
}
impl From<crate::llm_client::event::Event> for Event {
fn from(value: crate::llm_client::event::Event) -> Self {
match value {
crate::llm_client::event::Event::Ping(p) => Event::Ping(p.into()),
crate::llm_client::event::Event::Usage(u) => Event::Usage(u.into()),
crate::llm_client::event::Event::Status(s) => Event::Status(s.into()),
crate::llm_client::event::Event::Error(e) => Event::Error(e.into()),
crate::llm_client::event::Event::BlockStart(s) => Event::BlockStart(s.into()),
crate::llm_client::event::Event::BlockDelta(d) => Event::BlockDelta(d.into()),
crate::llm_client::event::Event::BlockStop(s) => Event::BlockStop(s.into()),
crate::llm_client::event::Event::BlockAbort(a) => Event::BlockAbort(a.into()),
}
}
}
@@ -9,25 +9,39 @@
//! - [`TextBlockCollector`] - テキストブロックを収集するHandler //! - [`TextBlockCollector`] - テキストブロックを収集するHandler
//! - [`ToolCallCollector`] - ツール呼び出しを収集するHandler //! - [`ToolCallCollector`] - ツール呼び出しを収集するHandler
pub mod event;
mod text_block_collector; mod text_block_collector;
mod timeline; mod timeline;
mod tool_call_collector; mod tool_call_collector;
// 公開API // 公開API
pub use event::*;
pub use text_block_collector::TextBlockCollector; pub use text_block_collector::TextBlockCollector;
pub use timeline::{ErasedHandler, HandlerWrapper, Timeline}; pub use timeline::{ErasedHandler, HandlerWrapper, Timeline};
pub use tool_call_collector::ToolCallCollector; pub use tool_call_collector::ToolCallCollector;
// worker-typesからのre-export // 型定義からのre-export
pub use worker_types::{ pub use crate::handler::{
// Core traits
Handler, Kind,
// Block Kinds
TextBlockKind, ThinkingBlockKind, ToolUseBlockKind,
// Block Events
TextBlockEvent, TextBlockStart, TextBlockStop,
ThinkingBlockEvent, ThinkingBlockStart, ThinkingBlockStop,
ToolUseBlockEvent, ToolUseBlockStart, ToolUseBlockStop,
// Meta Kinds // Meta Kinds
ErrorKind, PingKind, StatusKind, UsageKind, ErrorKind,
// Core traits
Handler,
Kind,
PingKind,
StatusKind,
// Block Events
TextBlockEvent,
// Block Kinds
TextBlockKind,
TextBlockStart,
TextBlockStop,
ThinkingBlockEvent,
ThinkingBlockKind,
ThinkingBlockStart,
ThinkingBlockStop,
ToolUseBlockEvent,
ToolUseBlockKind,
ToolUseBlockStart,
ToolUseBlockStop,
UsageKind,
}; };
@@ -3,8 +3,8 @@
//! TimelineのTextBlockHandler として登録され、 //! TimelineのTextBlockHandler として登録され、
//! ストリーム中のテキストブロックを収集する。 //! ストリーム中のテキストブロックを収集する。
use crate::handler::{Handler, TextBlockEvent, TextBlockKind};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use worker_types::{Handler, TextBlockEvent, TextBlockKind};
/// TextBlockから収集したテキスト情報を保持 /// TextBlockから収集したテキスト情報を保持
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -85,7 +85,7 @@ impl Handler<TextBlockKind> for TextBlockCollector {
mod tests { mod tests {
use super::*; use super::*;
use crate::timeline::Timeline; use crate::timeline::Timeline;
use worker_types::Event; use crate::timeline::event::Event;
/// TextBlockCollectorが単一のテキストブロックを正しく収集することを確認 /// TextBlockCollectorが単一のテキストブロックを正しく収集することを確認
#[test] #[test]
@@ -5,7 +5,8 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use worker_types::*; use super::event::*;
use crate::handler::*;
// ============================================================================= // =============================================================================
// Type-erased Handler // Type-erased Handler
@@ -327,7 +328,7 @@ where
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::{Timeline, Handler, TextBlockKind, TextBlockEvent}; /// use llm_worker::{Timeline, Handler, TextBlockKind, TextBlockEvent};
/// ///
/// struct MyHandler; /// struct MyHandler;
/// impl Handler<TextBlockKind> for MyHandler { /// impl Handler<TextBlockKind> for MyHandler {
@@ -3,8 +3,11 @@
//! TimelineのToolUseBlockHandler として登録され、 //! TimelineのToolUseBlockHandler として登録され、
//! ストリーム中のToolUseブロックを収集する。 //! ストリーム中のToolUseブロックを収集する。
use crate::{
handler::{Handler, ToolUseBlockEvent, ToolUseBlockKind},
hook::ToolCall,
};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use worker_types::{Handler, ToolCall, ToolUseBlockEvent, ToolUseBlockKind};
/// ToolUseブロックから収集したツール呼び出し情報を保持 /// ToolUseブロックから収集したツール呼び出し情報を保持
/// ///
@@ -98,7 +101,7 @@ impl Handler<ToolUseBlockKind> for ToolCallCollector {
mod tests { mod tests {
use super::*; use super::*;
use crate::timeline::Timeline; use crate::timeline::Timeline;
use worker_types::Event; use crate::timeline::event::Event;
#[test] #[test]
fn test_collect_single_tool_call() { fn test_collect_single_tool_call() {
@@ -31,7 +31,7 @@ pub enum ToolError {
/// 通常は`#[tool]`マクロを使用して自動実装します: /// 通常は`#[tool]`マクロを使用して自動実装します:
/// ///
/// ```ignore /// ```ignore
/// use worker::tool; /// use llm_worker::tool;
/// ///
/// #[tool(description = "Search the web for information")] /// #[tool(description = "Search the web for information")]
/// async fn search(query: String) -> String { /// async fn search(query: String) -> String {
@@ -43,7 +43,7 @@ pub enum ToolError {
/// # 手動実装 /// # 手動実装
/// ///
/// ```ignore /// ```ignore
/// use worker::{Tool, ToolError}; /// use llm_worker::tool::{Tool, ToolError};
/// use serde_json::{json, Value}; /// use serde_json::{json, Value};
/// ///
/// struct MyTool; /// struct MyTool;
+314 -53
View File
@@ -5,15 +5,17 @@ use std::sync::{Arc, Mutex};
use futures::StreamExt; use futures::StreamExt;
use tracing::{debug, info, trace, warn}; use tracing::{debug, info, trace, warn};
use crate::timeline::{TextBlockCollector, Timeline, ToolCallCollector}; use crate::{
use crate::llm_client::{ClientError, LlmClient, Request, ToolDefinition}; ContentPart, Message, MessageContent, Role,
use crate::subscriber_adapter::{ hook::{ControlFlow, HookError, ToolCall, ToolResult, TurnResult, WorkerHook},
llm_client::{ClientError, ConfigWarning, LlmClient, Request, RequestConfig, ToolDefinition},
state::{Locked, Mutable, WorkerState},
subscriber::{
ErrorSubscriberAdapter, StatusSubscriberAdapter, TextBlockSubscriberAdapter, ErrorSubscriberAdapter, StatusSubscriberAdapter, TextBlockSubscriberAdapter,
ToolUseBlockSubscriberAdapter, UsageSubscriberAdapter, ToolUseBlockSubscriberAdapter, UsageSubscriberAdapter, WorkerSubscriber,
}; },
use worker_types::{ timeline::{TextBlockCollector, Timeline, ToolCallCollector},
ContentPart, ControlFlow, HookError, Locked, Message, MessageContent, Mutable, Tool, ToolCall, tool::{Tool, ToolError},
ToolError, ToolResult, TurnResult, WorkerHook, WorkerState, WorkerSubscriber,
}; };
// ============================================================================= // =============================================================================
@@ -35,6 +37,9 @@ pub enum WorkerError {
/// 処理が中断された /// 処理が中断された
#[error("Aborted: {0}")] #[error("Aborted: {0}")]
Aborted(String), Aborted(String),
/// 設定に関する警告(未サポートのオプション)
#[error("Config warnings: {}", .0.iter().map(|w| w.to_string()).collect::<Vec<_>>().join(", "))]
ConfigWarnings(Vec<ConfigWarning>),
} }
// ============================================================================= // =============================================================================
@@ -48,6 +53,25 @@ pub struct WorkerConfig {
_private: (), _private: (),
} }
// =============================================================================
// Worker Result Types
// =============================================================================
/// Workerの実行結果(ステータス)
#[derive(Debug)]
pub enum WorkerResult<'a> {
/// 完了(ユーザー入力待ち状態)
Finished(&'a [Message]),
/// 一時停止(再開可能)
Paused(&'a [Message]),
}
/// 内部用: ツール実行結果
enum ToolExecutionResult {
Completed(Vec<ToolResult>),
Paused,
}
// ============================================================================= // =============================================================================
// ターン制御用コールバック保持 // ターン制御用コールバック保持
// ============================================================================= // =============================================================================
@@ -93,7 +117,7 @@ impl<S: WorkerSubscriber + 'static> TurnNotifier for SubscriberTurnNotifier<S> {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::{Worker, Message}; /// use llm_worker::{Worker, Message};
/// ///
/// // Workerを作成してツールを登録 /// // Workerを作成してツールを登録
/// let mut worker = Worker::new(client) /// let mut worker = Worker::new(client)
@@ -137,6 +161,8 @@ pub struct Worker<C: LlmClient, S: WorkerState = Mutable> {
turn_count: usize, turn_count: usize,
/// ターン通知用のコールバック /// ターン通知用のコールバック
turn_notifiers: Vec<Box<dyn TurnNotifier>>, turn_notifiers: Vec<Box<dyn TurnNotifier>>,
/// リクエスト設定(max_tokens, temperature等)
request_config: RequestConfig,
/// 状態マーカー /// 状態マーカー
_state: PhantomData<S>, _state: PhantomData<S>,
} }
@@ -161,7 +187,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::{Worker, WorkerSubscriber, TextBlockEvent}; /// use llm_worker::{Worker, WorkerSubscriber, TextBlockEvent};
/// ///
/// struct MyPrinter; /// struct MyPrinter;
/// impl WorkerSubscriber for MyPrinter { /// impl WorkerSubscriber for MyPrinter {
@@ -209,7 +235,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::Worker; /// use llm_worker::Worker;
/// use my_tools::SearchTool; /// use my_tools::SearchTool;
/// ///
/// worker.register_tool(SearchTool::new()); /// worker.register_tool(SearchTool::new());
@@ -234,7 +260,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// use worker::{Worker, WorkerHook, ControlFlow, ToolCall}; /// use llm_worker::{Worker, WorkerHook, ControlFlow, ToolCall};
/// ///
/// struct LoggingHook; /// struct LoggingHook;
/// ///
@@ -272,6 +298,83 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
self.turn_count self.turn_count
} }
/// 現在のリクエスト設定への参照を取得
pub fn request_config(&self) -> &RequestConfig {
&self.request_config
}
/// 最大トークン数を設定
///
/// この設定はキャッシュロックとは独立しており、各リクエストに適用されます。
///
/// # Examples
///
/// ```ignore
/// worker.set_max_tokens(4096);
/// ```
pub fn set_max_tokens(&mut self, max_tokens: u32) {
self.request_config.max_tokens = Some(max_tokens);
}
/// temperatureを設定
///
/// 0.0から1.0(または2.0)の範囲で設定します。
/// 低い値はより決定的な出力を、高い値はより多様な出力を生成します。
///
/// # Examples
///
/// ```ignore
/// worker.set_temperature(0.7);
/// ```
pub fn set_temperature(&mut self, temperature: f32) {
self.request_config.temperature = Some(temperature);
}
/// top_pを設定(nucleus sampling
///
/// # Examples
///
/// ```ignore
/// worker.set_top_p(0.9);
/// ```
pub fn set_top_p(&mut self, top_p: f32) {
self.request_config.top_p = Some(top_p);
}
/// top_kを設定
///
/// トークン選択時に考慮する上位k個のトークンを指定します。
///
/// # Examples
///
/// ```ignore
/// worker.set_top_k(40);
/// ```
pub fn set_top_k(&mut self, top_k: u32) {
self.request_config.top_k = Some(top_k);
}
/// ストップシーケンスを追加
///
/// # Examples
///
/// ```ignore
/// worker.add_stop_sequence("\n\n");
/// ```
pub fn add_stop_sequence(&mut self, sequence: impl Into<String>) {
self.request_config.stop_sequences.push(sequence.into());
}
/// ストップシーケンスをクリア
pub fn clear_stop_sequences(&mut self) {
self.request_config.stop_sequences.clear();
}
/// リクエスト設定を一括で設定
pub fn set_request_config(&mut self, config: RequestConfig) {
self.request_config = config;
}
/// 登録されたツールからToolDefinitionのリストを生成 /// 登録されたツールからToolDefinitionのリストを生成
fn build_tool_definitions(&self) -> Vec<ToolDefinition> { fn build_tool_definitions(&self) -> Vec<ToolDefinition> {
self.tools self.tools
@@ -321,7 +424,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
} }
Some(Message { Some(Message {
role: worker_types::Role::Assistant, role: Role::Assistant,
content: MessageContent::Parts(parts), content: MessageContent::Parts(parts),
}) })
} }
@@ -337,39 +440,36 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
// メッセージを追加 // メッセージを追加
for msg in &self.history { for msg in &self.history {
// worker-types::Message から llm_client::Message への変換 // Message から llm_client::Message への変換
request = request.message(crate::llm_client::Message { request = request.message(crate::llm_client::Message {
role: match msg.role { role: match msg.role {
worker_types::Role::User => crate::llm_client::Role::User, Role::User => crate::llm_client::Role::User,
worker_types::Role::Assistant => crate::llm_client::Role::Assistant, Role::Assistant => crate::llm_client::Role::Assistant,
}, },
content: match &msg.content { content: match &msg.content {
worker_types::MessageContent::Text(t) => { MessageContent::Text(t) => crate::llm_client::MessageContent::Text(t.clone()),
crate::llm_client::MessageContent::Text(t.clone()) MessageContent::ToolResult {
}
worker_types::MessageContent::ToolResult {
tool_use_id, tool_use_id,
content, content,
} => crate::llm_client::MessageContent::ToolResult { } => crate::llm_client::MessageContent::ToolResult {
tool_use_id: tool_use_id.clone(), tool_use_id: tool_use_id.clone(),
content: content.clone(), content: content.clone(),
}, },
worker_types::MessageContent::Parts(parts) => { MessageContent::Parts(parts) => crate::llm_client::MessageContent::Parts(
crate::llm_client::MessageContent::Parts(
parts parts
.iter() .iter()
.map(|p| match p { .map(|p| match p {
worker_types::ContentPart::Text { text } => { ContentPart::Text { text } => {
crate::llm_client::ContentPart::Text { text: text.clone() } crate::llm_client::ContentPart::Text { text: text.clone() }
} }
worker_types::ContentPart::ToolUse { id, name, input } => { ContentPart::ToolUse { id, name, input } => {
crate::llm_client::ContentPart::ToolUse { crate::llm_client::ContentPart::ToolUse {
id: id.clone(), id: id.clone(),
name: name.clone(), name: name.clone(),
input: input.clone(), input: input.clone(),
} }
} }
worker_types::ContentPart::ToolResult { ContentPart::ToolResult {
tool_use_id, tool_use_id,
content, content,
} => crate::llm_client::ContentPart::ToolResult { } => crate::llm_client::ContentPart::ToolResult {
@@ -378,8 +478,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
}, },
}) })
.collect(), .collect(),
) ),
}
}, },
}); });
} }
@@ -389,6 +488,9 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
request = request.tool(tool_def.clone()); request = request.tool(tool_def.clone());
} }
// リクエスト設定を適用
request = request.config(self.request_config.clone());
request request
} }
@@ -404,6 +506,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
ControlFlow::Continue => continue, ControlFlow::Continue => continue,
ControlFlow::Skip => return Ok(ControlFlow::Skip), ControlFlow::Skip => return Ok(ControlFlow::Skip),
ControlFlow::Abort(reason) => return Ok(ControlFlow::Abort(reason)), ControlFlow::Abort(reason) => return Ok(ControlFlow::Abort(reason)),
ControlFlow::Pause => return Ok(ControlFlow::Pause),
} }
} }
Ok(ControlFlow::Continue) Ok(ControlFlow::Continue)
@@ -418,11 +521,39 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
TurnResult::ContinueWithMessages(msgs) => { TurnResult::ContinueWithMessages(msgs) => {
return Ok(TurnResult::ContinueWithMessages(msgs)); return Ok(TurnResult::ContinueWithMessages(msgs));
} }
TurnResult::Paused => return Ok(TurnResult::Paused),
} }
} }
Ok(TurnResult::Finish) Ok(TurnResult::Finish)
} }
/// 未実行のツール呼び出しがあるかチェック(Pauseからの復帰用)
fn get_pending_tool_calls(&self) -> Option<Vec<ToolCall>> {
let last_msg = self.history.last()?;
if last_msg.role != Role::Assistant {
return None;
}
let mut calls = Vec::new();
if let MessageContent::Parts(parts) = &last_msg.content {
for part in parts {
if let ContentPart::ToolUse { id, name, input } = part {
calls.push(ToolCall {
id: id.clone(),
name: name.clone(),
input: input.clone(),
});
}
}
}
if calls.is_empty() {
None
} else {
Some(calls)
}
}
/// ツールを並列実行 /// ツールを並列実行
/// ///
/// 全てのツールに対してbefore_tool_callフックを実行後、 /// 全てのツールに対してbefore_tool_callフックを実行後、
@@ -430,7 +561,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
async fn execute_tools( async fn execute_tools(
&self, &self,
tool_calls: Vec<ToolCall>, tool_calls: Vec<ToolCall>,
) -> Result<Vec<ToolResult>, WorkerError> { ) -> Result<ToolExecutionResult, WorkerError> {
use futures::future::join_all; use futures::future::join_all;
// Phase 1: before_tool_call フックを適用(スキップ/中断を判定) // Phase 1: before_tool_call フックを適用(スキップ/中断を判定)
@@ -448,6 +579,9 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
ControlFlow::Abort(reason) => { ControlFlow::Abort(reason) => {
return Err(WorkerError::Aborted(reason)); return Err(WorkerError::Aborted(reason));
} }
ControlFlow::Pause => {
return Ok(ToolExecutionResult::Paused);
}
} }
} }
if !skip { if !skip {
@@ -490,15 +624,22 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
ControlFlow::Abort(reason) => { ControlFlow::Abort(reason) => {
return Err(WorkerError::Aborted(reason)); return Err(WorkerError::Aborted(reason));
} }
ControlFlow::Pause => {
// after_tool_callでのPauseは結果を受け入れた後、次の処理前に止まる動作とする
// ここではContinue扱いとし、on_message_send等でPauseすることを期待する
// あるいはここでのPauseをサポートする場合は戻り値を調整する必要がある
// 現状はログを出してContinue
warn!("ControlFlow::Pause in after_tool_call is treated as Continue");
}
} }
} }
} }
Ok(results) Ok(ToolExecutionResult::Completed(results))
} }
/// 内部で使用するターン実行ロジック /// 内部で使用するターン実行ロジック
async fn run_turn_loop(&mut self) -> Result<(), WorkerError> { async fn run_turn_loop(&mut self) -> Result<WorkerResult<'_>, WorkerError> {
let tool_definitions = self.build_tool_definitions(); let tool_definitions = self.build_tool_definitions();
info!( info!(
@@ -507,6 +648,20 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
"Starting worker run" "Starting worker run"
); );
// Resume check: Pending tool calls
if let Some(tool_calls) = self.get_pending_tool_calls() {
info!("Resuming pending tool calls");
match self.execute_tools(tool_calls).await? {
ToolExecutionResult::Paused => return Ok(WorkerResult::Paused(&self.history)),
ToolExecutionResult::Completed(results) => {
for result in results {
self.history.push(Message::tool_result(&result.tool_use_id, &result.content));
}
// Continue to loop
}
}
}
loop { loop {
// ターン開始を通知 // ターン開始を通知
let current_turn = self.turn_count; let current_turn = self.turn_count;
@@ -517,14 +672,20 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
// Hook: on_message_send // Hook: on_message_send
let control = self.run_on_message_send_hooks().await?; let control = self.run_on_message_send_hooks().await?;
if let ControlFlow::Abort(reason) = control { match control {
ControlFlow::Abort(reason) => {
warn!(reason = %reason, "Aborted by hook"); warn!(reason = %reason, "Aborted by hook");
// ターン終了を通知(異常終了)
for notifier in &self.turn_notifiers { for notifier in &self.turn_notifiers {
notifier.on_turn_end(current_turn); notifier.on_turn_end(current_turn);
} }
return Err(WorkerError::Aborted(reason)); return Err(WorkerError::Aborted(reason));
} }
ControlFlow::Pause | ControlFlow::Skip => {
// Skip or Pause -> Pause the worker
return Ok(WorkerResult::Paused(&self.history));
}
ControlFlow::Continue => {}
}
// リクエスト構築 // リクエスト構築
let request = self.build_request(&tool_definitions); let request = self.build_request(&tool_definitions);
@@ -550,7 +711,8 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
} }
} }
let event = event_result?; let event = event_result?;
self.timeline.dispatch(&event); let timeline_event: crate::timeline::event::Event = event.into();
self.timeline.dispatch(&timeline_event);
} }
debug!(event_count = event_count, "Stream completed"); debug!(event_count = event_count, "Stream completed");
@@ -575,24 +737,35 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
let turn_result = self.run_on_turn_end_hooks().await?; let turn_result = self.run_on_turn_end_hooks().await?;
match turn_result { match turn_result {
TurnResult::Finish => { TurnResult::Finish => {
return Ok(()); return Ok(WorkerResult::Finished(&self.history));
} }
TurnResult::ContinueWithMessages(additional) => { TurnResult::ContinueWithMessages(additional) => {
self.history.extend(additional); self.history.extend(additional);
continue; continue;
} }
TurnResult::Paused => {
return Ok(WorkerResult::Paused(&self.history));
}
} }
} }
// ツール実行 // ツール実行
let tool_results = self.execute_tools(tool_calls).await?; match self.execute_tools(tool_calls).await? {
ToolExecutionResult::Paused => return Ok(WorkerResult::Paused(&self.history)),
ToolExecutionResult::Completed(results) => {
for result in results {
self.history.push(Message::tool_result(&result.tool_use_id, &result.content));
}
}
}
}
}
// ツール結果を履歴に追加 /// 実行を再開(Pause状態からの復帰)
for result in tool_results { ///
self.history /// 新しいユーザーメッセージを履歴に追加せず、現在の状態からターン処理を再開する。
.push(Message::tool_result(&result.tool_use_id, &result.content)); pub async fn resume(&mut self) -> Result<WorkerResult<'_>, WorkerError> {
} self.run_turn_loop().await
}
} }
} }
@@ -623,6 +796,7 @@ impl<C: LlmClient> Worker<C, Mutable> {
locked_prefix_len: 0, locked_prefix_len: 0,
turn_count: 0, turn_count: 0,
turn_notifiers: Vec::new(), turn_notifiers: Vec::new(),
request_config: RequestConfig::default(),
_state: PhantomData, _state: PhantomData,
} }
} }
@@ -638,6 +812,95 @@ impl<C: LlmClient> Worker<C, Mutable> {
self.system_prompt = Some(prompt.into()); self.system_prompt = Some(prompt.into());
} }
/// 最大トークン数を設定(ビルダーパターン)
///
/// # Examples
///
/// ```ignore
/// let worker = Worker::new(client)
/// .system_prompt("You are a helpful assistant.")
/// .max_tokens(4096);
/// ```
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
self.request_config.max_tokens = Some(max_tokens);
self
}
/// temperatureを設定(ビルダーパターン)
///
/// # Examples
///
/// ```ignore
/// let worker = Worker::new(client)
/// .temperature(0.7);
/// ```
pub fn temperature(mut self, temperature: f32) -> Self {
self.request_config.temperature = Some(temperature);
self
}
/// top_pを設定(ビルダーパターン)
pub fn top_p(mut self, top_p: f32) -> Self {
self.request_config.top_p = Some(top_p);
self
}
/// top_kを設定(ビルダーパターン)
pub fn top_k(mut self, top_k: u32) -> Self {
self.request_config.top_k = Some(top_k);
self
}
/// ストップシーケンスを追加(ビルダーパターン)
pub fn stop_sequence(mut self, sequence: impl Into<String>) -> Self {
self.request_config.stop_sequences.push(sequence.into());
self
}
/// リクエスト設定をまとめて設定(ビルダーパターン)
///
/// # Examples
///
/// ```ignore
/// let config = RequestConfig::new()
/// .with_max_tokens(4096)
/// .with_temperature(0.7);
///
/// let worker = Worker::new(client)
/// .system_prompt("...")
/// .with_config(config);
/// ```
pub fn with_config(mut self, config: RequestConfig) -> Self {
self.request_config = config;
self
}
/// 現在の設定をプロバイダに対してバリデーションする
///
/// 未サポートの設定があればエラーを返す。
/// チェーンの最後で呼び出すことで、設定の問題を早期に検出できる。
///
/// # Examples
///
/// ```ignore
/// let worker = Worker::new(client)
/// .temperature(0.7)
/// .top_k(40)
/// .validate()?; // OpenAIならtop_kがサポートされないためエラー
/// ```
///
/// # Returns
/// * `Ok(Self)` - バリデーション成功
/// * `Err(WorkerError::ConfigWarnings)` - 未サポートの設定がある
pub fn validate(self) -> Result<Self, WorkerError> {
let warnings = self.client.validate_config(&self.request_config);
if warnings.is_empty() {
Ok(self)
} else {
Err(WorkerError::ConfigWarnings(warnings))
}
}
/// 履歴への可変参照を取得 /// 履歴への可変参照を取得
/// ///
/// Mutable状態でのみ利用可能。履歴を自由に編集できる。 /// Mutable状態でのみ利用可能。履歴を自由に編集できる。
@@ -701,6 +964,7 @@ impl<C: LlmClient> Worker<C, Mutable> {
locked_prefix_len, locked_prefix_len,
turn_count: self.turn_count, turn_count: self.turn_count,
turn_notifiers: self.turn_notifiers, turn_notifiers: self.turn_notifiers,
request_config: self.request_config,
_state: PhantomData, _state: PhantomData,
} }
} }
@@ -712,10 +976,9 @@ impl<C: LlmClient> Worker<C, Mutable> {
/// ///
/// 注意: この関数は履歴を変更するため、キャッシュ保護が必要な場合は /// 注意: この関数は履歴を変更するため、キャッシュ保護が必要な場合は
/// `lock()` を呼んでからLocked状態で `run` を使用すること。 /// `lock()` を呼んでからLocked状態で `run` を使用すること。
pub async fn run(&mut self, user_input: impl Into<String>) -> Result<&[Message], WorkerError> { pub async fn run(&mut self, user_input: impl Into<String>) -> Result<WorkerResult<'_>, WorkerError> {
self.history.push(Message::user(user_input)); self.history.push(Message::user(user_input));
self.run_turn_loop().await?; self.run_turn_loop().await
Ok(&self.history)
} }
/// 複数メッセージでターンを実行(Mutable状態) /// 複数メッセージでターンを実行(Mutable状態)
@@ -724,10 +987,9 @@ impl<C: LlmClient> Worker<C, Mutable> {
pub async fn run_with_messages( pub async fn run_with_messages(
&mut self, &mut self,
messages: Vec<Message>, messages: Vec<Message>,
) -> Result<&[Message], WorkerError> { ) -> Result<WorkerResult<'_>, WorkerError> {
self.history.extend(messages); self.history.extend(messages);
self.run_turn_loop().await?; self.run_turn_loop().await
Ok(&self.history)
} }
} }
@@ -740,20 +1002,18 @@ impl<C: LlmClient> Worker<C, Locked> {
/// ///
/// 新しいユーザーメッセージを履歴の末尾に追加し、LLMにリクエストを送信する。 /// 新しいユーザーメッセージを履歴の末尾に追加し、LLMにリクエストを送信する。
/// ロック時点より前の履歴(プレフィックス)は不変であるため、キャッシュヒットが保証される。 /// ロック時点より前の履歴(プレフィックス)は不変であるため、キャッシュヒットが保証される。
pub async fn run(&mut self, user_input: impl Into<String>) -> Result<&[Message], WorkerError> { pub async fn run(&mut self, user_input: impl Into<String>) -> Result<WorkerResult<'_>, WorkerError> {
self.history.push(Message::user(user_input)); self.history.push(Message::user(user_input));
self.run_turn_loop().await?; self.run_turn_loop().await
Ok(&self.history)
} }
/// 複数メッセージでターンを実行(Locked状態) /// 複数メッセージでターンを実行(Locked状態)
pub async fn run_with_messages( pub async fn run_with_messages(
&mut self, &mut self,
messages: Vec<Message>, messages: Vec<Message>,
) -> Result<&[Message], WorkerError> { ) -> Result<WorkerResult<'_>, WorkerError> {
self.history.extend(messages); self.history.extend(messages);
self.run_turn_loop().await?; self.run_turn_loop().await
Ok(&self.history)
} }
/// ロック時点のプレフィックス長を取得 /// ロック時点のプレフィックス長を取得
@@ -778,6 +1038,7 @@ impl<C: LlmClient> Worker<C, Locked> {
locked_prefix_len: 0, locked_prefix_len: 0,
turn_count: self.turn_count, turn_count: self.turn_count,
turn_notifiers: self.turn_notifiers, turn_notifiers: self.turn_notifiers,
request_config: self.request_config,
_state: PhantomData, _state: PhantomData,
} }
} }
@@ -8,9 +8,9 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait; use async_trait::async_trait;
use futures::Stream; use futures::Stream;
use worker::llm_client::{ClientError, LlmClient, Request}; use llm_worker::llm_client::event::{BlockType, DeltaContent, Event};
use worker::timeline::{Handler, TextBlockEvent, TextBlockKind, Timeline}; use llm_worker::llm_client::{ClientError, LlmClient, Request};
use worker_types::{BlockType, DeltaContent, Event}; use llm_worker::timeline::{Handler, TextBlockEvent, TextBlockKind, Timeline};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@@ -267,7 +267,8 @@ pub fn assert_timeline_integration(subdir: &str) {
}); });
for event in &events { for event in &events {
timeline.dispatch(event); let timeline_event: llm_worker::timeline::event::Event = event.clone().into();
timeline.dispatch(&timeline_event);
} }
let texts = collected.lock().unwrap(); let texts = collected.lock().unwrap();
@@ -7,11 +7,10 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use async_trait::async_trait; use async_trait::async_trait;
use worker::Worker; use llm_worker::Worker;
use worker_types::{ use llm_worker::hook::{ControlFlow, HookError, ToolCall, ToolResult, WorkerHook};
ControlFlow, Event, HookError, ResponseStatus, StatusEvent, Tool, ToolCall, ToolError, use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent};
ToolResult, WorkerHook, use llm_worker::tool::{Tool, ToolError};
};
mod common; mod common;
use common::MockLlmClient; use common::MockLlmClient;
@@ -7,12 +7,12 @@ mod common;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use common::MockLlmClient; use common::MockLlmClient;
use worker::subscriber::WorkerSubscriber; use llm_worker::Worker;
use worker::Worker; use llm_worker::hook::ToolCall;
use worker_types::{ use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent as ClientStatusEvent};
ErrorEvent, Event, ResponseStatus, StatusEvent, TextBlockEvent, ToolCall, ToolUseBlockEvent, use llm_worker::subscriber::WorkerSubscriber;
UsageEvent, use llm_worker::timeline::event::{ErrorEvent, StatusEvent, UsageEvent};
}; use llm_worker::timeline::{TextBlockEvent, ToolUseBlockEvent};
// ============================================================================= // =============================================================================
// Test Subscriber // Test Subscriber
@@ -101,7 +101,7 @@ async fn test_subscriber_text_block_events() {
Event::text_delta(0, "Hello, "), Event::text_delta(0, "Hello, "),
Event::text_delta(0, "World!"), Event::text_delta(0, "World!"),
Event::text_block_stop(0, None), Event::text_block_stop(0, None),
Event::Status(StatusEvent { Event::Status(ClientStatusEvent {
status: ResponseStatus::Completed, status: ResponseStatus::Completed,
}), }),
]; ];
@@ -141,7 +141,7 @@ async fn test_subscriber_tool_call_complete() {
Event::tool_input_delta(0, r#"{"city":"#), Event::tool_input_delta(0, r#"{"city":"#),
Event::tool_input_delta(0, r#""Tokyo"}"#), Event::tool_input_delta(0, r#""Tokyo"}"#),
Event::tool_use_stop(0), Event::tool_use_stop(0),
Event::Status(StatusEvent { Event::Status(ClientStatusEvent {
status: ResponseStatus::Completed, status: ResponseStatus::Completed,
}), }),
]; ];
@@ -172,7 +172,7 @@ async fn test_subscriber_turn_events() {
Event::text_block_start(0), Event::text_block_start(0),
Event::text_delta(0, "Done!"), Event::text_delta(0, "Done!"),
Event::text_block_stop(0, None), Event::text_block_stop(0, None),
Event::Status(StatusEvent { Event::Status(ClientStatusEvent {
status: ResponseStatus::Completed, status: ResponseStatus::Completed,
}), }),
]; ];
@@ -210,7 +210,7 @@ async fn test_subscriber_usage_events() {
Event::text_delta(0, "Hello"), Event::text_delta(0, "Hello"),
Event::text_block_stop(0, None), Event::text_block_stop(0, None),
Event::usage(100, 50), Event::usage(100, 50),
Event::Status(StatusEvent { Event::Status(ClientStatusEvent {
status: ResponseStatus::Completed, status: ResponseStatus::Completed,
}), }),
]; ];
@@ -9,8 +9,8 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use schemars; use schemars;
use serde; use serde;
use worker_macros::tool_registry; use llm_worker::tool::Tool;
use worker_types::Tool; use llm_worker_macros::tool_registry;
// ============================================================================= // =============================================================================
// Test: Basic Tool Generation // Test: Basic Tool Generation
+40
View File
@@ -0,0 +1,40 @@
use llm_worker::llm_client::LlmClient;
use llm_worker::llm_client::providers::openai::OpenAIClient;
use llm_worker::{Worker, WorkerError};
#[test]
fn test_openai_top_k_warning() {
// ダミーキーでクライアント作成(validate_configは通信しないため安全)
let client = OpenAIClient::new("dummy-key", "gpt-4o");
// top_kを設定したWorkerを作成
let worker = Worker::new(client).top_k(50); // OpenAIはtop_k非対応
// validate()を実行
let result = worker.validate();
// エラーが返り、ConfigWarningsが含まれていることを確認
match result {
Err(WorkerError::ConfigWarnings(warnings)) => {
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].option_name, "top_k");
println!("Got expected warning: {}", warnings[0]);
}
Ok(_) => panic!("Should have returned validation error"),
Err(e) => panic!("Unexpected error type: {:?}", e),
}
}
#[test]
fn test_openai_valid_config() {
let client = OpenAIClient::new("dummy-key", "gpt-4o");
// validな設定(temperatureのみ)
let worker = Worker::new(client).temperature(0.7);
// validate()を実行
let result = worker.validate();
// 成功を確認
assert!(result.is_ok());
}
@@ -11,8 +11,8 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait; use async_trait::async_trait;
use common::MockLlmClient; use common::MockLlmClient;
use worker::Worker; use llm_worker::Worker;
use worker_types::{Tool, ToolError}; use llm_worker::tool::{Tool, ToolError};
/// フィクスチャディレクトリのパス /// フィクスチャディレクトリのパス
fn fixtures_dir() -> std::path::PathBuf { fn fixtures_dir() -> std::path::PathBuf {
@@ -100,7 +100,7 @@ fn test_mock_client_from_fixture() {
/// fixtureファイルを使わず、プログラムでイベントを構築してクライアントを作成する。 /// fixtureファイルを使わず、プログラムでイベントを構築してクライアントを作成する。
#[test] #[test]
fn test_mock_client_from_events() { fn test_mock_client_from_events() {
use worker_types::Event; use llm_worker::llm_client::event::Event;
// 直接イベントを指定 // 直接イベントを指定
let events = vec![ let events = vec![
@@ -178,7 +178,7 @@ async fn test_worker_tool_call() {
/// テストの独立性を高め、外部ファイルへの依存を排除したい場合に有用。 /// テストの独立性を高め、外部ファイルへの依存を排除したい場合に有用。
#[tokio::test] #[tokio::test]
async fn test_worker_with_programmatic_events() { async fn test_worker_with_programmatic_events() {
use worker_types::{Event, ResponseStatus, StatusEvent}; use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent};
// プログラムでイベントシーケンスを構築 // プログラムでイベントシーケンスを構築
let events = vec![ let events = vec![
@@ -205,8 +205,8 @@ async fn test_worker_with_programmatic_events() {
/// id, name, inputJSON)を正しく抽出できることを検証する。 /// id, name, inputJSON)を正しく抽出できることを検証する。
#[tokio::test] #[tokio::test]
async fn test_tool_call_collector_integration() { async fn test_tool_call_collector_integration() {
use worker::timeline::{Timeline, ToolCallCollector}; use llm_worker::llm_client::event::Event;
use worker_types::Event; use llm_worker::timeline::{Timeline, ToolCallCollector};
// ToolUseブロックを含むイベントシーケンス // ToolUseブロックを含むイベントシーケンス
let events = vec![ let events = vec![
@@ -222,7 +222,8 @@ async fn test_tool_call_collector_integration() {
// イベントをディスパッチ // イベントをディスパッチ
for event in &events { for event in &events {
timeline.dispatch(event); let timeline_event: llm_worker::timeline::event::Event = event.clone().into();
timeline.dispatch(&timeline_event);
} }
// 収集されたToolCallを確認 // 収集されたToolCallを確認
@@ -6,8 +6,9 @@
mod common; mod common;
use common::MockLlmClient; use common::MockLlmClient;
use worker::Worker; use llm_worker::Worker;
use worker_types::{Event, Message, MessageContent, ResponseStatus, StatusEvent}; use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent};
use llm_worker::{Message, MessageContent};
// ============================================================================= // =============================================================================
// Mutable状態のテスト // Mutable状態のテスト
-12
View File
@@ -1,12 +0,0 @@
[package]
name = "worker-types"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
async-trait = "0.1.89"
schemars = "1.2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0.17"
-24
View File
@@ -1,24 +0,0 @@
//! worker-types - LLMワーカーの型定義
//!
//! このクレートは`worker`クレートで使用される型を提供します。
//! 通常は直接使用せず、`worker`クレート経由で利用してください。
//!
//! ```ignore
//! use worker::{Event, Message, Tool, WorkerHook};
//! ```
mod event;
mod handler;
mod hook;
mod message;
mod state;
mod subscriber;
mod tool;
pub use event::*;
pub use handler::*;
pub use hook::*;
pub use message::*;
pub use state::*;
pub use subscriber::*;
pub use tool::*;
-131
View File
@@ -1,131 +0,0 @@
//! イベント購読
//!
//! LLMからのストリーミングイベントをリアルタイムで受信するためのトレイト。
//! UIへのストリーム表示やプログレス表示に使用します。
use crate::{ErrorEvent, StatusEvent, TextBlockEvent, ToolCall, ToolUseBlockEvent, UsageEvent};
// =============================================================================
// WorkerSubscriber Trait
// =============================================================================
/// LLMからのストリーミングイベントを購読するトレイト
///
/// Workerに登録すると、テキスト生成やツール呼び出しのイベントを
/// リアルタイムで受信できます。UIへのストリーム表示に最適です。
///
/// # 受信できるイベント
///
/// - **ブロックイベント**: テキスト、ツール使用(スコープ付き)
/// - **メタイベント**: 使用量、ステータス、エラー
/// - **完了イベント**: テキスト完了、ツール呼び出し完了
/// - **ターン制御**: ターン開始、ターン終了
///
/// # Examples
///
/// ```ignore
/// use worker::{WorkerSubscriber, TextBlockEvent};
///
/// struct StreamPrinter;
///
/// impl WorkerSubscriber for StreamPrinter {
/// type TextBlockScope = ();
/// type ToolUseBlockScope = ();
///
/// fn on_text_block(&mut self, _: &mut (), event: &TextBlockEvent) {
/// if let TextBlockEvent::Delta(text) = event {
/// print!("{}", text); // リアルタイム出力
/// }
/// }
///
/// fn on_text_complete(&mut self, text: &str) {
/// println!("\n--- Complete: {} chars ---", text.len());
/// }
/// }
///
/// // Workerに登録
/// worker.subscribe(StreamPrinter);
/// ```
pub trait WorkerSubscriber: Send {
// =========================================================================
// スコープ型(ブロックイベント用)
// =========================================================================
/// テキストブロック処理用のスコープ型
///
/// ブロック開始時にDefault::default()で生成され、
/// ブロック終了時に破棄される。
type TextBlockScope: Default + Send;
/// ツール使用ブロック処理用のスコープ型
type ToolUseBlockScope: Default + Send;
// =========================================================================
// ブロックイベント(スコープ管理あり)
// =========================================================================
/// テキストブロックイベント
///
/// Start/Delta/Stopのライフサイクルを持つ。
/// scopeはブロック開始時に生成され、終了時に破棄される。
#[allow(unused_variables)]
fn on_text_block(&mut self, scope: &mut Self::TextBlockScope, event: &TextBlockEvent) {}
/// ツール使用ブロックイベント
///
/// Start/InputJsonDelta/Stopのライフサイクルを持つ。
#[allow(unused_variables)]
fn on_tool_use_block(
&mut self,
scope: &mut Self::ToolUseBlockScope,
event: &ToolUseBlockEvent,
) {
}
// =========================================================================
// 単発イベント(スコープ不要)
// =========================================================================
/// 使用量イベント
#[allow(unused_variables)]
fn on_usage(&mut self, event: &UsageEvent) {}
/// ステータスイベント
#[allow(unused_variables)]
fn on_status(&mut self, event: &StatusEvent) {}
/// エラーイベント
#[allow(unused_variables)]
fn on_error(&mut self, event: &ErrorEvent) {}
// =========================================================================
// 累積イベント(Worker層で追加)
// =========================================================================
/// テキスト完了イベント
///
/// テキストブロックが完了した時点で、累積されたテキスト全体が渡される。
/// ブロック処理後の最終結果を受け取るのに便利。
#[allow(unused_variables)]
fn on_text_complete(&mut self, text: &str) {}
/// ツール呼び出し完了イベント
///
/// ツール使用ブロックが完了した時点で、完全なToolCallが渡される。
#[allow(unused_variables)]
fn on_tool_call_complete(&mut self, call: &ToolCall) {}
// =========================================================================
// ターン制御
// =========================================================================
/// ターン開始時
///
/// `turn`は0から始まるターン番号。
#[allow(unused_variables)]
fn on_turn_start(&mut self, turn: usize) {}
/// ターン終了時
#[allow(unused_variables)]
fn on_turn_end(&mut self, turn: usize) {}
}
-24
View File
@@ -1,24 +0,0 @@
[package]
name = "worker"
version = "0.1.0"
edition = "2024"
[dependencies]
async-trait = "0.1.89"
eventsource-stream = "0.2.3"
futures = "0.3.31"
reqwest = { version = "0.13.1", features = ["stream", "json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"
worker-macros = { path = "../worker-macros" }
worker-types = { path = "../worker-types" }
[dev-dependencies]
clap = { version = "4.5.54", features = ["derive", "env"] }
schemars = "1.2.0"
tempfile = "3.24.0"
dotenv = "0.15.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-93
View File
@@ -1,93 +0,0 @@
//! worker - LLMワーカーライブラリ
//!
//! LLMとの対話を管理するコンポーネントを提供します。
//!
//! # 主要なコンポーネント
//!
//! - [`Worker`] - LLMとの対話を管理する中心コンポーネント
//! - [`tool::Tool`] - LLMから呼び出し可能なツール
//! - [`hook::WorkerHook`] - ターン進行への介入
//! - [`subscriber::WorkerSubscriber`] - ストリーミングイベントの購読
//!
//! # Quick Start
//!
//! ```ignore
//! use worker::{Worker, Message};
//!
//! // Workerを作成
//! let mut worker = Worker::new(client)
//! .system_prompt("You are a helpful assistant.");
//!
//! // ツールを登録(オプション)
//! use worker::tool::Tool;
//! worker.register_tool(my_tool);
//!
//! // 対話を実行
//! let history = worker.run("Hello!").await?;
//! ```
//!
//! # キャッシュ保護
//!
//! KVキャッシュのヒット率を最大化するには、[`Worker::lock()`]で
//! ロック状態に遷移してから実行してください。
//!
//! ```ignore
//! let mut locked = worker.lock();
//! locked.run("user input").await?;
//! ```
pub mod llm_client;
pub mod timeline;
mod subscriber_adapter;
mod worker;
// =============================================================================
// トップレベル公開(最も頻繁に使う型)
// =============================================================================
pub use worker::{Worker, WorkerConfig, WorkerError};
pub use worker_types::{ContentPart, Message, MessageContent, Role};
// =============================================================================
// 意味のあるモジュールとして公開
// =============================================================================
/// ツール定義
///
/// LLMから呼び出し可能なツールを定義するためのトレイトと型。
pub mod tool {
pub use worker_types::{Tool, ToolError};
}
/// Hook機能
///
/// ターンの進行・ツール実行に介入するためのトレイトと型。
pub mod hook {
pub use worker_types::{ControlFlow, HookError, ToolCall, ToolResult, TurnResult, WorkerHook};
}
/// イベント購読
///
/// LLMからのストリーミングイベントをリアルタイムで受信するためのトレイト。
pub mod subscriber {
pub use worker_types::WorkerSubscriber;
}
/// イベント型
///
/// LLMからのストリーミングレスポンスを表現するイベント型。
/// Timeline層を直接使用する場合に必要です。
pub mod event {
pub use worker_types::{
BlockAbort, BlockDelta, BlockMetadata, BlockStart, BlockStop, BlockType, DeltaContent,
ErrorEvent, Event, PingEvent, ResponseStatus, StatusEvent, StopReason, UsageEvent,
};
}
/// Worker状態
///
/// Type-stateパターンによるキャッシュ保護のための状態マーカー型。
pub mod state {
pub use worker_types::{Locked, Mutable, WorkerState};
}
-41
View File
@@ -1,41 +0,0 @@
//! LLMクライアント共通trait定義
use std::pin::Pin;
use async_trait::async_trait;
use futures::Stream;
use worker_types::Event;
use crate::llm_client::{ClientError, Request};
/// LLMクライアントのtrait
///
/// 各プロバイダはこのtraitを実装し、統一されたインターフェースを提供する。
#[async_trait]
pub trait LlmClient: Send + Sync {
/// ストリーミングリクエストを送信し、Eventストリームを返す
///
/// # Arguments
/// * `request` - リクエスト情報
///
/// # Returns
/// * `Ok(Stream)` - イベントストリーム
/// * `Err(ClientError)` - エラー
async fn stream(
&self,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<Event, ClientError>> + Send>>, ClientError>;
}
/// `Box<dyn LlmClient>` に対する `LlmClient` の実装
///
/// これにより、動的ディスパッチを使用するクライアントも `Worker` で利用可能になる。
#[async_trait]
impl LlmClient for Box<dyn LlmClient> {
async fn stream(
&self,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<Event, ClientError>> + Send>>, ClientError> {
(**self).stream(request).await
}
}