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