1use super::config::ContextConfig;
2use super::task_state::TaskState;
3use super::token_engine::ContextTokenEngine;
4use crate::types::message::Message;
5
6#[derive(Debug, Clone)]
8pub struct Partition {
9 pub messages: Vec<Message>,
10 pub token_count: u32,
11}
12
13impl Partition {
14 pub fn new() -> Self {
15 Self { messages: Vec::new(), token_count: 0 }
16 }
17
18 pub fn push(&mut self, mut msg: Message, token_count: u32) {
19 msg.token_count = Some(token_count);
20 self.token_count += token_count;
21 self.messages.push(msg);
22 }
23
24 pub fn clear(&mut self) {
25 self.messages.clear();
26 self.token_count = 0;
27 }
28
29 pub fn len(&self) -> usize { self.messages.len() }
30 pub fn is_empty(&self) -> bool { self.messages.is_empty() }
31}
32
33impl Default for Partition {
34 fn default() -> Self { Self::new() }
35}
36
37#[derive(Debug, Clone)]
41pub struct KnowledgeEntry {
42 pub key: Option<compact_str::CompactString>,
44 pub message: Message,
45 pub tokens: u32,
46 pub pinned: bool,
48 pub evict_at_boundary: bool,
52 pub pending: Option<Box<(Message, u32)>>,
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct KnowledgeSweep {
60 pub removed_keys: Vec<String>,
61 pub tokens_freed: u32,
62 pub changed: bool,
64}
65
66#[derive(Debug, Clone, Default)]
70pub struct KnowledgePartition {
71 pub entries: Vec<KnowledgeEntry>,
72 pub token_count: u32,
73}
74
75impl KnowledgePartition {
76 pub fn new() -> Self { Self::default() }
77
78 pub fn push(&mut self, mut msg: Message, token_count: u32) {
80 msg.token_count = Some(token_count);
81 self.token_count += token_count;
82 self.entries.push(KnowledgeEntry {
83 key: None,
84 message: msg,
85 tokens: token_count,
86 pinned: false,
87 evict_at_boundary: false,
88 pending: None,
89 });
90 }
91
92 pub fn push_entry(
96 &mut self,
97 key: Option<compact_str::CompactString>,
98 mut msg: Message,
99 tokens: u32,
100 pinned: bool,
101 ) {
102 msg.token_count = Some(tokens);
103 if let Some(ref k) = key {
104 if let Some(entry) = self.entries.iter_mut().find(|e| e.key.as_ref() == Some(k)) {
105 entry.pending = Some(Box::new((msg, tokens)));
106 entry.evict_at_boundary = false;
107 entry.pinned = pinned;
108 return;
109 }
110 }
111 self.token_count += tokens;
112 self.entries.push(KnowledgeEntry {
113 key,
114 message: msg,
115 tokens,
116 pinned,
117 evict_at_boundary: false,
118 pending: None,
119 });
120 }
121
122 pub fn remove(&mut self, key: &str) -> bool {
125 match self.entries.iter_mut().find(|e| e.key.as_deref() == Some(key)) {
126 Some(entry) => {
127 entry.evict_at_boundary = true;
128 entry.pending = None;
129 true
130 }
131 None => false,
132 }
133 }
134
135 pub fn sweep_at_boundary(&mut self) -> KnowledgeSweep {
138 let mut sweep = KnowledgeSweep::default();
139 for entry in &mut self.entries {
140 if let Some(replacement) = entry.pending.take() {
141 let (msg, tokens) = *replacement;
142 self.token_count = self.token_count - entry.tokens + tokens;
143 entry.message = msg;
144 entry.tokens = tokens;
145 sweep.changed = true;
146 }
147 }
148 let before = self.entries.len();
149 self.entries.retain(|e| {
150 if e.evict_at_boundary {
151 if let Some(ref k) = e.key {
152 sweep.removed_keys.push(k.to_string());
153 }
154 sweep.tokens_freed += e.tokens;
155 false
156 } else {
157 true
158 }
159 });
160 if self.entries.len() != before {
161 self.token_count = self.token_count.saturating_sub(sweep.tokens_freed);
162 sweep.changed = true;
163 }
164 sweep
165 }
166
167 pub fn messages(&self) -> impl Iterator<Item = &Message> {
169 self.entries.iter().map(|e| &e.message)
170 }
171
172 pub fn len(&self) -> usize { self.entries.len() }
173 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
174}
175
176pub struct ContextPartitions {
195 pub system: Partition,
196 pub knowledge: KnowledgePartition,
197 pub task_state: TaskState,
198 pub signals: Vec<String>,
201 pub history: Partition,
202}
203
204impl ContextPartitions {
205 pub fn new(_config: &ContextConfig) -> Self {
206 Self {
207 system: Partition::new(),
208 knowledge: KnowledgePartition::new(),
209 task_state: TaskState::default(),
210 signals: Vec::new(),
211 history: Partition::new(),
212 }
213 }
214
215 pub fn total_tokens(&self, engine: &ContextTokenEngine) -> u32 {
218 self.system.token_count
219 + self.knowledge.token_count
220 + engine.count(&self.task_state.format_compact())
221 + self.history.token_count
222 }
223}
224
225impl Default for ContextPartitions {
226 fn default() -> Self {
227 Self::new(&ContextConfig::default())
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::context::config::ContextConfig;
235 use crate::context::token_engine::ContextTokenEngine;
236 use crate::types::message::Message;
237
238 fn engine() -> ContextTokenEngine { ContextTokenEngine::char_approx() }
239
240 #[test]
241 fn push_updates_token_count() {
242 let mut ctx = ContextPartitions::new(&ContextConfig::default());
243 let base = ctx.total_tokens(&engine());
244 ctx.system.push(Message::system("rules"), 10);
245 ctx.history.push(Message::user("hello"), 5);
246 assert_eq!(ctx.total_tokens(&engine()), base + 15);
247 }
248
249 #[test]
250 fn task_state_tokens_included_in_total() {
251 use crate::context::task_state::TaskState;
252 let mut ctx = ContextPartitions::new(&ContextConfig::default());
253 let before = ctx.total_tokens(&engine());
254 ctx.task_state = TaskState { goal: "do something important".to_string(), ..Default::default() };
255 let after = ctx.total_tokens(&engine());
256 assert!(after > before, "task_state should contribute to total_tokens");
257 }
258
259 #[test]
260 fn knowledge_tokens_included_in_total() {
261 let mut ctx = ContextPartitions::new(&ContextConfig::default());
262 let before = ctx.total_tokens(&engine());
263 ctx.knowledge.push(Message::system("skill: debug"), 20);
264 assert_eq!(ctx.total_tokens(&engine()), before + 20);
265 }
266
267 fn text_of(p: &KnowledgePartition) -> Vec<String> {
270 p.messages().filter_map(|m| m.content.as_text().map(str::to_string)).collect()
271 }
272
273 #[test]
274 fn keyed_upsert_defers_to_boundary() {
275 let mut p = KnowledgePartition::new();
276 p.push_entry(Some("ref".into()), Message::system("v1"), 10, false);
277 p.push_entry(Some("ref".into()), Message::system("v2"), 12, false);
278 assert_eq!(p.len(), 1);
280 assert_eq!(text_of(&p), vec!["v1"]);
281 assert_eq!(p.token_count, 10);
282
283 let sweep = p.sweep_at_boundary();
284 assert!(sweep.changed);
285 assert!(sweep.removed_keys.is_empty(), "upsert-only sweep removes nothing");
286 assert_eq!(text_of(&p), vec!["v2"]);
287 assert_eq!(p.token_count, 12);
288 }
289
290 #[test]
291 fn remove_marks_then_sweep_drops() {
292 let mut p = KnowledgePartition::new();
293 p.push_entry(Some("ref".into()), Message::system("pinned ref"), 8, false);
294 assert!(p.remove("ref"));
295 assert_eq!(p.len(), 1);
297 assert_eq!(text_of(&p), vec!["pinned ref"]);
298
299 let sweep = p.sweep_at_boundary();
300 assert!(sweep.changed);
301 assert_eq!(sweep.removed_keys, vec!["ref".to_string()]);
302 assert_eq!(sweep.tokens_freed, 8);
303 assert!(p.is_empty());
304 assert_eq!(p.token_count, 0);
305 }
306
307 #[test]
308 fn remove_unknown_key_errs_open() {
309 let mut p = KnowledgePartition::new();
310 p.push(Message::system("unkeyed"), 5);
311 assert!(!p.remove("missing"));
312 assert!(!p.sweep_at_boundary().changed);
313 assert_eq!(p.len(), 1);
314 }
315
316 #[test]
317 fn same_key_push_after_remove_revives_entry() {
318 let mut p = KnowledgePartition::new();
319 p.push_entry(Some("ref".into()), Message::system("v1"), 5, false);
320 p.remove("ref");
321 p.push_entry(Some("ref".into()), Message::system("v2"), 6, false);
324 let sweep = p.sweep_at_boundary();
325 assert!(sweep.removed_keys.is_empty());
326 assert_eq!(text_of(&p), vec!["v2"]);
327 }
328
329 #[test]
330 fn fresh_keys_and_unkeyed_append_immediately() {
331 let mut p = KnowledgePartition::new();
332 p.push(Message::system("legacy"), 3);
333 p.push_entry(Some("a".into()), Message::system("fresh"), 4, true);
334 assert_eq!(text_of(&p), vec!["legacy", "fresh"]);
336 assert_eq!(p.token_count, 7);
337 assert!(p.entries[1].pinned);
338 }
339}