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