Skip to main content

claude_agent/agent/options/
builder.rs

1//! Fluent builder for configuring and constructing [`crate::Agent`] instances.
2//!
3//! # Overview
4//!
5//! `AgentBuilder` provides a chainable API for configuring all aspects of an agent:
6//! model selection, tool access, security policies, budget limits, and more.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! use claude_agent::{Agent, ToolAccess, Auth};
12//!
13//! # async fn example() -> claude_agent::Result<()> {
14//! let agent = Agent::builder()
15//!     .auth(Auth::from_env()).await?
16//!     .model("claude-sonnet-4-5")
17//!     .tools(ToolAccess::all())
18//!     .working_dir("./project")
19//!     .max_iterations(50)
20//!     .build()
21//!     .await?;
22//! # Ok(())
23//! # }
24//! ```
25
26use std::path::PathBuf;
27use std::sync::Arc;
28use std::time::Duration;
29
30use crate::auth::{Credential, OAuthConfig};
31use crate::budget::TenantBudgetManager;
32use crate::client::{CloudProvider, FallbackConfig, ModelConfig, ProviderConfig};
33use crate::common::IndexRegistry;
34use crate::context::{LeveledMemoryProvider, RuleIndex};
35use crate::hooks::{Hook, HookManager};
36use crate::output_style::OutputStyle;
37use crate::permissions::{PermissionMode, PermissionPolicy, PermissionRule};
38use crate::skills::SkillIndex;
39use crate::subagents::{SubagentIndex, builtin_subagents};
40use crate::tools::{Tool, ToolAccess};
41
42use crate::agent::config::{AgentConfig, SystemPromptMode};
43
44/// Default number of messages to preserve during context compaction.
45pub const DEFAULT_COMPACT_KEEP_MESSAGES: usize = 4;
46
47/// Fluent builder for constructing [`crate::Agent`] instances with custom configuration.
48///
49/// Use [`crate::Agent::builder()`] to create a new builder instance.
50#[derive(Default)]
51pub struct AgentBuilder {
52    pub(super) config: AgentConfig,
53    pub(super) credential: Option<Credential>,
54    pub(super) auth_type: Option<crate::auth::Auth>,
55    pub(super) oauth_config: Option<OAuthConfig>,
56    pub(super) cloud_provider: Option<CloudProvider>,
57    pub(super) model_config: Option<ModelConfig>,
58    pub(super) provider_config: Option<ProviderConfig>,
59    pub(super) skill_registry: Option<IndexRegistry<SkillIndex>>,
60    pub(super) subagent_registry: Option<IndexRegistry<SubagentIndex>>,
61    pub(super) rule_indices: Vec<RuleIndex>,
62    pub(super) hooks: HookManager,
63    pub(super) custom_tools: Vec<Arc<dyn Tool>>,
64    pub(super) memory_provider: Option<LeveledMemoryProvider>,
65    pub(super) sandbox_settings: Option<crate::config::SandboxSettings>,
66    pub(super) initial_messages: Option<Vec<crate::types::Message>>,
67    pub(super) resume_session_id: Option<String>,
68    pub(super) resumed_session: Option<crate::session::Session>,
69    pub(super) tenant_budget_manager: Option<TenantBudgetManager>,
70    pub(super) fallback_config: Option<FallbackConfig>,
71    pub(super) output_style_name: Option<String>,
72    pub(super) mcp_configs: std::collections::HashMap<String, crate::mcp::McpServerConfig>,
73    pub(super) mcp_manager: Option<std::sync::Arc<crate::mcp::McpManager>>,
74    pub(super) mcp_toolset_registry: Option<crate::mcp::McpToolsetRegistry>,
75    pub(super) tool_search_config: Option<crate::tools::ToolSearchConfig>,
76    pub(super) tool_search_manager: Option<std::sync::Arc<crate::tools::ToolSearchManager>>,
77    pub(super) session_manager: Option<crate::session::SessionManager>,
78
79    // Resource level flags - loaded in fixed order during build()
80    // Order: Enterprise → User → Project → Local (later overrides earlier)
81    pub(super) load_enterprise: bool,
82    pub(super) load_user: bool,
83    pub(super) load_project: bool,
84    pub(super) load_local: bool,
85
86    #[cfg(feature = "aws")]
87    pub(super) aws_region: Option<String>,
88    #[cfg(feature = "gcp")]
89    pub(super) gcp_project: Option<String>,
90    #[cfg(feature = "gcp")]
91    pub(super) gcp_region: Option<String>,
92    #[cfg(feature = "azure")]
93    pub(super) azure_resource: Option<String>,
94
95    #[cfg(feature = "plugins")]
96    pub(super) plugin_dirs: Vec<PathBuf>,
97}
98
99impl AgentBuilder {
100    /// Creates a new builder with default configuration.
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    // =========================================================================
106    // Configuration
107    // =========================================================================
108
109    /// Sets the complete agent configuration, replacing all defaults.
110    pub fn agent_config(mut self, config: AgentConfig) -> Self {
111        self.config = config;
112        self
113    }
114
115    /// Sets the API provider configuration (timeouts, beta features, etc.).
116    pub fn provider_config(mut self, config: ProviderConfig) -> Self {
117        self.provider_config = Some(config);
118        self
119    }
120
121    // =========================================================================
122    // Authentication
123    // =========================================================================
124
125    /// Configures authentication for the API.
126    ///
127    /// # Supported Methods
128    /// - `Auth::from_env()` - Uses `ANTHROPIC_API_KEY` environment variable
129    /// - `Auth::api_key("sk-...")` - Explicit API key
130    /// - `Auth::claude_cli()` - Uses Claude CLI OAuth (requires `cli-integration` feature)
131    /// - `Auth::bedrock("region")` - AWS Bedrock (requires `aws` feature)
132    /// - `Auth::vertex("project", "region")` - GCP Vertex AI (requires `gcp` feature)
133    ///
134    /// # Example
135    /// ```rust,no_run
136    /// # use claude_agent::{Agent, Auth};
137    /// # async fn example() -> claude_agent::Result<()> {
138    /// let agent = Agent::builder()
139    ///     .auth(Auth::from_env()).await?
140    ///     .build().await?;
141    /// # Ok(())
142    /// # }
143    /// ```
144    pub async fn auth(mut self, auth: impl Into<crate::auth::Auth>) -> crate::Result<Self> {
145        let auth = auth.into();
146
147        #[allow(unreachable_patterns)]
148        match &auth {
149            #[cfg(feature = "aws")]
150            crate::auth::Auth::Bedrock { region } => {
151                self.cloud_provider = Some(CloudProvider::Bedrock);
152                self.aws_region = Some(region.clone());
153                self.model_config = Some(ModelConfig::bedrock());
154                self = self.apply_provider_models();
155            }
156            #[cfg(feature = "gcp")]
157            crate::auth::Auth::Vertex { project, region } => {
158                self.cloud_provider = Some(CloudProvider::Vertex);
159                self.gcp_project = Some(project.clone());
160                self.gcp_region = Some(region.clone());
161                self.model_config = Some(ModelConfig::vertex());
162                self = self.apply_provider_models();
163            }
164            #[cfg(feature = "azure")]
165            crate::auth::Auth::Foundry { resource } => {
166                self.cloud_provider = Some(CloudProvider::Foundry);
167                self.azure_resource = Some(resource.clone());
168                self.model_config = Some(ModelConfig::foundry());
169                self = self.apply_provider_models();
170            }
171            _ => {}
172        }
173
174        let credential = auth.resolve().await?;
175        if !credential.is_default() {
176            self.credential = Some(credential);
177        }
178
179        self.auth_type = Some(auth);
180
181        if self.supports_server_tools() {
182            self.config.server_tools = crate::agent::config::ServerToolsConfig::all();
183        }
184
185        Ok(self)
186    }
187
188    /// Sets OAuth configuration for token refresh.
189    pub fn oauth_config(mut self, config: OAuthConfig) -> Self {
190        self.oauth_config = Some(config);
191        self
192    }
193
194    /// Returns whether the current auth method supports server-side tools.
195    pub fn supports_server_tools(&self) -> bool {
196        self.auth_type
197            .as_ref()
198            .map(|a| a.supports_server_tools())
199            .unwrap_or(true)
200    }
201
202    // =========================================================================
203    // Model Configuration
204    // =========================================================================
205
206    /// Sets both primary and small model configurations.
207    pub fn models(mut self, config: ModelConfig) -> Self {
208        self.model_config = Some(config.clone());
209        self.config.model.primary = config.primary;
210        self.config.model.small = config.small;
211        self
212    }
213
214    #[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))]
215    fn apply_provider_models(mut self) -> Self {
216        if let Some(ref config) = self.model_config {
217            if self.config.model.primary
218                == crate::agent::config::AgentModelConfig::default().primary
219            {
220                self.config.model.primary = config.primary.clone();
221            }
222            if self.config.model.small == crate::agent::config::AgentModelConfig::default().small {
223                self.config.model.small = config.small.clone();
224            }
225        }
226        self
227    }
228
229    /// Sets the primary model for main operations.
230    ///
231    /// Default: `claude-sonnet-4-5-20250514`
232    pub fn model(mut self, model: impl Into<String>) -> Self {
233        self.config.model.primary = model.into();
234        self
235    }
236
237    /// Sets the smaller model for quick operations (e.g., subagents).
238    ///
239    /// Default: `claude-haiku-4-5-20251001`
240    pub fn small_model(mut self, model: impl Into<String>) -> Self {
241        self.config.model.small = model.into();
242        self
243    }
244
245    /// Sets the maximum tokens per response.
246    ///
247    /// Default: 8192. Values exceeding this require the 128k beta feature,
248    /// which is automatically enabled when using `ProviderConfig::with_max_tokens`.
249    pub fn max_tokens(mut self, tokens: u32) -> Self {
250        self.config.model.max_tokens = tokens;
251        self
252    }
253
254    /// Enables extended context window (1M tokens for supported models).
255    ///
256    /// Requires the `context-1m-2025-08-07` beta feature.
257    /// Currently supported: `claude-sonnet-4-5-20250929`
258    pub fn extended_context(mut self, enabled: bool) -> Self {
259        self.config.model.extended_context = enabled;
260        self
261    }
262
263    // =========================================================================
264    // Tools
265    // =========================================================================
266
267    /// Sets tool access policy.
268    ///
269    /// # Options
270    /// - `ToolAccess::all()` - Enable all built-in tools
271    /// - `ToolAccess::none()` - Disable all tools
272    /// - `ToolAccess::only(["Read", "Write"])` - Enable specific tools
273    /// - `ToolAccess::except(["Bash"])` - Enable all except specific tools
274    pub fn tools(mut self, access: ToolAccess) -> Self {
275        self.config.security.tool_access = access;
276        self
277    }
278
279    /// Registers a custom tool implementation.
280    pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
281        self.custom_tools.push(Arc::new(tool));
282        self
283    }
284
285    // =========================================================================
286    // Execution
287    // =========================================================================
288
289    /// Sets the working directory for file operations.
290    pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
291        self.config.working_dir = Some(path.into());
292        self
293    }
294
295    /// Sets the maximum number of agentic loop iterations.
296    ///
297    /// Default: `100`
298    pub fn max_iterations(mut self, max: usize) -> Self {
299        self.config.execution.max_iterations = max;
300        self
301    }
302
303    /// Sets the overall execution timeout.
304    ///
305    /// Default: `300 seconds`
306    pub fn timeout(mut self, timeout: Duration) -> Self {
307        self.config.execution.timeout = Some(timeout);
308        self
309    }
310
311    /// Sets the timeout between streaming chunks.
312    ///
313    /// This timeout detects stalled connections when no data is received
314    /// for the specified duration during streaming responses.
315    ///
316    /// Default: `60 seconds`
317    ///
318    /// For large projects or slow network conditions, consider increasing
319    /// this value (e.g., 180 seconds).
320    pub fn chunk_timeout(mut self, timeout: Duration) -> Self {
321        self.config.execution.chunk_timeout = timeout;
322        self
323    }
324
325    /// Enables or disables automatic context compaction.
326    ///
327    /// Default: `true`
328    pub fn auto_compact(mut self, enabled: bool) -> Self {
329        self.config.execution.auto_compact = enabled;
330        self
331    }
332
333    /// Sets the number of messages to preserve during compaction.
334    ///
335    /// Default: `4`
336    pub fn compact_keep_messages(mut self, count: usize) -> Self {
337        self.config.execution.compact_keep_messages = count;
338        self
339    }
340
341    // =========================================================================
342    // Prompts
343    // =========================================================================
344
345    /// Sets a custom system prompt, replacing the default.
346    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
347        self.config.prompt.system_prompt = Some(prompt.into());
348        self
349    }
350
351    /// Sets how the system prompt is applied.
352    pub fn system_prompt_mode(mut self, mode: SystemPromptMode) -> Self {
353        self.config.prompt.system_prompt_mode = mode;
354        self
355    }
356
357    /// Appends to the default system prompt instead of replacing it.
358    pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
359        self.config.prompt.system_prompt_mode = SystemPromptMode::Append;
360        self.config.prompt.system_prompt = Some(prompt.into());
361        self
362    }
363
364    /// Sets the output style for response formatting.
365    pub fn output_style(mut self, style: OutputStyle) -> Self {
366        self.config.prompt.output_style = Some(style);
367        self
368    }
369
370    /// Sets the output style by name (loaded from configuration).
371    pub fn output_style_name(mut self, name: impl Into<String>) -> Self {
372        self.output_style_name = Some(name.into());
373        self
374    }
375
376    /// Sets a JSON schema for structured output.
377    pub fn output_schema(mut self, schema: serde_json::Value) -> Self {
378        self.config.prompt.output_schema = Some(schema);
379        self
380    }
381
382    /// Enables structured output with automatic schema generation.
383    pub fn structured_output<T: schemars::JsonSchema>(mut self) -> Self {
384        let schema = schemars::schema_for!(T);
385        self.config.prompt.output_schema = serde_json::to_value(schema).ok();
386        self
387    }
388
389    // =========================================================================
390    // Permissions
391    // =========================================================================
392
393    /// Sets the complete permission policy.
394    pub fn permission_policy(mut self, policy: PermissionPolicy) -> Self {
395        self.config.security.permission_policy = policy;
396        self
397    }
398
399    /// Sets the permission mode (permissive, default, or strict).
400    pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
401        self.config.security.permission_policy.mode = mode;
402        self
403    }
404
405    /// Adds a rule to allow a tool or pattern.
406    pub fn allow_tool(mut self, pattern: impl Into<String>) -> Self {
407        let pattern = pattern.into();
408        let rule = if pattern.contains('(') && pattern.contains(')') {
409            PermissionRule::allow_scoped(&pattern)
410        } else {
411            PermissionRule::allow(&pattern)
412        };
413        self.config.security.permission_policy.rules.push(rule);
414        self
415    }
416
417    /// Adds a rule to deny a tool or pattern.
418    pub fn deny_tool(mut self, pattern: impl Into<String>) -> Self {
419        let pattern = pattern.into();
420        let rule = if pattern.contains('(') && pattern.contains(')') {
421            PermissionRule::deny_scoped(&pattern)
422        } else {
423            PermissionRule::deny(&pattern)
424        };
425        self.config.security.permission_policy.rules.push(rule);
426        self
427    }
428
429    // =========================================================================
430    // Environment
431    // =========================================================================
432
433    /// Sets an environment variable for tool execution.
434    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
435        self.config.security.env.insert(key.into(), value.into());
436        self
437    }
438
439    /// Sets multiple environment variables for tool execution.
440    pub fn envs(
441        mut self,
442        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
443    ) -> Self {
444        for (k, v) in vars {
445            self.config.security.env.insert(k.into(), v.into());
446        }
447        self
448    }
449
450    // =========================================================================
451    // Sandbox & Network
452    // =========================================================================
453
454    /// Adds a domain to the network allowlist.
455    pub fn allow_domain(mut self, domain: impl Into<String>) -> Self {
456        self.sandbox_settings
457            .get_or_insert_with(crate::config::SandboxSettings::default)
458            .network
459            .allowed_domains
460            .insert(domain.into());
461        self
462    }
463
464    /// Adds a domain to the network blocklist.
465    pub fn deny_domain(mut self, domain: impl Into<String>) -> Self {
466        self.sandbox_settings
467            .get_or_insert_with(crate::config::SandboxSettings::default)
468            .network
469            .blocked_domains
470            .insert(domain.into());
471        self
472    }
473
474    /// Enables or disables sandbox isolation.
475    pub fn sandbox_enabled(mut self, enabled: bool) -> Self {
476        self.sandbox_settings
477            .get_or_insert_with(crate::config::SandboxSettings::default)
478            .enabled = enabled;
479        self
480    }
481
482    /// Excludes a command from sandbox restrictions.
483    pub fn exclude_command(mut self, command: impl Into<String>) -> Self {
484        self.sandbox_settings
485            .get_or_insert_with(crate::config::SandboxSettings::default)
486            .excluded_commands
487            .push(command.into());
488        self
489    }
490
491    // =========================================================================
492    // Budget
493    // =========================================================================
494
495    /// Sets the maximum budget in USD.
496    pub fn max_budget_usd(mut self, amount: f64) -> Self {
497        self.config.budget.max_cost_usd = Some(amount);
498        self
499    }
500
501    /// Sets the tenant ID for multi-tenant budget tracking.
502    pub fn tenant_id(mut self, id: impl Into<String>) -> Self {
503        self.config.budget.tenant_id = Some(id.into());
504        self
505    }
506
507    /// Sets a shared tenant budget manager.
508    pub fn tenant_budget_manager(mut self, manager: TenantBudgetManager) -> Self {
509        self.tenant_budget_manager = Some(manager);
510        self
511    }
512
513    /// Sets the model to fall back to when budget is exceeded.
514    pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
515        self.config.budget.fallback_model = Some(model.into());
516        self
517    }
518
519    /// Sets the complete fallback configuration.
520    pub fn fallback(mut self, config: FallbackConfig) -> Self {
521        self.fallback_config = Some(config);
522        self
523    }
524
525    // =========================================================================
526    // Session
527    // =========================================================================
528
529    /// Sets a custom session manager for persistence.
530    pub fn session_manager(mut self, manager: crate::session::SessionManager) -> Self {
531        self.session_manager = Some(manager);
532        self
533    }
534
535    /// Forks an existing session, creating a new branch.
536    pub async fn fork_session(mut self, session_id: impl Into<String>) -> crate::Result<Self> {
537        let manager = self.session_manager.take().unwrap_or_default();
538        let session_id_str: String = session_id.into();
539        let original_id = crate::session::SessionId::from(session_id_str);
540        let forked = manager
541            .fork(&original_id)
542            .await
543            .map_err(|e| crate::Error::Session(e.to_string()))?;
544
545        self.initial_messages = Some(forked.to_api_messages());
546        self.resume_session_id = Some(forked.id.to_string());
547        self.session_manager = Some(manager);
548        Ok(self)
549    }
550
551    /// Resumes an existing session by ID.
552    pub async fn resume_session(mut self, session_id: impl Into<String>) -> crate::Result<Self> {
553        let session_id_str: String = session_id.into();
554        let id = crate::session::SessionId::from(session_id_str);
555        let manager = self.session_manager.take().unwrap_or_default();
556        let session = manager.get(&id).await?;
557
558        let messages: Vec<crate::types::Message> = session
559            .messages
560            .iter()
561            .map(|m| crate::types::Message {
562                role: m.role,
563                content: m.content.clone(),
564            })
565            .collect();
566
567        self.initial_messages = Some(messages);
568        self.resume_session_id = Some(id.to_string());
569        self.resumed_session = Some(session);
570        self.session_manager = Some(manager);
571        Ok(self)
572    }
573
574    /// Sets initial messages for the conversation.
575    pub fn messages(mut self, messages: Vec<crate::types::Message>) -> Self {
576        self.initial_messages = Some(messages);
577        self
578    }
579
580    // =========================================================================
581    // MCP (Model Context Protocol)
582    // =========================================================================
583
584    /// Adds an MCP server configuration.
585    pub fn mcp_server(
586        mut self,
587        name: impl Into<String>,
588        config: crate::mcp::McpServerConfig,
589    ) -> Self {
590        self.mcp_configs.insert(name.into(), config);
591        self
592    }
593
594    /// Adds an MCP server using stdio transport.
595    pub fn mcp_stdio(
596        mut self,
597        name: impl Into<String>,
598        command: impl Into<String>,
599        args: Vec<String>,
600    ) -> Self {
601        self.mcp_configs.insert(
602            name.into(),
603            crate::mcp::McpServerConfig::Stdio {
604                command: command.into(),
605                args,
606                env: std::collections::HashMap::new(),
607                cwd: None,
608            },
609        );
610        self
611    }
612
613    /// Sets an owned MCP manager.
614    pub fn mcp_manager(mut self, manager: crate::mcp::McpManager) -> Self {
615        self.mcp_manager = Some(std::sync::Arc::new(manager));
616        self
617    }
618
619    /// Sets a shared MCP manager (for multi-agent scenarios).
620    pub fn shared_mcp_manager(mut self, manager: std::sync::Arc<crate::mcp::McpManager>) -> Self {
621        self.mcp_manager = Some(manager);
622        self
623    }
624
625    /// Registers an MCP toolset configuration for deferred loading.
626    pub fn mcp_toolset(mut self, toolset: crate::mcp::McpToolset) -> Self {
627        self.mcp_toolset_registry
628            .get_or_insert_with(crate::mcp::McpToolsetRegistry::new)
629            .register(toolset);
630        self
631    }
632
633    // =========================================================================
634    // Tool Search
635    // =========================================================================
636
637    /// Enables tool search with default configuration.
638    pub fn with_tool_search(mut self) -> Self {
639        self.tool_search_config = Some(crate::tools::ToolSearchConfig::default());
640        self
641    }
642
643    /// Sets the tool search configuration.
644    pub fn tool_search_config(mut self, config: crate::tools::ToolSearchConfig) -> Self {
645        self.tool_search_config = Some(config);
646        self
647    }
648
649    /// Sets the tool search threshold as a fraction of context window (0.0 - 1.0).
650    pub fn tool_search_threshold(mut self, threshold: f64) -> Self {
651        let config = self
652            .tool_search_config
653            .get_or_insert_with(crate::tools::ToolSearchConfig::default);
654        config.threshold = threshold.clamp(0.0, 1.0);
655        self
656    }
657
658    /// Sets the search mode for tool search.
659    pub fn tool_search_mode(mut self, mode: crate::tools::SearchMode) -> Self {
660        let config = self
661            .tool_search_config
662            .get_or_insert_with(crate::tools::ToolSearchConfig::default);
663        config.search_mode = mode;
664        self
665    }
666
667    /// Sets tools that should always be loaded immediately (never deferred).
668    pub fn always_load_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
669        let config = self
670            .tool_search_config
671            .get_or_insert_with(crate::tools::ToolSearchConfig::default);
672        config.always_load = tools.into_iter().map(Into::into).collect();
673        self
674    }
675
676    /// Sets a shared tool search manager.
677    pub fn shared_tool_search_manager(
678        mut self,
679        manager: std::sync::Arc<crate::tools::ToolSearchManager>,
680    ) -> Self {
681        self.tool_search_manager = Some(manager);
682        self
683    }
684
685    // =========================================================================
686    // Skills
687    // =========================================================================
688
689    /// Sets a complete skill registry.
690    pub fn skill_registry(mut self, registry: IndexRegistry<SkillIndex>) -> Self {
691        self.skill_registry = Some(registry);
692        self
693    }
694
695    /// Registers a single skill index.
696    pub fn skill(mut self, skill: SkillIndex) -> Self {
697        self.skill_registry
698            .get_or_insert_with(IndexRegistry::new)
699            .register(skill);
700        self
701    }
702
703    /// Adds a rule index for rule discovery.
704    pub fn rule_index(mut self, index: RuleIndex) -> Self {
705        self.rule_indices.push(index);
706        self
707    }
708
709    /// Adds memory content (CLAUDE.md style).
710    pub fn memory_content(mut self, content: impl Into<String>) -> Self {
711        self.memory_provider
712            .get_or_insert_with(LeveledMemoryProvider::new)
713            .add_content(content);
714        self
715    }
716
717    /// Adds local memory content (CLAUDE.local.md style).
718    pub fn local_memory_content(mut self, content: impl Into<String>) -> Self {
719        self.memory_provider
720            .get_or_insert_with(LeveledMemoryProvider::new)
721            .add_local_content(content);
722        self
723    }
724
725    // =========================================================================
726    // Subagents
727    // =========================================================================
728
729    /// Sets a complete subagent registry.
730    pub fn subagent_registry(mut self, registry: IndexRegistry<SubagentIndex>) -> Self {
731        self.subagent_registry = Some(registry);
732        self
733    }
734
735    /// Registers a single subagent.
736    pub fn subagent(mut self, subagent: SubagentIndex) -> Self {
737        self.subagent_registry
738            .get_or_insert_with(|| {
739                let mut registry = IndexRegistry::new();
740                registry.register_all(builtin_subagents());
741                registry
742            })
743            .register(subagent);
744        self
745    }
746
747    // =========================================================================
748    // Plugins
749    // =========================================================================
750
751    /// Adds a plugin directory to discover and load plugins from.
752    ///
753    /// Each plugin requires a `.claude-plugin/plugin.json` manifest.
754    /// If `dir` is a plugin root, it is loaded directly.
755    /// Otherwise, child directories with manifests are discovered automatically.
756    #[cfg(feature = "plugins")]
757    pub fn plugin_dir(mut self, dir: impl Into<PathBuf>) -> Self {
758        self.plugin_dirs.push(dir.into());
759        self
760    }
761
762    /// Adds multiple plugin directories.
763    #[cfg(feature = "plugins")]
764    pub fn plugin_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
765        self.plugin_dirs.extend(dirs.into_iter().map(Into::into));
766        self
767    }
768
769    // =========================================================================
770    // Hooks
771    // =========================================================================
772
773    /// Registers an event hook.
774    pub fn hook<H: Hook + 'static>(mut self, hook: H) -> Self {
775        self.hooks.register(hook);
776        self
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783    use crate::client::DEFAULT_MAX_TOKENS;
784
785    #[test]
786    fn test_tool_access() {
787        assert!(ToolAccess::all().is_allowed("Read"));
788        assert!(!ToolAccess::none().is_allowed("Read"));
789        assert!(ToolAccess::only(["Read", "Write"]).is_allowed("Read"));
790        assert!(!ToolAccess::only(["Read", "Write"]).is_allowed("Bash"));
791        assert!(!ToolAccess::except(["Bash"]).is_allowed("Bash"));
792        assert!(ToolAccess::except(["Bash"]).is_allowed("Read"));
793    }
794
795    #[test]
796    fn test_max_tokens_default() {
797        let builder = AgentBuilder::new();
798        assert_eq!(builder.config.model.max_tokens, DEFAULT_MAX_TOKENS);
799    }
800
801    #[test]
802    fn test_max_tokens_custom() {
803        let builder = AgentBuilder::new().max_tokens(16384);
804        assert_eq!(builder.config.model.max_tokens, 16384);
805    }
806}