Skip to main content

claude_agent/agent/options/
build.rs

1//! Agent build methods.
2
3use std::sync::Arc;
4
5use crate::client::{CloudProvider, ProviderConfig};
6use crate::common::Index;
7use crate::common::IndexRegistry;
8use crate::context::{MemoryProvider, PromptOrchestrator, RuleIndex, StaticContext};
9use crate::skills::SkillExecutor;
10use crate::tools::{ToolRegistry, ToolSearchConfig, ToolSearchManager};
11
12use super::builder::AgentBuilder;
13
14impl AgentBuilder {
15    pub async fn build(mut self) -> crate::Result<crate::agent::Agent> {
16        // Load resources in fixed order (regardless of chaining order)
17        // Order: Enterprise → User → Project → Local (later overrides earlier)
18        self.load_resources_by_level().await;
19
20        #[cfg(feature = "plugins")]
21        self.load_plugins().await;
22
23        self.resolve_output_style().await?;
24        self.resolve_model_aliases();
25        self.connect_mcp_servers().await?;
26        self.initialize_tool_search().await;
27
28        let client = self.build_client().await?;
29        let tools = self.build_tools().await;
30        let orchestrator = self.build_orchestrator().await;
31
32        let tenant_budget = self.tenant_budget_manager.as_ref().and_then(|m| {
33            self.config
34                .budget
35                .tenant_id
36                .as_ref()
37                .and_then(|id| m.get(id))
38        });
39
40        let mut agent = crate::agent::Agent::with_orchestrator(
41            client,
42            self.config,
43            tools,
44            self.hooks,
45            orchestrator,
46        );
47
48        if let Some(messages) = self.initial_messages {
49            agent = agent.with_initial_messages(messages);
50        }
51        if let Some(id) = self.resume_session_id {
52            agent = agent.with_session_id(id);
53        }
54        if let Some(mcp) = self.mcp_manager {
55            agent = agent.with_mcp_manager(mcp);
56        }
57        if let Some(budget) = tenant_budget {
58            agent = agent.with_tenant_budget(budget);
59        }
60        if let Some(tsm) = self.tool_search_manager {
61            agent = agent.with_tool_search_manager(tsm);
62        }
63
64        Ok(agent)
65    }
66
67    #[cfg(feature = "cli-integration")]
68    async fn resolve_output_style(&mut self) -> crate::Result<()> {
69        use crate::common::{Provider, SourceType};
70        use crate::output_style::{
71            ChainOutputStyleProvider, InMemoryOutputStyleProvider, builtin_styles,
72            file_output_style_provider,
73        };
74
75        if self.config.prompt.output_style.is_some() {
76            return Ok(());
77        }
78
79        let Some(ref name) = self.output_style_name else {
80            return Ok(());
81        };
82
83        let builtins = InMemoryOutputStyleProvider::new()
84            .with_items(builtin_styles())
85            .with_priority(0)
86            .with_source_type(SourceType::Builtin);
87
88        let mut chain = ChainOutputStyleProvider::new().with(builtins);
89
90        if let Some(ref working_dir) = self.config.working_dir {
91            let project = file_output_style_provider()
92                .with_project_path(working_dir)
93                .with_priority(20)
94                .with_source_type(SourceType::Project);
95            chain = chain.with(project);
96        }
97
98        let user = file_output_style_provider()
99            .with_user_path()
100            .with_priority(10)
101            .with_source_type(SourceType::User);
102        chain = chain.with(user);
103
104        if let Ok(Some(style)) = chain.get(name).await {
105            self.config.prompt.output_style = Some(style);
106        }
107
108        Ok(())
109    }
110
111    #[cfg(not(feature = "cli-integration"))]
112    async fn resolve_output_style(&mut self) -> crate::Result<()> {
113        Ok(())
114    }
115
116    fn resolve_model_aliases(&mut self) {
117        let provider = self.cloud_provider.unwrap_or_else(CloudProvider::from_env);
118        let model_config = self
119            .model_config
120            .clone()
121            .unwrap_or_else(|| provider.default_models());
122
123        // Resolve primary model alias
124        let primary = &self.config.model.primary;
125        let resolved_primary = model_config.resolve_alias(primary);
126        if resolved_primary != primary {
127            tracing::debug!(
128                alias = %primary,
129                resolved = %resolved_primary,
130                "Resolved primary model alias"
131            );
132            self.config.model.primary = resolved_primary.to_string();
133        }
134
135        // Resolve small model alias
136        let small = &self.config.model.small;
137        let resolved_small = model_config.resolve_alias(small);
138        if resolved_small != small {
139            tracing::debug!(
140                alias = %small,
141                resolved = %resolved_small,
142                "Resolved small model alias"
143            );
144            self.config.model.small = resolved_small.to_string();
145        }
146    }
147
148    async fn connect_mcp_servers(&mut self) -> crate::Result<()> {
149        if self.mcp_configs.is_empty() && self.mcp_manager.is_none() {
150            return Ok(());
151        }
152
153        let manager = self
154            .mcp_manager
155            .take()
156            .unwrap_or_else(|| std::sync::Arc::new(crate::mcp::McpManager::new()));
157
158        for (name, config) in std::mem::take(&mut self.mcp_configs) {
159            manager.add_server(&name, config).await.map_err(|e| {
160                crate::Error::Mcp(crate::mcp::McpError::ConnectionFailed {
161                    message: format!("{}: {}", name, e),
162                })
163            })?;
164        }
165
166        self.mcp_manager = Some(manager);
167        Ok(())
168    }
169
170    async fn initialize_tool_search(&mut self) {
171        let Some(ref mcp_manager) = self.mcp_manager else {
172            return;
173        };
174
175        // Use shared manager if provided, otherwise create new one
176        let manager = if let Some(shared) = self.tool_search_manager.take() {
177            shared
178        } else {
179            let config = self.tool_search_config.take().unwrap_or_else(|| {
180                let context_window =
181                    crate::types::context_window::for_model(&self.config.model.primary) as usize;
182                ToolSearchConfig::default().with_context_window(context_window)
183            });
184            Arc::new(ToolSearchManager::new(config))
185        };
186
187        // Set toolset registry if available
188        if let Some(registry) = self.mcp_toolset_registry.take() {
189            manager.set_toolset_registry(registry);
190        }
191
192        // Build the search index from MCP tools
193        manager.build_index(mcp_manager).await;
194
195        let prepared = manager.prepare_tools().await;
196        tracing::debug!(
197            use_search = prepared.use_search,
198            immediate_count = prepared.immediate.len(),
199            deferred_count = prepared.deferred.len(),
200            total_tokens = prepared.total_tokens,
201            threshold_tokens = prepared.threshold_tokens,
202            "Tool search initialized"
203        );
204
205        self.tool_search_manager = Some(manager);
206    }
207
208    /// Loads resources from enabled levels in fixed order.
209    ///
210    /// The order is always: Enterprise → User → Project → Local
211    /// regardless of the order `with_*_resources()` methods were called.
212    /// This ensures consistent override behavior where later levels
213    /// override settings from earlier levels.
214    #[cfg(feature = "cli-integration")]
215    async fn load_resources_by_level(&mut self) {
216        // 1. Enterprise (lowest priority)
217        if self.load_enterprise {
218            self.load_enterprise_resources().await;
219        }
220
221        // 2. User
222        if self.load_user {
223            self.load_user_resources().await;
224        }
225
226        // 3. Project
227        if self.load_project {
228            self.load_project_resources().await;
229        }
230
231        // 4. Local (highest priority, overrides all)
232        if self.load_local {
233            self.load_local_resources().await;
234        }
235    }
236
237    #[cfg(not(feature = "cli-integration"))]
238    async fn load_resources_by_level(&mut self) {
239        // No-op when cli-integration is disabled
240    }
241
242    #[cfg(feature = "plugins")]
243    async fn load_plugins(&mut self) {
244        use crate::plugins::{PluginDiscovery, PluginManager};
245        use crate::subagents::builtin_subagents;
246
247        let mut dirs = std::mem::take(&mut self.plugin_dirs);
248        if let Some(default_dir) = PluginDiscovery::default_plugins_dir()
249            && default_dir.exists()
250            && !dirs.contains(&default_dir)
251        {
252            dirs.push(default_dir);
253        }
254        if dirs.is_empty() {
255            return;
256        }
257
258        let manager = match PluginManager::load_from_dirs(&dirs).await {
259            Ok(m) => m,
260            Err(e) => {
261                tracing::warn!(error = %e, "Failed to load plugins");
262                return;
263            }
264        };
265
266        if manager.plugin_count() == 0 {
267            return;
268        }
269
270        let skill_registry = self.skill_registry.get_or_insert_with(IndexRegistry::new);
271        manager.register_skills(skill_registry);
272
273        let subagent_registry = self.subagent_registry.get_or_insert_with(|| {
274            let mut registry = IndexRegistry::new();
275            registry.register_all(builtin_subagents());
276            registry
277        });
278        manager.register_subagents(subagent_registry);
279
280        for (name, config) in manager.mcp_servers() {
281            self.mcp_configs.insert(name.clone(), config.clone());
282        }
283
284        let mut hook_counters = std::collections::HashMap::<String, usize>::new();
285        for entry in manager.hooks() {
286            let key = format!("{}:{}", entry.plugin, entry.event);
287            let count = hook_counters.entry(key).or_insert(0);
288            let hook_name = crate::plugins::namespace::namespaced(
289                &entry.plugin,
290                &format!("{}-{}", entry.event, count),
291            );
292            *count += 1;
293            let mut hook =
294                crate::hooks::CommandHook::from_event_config(hook_name, entry.event, &entry.config);
295            hook = hook.with_env(
296                "CLAUDE_PLUGIN_ROOT",
297                entry.plugin_root.display().to_string(),
298            );
299            self.hooks.register(hook);
300        }
301    }
302
303    async fn build_orchestrator(&mut self) -> PromptOrchestrator {
304        let mut static_context = StaticContext::new();
305
306        if let Some(ref prompt) = self.config.prompt.system_prompt {
307            static_context = static_context.with_system_prompt(prompt.clone());
308        }
309
310        let mut claude_md = String::new();
311        let mut rule_indices = std::mem::take(&mut self.rule_indices);
312
313        if let Some(ref provider) = self.memory_provider
314            && let Ok(content) = provider.load().await
315        {
316            claude_md = content.combined_claude_md();
317            rule_indices.extend(content.rule_indices);
318        }
319
320        if !claude_md.is_empty() {
321            static_context = static_context.with_claude_md(claude_md);
322        }
323
324        // Get skill registry for building summary
325        let skill_registry = self.skill_registry.clone().unwrap_or_default();
326
327        if !skill_registry.is_empty() {
328            let mut lines = vec!["# Available Skills".to_string()];
329            for skill in skill_registry.iter() {
330                lines.push(skill.to_summary_line());
331            }
332            static_context = static_context.with_skill_summary(lines.join("\n"));
333        }
334
335        let mut rule_registry: IndexRegistry<RuleIndex> = IndexRegistry::new();
336        if !rule_indices.is_empty() {
337            let summary = {
338                let mut lines = vec!["# Available Rules".to_string()];
339                for rule in &rule_indices {
340                    lines.push(rule.to_summary_line());
341                }
342                lines.join("\n")
343            };
344            static_context = static_context.with_rules_summary(summary);
345            rule_registry.register_all(rule_indices);
346        }
347
348        PromptOrchestrator::new(static_context, &self.config.model.primary)
349            .with_rule_registry(rule_registry)
350            .with_skill_registry(skill_registry)
351    }
352
353    async fn build_tools(&mut self) -> Arc<ToolRegistry> {
354        let skill_registry = self.skill_registry.take().unwrap_or_default();
355        let skill_count = skill_registry.iter().count();
356        tracing::debug!(skill_count, "build_tools: skill_registry taken");
357        let subagent_registry = self.subagent_registry.take();
358        let skill_executor = SkillExecutor::new(skill_registry);
359
360        let working_dir = self
361            .config
362            .working_dir
363            .clone()
364            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
365
366        let sandbox_config = self
367            .sandbox_settings
368            .take()
369            .map(|s| s.to_sandbox_config(working_dir.clone()));
370
371        let (tool_state, session_id) = match self.resumed_session.take() {
372            Some(session) => {
373                let id = session.id;
374                (crate::session::ToolState::from_session(session), id)
375            }
376            None => {
377                let id = crate::session::SessionId::new();
378                (crate::session::ToolState::new(id), id)
379            }
380        };
381
382        let mut builder = crate::tools::ToolRegistryBuilder::new()
383            .access(self.config.security.tool_access.clone())
384            .working_dir(working_dir)
385            .skill_executor(skill_executor)
386            .policy(self.config.security.permission_policy.clone())
387            .tool_state(tool_state)
388            .session_id(session_id);
389
390        if let Some(sr) = subagent_registry {
391            builder = builder.subagent_registry(sr);
392        }
393        if let Some(sc) = sandbox_config {
394            builder = builder.sandbox_config(sc);
395        }
396
397        let mut tools = builder.build();
398
399        for tool in std::mem::take(&mut self.custom_tools) {
400            tools.register(tool);
401        }
402
403        if let Some(ref mcp_manager) = self.mcp_manager {
404            let mcp_tools = crate::tools::create_mcp_tools(Arc::clone(mcp_manager)).await;
405            for tool in mcp_tools {
406                tools.register(tool);
407            }
408        }
409
410        Arc::new(tools)
411    }
412
413    async fn build_client(&mut self) -> crate::Result<crate::Client> {
414        let provider = self.cloud_provider.unwrap_or_else(CloudProvider::from_env);
415        let models = self
416            .model_config
417            .take()
418            .unwrap_or_else(|| provider.default_models());
419        let mut config = self
420            .provider_config
421            .take()
422            .unwrap_or_else(|| ProviderConfig::new(models));
423
424        config = config.with_max_tokens(self.config.model.max_tokens);
425
426        if self.supports_server_tools() {
427            config.beta.add(crate::client::BetaFeature::WebSearch);
428            config.beta.add(crate::client::BetaFeature::WebFetch);
429            tracing::debug!("Enabled server-side web tools");
430        }
431
432        // Enable tool search beta if manager is configured
433        if self.tool_search_manager.is_some() {
434            config.beta.add(crate::client::BetaFeature::AdvancedToolUse);
435            tracing::debug!("Enabled advanced tool use for tool search");
436        }
437
438        // Enable 1M context window beta if extended context is enabled
439        if self.config.model.extended_context {
440            config.beta.add(crate::client::BetaFeature::Context1M);
441            tracing::debug!("Enabled extended context window (1M tokens)");
442        }
443
444        // Enable structured outputs beta if output_schema is configured
445        if self.config.prompt.output_schema.is_some() {
446            config
447                .beta
448                .add(crate::client::BetaFeature::StructuredOutputs);
449            tracing::debug!("Enabled structured outputs beta for JSON schema");
450        }
451
452        let mut builder = crate::Client::builder().config(config);
453
454        match provider {
455            CloudProvider::Anthropic => {
456                builder = builder.anthropic();
457                if let Some(cred) = self.credential.take() {
458                    builder = builder.auth(cred).await?;
459                }
460                if let Some(oauth_config) = self.oauth_config.take() {
461                    builder = builder.oauth_config(oauth_config);
462                }
463            }
464            #[cfg(feature = "aws")]
465            CloudProvider::Bedrock => {
466                let region = self.aws_region.take().unwrap_or_else(|| "us-east-1".into());
467                builder = builder.with_aws_region(region);
468            }
469            #[cfg(feature = "gcp")]
470            CloudProvider::Vertex => {
471                let project = self
472                    .gcp_project
473                    .take()
474                    .ok_or_else(|| crate::Error::Config("Vertex requires gcp_project".into()))?;
475                let region = self
476                    .gcp_region
477                    .take()
478                    .unwrap_or_else(|| "us-central1".into());
479                builder = builder.with_gcp(project, region);
480            }
481            #[cfg(feature = "azure")]
482            CloudProvider::Foundry => {
483                let resource = self.azure_resource.take().ok_or_else(|| {
484                    crate::Error::Config("Foundry requires azure_resource".into())
485                })?;
486                builder = builder.with_azure_resource(resource);
487            }
488        }
489
490        if let Some(fallback) = self.fallback_config.take() {
491            builder = builder.fallback(fallback);
492        } else if let Some(ref model) = self.config.budget.fallback_model {
493            builder = builder.fallback_model(model);
494        }
495
496        builder.build().await
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_resolve_model_aliases() {
506        let mut builder = AgentBuilder::new();
507
508        // Set alias as model
509        builder.config.model.primary = "sonnet".to_string();
510        builder.config.model.small = "haiku".to_string();
511
512        // Resolve aliases
513        builder.resolve_model_aliases();
514
515        // Should be resolved to full model IDs
516        assert!(
517            builder.config.model.primary.contains("sonnet"),
518            "Primary model should contain 'sonnet': {}",
519            builder.config.model.primary
520        );
521        assert!(
522            builder.config.model.primary.starts_with("claude-"),
523            "Primary model should start with 'claude-': {}",
524            builder.config.model.primary
525        );
526        assert!(
527            builder.config.model.small.contains("haiku"),
528            "Small model should contain 'haiku': {}",
529            builder.config.model.small
530        );
531        assert!(
532            builder.config.model.small.starts_with("claude-"),
533            "Small model should start with 'claude-': {}",
534            builder.config.model.small
535        );
536    }
537
538    #[test]
539    fn test_resolve_model_aliases_full_id_unchanged() {
540        let mut builder = AgentBuilder::new();
541
542        // Set full model ID (not an alias)
543        let full_id = "claude-sonnet-4-5-20250929";
544        builder.config.model.primary = full_id.to_string();
545
546        // Resolve aliases
547        builder.resolve_model_aliases();
548
549        // Should remain unchanged
550        assert_eq!(builder.config.model.primary, full_id);
551    }
552
553    #[test]
554    fn test_resolve_model_aliases_opus() {
555        let mut builder = AgentBuilder::new();
556
557        builder.config.model.primary = "opus".to_string();
558        builder.resolve_model_aliases();
559
560        assert!(
561            builder.config.model.primary.contains("opus"),
562            "Primary model should contain 'opus': {}",
563            builder.config.model.primary
564        );
565    }
566}