1use std::sync::Arc;
3use serde_json::Value;
4use std::collections::{HashMap, HashSet};
5use crate::tools::Tool;
6
7#[derive(Clone)]
10pub struct ToolRegistry {
11 tools: HashMap<String, Arc<dyn Tool>>,
12 cached_schema: Arc<Vec<Value>>,
14 api_to_runtime_names: HashMap<String, String>,
16 input_name_maps: HashMap<String, SchemaNameMap>,
18}
19
20#[derive(Clone, Debug, Default)]
21struct SchemaNameMap {
22 api_to_runtime: HashMap<String, String>,
23 children: HashMap<String, SchemaNameMap>,
24 items: Option<Box<SchemaNameMap>>,
26}
27
28impl Default for ToolRegistry {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl ToolRegistry {
35 pub fn new() -> Self {
36 let tools: Vec<Arc<dyn Tool>> = vec![
37 Arc::new(crate::tools::bash::BashTool),
38 Arc::new(crate::tools::read::ReadTool),
39 Arc::new(crate::tools::write::WriteTool),
40 Arc::new(crate::tools::edit::EditTool),
41 Arc::new(crate::tools::grep::GrepTool),
42 Arc::new(crate::tools::find::FindTool),
43 Arc::new(crate::tools::ls::LsTool),
44 Arc::new(crate::tools::subagent::SubagentTool),
45 Arc::new(crate::tools::subagent::start::SubagentStartTool),
46 Arc::new(crate::tools::subagent::status::SubagentStatusTool),
47 Arc::new(crate::tools::subagent::steer::SubagentSteerTool),
48 Arc::new(crate::tools::subagent::collect::SubagentCollectTool),
49 Arc::new(crate::tools::subagent::resume::SubagentResumeTool),
50 Arc::new(crate::tools::shell::ShellStartTool),
51 Arc::new(crate::tools::shell::ShellSendTool),
52 Arc::new(crate::tools::shell::ShellEndTool),
53 ];
54 Self::from_tools(tools)
55 }
56
57 pub fn empty() -> Self {
60 Self::from_tools(Vec::new())
61 }
62
63 pub fn disable(&mut self, names: &[String]) {
66 if names.is_empty() {
67 return;
68 }
69 let remaining: Vec<Arc<dyn Tool>> = self
70 .tools
71 .values()
72 .filter(|t| !names.iter().any(|n| n == t.name()))
73 .cloned()
74 .collect();
75 *self = Self::from_tools(remaining);
76 }
77
78 pub fn without_subagent() -> Self {
80 let tools: Vec<Arc<dyn Tool>> = vec![
81 Arc::new(crate::tools::bash::BashTool),
82 Arc::new(crate::tools::read::ReadTool),
83 Arc::new(crate::tools::write::WriteTool),
84 Arc::new(crate::tools::edit::EditTool),
85 Arc::new(crate::tools::grep::GrepTool),
86 Arc::new(crate::tools::find::FindTool),
87 Arc::new(crate::tools::ls::LsTool),
88 Arc::new(crate::tools::shell::ShellStartTool),
89 Arc::new(crate::tools::shell::ShellSendTool),
90 Arc::new(crate::tools::shell::ShellEndTool),
91 ];
92 Self::from_tools(tools)
93 }
94
95 pub fn without_subagent_with_extensions(extension_tools: &ToolRegistry) -> Self {
103 let mut combined = Self::without_subagent();
104 for tool in extension_tools.tools.values() {
105 if tool.extension_id().is_some() {
106 combined.tools.insert(tool.name().to_string(), tool.clone());
107 }
108 }
109 combined.rebuild_schema();
110 combined
111 }
112
113 fn from_tools(tool_list: Vec<Arc<dyn Tool>>) -> Self {
114 let mut registry = ToolRegistry {
115 tools: HashMap::new(),
116 cached_schema: Arc::new(Vec::new()),
117 api_to_runtime_names: HashMap::new(),
118 input_name_maps: HashMap::new(),
119 };
120 for tool in tool_list {
124 let name = tool.name().to_string();
125 registry.tools.insert(name, tool);
126 }
127 registry.rebuild_schema();
128 registry
129 }
130
131 fn api_safe_name(name: &str, used: &HashSet<String>) -> String {
132 Self::api_safe_identifier(name, used, 128, false)
133 }
134
135 fn api_safe_property_name(name: &str, used: &HashSet<String>) -> String {
136 Self::api_safe_identifier(name, used, 64, true)
137 }
138
139 fn api_safe_identifier(name: &str, used: &HashSet<String>, max_len: usize, allow_dot: bool) -> String {
140 let mut sanitized = String::with_capacity(name.len());
141 for ch in name.chars() {
142 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || (allow_dot && ch == '.') {
143 sanitized.push(ch);
144 } else {
145 sanitized.push('_');
146 }
147 }
148 if sanitized.is_empty() {
149 sanitized.push_str("field");
150 }
151 if sanitized.len() > max_len {
152 sanitized.truncate(max_len);
153 }
154
155 let base = sanitized.clone();
156 let mut suffix = 2;
157 while used.contains(&sanitized) {
158 let suffix_str = format!("_{suffix}");
159 let keep = max_len.saturating_sub(suffix_str.len());
160 sanitized = format!("{}{}", &base[..base.len().min(keep)], suffix_str);
161 suffix += 1;
162 }
163 sanitized
164 }
165
166 fn sanitize_schema(mut schema: Value) -> (Value, SchemaNameMap) {
167 let mut map = SchemaNameMap::default();
168 let Some(obj) = schema.as_object_mut() else {
169 return (schema, map);
170 };
171
172 let mut required_name_map = HashMap::new();
173 if let Some(props_value) = obj.get_mut("properties") {
174 if let Some(props) = props_value.as_object_mut() {
175 let original = std::mem::take(props);
176 let mut used = HashSet::new();
177 for (runtime_name, child_schema) in original {
178 let api_name = Self::api_safe_property_name(&runtime_name, &used);
179 used.insert(api_name.clone());
180 required_name_map.insert(runtime_name.clone(), api_name.clone());
181 map.api_to_runtime.insert(api_name.clone(), runtime_name);
182
183 let (sanitized_child, child_map) = Self::sanitize_schema(child_schema);
184 if !child_map.api_to_runtime.is_empty() || !child_map.children.is_empty() {
185 map.children.insert(api_name.clone(), child_map);
186 }
187 props.insert(api_name, sanitized_child);
188 }
189 }
190 }
191
192 if let Some(required) = obj.get_mut("required").and_then(Value::as_array_mut) {
193 for item in required.iter_mut() {
194 if let Some(name) = item.as_str() {
195 if let Some(api_name) = required_name_map.get(name) {
196 *item = Value::String(api_name.clone());
197 }
198 }
199 }
200 }
201
202 if let Some(items) = obj.get_mut("items") {
205 let (sanitized_items, items_map) = Self::sanitize_schema(std::mem::take(items));
206 if !items_map.api_to_runtime.is_empty() || !items_map.children.is_empty() || items_map.items.is_some() {
207 map.items = Some(Box::new(items_map));
208 }
209 *items = sanitized_items;
210 }
211
212 (schema, map)
213 }
214
215 fn translate_input_names(input: Value, map: &SchemaNameMap) -> Value {
216 match input {
217 Value::Object(obj) => {
218 let mut out = serde_json::Map::new();
219 for (api_name, value) in obj {
220 let runtime_name = map.api_to_runtime.get(&api_name).cloned().unwrap_or_else(|| api_name.clone());
221 let value = if let Some(child) = map.children.get(&api_name) {
222 Self::translate_input_names(value, child)
223 } else {
224 value
225 };
226 out.insert(runtime_name, value);
227 }
228 Value::Object(out)
229 }
230 Value::Array(arr) => {
231 if let Some(items_map) = &map.items {
233 Value::Array(arr.into_iter().map(|v| Self::translate_input_names(v, items_map)).collect())
234 } else {
235 Value::Array(arr)
236 }
237 }
238 other => other,
239 }
240 }
241
242 fn rebuild_schema(&mut self) {
243 let mut used = HashSet::new();
244 let mut api_to_runtime_names = HashMap::new();
245 let mut input_name_maps = HashMap::new();
246 let mut schema = Vec::with_capacity(self.tools.len());
247
248 let mut sorted_tools: Vec<_> = self.tools.values().collect();
252 sorted_tools.sort_by_key(|t| t.name().to_string());
253
254 for tool in sorted_tools {
255 let runtime_name = tool.name();
256 let api_name = Self::api_safe_name(runtime_name, &used);
257 used.insert(api_name.clone());
258 api_to_runtime_names.insert(api_name.clone(), runtime_name.to_string());
259 let (input_schema, input_map) = Self::sanitize_schema(tool.parameters());
260 input_name_maps.insert(api_name.clone(), input_map);
261 schema.push(serde_json::json!({
262 "name": api_name,
263 "description": tool.description(),
264 "input_schema": input_schema
265 }));
266 }
267
268 self.api_to_runtime_names = api_to_runtime_names;
269 self.input_name_maps = input_name_maps;
270 self.cached_schema = Arc::new(schema);
271 }
272
273 pub fn register(&mut self, tool: Arc<dyn Tool>) {
276 let name = tool.name().to_string();
277 self.tools.insert(name, tool);
278 self.rebuild_schema();
279 }
280
281 pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
282 let runtime_name = self.api_to_runtime_names.get(name).map(String::as_str).unwrap_or(name);
283 self.tools.get(runtime_name)
284 }
285
286 pub fn runtime_name_for_api<'a>(&'a self, name: &'a str) -> &'a str {
287 self.api_to_runtime_names.get(name).map(String::as_str).unwrap_or(name)
288 }
289
290 pub fn translate_input_for_api_tool(&self, tool_name: &str, input: Value) -> Value {
291 if let Some(map) = self.input_name_maps.get(tool_name) {
292 Self::translate_input_names(input, map)
293 } else {
294 input
295 }
296 }
297
298 pub fn tools_schema(&self) -> Arc<Vec<Value>> {
299 Arc::clone(&self.cached_schema)
300 }
301
302 pub fn tool_names_for_extension(&self, extension_id: &str) -> Vec<String> {
305 let mut names: Vec<String> = self
306 .tools
307 .values()
308 .filter(|t| t.extension_id() == Some(extension_id))
309 .map(|t| t.name().to_string())
310 .collect();
311 names.sort();
312 names
313 }
314
315 pub fn iter_tools_sorted(&self) -> Vec<&Arc<dyn Tool>> {
318 let mut tools: Vec<_> = self.tools.values().collect();
319 tools.sort_by_key(|t| t.name());
320 tools
321 }
322}
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::{Result, ToolContext};
327 use serde_json::json;
328
329 struct NamedTool(&'static str);
330
331 #[async_trait::async_trait]
332 impl Tool for NamedTool {
333 fn name(&self) -> &str { self.0 }
334 fn description(&self) -> &str { "test tool" }
335 fn parameters(&self) -> Value { json!({"type": "object"}) }
336 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
337 Ok("ok".to_string())
338 }
339 }
340
341
342
343 struct SchemaTool;
344
345 #[async_trait::async_trait]
346 impl Tool for SchemaTool {
347 fn name(&self) -> &str { "schema_tool" }
348 fn description(&self) -> &str { "schema tool" }
349 fn parameters(&self) -> Value {
350 json!({
351 "type": "object",
352 "properties": {
353 "bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going": {"type": "string"},
354 "nested:obj": {
355 "type": "object",
356 "properties": {"inner/key": {"type": "string"}},
357 "required": ["inner/key"]
358 }
359 },
360 "required": [
361 "bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going",
362 "nested:obj"
363 ]
364 })
365 }
366 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
367 Ok("ok".to_string())
368 }
369 }
370
371 #[test]
372 fn tool_schema_uses_api_safe_names_and_maps_back() {
373 let registry = ToolRegistry::from_tools(vec![Arc::new(NamedTool("plugin:skill.tool"))]);
374
375 assert_eq!(registry.tools_schema()[0]["name"], "plugin_skill_tool");
376 assert!(registry.get("plugin:skill.tool").is_some());
377 assert!(registry.get("plugin_skill_tool").is_some());
378 assert_eq!(registry.runtime_name_for_api("plugin_skill_tool"), "plugin:skill.tool");
379 }
380
381 #[test]
382 fn tool_schema_disambiguates_sanitized_name_collisions() {
383 let registry = ToolRegistry::from_tools(vec![
384 Arc::new(NamedTool("a:b")),
385 Arc::new(NamedTool("a.b")),
386 ]);
387 let names: HashSet<String> = registry.tools_schema().iter()
388 .filter_map(|s| s["name"].as_str().map(str::to_string))
389 .collect();
390
391 assert_eq!(names.len(), 2);
392 assert!(names.contains("a_b"));
393 assert!(names.contains("a_b_2"));
394 assert!(registry.get("a_b").is_some());
395 assert!(registry.get("a_b_2").is_some());
396 }
397
398 #[test]
399 fn tool_schema_truncates_long_names_to_anthropic_limit() {
400 let long = "x".repeat(140);
401 let leaked: &'static str = Box::leak(long.into_boxed_str());
402 let registry = ToolRegistry::from_tools(vec![Arc::new(NamedTool(leaked))]);
403 let schema = registry.tools_schema();
404 let name = schema[0]["name"].as_str().unwrap();
405
406 assert_eq!(name.len(), 128);
407 assert!(name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'));
408 assert!(registry.get(name).is_some());
409 }
410
411 #[test]
412 fn tool_schema_sanitizes_input_property_names_and_translates_inputs_back() {
413 let registry = ToolRegistry::from_tools(vec![Arc::new(SchemaTool)]);
414 let schema = registry.tools_schema();
415 let input_schema = &schema[0]["input_schema"];
416 let props = input_schema["properties"].as_object().unwrap();
417
418 assert!(props.keys().all(|k| k.len() <= 64 && k.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')));
419 assert_eq!(input_schema["required"].as_array().unwrap()[0].as_str().unwrap().len(), 64);
420 assert_eq!(input_schema["required"][1], "nested_obj");
421 assert!(props["nested_obj"]["properties"].as_object().unwrap().contains_key("inner_key"));
422 assert_eq!(props["nested_obj"]["required"][0], "inner_key");
423
424 let first_required = input_schema["required"][0].as_str().unwrap();
425 let translated = registry.translate_input_for_api_tool("schema_tool", json!({
426 first_required: "value",
427 "nested_obj": {"inner_key": "nested"}
428 }));
429
430 assert_eq!(translated["bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going"], "value");
431 assert_eq!(translated["nested:obj"]["inner/key"], "nested");
432 }
433
434 #[test]
435 fn test_tool_registry_new() {
436 let registry = ToolRegistry::new();
437
438 assert_eq!(registry.tools_schema().len(), 16);
440
441 assert!(registry.get("bash").is_some());
443
444 assert!(registry.get("nonexistent").is_none());
446
447 assert!(registry.get("bash").is_some());
449 assert!(registry.get("read").is_some());
450 assert!(registry.get("write").is_some());
451 assert!(registry.get("edit").is_some());
452 assert!(registry.get("grep").is_some());
453 assert!(registry.get("find").is_some());
454 assert!(registry.get("ls").is_some());
455 assert!(registry.get("subagent").is_some());
456 }
457
458 #[test]
459 fn test_disable_removes_tools_from_registry_and_schema() {
460 let mut registry = ToolRegistry::new();
461 let before = registry.tools_schema().len();
462
463 registry.disable(&["bash".to_string(), "ls".to_string()]);
464
465 assert!(registry.get("bash").is_none());
467 assert!(registry.get("ls").is_none());
468 assert!(registry.get("read").is_some());
470 assert!(registry.get("subagent").is_some());
471 assert_eq!(registry.tools_schema().len(), before - 2);
473 }
474
475 #[test]
476 fn test_disable_empty_and_unknown_are_noops() {
477 let mut registry = ToolRegistry::new();
478 let n = registry.tools_schema().len();
479 registry.disable(&[]); assert_eq!(registry.tools_schema().len(), n);
481 registry.disable(&["not_a_real_tool".to_string()]); assert_eq!(registry.tools_schema().len(), n);
483 assert!(registry.get("bash").is_some());
484 }
485
486 #[test]
487 fn test_tool_registry_without_subagent() {
488 let registry = ToolRegistry::without_subagent();
489
490 assert_eq!(registry.tools_schema().len(), 10);
492 assert!(registry.get("subagent").is_none());
494
495 assert!(registry.get("bash").is_some());
497
498 assert!(registry.get("bash").is_some());
500 assert!(registry.get("read").is_some());
501 assert!(registry.get("write").is_some());
502 assert!(registry.get("edit").is_some());
503 assert!(registry.get("grep").is_some());
504 assert!(registry.get("find").is_some());
505 assert!(registry.get("ls").is_some());
506 }
507
508 #[test]
509 fn test_tool_registry_register() {
510 let mut registry = ToolRegistry::without_subagent();
511 let initial_count = registry.tools_schema().len();
512
513 struct TestTool;
515 #[async_trait::async_trait]
516 impl Tool for TestTool {
517 fn name(&self) -> &str { "test_tool" }
518 fn description(&self) -> &str { "A test tool" }
519 fn parameters(&self) -> Value { json!({"type": "object"}) }
520 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
521 Ok("test result".to_string())
522 }
523 }
524
525 registry.register(Arc::new(TestTool));
526
527 assert_eq!(registry.tools_schema().len(), initial_count + 1);
529
530 assert!(registry.get("test_tool").is_some());
532 }
533
534 #[test]
535 fn tool_names_for_extension_filters_by_owner_and_sorts() {
536 struct OwnedTool(&'static str, Option<&'static str>);
537 #[async_trait::async_trait]
538 impl Tool for OwnedTool {
539 fn name(&self) -> &str { self.0 }
540 fn description(&self) -> &str { "owned" }
541 fn parameters(&self) -> Value { json!({"type": "object"}) }
542 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
543 Ok("ok".to_string())
544 }
545 fn extension_id(&self) -> Option<&str> { self.1 }
546 }
547
548 let mut registry = ToolRegistry::without_subagent();
549 registry.register(Arc::new(OwnedTool("alpha:zed", Some("alpha"))));
550 registry.register(Arc::new(OwnedTool("alpha:bar", Some("alpha"))));
551 registry.register(Arc::new(OwnedTool("beta:thing", Some("beta"))));
552
553 assert_eq!(
554 registry.tool_names_for_extension("alpha"),
555 vec!["alpha:bar".to_string(), "alpha:zed".to_string()]
556 );
557 assert_eq!(
558 registry.tool_names_for_extension("beta"),
559 vec!["beta:thing".to_string()]
560 );
561 assert!(registry.tool_names_for_extension("ghost").is_empty());
562 assert!(registry.tool_names_for_extension("bash").is_empty());
564 }
565
566 struct OwnedTool(&'static str, Option<&'static str>);
567 #[async_trait::async_trait]
568 impl Tool for OwnedTool {
569 fn name(&self) -> &str { self.0 }
570 fn description(&self) -> &str { "owned" }
571 fn parameters(&self) -> Value { json!({"type": "object"}) }
572 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
573 Ok("ok".to_string())
574 }
575 fn extension_id(&self) -> Option<&str> { self.1 }
576 }
577
578 #[test]
579 fn without_subagent_excludes_subagent_tools() {
580 let registry = ToolRegistry::without_subagent();
581 assert!(registry.get("subagent").is_none());
582 assert!(registry.get("subagent_start").is_none());
583 assert!(registry.get("subagent_status").is_none());
584 assert!(registry.get("subagent_steer").is_none());
585 assert!(registry.get("subagent_collect").is_none());
586 assert!(registry.get("subagent_resume").is_none());
587 assert!(registry.get("bash").is_some());
589 assert!(registry.get("read").is_some());
590 }
591
592 #[test]
593 fn without_subagent_with_extensions_includes_extension_tools() {
594 let mut other = ToolRegistry::empty();
595 other.register(Arc::new(OwnedTool("alpha:do_thing", Some("alpha"))));
596
597 let merged = ToolRegistry::without_subagent_with_extensions(&other);
598
599 assert!(merged.get("alpha:do_thing").is_some());
601 assert!(merged.get("bash").is_some());
603 assert!(merged.get("read").is_some());
604 assert!(merged.get("subagent_start").is_none());
606 }
607
608 #[test]
609 fn without_subagent_with_extensions_excludes_built_ins_from_other_registry() {
610 let other = ToolRegistry::new();
615
616 let merged = ToolRegistry::without_subagent_with_extensions(&other);
617
618 let bash = merged.get("bash").expect("bash present");
620 assert!(bash.extension_id().is_none());
621 assert!(merged.get("subagent_start").is_none());
623 assert!(merged.get("subagent").is_none());
624 }
625
626 #[test]
627 fn without_subagent_with_extensions_does_not_overwrite_existing_builtin() {
628 let mut other = ToolRegistry::empty();
634 other.register(Arc::new(OwnedTool("ext:custom", Some("ext"))));
635
636 let merged = ToolRegistry::without_subagent_with_extensions(&other);
637 assert!(merged.get("ext:custom").is_some());
638 assert!(merged.get("bash").is_some());
639 assert!(merged.get("bash").unwrap().extension_id().is_none());
640 }
641}