1#[derive(Debug, Clone)]
5pub struct MemoryResult {
7 pub content: String,
8 pub score: f32,
9 pub source: String,
10 pub confidence: f32,
12}
13
14#[derive(Debug, Clone)]
15pub enum ContextSection {
17 Identity {
18 content: String,
19 },
20 Bootstrap {
21 files: Vec<(String, String)>, },
23 AlwaysOnSkills {
24 full_content: String,
25 },
26 AvailableSkills {
27 summaries: Vec<String>,
28 },
29 Memory {
30 results: Vec<MemoryResult>,
31 },
32}
33
34impl ContextSection {
35 fn priority(&self) -> u8 {
36 match self {
37 Self::Identity { .. } => 0,
38 Self::Bootstrap { .. } => 1,
39 Self::AlwaysOnSkills { .. } => 2,
40 Self::AvailableSkills { .. } => 3,
41 Self::Memory { .. } => 4,
42 }
43 }
44
45 fn render(&self) -> String {
46 match self {
47 Self::Identity { content } => format!("# Identity\n{}\n", content),
48 Self::Bootstrap { files } => {
49 let mut out = String::from("# Bootstrap\n");
50 for (name, content) in files {
51 out.push_str(&format!("## {}\n{}\n", name, content));
52 }
53 out
54 }
55 Self::AlwaysOnSkills { full_content } => {
56 format!("# Skills (Active)\n{}\n", full_content)
57 }
58 Self::AvailableSkills { summaries } => {
59 let mut out = String::from("# Skills (Available)\n");
60 for s in summaries {
61 out.push_str(&format!("- {}\n", s));
62 }
63 out
64 }
65 Self::Memory { results } => {
66 let mut out = String::from("# Memory\n");
67 for r in results {
68 out.push_str(&format!(
69 "<!-- score={:.2} confidence={:.2} src={} -->\n{}\n\n",
70 r.score, r.confidence, r.source, r.content
71 ));
72 }
73 out
74 }
75 }
76 }
77
78 fn render_partial(&self, token_budget: usize) -> String {
79 match self {
80 Self::Memory { results } => {
81 let mut out = String::from("# Memory\n");
82 let mut used = estimate_tokens(&out);
83 for r in results {
85 let line = format!(
86 "<!-- score={:.2} confidence={:.2} src={} -->\n{}\n\n",
87 r.score, r.confidence, r.source, r.content
88 );
89 let t = estimate_tokens(&line);
90 if used + t > token_budget {
91 break;
92 }
93 out.push_str(&line);
94 used += t;
95 }
96 out
97 }
98 other => other.render(),
99 }
100 }
101}
102
103pub fn estimate_tokens(text: &str) -> usize {
105 text.len().div_ceil(4)
106}
107
108pub struct ContextBuilder {
110 pub budget: usize,
111 pub sections: Vec<ContextSection>,
112}
113
114impl ContextBuilder {
115 pub fn new(budget: usize) -> Self {
117 Self {
118 budget,
119 sections: Vec::new(),
120 }
121 }
122
123 pub fn with_section(mut self, section: ContextSection) -> Self {
125 self.sections.push(section);
126 self
127 }
128
129 pub fn build(&self) -> String {
131 let mut sections = self.sections.clone();
132 sections.sort_by_key(|s| s.priority());
133
134 let mut output = String::new();
135 let mut used = 0usize;
136
137 for (i, section) in sections.iter().enumerate() {
138 let content = section.render();
139 let tokens = estimate_tokens(&content);
140 if used + tokens <= self.budget {
141 output.push_str(&content);
142 used += tokens;
143 } else {
144 let remaining = self.budget.saturating_sub(used);
146 if i == sections.len() - 1 || section.priority() == 4 {
147 output.push_str(§ion.render_partial(remaining));
148 }
149 break;
150 }
151 }
152 output
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn estimate_tokens_basic() {
162 assert_eq!(estimate_tokens("hello"), 2); assert_eq!(estimate_tokens("1234"), 1);
165 assert_eq!(estimate_tokens(""), 0);
166 }
167
168 #[test]
169 fn context_section_priority_order() {
170 let identity = ContextSection::Identity { content: "I am Aegis".into() };
171 let bootstrap = ContextSection::Bootstrap { files: vec![] };
172 let memory = ContextSection::Memory { results: vec![] };
173 assert!(identity.priority() < bootstrap.priority());
174 assert!(bootstrap.priority() < memory.priority());
175 }
176
177 #[test]
178 fn context_section_render_identity() {
179 let section = ContextSection::Identity { content: "I am Aegis".into() };
180 let rendered = section.render();
181 assert!(rendered.contains("# Identity"));
182 assert!(rendered.contains("I am Aegis"));
183 }
184
185 #[test]
186 fn context_section_render_bootstrap() {
187 let section = ContextSection::Bootstrap {
188 files: vec![("rules.md".into(), "Be helpful".into())],
189 };
190 let rendered = section.render();
191 assert!(rendered.contains("# Bootstrap"));
192 assert!(rendered.contains("rules.md"));
193 assert!(rendered.contains("Be helpful"));
194 }
195
196 #[test]
197 fn context_section_render_available_skills() {
198 let section = ContextSection::AvailableSkills {
199 summaries: vec!["skill-a".into(), "skill-b".into()],
200 };
201 let rendered = section.render();
202 assert!(rendered.contains("# Skills (Available)"));
203 assert!(rendered.contains("skill-a"));
204 assert!(rendered.contains("skill-b"));
205 }
206
207 #[test]
208 fn context_builder_respects_budget() {
209 let builder = ContextBuilder::new(10) .with_section(ContextSection::Identity { content: "I am Aegis the runtime".into() })
211 .with_section(ContextSection::Bootstrap { files: vec![("f".into(), "content".into())] });
212 let output = builder.build();
213 assert!(!output.is_empty());
215 }
216
217 #[test]
218 fn context_builder_priority_sorting() {
219 let builder = ContextBuilder::new(10000)
221 .with_section(ContextSection::Memory {
222 results: vec![MemoryResult {
223 content: "some memory".into(),
224 score: 0.9,
225 source: "test".into(),
226 confidence: 0.8,
227 }],
228 })
229 .with_section(ContextSection::Identity { content: "I am Aegis".into() });
230 let output = builder.build();
231 let identity_pos = output.find("I am Aegis").unwrap();
232 let memory_pos = output.find("some memory").unwrap();
233 assert!(identity_pos < memory_pos, "identity should come before memory");
234 }
235
236 #[test]
237 fn context_builder_empty() {
238 let builder = ContextBuilder::new(1000);
239 let output = builder.build();
240 assert!(output.is_empty());
241 }
242
243 #[test]
244 fn context_section_render_partial_memory() {
245 let results = vec![
246 MemoryResult { content: "a".repeat(500), score: 0.9, source: "s1".into(), confidence: 0.8 },
247 MemoryResult { content: "b".repeat(500), score: 0.5, source: "s2".into(), confidence: 0.7 },
248 ];
249 let section = ContextSection::Memory { results };
250 let partial = section.render_partial(10);
252 assert!(partial.len() < section.render().len());
253 }
254}