Skip to main content

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::Managed => 5,
56            SourceType::Builtin => 0,
57            SourceType::Plugin => -5,
58        }
59    }
60
61    /// Generate a summary line for context injection.
62    ///
63    /// This should be a compact representation suitable for system prompts.
64    fn to_summary_line(&self) -> String;
65
66    /// Load the full content from the source.
67    ///
68    /// This is the lazy-loading mechanism. Content is fetched only when needed.
69    async fn load_content(&self) -> crate::Result<String> {
70        self.source().load().await
71    }
72
73    /// Get a short description for this index entry.
74    fn description(&self) -> &str {
75        ""
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use async_trait::async_trait;
82
83    use super::*;
84    use crate::common::Named;
85
86    #[derive(Clone, Debug)]
87    struct TestIndex {
88        name: String,
89        desc: String,
90        source: ContentSource,
91        source_type: SourceType,
92    }
93
94    impl Named for TestIndex {
95        fn name(&self) -> &str {
96            &self.name
97        }
98    }
99
100    #[async_trait]
101    impl Index for TestIndex {
102        fn source(&self) -> &ContentSource {
103            &self.source
104        }
105
106        fn source_type(&self) -> SourceType {
107            self.source_type
108        }
109
110        fn to_summary_line(&self) -> String {
111            format!("- {}: {}", self.name, self.desc)
112        }
113
114        fn description(&self) -> &str {
115            &self.desc
116        }
117    }
118
119    #[test]
120    fn test_priority_ordering() {
121        let builtin = TestIndex {
122            name: "test".into(),
123            desc: "desc".into(),
124            source: ContentSource::in_memory(""),
125            source_type: SourceType::Builtin,
126        };
127
128        let user = TestIndex {
129            name: "test".into(),
130            desc: "desc".into(),
131            source: ContentSource::in_memory(""),
132            source_type: SourceType::User,
133        };
134
135        let project = TestIndex {
136            name: "test".into(),
137            desc: "desc".into(),
138            source: ContentSource::in_memory(""),
139            source_type: SourceType::Project,
140        };
141
142        let plugin = TestIndex {
143            name: "test".into(),
144            desc: "desc".into(),
145            source: ContentSource::in_memory(""),
146            source_type: SourceType::Plugin,
147        };
148
149        assert!(project.priority() > user.priority());
150        assert!(user.priority() > builtin.priority());
151        assert!(builtin.priority() > plugin.priority());
152        assert_eq!(plugin.priority(), -5);
153    }
154
155    #[tokio::test]
156    async fn test_load_content() {
157        let index = TestIndex {
158            name: "test".into(),
159            desc: "desc".into(),
160            source: ContentSource::in_memory("full content here"),
161            source_type: SourceType::User,
162        };
163
164        let content = index.load_content().await.unwrap();
165        assert_eq!(content, "full content here");
166    }
167
168    #[test]
169    fn test_summary_line() {
170        let index = TestIndex {
171            name: "commit".into(),
172            desc: "Create git commits".into(),
173            source: ContentSource::in_memory(""),
174            source_type: SourceType::User,
175        };
176
177        assert_eq!(index.to_summary_line(), "- commit: Create git commits");
178    }
179}