1use 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
58pub struct AgentBuilder {
64 model: ResolvedModel,
65 static_tools: Vec<ExecutableTool>,
67 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 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 "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 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 "alpha".hash(&mut hasher1);
122 "beta".hash(&mut hasher1);
123
124 "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 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 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 pub fn tool(mut self, reg: ExecutableTool) -> Self {
170 self.static_tools.push(reg);
171 self
172 }
173
174 pub fn tools(mut self, registrations: impl IntoIterator<Item = ExecutableTool>) -> Self {
176 self.static_tools.extend(registrations);
177 self
178 }
179
180 pub fn catalog(mut self, catalog: Arc<dyn ToolCatalog>) -> Self {
198 self.catalogs.push(catalog);
199 self
200 }
201
202 pub fn max_iterations(mut self, max: usize) -> Self {
204 self.config.max_iterations = max;
205 self
206 }
207
208 pub fn max_output_tokens(mut self, max: u32) -> Self {
210 self.config.max_output_tokens = max;
211 self
212 }
213
214 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 pub fn system(mut self, system: impl Into<Prompt>) -> Self {
245 self.config.system = Some(system.into());
246 self
247 }
248
249 #[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 pub fn tool_cache_policy(mut self, policy: ToolCachePolicy) -> Self {
264 self.config.tool_cache_policy = policy;
265 self
266 }
267
268 pub fn request_options(mut self, opts: RequestOptions) -> Self {
272 self.config.request_options = opts;
273 self
274 }
275
276 pub fn temperature(mut self, t: f64) -> Self {
278 self.config.request_options.temperature = Some(t);
279 self
280 }
281
282 pub fn top_p(mut self, p: f64) -> Self {
284 self.config.request_options.top_p = Some(p);
285 self
286 }
287
288 pub fn seed(mut self, s: u64) -> Self {
290 self.config.request_options.seed = Some(s);
291 self
292 }
293
294 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
296 self.config.request_options.tool_choice = Some(choice);
297 self
298 }
299
300 pub fn stop_sequences(mut self, seqs: Vec<String>) -> Self {
302 self.config.request_options.stop_sequences = Some(seqs);
303 self
304 }
305
306 pub fn prefill(mut self, text: String) -> Self {
308 self.config.request_options.prefill = Some(text);
309 self
310 }
311
312 pub fn reasoning(mut self, r: ReasoningConfig) -> Self {
314 self.config.request_options.reasoning = Some(r);
315 self
316 }
317
318 pub fn stream_thinking(mut self, enable: bool) -> Self {
320 self.config.stream_thinking = enable;
321 self
322 }
323
324 pub fn reasoning_budget(mut self, max: u32) -> Self {
326 self.config.request_options.max_reasoning_tokens = Some(max);
327 self
328 }
329
330 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 pub fn fallback(mut self, fallback: Arc<dyn FallbackStrategy>) -> Self {
338 self.deps.fallback = fallback;
339 self
340 }
341
342 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
344 self.config.retry_policy = policy;
345 self
346 }
347
348 pub fn context_budget(mut self, budget: ContextBudget) -> Self {
351 self.config.context_budget = budget;
352 self
353 }
354
355 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 self.model.provider.provider_id().hash(&mut hasher);
382 self.model.model.hash(&mut hasher);
383
384 for t in &self.static_tools {
387 t.definition().name.hash(&mut hasher);
388 }
389
390 if let Some(ref system) = self.config.system {
392 format!("{:?}", system).hash(&mut hasher);
393 }
394
395 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 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 pub fn compile(self) -> ToolUseLoop {
470 let config = self.config.clone();
471 let graph = self.build();
472 ToolUseLoop::new(graph, config)
473 }
474
475 fn into_parts(self) -> (ResolvedModel, ToolExecutor, ToolUseConfig, ToolUseDeps) {
477 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 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}