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#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::{Result, ToolContext};
319 use serde_json::json;
320
321 struct NamedTool(&'static str);
322
323 #[async_trait::async_trait]
324 impl Tool for NamedTool {
325 fn name(&self) -> &str { self.0 }
326 fn description(&self) -> &str { "test tool" }
327 fn parameters(&self) -> Value { json!({"type": "object"}) }
328 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
329 Ok("ok".to_string())
330 }
331 }
332
333
334
335 struct SchemaTool;
336
337 #[async_trait::async_trait]
338 impl Tool for SchemaTool {
339 fn name(&self) -> &str { "schema_tool" }
340 fn description(&self) -> &str { "schema tool" }
341 fn parameters(&self) -> Value {
342 json!({
343 "type": "object",
344 "properties": {
345 "bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going": {"type": "string"},
346 "nested:obj": {
347 "type": "object",
348 "properties": {"inner/key": {"type": "string"}},
349 "required": ["inner/key"]
350 }
351 },
352 "required": [
353 "bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going",
354 "nested:obj"
355 ]
356 })
357 }
358 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
359 Ok("ok".to_string())
360 }
361 }
362
363 #[test]
364 fn tool_schema_uses_api_safe_names_and_maps_back() {
365 let registry = ToolRegistry::from_tools(vec![Arc::new(NamedTool("plugin:skill.tool"))]);
366
367 assert_eq!(registry.tools_schema()[0]["name"], "plugin_skill_tool");
368 assert!(registry.get("plugin:skill.tool").is_some());
369 assert!(registry.get("plugin_skill_tool").is_some());
370 assert_eq!(registry.runtime_name_for_api("plugin_skill_tool"), "plugin:skill.tool");
371 }
372
373 #[test]
374 fn tool_schema_disambiguates_sanitized_name_collisions() {
375 let registry = ToolRegistry::from_tools(vec![
376 Arc::new(NamedTool("a:b")),
377 Arc::new(NamedTool("a.b")),
378 ]);
379 let names: HashSet<String> = registry.tools_schema().iter()
380 .filter_map(|s| s["name"].as_str().map(str::to_string))
381 .collect();
382
383 assert_eq!(names.len(), 2);
384 assert!(names.contains("a_b"));
385 assert!(names.contains("a_b_2"));
386 assert!(registry.get("a_b").is_some());
387 assert!(registry.get("a_b_2").is_some());
388 }
389
390 #[test]
391 fn tool_schema_truncates_long_names_to_anthropic_limit() {
392 let long = "x".repeat(140);
393 let leaked: &'static str = Box::leak(long.into_boxed_str());
394 let registry = ToolRegistry::from_tools(vec![Arc::new(NamedTool(leaked))]);
395 let schema = registry.tools_schema();
396 let name = schema[0]["name"].as_str().unwrap();
397
398 assert_eq!(name.len(), 128);
399 assert!(name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'));
400 assert!(registry.get(name).is_some());
401 }
402
403 #[test]
404 fn tool_schema_sanitizes_input_property_names_and_translates_inputs_back() {
405 let registry = ToolRegistry::from_tools(vec![Arc::new(SchemaTool)]);
406 let schema = registry.tools_schema();
407 let input_schema = &schema[0]["input_schema"];
408 let props = input_schema["properties"].as_object().unwrap();
409
410 assert!(props.keys().all(|k| k.len() <= 64 && k.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')));
411 assert_eq!(input_schema["required"].as_array().unwrap()[0].as_str().unwrap().len(), 64);
412 assert_eq!(input_schema["required"][1], "nested_obj");
413 assert!(props["nested_obj"]["properties"].as_object().unwrap().contains_key("inner_key"));
414 assert_eq!(props["nested_obj"]["required"][0], "inner_key");
415
416 let first_required = input_schema["required"][0].as_str().unwrap();
417 let translated = registry.translate_input_for_api_tool("schema_tool", json!({
418 first_required: "value",
419 "nested_obj": {"inner_key": "nested"}
420 }));
421
422 assert_eq!(translated["bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going"], "value");
423 assert_eq!(translated["nested:obj"]["inner/key"], "nested");
424 }
425
426 #[test]
427 fn test_tool_registry_new() {
428 let registry = ToolRegistry::new();
429
430 assert_eq!(registry.tools_schema().len(), 16);
432
433 assert!(registry.get("bash").is_some());
435
436 assert!(registry.get("nonexistent").is_none());
438
439 assert!(registry.get("bash").is_some());
441 assert!(registry.get("read").is_some());
442 assert!(registry.get("write").is_some());
443 assert!(registry.get("edit").is_some());
444 assert!(registry.get("grep").is_some());
445 assert!(registry.get("find").is_some());
446 assert!(registry.get("ls").is_some());
447 assert!(registry.get("subagent").is_some());
448 }
449
450 #[test]
451 fn test_disable_removes_tools_from_registry_and_schema() {
452 let mut registry = ToolRegistry::new();
453 let before = registry.tools_schema().len();
454
455 registry.disable(&["bash".to_string(), "ls".to_string()]);
456
457 assert!(registry.get("bash").is_none());
459 assert!(registry.get("ls").is_none());
460 assert!(registry.get("read").is_some());
462 assert!(registry.get("subagent").is_some());
463 assert_eq!(registry.tools_schema().len(), before - 2);
465 }
466
467 #[test]
468 fn test_disable_empty_and_unknown_are_noops() {
469 let mut registry = ToolRegistry::new();
470 let n = registry.tools_schema().len();
471 registry.disable(&[]); assert_eq!(registry.tools_schema().len(), n);
473 registry.disable(&["not_a_real_tool".to_string()]); assert_eq!(registry.tools_schema().len(), n);
475 assert!(registry.get("bash").is_some());
476 }
477
478 #[test]
479 fn test_tool_registry_without_subagent() {
480 let registry = ToolRegistry::without_subagent();
481
482 assert_eq!(registry.tools_schema().len(), 10);
484 assert!(registry.get("subagent").is_none());
486
487 assert!(registry.get("bash").is_some());
489
490 assert!(registry.get("bash").is_some());
492 assert!(registry.get("read").is_some());
493 assert!(registry.get("write").is_some());
494 assert!(registry.get("edit").is_some());
495 assert!(registry.get("grep").is_some());
496 assert!(registry.get("find").is_some());
497 assert!(registry.get("ls").is_some());
498 }
499
500 #[test]
501 fn test_tool_registry_register() {
502 let mut registry = ToolRegistry::without_subagent();
503 let initial_count = registry.tools_schema().len();
504
505 struct TestTool;
507 #[async_trait::async_trait]
508 impl Tool for TestTool {
509 fn name(&self) -> &str { "test_tool" }
510 fn description(&self) -> &str { "A test tool" }
511 fn parameters(&self) -> Value { json!({"type": "object"}) }
512 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
513 Ok("test result".to_string())
514 }
515 }
516
517 registry.register(Arc::new(TestTool));
518
519 assert_eq!(registry.tools_schema().len(), initial_count + 1);
521
522 assert!(registry.get("test_tool").is_some());
524 }
525
526 #[test]
527 fn tool_names_for_extension_filters_by_owner_and_sorts() {
528 struct OwnedTool(&'static str, Option<&'static str>);
529 #[async_trait::async_trait]
530 impl Tool for OwnedTool {
531 fn name(&self) -> &str { self.0 }
532 fn description(&self) -> &str { "owned" }
533 fn parameters(&self) -> Value { json!({"type": "object"}) }
534 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
535 Ok("ok".to_string())
536 }
537 fn extension_id(&self) -> Option<&str> { self.1 }
538 }
539
540 let mut registry = ToolRegistry::without_subagent();
541 registry.register(Arc::new(OwnedTool("alpha:zed", Some("alpha"))));
542 registry.register(Arc::new(OwnedTool("alpha:bar", Some("alpha"))));
543 registry.register(Arc::new(OwnedTool("beta:thing", Some("beta"))));
544
545 assert_eq!(
546 registry.tool_names_for_extension("alpha"),
547 vec!["alpha:bar".to_string(), "alpha:zed".to_string()]
548 );
549 assert_eq!(
550 registry.tool_names_for_extension("beta"),
551 vec!["beta:thing".to_string()]
552 );
553 assert!(registry.tool_names_for_extension("ghost").is_empty());
554 assert!(registry.tool_names_for_extension("bash").is_empty());
556 }
557
558 struct OwnedTool(&'static str, Option<&'static str>);
559 #[async_trait::async_trait]
560 impl Tool for OwnedTool {
561 fn name(&self) -> &str { self.0 }
562 fn description(&self) -> &str { "owned" }
563 fn parameters(&self) -> Value { json!({"type": "object"}) }
564 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
565 Ok("ok".to_string())
566 }
567 fn extension_id(&self) -> Option<&str> { self.1 }
568 }
569
570 #[test]
571 fn without_subagent_excludes_subagent_tools() {
572 let registry = ToolRegistry::without_subagent();
573 assert!(registry.get("subagent").is_none());
574 assert!(registry.get("subagent_start").is_none());
575 assert!(registry.get("subagent_status").is_none());
576 assert!(registry.get("subagent_steer").is_none());
577 assert!(registry.get("subagent_collect").is_none());
578 assert!(registry.get("subagent_resume").is_none());
579 assert!(registry.get("bash").is_some());
581 assert!(registry.get("read").is_some());
582 }
583
584 #[test]
585 fn without_subagent_with_extensions_includes_extension_tools() {
586 let mut other = ToolRegistry::empty();
587 other.register(Arc::new(OwnedTool("alpha:do_thing", Some("alpha"))));
588
589 let merged = ToolRegistry::without_subagent_with_extensions(&other);
590
591 assert!(merged.get("alpha:do_thing").is_some());
593 assert!(merged.get("bash").is_some());
595 assert!(merged.get("read").is_some());
596 assert!(merged.get("subagent_start").is_none());
598 }
599
600 #[test]
601 fn without_subagent_with_extensions_excludes_built_ins_from_other_registry() {
602 let other = ToolRegistry::new();
607
608 let merged = ToolRegistry::without_subagent_with_extensions(&other);
609
610 let bash = merged.get("bash").expect("bash present");
612 assert!(bash.extension_id().is_none());
613 assert!(merged.get("subagent_start").is_none());
615 assert!(merged.get("subagent").is_none());
616 }
617
618 #[test]
619 fn without_subagent_with_extensions_does_not_overwrite_existing_builtin() {
620 let mut other = ToolRegistry::empty();
626 other.register(Arc::new(OwnedTool("ext:custom", Some("ext"))));
627
628 let merged = ToolRegistry::without_subagent_with_extensions(&other);
629 assert!(merged.get("ext:custom").is_some());
630 assert!(merged.get("bash").is_some());
631 assert!(merged.get("bash").unwrap().extension_id().is_none());
632 }
633}