lellm_agent/runtime/tools/
mod.rs1mod args;
6mod executor;
7
8pub use args::ToolArgs;
9pub use executor::{
10 BatchExecutionResult, ParallelSafety, ToolCategory, ToolExecutor, ToolRegistration,
11 execute_batch_with,
12};
13
14pub(crate) type ToolFn = std::sync::Arc<
16 dyn Fn(
17 &serde_json::Value,
18 )
19 -> std::pin::Pin<Box<dyn std::future::Future<Output = lellm_core::ToolResult> + Send>>
20 + Send
21 + Sync,
22>;
23
24pub struct ToolSnapshot {
31 version: u64,
32 tools: std::sync::Arc<indexmap::IndexMap<String, ToolRegistration>>,
33 definitions: std::sync::OnceLock<Vec<lellm_core::ToolDefinition>>,
34}
35
36impl ToolSnapshot {
37 pub fn new(tools: indexmap::IndexMap<String, ToolRegistration>, version: u64) -> Self {
39 Self {
40 version,
41 tools: std::sync::Arc::new(tools),
42 definitions: std::sync::OnceLock::new(),
43 }
44 }
45
46 pub fn get(&self, name: &str) -> Option<&ToolRegistration> {
48 self.tools.get(name)
49 }
50
51 pub fn definitions(&self) -> &[lellm_core::ToolDefinition] {
53 self.definitions
54 .get_or_init(|| self.tools.values().map(|t| t.definition.clone()).collect())
55 }
56
57 pub fn has_tools(&self) -> bool {
59 !self.tools.is_empty()
60 }
61
62 pub fn version(&self) -> u64 {
64 self.version
65 }
66
67 pub fn len(&self) -> usize {
69 self.tools.len()
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.tools.is_empty()
75 }
76}
77
78#[async_trait::async_trait]
92pub trait ToolCatalog: Send + Sync {
93 async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot>;
98}
99
100pub struct StaticCatalog {
102 snapshot: std::sync::Arc<ToolSnapshot>,
103}
104
105impl StaticCatalog {
106 pub fn from_tools(tools: Vec<ToolRegistration>) -> Self {
108 let mut map = indexmap::IndexMap::with_capacity(tools.len());
109 for reg in tools {
110 map.insert(reg.definition.name.clone(), reg);
111 }
112 Self {
113 snapshot: std::sync::Arc::new(ToolSnapshot::new(map, 0)),
114 }
115 }
116
117 pub fn empty() -> Self {
119 Self {
120 snapshot: std::sync::Arc::new(ToolSnapshot::new(indexmap::IndexMap::new(), 0)),
121 }
122 }
123}
124
125#[async_trait::async_trait]
126impl ToolCatalog for StaticCatalog {
127 async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot> {
128 self.snapshot.clone()
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
134pub enum ConflictPolicy {
135 #[default]
137 Shadow,
138 Error,
140}
141
142#[derive(Debug, Clone)]
144pub struct CatalogConflict {
145 pub tool_name: String,
147 pub winner: String,
149 pub loser: String,
151 pub policy: ConflictPolicy,
153}
154
155#[derive(Default)]
157pub struct CompositeCatalogBuilder {
158 sources: Vec<(String, std::sync::Arc<dyn ToolCatalog>)>,
159 conflict_policy: ConflictPolicy,
160}
161
162impl CompositeCatalogBuilder {
163 pub fn new() -> Self {
165 Self::default()
166 }
167
168 pub fn conflict_policy(mut self, policy: ConflictPolicy) -> Self {
170 self.conflict_policy = policy;
171 self
172 }
173
174 pub fn add(
176 mut self,
177 name: impl Into<String>,
178 catalog: std::sync::Arc<dyn ToolCatalog>,
179 ) -> Self {
180 self.sources.push((name.into(), catalog));
181 self
182 }
183
184 pub fn build(self) -> CompositeCatalog {
186 let sources: Vec<_> = self.sources.into_iter().map(|(_, c)| c).collect();
187 CompositeCatalog {
188 sources,
189 conflict_policy: self.conflict_policy,
190 version_counter: std::sync::atomic::AtomicU64::new(0),
191 conflicts: std::sync::Mutex::new(Vec::new()),
192 }
193 }
194}
195
196pub struct CompositeCatalog {
201 sources: Vec<std::sync::Arc<dyn ToolCatalog>>,
202 conflict_policy: ConflictPolicy,
203 version_counter: std::sync::atomic::AtomicU64,
204 conflicts: std::sync::Mutex<Vec<CatalogConflict>>,
205}
206
207impl CompositeCatalog {
208 pub fn builder() -> CompositeCatalogBuilder {
210 CompositeCatalogBuilder::new()
211 }
212
213 pub fn new(sources: Vec<std::sync::Arc<dyn ToolCatalog>>) -> Self {
217 Self {
218 sources,
219 conflict_policy: ConflictPolicy::default(),
220 version_counter: std::sync::atomic::AtomicU64::new(0),
221 conflicts: std::sync::Mutex::new(Vec::new()),
222 }
223 }
224
225 pub fn conflicts(&self) -> Vec<CatalogConflict> {
227 self.conflicts.lock().unwrap().clone()
228 }
229}
230
231#[async_trait::async_trait]
232impl ToolCatalog for CompositeCatalog {
233 async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot> {
234 let mut merged = indexmap::IndexMap::new();
235 let mut conflicts = Vec::new();
236
237 for (idx, source) in self.sources.iter().rev().enumerate() {
239 let snap = source.snapshot().await;
240 let snap_tools = &snap.tools;
241 let source_name = format!("source_{}", idx);
242 for (name, tool) in snap_tools.iter() {
243 if merged.contains_key(name) {
244 tracing::warn!(
245 tool_name = %name,
246 "Tool conflict detected in CompositeCatalog. Higher priority tool shadows the lower one."
247 );
248 conflicts.push(CatalogConflict {
249 tool_name: name.clone(),
250 winner: source_name.clone(),
251 loser: format!("source_{}", idx + 1),
252 policy: self.conflict_policy,
253 });
254 }
255 merged.insert(name.clone(), tool.clone());
256 }
257 }
258
259 if !conflicts.is_empty() {
261 *self.conflicts.lock().unwrap() = conflicts;
262 }
263
264 let version = self
265 .version_counter
266 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
267 + 1;
268 std::sync::Arc::new(ToolSnapshot::new(merged, version))
269 }
270}