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
14pub use crate::message_types::PlanDecision;
17
18#[derive(Debug, Clone)]
20pub struct ImageData {
21 pub base64: String,
23 pub media_type: String,
25}
26
27#[derive(Debug)]
29pub struct ToolResult {
30 pub output: String,
32 pub is_error: bool,
34 pub images: Vec<ImageData>,
36 pub plan_decision: PlanDecision,
38}
39
40pub trait Tool: Send + Sync {
42 fn name(&self) -> &str;
44 fn description(&self) -> Cow<'_, str>;
46 fn parameters_schema(&self) -> Value;
48 fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult;
50 fn requires_confirmation(&self) -> bool {
52 false
53 }
54 fn confirmation_message(&self, arguments: &str) -> String {
56 format!("调用工具 {} 参数: {}", self.name(), arguments)
57 }
58 fn is_available(&self) -> bool {
63 true
64 }
65}
66
67pub 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 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
91fn inline_refs(value: &mut Value, definitions: &serde_json::Map<String, Value>) {
93 match value {
94 Value::Object(map) => {
95 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 inline_refs(value, definitions);
103 return;
104 }
105 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
119pub 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
129pub struct ToolRegistry {
133 tools: Vec<Box<dyn Tool>>,
134 pub todo_manager: Arc<super::todo::TodoManager>,
136 pub plan_mode_state: Arc<super::plan::PlanModeState>,
138 #[allow(dead_code)]
140 pub worktree_state: Arc<super::worktree::WorktreeState>,
141 pub permission_queue: Option<Arc<PermissionQueue>>,
143 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 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 pub fn register(&mut self, tool: Box<dyn Tool>) {
245 self.tools.push(tool);
246 }
247
248 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 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 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(¶ms);
356 }
357 md.push_str(&format!("</{}>\n\n", name));
358 }
359
360 md.trim_end().to_string()
361 }
362
363 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 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 pub fn tool_names(&self) -> Vec<&str> {
426 self.tools.iter().map(|t| t.name()).collect()
427 }
428
429 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
468fn dedent(s: &str) -> String {
471 let lines: Vec<&str> = s.lines().collect();
472 if lines.is_empty() {
473 return String::new();
474 }
475
476 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 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
531fn 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;