claude_agent/common/
index.rs

1//! Index trait for progressive disclosure pattern.
2//!
3//! An `Index` represents minimal metadata that is always loaded in context,
4//! while the full content is loaded on-demand via `ContentSource`.
5
6use async_trait::async_trait;
7
8use super::{ContentSource, Named, SourceType};
9
10/// Core trait for index entries in the progressive disclosure pattern.
11///
12/// Index entries contain minimal metadata (name, description) that is always
13/// available in the system prompt, while full content is loaded on-demand.
14///
15/// # Token Efficiency
16///
17/// By keeping only metadata in context:
18/// - 50 skills × ~20 tokens each = ~1,000 tokens (always loaded)
19/// - vs 50 skills × ~500 tokens each = ~25,000 tokens (if fully loaded)
20///
21/// # Example Implementation
22///
23/// ```ignore
24/// pub struct SkillIndex {
25///     name: String,
26///     description: String,
27///     source: ContentSource,
28///     source_type: SourceType,
29/// }
30///
31/// impl Index for SkillIndex {
32///     fn source(&self) -> &ContentSource { &self.source }
33///     fn source_type(&self) -> SourceType { self.source_type }
34///     fn to_summary_line(&self) -> String {
35///         format!("- {}: {}", self.name, self.description)
36///     }
37/// }
38/// ```
39#[async_trait]
40pub trait Index: Named + Clone + Send + Sync + 'static {
41    /// Get the content source for lazy loading.
42    fn source(&self) -> &ContentSource;
43
44    /// Get the source type (builtin, user, project, managed).
45    fn source_type(&self) -> SourceType;
46
47    /// Get the priority for override ordering.
48    ///
49    /// Higher priority indices override lower priority ones with the same name.
50    /// Default ordering: Project(20) > User(10) > Builtin(0)
51    fn priority(&self) -> i32 {
52        match self.source_type() {
53            SourceType::Project => 20,
54            SourceType::User => 10,
55            SourceType::Builtin => 0,
56            SourceType::Managed => 5,
57        }
58    }
59
60    /// Generate a summary line for context injection.
61    ///
62    /// This should be a compact representation suitable for system prompts.
63    fn to_summary_line(&self) -> String;
64
65    /// Load the full content from the source.
66    ///
67    /// This is the lazy-loading mechanism. Content is fetched only when needed.
68    async fn load_content(&self) -> crate::Result<String> {
69        self.source().load().await
70    }
71
72    /// Get a short description for this index entry.
73    fn description(&self) -> &str {
74        ""
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use async_trait::async_trait;
81
82    use super::*;
83    use crate::common::Named;
84
85    #[derive(Clone, Debug)]
86    struct TestIndex {
87        name: String,
88        desc: String,
89        source: ContentSource,
90        source_type: SourceType,
91    }
92
93    impl Named for TestIndex {
94        fn name(&self) -> &str {
95            &self.name
96        }
97    }
98
99    #[async_trait]
100    impl Index for TestIndex {
101        fn source(&self) -> &ContentSource {
102            &self.source
103        }
104
105        fn source_type(&self) -> SourceType {
106            self.source_type
107        }
108
109        fn to_summary_line(&self) -> String {
110            format!("- {}: {}", self.name, self.desc)
111        }
112
113        fn description(&self) -> &str {
114            &self.desc
115        }
116    }
117
118    #[test]
119    fn test_priority_ordering() {
120        let builtin = TestIndex {
121            name: "test".into(),
122            desc: "desc".into(),
123            source: ContentSource::in_memory(""),
124            source_type: SourceType::Builtin,
125        };
126
127        let user = TestIndex {
128            name: "test".into(),
129            desc: "desc".into(),
130            source: ContentSource::in_memory(""),
131            source_type: SourceType::User,
132        };
133
134        let project = TestIndex {
135            name: "test".into(),
136            desc: "desc".into(),
137            source: ContentSource::in_memory(""),
138            source_type: SourceType::Project,
139        };
140
141        assert!(project.priority() > user.priority());
142        assert!(user.priority() > builtin.priority());
143    }
144
145    #[tokio::test]
146    async fn test_load_content() {
147        let index = TestIndex {
148            name: "test".into(),
149            desc: "desc".into(),
150            source: ContentSource::in_memory("full content here"),
151            source_type: SourceType::User,
152        };
153
154        let content = index.load_content().await.unwrap();
155        assert_eq!(content, "full content here");
156    }
157
158    #[test]
159    fn test_summary_line() {
160        let index = TestIndex {
161            name: "commit".into(),
162            desc: "Create git commits".into(),
163            source: ContentSource::in_memory(""),
164            source_type: SourceType::User,
165        };
166
167        assert_eq!(index.to_summary_line(), "- commit: Create git commits");
168    }
169}