claude_agent/context/
builder.rs1use std::path::{Path, PathBuf};
4
5use crate::client::DEFAULT_MODEL;
6use crate::common::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 skill(mut self, skill: SkillIndex) -> Self {
63 self.skill_registry.register(skill);
64 self
65 }
66
67 pub fn skills(mut self, skills: impl IntoIterator<Item = SkillIndex>) -> Self {
68 self.skill_registry.register_all(skills);
69 self
70 }
71
72 pub fn skill_registry(mut self, registry: IndexRegistry<SkillIndex>) -> Self {
73 self.skill_registry = registry;
74 self
75 }
76
77 pub fn rule(mut self, rule: RuleIndex) -> Self {
78 self.rule_registry.register(rule);
79 self
80 }
81
82 pub fn rules(mut self, rules: impl IntoIterator<Item = RuleIndex>) -> Self {
83 self.rule_registry.register_all(rules);
84 self
85 }
86
87 pub fn 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.system_prompt(prompt.clone());
119 }
120
121 if let Some(ref md) = self.claude_md {
122 static_context = static_context.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.skill_summary(skill_summary);
128 }
129
130 let rules_summary = self.build_rules_summary();
131 if !rules_summary.is_empty() {
132 static_context = static_context.rules_summary(rules_summary);
133 }
134
135 let orchestrator = PromptOrchestrator::new(static_context, &self.model)
136 .rule_registry(self.rule_registry)
137 .skill_registry(self.skill_registry);
138
139 Ok(orchestrator)
140 }
141
142 fn build_skill_summary(&self) -> String {
143 let summary = self.skill_registry.build_summary();
144 if summary.is_empty() {
145 return String::new();
146 }
147 format!("# Available Skills\n{summary}")
148 }
149
150 fn build_rules_summary(&self) -> String {
151 let summary = self.rule_registry.build_priority_summary();
152 if summary.is_empty() {
153 return String::new();
154 }
155 format!("# Available Rules\n{summary}")
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_context_builder_basic() {
165 let orchestrator = ContextBuilder::new()
166 .system_prompt("You are helpful")
167 .claude_md("# Project\nA test project")
168 .model("claude-sonnet-4-5")
169 .build()
170 .unwrap();
171
172 let static_context = orchestrator.static_context();
173 assert!(static_context.system_prompt.contains("helpful"));
174 assert!(static_context.claude_md.contains("test project"));
175 }
176
177 #[test]
178 fn test_context_builder_with_skills() {
179 let skill = SkillIndex::new("test", "A test skill");
180
181 let orchestrator = ContextBuilder::new().skill(skill).build().unwrap();
182
183 assert!(!orchestrator.static_context().skill_summary.is_empty());
184 }
185
186 #[tokio::test]
187 async fn test_load_from_directory() {
188 use tempfile::tempdir;
189 use tokio::fs;
190
191 let dir = tempdir().unwrap();
192 fs::write(dir.path().join("CLAUDE.md"), "# Test Project")
193 .await
194 .unwrap();
195
196 let rules_dir = dir.path().join(".claude").join("rules");
197 fs::create_dir_all(&rules_dir).await.unwrap();
198 fs::write(
199 rules_dir.join("test.md"),
200 r#"---
201paths: **/*.rs
202---
203
204# Test Rule"#,
205 )
206 .await
207 .unwrap();
208
209 let orchestrator = ContextBuilder::new()
210 .load_from_directory(dir.path())
211 .await
212 .build()
213 .unwrap();
214
215 assert!(
216 orchestrator
217 .static_context()
218 .claude_md
219 .contains("Test Project")
220 );
221 assert!(!orchestrator.static_context().rules_summary.is_empty());
224 }
225
226 #[tokio::test]
227 async fn test_rule_registry_integration() {
228 use crate::common::ContentSource;
229
230 let rule = RuleIndex::new("test-rule")
231 .description("Test description")
232 .paths(vec!["**/*.rs".into()])
233 .source(ContentSource::in_memory("Rule content"));
234
235 let orchestrator = ContextBuilder::new().rule(rule).build().unwrap();
236
237 let registry = orchestrator.get_rule_registry().await;
239 assert!(registry.contains("test-rule"));
240 }
241}