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::from_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.initial_messages(messages);
50        }
51        if let Some(id) = self.resume_session_id {
52            agent = agent.session_id(id);
53        }
54        if let Some(mcp) = self.mcp_manager {
55            agent = agent.mcp_manager(mcp);
56        }
57        if let Some(budget) = tenant_budget {
58            agent = agent.tenant_budget(budget);
59        }
60        if let Some(tsm) = self.tool_search_manager {
61            agent = agent.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            .items(builtin_styles())
85            .priority(0)
86            .source_type(SourceType::Builtin);
87
88        let mut chain = ChainOutputStyleProvider::new().provider(builtins);
89
90        if let Some(ref working_dir) = self.config.working_dir {
91            let project = file_output_style_provider()
92                .project_path(working_dir)
93                .priority(20)
94                .source_type(SourceType::Project);
95            chain = chain.provider(project);
96        }
97
98        let user = file_output_style_provider()
99            .user_path()
100            .priority(10)
101            .source_type(SourceType::User);
102        chain = chain.provider(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().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        manager.build_index(mcp_manager).await;
193
194        let prepared = manager.prepare_tools().await;
195        tracing::debug!(
196            use_search = prepared.use_search,
197            immediate_count = prepared.immediate.len(),
198            deferred_count = prepared.deferred.len(),
199            total_tokens = prepared.total_tokens,
200            threshold_tokens = prepared.threshold_tokens,
201            "Tool search initialized"
202        );
203
204        self.tool_search_manager = Some(manager);
205    }
206
207    /// Loads resources from enabled levels in fixed order.
208    ///
209    /// The order is always: Enterprise → User → Project → Local
210    /// regardless of the order `with_*_resources()` methods were called.
211    /// This ensures consistent override behavior where later levels
212    /// override settings from earlier levels.
213    #[cfg(feature = "cli-integration")]
214    async fn load_resources_by_level(&mut self) {
215        // 1. Enterprise (lowest priority)
216        if self.load_enterprise {
217            self.load_enterprise_resources().await;
218        }
219
220        // 2. User
221        if self.load_user {
222            self.load_user_resources().await;
223        }
224
225        // 3. Project
226        if self.load_project {
227            self.load_project_resources().await;
228        }
229
230        // 4. Local (highest priority, overrides all)
231        if self.load_local {
232            self.load_local_resources().await;
233        }
234    }
235
236    #[cfg(not(feature = "cli-integration"))]
237    async fn load_resources_by_level(&mut self) {
238        // No-op when cli-integration is disabled
239    }
240
241    #[cfg(feature = "plugins")]
242    async fn load_plugins(&mut self) {
243        use crate::plugins::{PluginDiscovery, PluginManager};
244        use crate::subagents::builtin_subagents;
245
246        let mut dirs = std::mem::take(&mut self.plugin_dirs);
247        if let Some(default_dir) = PluginDiscovery::default_plugins_dir()
248            && default_dir.exists()
249            && !dirs.contains(&default_dir)
250        {
251            dirs.push(default_dir);
252        }
253        if dirs.is_empty() {
254            return;
255        }
256
257        let manager = match PluginManager::load_from_dirs(&dirs).await {
258            Ok(m) => m,
259            Err(e) => {
260                tracing::warn!(error = %e, "Failed to load plugins");
261                return;
262            }
263        };
264
265        if manager.plugin_count() == 0 {
266            return;
267        }
268
269        let skill_registry = self.skill_registry.get_or_insert_with(IndexRegistry::new);
270        manager.register_skills(skill_registry);
271
272        let subagent_registry = self.subagent_registry.get_or_insert_with(|| {
273            let mut registry = IndexRegistry::new();
274            registry.register_all(builtin_subagents());
275            registry
276        });
277        manager.register_subagents(subagent_registry);
278
279        for (name, config) in manager.mcp_servers() {
280            self.mcp_configs.insert(name.clone(), config.clone());
281        }
282
283        let mut hook_counters = std::collections::HashMap::<String, usize>::new();
284        for entry in manager.hooks() {
285            let key = format!("{}:{}", entry.plugin, entry.event);
286            let count = hook_counters.entry(key).or_insert(0);
287            let hook_name = crate::plugins::namespace::namespaced(
288                &entry.plugin,
289                &format!("{}-{}", entry.event, count),
290            );
291            *count += 1;
292            let mut hook =
293                crate::hooks::CommandHook::from_event_config(hook_name, entry.event, &entry.config);
294            hook = hook.env(
295                "CLAUDE_PLUGIN_ROOT",
296                entry.plugin_root.display().to_string(),
297            );
298            self.hooks.register(hook);
299        }
300    }
301
302    async fn build_orchestrator(&mut self) -> PromptOrchestrator {
303        let mut static_context = StaticContext::new();
304
305        if let Some(ref prompt) = self.config.prompt.system_prompt {
306            static_context = static_context.system_prompt(prompt.clone());
307        }
308
309        let mut claude_md = String::new();
310        let mut rule_indices = std::mem::take(&mut self.rule_indices);
311
312        if let Some(ref provider) = self.memory_provider
313            && let Ok(content) = provider.load().await
314        {
315            claude_md = content.combined_claude_md();
316            rule_indices.extend(content.rule_indices);
317        }
318
319        if !claude_md.is_empty() {
320            static_context = static_context.claude_md(claude_md);
321        }
322
323        // Get skill registry for building summary
324        let skill_registry = self.skill_registry.clone().unwrap_or_default();
325
326        if !skill_registry.is_empty() {
327            let mut lines = vec!["# Available Skills".to_string()];
328            for skill in skill_registry.iter() {
329                lines.push(skill.to_summary_line());
330            }
331            static_context = static_context.skill_summary(lines.join("\n"));
332        }
333
334        let mut rule_registry: IndexRegistry<RuleIndex> = IndexRegistry::new();
335        if !rule_indices.is_empty() {
336            let summary = {
337                let mut lines = vec!["# Available Rules".to_string()];
338                for rule in &rule_indices {
339                    lines.push(rule.to_summary_line());
340                }
341                lines.join("\n")
342            };
343            static_context = static_context.rules_summary(summary);
344            rule_registry.register_all(rule_indices);
345        }
346
347        PromptOrchestrator::new(static_context, &self.config.model.primary)
348            .rule_registry(rule_registry)
349            .skill_registry(skill_registry)
350    }
351
352    async fn build_tools(&mut self) -> Arc<ToolRegistry> {
353        let skill_registry = self.skill_registry.take().unwrap_or_default();
354        let skill_count = skill_registry.iter().count();
355        tracing::debug!(skill_count, "build_tools: skill_registry taken");
356        let subagent_registry = self.subagent_registry.take();
357        let skill_executor = SkillExecutor::new(skill_registry);
358
359        let working_dir = self
360            .config
361            .working_dir
362            .clone()
363            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
364
365        let sandbox_config = self
366            .sandbox_settings
367            .take()
368            .map(|s| s.to_sandbox_config(working_dir.clone()));
369
370        let (tool_state, session_id) = match self.resumed_session.take() {
371            Some(session) => {
372                let id = session.id;
373                (crate::session::ToolState::from_session(session), id)
374            }
375            None => {
376                let id = crate::session::SessionId::new();
377                (crate::session::ToolState::new(id), id)
378            }
379        };
380
381        let mut builder = crate::tools::ToolRegistryBuilder::new()
382            .access(self.config.security.tool_access.clone())
383            .working_dir(working_dir)
384            .skill_executor(skill_executor)
385            .policy(self.config.security.permission_policy.clone())
386            .tool_state(tool_state)
387            .session_id(session_id);
388
389        if let Some(sr) = subagent_registry {
390            builder = builder.subagent_registry(sr);
391        }
392        if let Some(sc) = sandbox_config {
393            builder = builder.sandbox_config(sc);
394        }
395
396        let mut tools = builder.build();
397
398        for tool in std::mem::take(&mut self.custom_tools) {
399            tools.register(tool);
400        }
401
402        if let Some(ref mcp_manager) = self.mcp_manager {
403            let mcp_tools = crate::tools::create_mcp_tools(Arc::clone(mcp_manager)).await;
404            for tool in mcp_tools {
405                tools.register(tool);
406            }
407        }
408
409        Arc::new(tools)
410    }
411
412    async fn build_client(&mut self) -> crate::Result<crate::Client> {
413        let provider = self.cloud_provider.unwrap_or_else(CloudProvider::from_env);
414        let models = self
415            .model_config
416            .take()
417            .unwrap_or_else(|| provider.default_models());
418        let mut config = self
419            .provider_config
420            .take()
421            .unwrap_or_else(|| ProviderConfig::new(models));
422
423        config = config.max_tokens(self.config.model.max_tokens);
424
425        if self.supports_server_tools() {
426            config.beta.add(crate::client::BetaFeature::WebSearch);
427            config.beta.add(crate::client::BetaFeature::WebFetch);
428            tracing::debug!("Enabled server-side web tools");
429        }
430
431        // Enable tool search beta if manager is configured
432        if self.tool_search_manager.is_some() {
433            config.beta.add(crate::client::BetaFeature::AdvancedToolUse);
434            tracing::debug!("Enabled advanced tool use for tool search");
435        }
436
437        // Enable 1M context window beta if extended context is enabled
438        if self.config.model.extended_context {
439            config.beta.add(crate::client::BetaFeature::Context1M);
440            tracing::debug!("Enabled extended context window (1M tokens)");
441        }
442
443        // Enable structured outputs beta if output_schema is configured
444        if self.config.prompt.output_schema.is_some() {
445            config
446                .beta
447                .add(crate::client::BetaFeature::StructuredOutputs);
448            tracing::debug!("Enabled structured outputs beta for JSON schema");
449        }
450
451        let mut builder = crate::Client::builder().config(config);
452
453        match provider {
454            CloudProvider::Anthropic => {
455                builder = builder.anthropic();
456                if let Some(cred) = self.credential.take() {
457                    builder = builder.auth(cred).await?;
458                }
459                if let Some(oauth_config) = self.oauth_config.take() {
460                    builder = builder.oauth_config(oauth_config);
461                }
462            }
463            #[cfg(feature = "aws")]
464            CloudProvider::Bedrock => {
465                let region = self.aws_region.take().unwrap_or_else(|| "us-east-1".into());
466                builder = builder.aws_region(region);
467            }
468            #[cfg(feature = "gcp")]
469            CloudProvider::Vertex => {
470                let project = self
471                    .gcp_project
472                    .take()
473                    .ok_or_else(|| crate::Error::Config("Vertex requires gcp_project".into()))?;
474                let region = self
475                    .gcp_region
476                    .take()
477                    .unwrap_or_else(|| "us-central1".into());
478                builder = builder.gcp(project, region);
479            }
480            #[cfg(feature = "azure")]
481            CloudProvider::Foundry => {
482                let resource = self.azure_resource.take().ok_or_else(|| {
483                    crate::Error::Config("Foundry requires azure_resource".into())
484                })?;
485                builder = builder.azure_resource(resource);
486            }
487        }
488
489        if let Some(fallback) = self.fallback_config.take() {
490            builder = builder.fallback(fallback);
491        } else if let Some(ref model) = self.config.budget.fallback_model {
492            builder = builder.fallback_model(model);
493        }
494
495        builder.build().await
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_resolve_model_aliases() {
505        let mut builder = AgentBuilder::new();
506
507        // Set alias as model
508        builder.config.model.primary = "sonnet".to_string();
509        builder.config.model.small = "haiku".to_string();
510
511        // Resolve aliases
512        builder.resolve_model_aliases();
513
514        // Should be resolved to full model IDs
515        assert!(
516            builder.config.model.primary.contains("sonnet"),
517            "Primary model should contain 'sonnet': {}",
518            builder.config.model.primary
519        );
520        assert!(
521            builder.config.model.primary.starts_with("claude-"),
522            "Primary model should start with 'claude-': {}",
523            builder.config.model.primary
524        );
525        assert!(
526            builder.config.model.small.contains("haiku"),
527            "Small model should contain 'haiku': {}",
528            builder.config.model.small
529        );
530        assert!(
531            builder.config.model.small.starts_with("claude-"),
532            "Small model should start with 'claude-': {}",
533            builder.config.model.small
534        );
535    }
536
537    #[test]
538    fn test_resolve_model_aliases_full_id_unchanged() {
539        let mut builder = AgentBuilder::new();
540
541        // Set full model ID (not an alias)
542        let full_id = "claude-sonnet-4-5-20250929";
543        builder.config.model.primary = full_id.to_string();
544
545        // Resolve aliases
546        builder.resolve_model_aliases();
547
548        // Should remain unchanged
549        assert_eq!(builder.config.model.primary, full_id);
550    }
551
552    #[test]
553    fn test_resolve_model_aliases_opus() {
554        let mut builder = AgentBuilder::new();
555
556        builder.config.model.primary = "opus".to_string();
557        builder.resolve_model_aliases();
558
559        assert!(
560            builder.config.model.primary.contains("opus"),
561            "Primary model should contain 'opus': {}",
562            builder.config.model.primary
563        );
564    }
565}