lellm_agent/runtime/tools/
mod.rs1mod executor;
11
12pub use lellm_core::{ExecutableTool, ParallelSafety, ToolArgs, ToolCategory, ToolFn};
14
15pub use executor::{BatchExecutionResult, ToolExecutor, execute_batch_with};
17
18pub struct ToolSnapshot {
25 version: u64,
26 tools: std::sync::Arc<indexmap::IndexMap<String, ExecutableTool>>,
27 definitions: std::sync::OnceLock<Vec<lellm_core::ToolDefinition>>,
28}
29
30impl ToolSnapshot {
31 pub fn new(tools: indexmap::IndexMap<String, ExecutableTool>, version: u64) -> Self {
33 Self {
34 version,
35 tools: std::sync::Arc::new(tools),
36 definitions: std::sync::OnceLock::new(),
37 }
38 }
39
40 pub fn get(&self, name: &str) -> Option<&ExecutableTool> {
42 self.tools.get(name)
43 }
44
45 pub fn definitions(&self) -> &[lellm_core::ToolDefinition] {
47 self.definitions
48 .get_or_init(|| self.tools.values().map(|t| t.definition.clone()).collect())
49 }
50
51 pub fn has_tools(&self) -> bool {
53 !self.tools.is_empty()
54 }
55
56 pub fn version(&self) -> u64 {
58 self.version
59 }
60
61 pub fn len(&self) -> usize {
63 self.tools.len()
64 }
65
66 pub fn is_empty(&self) -> bool {
68 self.tools.is_empty()
69 }
70}
71
72#[async_trait::async_trait]
86pub trait ToolCatalog: Send + Sync {
87 async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot>;
92}
93
94pub struct StaticCatalog {
96 snapshot: std::sync::Arc<ToolSnapshot>,
97}
98
99impl StaticCatalog {
100 pub fn from_tools(tools: Vec<ExecutableTool>) -> Self {
102 let mut map = indexmap::IndexMap::with_capacity(tools.len());
103 for reg in tools {
104 map.insert(reg.definition.name.clone(), reg);
105 }
106 Self {
107 snapshot: std::sync::Arc::new(ToolSnapshot::new(map, 0)),
108 }
109 }
110
111 pub fn empty() -> Self {
113 Self {
114 snapshot: std::sync::Arc::new(ToolSnapshot::new(indexmap::IndexMap::new(), 0)),
115 }
116 }
117}
118
119#[async_trait::async_trait]
120impl ToolCatalog for StaticCatalog {
121 async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot> {
122 self.snapshot.clone()
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
128pub enum ConflictPolicy {
129 #[default]
131 Shadow,
132 Error,
134}
135
136#[derive(Debug, Clone)]
138pub struct CatalogConflict {
139 pub tool_name: String,
141 pub winner: String,
143 pub loser: String,
145 pub policy: ConflictPolicy,
147}
148
149#[derive(Default)]
151pub struct CompositeCatalogBuilder {
152 sources: Vec<(String, std::sync::Arc<dyn ToolCatalog>)>,
153 conflict_policy: ConflictPolicy,
154}
155
156impl CompositeCatalogBuilder {
157 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub fn conflict_policy(mut self, policy: ConflictPolicy) -> Self {
164 self.conflict_policy = policy;
165 self
166 }
167
168 pub fn add(
170 mut self,
171 name: impl Into<String>,
172 catalog: std::sync::Arc<dyn ToolCatalog>,
173 ) -> Self {
174 self.sources.push((name.into(), catalog));
175 self
176 }
177
178 pub fn build(self) -> CompositeCatalog {
180 let sources: Vec<_> = self.sources.into_iter().map(|(_, c)| c).collect();
181 CompositeCatalog {
182 sources,
183 conflict_policy: self.conflict_policy,
184 version_counter: std::sync::atomic::AtomicU64::new(0),
185 conflicts: std::sync::Mutex::new(Vec::new()),
186 }
187 }
188}
189
190pub struct CompositeCatalog {
195 sources: Vec<std::sync::Arc<dyn ToolCatalog>>,
196 conflict_policy: ConflictPolicy,
197 version_counter: std::sync::atomic::AtomicU64,
198 conflicts: std::sync::Mutex<Vec<CatalogConflict>>,
199}
200
201impl CompositeCatalog {
202 pub fn builder() -> CompositeCatalogBuilder {
204 CompositeCatalogBuilder::new()
205 }
206
207 pub fn new(sources: Vec<std::sync::Arc<dyn ToolCatalog>>) -> Self {
211 Self {
212 sources,
213 conflict_policy: ConflictPolicy::default(),
214 version_counter: std::sync::atomic::AtomicU64::new(0),
215 conflicts: std::sync::Mutex::new(Vec::new()),
216 }
217 }
218
219 pub fn conflicts(&self) -> Vec<CatalogConflict> {
221 self.conflicts.lock().unwrap().clone()
222 }
223}
224
225#[async_trait::async_trait]
226impl ToolCatalog for CompositeCatalog {
227 async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot> {
228 let mut merged = indexmap::IndexMap::new();
229 let mut conflicts = Vec::new();
230
231 for (idx, source) in self.sources.iter().rev().enumerate() {
233 let snap = source.snapshot().await;
234 let snap_tools = &snap.tools;
235 let source_name = format!("source_{}", idx);
236 for (name, tool) in snap_tools.iter() {
237 if merged.contains_key(name) {
238 tracing::warn!(
239 tool_name = %name,
240 "Tool conflict detected in CompositeCatalog. Higher priority tool shadows the lower one."
241 );
242 conflicts.push(CatalogConflict {
243 tool_name: name.clone(),
244 winner: source_name.clone(),
245 loser: format!("source_{}", idx + 1),
246 policy: self.conflict_policy,
247 });
248 }
249 merged.insert(name.clone(), tool.clone());
250 }
251 }
252
253 if !conflicts.is_empty() {
255 *self.conflicts.lock().unwrap() = conflicts;
256 }
257
258 let version = self
259 .version_counter
260 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
261 + 1;
262 std::sync::Arc::new(ToolSnapshot::new(merged, version))
263 }
264}