claude_agent/context/
builder.rs1use std::path::{Path, PathBuf};
4
5use crate::client::DEFAULT_MODEL;
6use crate::common::{Index, IndexRegistry};
7use crate::skills::SkillIndex;
8
9use super::ContextResult;
10use super::memory_loader::MemoryLoader;
11use super::orchestrator::PromptOrchestrator;
12use super::rule_index::RuleIndex;
13use super::static_context::StaticContext;
14
15pub struct ContextBuilder {
16 system_prompt: Option<String>,
17 claude_md: Option<String>,
18 skill_registry: IndexRegistry<SkillIndex>,
19 rule_registry: IndexRegistry<RuleIndex>,
20 working_dir: Option<PathBuf>,
21 model: String,
22}
23
24impl Default for ContextBuilder {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl ContextBuilder {
31 pub fn new() -> Self {
32 Self {
33 system_prompt: None,
34 claude_md: None,
35 skill_registry: IndexRegistry::new(),
36 rule_registry: IndexRegistry::new(),
37 working_dir: None,
38 model: DEFAULT_MODEL.to_string(),
39 }
40 }
41
42 pub fn model(mut self, model: impl Into<String>) -> Self {
43 self.model = model.into();
44 self
45 }
46
47 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
48 self.system_prompt = Some(prompt.into());
49 self
50 }
51
52 pub fn claude_md(mut self, content: impl Into<String>) -> Self {
53 self.claude_md = Some(content.into());
54 self
55 }
56
57 pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
58 self.working_dir = Some(path.into());
59 self
60 }
61
62 pub fn with_skill(mut self, skill: SkillIndex) -> Self {
63 self.skill_registry.register(skill);
64 self
65 }
66
67 pub fn with_skills(mut self, skills: impl IntoIterator<Item = SkillIndex>) -> Self {
68 self.skill_registry.register_all(skills);
69 self
70 }
71
72 pub fn with_skill_registry(mut self, registry: IndexRegistry<SkillIndex>) -> Self {
73 self.skill_registry = registry;
74 self
75 }
76
77 pub fn with_rule(mut self, rule: RuleIndex) -> Self {
78 self.rule_registry.register(rule);
79 self
80 }
81
82 pub fn with_rules(mut self, rules: impl IntoIterator<Item = RuleIndex>) -> Self {
83 self.rule_registry.register_all(rules);
84 self
85 }
86
87 pub fn with_rule_registry(mut self, registry: IndexRegistry<RuleIndex>) -> Self {
88 self.rule_registry = registry;
89 self
90 }
91
92 pub async fn load_from_directory(mut self, dir: impl AsRef<Path>) -> Self {
93 let dir = dir.as_ref();
94 let loader = MemoryLoader::new();
95
96 if let Ok(content) = loader.load(dir).await {
97 let combined = content.combined_claude_md();
98 if !combined.is_empty() {
99 self.claude_md = Some(match self.claude_md {
100 Some(existing) => format!("{}\n\n{}", existing, combined),
101 None => combined,
102 });
103 }
104
105 for rule in content.rule_indices {
107 self.rule_registry.register(rule);
108 }
109 }
110
111 self
112 }
113
114 pub fn build(self) -> ContextResult<PromptOrchestrator> {
115 let mut static_context = StaticContext::new();
116
117 if let Some(ref prompt) = self.system_prompt {
118 static_context = static_context.with_system_prompt(prompt.clone());
119 }
120
121 if let Some(ref md) = self.claude_md {
122 static_context = static_context.with_claude_md(md.clone());
123 }
124
125 let skill_summary = self.build_skill_summary();
126 if !skill_summary.is_empty() {
127 static_context = static_context.with_skill_summary(skill_summary);
128 }
129
130 let rules_summary = self.build_rules_summary();
132 if !rules_summary.is_empty() {
133 static_context = static_context.with_rules_summary(rules_summary);
134 }
135
136 let orchestrator = PromptOrchestrator::new(static_context, &self.model)
137 .with_rule_registry(self.rule_registry)
138 .with_skill_registry(self.skill_registry);
139
140 Ok(orchestrator)
141 }
142
143 fn build_skill_summary(&self) -> String {
144 if self.skill_registry.is_empty() {
145 return String::new();
146 }
147
148 let mut lines = vec!["# Available Skills".to_string()];
149 for skill in self.skill_registry.iter() {
150 lines.push(skill.to_summary_line());
151 }
152 lines.join("\n")
153 }
154
155 fn build_rules_summary(&self) -> String {
156 if self.rule_registry.is_empty() {
157 return String::new();
158 }
159
160 let mut lines = vec!["# Available Rules".to_string()];
161 for rule in self.rule_registry.sorted_by_priority() {
162 lines.push(rule.to_summary_line());
163 }
164 lines.join("\n")
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_context_builder_basic() {
174 let orchestrator = ContextBuilder::new()
175 .system_prompt("You are helpful")
176 .claude_md("# Project\nA test project")
177 .model("claude-sonnet-4-5")
178 .build()
179 .unwrap();
180
181 let static_context = orchestrator.static_context();
182 assert!(static_context.system_prompt.contains("helpful"));
183 assert!(static_context.claude_md.contains("test project"));
184 }
185
186 #[test]
187 fn test_context_builder_with_skills() {
188 let skill = SkillIndex::new("test", "A test skill");
189
190 let orchestrator = ContextBuilder::new().with_skill(skill).build().unwrap();
191
192 assert!(!orchestrator.static_context().skill_index_summary.is_empty());
193 }
194
195 #[tokio::test]
196 async fn test_load_from_directory() {
197 use tempfile::tempdir;
198 use tokio::fs;
199
200 let dir = tempdir().unwrap();
201 fs::write(dir.path().join("CLAUDE.md"), "# Test Project")
202 .await
203 .unwrap();
204
205 let rules_dir = dir.path().join(".claude").join("rules");
206 fs::create_dir_all(&rules_dir).await.unwrap();
207 fs::write(
208 rules_dir.join("test.md"),
209 r#"---
210paths: **/*.rs
211---
212
213# Test Rule"#,
214 )
215 .await
216 .unwrap();
217
218 let orchestrator = ContextBuilder::new()
219 .load_from_directory(dir.path())
220 .await
221 .build()
222 .unwrap();
223
224 assert!(
225 orchestrator
226 .static_context()
227 .claude_md
228 .contains("Test Project")
229 );
230 assert!(!orchestrator.static_context().rules_summary.is_empty());
233 }
234
235 #[tokio::test]
236 async fn test_rule_registry_integration() {
237 use crate::common::ContentSource;
238
239 let rule = RuleIndex::new("test-rule")
240 .with_description("Test description")
241 .with_paths(vec!["**/*.rs".into()])
242 .with_source(ContentSource::in_memory("Rule content"));
243
244 let orchestrator = ContextBuilder::new().with_rule(rule).build().unwrap();
245
246 let registry = orchestrator.rule_registry().await;
248 assert!(registry.contains("test-rule"));
249 }
250}