mixtape_core/agent/
builder.rs

1//! AgentBuilder for fluent agent construction
2//!
3//! The builder pattern allows configuring all agent options before
4//! creating the provider, moving the async work to `.build().await`.
5//!
6//! Also contains post-construction mutation methods for Agent (`set_*`, `add_*`)
7//! for runtime configuration changes.
8
9use std::collections::HashMap;
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::sync::RwLock;
15
16use crate::conversation::{BoxedConversationManager, SlidingWindowConversationManager};
17use crate::permission::{GrantStore, ToolAuthorizationPolicy, ToolCallAuthorizer};
18use crate::provider::ModelProvider;
19use crate::tool::{box_tool, DynTool, Tool};
20
21use super::context::{ContextConfig, ContextSource};
22use super::types::{DEFAULT_MAX_CONCURRENT_TOOLS, DEFAULT_PERMISSION_TIMEOUT};
23use super::Agent;
24
25#[cfg(feature = "session")]
26use crate::session::SessionStore;
27
28#[cfg(feature = "bedrock")]
29use crate::model::BedrockModel;
30#[cfg(feature = "bedrock")]
31use crate::provider::BedrockProvider;
32
33#[cfg(feature = "anthropic")]
34use crate::model::AnthropicModel;
35#[cfg(feature = "anthropic")]
36use crate::provider::AnthropicProvider;
37
38/// Factory function that creates a provider asynchronously
39type ProviderFactory = Box<
40    dyn FnOnce()
41            -> Pin<Box<dyn Future<Output = crate::error::Result<Arc<dyn ModelProvider>>> + Send>>
42        + Send,
43>;
44
45/// Builder for creating an Agent with fluent configuration
46///
47/// Use `Agent::builder()` to create a new builder, configure it with
48/// the various `with_*` methods, and call `.build().await` to create
49/// the agent.
50///
51/// # Example
52///
53/// ```ignore
54/// use mixtape_core::{Agent, ClaudeHaiku4_5, Result};
55///
56/// #[tokio::main]
57/// async fn main() -> Result<()> {
58///     let agent = Agent::builder()
59///         .bedrock(ClaudeHaiku4_5)
60///         .with_system_prompt("You are a helpful assistant")
61///         .add_tool(Calculator)
62///         .build()
63///         .await?;
64///
65///     let response = agent.run("What's 2 + 2?").await?;
66///     println!("{}", response);
67///     Ok(())
68/// }
69/// ```
70pub struct AgentBuilder {
71    provider_factory: Option<ProviderFactory>,
72    tools: Vec<Box<dyn DynTool>>,
73    system_prompt: Option<String>,
74    max_concurrent_tools: usize,
75    /// Custom grant store (if None, uses MemoryGrantStore)
76    pub(super) grant_store: Option<Box<dyn GrantStore>>,
77    /// Policy for tools without grants (default: AutoDeny)
78    pub(super) authorization_policy: ToolAuthorizationPolicy,
79    /// Timeout for authorization requests
80    pub(super) authorization_timeout: Duration,
81    conversation_manager: Option<BoxedConversationManager>,
82    #[cfg(feature = "session")]
83    session_store: Option<Arc<dyn SessionStore>>,
84    // MCP fields - configured via mcp.rs
85    #[cfg(feature = "mcp")]
86    pub(super) mcp_servers: Vec<crate::mcp::McpServerConfig>,
87    #[cfg(feature = "mcp")]
88    pub(super) mcp_config_files: Vec<std::path::PathBuf>,
89    // Context file fields
90    /// Context file sources (resolved at runtime)
91    context_sources: Vec<ContextSource>,
92    /// Context configuration (size limits)
93    context_config: ContextConfig,
94}
95
96impl Default for AgentBuilder {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102impl AgentBuilder {
103    /// Create a new AgentBuilder with default settings
104    pub fn new() -> Self {
105        Self {
106            provider_factory: None,
107            tools: Vec::new(),
108            system_prompt: None,
109            max_concurrent_tools: DEFAULT_MAX_CONCURRENT_TOOLS,
110            grant_store: None,
111            authorization_policy: ToolAuthorizationPolicy::default(), // AutoDeny by default
112            authorization_timeout: DEFAULT_PERMISSION_TIMEOUT,
113            conversation_manager: None,
114            #[cfg(feature = "session")]
115            session_store: None,
116            #[cfg(feature = "mcp")]
117            mcp_servers: Vec::new(),
118            #[cfg(feature = "mcp")]
119            mcp_config_files: Vec::new(),
120            context_sources: Vec::new(),
121            context_config: ContextConfig::default(),
122        }
123    }
124
125    /// Configure the agent to use AWS Bedrock with the specified model
126    ///
127    /// The AWS credentials will be loaded from the environment when
128    /// `.build().await` is called.
129    ///
130    /// # Example
131    ///
132    /// ```ignore
133    /// let agent = Agent::builder()
134    ///     .bedrock(ClaudeSonnet4_5)
135    ///     .build()
136    ///     .await?;
137    /// ```
138    #[cfg(feature = "bedrock")]
139    pub fn bedrock(mut self, model: impl BedrockModel + 'static) -> Self {
140        self.provider_factory = Some(Box::new(move || {
141            Box::pin(async move {
142                let provider = BedrockProvider::new(model).await?;
143                Ok(Arc::new(provider) as Arc<dyn ModelProvider>)
144            })
145        }));
146        self
147    }
148
149    /// Configure the agent to use the Anthropic API directly
150    ///
151    /// # Example
152    ///
153    /// ```ignore
154    /// let agent = Agent::builder()
155    ///     .anthropic(ClaudeSonnet4_5, "sk-ant-...")
156    ///     .build()
157    ///     .await?;
158    /// ```
159    #[cfg(feature = "anthropic")]
160    pub fn anthropic(
161        mut self,
162        model: impl AnthropicModel + 'static,
163        api_key: impl Into<String>,
164    ) -> Self {
165        let api_key = api_key.into();
166        self.provider_factory = Some(Box::new(move || {
167            Box::pin(async move {
168                let provider = AnthropicProvider::new(api_key, model)?;
169                Ok(Arc::new(provider) as Arc<dyn ModelProvider>)
170            })
171        }));
172        self
173    }
174
175    /// Configure the agent to use the Anthropic API with key from environment
176    ///
177    /// Reads `ANTHROPIC_API_KEY` from the environment.
178    ///
179    /// # Example
180    ///
181    /// ```ignore
182    /// let agent = Agent::builder()
183    ///     .anthropic_from_env(ClaudeSonnet4_5)
184    ///     .build()
185    ///     .await?;
186    /// ```
187    #[cfg(feature = "anthropic")]
188    pub fn anthropic_from_env(mut self, model: impl AnthropicModel + 'static) -> Self {
189        self.provider_factory = Some(Box::new(move || {
190            Box::pin(async move {
191                let provider = AnthropicProvider::from_env(model)?;
192                Ok(Arc::new(provider) as Arc<dyn ModelProvider>)
193            })
194        }));
195        self
196    }
197
198    /// Use a pre-configured provider
199    ///
200    /// Use this when you need custom provider configuration (e.g., custom
201    /// retry settings, inference profiles) or a custom provider implementation.
202    ///
203    /// # Example
204    ///
205    /// ```ignore
206    /// let provider = BedrockProvider::new(ClaudeSonnet4_5).await
207    ///     .with_max_retries(5)
208    ///     .with_inference_profile(InferenceProfile::US);
209    ///
210    /// let agent = Agent::builder()
211    ///     .provider(provider)
212    ///     .build()
213    ///     .await?;
214    /// ```
215    pub fn provider(mut self, provider: impl ModelProvider + 'static) -> Self {
216        let provider = Arc::new(provider) as Arc<dyn ModelProvider>;
217        self.provider_factory = Some(Box::new(move || Box::pin(async move { Ok(provider) })));
218        self
219    }
220
221    /// Add a tool to the agent
222    ///
223    /// # Example
224    ///
225    /// ```ignore
226    /// let agent = Agent::builder()
227    ///     .bedrock(ClaudeHaiku4_5)
228    ///     .add_tool(Calculator)
229    ///     .add_tool(WeatherLookup)
230    ///     .build()
231    ///     .await?;
232    /// ```
233    pub fn add_tool(mut self, tool: impl Tool + 'static) -> Self {
234        self.tools.push(box_tool(tool));
235        self
236    }
237
238    /// Add multiple tools to the agent
239    ///
240    /// Accepts pre-boxed dynamic tools, typically from tool group helper functions.
241    ///
242    /// # Example
243    ///
244    /// ```ignore
245    /// use mixtape_tools::sqlite;
246    ///
247    /// // Add all read-only SQLite tools
248    /// let agent = Agent::builder()
249    ///     .bedrock(ClaudeHaiku4_5)
250    ///     .add_tools(sqlite::read_only_tools())
251    ///     .build()
252    ///     .await?;
253    ///
254    /// // Or add all SQLite tools
255    /// let agent = Agent::builder()
256    ///     .bedrock(ClaudeHaiku4_5)
257    ///     .add_tools(sqlite::all_tools())
258    ///     .build()
259    ///     .await?;
260    /// ```
261    pub fn add_tools(mut self, tools: impl IntoIterator<Item = Box<dyn DynTool>>) -> Self {
262        self.tools.extend(tools);
263        self
264    }
265
266    /// Set the system prompt
267    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
268        self.system_prompt = Some(prompt.into());
269        self
270    }
271
272    /// Set the maximum number of tools that can execute concurrently
273    pub fn with_max_concurrent_tools(mut self, max: usize) -> Self {
274        self.max_concurrent_tools = max;
275        self
276    }
277
278    // Authorization methods are in permission.rs:
279    // - with_grant_store
280    // - with_authorization_timeout
281
282    /// Set a custom conversation manager
283    pub fn with_conversation_manager(
284        mut self,
285        manager: impl crate::conversation::ConversationManager + 'static,
286    ) -> Self {
287        self.conversation_manager = Some(Box::new(manager));
288        self
289    }
290
291    /// Enable session management for conversation memory
292    #[cfg(feature = "session")]
293    pub fn with_session_store(mut self, store: impl SessionStore + 'static) -> Self {
294        self.session_store = Some(Arc::new(store));
295        self
296    }
297
298    // Context file methods
299
300    /// Add literal string content as context
301    ///
302    /// The content will be included directly in the system prompt.
303    /// Use this for dynamic context that doesn't come from a file.
304    ///
305    /// # Example
306    /// ```ignore
307    /// let agent = Agent::builder()
308    ///     .bedrock(ClaudeSonnet4_5)
309    ///     .add_context("# Project Rules\nAlways use async/await.")
310    ///     .build()
311    ///     .await?;
312    /// ```
313    pub fn add_context(mut self, content: impl Into<String>) -> Self {
314        self.context_sources.push(ContextSource::Content {
315            content: content.into(),
316        });
317        self
318    }
319
320    /// Add a required context file
321    ///
322    /// The path supports variable substitution:
323    /// - `$CWD` - current working directory at resolution time
324    /// - `$HOME` or `~` - user's home directory
325    ///
326    /// Relative paths are resolved against the current working directory.
327    /// The file must exist or an error is returned at runtime.
328    ///
329    /// Context files are resolved at runtime (each `run()` call), allowing
330    /// files to change between runs.
331    ///
332    /// # Example
333    /// ```ignore
334    /// let agent = Agent::builder()
335    ///     .bedrock(ClaudeSonnet4_5)
336    ///     .add_context_file("~/.config/myagent/rules.md")
337    ///     .build()
338    ///     .await?;
339    /// ```
340    pub fn add_context_file(mut self, path: impl Into<String>) -> Self {
341        self.context_sources.push(ContextSource::File {
342            path: path.into(),
343            required: true,
344        });
345        self
346    }
347
348    /// Add an optional context file
349    ///
350    /// Same as `add_context_file()` but the file is optional.
351    /// If the file doesn't exist, it will be silently skipped.
352    ///
353    /// # Example
354    /// ```ignore
355    /// let agent = Agent::builder()
356    ///     .bedrock(ClaudeSonnet4_5)
357    ///     .add_optional_context_file("AGENTS.md")
358    ///     .build()
359    ///     .await?;
360    /// ```
361    pub fn add_optional_context_file(mut self, path: impl Into<String>) -> Self {
362        self.context_sources.push(ContextSource::File {
363            path: path.into(),
364            required: false,
365        });
366        self
367    }
368
369    /// Add multiple required context files
370    ///
371    /// All files must exist or an error is returned at runtime.
372    /// Files are loaded in the order provided.
373    ///
374    /// # Example
375    /// ```ignore
376    /// let agent = Agent::builder()
377    ///     .bedrock(ClaudeSonnet4_5)
378    ///     .add_context_files(["rules.md", "examples.md"])
379    ///     .build()
380    ///     .await?;
381    /// ```
382    pub fn add_context_files(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
383        self.context_sources.push(ContextSource::Files {
384            paths: paths.into_iter().map(|p| p.into()).collect(),
385            required: true,
386        });
387        self
388    }
389
390    /// Add multiple optional context files
391    ///
392    /// Files that exist are loaded; missing files are skipped.
393    /// Files are loaded in the order provided.
394    ///
395    /// # Example
396    /// ```ignore
397    /// let agent = Agent::builder()
398    ///     .bedrock(ClaudeSonnet4_5)
399    ///     .add_optional_context_files(["AGENTS.md", "agents.md", "CLAUDE.md"])
400    ///     .build()
401    ///     .await?;
402    /// ```
403    pub fn add_optional_context_files(
404        mut self,
405        paths: impl IntoIterator<Item = impl Into<String>>,
406    ) -> Self {
407        self.context_sources.push(ContextSource::Files {
408            paths: paths.into_iter().map(|p| p.into()).collect(),
409            required: false,
410        });
411        self
412    }
413
414    /// Add context files matching a glob pattern
415    ///
416    /// The pattern supports variable substitution (same as `add_context_file()`).
417    /// Files matching the pattern are sorted alphabetically and loaded in order.
418    ///
419    /// Glob patterns are inherently optional - zero matches is acceptable.
420    ///
421    /// # Example
422    /// ```ignore
423    /// let agent = Agent::builder()
424    ///     .bedrock(ClaudeSonnet4_5)
425    ///     .add_context_files_glob("$CWD/.context/*.md")
426    ///     .build()
427    ///     .await?;
428    /// ```
429    pub fn add_context_files_glob(mut self, pattern: impl Into<String>) -> Self {
430        self.context_sources.push(ContextSource::Glob {
431            pattern: pattern.into(),
432        });
433        self
434    }
435
436    /// Configure context file size limits
437    ///
438    /// # Example
439    /// ```ignore
440    /// use mixtape_core::ContextConfig;
441    ///
442    /// let agent = Agent::builder()
443    ///     .bedrock(ClaudeSonnet4_5)
444    ///     .with_context_config(ContextConfig {
445    ///         max_file_size: 512 * 1024,       // 512KB per file
446    ///         max_total_size: 2 * 1024 * 1024, // 2MB total
447    ///     })
448    ///     .with_context_pattern("$CWD/docs/*.md")
449    ///     .build()
450    ///     .await?;
451    /// ```
452    pub fn with_context_config(mut self, config: ContextConfig) -> Self {
453        self.context_config = config;
454        self
455    }
456
457    // MCP methods are in mcp.rs:
458    // - with_mcp_server
459    // - with_mcp_config_file
460
461    /// Build the agent
462    ///
463    /// This is where the async provider creation happens. For Bedrock,
464    /// this loads AWS credentials from the environment.
465    ///
466    /// # Errors
467    ///
468    /// Returns an error if no provider was configured (call `.bedrock()`,
469    /// `.anthropic()`, or `.provider()` first).
470    ///
471    /// # Example
472    ///
473    /// ```ignore
474    /// let agent = Agent::builder()
475    ///     .bedrock(ClaudeHaiku4_5)
476    ///     .build()
477    ///     .await?;
478    /// ```
479    pub async fn build(self) -> crate::error::Result<Agent> {
480        let provider_factory = self
481            .provider_factory
482            .ok_or_else(|| crate::error::Error::Config(
483                "No provider configured. Call .bedrock(), .anthropic(), or .provider() before .build()".to_string()
484            ))?;
485
486        let provider = provider_factory().await?;
487
488        let conversation_manager = self
489            .conversation_manager
490            .unwrap_or_else(|| Box::new(SlidingWindowConversationManager::new()));
491
492        // Create authorizer with custom store or default MemoryGrantStore,
493        // and apply the configured policy
494        let authorizer = match self.grant_store {
495            Some(store) => ToolCallAuthorizer::with_boxed_store(store),
496            None => ToolCallAuthorizer::new(),
497        }
498        .with_authorization_policy(self.authorization_policy);
499
500        #[allow(unused_mut)]
501        let mut agent = Agent {
502            provider,
503            system_prompt: self.system_prompt,
504            max_concurrent_tools: self.max_concurrent_tools,
505            tools: self.tools,
506            hooks: Arc::new(parking_lot::RwLock::new(Vec::new())),
507            authorizer: Arc::new(RwLock::new(authorizer)),
508            authorization_timeout: self.authorization_timeout,
509            pending_authorizations: Arc::new(RwLock::new(HashMap::new())),
510            #[cfg(feature = "mcp")]
511            mcp_clients: Vec::new(),
512            conversation_manager: parking_lot::RwLock::new(conversation_manager),
513            #[cfg(feature = "session")]
514            session_store: self.session_store,
515            // Context file fields
516            context_sources: self.context_sources,
517            context_config: self.context_config,
518            last_context_result: parking_lot::RwLock::new(None),
519        };
520
521        // Connect to MCP servers specified in builder
522        #[cfg(feature = "mcp")]
523        {
524            super::mcp::connect_mcp_servers(&mut agent, self.mcp_servers, self.mcp_config_files)
525                .await?;
526        }
527
528        Ok(agent)
529    }
530}
531
532impl Agent {
533    /// Create a new AgentBuilder for fluent configuration
534    ///
535    /// # Example
536    ///
537    /// ```ignore
538    /// use mixtape_core::{Agent, ClaudeHaiku4_5, Result};
539    ///
540    /// #[tokio::main]
541    /// async fn main() -> Result<()> {
542    ///     let agent = Agent::builder()
543    ///         .bedrock(ClaudeHaiku4_5)
544    ///         .with_system_prompt("You are a helpful assistant")
545    ///         .build()
546    ///         .await?;
547    ///
548    ///     let response = agent.run("Hello!").await?;
549    ///     println!("{}", response);
550    ///     Ok(())
551    /// }
552    /// ```
553    pub fn builder() -> AgentBuilder {
554        AgentBuilder::new()
555    }
556
557    // Post-construction methods are in their respective modules:
558    // - add_mcp_server, add_mcp_config_file are in mcp.rs
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use crate::box_tools;
565    use crate::conversation::SimpleConversationManager;
566    use crate::provider::{ModelProvider, ProviderError};
567    use crate::types::{ContentBlock, Message, Role, StopReason, ToolDefinition};
568    use crate::ModelResponse;
569
570    /// Mock provider for builder tests
571    #[derive(Clone)]
572    struct MockProvider;
573
574    #[async_trait::async_trait]
575    impl ModelProvider for MockProvider {
576        fn name(&self) -> &str {
577            "MockProvider"
578        }
579
580        fn max_context_tokens(&self) -> usize {
581            200_000
582        }
583
584        fn max_output_tokens(&self) -> usize {
585            8_192
586        }
587
588        async fn generate(
589            &self,
590            _messages: Vec<Message>,
591            _tools: Vec<ToolDefinition>,
592            _system_prompt: Option<String>,
593        ) -> Result<ModelResponse, ProviderError> {
594            Ok(ModelResponse {
595                message: Message {
596                    role: Role::Assistant,
597                    content: vec![ContentBlock::Text("ok".to_string())],
598                },
599                stop_reason: StopReason::EndTurn,
600                usage: None,
601            })
602        }
603    }
604
605    #[test]
606    fn test_builder_creation() {
607        let builder = Agent::builder();
608        assert!(builder.provider_factory.is_none());
609        assert!(builder.tools.is_empty());
610        assert!(builder.system_prompt.is_none());
611    }
612
613    #[test]
614    fn test_builder_default() {
615        let builder = AgentBuilder::default();
616        assert!(builder.provider_factory.is_none());
617        assert_eq!(builder.max_concurrent_tools, DEFAULT_MAX_CONCURRENT_TOOLS);
618        assert_eq!(builder.authorization_timeout, DEFAULT_PERMISSION_TIMEOUT);
619    }
620
621    #[test]
622    fn test_builder_system_prompt() {
623        let builder = Agent::builder().with_system_prompt("Test prompt");
624        assert_eq!(builder.system_prompt, Some("Test prompt".to_string()));
625    }
626
627    #[test]
628    fn test_builder_max_concurrent_tools() {
629        let builder = Agent::builder().with_max_concurrent_tools(4);
630        assert_eq!(builder.max_concurrent_tools, 4);
631    }
632
633    #[test]
634    fn test_builder_conversation_manager() {
635        let builder =
636            Agent::builder().with_conversation_manager(SimpleConversationManager::new(100));
637        assert!(builder.conversation_manager.is_some());
638    }
639
640    #[tokio::test]
641    async fn test_build_with_provider() {
642        let agent = Agent::builder()
643            .provider(MockProvider)
644            .build()
645            .await
646            .unwrap();
647
648        assert_eq!(agent.provider.name(), "MockProvider");
649    }
650
651    #[tokio::test]
652    async fn test_build_with_system_prompt() {
653        let agent = Agent::builder()
654            .provider(MockProvider)
655            .with_system_prompt("Be helpful")
656            .build()
657            .await
658            .unwrap();
659
660        assert_eq!(agent.system_prompt, Some("Be helpful".to_string()));
661    }
662
663    #[tokio::test]
664    async fn test_build_with_conversation_manager() {
665        let agent = Agent::builder()
666            .provider(MockProvider)
667            .with_conversation_manager(SimpleConversationManager::new(100))
668            .build()
669            .await
670            .unwrap();
671
672        // Just verify it built successfully with custom manager
673        assert_eq!(agent.provider.name(), "MockProvider");
674    }
675
676    #[tokio::test]
677    async fn test_build_without_provider_fails() {
678        let result = Agent::builder().build().await;
679        match result {
680            Err(err) => assert!(err.is_config()),
681            Ok(_) => panic!("Expected error when building without provider"),
682        }
683    }
684
685    #[tokio::test]
686    async fn test_builder_chaining() {
687        let agent = Agent::builder()
688            .provider(MockProvider)
689            .with_system_prompt("Test")
690            .with_max_concurrent_tools(8)
691            .with_authorization_timeout(Duration::from_secs(60))
692            .build()
693            .await
694            .unwrap();
695
696        assert_eq!(agent.system_prompt, Some("Test".to_string()));
697        assert_eq!(agent.max_concurrent_tools, 8);
698        assert_eq!(agent.authorization_timeout, Duration::from_secs(60));
699    }
700
701    // ===== add_tool/add_tools Builder Tests =====
702
703    #[test]
704    fn test_builder_add_tool_single() {
705        use crate::tool::{Tool, ToolError, ToolResult};
706        use schemars::JsonSchema;
707        use serde::{Deserialize, Serialize};
708
709        #[derive(Debug, Deserialize, Serialize, JsonSchema)]
710        #[allow(dead_code)]
711        struct TestInput {
712            value: String,
713        }
714
715        struct TestTool;
716
717        impl Tool for TestTool {
718            type Input = TestInput;
719            fn name(&self) -> &str {
720                "test_tool"
721            }
722            fn description(&self) -> &str {
723                "A test tool"
724            }
725            async fn execute(&self, _input: Self::Input) -> Result<ToolResult, ToolError> {
726                Ok(ToolResult::text("result"))
727            }
728        }
729
730        let builder = Agent::builder().add_tool(TestTool);
731        assert_eq!(builder.tools.len(), 1);
732        assert_eq!(builder.tools[0].name(), "test_tool");
733    }
734
735    #[test]
736    fn test_builder_add_tools_multiple() {
737        use crate::tool::{Tool, ToolError, ToolResult};
738        use schemars::JsonSchema;
739        use serde::{Deserialize, Serialize};
740
741        #[derive(Debug, Deserialize, Serialize, JsonSchema)]
742        #[allow(dead_code)]
743        struct TestInput {
744            value: String,
745        }
746
747        #[derive(Clone)]
748        struct TestTool {
749            name: &'static str,
750            description: &'static str,
751        }
752
753        impl Tool for TestTool {
754            type Input = TestInput;
755            fn name(&self) -> &str {
756                self.name
757            }
758            fn description(&self) -> &str {
759                self.description
760            }
761            async fn execute(&self, _input: Self::Input) -> Result<ToolResult, ToolError> {
762                Ok(ToolResult::text(self.name))
763            }
764        }
765
766        let builder = Agent::builder().add_tools(box_tools![
767            TestTool {
768                name: "tool1",
769                description: "First tool",
770            },
771            TestTool {
772                name: "tool2",
773                description: "Second tool",
774            },
775            TestTool {
776                name: "tool3",
777                description: "Third tool",
778            },
779        ]);
780
781        assert_eq!(builder.tools.len(), 3);
782        assert_eq!(builder.tools[0].name(), "tool1");
783        assert_eq!(builder.tools[1].name(), "tool2");
784        assert_eq!(builder.tools[2].name(), "tool3");
785    }
786
787    #[test]
788    fn test_builder_add_tools_empty() {
789        use crate::tool::{Tool, ToolError, ToolResult};
790        use schemars::JsonSchema;
791        use serde::{Deserialize, Serialize};
792
793        #[derive(Debug, Deserialize, Serialize, JsonSchema)]
794        #[allow(dead_code)]
795        struct TestInput {
796            value: String,
797        }
798
799        #[allow(dead_code)]
800        struct TestTool;
801        impl Tool for TestTool {
802            type Input = TestInput;
803            fn name(&self) -> &str {
804                "test"
805            }
806            fn description(&self) -> &str {
807                "Test"
808            }
809            async fn execute(&self, _input: Self::Input) -> Result<ToolResult, ToolError> {
810                Ok(ToolResult::text("ok"))
811            }
812        }
813
814        let builder = Agent::builder().add_tools(box_tools![]);
815
816        assert_eq!(builder.tools.len(), 0);
817    }
818
819    #[test]
820    fn test_builder_add_tool_and_add_tools_chaining() {
821        use crate::tool::{Tool, ToolError, ToolResult};
822        use schemars::JsonSchema;
823        use serde::{Deserialize, Serialize};
824
825        #[derive(Debug, Deserialize, Serialize, JsonSchema)]
826        struct TestInput {}
827
828        struct Tool1;
829        impl Tool for Tool1 {
830            type Input = TestInput;
831            fn name(&self) -> &str {
832                "tool1"
833            }
834            fn description(&self) -> &str {
835                "First"
836            }
837            async fn execute(&self, _input: Self::Input) -> Result<ToolResult, ToolError> {
838                Ok(ToolResult::text("1"))
839            }
840        }
841
842        #[derive(Clone)]
843        struct Tool2;
844        impl Tool for Tool2 {
845            type Input = TestInput;
846            fn name(&self) -> &str {
847                "tool2"
848            }
849            fn description(&self) -> &str {
850                "Second"
851            }
852            async fn execute(&self, _input: Self::Input) -> Result<ToolResult, ToolError> {
853                Ok(ToolResult::text("2"))
854            }
855        }
856
857        // Mix add_tool (single) with box_tools! macro
858        let builder = Agent::builder()
859            .add_tool(Tool1)
860            .add_tools(box_tools![Tool2, Tool2]);
861
862        assert_eq!(builder.tools.len(), 3);
863        assert_eq!(builder.tools[0].name(), "tool1");
864        assert_eq!(builder.tools[1].name(), "tool2");
865        assert_eq!(builder.tools[2].name(), "tool2");
866    }
867
868    #[tokio::test]
869    async fn test_build_with_add_tools() {
870        use crate::tool::{Tool, ToolError, ToolResult};
871        use schemars::JsonSchema;
872        use serde::{Deserialize, Serialize};
873
874        #[derive(Debug, Deserialize, Serialize, JsonSchema)]
875        struct TestInput {}
876
877        #[derive(Clone)]
878        struct NamedTool {
879            tool_name: &'static str,
880            tool_desc: &'static str,
881        }
882
883        impl Tool for NamedTool {
884            type Input = TestInput;
885            fn name(&self) -> &str {
886                self.tool_name
887            }
888            fn description(&self) -> &str {
889                self.tool_desc
890            }
891            async fn execute(&self, _input: Self::Input) -> Result<ToolResult, ToolError> {
892                Ok(ToolResult::text(self.tool_name))
893            }
894        }
895
896        let agent = Agent::builder()
897            .provider(MockProvider)
898            .add_tools(box_tools![
899                NamedTool {
900                    tool_name: "calculator",
901                    tool_desc: "Calculates things",
902                },
903                NamedTool {
904                    tool_name: "weather",
905                    tool_desc: "Gets weather",
906                },
907            ])
908            .build()
909            .await
910            .unwrap();
911
912        let tools = agent.list_tools();
913        assert_eq!(tools.len(), 2);
914
915        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
916        assert!(names.contains(&"calculator"));
917        assert!(names.contains(&"weather"));
918    }
919}