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 #[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 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 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 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 if let Some(registry) = self.mcp_toolset_registry.take() {
189 manager.set_toolset_registry(registry);
190 }
191
192 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 #[cfg(feature = "cli-integration")]
215 async fn load_resources_by_level(&mut self) {
216 if self.load_enterprise {
218 self.load_enterprise_resources().await;
219 }
220
221 if self.load_user {
223 self.load_user_resources().await;
224 }
225
226 if self.load_project {
228 self.load_project_resources().await;
229 }
230
231 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 }
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 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 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 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 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 builder.config.model.primary = "sonnet".to_string();
510 builder.config.model.small = "haiku".to_string();
511
512 builder.resolve_model_aliases();
514
515 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 let full_id = "claude-sonnet-4-5-20250929";
544 builder.config.model.primary = full_id.to_string();
545
546 builder.resolve_model_aliases();
548
549 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}