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