feat(skill-execution): implement execute_skill tool with full execution chain
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

- Add ExecuteSkillTool for LLM to call skills during conversation
- Implement SkillExecutor trait in Kernel for skill execution
- Update AgentLoop to support tool execution with skill_executor
- Add default skills_dir configuration in KernelConfig
- Connect frontend skillMarketStore to backend skill_list command
- Update technical documentation with Skill system architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-24 13:24:23 +08:00
parent 1441f98c5e
commit 504d5746aa
8 changed files with 698 additions and 131 deletions

View File

@@ -3,13 +3,13 @@
//! These commands provide direct access to the internal ZCLAW Kernel,
//! eliminating the need for external OpenFang process.
use std::path::PathBuf;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, Manager, State};
use tauri::{AppHandle, Emitter, State};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use futures::StreamExt;
use zclaw_kernel::Kernel;
use zclaw_types::{AgentConfig, AgentId, AgentInfo, AgentState};
use zclaw_types::{AgentConfig, AgentId, AgentInfo};
/// Kernel state wrapper for Tauri
pub type KernelState = Arc<Mutex<Option<Kernel>>>;
@@ -443,3 +443,242 @@ pub async fn agent_chat_stream(
pub fn create_kernel_state() -> KernelState {
Arc::new(Mutex::new(None))
}
// ============================================================================
// Skills Commands - Dynamic Discovery
// ============================================================================
/// Skill information response for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillInfoResponse {
pub id: String,
pub name: String,
pub description: String,
pub version: String,
pub capabilities: Vec<String>,
pub tags: Vec<String>,
pub mode: String,
pub enabled: bool,
}
impl From<zclaw_skills::SkillManifest> for SkillInfoResponse {
fn from(manifest: zclaw_skills::SkillManifest) -> Self {
Self {
id: manifest.id.to_string(),
name: manifest.name,
description: manifest.description,
version: manifest.version,
capabilities: manifest.capabilities,
tags: manifest.tags,
mode: format!("{:?}", manifest.mode),
enabled: manifest.enabled,
}
}
}
/// List all discovered skills
///
/// Returns skills from the Kernel's SkillRegistry.
/// Skills are loaded from the skills/ directory during kernel initialization.
#[tauri::command]
pub async fn skill_list(
state: State<'_, KernelState>,
) -> Result<Vec<SkillInfoResponse>, String> {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
let skills = kernel.list_skills().await;
Ok(skills.into_iter().map(SkillInfoResponse::from).collect())
}
/// Refresh skills from a directory
///
/// Re-scans the skills directory for new or updated skills.
/// Optionally accepts a custom directory path to scan.
#[tauri::command]
pub async fn skill_refresh(
state: State<'_, KernelState>,
skill_dir: Option<String>,
) -> Result<Vec<SkillInfoResponse>, String> {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
// Convert optional string to PathBuf
let dir_path = skill_dir.map(PathBuf::from);
// Refresh skills
kernel.refresh_skills(dir_path)
.await
.map_err(|e| format!("Failed to refresh skills: {}", e))?;
// Return updated list
let skills = kernel.list_skills().await;
Ok(skills.into_iter().map(SkillInfoResponse::from).collect())
}
// ============================================================================
// Skill Execution Command
// ============================================================================
/// Skill execution context
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillContext {
pub agent_id: String,
pub session_id: String,
pub working_dir: Option<String>,
}
impl From<SkillContext> for zclaw_skills::SkillContext {
fn from(ctx: SkillContext) -> Self {
Self {
agent_id: ctx.agent_id,
session_id: ctx.session_id,
working_dir: ctx.working_dir.map(std::path::PathBuf::from),
env: std::collections::HashMap::new(),
timeout_secs: 300,
network_allowed: true,
file_access_allowed: true,
}
}
}
/// Skill execution result
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillResult {
pub success: bool,
pub output: serde_json::Value,
pub error: Option<String>,
pub duration_ms: Option<u64>,
}
impl From<zclaw_skills::SkillResult> for SkillResult {
fn from(result: zclaw_skills::SkillResult) -> Self {
Self {
success: result.success,
output: result.output,
error: result.error,
duration_ms: result.duration_ms,
}
}
}
/// Execute a skill
///
/// Executes a skill with the given ID and input.
/// Returns the skill result as JSON.
#[tauri::command]
pub async fn skill_execute(
state: State<'_, KernelState>,
id: String,
context: SkillContext,
input: serde_json::Value,
) -> Result<SkillResult, String> {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
// Execute skill
let result = kernel.execute_skill(&id, context.into(), input).await
.map_err(|e| format!("Failed to execute skill: {}", e))?;
Ok(SkillResult::from(result))
}
// ============================================================================
// Hands Commands - Autonomous Capabilities
// ============================================================================
/// Hand information response for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HandInfoResponse {
pub id: String,
pub name: String,
pub description: String,
pub needs_approval: bool,
pub dependencies: Vec<String>,
pub tags: Vec<String>,
pub enabled: bool,
}
impl From<zclaw_hands::HandConfig> for HandInfoResponse {
fn from(config: zclaw_hands::HandConfig) -> Self {
Self {
id: config.id,
name: config.name,
description: config.description,
needs_approval: config.needs_approval,
dependencies: config.dependencies,
tags: config.tags,
enabled: config.enabled,
}
}
}
/// Hand execution result
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HandResult {
pub success: bool,
pub output: serde_json::Value,
pub error: Option<String>,
pub duration_ms: Option<u64>,
}
impl From<zclaw_hands::HandResult> for HandResult {
fn from(result: zclaw_hands::HandResult) -> Self {
Self {
success: result.success,
output: result.output,
error: result.error,
duration_ms: result.duration_ms,
}
}
}
/// List all registered hands
///
/// Returns hands from the Kernel's HandRegistry.
/// Hands are registered during kernel initialization.
#[tauri::command]
pub async fn hand_list(
state: State<'_, KernelState>,
) -> Result<Vec<HandInfoResponse>, String> {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
let hands = kernel.list_hands().await;
Ok(hands.into_iter().map(HandInfoResponse::from).collect())
}
/// Execute a hand
///
/// Executes a hand with the given ID and input.
/// Returns the hand result as JSON.
#[tauri::command]
pub async fn hand_execute(
state: State<'_, KernelState>,
id: String,
input: serde_json::Value,
) -> Result<HandResult, String> {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
// Execute hand
let result = kernel.execute_hand(&id, input).await
.map_err(|e| format!("Failed to execute hand: {}", e))?;
Ok(HandResult::from(result))
}