Skip to main content

xz_skill_core/traits/
executor.rs

1use crate::error::SkillError;
2use crate::types::skill::ToolDefinition;
3use async_trait::async_trait;
4
5/// Output from a single tool execution.
6///
7/// Contains the textual result and an error flag to distinguish
8/// successful execution from tool-level failures.
9#[derive(Debug, Clone)]
10pub struct ToolOutput {
11    /// The output produced by the tool.
12    pub output: String,
13    /// Whether the tool encountered an error during execution.
14    pub is_error: bool,
15}
16
17/// Skill tool executor.
18///
19/// Executes a single named tool within a skill identified by `skill_id`.
20/// This trait does **not** run an LLM loop — it only handles one tool call
21/// at a time. Callers are responsible for any higher-level orchestration.
22///
23/// # Example
24///
25/// ```ignore
26/// use async_trait::async_trait;
27/// use xz_skill_core::traits::{SkillExecutor, ToolOutput};
28/// use xz_skill_core::SkillError;
29/// use xz_skill_core::types::skill::ToolDefinition;
30///
31/// struct EchoExecutor;
32///
33/// #[async_trait]
34/// impl SkillExecutor for EchoExecutor {
35///     async fn execute_tool(
36///         &self,
37///         _skill_id: &str,
38///         tool_name: &str,
39///         args: serde_json::Value,
40///     ) -> Result<ToolOutput, SkillError> {
41///         Ok(ToolOutput {
42///             output: format!("{}: {}", tool_name, args),
43///             is_error: false,
44///         })
45///     }
46///
47///     fn tools(&self, _skill_id: &str) -> Result<Vec<ToolDefinition>, SkillError> {
48///         Ok(vec![ToolDefinition {
49///             name: "echo".into(),
50///             description: "Echoes input".into(),
51///             input_schema: serde_json::json!({}),
52///             tool_type: crate::types::skill::ToolType::Builtin {
53///                 handler: "echo".into(),
54///             },
55///         }])
56///     }
57/// }
58/// ```
59#[async_trait]
60pub trait SkillExecutor: Send + Sync {
61    /// Execute a single named tool with the given arguments.
62    ///
63    /// - `skill_id` — Identifies which skill provides the tool.
64    /// - `tool_name` — The name of the tool to invoke.
65    /// - `args` — JSON arguments passed to the tool.
66    async fn execute_tool(
67        &self,
68        skill_id: &str,
69        tool_name: &str,
70        args: serde_json::Value,
71    ) -> Result<ToolOutput, SkillError>;
72
73    /// List all tools available for a given skill.
74    ///
75    /// Returns the tool definitions so callers can discover capabilities
76    /// without executing anything.
77    fn tools(&self, skill_id: &str) -> Result<Vec<ToolDefinition>, SkillError>;
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    struct TestExecutor;
85
86    #[async_trait]
87    impl SkillExecutor for TestExecutor {
88        async fn execute_tool(
89            &self,
90            _skill_id: &str,
91            tool_name: &str,
92            args: serde_json::Value,
93        ) -> Result<ToolOutput, SkillError> {
94            Ok(ToolOutput { output: format!("{tool_name}: {args}"), is_error: false })
95        }
96
97        fn tools(&self, _skill_id: &str) -> Result<Vec<ToolDefinition>, SkillError> {
98            Ok(vec![])
99        }
100    }
101
102    #[tokio::test]
103    async fn test_execute_tool_returns_output() {
104        let exec = TestExecutor;
105        let result =
106            exec.execute_tool("test-skill", "echo", serde_json::json!("hello")).await.unwrap();
107        assert!(!result.is_error);
108        assert!(result.output.contains("echo"));
109    }
110
111    #[tokio::test]
112    async fn test_tools_returns_definitions() {
113        let exec = TestExecutor;
114        let tools = exec.tools("test-skill").unwrap();
115        assert!(tools.is_empty());
116    }
117}