Skip to main content

j_agent/tools/
definition.rs

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