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            Box::new(super::shell::ShellTool {
176                manager: Arc::clone(&background_manager),
177            }),
178            Box::new(super::file::ReadFileTool),
179            Box::new(super::file::WriteFileTool),
180            Box::new(super::file::EditFileTool),
181            Box::new(super::file::GlobTool),
182            Box::new(super::grep::GrepTool),
183            Box::new(super::web_fetch::WebFetchTool),
184            Box::new(super::web_search::WebSearchTool),
185            Box::new(super::browser::BrowserTool),
186            Box::new(super::ask::AskTool {
187                ask_tx: ask_tx.clone(),
188            }),
189            Box::new(super::background::TaskOutputTool {
190                manager: Arc::clone(&background_manager),
191            }),
192            Box::new(super::session::SessionTool {
193                manager: Arc::clone(&background_manager),
194            }),
195            Box::new(super::task::TaskTool {
196                manager: Arc::clone(&task_manager),
197            }),
198            Box::new(super::todo::TodoWriteTool {
199                manager: Arc::clone(&todo_manager),
200            }),
201            Box::new(super::todo::TodoReadTool {
202                manager: Arc::clone(&todo_manager),
203            }),
204            Box::new(super::compact_tool::CompactTool),
205            Box::new(super::hook::RegisterHookTool { hook_manager }),
206            #[cfg(target_os = "macos")]
207            Box::new(super::computer_use::ComputerUseTool::new()),
208            Box::new(super::plan::EnterPlanModeTool {
209                plan_state: Arc::clone(&plan_mode_state),
210            }),
211            Box::new(super::plan::ExitPlanModeTool {
212                plan_state: Arc::clone(&plan_mode_state),
213                ask_tx,
214                plan_approval_queue: Some(Arc::clone(&plan_approval_queue)),
215            }),
216            Box::new(super::worktree::EnterWorktreeTool {
217                state: Arc::clone(&worktree_state),
218            }),
219            Box::new(super::worktree::ExitWorktreeTool {
220                state: Arc::clone(&worktree_state),
221            }),
222        ];
223
224        let mut registry = Self {
225            todo_manager: Arc::clone(&todo_manager),
226            plan_mode_state: Arc::clone(&plan_mode_state),
227            worktree_state: Arc::clone(&worktree_state),
228            permission_queue: None,
229            plan_approval_queue: None,
230            tools,
231        };
232
233        if !skills.is_empty() {
234            registry.register(Box::new(super::skill::LoadSkillTool {
235                skills,
236                invoked_skills,
237            }));
238        }
239
240        registry
241    }
242
243    /// 注册一个新工具到注册中心
244    pub fn register(&mut self, tool: Box<dyn Tool>) {
245        self.tools.push(tool);
246    }
247
248    /// 根据名称获取工具的引用
249    pub fn get(&self, name: &str) -> Option<&dyn Tool> {
250        self.tools
251            .iter()
252            .find(|t| t.name() == name)
253            .map(|t| t.as_ref())
254    }
255
256    /// 执行指定名称的工具,自动处理计划模式下的权限限制
257    pub fn execute(&self, name: &str, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
258        let (is_active, plan_file_path) = self.plan_mode_state.get_state();
259        if is_active && !super::plan::is_allowed_in_plan_mode(name) {
260            let is_plan_file_write = (name == "Write" || name == "Edit") && {
261                if let Some(ref plan_path) = plan_file_path {
262                    serde_json::from_str::<serde_json::Value>(arguments)
263                        .ok()
264                        .and_then(|v| {
265                            v.get("path")
266                                .or_else(|| v.get("file_path"))
267                                .and_then(|p| p.as_str())
268                                .map(|p| {
269                                    let input_path = std::path::Path::new(p);
270                                    let plan_path_buf = std::path::Path::new(&plan_path);
271
272                                    if p == plan_path {
273                                        return true;
274                                    }
275
276                                    if input_path.is_relative()
277                                        && let Ok(cwd) = std::env::current_dir()
278                                    {
279                                        let absolute_path = cwd.join(input_path);
280                                        if let Ok(canonical_input) = absolute_path.canonicalize()
281                                            && let Ok(canonical_plan) = plan_path_buf.canonicalize()
282                                        {
283                                            return canonical_input == canonical_plan;
284                                        }
285                                    }
286
287                                    false
288                                })
289                        })
290                        .unwrap_or(false)
291                } else {
292                    false
293                }
294            };
295
296            if !is_plan_file_write {
297                return ToolResult {
298                    output: format!(
299                        "Tool '{}' is not available in plan mode. Only read-only tools are allowed. \
300                         Use ExitPlanMode to exit plan mode first.",
301                        name
302                    ),
303                    is_error: true,
304                    images: vec![],
305                    plan_decision: PlanDecision::None,
306                };
307            }
308        }
309
310        match self.get(name) {
311            Some(tool) => {
312                if !tool.is_available() {
313                    return ToolResult {
314                        output: format!("Tool '{}' is currently not available.", name),
315                        is_error: true,
316                        images: vec![],
317                        plan_decision: PlanDecision::None,
318                    };
319                }
320                tool.execute(arguments, cancelled)
321            }
322            None => ToolResult {
323                output: format!("未知工具: {}", name),
324                is_error: true,
325                images: vec![],
326                plan_decision: PlanDecision::None,
327            },
328        }
329    }
330
331    /// 构建工具摘要(排除 disabled 和 deferred 工具),用于 system prompt
332    pub fn build_tools_summary_non_deferred(
333        &self,
334        disabled: &[String],
335        deferred: &[String],
336    ) -> String {
337        let mut md = String::new();
338        for t in self
339            .tools
340            .iter()
341            .filter(|t| !disabled.iter().any(|d| d == t.name()))
342            .filter(|t| t.is_available())
343            .filter(|t| !deferred.iter().any(|d| d == t.name()))
344        {
345            let name = t.name();
346            md.push_str(&format!("<{}>\n", name));
347            let mut desc = dedent(t.description().trim());
348            if name == tool_names::LOAD_TOOL {
349                desc.push_str(&format_deferred_suffix(deferred));
350            }
351            md.push_str(&format!("description:\n{}\n", desc));
352            let params = json_schema_to_xml_params(&t.parameters_schema());
353            if !params.is_empty() {
354                md.push('\n');
355                md.push_str(&params);
356            }
357            md.push_str(&format!("</{}>\n\n", name));
358        }
359
360        md.trim_end().to_string()
361    }
362
363    /// 将未禁用、可用且非 deferred 的工具转换为 LLM 工具定义列表
364    /// LoadTool 始终包含在列表中,由调用方注入当前 deferred 工具列表到其描述末尾
365    pub fn to_llm_tools_non_deferred(
366        &self,
367        disabled: &[String],
368        deferred: &[String],
369    ) -> Vec<ToolDefinition> {
370        let mut tools: Vec<ToolDefinition> = self
371            .tools
372            .iter()
373            .filter(|t| !disabled.iter().any(|d| d == t.name()))
374            .filter(|t| t.is_available())
375            .filter(|t| !deferred.iter().any(|d| d == t.name()))
376            .map(|t| {
377                let mut desc = dedent(t.description().trim());
378                if t.name() == tool_names::LOAD_TOOL {
379                    desc.push_str(&format_deferred_suffix(deferred));
380                }
381                ToolDefinition {
382                    tool_type: "function".to_string(),
383                    function: FunctionObject {
384                        name: t.name().to_string(),
385                        description: Some(desc),
386                        parameters: Some(t.parameters_schema()),
387                        strict: None,
388                    },
389                }
390            })
391            .collect();
392
393        // LoadTool 始终加入列表(即使被列入 deferred 也保留入口,防止用户误配)。
394        // 描述末尾由调用方传入的 deferred 列表动态拼装。
395        // 若 registry 中无 LoadTool(如子 agent registry),此 if let 静默跳过——
396        // 子 agent 不支持动态加载,这是有意为之的降级行为。
397
398        if let Some(load_tool) = self
399            .tools
400            .iter()
401            .find(|t| t.name() == tool_names::LOAD_TOOL)
402            && load_tool.is_available()
403            && !disabled.iter().any(|d| d == load_tool.name())
404            && !tools
405                .iter()
406                .any(|t| t.function.name == tool_names::LOAD_TOOL)
407        {
408            let mut desc = dedent(load_tool.description().trim());
409            desc.push_str(&format_deferred_suffix(deferred));
410            tools.push(ToolDefinition {
411                tool_type: "function".to_string(),
412                function: FunctionObject {
413                    name: tool_names::LOAD_TOOL.to_string(),
414                    description: Some(desc),
415                    parameters: Some(load_tool.parameters_schema()),
416                    strict: None,
417                },
418            });
419        }
420
421        tools
422    }
423
424    /// 返回所有已注册工具的名称列表
425    pub fn tool_names(&self) -> Vec<&str> {
426        self.tools.iter().map(|t| t.name()).collect()
427    }
428
429    /// 构建会话状态摘要,包含计划模式和工作树等当前状态信息
430    pub fn build_session_state_summary(&self) -> String {
431        let mut parts = Vec::new();
432
433        let (plan_active, plan_file) = self.plan_mode_state.get_state();
434        if plan_active {
435            let mut s = String::from("## Session State: PLAN MODE\n\n");
436            s.push_str("You are currently in **Plan Mode**. Only read-only tools are available.\n");
437            s.push_str(
438                "Write your plan to the plan file, then use ExitPlanMode for user approval.\n",
439            );
440            if let Some(ref path) = plan_file {
441                s.push_str(&format!("Plan file: `{}`\n", path));
442            }
443            parts.push(s);
444        }
445
446        if let Some(session) = self.worktree_state.get_session() {
447            let mut s = String::from("## Session State: WORKTREE\n\n");
448            s.push_str("You are in an isolated git worktree.\n");
449            s.push_str(&format!("Branch: `{}`\n", session.branch));
450            s.push_str(&format!(
451                "Worktree path: `{}`\n",
452                session.worktree_path.display()
453            ));
454            s.push_str(&format!(
455                "Original cwd: `{}`\n",
456                session.original_cwd.display()
457            ));
458            parts.push(s);
459        }
460
461        if parts.is_empty() {
462            return String::new();
463        }
464        parts.join("\n")
465    }
466}
467
468/// 去除多行字符串每行的公共缩进,保留相对缩进结构。
469/// 空行或仅含空白字符的行被忽略在缩进计算中。
470fn dedent(s: &str) -> String {
471    let lines: Vec<&str> = s.lines().collect();
472    if lines.is_empty() {
473        return String::new();
474    }
475
476    // 找出非空行的最小公共缩进
477    let min_indent = lines
478        .iter()
479        .filter(|line| !line.trim().is_empty())
480        .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
481        .min()
482        .unwrap_or(0);
483
484    // 移除每行的公共缩进,空行保持为空
485    lines
486        .iter()
487        .map(|line| {
488            if line.trim().is_empty() {
489                String::new()
490            } else if line.len() >= min_indent {
491                line[min_indent..].to_string()
492            } else {
493                line.to_string()
494            }
495        })
496        .collect::<Vec<_>>()
497        .join("\n")
498}
499
500fn json_schema_to_xml_params(schema: &Value) -> String {
501    let properties = match schema.get("properties").and_then(|p| p.as_object()) {
502        Some(p) => p,
503        None => return String::new(),
504    };
505    let required: Vec<&str> = schema
506        .get("required")
507        .and_then(|r| r.as_array())
508        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
509        .unwrap_or_default();
510
511    let mut md = String::from("parameters:\n");
512    for (name, prop) in properties {
513        let type_str = prop
514            .get("type")
515            .and_then(|t| t.as_str())
516            .unwrap_or("string");
517        let desc = prop
518            .get("description")
519            .and_then(|d| d.as_str())
520            .unwrap_or("");
521        let req = if required.contains(&name.as_str()) {
522            ", required"
523        } else {
524            ""
525        };
526        md.push_str(&format!("- `{}` ({}{}) — {}\n", name, type_str, req, desc));
527    }
528    md
529}
530
531/// 把当前 deferred 工具列表格式化成 LoadTool 描述末尾的动态后缀。
532///
533/// 这部分文本本来是 `LoadTool::description()` 内部 lock `deferred_tools`
534/// 自己拼出来的,但那样会让一个本应静态的 trait 方法依赖运行时锁,
535/// 在外层 `build_tools_summary_non_deferred` / `to_llm_tools_non_deferred`
536/// 已经持锁的调用栈里再次 lock 同一 Mutex 时触发自死锁。
537///
538/// 现在的设计:调用方(已经持有 deferred 列表的副本)直接把后缀拼到
539/// LoadTool 的 description 输出末尾,`LoadTool::description()` 本身保持
540/// 静态、廉价、纯查询。
541fn format_deferred_suffix(deferred: &[String]) -> String {
542    if deferred.is_empty() {
543        "\n\nNo deferred tools available.".to_string()
544    } else {
545        format!("\n\nCurrently deferred tools: {}", deferred.join(", "))
546    }
547}
548
549#[cfg(test)]
550mod tests;