Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
B-HAND-1 修复: LLM 调用 hand_quiz/hand_researcher 等 Hand 工具后,
HandTool::execute() 原来返回假成功 JSON, 实际 Hand 并不执行.
修复方案 (沿用 SkillExecutor 模式):
- tool.rs: 新增 HandExecutor trait + ToolContext.hand_executor 字段
- hand_tool.rs: execute() 通过 context.hand_executor 分发到真实执行
- loop_runner.rs: AgentLoop 新增 hand_executor 字段 + builder + 3处 ToolContext 传递
- adapters.rs: 新增 KernelHandExecutor 桥接 HandRegistry.execute()
- kernel/mod.rs: 初始化 KernelHandExecutor + 注册到 AgentLoop
- messaging.rs: 两处 AgentLoop 构建添加 .with_hand_executor()
数据流: LLM tool call → HandTool::execute() → ToolContext.hand_executor
→ KernelHandExecutor → HandRegistry.execute() → Hand trait impl
809 tests passed, 0 failed.
156 lines
4.6 KiB
Rust
156 lines
4.6 KiB
Rust
//! Hand Tool Wrapper
|
|
//!
|
|
//! Bridges the Hand trait (zclaw-hands) to the Tool trait (zclaw-runtime),
|
|
//! allowing Hands to be registered in the ToolRegistry and callable by the LLM.
|
|
|
|
use async_trait::async_trait;
|
|
use serde_json::{json, Value};
|
|
use zclaw_types::Result;
|
|
|
|
use crate::tool::{Tool, ToolContext};
|
|
|
|
/// Wrapper that exposes a Hand as a Tool in the agent's tool registry.
|
|
///
|
|
/// When the LLM calls `hand_quiz`, `hand_researcher`, etc., the call is
|
|
/// routed through this wrapper to the actual Hand implementation.
|
|
pub struct HandTool {
|
|
/// Hand identifier (e.g., "hand_quiz", "hand_researcher")
|
|
name: String,
|
|
/// Human-readable description
|
|
description: String,
|
|
/// Input JSON schema
|
|
input_schema: Value,
|
|
/// Hand ID for registry lookup
|
|
hand_id: String,
|
|
}
|
|
|
|
impl HandTool {
|
|
/// Create a new HandTool wrapper from hand metadata.
|
|
pub fn new(
|
|
tool_name: &str,
|
|
description: &str,
|
|
input_schema: Value,
|
|
hand_id: &str,
|
|
) -> Self {
|
|
Self {
|
|
name: tool_name.to_string(),
|
|
description: description.to_string(),
|
|
input_schema,
|
|
hand_id: hand_id.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Build a HandTool from HandConfig fields.
|
|
pub fn from_config(hand_id: &str, description: &str, input_schema: Option<Value>) -> Self {
|
|
let tool_name = format!("hand_{}", hand_id);
|
|
let schema = input_schema.unwrap_or_else(|| {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"input": {
|
|
"type": "string",
|
|
"description": format!("Input for the {} hand", hand_id)
|
|
}
|
|
},
|
|
"required": []
|
|
})
|
|
});
|
|
Self::new(&tool_name, description, schema, hand_id)
|
|
}
|
|
|
|
/// Get the hand ID for registry lookup
|
|
pub fn hand_id(&self) -> &str {
|
|
&self.hand_id
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for HandTool {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
&self.description
|
|
}
|
|
|
|
fn input_schema(&self) -> Value {
|
|
self.input_schema.clone()
|
|
}
|
|
|
|
async fn execute(&self, input: Value, context: &ToolContext) -> Result<Value> {
|
|
// 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
|
|
)
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_hand_tool_creation() {
|
|
let tool = HandTool::from_config(
|
|
"quiz",
|
|
"Generate quizzes on various topics",
|
|
None,
|
|
);
|
|
assert_eq!(tool.name(), "hand_quiz");
|
|
assert_eq!(tool.hand_id(), "quiz");
|
|
assert!(tool.description().contains("quiz"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_hand_tool_custom_schema() {
|
|
let schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"topic": { "type": "string" },
|
|
"difficulty": { "type": "string" }
|
|
}
|
|
});
|
|
let tool = HandTool::from_config(
|
|
"quiz",
|
|
"Generate quizzes",
|
|
Some(schema.clone()),
|
|
);
|
|
assert_eq!(tool.input_schema(), schema);
|
|
}
|
|
|
|
#[tokio::test]
|
|
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,
|
|
};
|
|
let result = tool.execute(json!({"topic": "Python"}), &ctx).await;
|
|
assert!(result.is_ok());
|
|
let val = result.unwrap();
|
|
assert_eq!(val["hand_id"], "quiz");
|
|
assert_eq!(val["status"], "unavailable");
|
|
}
|
|
}
|