14 KiB
Worker 設計
概要
Workerはアプリケーションの「ターン」を制御する高レベルコンポーネントです。
LlmClientとTimelineを内包し、ユーザー定義のToolとHookを用いて自律的なインタラクションを行います。
Type-stateパターンにより、Mutable(編集可能)とCacheLocked(キャッシュ保護)の2つの状態を持ちます。
アーキテクチャ
graph TD
User[Application / User] -->|1. Run| Worker
Worker -->|2. Event Loop| Timeline
Timeline -->|3. Dispatch| Handler[Handlers (TextBlockCollector, ToolCallCollector)]
subgraph "Worker Layer"
Worker
Hook[HookRegistry]
ToolServer[ToolServerHandle]
end
subgraph "Core Layer"
Timeline
LlmClient
end
Worker -.->|Intervene| Hook
ToolServer -.->|Execute| Tool[User Defined Tools]
Worker -.->|Subscribe| Subscriber[WorkerSubscriber]
Worker 構造体
pub struct Worker<C: LlmClient, S: WorkerState = Mutable> {
client: C,
timeline: Timeline,
text_block_collector: TextBlockCollector,
tool_call_collector: ToolCallCollector,
tool_server: ToolServerHandle,
hooks: HookRegistry,
system_prompt: Option<String>,
history: Vec<Item>,
locked_prefix_len: usize,
turn_count: usize,
turn_notifiers: Vec<Box<dyn TurnNotifier>>,
request_config: RequestConfig,
last_run_interrupted: bool,
cancel_tx: mpsc::Sender<()>,
cancel_rx: mpsc::Receiver<()>,
_state: PhantomData<S>,
}
状態遷移 (Type-state)
| 状態 | 説明 | 利用可能な操作 |
|---|---|---|
Mutable |
初期状態。自由に編集可能 | system_prompt設定、history編集、tool/hook登録、lock() |
CacheLocked |
キャッシュ保護状態 | historyへの追記のみ、unlock() |
// Mutable → CacheLocked
let locked = worker.lock();
// CacheLocked → Mutable
let mutable = locked.unlock();
CacheLocked状態ではLLM APIのKVキャッシュヒットを確保するため、
システムプロンプトと既存の履歴(ロック時点のプレフィックス)は不変となります。
ライフサイクル (ターン制御)
Workerは以下のループ(ターン)を実行します。
- Start Turn:
Worker::run(user_input)呼び出し - Hook: OnPromptSubmit:
- ユーザーメッセージ(
Item)の改変、バリデーション、キャンセルが可能。 Cancelを返すとWorkerError::Abortedで終了。
- ユーザーメッセージ(
- Resume Check:
- 未応答のToolCall(Pauseから復帰した場合等)があれば、先にツール実行を行う。
- Turn Loop:
- Hook: PreLlmRequest:
- LLMリクエスト送信前にコンテキスト(
Vec<Item>)を改変可能。 Cancelを返すとターン中断。
- LLMリクエスト送信前にコンテキスト(
- Request & Stream:
- LLMへリクエスト送信。イベントストリーム開始。
Timelineによるイベント処理(TextBlockCollector,ToolCallCollector)。- ストリーム中に Hook: OnStreamChunk, OnTextDelta, OnToolCallDelta を実行。
- ストリーム完了時に Hook: OnStreamComplete を実行。
- Tool Handling (Parallel):
- レスポンス内に含まれる全てのTool Callを収集。
- 各Toolに対して Hook: PreToolCall を実行(実行可否、引数改変、Pause)。
- 許可されたToolを並列実行 (
join_all)(ToolServerHandle経由)。 - 各Tool実行後に Hook: PostToolCall を実行(結果の確認、加工)。
- Next Request Decision:
- Tool実行結果がある場合 -> 結果を
Item::FunctionCallOutputとしてhistoryに追加し、Step 4.1へ戻る(自動ループ)。 - Tool実行がない場合 -> Step 5へ。
- Tool実行結果がある場合 -> 結果を
- Hook: PreLlmRequest:
- Hook: OnTurnEnd:
- 最終的な応答に対するチェック(Lint/Fmtなど)。
ContinueWithMessages(items): 追加メッセージをhistoryに追加してStep 4.1へ戻ることで自己修正を促せる。Paused:WorkerResult::Pausedを返し、後でresume()で再開可能。Finish: ターン正常終了。
キャンセル機構
cancel_tx / cancel_rx チャネルによる非同期キャンセルをサポートします。
// キャンセルトリガーの取得
let sender = worker.cancel_sender();
// 別タスクからキャンセル
sender.try_send(()).ok();
// または直接
worker.cancel();
キャンセルはストリーム開始前、ストリーム受信中、ツール実行中のいずれのタイミングでも
tokio::select! により検出され、WorkerError::Cancelled を返します。
実行結果
pub enum WorkerResult {
/// 完了(ユーザー入力待ち)
Finished,
/// 一時停止(resume()で再開可能)
Paused,
}
resume() メソッドにより、Paused状態からユーザーメッセージを追加せずにターン処理を再開できます。
Hook 設計
Hook Trait
#[async_trait]
pub trait Hook<E: HookEventKind>: Send + Sync {
async fn call(&self, input: &mut E::Input) -> Result<E::Output, HookError>;
}
pub trait HookEventKind: Send + Sync + 'static {
type Input;
type Output;
}
Hook イベント一覧
| Hook | Input | Output | タイミング |
|---|---|---|---|
OnPromptSubmit |
Item |
OnPromptSubmitResult |
run() 直後、ユーザーメッセージ受信時 |
PreLlmRequest |
Vec<Item> |
PreLlmRequestResult |
各ターンのLLMリクエスト送信前 |
PreToolCall |
ToolCallContext |
PreToolCallResult |
各ツール実行前 |
PostToolCall |
PostToolCallContext |
PostToolCallResult |
各ツール実行後 |
OnTurnEnd |
Vec<Item> |
OnTurnEndResult |
ツール呼び出しなしでターン終了時 |
OnAbort |
String |
() |
エラーまたはキャンセルで中断時 |
OnTextDelta |
TextDeltaContext |
StreamHookResult |
テキストデルタ受信時 |
OnToolCallDelta |
ToolCallDeltaContext |
StreamHookResult |
ツール呼び出しJSON断片受信時 |
OnStreamChunk |
StreamChunkContext |
StreamHookResult |
ストリームイベント受信時 |
OnStreamComplete |
StreamCompleteContext |
StreamHookResult |
ストリーム完了時 |
Hook 結果型
pub enum OnPromptSubmitResult {
Continue,
Cancel(String),
}
pub enum PreLlmRequestResult {
Continue,
Cancel(String),
}
pub enum PreToolCallResult {
Continue,
Skip, // ツール実行をスキップ
Abort(String), // 処理中断
Pause, // 一時停止(resume()で再開)
}
pub enum PostToolCallResult {
Continue,
Abort(String),
}
pub enum OnTurnEndResult {
Finish,
ContinueWithMessages(Vec<Item>), // メッセージを追加してターン継続
Paused,
}
pub enum StreamHookResult {
Continue,
Abort(String),
Pause,
}
Tool Call Context
PreToolCall / PostToolCall は、ツール実行の文脈を含むコンテキストを受け取ります。
pub struct ToolCallContext {
pub call: ToolCall, // 呼び出し情報(改変可能)
pub meta: ToolMeta, // メタ情報(読み取り専用)
pub tool: Arc<dyn Tool>, // インスタンス(状態アクセス用)
}
pub struct PostToolCallContext {
pub call: ToolCall, // 呼び出し情報
pub result: ToolResult, // 実行結果(改変可能)
pub meta: ToolMeta, // メタ情報(読み取り専用)
pub tool: Arc<dyn Tool>, // インスタンス(状態アクセス用)
}
ストリーミングHookのコンテキスト
pub struct TextDeltaContext {
pub index: usize,
pub delta: String,
}
pub struct ToolCallDeltaContext {
pub index: usize,
pub delta_json_fragment: String,
}
pub struct StreamChunkContext {
pub event: crate::event::Event,
}
pub struct StreamCompleteContext {
pub turn: usize,
pub event_count: usize,
}
Hook 登録API
worker.add_on_prompt_submit_hook(my_hook);
worker.add_pre_llm_request_hook(my_hook);
worker.add_pre_tool_call_hook(my_hook);
worker.add_post_tool_call_hook(my_hook);
worker.add_on_turn_end_hook(my_hook);
worker.add_on_abort_hook(my_hook);
worker.add_on_text_delta_hook(my_hook);
worker.add_on_tool_call_delta_hook(my_hook);
worker.add_on_stream_chunk_hook(my_hook);
worker.add_on_stream_complete_hook(my_hook);
Worker Event API (Subscriber)
背景と目的
Workerは内部でイベントを処理し結果を返しますが、UIへのストリーミング表示やリアルタイムフィードバックには、イベントを外部に公開する仕組みが必要です。
WorkerSubscriber Trait
WorkerSubscriberトレイトを実装し、worker.subscribe() で一括登録します。
pub trait WorkerSubscriber: Send {
// スコープ型(ブロックイベント用)
type TextBlockScope: Default + Send + Sync;
type ToolUseBlockScope: Default + Send + Sync;
// === ブロックイベント(スコープ管理あり)===
fn on_text_block(
&mut self,
scope: &mut Self::TextBlockScope,
event: &TextBlockEvent,
) {}
fn on_tool_use_block(
&mut self,
scope: &mut Self::ToolUseBlockScope,
event: &ToolUseBlockEvent,
) {}
// === 単発イベント ===
fn on_usage(&mut self, event: &UsageEvent) {}
fn on_status(&mut self, event: &StatusEvent) {}
fn on_error(&mut self, event: &ErrorEvent) {}
// === 累積イベント(Worker層で追加)===
fn on_text_complete(&mut self, text: &str) {}
fn on_tool_call_complete(&mut self, call: &ToolCall) {}
// === ターン制御 ===
fn on_turn_start(&mut self, turn: usize) {}
fn on_turn_end(&mut self, turn: usize) {}
}
内部実装
SubscriberはWorker内部でAdapter群に分解され、各KindのHandlerとしてTimelineに登録されます。
累積イベント(on_text_complete, on_tool_call_complete)はAdapter内でブロック終了時にバッファから合成されます。
TextBlockSubscriberAdapter: テキストデルタを蓄積し、Stop時にon_text_completeを呼ぶToolUseBlockSubscriberAdapter: ID・名前・JSONを蓄積し、Stop時にon_tool_call_completeを呼ぶUsageSubscriberAdapter,StatusSubscriberAdapter,ErrorSubscriberAdapter: 単純な転送SubscriberTurnNotifier: ターン開始・終了の通知
使用例
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.subscribe(StreamPrinter);
let result = worker.run("Hello!").await?;
Worker API
生成と設定 (Mutable状態のみ)
// 生成(ビルダーパターン)
let worker = Worker::new(client)
.system_prompt("You are a helpful assistant.")
.max_tokens(4096)
.temperature(0.7)
.top_p(0.9)
.top_k(40)
.stop_sequence("\n\n")
.with_config(request_config)
.with_item(item)
.with_items(items);
// バリデーション(プロバイダの対応確認)
let worker = worker.validate()?;
// ミューテーション
worker.set_system_prompt("...");
worker.set_max_tokens(4096);
worker.set_temperature(0.7);
worker.set_top_p(0.9);
worker.set_top_k(40);
worker.add_stop_sequence("\n\n");
worker.clear_stop_sequences();
worker.set_request_config(config);
// 履歴操作
worker.push_item(item);
worker.extend_history(items);
worker.set_history(items);
worker.clear_history();
worker.history_mut(); // &mut Vec<Item>
// ツール登録
worker.register_tool(factory)?;
worker.register_tools(factories)?;
全状態で利用可能
// 実行
let result = worker.run("user input").await?;
let result = worker.resume().await?;
// キャンセル
worker.cancel();
let sender = worker.cancel_sender();
// 参照
worker.history(); // &[Item]
worker.get_system_prompt(); // Option<&str>
worker.turn_count(); // usize
worker.request_config(); // &RequestConfig
worker.last_run_interrupted(); // bool
worker.is_cancelled(); // bool
// ツールサーバー
worker.tool_server_handle(); // ToolServerHandle
// Timeline直接アクセス
worker.timeline_mut(); // &mut Timeline
// イベント購読
worker.subscribe(my_subscriber);
// Hook登録
worker.add_on_prompt_submit_hook(hook);
worker.add_pre_llm_request_hook(hook);
worker.add_pre_tool_call_hook(hook);
worker.add_post_tool_call_hook(hook);
worker.add_on_turn_end_hook(hook);
worker.add_on_abort_hook(hook);
worker.add_on_text_delta_hook(hook);
worker.add_on_tool_call_delta_hook(hook);
worker.add_on_stream_chunk_hook(hook);
worker.add_on_stream_complete_hook(hook);
エラー型
pub enum WorkerError {
Client(ClientError),
Tool(ToolError),
Hook(HookError),
Aborted(String),
Cancelled,
ConfigWarnings(Vec<ConfigWarning>),
}
pub enum ToolRegistryError {
DuplicateName(String),
}
公開エクスポート
lib.rs から以下がre-exportされています。
pub use message::{ContentPart, Item, Message, Role};
pub use worker::{ToolRegistryError, Worker, WorkerConfig, WorkerError, WorkerResult};
モジュール:
pub mod eventpub mod hookpub mod llm_clientpub mod statepub mod subscriberpub mod timelinepub mod toolpub mod tool_server