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
38pub use crate::message_types::PlanDecision;
41
42#[derive(Debug, Clone)]
44pub struct ImageData {
45 pub base64: String,
47 pub media_type: String,
49}
50
51#[derive(Debug)]
53pub struct ToolResult {
54 pub output: String,
56 pub is_error: bool,
58 pub images: Vec<ImageData>,
60 pub plan_decision: PlanDecision,
62}
63
64pub trait Tool: Send + Sync {
66 fn name(&self) -> &str;
68 fn description(&self) -> Cow<'_, str>;
70 fn parameters_schema(&self) -> Value;
72 fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult;
74 fn requires_confirmation(&self) -> bool {
76 false
77 }
78 fn confirmation_message(&self, arguments: &str) -> String {
80 format!("调用工具 {} 参数: {}", self.name(), arguments)
81 }
82 fn is_available(&self) -> bool {
87 true
88 }
89}
90
91pub 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 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
115fn inline_refs(value: &mut Value, definitions: &serde_json::Map<String, Value>) {
117 match value {
118 Value::Object(map) => {
119 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 inline_refs(value, definitions);
127 return;
128 }
129 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
143pub 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
153pub struct ToolRegistry {
157 tools: Vec<Box<dyn Tool>>,
158 pub todo_manager: Arc<TodoManager>,
160 pub plan_mode_state: Arc<PlanModeState>,
162 #[allow(dead_code)]
164 pub worktree_state: Arc<WorktreeState>,
165 pub permission_queue: Option<Arc<PermissionQueue>>,
167 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 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 pub fn register(&mut self, tool: Box<dyn Tool>) {
267 self.tools.push(tool);
268 }
269
270 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 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 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(¶ms);
378 }
379 md.push_str(&format!("</{}>\n\n", name));
380 }
381
382 md.trim_end().to_string()
383 }
384
385 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 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 pub fn tool_names(&self) -> Vec<&str> {
448 self.tools.iter().map(|t| t.name()).collect()
449 }
450
451 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
490fn dedent(s: &str) -> String {
493 let lines: Vec<&str> = s.lines().collect();
494 if lines.is_empty() {
495 return String::new();
496 }
497
498 let min_indent_bytes = lines
500 .iter()
501 .filter(|line| !line.trim().is_empty())
502 .map(|line| {
503 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 lines
514 .iter()
515 .map(|line| {
516 if line.trim().is_empty() || min_indent_bytes == 0 {
517 line.to_string()
518 } else {
519 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
567fn 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;