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::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 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().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;
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 #[cfg(feature = "cli-integration")]
214 async fn load_resources_by_level(&mut self) {
215 if self.load_enterprise {
217 self.load_enterprise_resources().await;
218 }
219
220 if self.load_user {
222 self.load_user_resources().await;
223 }
224
225 if self.load_project {
227 self.load_project_resources().await;
228 }
229
230 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 }
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 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 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 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 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 builder.config.model.primary = "sonnet".to_string();
509 builder.config.model.small = "haiku".to_string();
510
511 builder.resolve_model_aliases();
513
514 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 let full_id = "claude-sonnet-4-5-20250929";
543 builder.config.model.primary = full_id.to_string();
544
545 builder.resolve_model_aliases();
547
548 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}