Files
zclaw_openfang/crates/zclaw-runtime/src/tool.rs
iven 9871c254be
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
feat: sub-agent streaming progress — TaskTool emits real-time status events
- Rust: LoopEvent::SubtaskStatus variant added to loop_runner.rs
- Rust: ToolContext.event_sender field for streaming tool progress
- Rust: TaskTool emits started/running/completed/failed via event_sender
- Rust: StreamChatEvent::SubtaskStatus mapped in Tauri chat command
- TS: StreamEventSubtaskStatus type + onSubtaskStatus callback added
- TS: kernel-chat.ts handles subtaskStatus event from Tauri
- TS: streamStore.ts wires callback, maps backend→frontend status,
  updates assistant message subtasks array in real-time
2026-04-06 13:05:37 +08:00

194 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;