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 #[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 pub fn register(&mut self, tool: Box<dyn Tool>) {
250 self.tools.push(tool);
251 }
252
253 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 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 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(¶ms);
361 }
362 md.push_str(&format!("</{}>\n\n", name));
363 }
364
365 md.trim_end().to_string()
366 }
367
368 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 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 pub fn tool_names(&self) -> Vec<&str> {
431 self.tools.iter().map(|t| t.name()).collect()
432 }
433
434 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
473fn dedent(s: &str) -> String {
476 let lines: Vec<&str> = s.lines().collect();
477 if lines.is_empty() {
478 return String::new();
479 }
480
481 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 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
536fn 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;