Skip to main content

j_agent/tools/
definition.rs

1use crate::context::compact::InvokedSkillsMap;
2use crate::infra::hook::HookManager;
3use crate::infra::skill::Skill;
4use crate::llm::{FunctionObject, ToolDefinition};
5use crate::message_types::AskRequest;
6use crate::permission::queue::PermissionQueue;
7use crate::tools::tool_names;
8use schemars::JsonSchema;
9use serde::Deserialize;
10use serde_json::Value;
11use std::borrow::Cow;
12use std::sync::{Arc, Mutex, atomic::AtomicBool, mpsc};
13
14// ========== 核心类型 ==========
15
16pub use crate::message_types::PlanDecision;
17
18/// 图片数据,以 base64 编码存储
19#[derive(Debug, Clone)]
20pub struct ImageData {
21    /// 图片的 base64 编码数据
22    pub base64: String,
23    /// 图片的 MIME 媒体类型(如 "image/png")
24    pub media_type: String,
25}
26
27/// 工具执行结果,包含输出文本、错误标记、附加图片和计划决策
28#[derive(Debug)]
29pub struct ToolResult {
30    /// 工具执行的文本输出
31    pub output: String,
32    /// 是否为错误结果
33    pub is_error: bool,
34    /// 执行过程中产生的图片列表
35    pub images: Vec<ImageData>,
36    /// 计划模式下的决策结果
37    pub plan_decision: PlanDecision,
38}
39
40/// 工具核心接口,定义工具的名称、描述、参数模式和执行逻辑
41pub trait Tool: Send + Sync {
42    /// 返回工具名称
43    fn name(&self) -> &str;
44    /// 返回工具功能描述,静态描述返回 `Cow::Borrowed`,动态描述返回 `Cow::Owned`
45    fn description(&self) -> Cow<'_, str>;
46    /// 返回工具参数的 JSON Schema
47    fn parameters_schema(&self) -> Value;
48    /// 执行工具,传入参数字符串和取消信号,返回执行结果
49    fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult;
50    /// 该工具是否需要用户确认后才能执行
51    fn requires_confirmation(&self) -> bool {
52        false
53    }
54    /// 生成用户确认时显示的提示消息
55    fn confirmation_message(&self, arguments: &str) -> String {
56        format!("调用工具 {} 参数: {}", self.name(), arguments)
57    }
58    /// 工具是否当前可用(默认 `true`)。
59    ///
60    /// 返回 `false` 时,该工具不会出现在 LLM 的工具列表和工具摘要中,
61    /// 且直接调用会返回错误提示。
62    fn is_available(&self) -> bool {
63        true
64    }
65}
66
67/// 将实现了 `JsonSchema` 的类型转换为基础清理后的工具参数 JSON Schema,
68/// 自动内联所有 `$ref` 引用并移除 `$schema`、`title`、`definitions` 等冗余字段
69pub fn schema_to_tool_params<T: JsonSchema>() -> Value {
70    let root = schemars::schema_for!(T);
71    let mut v = serde_json::to_value(root).unwrap_or_default();
72
73    // Extract definitions before cleanup, then inline all $ref references
74    let definitions = v
75        .as_object()
76        .and_then(|o| o.get("definitions").cloned())
77        .and_then(|d| d.as_object().cloned());
78
79    if let Some(defs) = definitions {
80        inline_refs(&mut v, &defs);
81    }
82
83    if let Some(obj) = v.as_object_mut() {
84        obj.remove("$schema");
85        obj.remove("title");
86        obj.remove("definitions");
87    }
88    v
89}
90
91/// Recursively replace all `{"$ref": "#/definitions/X"}` with the inlined definition
92fn inline_refs(value: &mut Value, definitions: &serde_json::Map<String, Value>) {
93    match value {
94        Value::Object(map) => {
95            // If this object is a $ref, replace it entirely with the inlined definition
96            if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str())
97                && let Some(key) = ref_path.strip_prefix("#/definitions/")
98                && let Some(def) = definitions.get(key)
99            {
100                *value = def.clone();
101                // The inlined definition may itself contain $refs, so recurse
102                inline_refs(value, definitions);
103                return;
104            }
105            // Otherwise recurse into all values
106            for v in map.values_mut() {
107                inline_refs(v, definitions);
108            }
109        }
110        Value::Array(arr) => {
111            for v in arr.iter_mut() {
112                inline_refs(v, definitions);
113            }
114        }
115        _ => {}
116    }
117}
118
119/// 将 JSON 参数字符串解析为指定类型 `T`,解析失败时返回包含错误信息的 `ToolResult`
120pub fn parse_tool_args<T: for<'de> Deserialize<'de>>(arguments: &str) -> Result<T, ToolResult> {
121    serde_json::from_str::<T>(arguments).map_err(|e| ToolResult {
122        output: format!("参数解析失败: {}", e),
123        is_error: true,
124        images: vec![],
125        plan_decision: PlanDecision::None,
126    })
127}
128
129// ========== ToolRegistry ==========
130
131/// 工具注册中心,管理所有可用工具及其相关状态
132pub struct ToolRegistry {
133    tools: Vec<Box<dyn Tool>>,
134    /// 待办事项管理器
135    pub todo_manager: Arc<super::todo::TodoManager>,
136    /// 计划模式状态
137    pub plan_mode_state: Arc<super::plan::PlanModeState>,
138    /// 工作树状态(当前未使用)
139    #[allow(dead_code)]
140    pub worktree_state: Arc<super::worktree::WorktreeState>,
141    /// 权限请求队列
142    pub permission_queue: Option<Arc<PermissionQueue>>,
143    /// 计划审批队列
144    pub plan_approval_queue: Option<Arc<super::plan::PlanApprovalQueue>>,
145}
146
147impl std::fmt::Debug for ToolRegistry {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        let tool_names: Vec<&str> = self.tools.iter().map(|t| t.name()).collect();
150        f.debug_struct("ToolRegistry")
151            .field("tool_names", &tool_names)
152            .finish()
153    }
154}
155
156impl ToolRegistry {
157    /// 创建工具注册中心,初始化所有内置工具及相关状态
158    pub fn new(
159        skills: Vec<Skill>,
160        ask_tx: mpsc::Sender<AskRequest>,
161        background_manager: Arc<super::background::BackgroundManager>,
162        task_manager: Arc<super::task::TaskManager>,
163        hook_manager: Arc<Mutex<HookManager>>,
164        invoked_skills: InvokedSkillsMap,
165        todos_file_path: std::path::PathBuf,
166    ) -> Self {
167        let todo_manager = Arc::new(super::todo::TodoManager::new_with_file_path(
168            todos_file_path,
169        ));
170        let plan_mode_state = Arc::new(super::plan::PlanModeState::new());
171        let worktree_state = Arc::new(super::worktree::WorktreeState::new());
172        let plan_approval_queue = Arc::new(super::plan::PlanApprovalQueue::new());
173
174        let tools: Vec<Box<dyn Tool>> = vec![
175            #[cfg(unix)]
176            Box::new(super::shell::ShellTool {
177                manager: Arc::clone(&background_manager),
178            }),
179            #[cfg(windows)]
180            Box::new(super::powershell::PowerShellTool {
181                manager: Arc::clone(&background_manager),
182            }),
183            Box::new(super::file::ReadFileTool),
184            Box::new(super::file::WriteFileTool),
185            Box::new(super::file::EditFileTool),
186            Box::new(super::file::GlobTool),
187            Box::new(super::grep::GrepTool),
188            Box::new(super::web_fetch::WebFetchTool),
189            Box::new(super::web_search::WebSearchTool),
190            Box::new(super::browser::BrowserTool),
191            Box::new(super::ask::AskTool {
192                ask_tx: ask_tx.clone(),
193            }),
194            Box::new(super::background::TaskOutputTool {
195                manager: Arc::clone(&background_manager),
196            }),
197            Box::new(super::session::SessionTool {
198                manager: Arc::clone(&background_manager),
199            }),
200            Box::new(super::task::TaskTool {
201                manager: Arc::clone(&task_manager),
202            }),
203            Box::new(super::todo::TodoWriteTool {
204                manager: Arc::clone(&todo_manager),
205            }),
206            Box::new(super::todo::TodoReadTool {
207                manager: Arc::clone(&todo_manager),
208            }),
209            Box::new(super::compact_tool::CompactTool),
210            Box::new(super::hook::RegisterHookTool { hook_manager }),
211            #[cfg(target_os = "macos")]
212            Box::new(super::computer_use::ComputerUseTool::new()),
213            Box::new(super::plan::EnterPlanModeTool {
214                plan_state: Arc::clone(&plan_mode_state),
215            }),
216            Box::new(super::plan::ExitPlanModeTool {
217                plan_state: Arc::clone(&plan_mode_state),
218                ask_tx,
219                plan_approval_queue: Some(Arc::clone(&plan_approval_queue)),
220            }),
221            Box::new(super::worktree::EnterWorktreeTool {
222                state: Arc::clone(&worktree_state),
223            }),
224            Box::new(super::worktree::ExitWorktreeTool {
225                state: Arc::clone(&worktree_state),
226            }),
227        ];
228
229        let mut registry = Self {
230            todo_manager: Arc::clone(&todo_manager),
231            plan_mode_state: Arc::clone(&plan_mode_state),
232            worktree_state: Arc::clone(&worktree_state),
233            permission_queue: None,
234            plan_approval_queue: None,
235            tools,
236        };
237
238        if !skills.is_empty() {
239            registry.register(Box::new(super::skill::LoadSkillTool {
240                skills,
241                invoked_skills,
242            }));
243        }
244
245        registry
246    }
247
248    /// 注册一个新工具到注册中心
249    pub fn register(&mut self, tool: Box<dyn Tool>) {
250        self.tools.push(tool);
251    }
252
253    /// 根据名称获取工具的引用
254    pub fn get(&self, name: &str) -> Option<&dyn Tool> {
255        self.tools
256            .iter()
257            .find(|t| t.name() == name)
258            .map(|t| t.as_ref())
259    }
260
261    /// 执行指定名称的工具,自动处理计划模式下的权限限制
262    pub fn execute(&self, name: &str, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
263        let (is_active, plan_file_path) = self.plan_mode_state.get_state();
264        if is_active && !super::plan::is_allowed_in_plan_mode(name) {
265            let is_plan_file_write = (name == "Write" || name == "Edit") && {
266                if let Some(ref plan_path) = plan_file_path {
267                    serde_json::from_str::<serde_json::Value>(arguments)
268                        .ok()
269                        .and_then(|v| {
270                            v.get("path")
271                                .or_else(|| v.get("file_path"))
272                                .and_then(|p| p.as_str())
273                                .map(|p| {
274                                    let input_path = std::path::Path::new(p);
275                                    let plan_path_buf = std::path::Path::new(&plan_path);
276
277                                    if p == plan_path {
278                                        return true;
279                                    }
280
281                                    if input_path.is_relative()
282                                        && let Ok(cwd) = std::env::current_dir()
283                                    {
284                                        let absolute_path = cwd.join(input_path);
285                                        if let Ok(canonical_input) = absolute_path.canonicalize()
286                                            && let Ok(canonical_plan) = plan_path_buf.canonicalize()
287                                        {
288                                            return canonical_input == canonical_plan;
289                                        }
290                                    }
291
292                                    false
293                                })
294                        })
295                        .unwrap_or(false)
296                } else {
297                    false
298                }
299            };
300
301            if !is_plan_file_write {
302                return ToolResult {
303                    output: format!(
304                        "Tool '{}' is not available in plan mode. Only read-only tools are allowed. \
305                         Use ExitPlanMode to exit plan mode first.",
306                        name
307                    ),
308                    is_error: true,
309                    images: vec![],
310                    plan_decision: PlanDecision::None,
311                };
312            }
313        }
314
315        match self.get(name) {
316            Some(tool) => {
317                if !tool.is_available() {
318                    return ToolResult {
319                        output: format!("Tool '{}' is currently not available.", name),
320                        is_error: true,
321                        images: vec![],
322                        plan_decision: PlanDecision::None,
323                    };
324                }
325                tool.execute(arguments, cancelled)
326            }
327            None => ToolResult {
328                output: format!("未知工具: {}", name),
329                is_error: true,
330                images: vec![],
331                plan_decision: PlanDecision::None,
332            },
333        }
334    }
335
336    /// 构建工具摘要(排除 disabled 和 deferred 工具),用于 system prompt
337    pub fn build_tools_summary_non_deferred(
338        &self,
339        disabled: &[String],
340        deferred: &[String],
341    ) -> String {
342        let mut md = String::new();
343        for t in self
344            .tools
345            .iter()
346            .filter(|t| !disabled.iter().any(|d| d == t.name()))
347            .filter(|t| t.is_available())
348            .filter(|t| !deferred.iter().any(|d| d == t.name()))
349        {
350            let name = t.name();
351            md.push_str(&format!("<{}>\n", name));
352            let mut desc = dedent(t.description().trim());
353            if name == tool_names::LOAD_TOOL {
354                desc.push_str(&format_deferred_suffix(deferred));
355            }
356            md.push_str(&format!("description:\n{}\n", desc));
357            let params = json_schema_to_xml_params(&t.parameters_schema());
358            if !params.is_empty() {
359                md.push('\n');
360                md.push_str(&params);
361            }
362            md.push_str(&format!("</{}>\n\n", name));
363        }
364
365        md.trim_end().to_string()
366    }
367
368    /// 将未禁用、可用且非 deferred 的工具转换为 LLM 工具定义列表
369    /// LoadTool 始终包含在列表中,由调用方注入当前 deferred 工具列表到其描述末尾
370    pub fn to_llm_tools_non_deferred(
371        &self,
372        disabled: &[String],
373        deferred: &[String],
374    ) -> Vec<ToolDefinition> {
375        let mut tools: Vec<ToolDefinition> = self
376            .tools
377            .iter()
378            .filter(|t| !disabled.iter().any(|d| d == t.name()))
379            .filter(|t| t.is_available())
380            .filter(|t| !deferred.iter().any(|d| d == t.name()))
381            .map(|t| {
382                let mut desc = dedent(t.description().trim());
383                if t.name() == tool_names::LOAD_TOOL {
384                    desc.push_str(&format_deferred_suffix(deferred));
385                }
386                ToolDefinition {
387                    tool_type: "function".to_string(),
388                    function: FunctionObject {
389                        name: t.name().to_string(),
390                        description: Some(desc),
391                        parameters: Some(t.parameters_schema()),
392                        strict: None,
393                    },
394                }
395            })
396            .collect();
397
398        // LoadTool 始终加入列表(即使被列入 deferred 也保留入口,防止用户误配)。
399        // 描述末尾由调用方传入的 deferred 列表动态拼装。
400        // 若 registry 中无 LoadTool(如子 agent registry),此 if let 静默跳过——
401        // 子 agent 不支持动态加载,这是有意为之的降级行为。
402
403        if let Some(load_tool) = self
404            .tools
405            .iter()
406            .find(|t| t.name() == tool_names::LOAD_TOOL)
407            && load_tool.is_available()
408            && !disabled.iter().any(|d| d == load_tool.name())
409            && !tools
410                .iter()
411                .any(|t| t.function.name == tool_names::LOAD_TOOL)
412        {
413            let mut desc = dedent(load_tool.description().trim());
414            desc.push_str(&format_deferred_suffix(deferred));
415            tools.push(ToolDefinition {
416                tool_type: "function".to_string(),
417                function: FunctionObject {
418                    name: tool_names::LOAD_TOOL.to_string(),
419                    description: Some(desc),
420                    parameters: Some(load_tool.parameters_schema()),
421                    strict: None,
422                },
423            });
424        }
425
426        tools
427    }
428
429    /// 返回所有已注册工具的名称列表
430    pub fn tool_names(&self) -> Vec<&str> {
431        self.tools.iter().map(|t| t.name()).collect()
432    }
433
434    /// 构建会话状态摘要,包含计划模式和工作树等当前状态信息
435    pub fn build_session_state_summary(&self) -> String {
436        let mut parts = Vec::new();
437
438        let (plan_active, plan_file) = self.plan_mode_state.get_state();
439        if plan_active {
440            let mut s = String::from("## Session State: PLAN MODE\n\n");
441            s.push_str("You are currently in **Plan Mode**. Only read-only tools are available.\n");
442            s.push_str(
443                "Write your plan to the plan file, then use ExitPlanMode for user approval.\n",
444            );
445            if let Some(ref path) = plan_file {
446                s.push_str(&format!("Plan file: `{}`\n", path));
447            }
448            parts.push(s);
449        }
450
451        if let Some(session) = self.worktree_state.get_session() {
452            let mut s = String::from("## Session State: WORKTREE\n\n");
453            s.push_str("You are in an isolated git worktree.\n");
454            s.push_str(&format!("Branch: `{}`\n", session.branch));
455            s.push_str(&format!(
456                "Worktree path: `{}`\n",
457                session.worktree_path.display()
458            ));
459            s.push_str(&format!(
460                "Original cwd: `{}`\n",
461                session.original_cwd.display()
462            ));
463            parts.push(s);
464        }
465
466        if parts.is_empty() {
467            return String::new();
468        }
469        parts.join("\n")
470    }
471}
472
473/// 去除多行字符串每行的公共缩进,保留相对缩进结构。
474/// 空行或仅含空白字符的行被忽略在缩进计算中。
475fn dedent(s: &str) -> String {
476    let lines: Vec<&str> = s.lines().collect();
477    if lines.is_empty() {
478        return String::new();
479    }
480
481    // 找出非空行的最小公共缩进
482    let min_indent = lines
483        .iter()
484        .filter(|line| !line.trim().is_empty())
485        .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
486        .min()
487        .unwrap_or(0);
488
489    // 移除每行的公共缩进,空行保持为空
490    lines
491        .iter()
492        .map(|line| {
493            if line.trim().is_empty() {
494                String::new()
495            } else if line.len() >= min_indent {
496                line[min_indent..].to_string()
497            } else {
498                line.to_string()
499            }
500        })
501        .collect::<Vec<_>>()
502        .join("\n")
503}
504
505fn json_schema_to_xml_params(schema: &Value) -> String {
506    let properties = match schema.get("properties").and_then(|p| p.as_object()) {
507        Some(p) => p,
508        None => return String::new(),
509    };
510    let required: Vec<&str> = schema
511        .get("required")
512        .and_then(|r| r.as_array())
513        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
514        .unwrap_or_default();
515
516    let mut md = String::from("parameters:\n");
517    for (name, prop) in properties {
518        let type_str = prop
519            .get("type")
520            .and_then(|t| t.as_str())
521            .unwrap_or("string");
522        let desc = prop
523            .get("description")
524            .and_then(|d| d.as_str())
525            .unwrap_or("");
526        let req = if required.contains(&name.as_str()) {
527            ", required"
528        } else {
529            ""
530        };
531        md.push_str(&format!("- `{}` ({}{}) — {}\n", name, type_str, req, desc));
532    }
533    md
534}
535
536/// 把当前 deferred 工具列表格式化成 LoadTool 描述末尾的动态后缀。
537///
538/// 这部分文本本来是 `LoadTool::description()` 内部 lock `deferred_tools`
539/// 自己拼出来的,但那样会让一个本应静态的 trait 方法依赖运行时锁,
540/// 在外层 `build_tools_summary_non_deferred` / `to_llm_tools_non_deferred`
541/// 已经持锁的调用栈里再次 lock 同一 Mutex 时触发自死锁。
542///
543/// 现在的设计:调用方(已经持有 deferred 列表的副本)直接把后缀拼到
544/// LoadTool 的 description 输出末尾,`LoadTool::description()` 本身保持
545/// 静态、廉价、纯查询。
546fn format_deferred_suffix(deferred: &[String]) -> String {
547    if deferred.is_empty() {
548        "\n\nNo deferred tools available.".to_string()
549    } else {
550        format!("\n\nCurrently deferred tools: {}", deferred.join(", "))
551    }
552}
553
554#[cfg(test)]
555mod tests;