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
P0 修复: - B-MEM-2: 跨会话记忆丢失 — 添加 IdentityRecall 查询意图检测, 身份类查询绕过 FTS5/LIKE 文本搜索,直接按 scope 检索全部偏好+知识记忆; 缓存 GrowthIntegration 到 Kernel 避免每次请求重建空 scorer - B-HAND-1: Hands 未触发 — 创建 HandTool wrapper 实现 Tool trait, 在 create_tool_registry() 中注册所有已启用 Hands 为 LLM 可调用工具 P1 修复: - B-SCHED-4: 一次性定时未拦截 — 添加 RE_ONE_SHOT_TODAY 正则匹配 "下午3点半提醒我..."类无日期前缀的同日触发模式 - B-CHAT-2: 工具调用循环 — ToolErrorMiddleware 添加连续失败计数器, 3 次连续失败后自动 AbortLoop 防止无限重试 - B-CHAT-5: Stream 竞态 — cancelStream 后添加 500ms cancelCooldown, 防止后端 active-stream 检查竞态
195 lines
5.5 KiB
Rust
195 lines
5.5 KiB
Rust
//! Tool system for agent capabilities
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use async_trait::async_trait;
|
|
use serde_json::Value;
|
|
use tokio::sync::mpsc;
|
|
use zclaw_types::{AgentId, Result};
|
|
|
|
use crate::driver::ToolDefinition;
|
|
use crate::loop_runner::LoopEvent;
|
|
use crate::tool::builtin::PathValidator;
|
|
|
|
/// Tool trait for implementing agent tools
|
|
#[async_trait]
|
|
pub trait Tool: Send + Sync {
|
|
/// Get the tool name
|
|
fn name(&self) -> &str;
|
|
|
|
/// Get the tool description
|
|
fn description(&self) -> &str;
|
|
|
|
/// Get the JSON schema for input parameters
|
|
fn input_schema(&self) -> Value;
|
|
|
|
/// Execute the tool
|
|
async fn execute(&self, input: Value, context: &ToolContext) -> Result<Value>;
|
|
}
|
|
|
|
/// Skill executor trait for runtime skill execution
|
|
/// This allows tools to execute skills without direct dependency on zclaw-skills
|
|
#[async_trait]
|
|
pub trait SkillExecutor: Send + Sync {
|
|
/// Execute a skill by ID
|
|
async fn execute_skill(
|
|
&self,
|
|
skill_id: &str,
|
|
agent_id: &str,
|
|
session_id: &str,
|
|
input: Value,
|
|
) -> Result<Value>;
|
|
|
|
/// Return metadata for on-demand skill loading.
|
|
/// Default returns `None` (skill detail not available).
|
|
fn get_skill_detail(&self, skill_id: &str) -> Option<SkillDetail> {
|
|
let _ = skill_id;
|
|
None
|
|
}
|
|
|
|
/// Return lightweight index of all available skills.
|
|
/// Default returns empty (no index available).
|
|
fn list_skill_index(&self) -> Vec<SkillIndexEntry> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Lightweight skill index entry for system prompt injection.
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct SkillIndexEntry {
|
|
pub id: String,
|
|
pub description: String,
|
|
pub triggers: Vec<String>,
|
|
}
|
|
|
|
/// Full skill detail returned by `skill_load` tool.
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct SkillDetail {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub category: Option<String>,
|
|
pub input_schema: Option<Value>,
|
|
pub triggers: Vec<String>,
|
|
pub capabilities: Vec<String>,
|
|
}
|
|
|
|
/// Context provided to tool execution
|
|
pub struct ToolContext {
|
|
pub agent_id: AgentId,
|
|
pub working_directory: Option<String>,
|
|
pub session_id: Option<String>,
|
|
pub skill_executor: Option<Arc<dyn SkillExecutor>>,
|
|
/// Path validator for file system operations
|
|
pub path_validator: Option<PathValidator>,
|
|
/// Optional event sender for streaming tool progress to the frontend.
|
|
/// Tools like TaskTool use this to emit sub-agent status events.
|
|
pub event_sender: Option<mpsc::Sender<LoopEvent>>,
|
|
}
|
|
|
|
impl std::fmt::Debug for ToolContext {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ToolContext")
|
|
.field("agent_id", &self.agent_id)
|
|
.field("working_directory", &self.working_directory)
|
|
.field("session_id", &self.session_id)
|
|
.field("skill_executor", &self.skill_executor.as_ref().map(|_| "SkillExecutor"))
|
|
.field("path_validator", &self.path_validator.as_ref().map(|_| "PathValidator"))
|
|
.field("event_sender", &self.event_sender.as_ref().map(|_| "Sender<LoopEvent>"))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Clone for ToolContext {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
agent_id: self.agent_id.clone(),
|
|
working_directory: self.working_directory.clone(),
|
|
session_id: self.session_id.clone(),
|
|
skill_executor: self.skill_executor.clone(),
|
|
path_validator: self.path_validator.clone(),
|
|
event_sender: self.event_sender.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tool registry for managing available tools
|
|
/// Uses HashMap for O(1) lookup performance
|
|
#[derive(Clone)]
|
|
pub struct ToolRegistry {
|
|
/// Tool lookup by name (O(1))
|
|
tools: HashMap<String, Arc<dyn Tool>>,
|
|
/// Registration order for consistent iteration
|
|
tool_order: Vec<String>,
|
|
}
|
|
|
|
impl ToolRegistry {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tools: HashMap::new(),
|
|
tool_order: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn register(&mut self, tool: Box<dyn Tool>) {
|
|
let tool: Arc<dyn Tool> = Arc::from(tool);
|
|
let name = tool.name().to_string();
|
|
|
|
// Track order for new tools
|
|
if !self.tools.contains_key(&name) {
|
|
self.tool_order.push(name.clone());
|
|
}
|
|
|
|
self.tools.insert(name, tool);
|
|
}
|
|
|
|
/// Get tool by name - O(1) lookup
|
|
pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
|
|
self.tools.get(name).cloned()
|
|
}
|
|
|
|
/// List all tools in registration order
|
|
pub fn list(&self) -> Vec<&dyn Tool> {
|
|
self.tool_order
|
|
.iter()
|
|
.filter_map(|name| self.tools.get(name).map(|t| t.as_ref()))
|
|
.collect()
|
|
}
|
|
|
|
/// Get tool definitions in registration order
|
|
pub fn definitions(&self) -> Vec<ToolDefinition> {
|
|
self.tool_order
|
|
.iter()
|
|
.filter_map(|name| {
|
|
self.tools.get(name).map(|t| {
|
|
ToolDefinition::new(
|
|
t.name(),
|
|
t.description(),
|
|
t.input_schema(),
|
|
)
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Get number of registered tools
|
|
pub fn len(&self) -> usize {
|
|
self.tools.len()
|
|
}
|
|
|
|
/// Check if registry is empty
|
|
pub fn is_empty(&self) -> bool {
|
|
self.tools.is_empty()
|
|
}
|
|
}
|
|
|
|
impl Default for ToolRegistry {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
// Built-in tools module
|
|
pub mod builtin;
|
|
pub mod hand_tool;
|