From d03172610ddf250a4a0ccb56882957309bdf6d09 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 30 Aug 2025 04:22:42 +0900 Subject: [PATCH] add: docs --- CLAUDE.md | 55 +++ docs/architecture.md | 50 +++ docs/hooks.md | 718 ++++++++++++++++++++++++++++++++++++++++ docs/prompt-composer.md | 71 ++++ docs/worker-macro.md | 115 +++++++ docs/worker.md | 185 +++++++++++ 6 files changed, 1194 insertions(+) create mode 100644 CLAUDE.md create mode 100644 docs/architecture.md create mode 100644 docs/hooks.md create mode 100644 docs/prompt-composer.md create mode 100644 docs/worker-macro.md create mode 100644 docs/worker.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7f45d64 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with code in this repository. + +## Development Commands + +- **Build**: `cargo build --workspace` or `cargo check --workspace` for quick validation +- **Tests**: `cargo test --workspace` (note: tests currently fail due to unsafe environment variable operations) +- **Fix warnings**: `cargo fix --lib -p worker` +- **Dev environment**: Uses Nix flake - run `nix develop` to enter dev shell with Rust toolchain + +## Architecture Overview + +This is a Rust workspace implementing a multi-LLM worker system with tool calling capabilities. The architecture follows the Core Crate Pattern to avoid circular dependencies: + +### Workspace Structure +- **`worker-types`**: Core type definitions (Tool trait, Message, StreamEvent, hook types) - foundational types used by all other crates +- **`worker-macros`**: Procedural macros (`#[tool]`, `#[hook]`) for automatic Tool and WorkerHook trait implementations +- **`worker`**: Main library containing Worker struct, LLM clients, prompt composer, and session management + +### Key Components + +**Worker**: Central orchestrator that manages LLM interactions, tool execution, and session state. Supports streaming responses via async streams. + +**LLM Providers**: Modular clients in `worker/src/llm/` for: +- Anthropic (Claude) +- Google (Gemini) +- OpenAI (GPT) +- xAI (Grok) +- Ollama (local models) + +**Tool System**: Uses `#[tool]` macro to convert async functions into LLM-callable tools. Tools must return `ToolResult` and take a single struct argument implementing `Deserialize + Serialize + JsonSchema`. + +**Hook System**: Uses `#[hook]` macro to create lifecycle event handlers. Hook functions take `HookContext` and return `(HookContext, HookResult)` tuple, supporting events like OnMessageSend, PreToolUse, PostToolUse, and OnTurnCompleted with regex matcher support. + +**Prompt Management**: Handlebars-based templating system for dynamic prompt generation based on roles and context. + +**MCP Integration**: Model Context Protocol support for external tool server communication. + +## Development Notes + +- Edition 2024 Rust with async/await throughout +- Uses `tokio` for async runtime and `reqwest` for HTTP clients +- Environment-based configuration for API keys and base URLs +- Session persistence using JSON serialization +- Streaming responses via `futures-util` streams +- Current test suite has unsafe environment variable operations that need `unsafe` blocks to compile + +## Important Patterns + +- **Error Handling**: `ToolResult` for tools, `WorkerError` for library operations with automatic conversions +- **Streaming**: All LLM interactions return `StreamEvent` streams for real-time UI updates +- **Tool Registration**: Dynamic tool registration at runtime using boxed trait objects +- **Hook Registration**: Dynamic hook registration with lifecycle event filtering and regex matching +- **Core Crate Pattern**: `worker-macros` references `worker-types` directly via complete paths to prevent circular dependencies \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..697f174 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,50 @@ +## 各クレートの役割 + +- **`worker-types`**: 全ての基本型定義(Tool, Message, StreamEvent, フック関連型等) +- **`worker-macros`**: プロシージャルマクロ(#[tool])による自動実装 +- **`worker`**: メインライブラリ(Worker, LLMクライアント, プロンプトコンポーザー等) +- **`tools`**: 具体的なツール実装(read, write, bash, mcp等) + +`worker`クレートは`worker-types`の全型を再エクスポートし、外部からは`worker::types::`でアクセス可能です。 + +## エラーハンドリング戦略 + +- **`worker-types`**: `ToolResult` - 汎用的なツールエラー(`Result>`) +- **`worker`**: `WorkerError` - ライブラリ固有エラー(設定、ネットワーク、ツール実行、JSON解析等) + +自動的な型変換(`From` trait実装)により、一貫したエラーハンドリングを実現しています。認証エラーの自動検出機能も含まれています。 + +## マクロ実装 + +`worker-macros`は完全修飾パスで型を参照し、Core Crate Patternに準拠: + +```rust +// #[tool]マクロが生成するコード例 +impl ::worker_types::Tool for ReadFileTool { + fn name(&self) -> &str { "read_file" } + fn description(&self) -> &str { "ファイルを読み込みます" } + fn parameters_schema(&self) -> ::worker_types::serde_json::Value { /* スキーマ */ } + + #[::worker_types::async_trait::async_trait] + async fn execute(&self, args: ::worker_types::serde_json::Value) + -> ::worker_types::ToolResult<::worker_types::serde_json::Value> { + // 元の関数呼び出し + } +} +``` + +## 利用ガイドライン + +- **新しいツール**: `#[tool]`マクロでasync関数を装飾し、`worker::types::ToolResult`を返り値型に使用 +- **新しいLLMプロバイダ**: `worker/src/llm/`にクライアント実装を追加し、`LlmClient` enumに追加 +- **新しいフック**: `WorkerHook` traitを実装し、`HookManager`で管理 +- **型の変更**: `worker-types`で基本型を変更し、コンパイルエラーで影響範囲を確認 +- **MCP統合**: `mcp_tool`モジュールを使用してModel Context Protocolサーバーとの連携を実装 + +## 主要な設計パターン + +- **委譲パターン**: `LlmClient` enumでの各プロバイダクライアントへの委譲 +- **ストリーミング**: async streamを使用したリアルタイムイベント処理 +- **プロンプトテンプレート**: Handlebarsベースの動的プロンプト生成 +- **セッション管理**: メッセージ履歴の永続化と復元 +- **並列処理**: MCPサーバーの並列初期化による高速化 \ No newline at end of file diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 0000000..5b68a67 --- /dev/null +++ b/docs/hooks.md @@ -0,0 +1,718 @@ +# Worker Hook システム + +Workerのフックシステムは、LLMとの対話プロセスの各段階でカスタムロジックを実行し、システムの動作を柔軟に制御できる強力な機能です。 + +## 概要 + +Hooksシステムは、WorkerがLLMとの対話で実行する各フェーズ(メッセージ処理、ツール実行、ターン完了)でユーザー定義の処理を挿入できる機能です。これにより、以下のような高度なカスタマイズが可能になります: + +- **メッセージの前処理**: ユーザーメッセージにコンテキスト情報を追加 +- **ツール実行後の処理**: ファイル操作後の自動コミットやフォーマット +- **ターン完了時の処理**: 統計記録や追加情報の提供 +- **ストリーミング制御**: リアルタイム応答の変更・拡張 + +## アーキテクチャ + +### コア型 + +```rust +// worker-types/src/lib.rs +pub trait WorkerHook: Send + Sync { + fn name(&self) -> &str; + fn hook_type(&self) -> &str; + fn matcher(&self) -> &str; + async fn execute(&self, context: HookContext) -> (HookContext, HookResult); +} + +pub struct HookManager { + hooks: HashMap>>, +} + +pub enum HookEvent { + OnMessageSend, + PreToolUse, + PostToolUse, + OnTurnCompleted, +} +``` + +### Hook実行フロー + +``` +1. ユーザーメッセージ受信 +2. OnMessageSend hooks 実行 +3. プロンプト構築・LLMへ送信 +4. ストリーミング応答開始 +5. ツール呼び出し検出 + ├── PreToolUse hooks 実行 + ├── ツール実行 + └── PostToolUse hooks 実行 +6. 応答完了 +7. OnTurnCompleted hooks 実行 +8. 結果返却 +``` + +## Hook の種類 + +### フェーズ別Hook + +| フェーズ | タイプ名 | 実行タイミング | ストリーミング | +|---------|----------|---------------|---------------| +| メッセージ送信時 | `OnMessageSend` | ユーザーメッセージをLLMに送信する直前 | × | +| ツール実行前 | `PreToolUse` | 特定のツール実行直前 | ○ | +| ツール実行後 | `PostToolUse` | 特定のツール実行完了後 | ○ | +| ターン完了時 | `OnTurnCompleted` | AIの応答が完了した時点 | ○ | + +### マッチャーパターン + +`PreToolUse`、`PostToolUse`および`OnTurnCompleted`では、`matcher`パラメータで特定のツールに対してのみHookを実行できます: + +```rust +// Edit または Create ツールの実行後のみ実行 +#[hook(hook_type = "PostToolUse", matcher = "Edit|Create")] + +// すべてのツールで実行(matcher省略時) +#[hook(hook_type = "OnTurnCompleted")] + +// 正規表現マッチング +#[hook(hook_type = "PostToolUse", matcher = r"^(Read|Write)File.*")] +``` + +## Hook の作成方法 + +### 基本構文 + +```rust +use worker::types::{HookContext, HookResult, Role}; +use worker_macros::hook; + +#[hook(hook_type = "フェーズ名", matcher = "パターン")] +pub async fn your_hook_name(mut context: HookContext) -> HookResult { + // カスタムロジックをここに実装 + HookResult::Continue +} +``` + +### マクロを使わない実装 + +```rust +use worker::types::{WorkerHook, HookContext, HookResult}; +use async_trait::async_trait; + +pub struct CustomHook { + name: String, +} + +impl CustomHook { + pub fn new(name: String) -> Self { + Self { name } + } +} + +#[async_trait] +impl WorkerHook for CustomHook { + fn name(&self) -> &str { + &self.name + } + + fn hook_type(&self) -> &str { + "OnMessageSend" + } + + fn matcher(&self) -> &str { + "" + } + + async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { + // Hook処理をここに実装 + (context, HookResult::Continue) + } +} +``` + +## HookContext API + +`HookContext`は、Hook内で使用できる情報とメソッドを提供します: + +### 利用可能なデータ + +```rust +pub struct HookContext { + pub content: String, // 処理対象のコンテンツ + pub workspace_path: String, // 現在のワークスペースパス + pub message_history: Vec, // これまでの会話履歴 + pub tools: Vec, // 利用可能なツール一覧 + pub variables: HashMap, // Hook間で共有可能な変数 + pub tool_name: Option, // 実行中のツール名(ツール関連フックのみ) + pub tool_args: Option, // ツール実行引数(ツール関連フックのみ) + pub tool_result: Option, // ツール実行結果(PostToolUseのみ) +} +``` + +### 操作メソッド + +```rust +impl HookContext { + // ワークスペースでコマンドを実行 + pub async fn run_command(&self, command: &str) -> Result>; + + // ツールを実行(将来実装予定) + pub async fn run_tool(&self, tool_name: &str, args: serde_json::Value) -> Result>; + + // メッセージを履歴に追加 + pub fn add_message(&mut self, content: String, role: Role); + + // コンテンツを書き換え + pub fn set_content(&mut self, content: String); + + // 変数の設定・取得 + pub fn set_variable(&mut self, key: String, value: String); + pub fn get_variable(&self, key: &str) -> Option<&String>; + + // ツール情報の取得 + pub fn get_tool(&self, tool_name: &str) -> Option<&DynamicToolDefinition>; + pub fn list_tool_names(&self) -> Vec<&String>; +} +``` + +### ストリーミング用メソッド + +```rust +impl HookContext { + // ストリーミング中にメッセージを送信 + pub fn stream_message(&self, content: String, role: Role); + + // ストリーミング中にシステム通知を送信 + pub fn stream_system_message(&self, content: String); + + // ストリーミング中にデバッグ情報を送信 + pub fn stream_debug(&self, title: String, data: serde_json::Value); +} +``` + +## HookResult の種類 + +Hook関数は以下のいずれかの結果を返す必要があります: + +```rust +pub enum HookResult { + // 処理を続行 + Continue, + + // コンテンツを変更して続行 + ModifyContent(String), + + // システムメッセージを追加して続行 + AddMessage(String, Role), + + // 複数のメッセージを追加して続行 + AddMessages(Vec), + + // ターンを強制完了 + Complete, + + // エラーでターンを終了 + Error(String), + + // Hook処理をスキップ(デバッグ用) + Skip, +} +``` + +## 実用例 + +### 1. タイムスタンプ付きメッセージ + +```rust +/// メッセージ送信時に現在時刻を追加するHook +#[hook(hook_type = "OnMessageSend")] +pub async fn add_timestamp_hook(mut context: HookContext) -> HookResult { + let timestamp = chrono::Local::now().format("%H:%M:%S").to_string(); + let enhanced_content = format!("[{}] {}", timestamp, context.content); + HookResult::ModifyContent(enhanced_content) +} +``` + +### 2. ファイル操作後の自動Git追跡 + +```rust +/// ファイル編集後にGitステータスを確認するHook +#[hook(hook_type = "PostToolUse", matcher = "Edit|Create|Write")] +pub async fn file_change_notification_hook(mut context: HookContext) -> HookResult { + match context.run_command("git status --porcelain").await { + Ok(output) => { + if !output.trim().is_empty() { + context.add_message( + format!("📝 ファイル変更を検出:\n{}", output), + Role::System, + ); + HookResult::Continue + } else { + HookResult::Continue + } + } + Err(_) => HookResult::Continue, + } +} +``` + +### 3. 長い応答への注意喚起 + +```rust +/// 長い応答が生成された際に注意を促すHook +#[hook(hook_type = "OnTurnCompleted")] +pub async fn long_response_warning_hook(context: HookContext) -> HookResult { + if context.content.len() > 2000 { + HookResult::AddMessage( + "⚠️ 長い応答が生成されました。スクロールして全体を確認してください。".to_string(), + Role::System, + ) + } else { + HookResult::Continue + } +} +``` + +### 4. ツール実行前のバリデーション + +```rust +/// 危険なコマンドの実行前に確認を行うHook +#[hook(hook_type = "PreToolUse", matcher = "Execute|Shell")] +pub async fn dangerous_command_hook(context: HookContext) -> HookResult { + if let Some(args) = &context.tool_args { + if let Some(command) = args.get("command").and_then(|v| v.as_str()) { + let dangerous_commands = ["rm -rf", "format", "dd if="]; + + for dangerous in &dangerous_commands { + if command.contains(dangerous) { + return HookResult::Error(format!( + "危険なコマンド「{}」の実行が阻止されました: {}", + dangerous, command + )); + } + } + } + } + + HookResult::Continue +} +``` + +### 5. 自動読み取りHook (TUI用) + +```rust +/// ファイル作成・編集後に自動的にファイル内容を読み取るHook +#[hook(hook_type = "PostToolUse", matcher = "Edit|Create|Write")] +pub async fn auto_read_hook(mut context: HookContext) -> HookResult { + // ツール実行結果からファイルパスを抽出 + if let Some(tool_result) = &context.tool_result { + if let Ok(result_data) = serde_json::from_str::(tool_result) { + if let Some(file_path) = result_data.get("file_path").and_then(|v| v.as_str()) { + // ファイル内容を読み取って表示 + match tokio::fs::read_to_string(file_path).await { + Ok(content) => { + context.stream_system_message(format!( + "📄 **{}** の内容:\n```\n{}\n```", + file_path, content + )); + } + Err(e) => { + context.stream_system_message(format!( + "❌ ファイル読み取りエラー ({}): {}", + file_path, e + )); + } + } + } + } + } + + HookResult::Continue +} +``` + +## Hook の登録と管理 + +### Workerへの登録 + +```rust +use worker::Worker; + +// 単一のHookを登録 +worker.register_hook(Box::new(YourHook::new())); + +// 複数のHookを一括登録 +let hooks = vec![ + Box::new(TimestampHook::new()), + Box::new(FileNotificationHook::new()), + Box::new(DebugHook::new()), +]; +worker.register_hooks(hooks); + +// TUI用デフォルトHookの登録 +let tui_hooks = crate::tui::hooks::get_default_hooks(); +worker.register_hooks(tui_hooks); +``` + +### Hook の実行順序 + +同じフェーズで複数のHookが登録されている場合、登録順に実行されます。先に実行されたHookの結果(コンテキストの変更など)は、後続のHookに引き継がれます。 + +```rust +// 実行順序の例 +worker.register_hook(Box::new(TimestampHook)); // 1番目 +worker.register_hook(Box::new(ValidationHook)); // 2番目 +worker.register_hook(Box::new(LoggingHook)); // 3番目 +``` + +### Hook の中断 + +`HookResult::Complete`または`HookResult::Error`を返すHookがあると、それ以降のHookは実行されず、処理が中断されます。 + +### Hook の動的管理 + +```rust +impl Worker { + // Hook一覧を取得 + pub fn list_hooks(&self) -> Vec<(&str, &str)>; // (name, hook_type) + + // 特定のHookを削除 + pub fn remove_hook(&mut self, hook_name: &str) -> bool; + + // フェーズ別Hookを削除 + pub fn remove_hooks_by_phase(&mut self, hook_type: &str); + + // すべてのHookをクリア + pub fn clear_hooks(&mut self); +} +``` + +## ストリーミング処理 + +### ストリーミング中のHook実行 + +```rust +// worker/src/lib.rs の process_with_shared_state より +stream! { + // ... LLM応答処理中 ... + + // ツール呼び出し検出時 + if let Some(tool_calls) = &response.tool_calls { + for tool_call in tool_calls { + // PreToolUse hooks 実行 + let (context, hook_result) = execute_hooks( + HookEvent::PreToolUse, + tool_call.name.clone() + ).await; + + match hook_result { + HookResult::Error(msg) => { + yield Ok(StreamEvent::Error(msg)); + continue; + } + HookResult::Complete => break, + _ => {} + } + + // ツール実行 + let result = execute_tool(tool_call).await; + + // PostToolUse hooks 実行(ストリーミング中) + let (context, hook_result) = execute_hooks( + HookEvent::PostToolUse, + tool_call.name.clone() + ).await; + + // Hook結果を即座にストリーミング + if let HookResult::AddMessage(msg, role) = hook_result { + yield Ok(StreamEvent::HookMessage { + hook_name: "PostToolUse".to_string(), + content: msg, + role, + }); + } + } + } +} +``` + +## ベストプラクティス + +### 1. エラーハンドリング + +```rust +#[hook(hook_type = "OnTurnCompleted")] +pub async fn robust_hook(mut context: HookContext) -> HookResult { + match context.run_command("potentially_failing_command").await { + Ok(output) => { + // 成功時の処理 + context.add_message(format!("実行完了: {}", output), Role::System); + HookResult::Continue + } + Err(error) => { + // エラーログを記録するが、処理は継続 + tracing::warn!("Hook実行時にエラー: {}", error); + HookResult::Continue + } + } +} +``` + +### 2. パフォーマンス配慮 + +```rust +#[hook(hook_type = "OnTurnCompleted")] +pub async fn performance_aware_hook(context: HookContext) -> HookResult { + // 重い処理は条件分岐で制限 + if context.content.len() > 10000 { + // 大きなコンテンツの場合はスキップ + return HookResult::Skip; + } + + // 非同期処理は適切にawaitする + let result = tokio::time::timeout( + Duration::from_secs(5), + expensive_operation(&context) + ).await; + + match result { + Ok(output) => HookResult::AddMessage(output, Role::System), + Err(_) => { + tracing::warn!("Hook処理がタイムアウトしました"); + HookResult::Continue + } + } +} +``` + +### 3. 設定可能なHook + +```rust +#[hook(hook_type = "OnMessageSend")] +pub async fn configurable_hook(mut context: HookContext) -> HookResult { + // 環境変数で動作を制御 + let enabled = std::env::var("HOOK_ENABLED") + .unwrap_or_default() + .parse::() + .unwrap_or(false); + + if !enabled { + return HookResult::Skip; + } + + // 設定ファイルからオプション読み込み + let config_path = format!("{}/.nia/hook_config.json", context.workspace_path); + if let Ok(config_content) = tokio::fs::read_to_string(&config_path).await { + if let Ok(config) = serde_json::from_str::(&config_content) { + // 設定に基づく処理 + return process_with_config(&mut context, &config).await; + } + } + + HookResult::Continue +} +``` + +### 4. 条件付きHook + +```rust +#[hook(hook_type = "PostToolUse", matcher = ".*")] +pub async fn conditional_hook(context: HookContext) -> HookResult { + // ワークスペースの種類に応じて処理を変更 + let is_git_repo = context.run_command("git status").await.is_ok(); + let is_rust_project = tokio::fs::metadata( + format!("{}/Cargo.toml", context.workspace_path) + ).await.is_ok(); + + match (is_git_repo, is_rust_project) { + (true, true) => { + // Rustプロジェクト + Git + context.run_command("cargo fmt").await.ok(); + HookResult::AddMessage("Rustコードをフォーマットしました".to_string(), Role::System) + } + (true, false) => { + // その他のGitプロジェクト + HookResult::AddMessage("Gitプロジェクトで作業中です".to_string(), Role::System) + } + _ => HookResult::Continue + } +} +``` + +## デバッグとテスト + +### Hook のテスト + +```rust +#[cfg(test)] +mod tests { + use super::*; + use worker::types::*; + + #[tokio::test] + async fn test_timestamp_hook() { + let mut context = HookContext { + content: "Hello, world!".to_string(), + workspace_path: "/tmp".to_string(), + message_history: vec![], + tools: vec![], + variables: HashMap::new(), + tool_name: None, + tool_args: None, + tool_result: None, + }; + + let result = add_timestamp_hook(context).await; + + match result { + HookResult::ModifyContent(content) => { + assert!(content.contains("Hello, world!")); + assert!(content.contains("[")); + assert!(content.contains("]")); + } + _ => panic!("Expected ModifyContent"), + } + } +} +``` + +### Hook のデバッグ + +```rust +#[hook(hook_type = "OnMessageSend")] +pub async fn debug_hook(context: HookContext) -> HookResult { + tracing::debug!( + "Hook実行 - コンテンツ長: {}, ツール数: {}, 履歴数: {}", + context.content.len(), + context.tools.len(), + context.message_history.len() + ); + + // デバッグ情報をストリーミング + context.stream_debug( + "Hook Debug Info".to_string(), + serde_json::json!({ + "content_length": context.content.len(), + "tool_count": context.tools.len(), + "history_count": context.message_history.len(), + "workspace": context.workspace_path + }) + ); + + HookResult::Continue +} +``` + +## 高度なHookパターン + +### 状態を持つHook + +```rust +pub struct StatefulHook { + counter: Arc>, +} + +impl StatefulHook { + pub fn new() -> Self { + Self { + counter: Arc::new(Mutex::new(0)), + } + } +} + +#[async_trait] +impl WorkerHook for StatefulHook { + fn name(&self) -> &str { "stateful_hook" } + fn hook_type(&self) -> &str { "OnTurnCompleted" } + fn matcher(&self) -> &str { "" } + + async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { + let mut count = self.counter.lock().unwrap(); + *count += 1; + + context.set_variable("turn_count".to_string(), count.to_string()); + + if *count % 10 == 0 { + ( + context, + HookResult::AddMessage( + format!("🎉 {}回目のターンです!", count), + Role::System + ) + ) + } else { + (context, HookResult::Continue) + } + } +} +``` + +### チェーン可能なHook + +```rust +pub struct HookChain { + hooks: Vec>, +} + +impl HookChain { + pub fn new() -> Self { + Self { hooks: Vec::new() } + } + + pub fn add_hook(mut self, hook: Box) -> Self { + self.hooks.push(hook); + self + } +} + +#[async_trait] +impl WorkerHook for HookChain { + fn name(&self) -> &str { "hook_chain" } + fn hook_type(&self) -> &str { "OnMessageSend" } + fn matcher(&self) -> &str { "" } + + async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { + for hook in &self.hooks { + let (new_context, result) = hook.execute(context).await; + context = new_context; + + match result { + HookResult::Continue | HookResult::Skip => continue, + other => return (context, other), + } + } + + (context, HookResult::Continue) + } +} +``` + +## よくある質問 + +### Q: Hookはどのように読み込まれますか? + +A: 現在、HookはRustコードとしてコンパイル時に組み込まれます。`#[hook]`マクロを使用するか、`WorkerHook`トレイトを実装して`worker.register_hook()`で登録します。 + +### Q: Hook内でファイルシステムにアクセスできますか? + +A: はい。`run_command`メソッドを使用してシェルコマンドを実行できるほか、Rustの標準ライブラリやtokioを使用した直接的なファイル操作も可能です。 + +### Q: Hook間でデータを共有できますか? + +A: `HookContext`の`variables`を使用して、同一ターン内のHook間でデータを共有できます。永続的なデータ共有には、ファイルやデータベースを使用してください。 + +### Q: ストリーミング中にHookの結果を表示できますか? + +A: はい。`context.stream_message()`や`context.stream_system_message()`を使用することで、ストリーミング中にリアルタイムでメッセージを送信できます。 + +### Q: Hookでエラーが発生した場合はどうなりますか? + +A: `HookResult::Error`を返すと、そのターンは中断されます。継続したい場合は、エラーをログに記録して`HookResult::Continue`を返してください。 + +## 関連ドキュメント + +- [worker.md](worker.md) - Worker全体の文書 +- [worker-macro.md](worker-macro.md) - マクロシステム +- `worker/src/lib.rs` - Hook実装コード +- `worker-types/src/lib.rs` - Hook型定義 +- `nia-cli/src/tui/hooks/` - TUI用Hook実装例 \ No newline at end of file diff --git a/docs/prompt-composer.md b/docs/prompt-composer.md new file mode 100644 index 0000000..feccd6e --- /dev/null +++ b/docs/prompt-composer.md @@ -0,0 +1,71 @@ +# PromptComposer + +テンプレートベースのプロンプト構築システム。Handlebarsテンプレートエンジンによる動的プロンプト生成。 + +## 基本使用方法 + +```rust +// 初期化 +let composer = PromptComposer::from_config_file("role.yaml", context)?; +composer.initialize_session(&messages)?; + +// プロンプト構築 +let messages = composer.compose(&user_messages)?; +``` + +## テンプレート構文 + +### 変数展開 +```handlebars +{{workspace.project_name}} # プロジェクト名 +{{workspace.project_type}} # プロジェクト種別 +{{model.provider}}/{{model.model_name}} # モデル情報 +{{tools_schema}} # ツールスキーマ +``` + +### 条件分岐 +```handlebars +{{#if workspace.has_nia_md}} +Project info: {{workspace_content}} +{{/if}} + +{{#eq workspace.project_type "Rust"}} +Focus on memory safety and performance. +{{/eq}} +``` + +### 繰り返し処理 +```handlebars +{{#each tools}} +- **{{name}}**: {{description}} +{{/each}} +``` + +### パーシャルテンプレート +```handlebars +{{> header}} +{{> coding_guidelines}} +{{> footer}} +``` + +## カスタムヘルパー + +### include_file +外部ファイルを読み込み: +```handlebars +{{include_file "~/.config/nia/templates/guidelines.md"}} +``` + +### workspace_content +ワークスペースのnia.md内容を取得: +```handlebars +{{workspace_content}} +``` + +## 利用可能なコンテキスト変数 + +- `workspace`: プロジェクト情報(root_path、project_type、git_info等) +- `model`: LLMモデル情報(provider、model_name、capabilities) +- `session`: セッション情報(conversation_id、message_count) +- `user_input`: ユーザー入力内容 +- `tools_schema`: ツール定義JSON \ No newline at end of file diff --git a/docs/worker-macro.md b/docs/worker-macro.md new file mode 100644 index 0000000..2325e67 --- /dev/null +++ b/docs/worker-macro.md @@ -0,0 +1,115 @@ +# Worker Macro Documentation + +## `#[tool]` マクロ + +関数をLLMツールとして登録するためのプロシージャルマクロです。関数のドキュメントコメントを自動抽出してツール説明として利用し、JSONスキーマ生成、動的ツール登録に必要な`Tool` trait実装を自動生成します。 + +**重要**: このマクロはCore Crate Patternに基づき`worker-macros`クレートで実装され、`worker-types`クレートの型を完全修飾パスで直接参照しています。これにより循環依存を防ぎ、保守性の高いアーキテクチャを実現しています。 + +### 基本的な使用方法 + +```rust +use worker_macros::tool; +use worker::types::ToolResult; // worker-typesからの再エクスポート +use serde::{Deserialize, Serialize}; +use schemars::JsonSchema; + +#[derive(Deserialize, Serialize, JsonSchema)] +struct CalculateArgs { + /// 第一の数値 + a: i32, + /// 第二の数値 + b: i32, +} + +/// 二つの数値を加算し、結果を返します。 +/// このコメントはLLMに送られるツール説明になります。 +#[tool] +async fn calculate(args: CalculateArgs) -> ToolResult { + Ok(args.a + args.b) +} +``` + +### 動的ツール登録での使用方法 + +```rust +// Worker初期化 +let mut worker = Worker::new( + LlmProvider::Gemini, + "gemini-1.5-flash", + &api_keys, + None +)?; + +// マクロで生成されたツールを登録 +worker.register_tool(Box::new(CalculateTool::new()))?; + +// ツール実行 +let args = serde_json::json!({"a": 10, "b": 20}); +let result = worker.execute_tool("calculate", args).await?; +println!("Result: {}", result); // Result: 30 +``` + +### 機能 + +1. **自動ドキュメント抽出**: 関数のドキュメントコメント(`///`)を自動抽出してツール説明として使用 +2. **JSONスキーマ生成**: `schemars`を使用して引数型からJSONスキーマを自動生成 +3. **フォールバック説明**: ドキュメントコメントがない場合、関数名からデフォルト説明を生成 +4. **Tool trait実装**: 動的ツール登録に必要な全メソッドを自動実装 +5. **エラーハンドリング**: `ToolResult`型を使用した一貫したエラー処理 + +### 要件 + +- **関数シグニチャ**: `async fn`である必要があります +- **引数型**: 単一の構造体で`Deserialize + Serialize + JsonSchema`トレイトを実装必要 +- **返り値型**: `ToolResult`で`T`は`Serialize + JsonSchema`トレイトを実装必要 +- **エラー型**: `ToolResult = Result>`(`worker-types`で定義) + +**注意**: プリミティブ型(`i32`, `String`等)を直接引数に使用することはできません。必ず構造体でラップしてください。 + +### 生成されるコード + +マクロは以下の要素を自動生成します: + +1. **元の関数**: 元のasync関数をそのまま保持 +2. **Tool構造体**: 関数名からPascalCaseで構造体名を生成(`read_file` → `ReadFileTool`) +3. **Tool trait実装**: 以下のメソッドを自動実装: + - `name()`: 関数名を返す + - `description()`: ドキュメントコメントから抽出 + - `parameters_schema()`: 引数型からJSONスキーマを生成 + - `execute()`: 元の関数を呼び出して結果をシリアライズ + +**実装詳細**: マクロは`::worker_types::`名前空間で型を参照し、`async_trait`を使用してtrait実装を行います。 + +### 自動生成される構造体名 + +関数名からsnake_caseをPascalCaseに変換し、`Tool`サフィックスを付加: +- `read_file` → `ReadFileTool` +- `calculate_sum` → `CalculateSumTool` +- `fetch_data` → `FetchDataTool` +- `send_email` → `SendEmailTool` +- `process_image` → `ProcessImageTool` + +**コンストラクタ**: 生成された構造体には`new()`メソッドが自動的に提供されます。 + +## Core Crate Patternとの統合 + +`#[tool]`マクロはCore Crate Patternの利点を最大限に活用: + +- **循環依存の完全回避**: `worker-macros` → `worker-types`の一方向依存のみ +- **コンパイル時型安全性**: 型チェックで実行時エラーを事前検出 +- **自動更新保守性**: `worker-types`の型変更がマクロ生成コードに自動反映 +- **拡張容易性**: 新しいtraitや機能を簡単に追加可能 +- **パフォーマンス**: コンパイル時コード生成で実行時オーバーヘッドなし + +## メリットと使用事例 + +このアーキテクチャにより、以下のような利点が得られます: + +- **簡单なツール作成**: 関数にマクロを付けるだけでLLMツール完成 +- **ドキュメント連動**: コメントがそのままLLMのツール説明に +- **型安全なインターフェース**: JSONシリアライゼーションを自動処理 +- **動的登録サポート**: 実行時ツールの追加・削除が可能 +- **MCP統合**: Model Context Protocolとのシームレスな連携 + +これにより、柔軟性と使いやすさを兼ね備えたツールシステムが実現されています。 \ No newline at end of file diff --git a/docs/worker.md b/docs/worker.md new file mode 100644 index 0000000..be997fc --- /dev/null +++ b/docs/worker.md @@ -0,0 +1,185 @@ +# `worker` ライブラリ + +`worker`は、LLM(大規模言語モデル)との対話を抽象化し、複数のLLMプロバイダを統一的に扱うための共通ライブラリです。動的ツール登録システム、フックシステム、MCPプロトコル統合を提供します。 + +## アーキテクチャ + +**Core Crate Pattern**を採用:`worker-types`(基本型)→ `worker-macros`(マクロ)→ `worker`(メインライブラリ) + +循環依存を解消し、型安全性と保守性を向上させています。 + +## 概要 + +LLMプロバイダ(Gemini, Claude, OpenAI, Ollama, XAI)を統一インターフェースで利用できます。`Worker`構造体が中心となり、プロンプト管理、ツール登録、フックシステム、MCPサーバー統合を提供します。 + +## 主要なコンポーネント + +### `Worker` + +LLMとの対話における中心的な構造体です。 + +- **LLMクライアントの保持**: `LlmClient` enumを通じて複数プロバイダの統一インターフェースを提供 +- **プロンプト管理**: `PromptComposer`によるテンプレートベースのプロンプト組み立て +- **ストリーミング処理**: リアルタイムイベントストリーム(`process_task_stream`)とメッセージ履歴管理(`process_task_with_history`) +- **動的ツール管理**: 実行時ツール登録・実行、MCPサーバー統合 +- **フックシステム**: メッセージ送信、ツール利用、ターン完了時の拡張ポイント +- **セッション管理**: メッセージ履歴の保存・復元機能 + +### `Tool` トレイト + +動的ツール登録の基盤。`worker-types`で定義され、`#[tool]`マクロで自動実装可能です。 + +### `StreamEvent` + +LLMストリーミング応答のイベント型。テキストチャンク、ツール呼び出し、ツール結果、完了通知、デバッグ情報、フックメッセージをサポートします。 + +### `LlmClient` Enum + +各LLMプロバイダクライアントの統合enum。`LlmClientTrait`を実装し、統一されたストリーミング対話と接続確認機能を提供します。 + +### フックシステム + +- `WorkerHook` トレイト: カスタムフック実装の基盤 +- `HookManager`: フック登録・実行管理 +- `HookEvent`: OnMessageSend、PreToolUse、PostToolUse、OnTurnCompleted +- `HookContext`: フック実行時のコンテキスト情報 + +### その他の主要型 + +- `Message`: LLM対話のメッセージ(role + content + tool_calls) +- `LlmDebug`: デバッグログ制御と詳細出力 +- `ModelInfo`: モデル情報とサポート機能 +- `SessionData`: セッション永続化データ + +## 基本的な使用方法 + +```rust +use worker::{Worker, LlmProvider, types::{Message, Role, StreamEvent}}; +use futures_util::StreamExt; + +// Worker初期化 +let mut worker = Worker::new( + LlmProvider::Gemini, + "gemini-1.5-flash", + &api_keys, + None +)?; + +// ツール登録(オプション) +worker.register_tool(Box::new(some_tool))?; + +// メッセージ送信とストリーム処理(履歴あり) +let mut stream = worker.process_task_with_history( + "Hello, how are you?".to_string(), + None +).await; + +while let Some(Ok(event)) = stream.next().await { + match event { + StreamEvent::Chunk(text) => print!("{}", text), + StreamEvent::ToolCall(call) => println!("Tool: {}", call.name), + StreamEvent::ToolResult { tool_name, result } => { + println!("Result from {}: {:?}", tool_name, result); + }, + StreamEvent::Completion(_) => break, + _ => {} + } +} +``` + +### ツール登録と実行 + +```rust +// 個別ツール登録 +worker.register_tool(Box::new(ReadFileTool::new()))?; + +// MCPサーバーからのツール登録 +let mcp_config = McpServerConfig { + name: "filesystem".to_string(), + command: "npx".to_string(), + args: vec!["-y".to_string(), "@modelcontextprotocol/server-filesystem".to_string()], + env: HashMap::new(), +}; +worker.register_mcp_tools(mcp_config).await?; + +// 複数MCPサーバーの並列初期化 +worker.queue_mcp_server(config1); +worker.queue_mcp_server(config2); +worker.init_mcp_servers().await?; + +// ツール実行 +let result = worker.execute_tool("read_file", json!({"path": "./file.txt"})).await?; +``` + +### フックシステム + +```rust +// カスタムフック実装例 +struct MyHook; + +#[async_trait::async_trait] +impl WorkerHook for MyHook { + fn name(&self) -> &str { "my_hook" } + fn hook_type(&self) -> &str { "OnMessageSend" } + fn matcher(&self) -> &str { "" } + + async fn execute(&self, mut context: HookContext) -> (HookContext, HookResult) { + // メッセージ前処理 + let new_content = format!("[処理済み] {}", context.content); + context.set_content(new_content); + (context, HookResult::Continue) + } +} + +// フック登録 +worker.register_hook(Box::new(MyHook)); +``` + +### 主要API + +- `register_tool()`: ツール登録 +- `register_mcp_tools()`: MCPサーバーからのツール登録 +- `register_hook()`: フック登録 +- `get_tools()`: 登録済みツール一覧 +- `execute_tool()`: ツール実行 +- `process_task_stream()`: LLMストリーミング対話(履歴なし) +- `process_task_with_history()`: メッセージ履歴付きストリーミング対話 +- `get_session_data()`: セッションデータ取得 +- `load_session()`: セッション復元 + +## 型システム + +**Core Crate Pattern**により型を分離: +- `worker-types`: 基本型(Tool, Message, StreamEvent等) +- `worker`: 実装とエラー型(WorkerError等) + +後方互換性のため`worker::types::`経由で全型にアクセス可能。 + +## 主要型 + +- `ToolResult`: ツール実行結果 +- `DynamicToolDefinition`: ツール定義情報 +- `WorkerError`: ライブラリ固有エラー型 + +## LLMプロバイダサポート + +全プロバイダでストリーミング対話、ツール呼び出し、モデル一覧取得、接続確認を完全実装: +- Gemini (Google) +- Claude (Anthropic) +- OpenAI +- Ollama +- XAI (Grok) + +## 主要機能 + +- **Core Crate Pattern**: 循環依存解消による保守性向上 +- **動的ツール登録**: 実行時ツール追加・実行 +- **#[tool]マクロ**: 関数からツール自動生成 +- **フックシステム**: メッセージ送信・ツール利用・ターン完了時の拡張ポイント +- **ストリーミング対話**: リアルタイムLLM応答とイベント処理 +- **複数プロバイダ**: 統一インターフェースでの5つのLLMプロバイダサポート +- **MCP統合**: Model Context Protocolサーバーとの動的連携 +- **セッション管理**: メッセージ履歴の永続化・復元 +- **ワークスペース検出**: Git情報を含む作業環境の自動認識 +- **型安全性**: 強い型チェックとエラーハンドリング +- **並列処理**: MCPサーバーの並列初期化による高速化 \ No newline at end of file