1use super::config::ContextConfig;
2use super::task_state::TaskState;
3use super::token_engine::ContextTokenEngine;
4use crate::mm::value::{RetentionFeatures, RetentionKind, deterministic_retention_score};
5use crate::types::message::Message;
6
7#[derive(Debug, Clone)]
9pub struct Partition {
10 pub messages: Vec<Message>,
11 pub token_count: u32,
12}
13
14impl Partition {
15 pub fn new() -> Self {
16 Self {
17 messages: Vec::new(),
18 token_count: 0,
19 }
20 }
21
22 pub fn push(&mut self, mut msg: Message, token_count: u32) {
23 msg.token_count = Some(token_count);
24 self.token_count += token_count;
25 self.messages.push(msg);
26 }
27
28 pub fn clear(&mut self) {
29 self.messages.clear();
30 self.token_count = 0;
31 }
32
33 pub fn len(&self) -> usize {
34 self.messages.len()
35 }
36 pub fn is_empty(&self) -> bool {
37 self.messages.is_empty()
38 }
39}
40
41impl Default for Partition {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47#[derive(Debug, Clone)]
51pub struct KnowledgeEntry {
52 pub key: Option<compact_str::CompactString>,
54 pub message: Message,
55 pub tokens: u32,
56 pub pinned: bool,
58 pub evict_at_boundary: bool,
62 pub pending: Option<Box<(Message, u32)>>,
65 pub use_count: u64,
67 pub last_used_step: Option<u64>,
68}
69
70#[derive(Debug, Clone, Default)]
72pub struct KnowledgeSweep {
73 pub removed_keys: Vec<String>,
74 pub tokens_freed: u32,
75 pub changed: bool,
77}
78
79#[derive(Debug, Clone, Default)]
83pub struct KnowledgePartition {
84 pub entries: Vec<KnowledgeEntry>,
85 pub token_count: u32,
86}
87
88impl KnowledgePartition {
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn push(&mut self, msg: Message, token_count: u32) {
95 self.push_entry(None, msg, token_count, false);
96 }
97
98 pub fn push_entry(
102 &mut self,
103 key: Option<compact_str::CompactString>,
104 mut msg: Message,
105 tokens: u32,
106 pinned: bool,
107 ) {
108 msg.token_count = Some(tokens);
109 if let Some(ref k) = key {
110 if let Some(entry) = self.entries.iter_mut().find(|e| e.key.as_ref() == Some(k)) {
111 entry.pending = Some(Box::new((msg, tokens)));
112 entry.evict_at_boundary = false;
113 entry.pinned = pinned;
114 return;
115 }
116 }
117 self.token_count += tokens;
118 self.entries.push(KnowledgeEntry {
119 key,
120 message: msg,
121 tokens,
122 pinned,
123 evict_at_boundary: false,
124 pending: None,
125 use_count: 0,
126 last_used_step: None,
127 });
128 }
129
130 pub fn observe_references(&mut self, message: &Message, step: u64) {
134 let text = searchable_message_text(message);
135 let input_terms = lexical_terms(&text);
136 if text.is_empty() || input_terms.is_empty() {
137 return;
138 }
139 let folded = text.to_lowercase();
140 for entry in &mut self.entries {
141 if entry.evict_at_boundary {
142 continue;
143 }
144 let exact_key = entry
145 .key
146 .as_deref()
147 .filter(|key| key.len() >= 3)
148 .is_some_and(|key| folded.contains(&key.to_lowercase()));
149 let entry_text = searchable_message_text(&entry.message);
150 let entry_terms = lexical_terms(&entry_text);
151 let overlap = entry_terms.intersection(&input_terms).count();
152 let lexical_hit =
153 !entry_terms.is_empty() && overlap >= if entry_terms.len() == 1 { 1 } else { 2 };
154 if exact_key || lexical_hit {
155 entry.use_count = entry.use_count.saturating_add(1);
156 entry.last_used_step = Some(step);
157 }
158 }
159 }
160
161 pub fn retention_score(&self, index: usize, current_step: u64) -> Option<i64> {
162 let entry = self.entries.get(index)?;
163 Some(deterministic_retention_score(RetentionFeatures {
164 pinned: entry.pinned,
165 use_count: entry.use_count,
166 last_used_step: entry.last_used_step,
167 current_step,
168 lease_remaining_steps: None,
169 kind: retention_kind(entry.key.as_deref()),
170 tokens: entry.tokens,
171 confidence_ppm: 0,
172 stale_discount_ppm: 0,
173 }))
174 }
175
176 pub fn remove(&mut self, key: &str) -> bool {
179 match self
180 .entries
181 .iter_mut()
182 .find(|e| e.key.as_deref() == Some(key))
183 {
184 Some(entry) => {
185 entry.evict_at_boundary = true;
186 entry.pending = None;
187 true
188 }
189 None => false,
190 }
191 }
192
193 pub fn sweep_at_boundary(&mut self) -> KnowledgeSweep {
196 let mut sweep = KnowledgeSweep::default();
197 for entry in &mut self.entries {
198 if let Some(replacement) = entry.pending.take() {
199 let (msg, tokens) = *replacement;
200 self.token_count = self.token_count - entry.tokens + tokens;
201 entry.message = msg;
202 entry.tokens = tokens;
203 sweep.changed = true;
204 }
205 }
206 let before = self.entries.len();
207 self.entries.retain(|e| {
208 if e.evict_at_boundary {
209 if let Some(ref k) = e.key {
210 sweep.removed_keys.push(k.to_string());
211 }
212 sweep.tokens_freed += e.tokens;
213 false
214 } else {
215 true
216 }
217 });
218 if self.entries.len() != before {
219 self.token_count = self.token_count.saturating_sub(sweep.tokens_freed);
220 sweep.changed = true;
221 }
222 sweep
223 }
224
225 pub fn messages(&self) -> impl Iterator<Item = &Message> {
227 self.entries.iter().map(|e| &e.message)
228 }
229
230 pub fn len(&self) -> usize {
231 self.entries.len()
232 }
233 pub fn is_empty(&self) -> bool {
234 self.entries.is_empty()
235 }
236}
237
238fn retention_kind(key: Option<&str>) -> RetentionKind {
239 match key
240 .unwrap_or_default()
241 .split(':')
242 .next()
243 .unwrap_or_default()
244 {
245 "user" => RetentionKind::User,
246 "feedback" => RetentionKind::Feedback,
247 "project" | "memory" => RetentionKind::Project,
248 "reference" | "ref" => RetentionKind::Reference,
249 "skill" => RetentionKind::Skill,
250 "artifact" => RetentionKind::Artifact,
251 _ => RetentionKind::Other,
252 }
253}
254
255fn searchable_message_text(message: &Message) -> String {
256 let mut values = Vec::new();
257 match &message.content {
258 crate::types::message::Content::Text(text) => values.push(text.clone()),
259 crate::types::message::Content::Parts(parts) => {
260 for part in parts {
261 match part {
262 crate::types::message::ContentPart::Text { text } => values.push(text.clone()),
263 crate::types::message::ContentPart::ToolResult { output, .. } => {
264 values.push(output.clone())
265 }
266 crate::types::message::ContentPart::Image { url: Some(url), .. } => {
267 values.push(url.clone())
268 }
269 _ => {}
270 }
271 }
272 }
273 }
274 for call in &message.tool_calls {
275 values.push(call.name.to_string());
276 values.push(call.arguments.to_string());
277 }
278 values.join(" ")
279}
280
281fn lexical_terms(text: &str) -> std::collections::BTreeSet<String> {
282 let mut terms = std::collections::BTreeSet::new();
283 let mut segment = String::new();
284 let flush = |segment: &mut String, terms: &mut std::collections::BTreeSet<String>| {
285 if segment.is_empty() {
286 return;
287 }
288 let folded = segment.to_lowercase();
289 terms.insert(folded.clone());
290 let chars = folded.chars().collect::<Vec<_>>();
291 if chars.iter().any(|character| is_han(*character)) {
292 for pair in chars.windows(2) {
293 terms.insert(pair.iter().collect());
294 }
295 }
296 segment.clear();
297 };
298 for character in text.chars() {
299 if character.is_alphanumeric() {
300 segment.push(character);
301 } else {
302 flush(&mut segment, &mut terms);
303 }
304 }
305 flush(&mut segment, &mut terms);
306 terms
307}
308
309fn is_han(character: char) -> bool {
310 matches!(character as u32,
311 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0x20000..=0x3134F)
312}
313
314pub struct ContextPartitions {
334 pub system: Partition,
335 pub knowledge: KnowledgePartition,
336 pub task_state: TaskState,
337 pub signals: Vec<String>,
341 pub history: Partition,
342}
343
344impl ContextPartitions {
345 pub fn new(_config: &ContextConfig) -> Self {
346 Self {
347 system: Partition::new(),
348 knowledge: KnowledgePartition::new(),
349 task_state: TaskState::default(),
350 signals: Vec::new(),
351 history: Partition::new(),
352 }
353 }
354
355 pub fn total_tokens(&self, engine: &ContextTokenEngine) -> u32 {
358 let task_state = self.task_state.format_compact();
362 let task_state_tokens = if task_state.is_empty() {
363 0
364 } else {
365 engine.count(&task_state)
366 };
367 self.system.token_count
368 + self.knowledge.token_count
369 + task_state_tokens
370 + self.history.token_count
371 }
372}
373
374impl Default for ContextPartitions {
375 fn default() -> Self {
376 Self::new(&ContextConfig::default())
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::context::config::ContextConfig;
384 use crate::context::token_engine::ContextTokenEngine;
385 use crate::types::message::Message;
386
387 fn engine() -> ContextTokenEngine {
388 ContextTokenEngine::char_approx()
389 }
390
391 #[test]
392 fn push_updates_token_count() {
393 let mut ctx = ContextPartitions::new(&ContextConfig::default());
394 let base = ctx.total_tokens(&engine());
395 ctx.system.push(Message::system("rules"), 10);
396 ctx.history.push(Message::user("hello"), 5);
397 assert_eq!(ctx.total_tokens(&engine()), base + 15);
398 }
399
400 #[test]
401 fn task_state_tokens_included_in_total() {
402 use crate::context::task_state::TaskState;
403 let mut ctx = ContextPartitions::new(&ContextConfig::default());
404 let before = ctx.total_tokens(&engine());
405 ctx.task_state = TaskState {
406 goal: "do something important".to_string(),
407 ..Default::default()
408 };
409 let after = ctx.total_tokens(&engine());
410 assert!(
411 after > before,
412 "task_state should contribute to total_tokens"
413 );
414 }
415
416 #[test]
417 fn knowledge_tokens_included_in_total() {
418 let mut ctx = ContextPartitions::new(&ContextConfig::default());
419 let before = ctx.total_tokens(&engine());
420 ctx.knowledge.push(Message::system("skill: debug"), 20);
421 assert_eq!(ctx.total_tokens(&engine()), before + 20);
422 }
423
424 fn text_of(p: &KnowledgePartition) -> Vec<String> {
427 p.messages()
428 .filter_map(|m| m.content.as_text().map(str::to_string))
429 .collect()
430 }
431
432 #[test]
433 fn keyed_upsert_defers_to_boundary() {
434 let mut p = KnowledgePartition::new();
435 p.push_entry(Some("ref".into()), Message::system("v1"), 10, false);
436 p.push_entry(Some("ref".into()), Message::system("v2"), 12, false);
437 assert_eq!(p.len(), 1);
439 assert_eq!(text_of(&p), vec!["v1"]);
440 assert_eq!(p.token_count, 10);
441
442 let sweep = p.sweep_at_boundary();
443 assert!(sweep.changed);
444 assert!(
445 sweep.removed_keys.is_empty(),
446 "upsert-only sweep removes nothing"
447 );
448 assert_eq!(text_of(&p), vec!["v2"]);
449 assert_eq!(p.token_count, 12);
450 }
451
452 #[test]
453 fn remove_marks_then_sweep_drops() {
454 let mut p = KnowledgePartition::new();
455 p.push_entry(Some("ref".into()), Message::system("pinned ref"), 8, false);
456 assert!(p.remove("ref"));
457 assert_eq!(p.len(), 1);
459 assert_eq!(text_of(&p), vec!["pinned ref"]);
460
461 let sweep = p.sweep_at_boundary();
462 assert!(sweep.changed);
463 assert_eq!(sweep.removed_keys, vec!["ref".to_string()]);
464 assert_eq!(sweep.tokens_freed, 8);
465 assert!(p.is_empty());
466 assert_eq!(p.token_count, 0);
467 }
468
469 #[test]
470 fn remove_unknown_key_errs_open() {
471 let mut p = KnowledgePartition::new();
472 p.push(Message::system("unkeyed"), 5);
473 assert!(!p.remove("missing"));
474 assert!(!p.sweep_at_boundary().changed);
475 assert_eq!(p.len(), 1);
476 }
477
478 #[test]
479 fn same_key_push_after_remove_revives_entry() {
480 let mut p = KnowledgePartition::new();
481 p.push_entry(Some("ref".into()), Message::system("v1"), 5, false);
482 p.remove("ref");
483 p.push_entry(Some("ref".into()), Message::system("v2"), 6, false);
486 let sweep = p.sweep_at_boundary();
487 assert!(sweep.removed_keys.is_empty());
488 assert_eq!(text_of(&p), vec!["v2"]);
489 }
490
491 #[test]
492 fn fresh_keys_and_unkeyed_append_immediately() {
493 let mut p = KnowledgePartition::new();
494 p.push(Message::system("legacy"), 3);
495 p.push_entry(Some("a".into()), Message::system("fresh"), 4, true);
496 assert_eq!(text_of(&p), vec!["legacy", "fresh"]);
498 assert_eq!(p.token_count, 7);
499 assert!(p.entries[1].pinned);
500 }
501
502 #[test]
503 fn committed_message_updates_reference_usage_without_a_clock() {
504 let mut p = KnowledgePartition::new();
505 p.push_entry(
506 Some("project:orchid".into()),
507 Message::system("Atlas storage engine for ORCHID"),
508 8,
509 false,
510 );
511 p.push_entry(
512 Some("reference:unrelated".into()),
513 Message::system("Mercury deployment guide"),
514 8,
515 false,
516 );
517
518 p.observe_references(&Message::assistant("Use project:orchid and Atlas"), 7);
519
520 assert_eq!(p.entries[0].use_count, 1);
521 assert_eq!(p.entries[0].last_used_step, Some(7));
522 assert_eq!(p.entries[1].use_count, 0);
523 assert!(p.retention_score(0, 9) > p.retention_score(1, 9));
524 }
525}