ferrous_di/capabilities.rs
1//! Capability discovery and tool catalog functionality for agentic systems.
2//!
3//! This module provides infrastructure for discovering available tools and their
4//! capabilities at runtime. Essential for agent planners that need to discover,
5//! filter, and select appropriate tools based on requirements.
6
7use std::any::{TypeId, type_name};
8use std::collections::HashMap;
9use std::sync::Arc;
10use crate::{ServiceCollection, Key};
11
12/// Metadata about a tool's capabilities and requirements.
13///
14/// This trait should be implemented by tools that want to expose their
15/// capabilities to the agent planner. Tools can declare what they can do,
16/// what they require, and other metadata useful for selection.
17///
18/// # Examples
19///
20/// ```
21/// use ferrous_di::{ToolCapability, ServiceCollection};
22/// use std::sync::Arc;
23///
24/// struct FileSearchTool {
25/// root_dir: String,
26/// }
27///
28/// impl ToolCapability for FileSearchTool {
29/// fn name(&self) -> &str { "file_search" }
30/// fn description(&self) -> &str { "Search for files matching patterns" }
31/// fn version(&self) -> &str { "1.0.0" }
32///
33/// fn capabilities(&self) -> Vec<&str> {
34/// vec!["file_search", "pattern_matching", "filesystem_access"]
35/// }
36///
37/// fn requires(&self) -> Vec<&str> {
38/// vec!["filesystem_read"]
39/// }
40///
41/// fn tags(&self) -> Vec<&str> {
42/// vec!["files", "search", "core"]
43/// }
44/// }
45/// ```
46pub trait ToolCapability: Send + Sync {
47 /// The unique name/identifier of this tool.
48 fn name(&self) -> &str;
49
50 /// Human-readable description of what this tool does.
51 fn description(&self) -> &str;
52
53 /// Version of this tool.
54 fn version(&self) -> &str;
55
56 /// List of capabilities this tool provides.
57 ///
58 /// Capabilities are strings like "web_search", "file_read", "image_generation", etc.
59 fn capabilities(&self) -> Vec<&str>;
60
61 /// List of capabilities this tool requires to function.
62 ///
63 /// For example, a file tool might require "filesystem_access".
64 fn requires(&self) -> Vec<&str>;
65
66 /// Optional tags for categorization and filtering.
67 ///
68 /// Tags like "core", "experimental", "external", "local", etc.
69 fn tags(&self) -> Vec<&str> {
70 Vec::new()
71 }
72
73 /// Optional cost estimate for using this tool.
74 ///
75 /// This could be monetary cost, computational cost, time cost, etc.
76 /// Units are tool-specific but should be documented.
77 fn estimated_cost(&self) -> Option<f64> {
78 None
79 }
80
81 /// Optional reliability score (0.0 to 1.0).
82 ///
83 /// How reliable/stable is this tool? 1.0 = always works, 0.0 = very unreliable.
84 fn reliability(&self) -> Option<f64> {
85 None
86 }
87}
88
89/// Capability requirement for tool selection.
90///
91/// Used by planners to specify what capabilities they need when requesting
92/// tool recommendations.
93#[derive(Debug, Clone)]
94pub struct CapabilityRequirement {
95 /// The capability name that must be provided.
96 pub capability: String,
97 /// Whether this capability is required (vs nice-to-have).
98 pub required: bool,
99 /// Minimum version requirement (if applicable).
100 pub min_version: Option<String>,
101 /// Maximum acceptable cost for this capability.
102 pub max_cost: Option<f64>,
103 /// Minimum acceptable reliability for this capability.
104 pub min_reliability: Option<f64>,
105}
106
107impl CapabilityRequirement {
108 /// Creates a required capability.
109 pub fn required(capability: impl Into<String>) -> Self {
110 Self {
111 capability: capability.into(),
112 required: true,
113 min_version: None,
114 max_cost: None,
115 min_reliability: None,
116 }
117 }
118
119 /// Creates an optional capability.
120 pub fn optional(capability: impl Into<String>) -> Self {
121 Self {
122 capability: capability.into(),
123 required: false,
124 min_version: None,
125 max_cost: None,
126 min_reliability: None,
127 }
128 }
129
130 /// Sets minimum version requirement.
131 pub fn min_version(mut self, version: impl Into<String>) -> Self {
132 self.min_version = Some(version.into());
133 self
134 }
135
136 /// Sets maximum acceptable cost.
137 pub fn max_cost(mut self, cost: f64) -> Self {
138 self.max_cost = Some(cost);
139 self
140 }
141
142 /// Sets minimum acceptable reliability.
143 pub fn min_reliability(mut self, reliability: f64) -> Self {
144 self.min_reliability = Some(reliability);
145 self
146 }
147}
148
149/// Tool selection criteria for capability-based tool discovery.
150#[derive(Debug, Default)]
151pub struct ToolSelectionCriteria {
152 /// Required and optional capabilities.
153 pub capabilities: Vec<CapabilityRequirement>,
154 /// Tags that tools must have.
155 pub required_tags: Vec<String>,
156 /// Tags that tools should not have.
157 pub excluded_tags: Vec<String>,
158 /// Maximum total cost across all selected tools.
159 pub max_total_cost: Option<f64>,
160 /// Minimum average reliability across selected tools.
161 pub min_average_reliability: Option<f64>,
162 /// Maximum number of tools to return.
163 pub limit: Option<usize>,
164}
165
166impl ToolSelectionCriteria {
167 /// Creates new selection criteria.
168 pub fn new() -> Self {
169 Self::default()
170 }
171
172 /// Adds a required capability.
173 pub fn require(mut self, capability: impl Into<String>) -> Self {
174 self.capabilities.push(CapabilityRequirement::required(capability));
175 self
176 }
177
178 /// Adds a required capability with cost constraint.
179 pub fn require_with_cost(mut self, capability: impl Into<String>, max_cost: f64) -> Self {
180 self.capabilities.push(CapabilityRequirement::required(capability).max_cost(max_cost));
181 self
182 }
183
184 /// Adds an optional capability.
185 pub fn prefer(mut self, capability: impl Into<String>) -> Self {
186 self.capabilities.push(CapabilityRequirement::optional(capability));
187 self
188 }
189
190 /// Adds a required tag.
191 pub fn require_tag(mut self, tag: impl Into<String>) -> Self {
192 self.required_tags.push(tag.into());
193 self
194 }
195
196 /// Adds an excluded tag.
197 pub fn exclude_tag(mut self, tag: impl Into<String>) -> Self {
198 self.excluded_tags.push(tag.into());
199 self
200 }
201
202 /// Sets maximum total cost limit.
203 pub fn max_cost(mut self, cost: f64) -> Self {
204 self.max_total_cost = Some(cost);
205 self
206 }
207
208 /// Sets minimum reliability requirement.
209 pub fn min_reliability(mut self, reliability: f64) -> Self {
210 self.min_average_reliability = Some(reliability);
211 self
212 }
213
214 /// Sets maximum number of tools to return.
215 pub fn limit(mut self, count: usize) -> Self {
216 self.limit = Some(count);
217 self
218 }
219}
220
221/// Information about a discovered tool.
222#[derive(Debug, Clone)]
223pub struct ToolInfo {
224 /// The service key for resolving this tool.
225 pub key: Key,
226 /// Tool name.
227 pub name: String,
228 /// Tool description.
229 pub description: String,
230 /// Tool version.
231 pub version: String,
232 /// Capabilities provided.
233 pub capabilities: Vec<String>,
234 /// Capabilities required.
235 pub requires: Vec<String>,
236 /// Tool tags.
237 pub tags: Vec<String>,
238 /// Estimated cost.
239 pub estimated_cost: Option<f64>,
240 /// Reliability score.
241 pub reliability: Option<f64>,
242}
243
244impl ToolInfo {
245 /// Checks if this tool satisfies a capability requirement.
246 pub fn satisfies(&self, req: &CapabilityRequirement) -> bool {
247 // Must provide the capability
248 if !self.capabilities.contains(&req.capability) {
249 return false;
250 }
251
252 // Check cost constraint
253 if let Some(max_cost) = req.max_cost {
254 if let Some(cost) = self.estimated_cost {
255 if cost > max_cost {
256 return false;
257 }
258 }
259 }
260
261 // Check reliability constraint
262 if let Some(min_reliability) = req.min_reliability {
263 if let Some(reliability) = self.reliability {
264 if reliability < min_reliability {
265 return false;
266 }
267 } else {
268 // No reliability info - assume it doesn't meet requirement
269 return false;
270 }
271 }
272
273 // Version checking would go here if we implemented semver parsing
274 // For now, skip version checks
275
276 true
277 }
278}
279
280/// Tool discovery result.
281#[derive(Debug)]
282pub struct ToolDiscoveryResult {
283 /// Tools that match all required criteria.
284 pub matching_tools: Vec<ToolInfo>,
285 /// Tools that match some optional criteria.
286 pub partial_matches: Vec<ToolInfo>,
287 /// Required capabilities that couldn't be satisfied.
288 pub unsatisfied_requirements: Vec<String>,
289}
290
291/// Registry of available tools and their capabilities.
292pub(crate) struct CapabilityRegistry {
293 /// Map from service keys to tool capability info.
294 tools: HashMap<Key, ToolInfo>,
295}
296
297impl CapabilityRegistry {
298 /// Creates a new empty capability registry.
299 pub(crate) fn new() -> Self {
300 Self {
301 tools: HashMap::new(),
302 }
303 }
304
305 /// Registers a tool's capabilities.
306 pub(crate) fn register_tool<T: ?Sized + ToolCapability + 'static>(&mut self, key: Key, tool: &T) {
307 let info = ToolInfo {
308 key: key.clone(),
309 name: tool.name().to_string(),
310 description: tool.description().to_string(),
311 version: tool.version().to_string(),
312 capabilities: tool.capabilities().into_iter().map(|s| s.to_string()).collect(),
313 requires: tool.requires().into_iter().map(|s| s.to_string()).collect(),
314 tags: tool.tags().into_iter().map(|s| s.to_string()).collect(),
315 estimated_cost: tool.estimated_cost(),
316 reliability: tool.reliability(),
317 };
318
319 self.tools.insert(key, info);
320 }
321
322 /// Discovers tools based on selection criteria.
323 pub(crate) fn discover(&self, criteria: &ToolSelectionCriteria) -> ToolDiscoveryResult {
324 let mut matching_tools = Vec::new();
325 let mut partial_matches = Vec::new();
326 let mut unsatisfied_requirements = Vec::new();
327
328 // Find required capabilities that no tool satisfies
329 for req in &criteria.capabilities {
330 if req.required {
331 let satisfied = self.tools.values().any(|tool| tool.satisfies(req));
332 if !satisfied {
333 unsatisfied_requirements.push(req.capability.clone());
334 }
335 }
336 }
337
338 // Score each tool
339 for tool in self.tools.values() {
340 let mut score = self.score_tool(tool, criteria);
341
342 // Check required tags
343 let has_required_tags = criteria.required_tags.iter()
344 .all(|tag| tool.tags.contains(tag));
345
346 // Check excluded tags
347 let has_excluded_tags = criteria.excluded_tags.iter()
348 .any(|tag| tool.tags.contains(tag));
349
350 if !has_required_tags || has_excluded_tags {
351 score = 0.0; // Disqualify
352 }
353
354 if score > 0.5 {
355 matching_tools.push(tool.clone());
356 } else if score > 0.0 {
357 partial_matches.push(tool.clone());
358 }
359 }
360
361 // Sort by score (would need to store scores, simplified for now)
362 matching_tools.sort_by(|a, b| a.name.cmp(&b.name));
363 partial_matches.sort_by(|a, b| a.name.cmp(&b.name));
364
365 // Apply limit
366 if let Some(limit) = criteria.limit {
367 matching_tools.truncate(limit);
368 }
369
370 ToolDiscoveryResult {
371 matching_tools,
372 partial_matches,
373 unsatisfied_requirements,
374 }
375 }
376
377 /// Scores a tool against the selection criteria (0.0 to 1.0).
378 fn score_tool(&self, tool: &ToolInfo, criteria: &ToolSelectionCriteria) -> f64 {
379 let mut score = 0.0;
380 let mut max_score = 0.0;
381
382 // Score based on capability satisfaction
383 for req in &criteria.capabilities {
384 max_score += if req.required { 1.0 } else { 0.5 };
385
386 if tool.satisfies(req) {
387 score += if req.required { 1.0 } else { 0.5 };
388 } else if req.required {
389 // Failed required capability - disqualify
390 return 0.0;
391 }
392 }
393
394 if max_score == 0.0 {
395 return 1.0; // No capability requirements, all tools qualify
396 }
397
398 score / max_score
399 }
400
401 /// Gets all registered tools.
402 pub(crate) fn all_tools(&self) -> Vec<&ToolInfo> {
403 self.tools.values().collect()
404 }
405
406 /// Gets tool info by key.
407 pub(crate) fn get_tool(&self, key: &Key) -> Option<&ToolInfo> {
408 self.tools.get(key)
409 }
410}
411
412impl ServiceCollection {
413 /// Registers a service as a tool with capabilities.
414 ///
415 /// This combines service registration with capability metadata registration,
416 /// making the tool discoverable through the capability system.
417 ///
418 /// # Examples
419 ///
420 /// ```
421 /// use ferrous_di::{ServiceCollection, ToolCapability};
422 /// use std::sync::Arc;
423 ///
424 /// struct WebSearchTool {
425 /// api_key: String,
426 /// }
427 ///
428 /// impl ToolCapability for WebSearchTool {
429 /// fn name(&self) -> &str { "web_search" }
430 /// fn description(&self) -> &str { "Search the web for information" }
431 /// fn version(&self) -> &str { "2.1.0" }
432 /// fn capabilities(&self) -> Vec<&str> { vec!["web_search", "information_retrieval"] }
433 /// fn requires(&self) -> Vec<&str> { vec!["internet_access", "api_key"] }
434 /// fn tags(&self) -> Vec<&str> { vec!["external", "search", "web"] }
435 /// fn estimated_cost(&self) -> Option<f64> { Some(0.001) } // $0.001 per query
436 /// fn reliability(&self) -> Option<f64> { Some(0.95) } // 95% reliable
437 /// }
438 ///
439 /// let mut services = ServiceCollection::new();
440 /// let tool = WebSearchTool {
441 /// api_key: "secret-key".to_string(),
442 /// };
443 /// services.add_tool_singleton(tool);
444 /// ```
445 pub fn add_tool_singleton<T>(&mut self, tool: T) -> &mut Self
446 where
447 T: ToolCapability + Send + Sync + 'static,
448 {
449 // Register capabilities first
450 let key = Key::Type(TypeId::of::<T>(), type_name::<T>());
451 self.capabilities.register_tool(key.clone(), &tool);
452
453 // Then register as regular singleton
454 self.add_singleton(tool);
455 self
456 }
457
458 /// Registers a trait as a tool with capabilities.
459 ///
460 /// # Examples
461 ///
462 /// ```
463 /// use ferrous_di::{ServiceCollection, ToolCapability};
464 /// use std::sync::Arc;
465 ///
466 /// trait SearchTool: ToolCapability + Send + Sync {
467 /// fn search(&self, query: &str) -> Vec<String>;
468 /// }
469 ///
470 /// struct GoogleSearchTool;
471 ///
472 /// impl ToolCapability for GoogleSearchTool {
473 /// fn name(&self) -> &str { "google_search" }
474 /// fn description(&self) -> &str { "Search using Google" }
475 /// fn version(&self) -> &str { "1.0.0" }
476 /// fn capabilities(&self) -> Vec<&str> { vec!["web_search"] }
477 /// fn requires(&self) -> Vec<&str> { vec!["internet"] }
478 /// }
479 ///
480 /// impl SearchTool for GoogleSearchTool {
481 /// fn search(&self, query: &str) -> Vec<String> {
482 /// // Implementation here
483 /// vec![format!("Results for: {}", query)]
484 /// }
485 /// }
486 ///
487 /// let mut services = ServiceCollection::new();
488 /// let tool = Arc::new(GoogleSearchTool);
489 /// services.add_tool_trait::<dyn SearchTool>(tool);
490 /// ```
491 pub fn add_tool_trait<T>(&mut self, tool: Arc<T>) -> &mut Self
492 where
493 T: ?Sized + ToolCapability + Send + Sync + 'static,
494 {
495 // Register capabilities first
496 let key = Key::Trait(type_name::<T>());
497 self.capabilities.register_tool(key.clone(), tool.as_ref());
498
499 // Then register as trait
500 self.add_singleton_trait::<T>(tool);
501 self
502 }
503}
504
505// Implementation is in provider/mod.rs to access inner struct