Skip to main content

rs_agent/
catalog.rs

1//! Tool and SubAgent catalog implementations
2//!
3//! This module provides the default in-memory catalog implementations for both
4//! tools and sub-agents, matching the structure from go-agent's catalog.go.
5
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8
9use crate::error::{AgentError, Result};
10use crate::tools::Tool;
11use crate::types::{SubAgent, SubAgentDirectory, ToolSpec};
12
13/// StaticToolCatalog is the default in-memory implementation of a tool registry.
14/// It maintains tools in registration order and provides thread-safe lookup.
15pub struct StaticToolCatalog {
16    tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
17    specs: RwLock<HashMap<String, ToolSpec>>,
18    order: RwLock<Vec<String>>,
19}
20
21impl StaticToolCatalog {
22    /// Creates a new empty catalog
23    pub fn new() -> Self {
24        Self {
25            tools: RwLock::new(HashMap::new()),
26            specs: RwLock::new(HashMap::new()),
27            order: RwLock::new(Vec::new()),
28        }
29    }
30
31    /// Register a tool in the catalog using a lower-cased key.
32    /// Duplicate names return an error.
33    pub fn register(&self, tool: Arc<dyn Tool>) -> Result<()> {
34        let spec = tool.spec();
35        let key = spec.name.to_lowercase().trim().to_string();
36
37        if key.is_empty() {
38            return Err(AgentError::ToolError("tool name is empty".into()));
39        }
40
41        let mut tools = self.tools.write().unwrap();
42        let mut specs = self.specs.write().unwrap();
43        let mut order = self.order.write().unwrap();
44
45        if tools.contains_key(&key) {
46            return Err(AgentError::ToolError(format!(
47                "tool {} already registered",
48                spec.name
49            )));
50        }
51
52        tools.insert(key.clone(), tool);
53        specs.insert(key.clone(), spec);
54        order.push(key);
55
56        Ok(())
57    }
58
59    /// Lookup a tool and its specification by name
60    pub fn lookup(&self, name: &str) -> Option<(Arc<dyn Tool>, ToolSpec)> {
61        let key = name.to_lowercase().trim().to_string();
62
63        let tools = self.tools.read().unwrap();
64        let specs = self.specs.read().unwrap();
65
66        if let Some(tool) = tools.get(&key) {
67            if let Some(spec) = specs.get(&key) {
68                return Some((Arc::clone(tool), spec.clone()));
69            }
70        }
71
72        None
73    }
74
75    /// Returns a snapshot of all tool specifications in registration order
76    pub fn specs(&self) -> Vec<ToolSpec> {
77        let order = self.order.read().unwrap();
78        let specs = self.specs.read().unwrap();
79
80        order
81            .iter()
82            .filter_map(|key| specs.get(key).cloned())
83            .collect()
84    }
85
86    /// Returns all registered tools in order
87    pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
88        let order = self.order.read().unwrap();
89        let tools = self.tools.read().unwrap();
90
91        order
92            .iter()
93            .filter_map(|key| tools.get(key).map(Arc::clone))
94            .collect()
95    }
96}
97
98impl Default for StaticToolCatalog {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// StaticSubAgentDirectory is the default SubAgentDirectory implementation.
105/// It maintains sub-agents in registration order and provides thread-safe lookup.
106pub struct StaticSubAgentDirectory {
107    subagents: RwLock<HashMap<String, Arc<dyn SubAgent>>>,
108    order: RwLock<Vec<String>>,
109}
110
111impl StaticSubAgentDirectory {
112    /// Creates a new empty directory
113    pub fn new() -> Self {
114        Self {
115            subagents: RwLock::new(HashMap::new()),
116            order: RwLock::new(Vec::new()),
117        }
118    }
119}
120
121impl Default for StaticSubAgentDirectory {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl SubAgentDirectory for StaticSubAgentDirectory {
128    /// Register a sub-agent. Duplicate names return an error.
129    fn register(&self, subagent: Arc<dyn SubAgent>) -> Result<()> {
130        let name = subagent.name();
131        let key = name.to_lowercase().trim().to_string();
132
133        if key.is_empty() {
134            return Err(AgentError::Other("sub-agent name is empty".into()));
135        }
136
137        let mut subagents = self.subagents.write().unwrap();
138        let mut order = self.order.write().unwrap();
139
140        if subagents.contains_key(&key) {
141            return Err(AgentError::Other(format!(
142                "sub-agent {} already registered",
143                name
144            )));
145        }
146
147        subagents.insert(key.clone(), subagent);
148        order.push(key);
149
150        Ok(())
151    }
152
153    /// Lookup a sub-agent by name
154    fn lookup(&self, name: &str) -> Option<Arc<dyn SubAgent>> {
155        let key = name.to_lowercase().trim().to_string();
156        let subagents = self.subagents.read().unwrap();
157        subagents.get(&key).map(Arc::clone)
158    }
159
160    /// Returns all registered sub-agents in registration order
161    fn all(&self) -> Vec<Arc<dyn SubAgent>> {
162        let order = self.order.read().unwrap();
163        let subagents = self.subagents.read().unwrap();
164
165        order
166            .iter()
167            .filter_map(|key| subagents.get(key).map(Arc::clone))
168            .collect()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use async_trait::async_trait;
176    use crate::types::{ToolRequest, ToolResponse};
177
178    struct TestTool {
179        name: String,
180    }
181
182    #[async_trait]
183    impl Tool for TestTool {
184        fn spec(&self) -> ToolSpec {
185            ToolSpec {
186                name: self.name.clone(),
187                description: "Test tool".into(),
188                input_schema: serde_json::json!({}),
189                examples: None,
190            }
191        }
192
193        async fn invoke(&self, _req: ToolRequest) -> Result<ToolResponse> {
194            Ok(ToolResponse {
195                content: "test".into(),
196                metadata: None,
197            })
198        }
199    }
200
201    #[test]
202    fn catalog_registers_and_lookups_tools() {
203        let catalog = StaticToolCatalog::new();
204        let tool = Arc::new(TestTool {
205            name: "test.tool".into(),
206        });
207
208        catalog.register(tool).unwrap();
209        assert!(catalog.lookup("test.tool").is_some());
210        assert!(catalog.lookup("unknown").is_none());
211    }
212
213    #[test]
214    fn catalog_prevents_duplicate_registration() {
215        let catalog = StaticToolCatalog::new();
216        let tool1 = Arc::new(TestTool {
217            name: "test.tool".into(),
218        });
219        let tool2 = Arc::new(TestTool {
220            name: "test.tool".into(),
221        });
222
223        catalog.register(tool1).unwrap();
224        assert!(catalog.register(tool2).is_err());
225    }
226
227    struct TestSubAgent {
228        name: String,
229    }
230
231    #[async_trait]
232    impl SubAgent for TestSubAgent {
233        fn name(&self) -> String {
234            self.name.clone()
235        }
236
237        fn description(&self) -> String {
238            "Test sub-agent".into()
239        }
240
241        async fn run(&self, _input: String) -> Result<String> {
242            Ok("test output".into())
243        }
244    }
245
246    #[test]
247    fn directory_registers_and_lookups_subagents() {
248        let dir = StaticSubAgentDirectory::new();
249        let subagent = Arc::new(TestSubAgent {
250            name: "test.agent".into(),
251        });
252
253        dir.register(subagent).unwrap();
254        assert!(dir.lookup("test.agent").is_some());
255        assert!(dir.lookup("unknown").is_none());
256    }
257
258    #[test]
259    fn directory_prevents_duplicate_registration() {
260        let dir = StaticSubAgentDirectory::new();
261        let sa1 = Arc::new(TestSubAgent {
262            name: "test.agent".into(),
263        });
264        let sa2 = Arc::new(TestSubAgent {
265            name: "test.agent".into(),
266        });
267
268        dir.register(sa1).unwrap();
269        assert!(dir.register(sa2).is_err());
270    }
271}