claude_agent/agent/options/
build.rs1use 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 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 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 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 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 if let Some(registry) = self.mcp_toolset_registry.take() {
185 manager.set_toolset_registry(registry);
186 }
187
188 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 #[cfg(feature = "cli-integration")]
211 async fn load_resources_by_level(&mut self) {
212 if self.load_enterprise {
214 self.load_enterprise_resources().await;
215 }
216
217 if self.load_user {
219 self.load_user_resources().await;
220 }
221
222 if self.load_project {
224 self.load_project_resources().await;
225 }
226
227 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 }
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 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 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 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 builder.config.model.primary = "sonnet".to_string();
437 builder.config.model.small = "haiku".to_string();
438
439 builder.resolve_model_aliases();
441
442 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 let full_id = "claude-sonnet-4-5-20250929";
471 builder.config.model.primary = full_id.to_string();
472
473 builder.resolve_model_aliases();
475
476 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}