diff --git a/crates/zclaw-kernel/src/kernel/adapters.rs b/crates/zclaw-kernel/src/kernel/adapters.rs index 9686718..66c0e3e 100644 --- a/crates/zclaw-kernel/src/kernel/adapters.rs +++ b/crates/zclaw-kernel/src/kernel/adapters.rs @@ -3,11 +3,12 @@ use std::pin::Pin; use std::sync::Arc; use async_trait::async_trait; -use serde_json::Value; +use serde_json::{json, Value}; -use zclaw_runtime::{LlmDriver, tool::SkillExecutor}; +use zclaw_runtime::{LlmDriver, tool::{SkillExecutor, HandExecutor}}; use zclaw_skills::{SkillRegistry, LlmCompleter}; -use zclaw_types::Result; +use zclaw_hands::HandRegistry; +use zclaw_types::{AgentId, Result}; /// Adapter that bridges `zclaw_runtime::LlmDriver` -> `zclaw_skills::LlmCompleter` pub(crate) struct LlmDriverAdapter { @@ -134,3 +135,47 @@ impl AgentInbox { self.pending.push_back(envelope); } } + +/// Hand executor implementation for Kernel +/// +/// Bridges `zclaw_runtime::tool::HandExecutor` → `zclaw_hands::HandRegistry`, +/// allowing `HandTool::execute()` to dispatch to the real Hand implementations. +pub struct KernelHandExecutor { + hands: Arc, +} + +impl KernelHandExecutor { + pub fn new(hands: Arc) -> Self { + Self { hands } + } +} + +#[async_trait] +impl HandExecutor for KernelHandExecutor { + async fn execute_hand( + &self, + hand_id: &str, + agent_id: &AgentId, + input: Value, + ) -> Result { + let context = zclaw_hands::HandContext { + agent_id: agent_id.clone(), + working_dir: None, + env: std::collections::HashMap::new(), + timeout_secs: 300, + callback_url: None, + }; + let result = self.hands.execute(hand_id, &context, input).await?; + if result.success { + Ok(result.output) + } else { + Ok(json!({ + "hand_id": hand_id, + "status": "failed", + "error": result.error.unwrap_or_else(|| "Unknown hand execution error".to_string()), + "output": result.output, + "duration_ms": result.duration_ms, + })) + } + } +} diff --git a/crates/zclaw-kernel/src/kernel/messaging.rs b/crates/zclaw-kernel/src/kernel/messaging.rs index 8c5dce9..e6dab6d 100644 --- a/crates/zclaw-kernel/src/kernel/messaging.rs +++ b/crates/zclaw-kernel/src/kernel/messaging.rs @@ -64,6 +64,7 @@ impl Kernel { ) .with_model(&model) .with_skill_executor(self.skill_executor.clone()) + .with_hand_executor(self.hand_executor.clone()) .with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens())) .with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature())) .with_compaction_threshold( @@ -176,6 +177,7 @@ impl Kernel { ) .with_model(&model) .with_skill_executor(self.skill_executor.clone()) + .with_hand_executor(self.hand_executor.clone()) .with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens())) .with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature())) .with_compaction_threshold( diff --git a/crates/zclaw-kernel/src/kernel/mod.rs b/crates/zclaw-kernel/src/kernel/mod.rs index d572b10..a470608 100644 --- a/crates/zclaw-kernel/src/kernel/mod.rs +++ b/crates/zclaw-kernel/src/kernel/mod.rs @@ -27,6 +27,7 @@ use zclaw_skills::SkillRegistry; use zclaw_hands::{HandRegistry, hands::{BrowserHand, QuizHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, ReminderHand, quiz::LlmQuizGenerator}}; pub use adapters::KernelSkillExecutor; +pub use adapters::KernelHandExecutor; pub use messaging::ChatModeConfig; /// The ZCLAW Kernel @@ -40,6 +41,7 @@ pub struct Kernel { llm_completer: Arc, skills: Arc, skill_executor: Arc, + hand_executor: Arc, hands: Arc, /// Cached hand configs (populated at boot, used for tool registry) hand_configs: Vec, @@ -105,6 +107,9 @@ impl Kernel { // Create skill executor let skill_executor = Arc::new(KernelSkillExecutor::new(skills.clone(), driver.clone())); + // Create hand executor — bridges HandTool calls to the HandRegistry + let hand_executor = Arc::new(KernelHandExecutor::new(hands.clone())); + // Create LLM completer for skill system (shared with skill_executor) let llm_completer: Arc = Arc::new(adapters::LlmDriverAdapter { @@ -152,6 +157,7 @@ impl Kernel { llm_completer, skills, skill_executor, + hand_executor, hands, hand_configs, trigger_manager, diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index f614cc8..7f40522 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -7,7 +7,7 @@ use zclaw_types::{AgentId, SessionId, Message, Result}; use crate::driver::{LlmDriver, CompletionRequest, ContentBlock}; use crate::stream::StreamChunk; -use crate::tool::{ToolRegistry, ToolContext, SkillExecutor}; +use crate::tool::{ToolRegistry, ToolContext, SkillExecutor, HandExecutor}; use crate::tool::builtin::PathValidator; use crate::growth::GrowthIntegration; use crate::compaction::{self, CompactionConfig}; @@ -28,6 +28,7 @@ pub struct AgentLoop { max_tokens: u32, temperature: f32, skill_executor: Option>, + hand_executor: Option>, path_validator: Option, /// Growth system integration (optional) growth: Option, @@ -64,6 +65,7 @@ impl AgentLoop { max_tokens: 16384, temperature: 0.7, skill_executor: None, + hand_executor: None, path_validator: None, growth: None, compaction_threshold: 0, @@ -81,6 +83,12 @@ impl AgentLoop { self } + /// Set the hand executor for dispatching Hand tool calls to HandRegistry + pub fn with_hand_executor(mut self, executor: Arc) -> Self { + self.hand_executor = Some(executor); + self + } + /// Set the path validator for file system operations pub fn with_path_validator(mut self, validator: PathValidator) -> Self { self.path_validator = Some(validator); @@ -199,6 +207,7 @@ impl AgentLoop { working_directory: working_dir, session_id: Some(session_id.to_string()), skill_executor: self.skill_executor.clone(), + hand_executor: self.hand_executor.clone(), path_validator: Some(path_validator), event_sender: None, } @@ -567,6 +576,7 @@ impl AgentLoop { let tools = self.tools.clone(); let middleware_chain = self.middleware_chain.clone(); let skill_executor = self.skill_executor.clone(); + let hand_executor = self.hand_executor.clone(); let path_validator = self.path_validator.clone(); let agent_id = self.agent_id.clone(); let model = self.model.clone(); @@ -849,6 +859,7 @@ impl AgentLoop { working_directory: working_dir, session_id: Some(session_id_clone.to_string()), skill_executor: skill_executor.clone(), + hand_executor: hand_executor.clone(), path_validator: Some(pv), event_sender: Some(tx.clone()), }; @@ -903,6 +914,7 @@ impl AgentLoop { working_directory: working_dir, session_id: Some(session_id_clone.to_string()), skill_executor: skill_executor.clone(), + hand_executor: hand_executor.clone(), path_validator: Some(pv), event_sender: Some(tx.clone()), }; diff --git a/crates/zclaw-runtime/src/tool.rs b/crates/zclaw-runtime/src/tool.rs index 98b9f6c..b13bad7 100644 --- a/crates/zclaw-runtime/src/tool.rs +++ b/crates/zclaw-runtime/src/tool.rs @@ -74,12 +74,27 @@ pub struct SkillDetail { pub capabilities: Vec, } +/// Hand executor trait for runtime hand execution +/// This allows tools (HandTool) to execute hands without direct dependency on zclaw-hands +#[async_trait] +pub trait HandExecutor: Send + Sync { + /// Execute a hand by ID, returning the output as JSON + async fn execute_hand( + &self, + hand_id: &str, + agent_id: &AgentId, + input: Value, + ) -> Result; +} + /// Context provided to tool execution pub struct ToolContext { pub agent_id: AgentId, pub working_directory: Option, pub session_id: Option, pub skill_executor: Option>, + /// Hand executor for dispatching Hand tool calls to the HandRegistry + pub hand_executor: Option>, /// Path validator for file system operations pub path_validator: Option, /// Optional event sender for streaming tool progress to the frontend. @@ -94,6 +109,7 @@ impl std::fmt::Debug for ToolContext { .field("working_directory", &self.working_directory) .field("session_id", &self.session_id) .field("skill_executor", &self.skill_executor.as_ref().map(|_| "SkillExecutor")) + .field("hand_executor", &self.hand_executor.as_ref().map(|_| "HandExecutor")) .field("path_validator", &self.path_validator.as_ref().map(|_| "PathValidator")) .field("event_sender", &self.event_sender.as_ref().map(|_| "Sender")) .finish() @@ -107,6 +123,7 @@ impl Clone for ToolContext { working_directory: self.working_directory.clone(), session_id: self.session_id.clone(), skill_executor: self.skill_executor.clone(), + hand_executor: self.hand_executor.clone(), path_validator: self.path_validator.clone(), event_sender: self.event_sender.clone(), } diff --git a/crates/zclaw-runtime/src/tool/builtin/file_read.rs b/crates/zclaw-runtime/src/tool/builtin/file_read.rs index 25fa015..1d28322 100644 --- a/crates/zclaw-runtime/src/tool/builtin/file_read.rs +++ b/crates/zclaw-runtime/src/tool/builtin/file_read.rs @@ -139,6 +139,7 @@ mod tests { working_directory: None, session_id: None, skill_executor: None, + hand_executor: None, path_validator, event_sender: None, }; diff --git a/crates/zclaw-runtime/src/tool/builtin/file_write.rs b/crates/zclaw-runtime/src/tool/builtin/file_write.rs index 0f1803c..354e958 100644 --- a/crates/zclaw-runtime/src/tool/builtin/file_write.rs +++ b/crates/zclaw-runtime/src/tool/builtin/file_write.rs @@ -162,6 +162,7 @@ mod tests { working_directory: None, session_id: None, skill_executor: None, + hand_executor: None, path_validator, event_sender: None, } diff --git a/crates/zclaw-runtime/src/tool/hand_tool.rs b/crates/zclaw-runtime/src/tool/hand_tool.rs index 455fd78..f6572cf 100644 --- a/crates/zclaw-runtime/src/tool/hand_tool.rs +++ b/crates/zclaw-runtime/src/tool/hand_tool.rs @@ -78,21 +78,26 @@ impl Tool for HandTool { self.input_schema.clone() } - async fn execute(&self, input: Value, _context: &ToolContext) -> Result { - // Hand execution is delegated to HandRegistry via the kernel's - // hand execution path. This tool acts as the LLM-facing interface. - // The actual execution is handled by the HandRegistry when the - // kernel processes the tool call. - - // For now, return a structured result that indicates the hand was invoked. - // The kernel's hand execution layer will handle the actual execution - // and emit HandStart/HandEnd events. - Ok(json!({ - "hand_id": self.hand_id, - "status": "invoked", - "input": input, - "message": format!("Hand '{}' invoked successfully", self.hand_id) - })) + async fn execute(&self, input: Value, context: &ToolContext) -> Result { + // Delegate to the HandExecutor (bridged from HandRegistry via kernel). + // If no hand_executor is available (e.g., standalone runtime without kernel), + // return a descriptive error so the LLM knows the hand is unavailable. + match &context.hand_executor { + Some(executor) => { + executor.execute_hand(&self.hand_id, &context.agent_id, input).await + } + None => { + Ok(json!({ + "hand_id": self.hand_id, + "status": "unavailable", + "error": format!( + "Hand '{}' cannot execute: no hand executor configured. \ + This usually means the kernel is not running or hands are not registered.", + self.hand_id + ) + })) + } + } } } @@ -130,13 +135,14 @@ mod tests { } #[tokio::test] - async fn test_hand_tool_execute() { + async fn test_hand_tool_execute_no_executor() { let tool = HandTool::from_config("quiz", "Generate quizzes", None); let ctx = ToolContext { agent_id: zclaw_types::AgentId::new(), working_directory: None, session_id: None, skill_executor: None, + hand_executor: None, path_validator: None, event_sender: None, }; @@ -144,6 +150,6 @@ mod tests { assert!(result.is_ok()); let val = result.unwrap(); assert_eq!(val["hand_id"], "quiz"); - assert_eq!(val["status"], "invoked"); + assert_eq!(val["status"], "unavailable"); } }