Skip to main content

j_agent/tools/
skill.rs

1use crate::context::compact::{InvokedSkillsMap, record_skill_invocation};
2use crate::infra::skill::{Skill, resolve_skill_content};
3use crate::tools::{PlanDecision, Tool, ToolResult, parse_tool_args, schema_to_tool_params};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use serde_json::Value;
7use std::borrow::Cow;
8use std::sync::{Arc, atomic::AtomicBool};
9
10/// LoadSkillTool 参数
11#[derive(Deserialize, JsonSchema)]
12struct LoadSkillParams {
13    /// Name of the skill to load
14    name: String,
15    /// Arguments to pass to the skill (optional)
16    #[serde(default)]
17    arguments: Option<String>,
18}
19
20// ========== LoadSkillTool ==========
21
22/// 技能加载工具,用于将指定技能的完整内容加载到上下文中
23#[derive(Debug)]
24pub struct LoadSkillTool {
25    /// 可用技能列表
26    pub skills: Vec<Skill>,
27    /// 已调用技能追踪(执行时记录,供 auto_compact 后恢复)
28    pub invoked_skills: InvokedSkillsMap,
29}
30
31impl LoadSkillTool {
32    pub const NAME: &'static str = "LoadSkill";
33}
34
35impl Tool for LoadSkillTool {
36    fn name(&self) -> &str {
37        Self::NAME
38    }
39
40    fn description(&self) -> Cow<'_, str> {
41        "Load the full content of a specified skill into context for more information, helping you better complete the task. Check the skills list for available skill names and directory paths.".into()
42    }
43
44    fn parameters_schema(&self) -> Value {
45        schema_to_tool_params::<LoadSkillParams>()
46    }
47
48    fn execute(&self, arguments: &str, _cancelled: &Arc<AtomicBool>) -> ToolResult {
49        let params: LoadSkillParams = match parse_tool_args(arguments) {
50            Ok(p) => p,
51            Err(e) => return e,
52        };
53
54        if params.name.is_empty() {
55            return ToolResult {
56                output: "参数缺少 name 字段".to_string(),
57                is_error: true,
58                images: vec![],
59                plan_decision: PlanDecision::None,
60            };
61        }
62
63        let args_str = params.arguments.as_deref().unwrap_or("");
64
65        match self
66            .skills
67            .iter()
68            .find(|s| s.frontmatter.name == params.name)
69        {
70            Some(skill) => {
71                let content = resolve_skill_content(skill);
72                let resolved = content.replace("$ARGUMENTS", args_str);
73                // 记录技能调用(供 auto_compact 后恢复技能指令)
74                record_skill_invocation(
75                    &self.invoked_skills,
76                    skill.frontmatter.name.clone(),
77                    skill.dir_path.display().to_string(),
78                    resolved.clone(),
79                );
80                ToolResult {
81                    output: resolved,
82                    is_error: false,
83                    images: vec![],
84                    plan_decision: PlanDecision::None,
85                }
86            }
87            None => {
88                let available: Vec<&str> = self
89                    .skills
90                    .iter()
91                    .map(|s| s.frontmatter.name.as_str())
92                    .collect();
93                ToolResult {
94                    output: format!(
95                        "未找到技能 '{}'。可用技能: {}",
96                        params.name,
97                        available.join(", ")
98                    ),
99                    is_error: true,
100                    images: vec![],
101                    plan_decision: PlanDecision::None,
102                }
103            }
104        }
105    }
106
107    fn requires_confirmation(&self) -> bool {
108        false
109    }
110}