1use super::config::ContextConfig;
2use super::measurement::TokenMeasurement;
3use super::task_state::TaskState;
4use super::token_engine::ContextTokenEngine;
5use crate::mm::value::{RetentionFeatures, RetentionKind, deterministic_retention_score};
6use crate::types::message::CoreMessage;
7
8#[derive(Debug, Clone)]
10pub struct Partition {
11 pub messages: Vec<CoreMessage>,
12 pub measurements: Vec<TokenMeasurement>,
14 pub token_count: u32,
15}
16
17impl Partition {
18 pub fn new() -> Self {
19 Self {
20 messages: Vec::new(),
21 measurements: Vec::new(),
22 token_count: 0,
23 }
24 }
25
26 pub fn push(&mut self, msg: CoreMessage, token_count: u32) {
27 let measurement = TokenMeasurement::for_message(&msg, token_count);
28 self.token_count += token_count;
29 self.messages.push(msg);
30 self.measurements.push(measurement);
31 }
32
33 pub fn clear(&mut self) {
34 self.messages.clear();
35 self.measurements.clear();
36 self.token_count = 0;
37 }
38
39 pub fn measured_tokens(&self, index: usize, engine: &ContextTokenEngine) -> u32 {
40 self.measurements
41 .get(index)
42 .filter(|measurement| {
43 self.messages
44 .get(index)
45 .is_some_and(|message| measurement.matches_message(message))
46 })
47 .map(|m| m.tokens)
48 .unwrap_or_else(|| {
49 self.messages
50 .get(index)
51 .map(|m| engine.count_message(m))
52 .unwrap_or(0)
53 })
54 }
55
56 pub fn set_measured_tokens(&mut self, index: usize, tokens: u32) {
57 if let Some(message) = self.messages.get(index) {
58 let measurement = TokenMeasurement::for_message(message, tokens);
59 if let Some(slot) = self.measurements.get_mut(index) {
60 *slot = measurement;
61 } else {
62 self.measurements.push(measurement);
63 }
64 }
65 }
66
67 pub fn len(&self) -> usize {
68 self.messages.len()
69 }
70 pub fn is_empty(&self) -> bool {
71 self.messages.is_empty()
72 }
73}
74
75impl Default for Partition {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81#[derive(Debug, Clone)]
88pub struct KnowledgeEntry {
89 pub key: Option<compact_str::CompactString>,
91 pub message: CoreMessage,
92 pub tokens: u32,
93 pub pinned: bool,
95 pub evict_at_boundary: bool,
99 pub pending: Option<Box<(CoreMessage, u32)>>,
102 pub use_count: u64,
104 pub last_used_step: Option<u64>,
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct KnowledgeSweep {
110 pub removed_keys: Vec<String>,
111 pub tokens_freed: u32,
112 pub changed: bool,
114}
115
116#[derive(Debug, Clone, Default)]
120pub struct KnowledgePartition {
121 pub entries: Vec<KnowledgeEntry>,
122 pub token_count: u32,
123}
124
125impl KnowledgePartition {
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 pub fn push(&mut self, msg: CoreMessage, token_count: u32) {
132 self.push_entry(None, msg, token_count, false);
133 }
134
135 pub fn push_entry(
139 &mut self,
140 key: Option<compact_str::CompactString>,
141 msg: CoreMessage,
142 tokens: u32,
143 pinned: bool,
144 ) {
145 if let Some(ref k) = key {
146 if let Some(entry) = self.entries.iter_mut().find(|e| e.key.as_ref() == Some(k)) {
147 entry.pending = Some(Box::new((msg, tokens)));
148 entry.evict_at_boundary = false;
149 entry.pinned = pinned;
150 return;
151 }
152 }
153 self.token_count += tokens;
154 self.entries.push(KnowledgeEntry {
155 key,
156 message: msg,
157 tokens,
158 pinned,
159 evict_at_boundary: false,
160 pending: None,
161 use_count: 0,
162 last_used_step: None,
163 });
164 }
165
166 pub fn observe_references(&mut self, message: &CoreMessage, step: u64) {
170 let text = searchable_message_text(message);
171 let input_terms = lexical_terms(&text);
172 if text.is_empty() || input_terms.is_empty() {
173 return;
174 }
175 let folded = text.to_lowercase();
176 for entry in &mut self.entries {
177 if entry.evict_at_boundary {
178 continue;
179 }
180 let exact_key = entry
181 .key
182 .as_deref()
183 .filter(|key| key.len() >= 3)
184 .is_some_and(|key| folded.contains(&key.to_lowercase()));
185 let entry_text = searchable_message_text(&entry.message);
186 let entry_terms = lexical_terms(&entry_text);
187 let overlap = entry_terms.intersection(&input_terms).count();
188 let lexical_hit =
189 !entry_terms.is_empty() && overlap >= if entry_terms.len() == 1 { 1 } else { 2 };
190 if exact_key || lexical_hit {
191 entry.use_count = entry.use_count.saturating_add(1);
192 entry.last_used_step = Some(step);
193 }
194 }
195 }
196
197 pub fn retention_score(&self, index: usize, current_step: u64) -> Option<i64> {
198 let entry = self.entries.get(index)?;
199 Some(deterministic_retention_score(RetentionFeatures {
200 pinned: entry.pinned,
201 use_count: entry.use_count,
202 last_used_step: entry.last_used_step,
203 current_step,
204 lease_remaining_steps: None,
205 kind: retention_kind(entry.key.as_deref()),
206 tokens: entry.tokens,
207 confidence_ppm: 0,
208 stale_discount_ppm: 0,
209 }))
210 }
211
212 pub fn remove(&mut self, key: &str) -> bool {
215 match self
216 .entries
217 .iter_mut()
218 .find(|e| e.key.as_deref() == Some(key))
219 {
220 Some(entry) => {
221 entry.evict_at_boundary = true;
222 entry.pending = None;
223 true
224 }
225 None => false,
226 }
227 }
228
229 pub fn sweep_at_boundary(&mut self) -> KnowledgeSweep {
232 let mut sweep = KnowledgeSweep::default();
233 for entry in &mut self.entries {
234 if let Some(replacement) = entry.pending.take() {
235 let (msg, tokens) = *replacement;
236 self.token_count = self.token_count - entry.tokens + tokens;
237 entry.message = msg;
238 entry.tokens = tokens;
239 sweep.changed = true;
240 }
241 }
242 let before = self.entries.len();
243 self.entries.retain(|e| {
244 if e.evict_at_boundary {
245 if let Some(ref k) = e.key {
246 sweep.removed_keys.push(k.to_string());
247 }
248 sweep.tokens_freed += e.tokens;
249 false
250 } else {
251 true
252 }
253 });
254 if self.entries.len() != before {
255 self.token_count = self.token_count.saturating_sub(sweep.tokens_freed);
256 sweep.changed = true;
257 }
258 sweep
259 }
260
261 pub fn messages(&self) -> impl Iterator<Item = &CoreMessage> {
263 self.entries.iter().map(|e| &e.message)
264 }
265
266 pub fn len(&self) -> usize {
267 self.entries.len()
268 }
269 pub fn is_empty(&self) -> bool {
270 self.entries.is_empty()
271 }
272}
273
274fn retention_kind(key: Option<&str>) -> RetentionKind {
275 match key
276 .unwrap_or_default()
277 .split(':')
278 .next()
279 .unwrap_or_default()
280 {
281 "user" => RetentionKind::User,
282 "feedback" => RetentionKind::Feedback,
283 "project" | "memory" => RetentionKind::Project,
284 "reference" | "ref" => RetentionKind::Reference,
285 "skill" => RetentionKind::Skill,
286 "artifact" => RetentionKind::Artifact,
287 _ => RetentionKind::Other,
288 }
289}
290
291fn searchable_message_text(message: &CoreMessage) -> String {
292 let mut values = Vec::new();
293 match &message.content {
294 crate::types::message::Content::Text(text) => values.push(text.clone()),
295 crate::types::message::Content::Parts(parts) => {
296 for part in parts {
297 match part {
298 crate::types::message::ContentPart::Text { text } => values.push(text.clone()),
299 crate::types::message::ContentPart::ToolResult { output, .. } => {
300 values.push(output.clone())
301 }
302 crate::types::message::ContentPart::Image {
303 source: crate::types::durable_content::DurableSource::Url { url },
304 ..
305 } => values.push(url.clone()),
306 _ => {}
307 }
308 }
309 }
310 }
311 for call in &message.tool_calls {
312 values.push(call.name.to_string());
313 values.push(call.arguments.to_string());
314 }
315 values.join(" ")
316}
317
318fn lexical_terms(text: &str) -> std::collections::BTreeSet<String> {
319 let mut terms = std::collections::BTreeSet::new();
320 let mut segment = String::new();
321 let flush = |segment: &mut String, terms: &mut std::collections::BTreeSet<String>| {
322 if segment.is_empty() {
323 return;
324 }
325 let folded = segment.to_lowercase();
326 terms.insert(folded.clone());
327 let chars = folded.chars().collect::<Vec<_>>();
328 if chars.iter().any(|character| is_han(*character)) {
329 for pair in chars.windows(2) {
330 terms.insert(pair.iter().collect());
331 }
332 }
333 segment.clear();
334 };
335 for character in text.chars() {
336 if character.is_alphanumeric() {
337 segment.push(character);
338 } else {
339 flush(&mut segment, &mut terms);
340 }
341 }
342 flush(&mut segment, &mut terms);
343 terms
344}
345
346fn is_han(character: char) -> bool {
347 matches!(character as u32,
348 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0x20000..=0x3134F)
349}
350
351pub struct ContextPartitions {
371 pub system: Partition,
372 pub knowledge: KnowledgePartition,
373 pub task_state: TaskState,
374 pub signals: Vec<String>,
378 pub history: Partition,
379}
380
381impl ContextPartitions {
382 pub fn new(_config: &ContextConfig) -> Self {
383 Self {
384 system: Partition::new(),
385 knowledge: KnowledgePartition::new(),
386 task_state: TaskState::default(),
387 signals: Vec::new(),
388 history: Partition::new(),
389 }
390 }
391
392 pub fn total_tokens(&self, engine: &ContextTokenEngine) -> u32 {
395 let task_state = self.task_state.format_compact();
399 let task_state_tokens = if task_state.is_empty() {
400 0
401 } else {
402 engine.count(&task_state)
403 };
404 self.system.token_count
405 + self.knowledge.token_count
406 + task_state_tokens
407 + self.history.token_count
408 }
409}
410
411impl Default for ContextPartitions {
412 fn default() -> Self {
413 Self::new(&ContextConfig::default())
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use crate::context::config::ContextConfig;
421 use crate::context::token_engine::ContextTokenEngine;
422 use crate::types::message::CoreMessage;
423
424 fn engine() -> ContextTokenEngine {
425 ContextTokenEngine::char_approx()
426 }
427
428 #[test]
429 fn stale_measurement_does_not_count_a_replaced_message() {
430 let mut partition = Partition::new();
431 partition.push(CoreMessage::user("old"), 1);
432 partition.messages[0] = CoreMessage::user("new evidence ".repeat(100));
433 let expected = engine().count_message(&partition.messages[0]);
434 assert!(expected > 1);
435 assert_eq!(partition.measured_tokens(0, &engine()), expected);
436 }
437
438 #[test]
439 fn push_updates_token_count() {
440 let mut ctx = ContextPartitions::new(&ContextConfig::default());
441 let base = ctx.total_tokens(&engine());
442 ctx.system.push(CoreMessage::system("rules"), 10);
443 ctx.history.push(CoreMessage::user("hello"), 5);
444 assert_eq!(ctx.total_tokens(&engine()), base + 15);
445 }
446
447 #[test]
448 fn task_state_tokens_included_in_total() {
449 use crate::context::task_state::TaskState;
450 let mut ctx = ContextPartitions::new(&ContextConfig::default());
451 let before = ctx.total_tokens(&engine());
452 ctx.task_state = TaskState {
453 goal: "do something important".to_string(),
454 ..Default::default()
455 };
456 let after = ctx.total_tokens(&engine());
457 assert!(
458 after > before,
459 "task_state should contribute to total_tokens"
460 );
461 }
462
463 #[test]
464 fn knowledge_tokens_included_in_total() {
465 let mut ctx = ContextPartitions::new(&ContextConfig::default());
466 let before = ctx.total_tokens(&engine());
467 ctx.knowledge.push(CoreMessage::system("skill: debug"), 20);
468 assert_eq!(ctx.total_tokens(&engine()), before + 20);
469 }
470
471 fn text_of(p: &KnowledgePartition) -> Vec<String> {
474 p.messages()
475 .filter_map(|m| m.content.as_text().map(str::to_string))
476 .collect()
477 }
478
479 #[test]
480 fn keyed_upsert_defers_to_boundary() {
481 let mut p = KnowledgePartition::new();
482 p.push_entry(
483 Some("ref".into()),
484 CoreMessage::system("original"),
485 10,
486 false,
487 );
488 p.push_entry(
489 Some("ref".into()),
490 CoreMessage::system("updated"),
491 12,
492 false,
493 );
494 assert_eq!(p.len(), 1);
496 assert_eq!(text_of(&p), vec!["original"]);
497 assert_eq!(p.token_count, 10);
498
499 let sweep = p.sweep_at_boundary();
500 assert!(sweep.changed);
501 assert!(
502 sweep.removed_keys.is_empty(),
503 "upsert-only sweep removes nothing"
504 );
505 assert_eq!(text_of(&p), vec!["updated"]);
506 assert_eq!(p.token_count, 12);
507 }
508
509 #[test]
510 fn remove_marks_then_sweep_drops() {
511 let mut p = KnowledgePartition::new();
512 p.push_entry(
513 Some("ref".into()),
514 CoreMessage::system("pinned ref"),
515 8,
516 false,
517 );
518 assert!(p.remove("ref"));
519 assert_eq!(p.len(), 1);
521 assert_eq!(text_of(&p), vec!["pinned ref"]);
522
523 let sweep = p.sweep_at_boundary();
524 assert!(sweep.changed);
525 assert_eq!(sweep.removed_keys, vec!["ref".to_string()]);
526 assert_eq!(sweep.tokens_freed, 8);
527 assert!(p.is_empty());
528 assert_eq!(p.token_count, 0);
529 }
530
531 #[test]
532 fn remove_unknown_key_errs_open() {
533 let mut p = KnowledgePartition::new();
534 p.push(CoreMessage::system("unkeyed"), 5);
535 assert!(!p.remove("missing"));
536 assert!(!p.sweep_at_boundary().changed);
537 assert_eq!(p.len(), 1);
538 }
539
540 #[test]
541 fn same_key_push_after_remove_revives_entry() {
542 let mut p = KnowledgePartition::new();
543 p.push_entry(Some("ref".into()), CoreMessage::system("older"), 5, false);
544 p.remove("ref");
545 p.push_entry(Some("ref".into()), CoreMessage::system("newer"), 6, false);
548 let sweep = p.sweep_at_boundary();
549 assert!(sweep.removed_keys.is_empty());
550 assert_eq!(text_of(&p), vec!["newer"]);
551 }
552
553 #[test]
554 fn fresh_keys_and_unkeyed_append_immediately() {
555 let mut p = KnowledgePartition::new();
556 p.push(CoreMessage::system("existing"), 3);
557 p.push_entry(Some("a".into()), CoreMessage::system("fresh"), 4, true);
558 assert_eq!(text_of(&p), vec!["existing", "fresh"]);
560 assert_eq!(p.token_count, 7);
561 assert!(p.entries[1].pinned);
562 }
563
564 #[test]
565 fn committed_message_updates_reference_usage_without_a_clock() {
566 let mut p = KnowledgePartition::new();
567 p.push_entry(
568 Some("project:orchid".into()),
569 CoreMessage::system("Atlas storage engine for ORCHID"),
570 8,
571 false,
572 );
573 p.push_entry(
574 Some("reference:unrelated".into()),
575 CoreMessage::system("Mercury deployment guide"),
576 8,
577 false,
578 );
579
580 p.observe_references(&CoreMessage::assistant("Use project:orchid and Atlas"), 7);
581
582 assert_eq!(p.entries[0].use_count, 1);
583 assert_eq!(p.entries[0].last_used_step, Some(7));
584 assert_eq!(p.entries[1].use_count, 0);
585 assert!(p.retention_score(0, 9) > p.retention_score(1, 9));
586 }
587}