pub mod edge_cases;
pub mod hook;
#[cfg(test)]
mod test_transcripts;
pub use hook::{FileContextExt, FileContextHook, FILE_CONTEXT_KEY};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FileOperation {
Read,
Write,
Edit,
Other,
}
impl FileOperation {
pub fn is_full_snapshot(&self) -> bool {
matches!(self, FileOperation::Write)
}
pub fn is_read(&self) -> bool {
matches!(self, FileOperation::Read)
}
pub fn is_mutating(&self) -> bool {
matches!(self, FileOperation::Write | FileOperation::Edit)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LineRange {
pub offset: usize,
pub limit: Option<usize>,
}
impl LineRange {
pub fn new(offset: usize, limit: Option<usize>) -> Self {
Self { offset, limit }
}
pub fn is_full_read(&self) -> bool {
self.offset == 0 && self.limit.is_none()
}
pub fn contains(&self, other: &LineRange) -> bool {
if self.offset > other.offset {
return false;
}
match (self.limit, other.limit) {
(None, _) => true, (Some(_), None) => false, (Some(self_limit), Some(other_limit)) => {
let self_end = self.offset + self_limit;
let other_end = other.offset + other_limit;
self_end >= other_end
}
}
}
pub fn overlaps(&self, other: &LineRange) -> bool {
let self_start = self.offset;
let other_start = other.offset;
match (self.limit, other.limit) {
(None, None) => true, (None, Some(_)) => {
other_start >= self_start || other_start + other.limit.unwrap_or(0) > self_start
}
(Some(_), None) => {
self_start >= other_start || self_start + self.limit.unwrap_or(0) > other_start
}
(Some(self_limit), Some(other_limit)) => {
let self_end = self_start + self_limit;
let other_end = other_start + other_limit;
self_start < other_end && other_start < self_end
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContentHash(pub String);
impl ContentHash {
pub fn from_content(content: &str) -> Self {
let hash = blake3::hash(content.as_bytes());
Self(hash.to_hex().to_string())
}
pub fn from_hex(hex: impl Into<String>) -> Self {
Self(hex.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClassificationKey {
pub message_id: String,
pub tool_call_id: String,
}
impl ClassificationKey {
pub fn new(message_id: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
Self {
message_id: message_id.into(),
tool_call_id: tool_call_id.into(),
}
}
}
impl std::fmt::Display for ClassificationKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.message_id, self.tool_call_id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageClassification {
pub message_id: String,
pub tool_call_id: String,
pub sequence: u64,
pub role: MessageRole,
pub operation: FileOperation,
pub file_path: Option<PathBuf>,
pub line_range: Option<LineRange>,
pub content_hash: Option<ContentHash>,
pub content_size: usize,
}
impl MessageClassification {
pub fn key(&self) -> ClassificationKey {
ClassificationKey::new(&self.message_id, &self.tool_call_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageRole {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileArtifact {
pub path: PathBuf,
pub state: FileState,
pub operations: Vec<FileOperationRecord>,
pub latest_snapshot_key: Option<ClassificationKey>,
pub latest_hash: Option<ContentHash>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileState {
Read,
Written,
Edited,
Deleted,
Renamed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileOperationRecord {
pub key: ClassificationKey,
pub sequence: u64,
pub operation: FileOperation,
pub line_range: Option<LineRange>,
pub content_hash: Option<ContentHash>,
pub is_result: bool,
}
impl FileArtifact {
pub fn new(path: PathBuf) -> Self {
Self {
path,
state: FileState::Read,
operations: Vec::new(),
latest_snapshot_key: None,
latest_hash: None,
}
}
pub fn record_operation(&mut self, record: FileOperationRecord) {
match record.operation {
FileOperation::Read => {
if self.state == FileState::Deleted {
self.state = FileState::Read;
}
if record.is_result && record.line_range.map(|r| r.is_full_read()).unwrap_or(true) {
self.latest_snapshot_key = Some(record.key.clone());
}
}
FileOperation::Write => {
self.state = FileState::Written;
if record.is_result {
self.latest_snapshot_key = Some(record.key.clone());
}
}
FileOperation::Edit => {
if self.state != FileState::Written {
self.state = FileState::Edited;
}
}
FileOperation::Other => {}
}
if record.content_hash.is_some() {
self.latest_hash = record.content_hash.clone();
}
self.operations.push(record);
}
pub fn latest_full_read(&self) -> Option<&FileOperationRecord> {
self.operations.iter().rev().find(|op| {
op.operation == FileOperation::Read
&& op.is_result
&& op.line_range.map(|r| r.is_full_read()).unwrap_or(true)
})
}
pub fn has_newer_operation(&self, sequence: u64) -> bool {
self.operations.iter().any(|op| op.sequence > sequence)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SupersedenceAction {
Keep,
Drop,
MoveToEnd,
ReplaceWithSnapshot,
}
#[derive(Debug, Clone)]
pub struct SupersedenceRules {
pub drop_superseded_reads: bool,
pub drop_reads_after_write: bool,
pub preserve_edit_history: bool,
pub max_edits_per_file: usize,
pub enforce_read_after_write: bool,
}
impl Default for SupersedenceRules {
fn default() -> Self {
Self {
drop_superseded_reads: true,
drop_reads_after_write: true,
preserve_edit_history: false,
max_edits_per_file: 3,
enforce_read_after_write: true,
}
}
}
impl SupersedenceRules {
pub fn evaluate_read(
&self,
artifact: &FileArtifact,
classification: &MessageClassification,
) -> SupersedenceAction {
let current_range = classification.line_range;
let current_seq = classification.sequence;
let has_newer_write = artifact.operations.iter().any(|op| {
op.sequence > current_seq && op.operation == FileOperation::Write && op.is_result
});
if has_newer_write && self.drop_reads_after_write {
return SupersedenceAction::Drop;
}
let has_newer_full_read = artifact.operations.iter().any(|op| {
op.sequence > current_seq
&& op.operation == FileOperation::Read
&& op.is_result
&& op.line_range.map(|r| r.is_full_read()).unwrap_or(true)
});
if has_newer_full_read && self.drop_superseded_reads {
if current_range.map(|r| !r.is_full_read()).unwrap_or(false) {
return SupersedenceAction::Drop;
}
return SupersedenceAction::Drop;
}
if let Some(current_range) = current_range {
for op in &artifact.operations {
if op.sequence > current_seq && op.operation == FileOperation::Read && op.is_result
{
if let Some(other_range) = op.line_range {
if other_range.contains(¤t_range) {
return SupersedenceAction::Drop;
}
if other_range.overlaps(¤t_range) {
let other_coverage = other_range.limit.unwrap_or(usize::MAX);
let self_coverage = current_range.limit.unwrap_or(usize::MAX);
if other_range.offset <= current_range.offset
&& other_coverage >= self_coverage
{
return SupersedenceAction::Drop;
}
}
}
}
}
}
if artifact.latest_snapshot_key.as_ref() == Some(&classification.key()) {
return SupersedenceAction::MoveToEnd;
}
SupersedenceAction::Keep
}
pub fn evaluate_write(
&self,
artifact: &FileArtifact,
classification: &MessageClassification,
) -> SupersedenceAction {
let is_latest_write = !artifact.operations.iter().any(|op| {
op.sequence > classification.sequence
&& op.operation == FileOperation::Write
&& op.is_result
});
if is_latest_write && classification.role == MessageRole::User {
SupersedenceAction::MoveToEnd
} else {
SupersedenceAction::Keep
}
}
pub fn evaluate_edit(
&self,
artifact: &FileArtifact,
classification: &MessageClassification,
) -> SupersedenceAction {
let edits_after = artifact
.operations
.iter()
.filter(|op| {
op.sequence > classification.sequence
&& op.operation == FileOperation::Edit
&& op.is_result
})
.count();
let has_newer_snapshot = artifact.operations.iter().any(|op| {
op.sequence > classification.sequence
&& (op.operation == FileOperation::Write
|| (op.operation == FileOperation::Read
&& op.line_range.map(|r| r.is_full_read()).unwrap_or(true)))
&& op.is_result
});
if has_newer_snapshot {
return SupersedenceAction::Drop;
}
if !self.preserve_edit_history {
if edits_after > 0 {
return SupersedenceAction::Drop;
}
} else if edits_after >= self.max_edits_per_file {
return SupersedenceAction::Drop;
}
SupersedenceAction::Keep
}
pub fn evaluate(
&self,
artifact: &FileArtifact,
classification: &MessageClassification,
) -> SupersedenceAction {
match classification.operation {
FileOperation::Read => self.evaluate_read(artifact, classification),
FileOperation::Write => self.evaluate_write(artifact, classification),
FileOperation::Edit => self.evaluate_edit(artifact, classification),
FileOperation::Other => SupersedenceAction::Keep,
}
}
}
static GLOBAL_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Default)]
pub struct ContextClassifier {
artifacts: HashMap<PathBuf, FileArtifact>,
classifications: HashMap<ClassificationKey, MessageClassification>,
message_index: HashMap<String, Vec<ClassificationKey>>,
sequence_counter: u64,
}
impl ContextClassifier {
pub fn new() -> Self {
Self {
sequence_counter: GLOBAL_SEQUENCE.fetch_add(1000, Ordering::SeqCst),
..Default::default()
}
}
fn next_sequence(&mut self) -> u64 {
self.sequence_counter += 1;
self.sequence_counter
}
fn normalize_path(&self, path: &str) -> PathBuf {
let path = PathBuf::from(path);
if path.is_absolute() {
path
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("/"))
.join(path)
}
}
pub fn classify_tool_call(
&mut self,
tool_name: &str,
args: &serde_json::Value,
message_id: &str,
tool_call_id: &str,
) -> MessageClassification {
let sequence = self.next_sequence();
let (operation, file_path, line_range) = self.extract_operation_metadata(tool_name, args);
let classification = MessageClassification {
message_id: message_id.to_string(),
tool_call_id: tool_call_id.to_string(),
sequence,
role: MessageRole::Assistant,
operation,
file_path: file_path.clone(),
line_range,
content_hash: None, content_size: 0,
};
let key = classification.key();
if let Some(path) = file_path {
let artifact = self
.artifacts
.entry(path.clone())
.or_insert_with(|| FileArtifact::new(path));
artifact.record_operation(FileOperationRecord {
key: key.clone(),
sequence,
operation,
line_range,
content_hash: None,
is_result: false,
});
}
self.message_index
.entry(message_id.to_string())
.or_default()
.push(key.clone());
self.classifications.insert(key, classification.clone());
classification
}
pub fn classify_tool_result(
&mut self,
tool_name: &str,
result: &str,
message_id: &str,
tool_call_id: &str,
) -> MessageClassification {
let sequence = self.next_sequence();
let call_classification = self
.classifications
.values()
.find(|c| c.tool_call_id == tool_call_id && c.role == MessageRole::Assistant);
let (operation, file_path, line_range) = if let Some(call) = call_classification {
(call.operation, call.file_path.clone(), call.line_range)
} else {
self.extract_result_metadata(tool_name, result)
};
let content_hash = if operation.is_read() || operation.is_mutating() {
Some(ContentHash::from_content(result))
} else {
None
};
let classification = MessageClassification {
message_id: message_id.to_string(),
tool_call_id: tool_call_id.to_string(),
sequence,
role: MessageRole::User,
operation,
file_path: file_path.clone(),
line_range,
content_hash: content_hash.clone(),
content_size: result.len(),
};
let key = classification.key();
if let Some(path) = file_path {
let artifact = self
.artifacts
.entry(path.clone())
.or_insert_with(|| FileArtifact::new(path));
artifact.record_operation(FileOperationRecord {
key: key.clone(),
sequence,
operation,
line_range,
content_hash,
is_result: true,
});
}
self.message_index
.entry(message_id.to_string())
.or_default()
.push(key.clone());
self.classifications.insert(key, classification.clone());
classification
}
fn extract_operation_metadata(
&self,
tool_name: &str,
args: &serde_json::Value,
) -> (FileOperation, Option<PathBuf>, Option<LineRange>) {
match tool_name {
"read" => {
let file_path = args
.get("file_path")
.and_then(|v| v.as_str())
.map(|s| self.normalize_path(s));
let offset = args
.get("offset")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.unwrap_or(0);
let limit = args
.get("limit")
.and_then(|v| v.as_u64())
.map(|n| n as usize);
let line_range = if offset == 0 && limit.is_none() {
None } else {
Some(LineRange::new(offset, limit))
};
(FileOperation::Read, file_path, line_range)
}
"write" => {
let file_path = args
.get("file_path")
.and_then(|v| v.as_str())
.map(|s| self.normalize_path(s));
(FileOperation::Write, file_path, None)
}
"edit" => {
let file_path = args
.get("file_path")
.and_then(|v| v.as_str())
.map(|s| self.normalize_path(s));
(FileOperation::Edit, file_path, None)
}
_ => (FileOperation::Other, None, None),
}
}
fn extract_result_metadata(
&self,
tool_name: &str,
result: &str,
) -> (FileOperation, Option<PathBuf>, Option<LineRange>) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(result) {
let file_path = json
.get("file_path")
.and_then(|v| v.as_str())
.map(|s| self.normalize_path(s));
let operation = match tool_name {
"read" => FileOperation::Read,
"write" => FileOperation::Write,
"edit" => FileOperation::Edit,
_ => FileOperation::Other,
};
return (operation, file_path, None);
}
(FileOperation::Other, None, None)
}
pub fn get_artifact(&self, path: &PathBuf) -> Option<&FileArtifact> {
self.artifacts.get(path)
}
pub fn artifacts(&self) -> &HashMap<PathBuf, FileArtifact> {
&self.artifacts
}
pub fn get_classification_by_key(
&self,
key: &ClassificationKey,
) -> Option<&MessageClassification> {
self.classifications.get(key)
}
pub fn get_classifications_for_message(&self, message_id: &str) -> Vec<&MessageClassification> {
self.message_index
.get(message_id)
.map(|keys| {
keys.iter()
.filter_map(|k| self.classifications.get(k))
.collect()
})
.unwrap_or_default()
}
pub fn is_message_classified(&self, message_id: &str) -> bool {
self.message_index.contains_key(message_id)
}
#[deprecated(note = "Use get_classifications_for_message for multi-tool support")]
pub fn get_classification(&self, message_id: &str) -> Option<&MessageClassification> {
self.get_classifications_for_message(message_id)
.into_iter()
.next()
}
pub fn classifications(&self) -> &HashMap<ClassificationKey, MessageClassification> {
&self.classifications
}
pub fn get_keys_for_message(&self, message_id: &str) -> Option<&Vec<ClassificationKey>> {
self.message_index.get(message_id)
}
pub fn clear(&mut self) {
self.artifacts.clear();
self.classifications.clear();
self.message_index.clear();
self.sequence_counter = GLOBAL_SEQUENCE.fetch_add(1000, Ordering::SeqCst);
}
}
#[derive(Debug)]
pub struct ContextManager {
classifier: ContextClassifier,
rules: SupersedenceRules,
}
impl ContextManager {
pub fn new() -> Self {
Self {
classifier: ContextClassifier::new(),
rules: SupersedenceRules::default(),
}
}
pub fn with_rules(rules: SupersedenceRules) -> Self {
Self {
classifier: ContextClassifier::new(),
rules,
}
}
pub fn classifier(&self) -> &ContextClassifier {
&self.classifier
}
pub fn classifier_mut(&mut self) -> &mut ContextClassifier {
&mut self.classifier
}
pub fn rules(&self) -> &SupersedenceRules {
&self.rules
}
pub fn compute_optimized_order(&self) -> ContextOptimizationResult<'_> {
let mut keep: Vec<ClassificationKey> = Vec::new();
let mut drop: Vec<ClassificationKey> = Vec::new();
let mut move_to_end: Vec<ClassificationKey> = Vec::new();
let mut sorted_classifications: Vec<_> = self.classifier.classifications.values().collect();
sorted_classifications.sort_by_key(|c| c.sequence);
for classification in sorted_classifications {
let action = if let Some(path) = &classification.file_path {
if let Some(artifact) = self.classifier.artifacts.get(path) {
self.rules.evaluate(artifact, classification)
} else {
SupersedenceAction::Keep
}
} else {
SupersedenceAction::Keep
};
let key = classification.key();
match action {
SupersedenceAction::Keep => {
keep.push(key);
}
SupersedenceAction::Drop => {
drop.push(key);
}
SupersedenceAction::MoveToEnd => {
move_to_end.push(key);
}
SupersedenceAction::ReplaceWithSnapshot => {
keep.push(key);
}
}
}
ContextOptimizationResult {
keep,
drop,
move_to_end,
classifier_ref: &self.classifier,
}
}
pub fn needs_read_after_write(&self, path: &PathBuf) -> bool {
if !self.rules.enforce_read_after_write {
return false;
}
if let Some(artifact) = self.classifier.artifacts.get(path) {
let was_mutated = artifact
.operations
.iter()
.any(|op| op.operation.is_mutating() && op.is_result);
if !was_mutated {
return false;
}
let last_mutation_seq = artifact
.operations
.iter()
.filter(|op| op.operation.is_mutating() && op.is_result)
.map(|op| op.sequence)
.max()
.unwrap_or(0);
let has_read_after = artifact.operations.iter().any(|op| {
op.sequence > last_mutation_seq
&& op.operation == FileOperation::Read
&& op.is_result
&& op.line_range.map(|r| r.is_full_read()).unwrap_or(true)
});
!has_read_after
} else {
false
}
}
pub fn files_needing_read(&self) -> Vec<PathBuf> {
self.classifier
.artifacts
.keys()
.filter(|path| self.needs_read_after_write(path))
.cloned()
.collect()
}
pub fn clear(&mut self) {
self.classifier.clear();
}
}
impl Default for ContextManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ContextOptimizationResult<'a> {
pub keep: Vec<ClassificationKey>,
pub drop: Vec<ClassificationKey>,
pub move_to_end: Vec<ClassificationKey>,
classifier_ref: &'a ContextClassifier,
}
impl<'a> ContextOptimizationResult<'a> {
pub fn final_order(&self) -> Vec<ClassificationKey> {
let mut result = self.keep.clone();
result.extend(self.move_to_end.iter().cloned());
result
}
pub fn should_include_key(&self, key: &ClassificationKey) -> bool {
self.keep.contains(key) || self.move_to_end.contains(key)
}
pub fn should_drop_key(&self, key: &ClassificationKey) -> bool {
self.drop.contains(key)
}
pub fn should_include(&self, message_id: &str) -> bool {
if let Some(keys) = self.classifier_ref.get_keys_for_message(message_id) {
keys.iter().any(|k| self.should_include_key(k))
} else {
false
}
}
pub fn should_drop(&self, message_id: &str) -> bool {
if let Some(keys) = self.classifier_ref.get_keys_for_message(message_id) {
!keys.is_empty() && keys.iter().all(|k| self.should_drop_key(k))
} else {
false
}
}
pub fn is_classified(&self, message_id: &str) -> bool {
self.classifier_ref.is_message_classified(message_id)
}
pub fn final_message_order(&self) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for key in self.final_order() {
if seen.insert(key.message_id.clone()) {
result.push(key.message_id);
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_range_is_full_read() {
assert!(LineRange::new(0, None).is_full_read());
assert!(!LineRange::new(10, None).is_full_read());
assert!(!LineRange::new(0, Some(100)).is_full_read());
}
#[test]
fn test_line_range_contains() {
let full = LineRange::new(0, None);
let partial1 = LineRange::new(10, Some(50));
let partial2 = LineRange::new(20, Some(30));
assert!(full.contains(&partial1));
assert!(full.contains(&partial2));
assert!(!partial1.contains(&full));
assert!(partial1.contains(&partial2));
}
#[test]
fn test_line_range_overlaps() {
let range1 = LineRange::new(0, Some(50));
let range2 = LineRange::new(40, Some(30));
let range3 = LineRange::new(60, Some(20));
assert!(range1.overlaps(&range2));
assert!(!range1.overlaps(&range3));
}
#[test]
fn test_content_hash() {
let hash1 = ContentHash::from_content("hello world");
let hash2 = ContentHash::from_content("hello world");
let hash3 = ContentHash::from_content("different content");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_classifier_read_operation() {
let mut classifier = ContextClassifier::new();
let args = serde_json::json!({
"file_path": "/test/file.rs"
});
let classification = classifier.classify_tool_call("read", &args, "msg-1", "call-1");
assert_eq!(classification.operation, FileOperation::Read);
assert!(classification.file_path.is_some());
assert!(classification.line_range.is_none()); }
#[test]
fn test_classifier_partial_read() {
let mut classifier = ContextClassifier::new();
let args = serde_json::json!({
"file_path": "/test/file.rs",
"offset": 10,
"limit": 50
});
let classification = classifier.classify_tool_call("read", &args, "msg-1", "call-1");
assert_eq!(classification.operation, FileOperation::Read);
assert!(classification.line_range.is_some());
let range = classification.line_range.unwrap();
assert_eq!(range.offset, 10);
assert_eq!(range.limit, Some(50));
}
#[test]
fn test_classifier_write_operation() {
let mut classifier = ContextClassifier::new();
let args = serde_json::json!({
"file_path": "/test/new_file.rs",
"content": "fn main() {}"
});
let classification = classifier.classify_tool_call("write", &args, "msg-1", "call-1");
assert_eq!(classification.operation, FileOperation::Write);
assert!(classification.file_path.is_some());
}
#[test]
fn test_classifier_edit_operation() {
let mut classifier = ContextClassifier::new();
let args = serde_json::json!({
"file_path": "/test/file.rs",
"old_string": "old",
"new_string": "new"
});
let classification = classifier.classify_tool_call("edit", &args, "msg-1", "call-1");
assert_eq!(classification.operation, FileOperation::Edit);
assert!(classification.file_path.is_some());
}
#[test]
fn test_classifier_multiple_tool_calls_same_message() {
let mut classifier = ContextClassifier::new();
classifier.classify_tool_call(
"read",
&serde_json::json!({"file_path": "/test/file1.rs"}),
"msg-1",
"call-1",
);
classifier.classify_tool_call(
"read",
&serde_json::json!({"file_path": "/test/file2.rs"}),
"msg-1",
"call-2",
);
let classifications = classifier.get_classifications_for_message("msg-1");
assert_eq!(classifications.len(), 2);
assert!(classifier.is_message_classified("msg-1"));
let keys = classifier.get_keys_for_message("msg-1").unwrap();
assert_eq!(keys.len(), 2);
}
#[test]
fn test_file_artifact_state_transitions() {
let mut artifact = FileArtifact::new(PathBuf::from("/test/file.rs"));
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-1", "call-1"),
sequence: 1,
operation: FileOperation::Read,
line_range: None,
content_hash: Some(ContentHash::from_content("v1")),
is_result: true,
});
assert_eq!(artifact.state, FileState::Read);
assert!(artifact.latest_snapshot_key.is_some());
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-2", "call-2"),
sequence: 2,
operation: FileOperation::Edit,
line_range: None,
content_hash: Some(ContentHash::from_content("v2")),
is_result: true,
});
assert_eq!(artifact.state, FileState::Edited);
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-3", "call-3"),
sequence: 3,
operation: FileOperation::Write,
line_range: None,
content_hash: Some(ContentHash::from_content("v3")),
is_result: true,
});
assert_eq!(artifact.state, FileState::Written);
assert_eq!(
artifact.latest_snapshot_key,
Some(ClassificationKey::new("msg-3", "call-3"))
);
}
#[test]
fn test_supersedence_read_after_write() {
let rules = SupersedenceRules::default();
let mut artifact = FileArtifact::new(PathBuf::from("/test/file.rs"));
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-1", "call-1"),
sequence: 1,
operation: FileOperation::Read,
line_range: None,
content_hash: Some(ContentHash::from_content("v1")),
is_result: true,
});
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-2", "call-2"),
sequence: 2,
operation: FileOperation::Write,
line_range: None,
content_hash: Some(ContentHash::from_content("v2")),
is_result: true,
});
let classification = MessageClassification {
message_id: "msg-1".to_string(),
tool_call_id: "call-1".to_string(),
sequence: 1,
role: MessageRole::User,
operation: FileOperation::Read,
file_path: Some(PathBuf::from("/test/file.rs")),
line_range: None,
content_hash: Some(ContentHash::from_content("v1")),
content_size: 100,
};
let action = rules.evaluate(&artifact, &classification);
assert_eq!(action, SupersedenceAction::Drop);
}
#[test]
fn test_supersedence_newer_full_read() {
let rules = SupersedenceRules::default();
let mut artifact = FileArtifact::new(PathBuf::from("/test/file.rs"));
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-1", "call-1"),
sequence: 1,
operation: FileOperation::Read,
line_range: Some(LineRange::new(10, Some(50))),
content_hash: Some(ContentHash::from_content("partial")),
is_result: true,
});
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-2", "call-2"),
sequence: 2,
operation: FileOperation::Read,
line_range: None,
content_hash: Some(ContentHash::from_content("full")),
is_result: true,
});
let classification = MessageClassification {
message_id: "msg-1".to_string(),
tool_call_id: "call-1".to_string(),
sequence: 1,
role: MessageRole::User,
operation: FileOperation::Read,
file_path: Some(PathBuf::from("/test/file.rs")),
line_range: Some(LineRange::new(10, Some(50))),
content_hash: Some(ContentHash::from_content("partial")),
content_size: 100,
};
let action = rules.evaluate(&artifact, &classification);
assert_eq!(action, SupersedenceAction::Drop);
}
#[test]
fn test_latest_full_read_moves_to_end() {
let rules = SupersedenceRules::default();
let mut artifact = FileArtifact::new(PathBuf::from("/test/file.rs"));
artifact.record_operation(FileOperationRecord {
key: ClassificationKey::new("msg-1", "call-1"),
sequence: 1,
operation: FileOperation::Read,
line_range: None,
content_hash: Some(ContentHash::from_content("full")),
is_result: true,
});
let classification = MessageClassification {
message_id: "msg-1".to_string(),
tool_call_id: "call-1".to_string(),
sequence: 1,
role: MessageRole::User,
operation: FileOperation::Read,
file_path: Some(PathBuf::from("/test/file.rs")),
line_range: None,
content_hash: Some(ContentHash::from_content("full")),
content_size: 100,
};
let action = rules.evaluate(&artifact, &classification);
assert_eq!(action, SupersedenceAction::MoveToEnd);
}
#[test]
fn test_context_manager_optimization() {
let mut manager = ContextManager::new();
let args = serde_json::json!({"file_path": "/test/file.rs"});
manager
.classifier
.classify_tool_call("read", &args, "msg-1", "call-1");
manager.classifier.classify_tool_result(
"read",
r#"{"file_path":"/test/file.rs","content":"v1"}"#,
"msg-2",
"call-1",
);
let edit_args = serde_json::json!({
"file_path": "/test/file.rs",
"old_string": "old",
"new_string": "new"
});
manager
.classifier
.classify_tool_call("edit", &edit_args, "msg-3", "call-2");
manager.classifier.classify_tool_result(
"edit",
r#"{"file_path":"/test/file.rs"}"#,
"msg-4",
"call-2",
);
manager
.classifier
.classify_tool_call("read", &args, "msg-5", "call-3");
manager.classifier.classify_tool_result(
"read",
r#"{"file_path":"/test/file.rs","content":"v2"}"#,
"msg-6",
"call-3",
);
let result = manager.compute_optimized_order();
assert!(result.should_drop("msg-2"));
assert!(result.should_drop("msg-4"));
assert!(!result.should_drop("msg-6"));
}
#[test]
fn test_unclassified_messages_not_dropped() {
let mut manager = ContextManager::new();
manager.classifier.classify_tool_call(
"read",
&serde_json::json!({"file_path": "/test/file.rs"}),
"msg-1",
"call-1",
);
let result = manager.compute_optimized_order();
assert!(!result.should_drop("msg-unclassified"));
assert!(!result.is_classified("msg-unclassified"));
}
#[test]
fn test_needs_read_after_write() {
let mut manager = ContextManager::new();
let path = PathBuf::from("/test/file.rs");
let args = serde_json::json!({
"file_path": "/test/file.rs",
"content": "fn main() {}"
});
manager
.classifier
.classify_tool_call("write", &args, "msg-1", "call-1");
manager.classifier.classify_tool_result(
"write",
r#"{"file_path":"/test/file.rs"}"#,
"msg-2",
"call-1",
);
assert!(manager.needs_read_after_write(&path));
let read_args = serde_json::json!({"file_path": "/test/file.rs"});
manager
.classifier
.classify_tool_call("read", &read_args, "msg-3", "call-2");
manager.classifier.classify_tool_result(
"read",
r#"{"file_path":"/test/file.rs","content":"fn main() {}"}"#,
"msg-4",
"call-2",
);
assert!(!manager.needs_read_after_write(&path));
}
}