Skip to main content

matrixcode_core/prompt/
section.rs

1//! Section-based prompt composition
2//!
3//! Each section can be:
4//! - Static: constant string, cacheable
5//! - Dynamic: computed at runtime, not cacheable
6//!
7//! Predefined sections are available for common prompt components:
8//! - Red flags warning table
9//! - Skill priority rules
10//! - Tool usage guidelines
11
12use std::sync::Arc;
13
14/// Section content type
15#[derive(Clone)]
16pub enum SectionContent {
17    /// Static content that can be cached
18    Static(&'static str),
19    /// Dynamic content computed at runtime
20    Dynamic(Arc<dyn Fn() -> String + Send + Sync>),
21    /// Cached result (computed once and stored)
22    Cached(String),
23}
24
25impl SectionContent {
26    /// Create static content
27    pub fn static_content(s: &'static str) -> Self {
28        Self::Static(s)
29    }
30
31    /// Create dynamic content with a computation function
32    pub fn dynamic<F>(f: F) -> Self
33    where
34        F: Fn() -> String + Send + Sync + 'static,
35    {
36        Self::Dynamic(Arc::new(f))
37    }
38
39    /// Compute the content (for dynamic sections)
40    pub fn compute(&self) -> String {
41        match self {
42            Self::Static(s) => s.to_string(),
43            Self::Dynamic(f) => f(),
44            Self::Cached(s) => s.clone(),
45        }
46    }
47
48    /// Check if this content can be cached
49    pub fn is_cacheable(&self) -> bool {
50        matches!(self, Self::Static(_) | Self::Cached(_))
51    }
52
53    /// Mark content as cached (after computing)
54    pub fn cache(self, content: String) -> Self {
55        Self::Cached(content)
56    }
57}
58
59impl std::fmt::Debug for SectionContent {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            Self::Static(s) => f.debug_tuple("Static").field(&s.len()).finish(),
63            Self::Dynamic(_) => f.write_str("Dynamic(<function>)"),
64            Self::Cached(s) => f.debug_tuple("Cached").field(&s.len()).finish(),
65        }
66    }
67}
68
69/// A prompt section with name, content, and cacheability
70#[derive(Clone, Debug)]
71pub struct PromptSection {
72    /// Unique section identifier
73    pub name: String,
74    /// Section content
75    pub content: SectionContent,
76    /// Whether this section should be cached (default: true for static)
77    pub cacheable: bool,
78    /// Section priority/order (lower = earlier in prompt)
79    pub order: usize,
80}
81
82// ============================================================================
83// Predefined Sections - Common prompt components
84// ============================================================================
85
86/// Red flags warning section - prevents common AI reasoning errors
87///
88/// This section should be included early in the prompt to help the AI
89/// avoid common pitfalls like bypassing skill checks or acting before thinking.
90pub fn red_flags_section() -> PromptSection {
91    PromptSection::static_section("red_flags", RED_FLAGS_CONTENT).with_order(10)
92}
93
94/// Skill priority rules section - defines skill invocation order
95///
96/// Process skills (brainstorming, debugging) should be invoked before
97/// Implementation skills (frontend, code-review).
98pub fn skill_priority_section() -> PromptSection {
99    PromptSection::static_section("skill_priority", SKILL_PRIORITY_CONTENT).with_order(15)
100}
101
102/// Skill invocation rules section - mandatory skill check before action
103///
104/// This enforces the "1% rule": if there's even a 1% chance a skill applies,
105/// it MUST be invoked before any other response.
106pub fn skill_rules_section() -> PromptSection {
107    PromptSection::static_section("skill_rules", SKILL_RULES_CONTENT).with_order(5)
108}
109
110/// Tool usage guidelines section - how to use tools effectively
111pub fn tool_guidelines_section() -> PromptSection {
112    PromptSection::static_section("tool_guidelines", TOOL_GUIDELINES_CONTENT).with_order(20)
113}
114
115/// Predefined red flags content
116const RED_FLAGS_CONTENT: &'static str = "
117## Red Flags - STOP and reconsider
118
119These thoughts mean STOP — you're rationalizing:
120
121| Thought | Reality |
122|---------|---------|
123| \"This is just a simple question\" | Questions are tasks. Check for skills. |
124| \"I need more context first\" | Skill check comes BEFORE clarifying questions. |
125| \"Let me explore the codebase first\" | Skills tell you HOW to explore. Check first. |
126| \"I can check git/files quickly\" | Files lack conversation context. Check for skills. |
127| \"Let me gather information first\" | Skills tell you HOW to gather information. |
128| \"This doesn't need a formal skill\" | If a skill exists, use it. |
129| \"I remember this skill\" | Skills evolve. Read current version. |
130| \"This doesn't count as a task\" | Action = task. Check for skills. |
131| \"The skill is overkill\" | Simple things become complex. Use it. |
132| \"I'll just do this one thing first\" | Check BEFORE doing anything. |
133| \"This feels productive\" | Undisciplined action wastes time. Skills prevent this. |
134| \"I know what that means\" | Knowing the concept ≠ using the skill. Invoke it. |
135
136**When you catch yourself thinking these, STOP and invoke the relevant skill.**
137";
138
139/// Predefined skill priority content
140const SKILL_PRIORITY_CONTENT: &'static str = "
141## Skill Priority
142
143When multiple skills could apply, use this order:
144
1451. **Process skills first** (brainstorming, debugging, planning) - these determine HOW to approach the task
1462. **Implementation skills second** (frontend-design, mcp-builder, code-review) - these guide execution
147
148Examples:
149- \"Let's build X\" → brainstorming first, then implementation skills
150- \"Fix this bug\" → debugging first, then domain-specific skills
151- \"Review this code\" → code-review skill directly (domain-specific)
152
153## Skill Types
154
155- **Rigid skills** (TDD, debugging): Follow exactly. Don't adapt away discipline.
156- **Flexible skills** (patterns): Adapt principles to context.
157
158The skill itself tells you which type it is.
159";
160
161/// Predefined skill rules content
162const SKILL_RULES_CONTENT: &'static str = "
163<EXTREMELY-IMPORTANT>
164If you think there is even a **1% chance** a skill might apply to what you are doing,
165you **ABSOLUTELY MUST** invoke the skill.
166
167IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
168
169This is not negotiable. This is not optional. You cannot rationalize your way out of this.
170</EXTREMELY-IMPORTANT>
171
172## The Rule
173
174**Invoke relevant or requested skills BEFORE any response or action.**
175Even a 1% chance a skill might apply means that you should invoke the skill to check.
176If an invoked skill turns out to be wrong for the situation, you don't need to use it.
177";
178
179/// Predefined tool guidelines content
180const TOOL_GUIDELINES_CONTENT: &'static str = "
181## Tool Usage Guidelines
182
183### When to use tools
184
185| Task | Preferred Tool |
186|------|----------------|
187| Find files by pattern | `Glob` (not `find` command) |
188| Search file contents | `Grep` (not `grep` command) |
189| Read a specific file | `Read` (not `cat` command) |
190| Search code symbols | `codegraph_search` (not `grep`) |
191| Find function callers | `codegraph_callers` (not manual search) |
192| Trace code flow | `codegraph_trace` (one call = full path) |
193
194### CodeGraph vs Native Search
195
196Use **CodeGraph** for structural questions:
197- \"Where is X defined?\" → `codegraph_search`
198- \"What calls Y?\" → `codegraph_callers`
199- \"How does X reach Y?\" → `codegraph_trace`
200
201Use **native grep/read** for literal text:
202- String contents, comments, log messages
203- After you already have a specific file open
204
205### Rules of thumb
206
207- **Don't grep first** when looking up a symbol by name
208- **Trust CodeGraph results** — they come from full AST parse
209- **Don't re-verify** CodeGraph results with grep (slower, less accurate)
210";
211
212impl PromptSection {
213    /// Create a new static section
214    pub fn static_section(name: impl Into<String>, content: &'static str) -> Self {
215        Self {
216            name: name.into(),
217            content: SectionContent::static_content(content),
218            cacheable: true,
219            order: 0,
220        }
221    }
222
223    /// Create a new dynamic section
224    pub fn dynamic_section<F>(name: impl Into<String>, compute: F) -> Self
225    where
226        F: Fn() -> String + Send + Sync + 'static,
227    {
228        Self {
229            name: name.into(),
230            content: SectionContent::dynamic(compute),
231            cacheable: false,
232            order: 0,
233        }
234    }
235
236    /// Create a cached section (after computation)
237    pub fn cached_section(name: impl Into<String>, content: String) -> Self {
238        Self {
239            name: name.into(),
240            content: SectionContent::Cached(content),
241            cacheable: true,
242            order: 0,
243        }
244    }
245
246    /// Set section order
247    pub fn with_order(self, order: usize) -> Self {
248        Self { order, ..self }
249    }
250
251    /// Set cacheability
252    pub fn with_cacheable(self, cacheable: bool) -> Self {
253        Self { cacheable, ..self }
254    }
255
256    /// Compute and render the section content
257    pub fn render(&self) -> String {
258        let content = self.content.compute();
259        if content.is_empty() {
260            String::new()
261        } else {
262            format!("[{}]\n{}", self.name, content)
263        }
264    }
265
266    /// Compute raw content (without section header)
267    pub fn compute_content(&self) -> String {
268        self.content.compute()
269    }
270
271    /// Get estimated token count (approximate)
272    pub fn estimated_tokens(&self) -> usize {
273        // Rough estimate: ~4 chars per token for Chinese, ~1 token per word for English
274        let content = self.compute_content();
275        let chinese_chars = content
276            .chars()
277            .filter(|c| c.is_alphabetic() && c.len_utf8() > 1)
278            .count();
279        let english_words = content.split_whitespace().count();
280        chinese_chars / 3 + english_words + (content.len() - chinese_chars) / 4
281    }
282}
283
284/// Builder for creating prompt sections
285pub struct SectionBuilder {
286    sections: Vec<PromptSection>,
287}
288
289impl SectionBuilder {
290    pub fn new() -> Self {
291        Self {
292            sections: Vec::new(),
293        }
294    }
295
296    /// Add a static section
297    pub fn add_static(self, name: impl Into<String>, content: &'static str) -> Self {
298        self.add_section(PromptSection::static_section(name, content))
299    }
300
301    /// Add a dynamic section
302    pub fn add_dynamic<F>(self, name: impl Into<String>, compute: F) -> Self
303    where
304        F: Fn() -> String + Send + Sync + 'static,
305    {
306        self.add_section(PromptSection::dynamic_section(name, compute))
307    }
308
309    /// Add a section
310    pub fn add_section(mut self, section: PromptSection) -> Self {
311        self.sections.push(section);
312        self
313    }
314
315    /// Build sections sorted by order
316    pub fn build(self) -> Vec<PromptSection> {
317        let mut sections = self.sections;
318        sections.sort_by_key(|s| s.order);
319        sections
320    }
321}
322
323impl Default for SectionBuilder {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_static_section() {
335        let section = PromptSection::static_section("identity", "You are an AI assistant.");
336        assert!(section.cacheable);
337        assert_eq!(section.compute_content(), "You are an AI assistant.");
338    }
339
340    #[test]
341    fn test_dynamic_section() {
342        let section = PromptSection::dynamic_section("date", || {
343            format!("Current date: {}", chrono::Local::now().format("%Y-%m-%d"))
344        });
345        assert!(!section.cacheable);
346        let content = section.compute_content();
347        assert!(content.starts_with("Current date:"));
348    }
349
350    #[test]
351    fn test_render_with_header() {
352        let section = PromptSection::static_section("test", "Hello");
353        let rendered = section.render();
354        assert_eq!(rendered, "[test]\nHello");
355    }
356
357    #[test]
358    fn test_section_builder() {
359        let sections = SectionBuilder::new()
360            .add_static("a", "content a")
361            .add_static("b", "content b")
362            .build();
363        assert_eq!(sections.len(), 2);
364    }
365
366    #[test]
367    fn test_order_sorting() {
368        let sections = SectionBuilder::new()
369            .add_section(PromptSection::static_section("last", "c").with_order(10))
370            .add_section(PromptSection::static_section("first", "a").with_order(1))
371            .add_section(PromptSection::static_section("middle", "b").with_order(5))
372            .build();
373
374        assert_eq!(sections[0].name, "first");
375        assert_eq!(sections[1].name, "middle");
376        assert_eq!(sections[2].name, "last");
377    }
378
379    #[test]
380    fn test_predefined_red_flags_section() {
381        let section = red_flags_section();
382        assert_eq!(section.name, "red_flags");
383        assert!(section.cacheable);
384        assert!(section.order == 10);
385        let content = section.compute_content();
386        assert!(content.contains("Red Flags"));
387        assert!(content.contains("STOP"));
388        assert!(content.contains("This is just a simple question"));
389    }
390
391    #[test]
392    fn test_predefined_skill_priority_section() {
393        let section = skill_priority_section();
394        assert_eq!(section.name, "skill_priority");
395        assert!(section.cacheable);
396        assert!(section.order == 15);
397        let content = section.compute_content();
398        assert!(content.contains("Skill Priority"));
399        assert!(content.contains("Process skills first"));
400    }
401
402    #[test]
403    fn test_predefined_skill_rules_section() {
404        let section = skill_rules_section();
405        assert_eq!(section.name, "skill_rules");
406        assert!(section.cacheable);
407        assert!(section.order == 5); // Should be earliest
408        let content = section.compute_content();
409        assert!(content.contains("1%"));
410        assert!(content.contains("MUST"));
411    }
412
413    #[test]
414    fn test_predefined_tool_guidelines_section() {
415        let section = tool_guidelines_section();
416        assert_eq!(section.name, "tool_guidelines");
417        assert!(section.cacheable);
418        let content = section.compute_content();
419        assert!(content.contains("Glob"));
420        assert!(content.contains("Grep"));
421        assert!(content.contains("CodeGraph"));
422    }
423
424    #[test]
425    fn test_predefined_sections_order() {
426        // Verify predefined sections have correct order
427        let rules = skill_rules_section();
428        let flags = red_flags_section();
429        let priority = skill_priority_section();
430        let tools = tool_guidelines_section();
431
432        // Rules should come first (lowest order)
433        assert!(rules.order < flags.order);
434        assert!(flags.order < priority.order);
435        assert!(priority.order < tools.order);
436    }
437}