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, msg: Message, token_count: u32) {
80 self.push_entry(None, msg, token_count, false);
81 }
82
83 pub fn push_entry(
87 &mut self,
88 key: Option<compact_str::CompactString>,
89 mut msg: Message,
90 tokens: u32,
91 pinned: bool,
92 ) {
93 msg.token_count = Some(tokens);
94 if let Some(ref k) = key {
95 if let Some(entry) = self.entries.iter_mut().find(|e| e.key.as_ref() == Some(k)) {
96 entry.pending = Some(Box::new((msg, tokens)));
97 entry.evict_at_boundary = false;
98 entry.pinned = pinned;
99 return;
100 }
101 }
102 self.token_count += tokens;
103 self.entries.push(KnowledgeEntry {
104 key,
105 message: msg,
106 tokens,
107 pinned,
108 evict_at_boundary: false,
109 pending: None,
110 });
111 }
112
113 pub fn remove(&mut self, key: &str) -> bool {
116 match self.entries.iter_mut().find(|e| e.key.as_deref() == Some(key)) {
117 Some(entry) => {
118 entry.evict_at_boundary = true;
119 entry.pending = None;
120 true
121 }
122 None => false,
123 }
124 }
125
126 pub fn sweep_at_boundary(&mut self) -> KnowledgeSweep {
129 let mut sweep = KnowledgeSweep::default();
130 for entry in &mut self.entries {
131 if let Some(replacement) = entry.pending.take() {
132 let (msg, tokens) = *replacement;
133 self.token_count = self.token_count - entry.tokens + tokens;
134 entry.message = msg;
135 entry.tokens = tokens;
136 sweep.changed = true;
137 }
138 }
139 let before = self.entries.len();
140 self.entries.retain(|e| {
141 if e.evict_at_boundary {
142 if let Some(ref k) = e.key {
143 sweep.removed_keys.push(k.to_string());
144 }
145 sweep.tokens_freed += e.tokens;
146 false
147 } else {
148 true
149 }
150 });
151 if self.entries.len() != before {
152 self.token_count = self.token_count.saturating_sub(sweep.tokens_freed);
153 sweep.changed = true;
154 }
155 sweep
156 }
157
158 pub fn messages(&self) -> impl Iterator<Item = &Message> {
160 self.entries.iter().map(|e| &e.message)
161 }
162
163 pub fn len(&self) -> usize { self.entries.len() }
164 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
165}
166
167pub struct ContextPartitions {
187 pub system: Partition,
188 pub knowledge: KnowledgePartition,
189 pub task_state: TaskState,
190 pub signals: Vec<String>,
193 pub history: Partition,
194}
195
196impl ContextPartitions {
197 pub fn new(_config: &ContextConfig) -> Self {
198 Self {
199 system: Partition::new(),
200 knowledge: KnowledgePartition::new(),
201 task_state: TaskState::default(),
202 signals: Vec::new(),
203 history: Partition::new(),
204 }
205 }
206
207 pub fn total_tokens(&self, engine: &ContextTokenEngine) -> u32 {
210 self.system.token_count
211 + self.knowledge.token_count
212 + engine.count(&self.task_state.format_compact())
213 + self.history.token_count
214 }
215}
216
217impl Default for ContextPartitions {
218 fn default() -> Self {
219 Self::new(&ContextConfig::default())
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::context::config::ContextConfig;
227 use crate::context::token_engine::ContextTokenEngine;
228 use crate::types::message::Message;
229
230 fn engine() -> ContextTokenEngine { ContextTokenEngine::char_approx() }
231
232 #[test]
233 fn push_updates_token_count() {
234 let mut ctx = ContextPartitions::new(&ContextConfig::default());
235 let base = ctx.total_tokens(&engine());
236 ctx.system.push(Message::system("rules"), 10);
237 ctx.history.push(Message::user("hello"), 5);
238 assert_eq!(ctx.total_tokens(&engine()), base + 15);
239 }
240
241 #[test]
242 fn task_state_tokens_included_in_total() {
243 use crate::context::task_state::TaskState;
244 let mut ctx = ContextPartitions::new(&ContextConfig::default());
245 let before = ctx.total_tokens(&engine());
246 ctx.task_state = TaskState { goal: "do something important".to_string(), ..Default::default() };
247 let after = ctx.total_tokens(&engine());
248 assert!(after > before, "task_state should contribute to total_tokens");
249 }
250
251 #[test]
252 fn knowledge_tokens_included_in_total() {
253 let mut ctx = ContextPartitions::new(&ContextConfig::default());
254 let before = ctx.total_tokens(&engine());
255 ctx.knowledge.push(Message::system("skill: debug"), 20);
256 assert_eq!(ctx.total_tokens(&engine()), before + 20);
257 }
258
259 fn text_of(p: &KnowledgePartition) -> Vec<String> {
262 p.messages().filter_map(|m| m.content.as_text().map(str::to_string)).collect()
263 }
264
265 #[test]
266 fn keyed_upsert_defers_to_boundary() {
267 let mut p = KnowledgePartition::new();
268 p.push_entry(Some("ref".into()), Message::system("v1"), 10, false);
269 p.push_entry(Some("ref".into()), Message::system("v2"), 12, false);
270 assert_eq!(p.len(), 1);
272 assert_eq!(text_of(&p), vec!["v1"]);
273 assert_eq!(p.token_count, 10);
274
275 let sweep = p.sweep_at_boundary();
276 assert!(sweep.changed);
277 assert!(sweep.removed_keys.is_empty(), "upsert-only sweep removes nothing");
278 assert_eq!(text_of(&p), vec!["v2"]);
279 assert_eq!(p.token_count, 12);
280 }
281
282 #[test]
283 fn remove_marks_then_sweep_drops() {
284 let mut p = KnowledgePartition::new();
285 p.push_entry(Some("ref".into()), Message::system("pinned ref"), 8, false);
286 assert!(p.remove("ref"));
287 assert_eq!(p.len(), 1);
289 assert_eq!(text_of(&p), vec!["pinned ref"]);
290
291 let sweep = p.sweep_at_boundary();
292 assert!(sweep.changed);
293 assert_eq!(sweep.removed_keys, vec!["ref".to_string()]);
294 assert_eq!(sweep.tokens_freed, 8);
295 assert!(p.is_empty());
296 assert_eq!(p.token_count, 0);
297 }
298
299 #[test]
300 fn remove_unknown_key_errs_open() {
301 let mut p = KnowledgePartition::new();
302 p.push(Message::system("unkeyed"), 5);
303 assert!(!p.remove("missing"));
304 assert!(!p.sweep_at_boundary().changed);
305 assert_eq!(p.len(), 1);
306 }
307
308 #[test]
309 fn same_key_push_after_remove_revives_entry() {
310 let mut p = KnowledgePartition::new();
311 p.push_entry(Some("ref".into()), Message::system("v1"), 5, false);
312 p.remove("ref");
313 p.push_entry(Some("ref".into()), Message::system("v2"), 6, false);
316 let sweep = p.sweep_at_boundary();
317 assert!(sweep.removed_keys.is_empty());
318 assert_eq!(text_of(&p), vec!["v2"]);
319 }
320
321 #[test]
322 fn fresh_keys_and_unkeyed_append_immediately() {
323 let mut p = KnowledgePartition::new();
324 p.push(Message::system("legacy"), 3);
325 p.push_entry(Some("a".into()), Message::system("fresh"), 4, true);
326 assert_eq!(text_of(&p), vec!["legacy", "fresh"]);
328 assert_eq!(p.token_count, 7);
329 assert!(p.entries[1].pinned);
330 }
331}