Skip to main content

lellm_agent/runtime/tools/
mod.rs

1//! 工具系统 — 执行、目录抽象。
2//!
3//! 独立的工具子系统,被 runtime 层使用。
4//!
5//! **分层:**
6//! - 协议层(lellm-core):`ToolArgs`, `ToolDefinition`, `ParallelSafety`, `ToolCategory`
7//! - 可执行描述(lellm-core):`ExecutableTool`, `ToolFn`
8//! - 运行时层(本模块):`ToolExecutor`, `ToolCatalog`, `ToolSnapshot`
9
10mod executor;
11
12// Re-export protocol types from lellm-core
13pub use lellm_core::{ExecutableTool, ParallelSafety, ToolArgs, ToolCategory, ToolFn};
14
15// Re-export runtime types
16pub use executor::{BatchExecutionResult, ToolExecutor, execute_batch_with};
17
18// ─── 工具快照 ────────────────────────────────────────────────────
19
20/// 工具快照 — 冻结视图(Frozen View)。
21///
22/// 一旦创建,快照内容不再变化。通过 `version` 区分不同时刻的快照。
23/// `definitions` 通过 `OnceLock` 懒构建——大部分轮次不需要定义列表。
24pub 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    /// 从工具映射构建快照。
32    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    /// 按名称查找工具。
41    pub fn get(&self, name: &str) -> Option<&ExecutableTool> {
42        self.tools.get(name)
43    }
44
45    /// 获取所有工具定义(懒构建)。
46    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    /// 是否有工具。
52    pub fn has_tools(&self) -> bool {
53        !self.tools.is_empty()
54    }
55
56    /// 快照版本号。
57    pub fn version(&self) -> u64 {
58        self.version
59    }
60
61    /// 工具数量。
62    pub fn len(&self) -> usize {
63        self.tools.len()
64    }
65
66    /// 是否为空。
67    pub fn is_empty(&self) -> bool {
68        self.tools.is_empty()
69    }
70}
71
72// ─── 工具目录抽象 ────────────────────────────────────────────────
73
74/// 工具目录 — 静态或动态的工具集合。
75///
76/// **设计目标:**
77/// - 让 `ToolExecutor` 不关心工具来源(静态注册 vs MCP 发现)
78/// - 每轮迭代调用 `snapshot()` 一次,固定本轮工具集(避免同轮不一致)
79/// - `ExecutableTool` 必须 `Clone + Send + Sync`(快照在内存中传递)
80///
81/// **快照时机:**
82/// - `ToolUseLoop::execute()` — 每轮迭代开始前调用一次
83/// - `ToolUseLoop::execute_stream()` — 每轮迭代开始前调用一次
84/// - **禁止**在 `execute_batch` 内部调用(会导致同轮工具集漂移)
85#[async_trait::async_trait]
86pub trait ToolCatalog: Send + Sync {
87    /// 获取当前所有工具注册的快照。
88    ///
89    /// 返回的快照在调用瞬间冻结。
90    /// 后续调用可能返回不同的工具集(动态目录刷新)。
91    async fn snapshot(&self) -> std::sync::Arc<ToolSnapshot>;
92}
93
94/// 静态工具目录 — 构建后不可变的工具集合。
95pub struct StaticCatalog {
96    snapshot: std::sync::Arc<ToolSnapshot>,
97}
98
99impl StaticCatalog {
100    /// 从工具注册列表构建静态目录。
101    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    /// 空目录。
112    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/// 冲突解决策略
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
128pub enum ConflictPolicy {
129    /// 默认:前面优先级高,同名工具被遮蔽
130    #[default]
131    Shadow,
132    /// 严格模式:冲突即报错
133    Error,
134}
135
136/// 工具冲突详情
137#[derive(Debug, Clone)]
138pub struct CatalogConflict {
139    /// 冲突的工具名称
140    pub tool_name: String,
141    /// 获胜的 catalog 名称(优先级高)
142    pub winner: String,
143    /// 被覆盖的 catalog 名称(优先级低)
144    pub loser: String,
145    /// 使用的冲突策略
146    pub policy: ConflictPolicy,
147}
148
149/// 组合目录构建器
150#[derive(Default)]
151pub struct CompositeCatalogBuilder {
152    sources: Vec<(String, std::sync::Arc<dyn ToolCatalog>)>,
153    conflict_policy: ConflictPolicy,
154}
155
156impl CompositeCatalogBuilder {
157    /// 创建新的构建器
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// 设置冲突策略
163    pub fn conflict_policy(mut self, policy: ConflictPolicy) -> Self {
164        self.conflict_policy = policy;
165        self
166    }
167
168    /// 添加工具源(按优先级从高到低)
169    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    /// 构建组合目录
179    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
190/// 组合目录 — 按优先级合并多个工具源。
191///
192/// **遮蔽策略(Shadowing):** 靠前的源优先级高,同名工具被遮蔽。
193/// 遮蔽发生时通过 `tracing::warn!` 记录结构化日志。
194pub 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    /// 创建组合目录(Builder 模式)。
203    pub fn builder() -> CompositeCatalogBuilder {
204        CompositeCatalogBuilder::new()
205    }
206
207    /// 创建组合目录(简单模式,默认 Shadow 策略)。
208    ///
209    /// `sources` 按优先级从高到低排列。
210    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    /// 获取所有冲突详情
220    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        // 反向遍历(从低优先级到高优先级),高优先级自然覆盖低优先级
232        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        // 存储冲突信息
254        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}