Skip to main content

tycode_core/steering/
mod.rs

1pub mod autonomy;
2pub mod communication;
3pub mod style;
4pub mod tools;
5
6use std::collections::HashSet;
7use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use crate::agents::defaults;
13use crate::module::ContextComponent;
14use crate::module::Module;
15use crate::module::PromptComponent;
16use crate::settings::config::CommunicationTone;
17use crate::settings::SettingsManager;
18use crate::tools::r#trait::ToolExecutor;
19
20#[derive(Copy, Clone, Debug)]
21pub enum Builtin {
22    UnderstandingTools,
23    StyleMandates,
24    CommunicationGuidelines,
25}
26
27/// Module providing steering-related prompt components.
28///
29/// Bundles all prompt components that define agent behavior:
30/// - Style mandates (coding style guidelines)
31/// - Communication guidelines (how to communicate with user)
32/// - Tool instructions (how to use tools correctly)
33/// - Autonomy level (how autonomous the agent should be)
34pub struct SteeringModule {
35    documents: Arc<SteeringDocuments>,
36    settings: SettingsManager,
37}
38
39impl SteeringModule {
40    pub fn new(documents: Arc<SteeringDocuments>, settings: SettingsManager) -> Self {
41        Self {
42            documents,
43            settings,
44        }
45    }
46}
47
48impl Module for SteeringModule {
49    fn prompt_components(&self) -> Vec<Arc<dyn PromptComponent>> {
50        let autonomy_level = self.settings.settings().autonomy_level;
51
52        vec![
53            Arc::new(style::StyleMandatesComponent::new(self.documents.clone())),
54            Arc::new(tools::ToolInstructionsComponent::new(
55                self.documents.clone(),
56            )),
57            Arc::new(communication::CommunicationComponent::new(
58                self.documents.clone(),
59            )),
60            Arc::new(autonomy::AutonomyComponent::new(autonomy_level)),
61        ]
62    }
63
64    fn context_components(&self) -> Vec<Arc<dyn ContextComponent>> {
65        vec![]
66    }
67
68    fn tools(&self) -> Vec<Arc<dyn ToolExecutor>> {
69        vec![]
70    }
71}
72
73impl Builtin {
74    pub fn all() -> &'static [Builtin] {
75        &[
76            Builtin::UnderstandingTools,
77            Builtin::StyleMandates,
78            Builtin::CommunicationGuidelines,
79        ]
80    }
81
82    fn as_str(&self) -> &'static str {
83        match self {
84            Builtin::UnderstandingTools => "understanding_tools",
85            Builtin::StyleMandates => "style_mandates",
86            Builtin::CommunicationGuidelines => "communication_guidelines",
87        }
88    }
89}
90
91#[derive(Clone)]
92pub struct SteeringDocuments {
93    workspace_roots: Vec<PathBuf>,
94    home_dir: PathBuf,
95    communication_tone: CommunicationTone,
96}
97
98impl SteeringDocuments {
99    pub fn new(
100        workspace_roots: Vec<PathBuf>,
101        home_dir: PathBuf,
102        communication_tone: CommunicationTone,
103    ) -> Self {
104        Self {
105            workspace_roots,
106            home_dir,
107            communication_tone,
108        }
109    }
110
111    pub fn get_builtin(&self, builtin: Builtin) -> String {
112        let name = builtin.as_str();
113        if let Some(content) = self.load_from_workspace(name) {
114            return content;
115        }
116
117        if let Some(content) = self.load_from_home(name) {
118            return content;
119        }
120
121        self.get_default(name)
122    }
123
124    pub fn get_custom_documents(&self) -> Vec<String> {
125        let mut documents = Vec::new();
126        let mut seen_paths = HashSet::new();
127
128        for workspace in &self.workspace_roots {
129            let tycode_dir = workspace.join(".tycode");
130            self.collect_custom_from_dir(&tycode_dir, &mut documents, &mut seen_paths);
131        }
132
133        let home_tycode = self.home_dir.join(".tycode");
134        self.collect_custom_from_dir(&home_tycode, &mut documents, &mut seen_paths);
135
136        documents
137    }
138
139    pub fn get_external_documents(&self) -> Vec<String> {
140        let mut documents = Vec::new();
141
142        for workspace in &self.workspace_roots {
143            self.collect_cursor_docs(workspace, &mut documents);
144            self.collect_cline_docs(workspace, &mut documents);
145            self.collect_roo_docs(workspace, &mut documents);
146            self.collect_kiro_docs(workspace, &mut documents);
147        }
148
149        documents
150    }
151
152    pub fn build_steering_content(&self) -> String {
153        let mut sections = Vec::new();
154
155        for builtin in Builtin::all() {
156            sections.push(self.get_builtin(*builtin));
157        }
158
159        for doc in self.get_custom_documents() {
160            sections.push(doc);
161        }
162
163        for doc in self.get_external_documents() {
164            sections.push(doc);
165        }
166
167        sections.join("\n\n")
168    }
169
170    pub fn build_system_prompt(&self, core_prompt: &str, include_custom: bool) -> String {
171        let mut prompt = core_prompt.to_string();
172
173        if include_custom {
174            for doc in self.get_custom_documents() {
175                prompt.push_str("\n\n");
176                prompt.push_str(&doc);
177            }
178
179            for doc in self.get_external_documents() {
180                prompt.push_str("\n\n");
181                prompt.push_str(&doc);
182            }
183        }
184
185        prompt
186    }
187
188    fn load_from_workspace(&self, name: &str) -> Option<String> {
189        let filename = format!("{}.md", name);
190
191        for workspace in &self.workspace_roots {
192            let path = workspace.join(".tycode").join(&filename);
193            if let Some(content) = self.read_file(&path) {
194                tracing::debug!(
195                    "Loaded steering document override from workspace: {}",
196                    path.display()
197                );
198                return Some(content);
199            }
200        }
201
202        None
203    }
204
205    fn load_from_home(&self, name: &str) -> Option<String> {
206        let filename = format!("{}.md", name);
207        let path = self.home_dir.join(".tycode").join(&filename);
208
209        if let Some(content) = self.read_file(&path) {
210            tracing::debug!(
211                "Loaded steering document override from home: {}",
212                path.display()
213            );
214            return Some(content);
215        }
216
217        None
218    }
219
220    fn get_default(&self, name: &str) -> String {
221        match name {
222            "style_mandates" => defaults::STYLE_MANDATES.to_string(),
223            "communication_guidelines" => {
224                defaults::get_communication_guidelines(self.communication_tone).to_string()
225            }
226            "understanding_tools" => defaults::UNDERSTANDING_TOOLS.to_string(),
227            _ => String::new(),
228        }
229    }
230
231    fn collect_custom_from_dir(
232        &self,
233        dir: &Path,
234        documents: &mut Vec<String>,
235        seen_paths: &mut HashSet<PathBuf>,
236    ) {
237        let entries = match fs::read_dir(dir) {
238            Ok(entries) => entries,
239            Err(e) if e.kind() == io::ErrorKind::NotFound => return,
240            Err(e) => {
241                tracing::warn!("Failed to read directory {}: {:?}", dir.display(), e);
242                return;
243            }
244        };
245
246        for entry in entries {
247            let entry = match entry {
248                Ok(e) => e,
249                Err(e) => {
250                    tracing::warn!(
251                        "Error reading directory entry in {}: {:?}",
252                        dir.display(),
253                        e
254                    );
255                    continue;
256                }
257            };
258            let path = entry.path();
259
260            if !path.extension().map_or(false, |ext| ext == "md") {
261                continue;
262            }
263
264            if seen_paths.contains(&path) {
265                continue;
266            }
267
268            let stem = match path.file_stem().and_then(|s| s.to_str()) {
269                Some(s) => s,
270                None => continue,
271            };
272
273            if Builtin::all().iter().any(|b| b.as_str() == stem) {
274                continue;
275            }
276
277            if let Some(content) = self.read_file(&path) {
278                tracing::debug!("Loaded custom steering document: {}", path.display());
279                seen_paths.insert(path);
280                documents.push(content);
281            }
282        }
283    }
284
285    fn collect_cursor_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
286        let rules_dir = workspace.join(".cursor").join("rules");
287        self.collect_md_files_from_dir(&rules_dir, documents);
288
289        let cursorrules = workspace.join(".cursorrules");
290        if let Some(content) = self.read_file(&cursorrules) {
291            tracing::debug!("Loaded Cursor rules: {}", cursorrules.display());
292            documents.push(content);
293        }
294    }
295
296    fn collect_cline_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
297        let cline_dir = workspace.join(".cline");
298        self.collect_md_files_from_dir(&cline_dir, documents);
299
300        let clinerules = workspace.join(".clinerules");
301        if let Some(content) = self.read_file(&clinerules) {
302            tracing::debug!("Loaded Cline rules: {}", clinerules.display());
303            documents.push(content);
304        }
305    }
306
307    fn collect_roo_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
308        let rules_dir = workspace.join(".roo").join("rules");
309        self.collect_md_files_from_dir(&rules_dir, documents);
310
311        let roorules = workspace.join(".roorules");
312        if let Some(content) = self.read_file(&roorules) {
313            tracing::debug!("Loaded Roo rules: {}", roorules.display());
314            documents.push(content);
315        }
316    }
317
318    fn collect_kiro_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
319        let steering_dir = workspace.join(".kiro").join("steering-docs");
320        self.collect_md_files_from_dir(&steering_dir, documents);
321    }
322
323    fn collect_md_files_from_dir(&self, dir: &Path, documents: &mut Vec<String>) {
324        let entries = match fs::read_dir(dir) {
325            Ok(entries) => entries,
326            Err(e) if e.kind() == io::ErrorKind::NotFound => return,
327            Err(e) => {
328                tracing::warn!("Failed to read directory {}: {:?}", dir.display(), e);
329                return;
330            }
331        };
332
333        for entry in entries {
334            let entry = match entry {
335                Ok(e) => e,
336                Err(e) => {
337                    tracing::warn!(
338                        "Error reading directory entry in {}: {:?}",
339                        dir.display(),
340                        e
341                    );
342                    continue;
343                }
344            };
345            let path = entry.path();
346
347            if !path.extension().map_or(false, |ext| ext == "md") {
348                continue;
349            }
350
351            if let Some(content) = self.read_file(&path) {
352                tracing::debug!("Loaded external steering document: {}", path.display());
353                documents.push(content);
354            }
355        }
356    }
357
358    fn read_file(&self, path: &Path) -> Option<String> {
359        match fs::read_to_string(path) {
360            Ok(content) => Some(content),
361            Err(e) if e.kind() == io::ErrorKind::NotFound => None,
362            Err(e) => {
363                tracing::warn!(
364                    "Failed to read steering document {}: {:?}",
365                    path.display(),
366                    e
367                );
368                None
369            }
370        }
371    }
372}