Skip to main content

lellm_agent/runtime/
builder.rs

1//! AgentBuilder — Agent 链式构建器。
2//!
3//! 提供推荐的入口 API,一步构建 `Graph<AgentState>`。
4//!
5//! # 两层 API
6//!
7//! ```ignore
8//! use lellm_agent::{AgentBuilder, ToolUseLoop};
9//! use lellm_core::Prompt;
10//!
11//! // DSL 层 — build() 返回 Graph<AgentState>,可嵌入更大编排
12//! let graph = AgentBuilder::new(model)
13//!     .system("你是一个有帮助的助手。")
14//!     .tool(search_tool)
15//!     .tool(weather_tool)
16//!     .max_iterations(20)
17//!     .build();
18//!
19//! // 分层构建 — 最大化前缀缓存
20//! let graph = AgentBuilder::new(model)
21//!     .system(
22//!         Prompt::new()
23//!             .stable("核心身份…")
24//!             .stable("工具指南…")
25//!             .dynamic("会话上下文…")
26//!             .build(),
27//!     )
28//!     .build();
29//!
30//! // Facade 层 — compile() 返回 ToolUseLoop,提供 invoke() 便捷 API
31//! let result = AgentBuilder::new(model)
32//!     .tools([search_tool, weather_tool])
33//!     .compile()
34//!     .invoke(messages)
35//!     .await?;
36//!
37//! // 手动组合 — 等价于 compile()
38//! let graph = AgentBuilder::new(model).tools([...]).build();
39//! let agent = ToolUseLoop::new(graph, config);
40//! ```
41
42use std::sync::Arc;
43
44use lellm_core::{Prompt, ReasoningConfig, ToolChoice};
45use lellm_graph::Graph;
46use lellm_provider::ResolvedModel;
47
48use super::config::{ToolCachePolicy, ToolUseConfig, ToolUseDeps};
49use super::context::{ContextBudget, LocalCompactor};
50use super::fallback::FallbackStrategy;
51use super::react::{CompactorNode, LLMNode, ToolNode, build_react_graph};
52use super::request_opts::RequestOptions;
53use super::retry::RetryPolicy;
54use super::runtime::ToolUseLoop;
55use super::tools::{CompositeCatalog, ExecutableTool, StaticCatalog, ToolCatalog, ToolExecutor};
56use super::typed_state::{AgentState, AgentStateMerge};
57
58/// Agent 链式构建器 — 推荐的 Agent 创建方式。
59///
60/// 内部收集静态工具和动态目录,`build()` 时组装为 `ToolCatalog`,
61/// 再传给 `ToolUseLoop::new()`。所有 setter 返回 `self`(不借用),
62/// 支持流畅的链式调用。
63pub struct AgentBuilder {
64    model: ResolvedModel,
65    /// 收集通过 `.tool()` 注册的本地静态工具(最高优先级)
66    static_tools: Vec<ExecutableTool>,
67    /// 收集通过 `.catalog()` 注册的动态目录(按注册顺序,先绑定的优先级高于后绑定的)
68    catalogs: Vec<Arc<dyn ToolCatalog>>,
69    config: ToolUseConfig,
70    deps: ToolUseDeps,
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_canonical_hash_stability() {
79        // 相同输入应该产生相同的 hash
80        use std::collections::hash_map::DefaultHasher;
81        use std::hash::{Hash, Hasher};
82
83        let mut hasher1 = DefaultHasher::new();
84        let mut hasher2 = DefaultHasher::new();
85
86        // 模拟相同的 DSL 输入(工具顺序相同)
87        "test-provider".hash(&mut hasher1);
88        "test-model".hash(&mut hasher1);
89        "alpha".hash(&mut hasher1);
90        "beta".hash(&mut hasher1);
91        "test prompt".hash(&mut hasher1);
92        10usize.hash(&mut hasher1);
93        4096u32.hash(&mut hasher1);
94
95        "test-provider".hash(&mut hasher2);
96        "test-model".hash(&mut hasher2);
97        "alpha".hash(&mut hasher2);
98        "beta".hash(&mut hasher2);
99        "test prompt".hash(&mut hasher2);
100        10usize.hash(&mut hasher2);
101        4096u32.hash(&mut hasher2);
102
103        assert_eq!(
104            hasher1.finish(),
105            hasher2.finish(),
106            "Same inputs should produce same hash"
107        );
108    }
109
110    #[test]
111    fn test_canonical_hash_order_dependent() {
112        // D11: 工具顺序不同应该产生不同的 hash
113        // canonical = DSL 原貌,不做语义归一化
114        use std::collections::hash_map::DefaultHasher;
115        use std::hash::{Hash, Hasher};
116
117        let mut hasher1 = DefaultHasher::new();
118        let mut hasher2 = DefaultHasher::new();
119
120        // .tools([alpha, beta])
121        "alpha".hash(&mut hasher1);
122        "beta".hash(&mut hasher1);
123
124        // .tools([beta, alpha])
125        "beta".hash(&mut hasher2);
126        "alpha".hash(&mut hasher2);
127
128        assert_ne!(
129            hasher1.finish(),
130            hasher2.finish(),
131            "Tool order should affect hash — canonical = DSL 原貌"
132        );
133    }
134
135    #[test]
136    fn test_canonical_hash_different_inputs() {
137        // 不同输入应该产生不同的 hash
138        use std::collections::hash_map::DefaultHasher;
139        use std::hash::{Hash, Hasher};
140
141        let mut hasher1 = DefaultHasher::new();
142        let mut hasher2 = DefaultHasher::new();
143
144        "alpha".hash(&mut hasher1);
145        "alpha".hash(&mut hasher2);
146        "beta".hash(&mut hasher2);
147
148        assert_ne!(
149            hasher1.finish(),
150            hasher2.finish(),
151            "Different inputs should produce different hash"
152        );
153    }
154}
155
156impl AgentBuilder {
157    /// 创建构建器,绑定模型。
158    pub fn new(model: ResolvedModel) -> Self {
159        Self {
160            model,
161            static_tools: Vec::new(),
162            catalogs: Vec::new(),
163            config: ToolUseConfig::default(),
164            deps: ToolUseDeps::default(),
165        }
166    }
167
168    /// 注册工具。
169    pub fn tool(mut self, reg: ExecutableTool) -> Self {
170        self.static_tools.push(reg);
171        self
172    }
173
174    /// 批量注册工具。
175    pub fn tools(mut self, registrations: impl IntoIterator<Item = ExecutableTool>) -> Self {
176        self.static_tools.extend(registrations);
177        self
178    }
179
180    /// 绑定动态工具目录(MCP、插件系统等)。
181    ///
182    /// 可调用多次。按注册顺序,先绑定的优先级高于后绑定的。
183    /// 静态工具(`.tool()`)永远拥有最高优先级。
184    ///
185    /// # 示例
186    /// ```ignore
187    /// use std::sync::Arc;
188    /// use lellm_agent::{AgentBuilder, ToolCatalog};
189    /// use lellm_mcp::{McpCatalog, McpClient};
190    ///
191    /// let client = Arc::new(McpClient::with_transport(transport));
192    /// let catalog = McpCatalog::discover(client).await?;
193    /// let agent = AgentBuilder::new(model)
194    ///     .catalog(Arc::new(catalog))
195    ///     .build();
196    /// ```
197    pub fn catalog(mut self, catalog: Arc<dyn ToolCatalog>) -> Self {
198        self.catalogs.push(catalog);
199        self
200    }
201
202    /// 设置最大迭代轮次(默认 10)。
203    pub fn max_iterations(mut self, max: usize) -> Self {
204        self.config.max_iterations = max;
205        self
206    }
207
208    /// 设置每次 LLM 请求的最大输出 token 数(默认 4k)。
209    pub fn max_output_tokens(mut self, max: u32) -> Self {
210        self.config.max_output_tokens = max;
211        self
212    }
213
214    /// 设置整个 Agent Run 的最大输出 token 总数。
215    pub fn max_total_output_tokens(mut self, max: u32) -> Self {
216        self.config.max_total_output_tokens = Some(max);
217        self
218    }
219
220    /// 设置系统提示。
221    ///
222    /// 支持简单文本或分层 `Prompt`(通过 `From<String>` 自动转换)。
223    ///
224    /// # 示例
225    ///
226    /// ```ignore
227    /// // 简单文本
228    /// let agent = AgentBuilder::new(model)
229    ///     .system("你是一个有帮助的助手。")
230    ///     .build();
231    ///
232    /// // 分层构建 — 最大化前缀缓存
233    /// use lellm_core::Prompt;
234    /// let agent = AgentBuilder::new(model)
235    ///     .system(
236    ///         Prompt::new()
237    ///             .stable("核心身份…")
238    ///             .stable("工具指南…")
239    ///             .dynamic("会话上下文…")
240    ///             .build(),
241    ///     )
242    ///     .build();
243    /// ```
244    pub fn system(mut self, system: impl Into<Prompt>) -> Self {
245        self.config.system = Some(system.into());
246        self
247    }
248
249    /// 设置系统提示(纯文本)。
250    ///
251    /// 这是 `.system()` 的别名,保留用于向后兼容。
252    #[deprecated(since = "0.5.0", note = "Use `.system()` instead")]
253    pub fn system_prompt(mut self, prompt: String) -> Self {
254        self.config.system = Some(prompt.into());
255        self
256    }
257
258    /// 设置工具缓存策略(默认 `Auto`)。
259    ///
260    /// - `Auto`:为未设置 `cache_control` 的工具自动添加 `Breakpoint`
261    /// - `Preserve`:不修改用户设置的 `cache_control`
262    /// - `Disabled`:清除所有工具的 `cache_control`
263    pub fn tool_cache_policy(mut self, policy: ToolCachePolicy) -> Self {
264        self.config.tool_cache_policy = policy;
265        self
266    }
267
268    // ─── RequestOptions 快捷 setter ──────────────────────────
269
270    /// 设置完整的 RequestOptions(覆盖所有生成参数)。
271    pub fn request_options(mut self, opts: RequestOptions) -> Self {
272        self.config.request_options = opts;
273        self
274    }
275
276    /// 设置生成温度(0.0 ~ 2.0)。
277    pub fn temperature(mut self, t: f64) -> Self {
278        self.config.request_options.temperature = Some(t);
279        self
280    }
281
282    /// 设置 nucleus sampling 阈值(0.0 ~ 1.0)。
283    pub fn top_p(mut self, p: f64) -> Self {
284        self.config.request_options.top_p = Some(p);
285        self
286    }
287
288    /// 设置随机种子,保证可复现性。
289    pub fn seed(mut self, s: u64) -> Self {
290        self.config.request_options.seed = Some(s);
291        self
292    }
293
294    /// 设置工具选择策略(仅首轮生效)。
295    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
296        self.config.request_options.tool_choice = Some(choice);
297        self
298    }
299
300    /// 设置停止序列。
301    pub fn stop_sequences(mut self, seqs: Vec<String>) -> Self {
302        self.config.request_options.stop_sequences = Some(seqs);
303        self
304    }
305
306    /// 设置预填充文本(引导模型输出方向)。
307    pub fn prefill(mut self, text: String) -> Self {
308        self.config.request_options.prefill = Some(text);
309        self
310    }
311
312    /// 设置推理配置(控制模型是否进行深度推理)。
313    pub fn reasoning(mut self, r: ReasoningConfig) -> Self {
314        self.config.request_options.reasoning = Some(r);
315        self
316    }
317
318    /// 设置是否流式输出推理过程。
319    pub fn stream_thinking(mut self, enable: bool) -> Self {
320        self.config.stream_thinking = enable;
321        self
322    }
323
324    /// 设置单轮推理 Token 上限。
325    pub fn reasoning_budget(mut self, max: u32) -> Self {
326        self.config.request_options.max_reasoning_tokens = Some(max);
327        self
328    }
329
330    /// 设置整个 Agent Run 的最大推理 Token 总数。
331    pub fn max_total_reasoning_tokens(mut self, max: u32) -> Self {
332        self.config.max_total_reasoning_tokens = Some(max);
333        self
334    }
335
336    /// 设置 Fallback 策略。
337    pub fn fallback(mut self, fallback: Arc<dyn FallbackStrategy>) -> Self {
338        self.deps.fallback = fallback;
339        self
340    }
341
342    /// 设置工具重试策略。
343    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
344        self.config.retry_policy = policy;
345        self
346    }
347
348    /// 设置上下文预算(Token 上限 + 压缩策略)。
349    /// 若要关闭限制,设置 `max_tokens = usize::MAX`。
350    pub fn context_budget(mut self, budget: ContextBudget) -> Self {
351        self.config.context_budget = budget;
352        self
353    }
354
355    /// 计算 canonical AST hash — 保持 DSL 输入顺序。
356    ///
357    /// Hash 输入来自 DSL 层(model, tools, system prompt, config),
358    /// 不依赖 compiled graph 的 HashMap 迭代顺序。
359    /// 这保证:相同 DSL 输入永远产生相同 hash,Checkpoint 不会因此失效。
360    ///
361    /// # Hash 输入
362    ///
363    /// - provider_id(稳定字符串标识,如 "openai", "anthropic")
364    /// - model name
365    /// - tool names(保持 DSL 插入顺序,不排序)
366    /// - system prompt hash
367    /// - max_iterations
368    /// - max_output_tokens
369    ///
370    /// # 设计原则
371    ///
372    /// canonical = DSL 原貌,不做语义归一化。
373    /// `.tools([A, B])` 和 `.tools([B, A])` 产生不同 hash,
374    /// 因为它们是不同的 DSL 输入。
375    pub fn canonical_hash(&self) -> u64 {
376        use std::hash::{Hash, Hasher};
377
378        let mut hasher = std::collections::hash_map::DefaultHasher::new();
379
380        // 1. Model — 使用 provider_id()(稳定字符串)+ model 名称
381        self.model.provider.provider_id().hash(&mut hasher);
382        self.model.model.hash(&mut hasher);
383
384        // 2. Tools — 保持 DSL 插入顺序,不排序
385        //    .tools([A, B]) != .tools([B, A])
386        for t in &self.static_tools {
387            t.definition().name.hash(&mut hasher);
388        }
389
390        // 3. System prompt (hash 内容)
391        if let Some(ref system) = self.config.system {
392            format!("{:?}", system).hash(&mut hasher);
393        }
394
395        // 4. 结构性配置
396        self.config.max_iterations.hash(&mut hasher);
397        self.config.max_output_tokens.hash(&mut hasher);
398        self.config.max_total_output_tokens.hash(&mut hasher);
399        self.config.max_total_reasoning_tokens.hash(&mut hasher);
400
401        hasher.finish()
402    }
403
404    /// 构建 ReAct Graph — 返回 `Arc<Graph<AgentState>>`。
405    ///
406    /// 这是核心 API,返回共享的 Graph,可直接用 `graph.run_inline()` 执行。
407    /// Graph 携带 `canonical_hash`,用于 Checkpoint 的 graph compatibility 校验。
408    ///
409    /// # 示例
410    /// ```ignore
411    /// let graph = AgentBuilder::new(model).tools([...]).build();
412    ///
413    /// // 直接执行
414    /// let mut state = AgentState::from_messages(messages);
415    /// let mut ctx = ExecutionContext::new(
416    ///     &mut state,
417    ///     None,
418    ///     CancellationToken::new(),
419    ///     None,  // 不需要自动 checkpoint
420    ///     None,  // 不需要 Barrier
421    /// );
422    /// graph.run_inline(&mut ctx, max_steps).await?;
423    /// ```
424    pub fn build(self) -> Arc<Graph<AgentState, AgentStateMerge>> {
425        let canonical_hash = self.canonical_hash();
426        let (model, executor, config, deps) = self.into_parts();
427
428        let invoker = Arc::new(super::invoker::LlmInvoker::from_config(
429            model,
430            &config,
431            deps.fallback.clone(),
432        ));
433
434        let llm_node = LLMNode::new("llm", invoker, executor.clone(), config.clone());
435        let tool_node = ToolNode::new("tool", executor.clone(), config.clone());
436        let compactor_node = CompactorNode::new(
437            "compactor",
438            Arc::new(LocalCompactor::new()),
439            config.context_budget.clone(),
440        );
441
442        Arc::new(build_react_graph(
443            llm_node,
444            tool_node,
445            compactor_node,
446            canonical_hash,
447        ))
448    }
449
450    /// 编译为 ToolUseLoop — Facade 层。
451    ///
452    /// 返回 `ToolUseLoop`,持有共享的 Graph,提供 `invoke()` / `invoke_stream()` 等高级 API。
453    /// 内部调用 `Graph::run_inline()`,封装了 State 初始化和结果提取。
454    ///
455    /// 等价于手动组合:
456    /// ```ignore
457    /// let graph = AgentBuilder::new(model).tools([...]).build();
458    /// let agent = ToolUseLoop::new(graph, config);
459    /// ```
460    ///
461    /// # 示例
462    /// ```ignore
463    /// let result = AgentBuilder::new(model)
464    ///     .tools([search_tool, weather_tool])
465    ///     .compile()
466    ///     .invoke(messages)
467    ///     .await?;
468    /// ```
469    pub fn compile(self) -> ToolUseLoop {
470        let config = self.config.clone();
471        let graph = self.build();
472        ToolUseLoop::new(graph, config)
473    }
474
475    /// 内部辅助 — 分解为 (Model, Executor, Config, Deps)。
476    fn into_parts(self) -> (ResolvedModel, ToolExecutor, ToolUseConfig, ToolUseDeps) {
477        // 构造优先级队列:本地静态工具永远拥有最高优先级
478        let mut sources: Vec<Arc<dyn ToolCatalog>> = Vec::new();
479
480        if !self.static_tools.is_empty() {
481            sources.push(Arc::new(StaticCatalog::from_tools(self.static_tools)));
482        }
483
484        sources.extend(self.catalogs);
485
486        // 坍缩成最终的单根 Catalog
487        let final_catalog: Arc<dyn ToolCatalog> = match sources.len() {
488            0 => Arc::new(StaticCatalog::empty()),
489            1 => sources.remove(0),
490            _ => Arc::new(CompositeCatalog::new(sources)),
491        };
492
493        let executor =
494            ToolExecutor::with_retry_policy(final_catalog, self.config.retry_policy.clone());
495
496        (self.model, executor, self.config, self.deps)
497    }
498}