1use std::collections::HashMap;
88
89use chroma::{
90 types::{
91 GetResponse, Include, IncludeList, Metadata, MetadataComparison, MetadataExpression,
92 MetadataValue, PrimitiveOperator, Where,
93 },
94 ChromaCollection,
95};
96
97use crate::{AgentID, EmbeddingService, FileWrite, Transaction, TransactionChunk, TransactionID};
98use claudius::MessageParam;
99
100#[derive(Debug)]
104pub enum ContextManagerError {
105 ChromaError(String),
107 ChunkingError(crate::TransactionSerializationError),
109 GuidError,
111 EmbeddingError(anyhow::Error),
113 LoadAgentError(String),
115 FileError(FileSystemError),
117}
118
119#[derive(Debug)]
121pub enum FileSystemError {
122 InvalidPath(String),
124 FileNotFound(String),
126 InvalidPattern(String),
128 ContentNotFound(String),
130}
131
132impl std::fmt::Display for FileSystemError {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 FileSystemError::InvalidPath(msg) => write!(f, "Invalid file path: {}", msg),
136 FileSystemError::FileNotFound(path) => write!(f, "File not found: {}", path),
137 FileSystemError::InvalidPattern(msg) => write!(f, "Invalid search pattern: {}", msg),
138 FileSystemError::ContentNotFound(msg) => write!(f, "Content not found: {}", msg),
139 }
140 }
141}
142
143impl std::error::Error for FileSystemError {}
144
145impl From<FileSystemError> for ContextManagerError {
146 fn from(error: FileSystemError) -> Self {
147 ContextManagerError::FileError(error)
148 }
149}
150
151impl std::fmt::Display for ContextManagerError {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 match self {
154 ContextManagerError::ChromaError(e) => write!(f, "Chroma error: {}", e),
155 ContextManagerError::ChunkingError(e) => {
156 write!(f, "Transaction chunking error: {}", e)
157 }
158 ContextManagerError::GuidError => write!(f, "Failed to generate GUID"),
159 ContextManagerError::EmbeddingError(e) => write!(f, "Embedding error: {}", e),
160 ContextManagerError::LoadAgentError(e) => write!(f, "Agent loading error: {}", e),
161 ContextManagerError::FileError(e) => write!(f, "File system error: {}", e),
162 }
163 }
164}
165
166impl std::error::Error for ContextManagerError {
167 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168 match self {
169 ContextManagerError::ChromaError(_) => None,
170 ContextManagerError::ChunkingError(e) => Some(e),
171 ContextManagerError::GuidError => None,
172 ContextManagerError::EmbeddingError(e) => Some(e.as_ref()),
173 ContextManagerError::LoadAgentError(_) => None,
174 ContextManagerError::FileError(e) => Some(e),
175 }
176 }
177}
178
179impl From<crate::TransactionSerializationError> for ContextManagerError {
180 fn from(error: crate::TransactionSerializationError) -> Self {
181 ContextManagerError::ChunkingError(error)
182 }
183}
184
185impl From<anyhow::Error> for ContextManagerError {
186 fn from(error: anyhow::Error) -> Self {
187 ContextManagerError::EmbeddingError(error)
188 }
189}
190
191fn extract_metadata_string(
198 metadata: &HashMap<String, MetadataValue>,
199 field_name: &str,
200 chunk_id: &str,
201) -> Result<String, ContextManagerError> {
202 metadata
203 .get(field_name)
204 .and_then(|v| match v {
205 MetadataValue::Str(s) => Some(s.clone()),
206 _ => None,
207 })
208 .ok_or_else(|| {
209 ContextManagerError::LoadAgentError(format!(
210 "Missing or invalid {} in chunk {}",
211 field_name, chunk_id
212 ))
213 })
214}
215
216fn extract_metadata_u32(
221 metadata: &HashMap<String, MetadataValue>,
222 field_name: &str,
223 chunk_id: &str,
224) -> Result<u32, ContextManagerError> {
225 metadata
226 .get(field_name)
227 .and_then(|v| match v {
228 MetadataValue::Int(i) => Some(*i as u32),
229 _ => None,
230 })
231 .ok_or_else(|| {
232 ContextManagerError::LoadAgentError(format!(
233 "Missing or invalid {} in chunk {}",
234 field_name, chunk_id
235 ))
236 })
237}
238
239fn extract_metadata_u64(
244 metadata: &HashMap<String, MetadataValue>,
245 field_name: &str,
246 chunk_id: &str,
247) -> Result<u64, ContextManagerError> {
248 metadata
249 .get(field_name)
250 .and_then(|v| match v {
251 MetadataValue::Int(i) => Some(*i as u64),
252 _ => None,
253 })
254 .ok_or_else(|| {
255 ContextManagerError::LoadAgentError(format!(
256 "Missing or invalid {} in chunk {}",
257 field_name, chunk_id
258 ))
259 })
260}
261
262fn validate_file_path(path: &str) -> Result<(), FileSystemError> {
275 if path.is_empty() {
276 return Err(FileSystemError::InvalidPath(
277 "File path cannot be empty".to_string(),
278 ));
279 }
280
281 if path.contains('\0') {
282 return Err(FileSystemError::InvalidPath(
283 "File path cannot contain null bytes".to_string(),
284 ));
285 }
286
287 if path.contains("..") {
289 return Err(FileSystemError::InvalidPath(
290 "File path cannot contain '..' (path traversal)".to_string(),
291 ));
292 }
293
294 if !path.starts_with('/') {
296 return Err(FileSystemError::InvalidPath(
297 "File path must start with '/'".to_string(),
298 ));
299 }
300
301 if path.len() > 4096 {
303 return Err(FileSystemError::InvalidPath(
304 "File path too long (max 4096 characters)".to_string(),
305 ));
306 }
307
308 Ok(())
309}
310
311fn generate_chunk_id(
323 agent_id: AgentID,
324 context_seq_no: u32,
325 transaction_seq_no: u64,
326 chunk_seq_no: u32,
327) -> String {
328 format!(
329 "{}:{}:{}:{}",
330 agent_id, context_seq_no, transaction_seq_no, chunk_seq_no
331 )
332}
333
334fn metadata_equals(
335 key: impl Into<String>,
336 value: impl Into<MetadataValue>,
337) -> Where {
338 Where::Metadata(MetadataExpression {
339 key: key.into(),
340 comparison: MetadataComparison::Primitive(PrimitiveOperator::Equal, value.into()),
341 })
342}
343
344fn find_most_recent_file_content(
358 contexts: &[AgentContext],
359 mount: crate::MountID,
360 path: &str,
361) -> Option<String> {
362 for context in contexts.iter().rev() {
365 for transaction in context.transactions.iter().rev() {
366 for write in transaction.writes.iter().rev() {
367 if write.mount == mount && write.path == path {
368 return Some(write.data.clone());
369 }
370 }
371 }
372 }
373 None
374}
375
376#[derive(Debug, Clone)]
384pub struct AgentContext {
385 pub agent_id: AgentID,
387 pub context_seq_no: u32,
389 pub transactions: Vec<Transaction>,
391}
392
393#[derive(Debug, Clone)]
416pub struct AgentData {
417 pub agent_id: AgentID,
419 pub contexts: Vec<AgentContext>,
421}
422
423impl AgentData {
424 pub fn all_transactions(&self) -> Vec<&Transaction> {
426 let mut transactions = Vec::new();
427 for context in &self.contexts {
428 transactions.extend(context.transactions.iter());
429 }
430 transactions
431 }
432
433 pub fn latest_context(&self) -> Option<&AgentContext> {
435 self.contexts.iter().max_by_key(|c| c.context_seq_no)
436 }
437
438 pub fn get_context(&self, context_seq_no: u32) -> Option<&AgentContext> {
440 self.contexts
441 .iter()
442 .find(|c| c.context_seq_no == context_seq_no)
443 }
444
445 pub fn next_transaction<'a>(
450 &'a mut self,
451 context_manager: &'a ContextManager,
452 ) -> TransactionBuilder<'a> {
453 TransactionBuilder::new_in_current_context(self, context_manager)
454 }
455
456 pub fn new_context<'a>(
461 &'a mut self,
462 context_manager: &'a ContextManager,
463 ) -> TransactionBuilder<'a> {
464 TransactionBuilder::new_in_next_context(self, context_manager)
465 }
466
467 pub fn get_file_content(
469 &self,
470 mount: crate::MountID,
471 path: &str,
472 ) -> Result<Option<String>, FileSystemError> {
473 validate_file_path(path)?;
474 Ok(find_most_recent_file_content(&self.contexts, mount, path))
475 }
476
477 pub fn list_files(&self, mount: crate::MountID) -> Vec<String> {
479 use std::collections::HashSet;
480
481 let mut files: HashSet<String> = self
483 .contexts
484 .iter()
485 .flat_map(|context| &context.transactions)
486 .flat_map(|transaction| &transaction.writes)
487 .filter(|write| write.mount == mount)
488 .map(|write| write.path.clone())
489 .collect();
490
491 let mut result: Vec<String> = files.drain().collect();
493 result.sort();
494 result
495 }
496
497 pub fn search_file_contents(
499 &self,
500 mount: crate::MountID,
501 pattern: &str,
502 ) -> Result<Vec<(String, Vec<String>)>, FileSystemError> {
503 if pattern.is_empty() {
504 return Err(FileSystemError::InvalidPattern(
505 "Search pattern cannot be empty".to_string(),
506 ));
507 }
508
509 let files = self.list_files(mount);
510
511 let matches = files
513 .into_iter()
514 .filter_map(|file_path| {
515 match self.get_file_content(mount, &file_path) {
517 Ok(Some(content)) => {
518 let matching_lines: Vec<String> = content
520 .lines()
521 .enumerate()
522 .filter_map(|(line_num, line)| {
523 if line.contains(pattern) {
524 Some(format!("{}:{}", line_num + 1, line))
525 } else {
526 None
527 }
528 })
529 .collect();
530
531 if matching_lines.is_empty() {
532 None
533 } else {
534 Some((file_path, matching_lines))
535 }
536 }
537 Ok(None) | Err(_) => None, }
539 })
540 .collect();
541
542 Ok(matches)
543 }
544}
545
546pub struct TransactionBuilder<'a> {
553 agent_data: &'a mut AgentData,
554 context_manager: &'a ContextManager,
555 context_seq_no: u32,
556 transaction_seq_no: u64,
557 msgs: Vec<MessageParam>,
558 writes: Vec<FileWrite>,
559}
560
561impl<'a> TransactionBuilder<'a> {
562 fn create_transaction(&self) -> Transaction {
564 Transaction {
565 agent_id: self.agent_data.agent_id,
566 context_seq_no: self.context_seq_no,
567 transaction_seq_no: self.transaction_seq_no,
568 msgs: self.msgs.clone(),
569 writes: self.writes.clone(),
570 }
571 }
572
573 fn add_validated_write<P: Into<String>, D: Into<String>>(
575 &mut self,
576 mount: crate::MountID,
577 path: P,
578 data: D,
579 ) -> Result<(), FileSystemError> {
580 let path_str = path.into();
581 validate_file_path(&path_str)?;
582
583 self.writes.push(FileWrite {
584 mount,
585 path: path_str,
586 data: data.into(),
587 });
588 Ok(())
589 }
590
591 fn new_in_current_context(
593 agent_data: &'a mut AgentData,
594 context_manager: &'a ContextManager,
595 ) -> Self {
596 let (context_seq_no, transaction_seq_no) =
597 if let Some(latest_context) = agent_data.latest_context() {
598 let next_transaction_seq = latest_context
599 .transactions
600 .iter()
601 .map(|t| t.transaction_seq_no)
602 .max()
603 .unwrap_or(0)
604 + 1;
605 (latest_context.context_seq_no, next_transaction_seq)
606 } else {
607 (1, 1)
609 };
610
611 TransactionBuilder {
612 agent_data,
613 context_manager,
614 context_seq_no,
615 transaction_seq_no,
616 msgs: Vec::new(),
617 writes: Vec::new(),
618 }
619 }
620
621 fn new_in_next_context(
623 agent_data: &'a mut AgentData,
624 context_manager: &'a ContextManager,
625 ) -> Self {
626 let next_context_seq = agent_data
627 .contexts
628 .iter()
629 .map(|c| c.context_seq_no)
630 .max()
631 .unwrap_or(0)
632 + 1;
633
634 TransactionBuilder {
635 agent_data,
636 context_manager,
637 context_seq_no: next_context_seq,
638 transaction_seq_no: 1, msgs: Vec::new(),
640 writes: Vec::new(),
641 }
642 }
643
644 pub fn message(mut self, message: MessageParam) -> Self {
646 self.msgs.push(message);
647 self
648 }
649
650 pub fn messages(mut self, messages: Vec<MessageParam>) -> Self {
652 self.msgs.extend(messages);
653 self
654 }
655
656 pub fn write_file<P: Into<String>, D: Into<String>>(
658 mut self,
659 mount: crate::MountID,
660 path: P,
661 data: D,
662 ) -> Result<Self, FileSystemError> {
663 self.add_validated_write(mount, path, data)?;
664 Ok(self)
665 }
666
667 pub fn write_files(mut self, writes: Vec<FileWrite>) -> Result<Self, FileSystemError> {
669 for write in &writes {
671 validate_file_path(&write.path)?;
672 }
673
674 self.writes.extend(writes);
675 Ok(self)
676 }
677
678 pub fn str_replace_file<P: Into<String>>(
680 mut self,
681 mount: crate::MountID,
682 path: P,
683 old_content: &str,
684 new_content: &str,
685 ) -> Result<Self, FileSystemError> {
686 let path_str = path.into();
687 match self.agent_data.get_file_content(mount, &path_str)? {
688 Some(current_content) => {
689 if !current_content.contains(old_content) {
690 return Err(FileSystemError::ContentNotFound(format!(
691 "Content '{}' not found in file {}",
692 old_content, path_str
693 )));
694 }
695 let updated_content = current_content.replace(old_content, new_content);
696 self.writes.push(FileWrite {
697 mount,
698 path: path_str,
699 data: updated_content,
700 });
701 Ok(self)
702 }
703 None => Err(FileSystemError::FileNotFound(path_str)),
704 }
705 }
706
707 pub fn insert_file<P: Into<String>, D: Into<String>>(
709 mut self,
710 mount: crate::MountID,
711 path: P,
712 content: D,
713 ) -> Result<Self, FileSystemError> {
714 self.add_validated_write(mount, path, content)?;
715 Ok(self)
716 }
717
718 pub fn get_buffered_content(
720 &self,
721 mount: crate::MountID,
722 path: &str,
723 ) -> Result<Option<String>, FileSystemError> {
724 validate_file_path(path)?;
725
726 for write in self.writes.iter().rev() {
728 if write.mount == mount && write.path == path {
729 return Ok(Some(write.data.clone()));
730 }
731 }
732 self.agent_data.get_file_content(mount, path)
734 }
735
736 pub fn view_file(&self, mount: crate::MountID, path: &str) -> Option<String> {
738 self.get_buffered_content(mount, path).ok().flatten()
739 }
740
741 pub fn list_files(&self, mount: crate::MountID) -> Vec<String> {
743 use std::collections::HashSet;
744
745 let mut files: HashSet<String> = self.agent_data.list_files(mount).into_iter().collect();
747
748 for write in &self.writes {
750 if write.mount == mount {
751 files.insert(write.path.clone());
752 }
753 }
754
755 let mut result: Vec<String> = files.into_iter().collect();
756 result.sort();
757 result
758 }
759
760 pub fn search_files(&self, mount: crate::MountID, pattern: &str) -> Vec<(String, Vec<String>)> {
762 let files = self.list_files(mount);
763
764 files
765 .into_iter()
766 .filter_map(|file_path| {
767 match self.get_buffered_content(mount, &file_path) {
769 Ok(Some(content)) => {
770 let matching_lines: Vec<String> = content
771 .lines()
772 .enumerate()
773 .filter_map(|(line_num, line)| {
774 if line.contains(pattern) {
775 Some(format!("{}:{}", line_num + 1, line))
776 } else {
777 None
778 }
779 })
780 .collect();
781
782 if matching_lines.is_empty() {
783 None
784 } else {
785 Some((file_path, matching_lines))
786 }
787 }
788 Ok(None) | Err(_) => None,
789 }
790 })
791 .collect()
792 }
793
794 pub fn get_write_summary(&self) -> Vec<(crate::MountID, String, usize)> {
796 let mut summary = Vec::new();
797 for write in &self.writes {
798 summary.push((write.mount, write.path.clone(), write.data.len()));
799 }
800 summary
801 }
802
803 pub async fn save(mut self) -> Result<String, ContextManagerError> {
811 let transaction = self.create_transaction();
813
814 let nonce = self
816 .context_manager
817 .persist_transaction(&transaction)
818 .await?;
819
820 self.update_agent_data(transaction);
822
823 Ok(nonce)
824 }
825
826 pub fn build(self) -> Transaction {
831 self.create_transaction()
832 }
833
834 fn update_agent_data(&mut self, transaction: Transaction) {
836 if let Some(context) = self
838 .agent_data
839 .contexts
840 .iter_mut()
841 .find(|c| c.context_seq_no == self.context_seq_no)
842 {
843 context.transactions.push(transaction);
845 context.transactions.sort_by_key(|t| t.transaction_seq_no);
847 } else {
848 let new_context = AgentContext {
850 agent_id: self.agent_data.agent_id,
851 context_seq_no: self.context_seq_no,
852 transactions: vec![transaction],
853 };
854 self.agent_data.contexts.push(new_context);
855 self.agent_data.contexts.sort_by_key(|c| c.context_seq_no);
857 }
858 }
859}
860
861pub struct ContextManager {
870 collection: ChromaCollection,
871 embedding_service: EmbeddingService,
872}
873
874impl ContextManager {
875 pub fn new(collection: ChromaCollection) -> Result<Self, ContextManagerError> {
877 let embedding_service = EmbeddingService::new()?;
878 Ok(ContextManager {
879 collection,
880 embedding_service,
881 })
882 }
883
884 pub async fn persist_transaction(
889 &self,
890 transaction: &Transaction,
891 ) -> Result<String, ContextManagerError> {
892 let nonce = TransactionID::generate()
894 .ok_or(ContextManagerError::GuidError)?
895 .to_string();
896
897 let chunks = transaction.chunk_transaction()?;
899
900 let chunk_ids: Vec<String> = chunks
902 .iter()
903 .map(|chunk| {
904 generate_chunk_id(
905 chunk.agent_id,
906 chunk.context_seq_no,
907 chunk.transaction_seq_no,
908 chunk.chunk_seq_no,
909 )
910 })
911 .collect();
912
913 let metadatas: Vec<Metadata> = chunks
915 .iter()
916 .map(|chunk| {
917 let mut metadata: Metadata = HashMap::new();
918 metadata.insert("nonce".to_string(), MetadataValue::Str(nonce.clone()));
919 metadata.insert(
920 "agent_id".to_string(),
921 MetadataValue::Str(chunk.agent_id.to_string()),
922 );
923 metadata.insert(
924 "context_seq_no".to_string(),
925 MetadataValue::Int(chunk.context_seq_no as i64),
926 );
927 metadata.insert(
928 "transaction_seq_no".to_string(),
929 MetadataValue::Int(chunk.transaction_seq_no as i64),
930 );
931 metadata.insert(
932 "chunk_seq_no".to_string(),
933 MetadataValue::Int(chunk.chunk_seq_no as i64),
934 );
935 metadata.insert(
936 "total_chunks".to_string(),
937 MetadataValue::Int(chunk.total_chunks as i64),
938 );
939 metadata
940 })
941 .collect();
942 let metadata_entries: Vec<Option<Metadata>> =
943 metadatas.into_iter().map(Some).collect();
944
945 let document_texts: Vec<String> = chunks.iter().map(|chunk| chunk.data.clone()).collect();
947 let document_refs: Vec<&str> = document_texts.iter().map(|doc| doc.as_str()).collect();
948
949 let embeddings = self.embedding_service.embed(&document_refs)?;
951 let documents: Vec<Option<String>> =
952 document_texts.into_iter().map(Some).collect();
953
954 self.collection
956 .add(chunk_ids, embeddings, Some(documents), None, Some(metadata_entries))
957 .await
958 .map_err(|e| ContextManagerError::ChromaError(e.to_string()))?;
959
960 let verification_successful = self.verify_persistence(transaction, &nonce).await?;
962 if !verification_successful {
963 return Err(ContextManagerError::ChromaError(
964 "Transaction persistence verification failed".to_string(),
965 ));
966 }
967
968 Ok(nonce)
969 }
970
971 pub async fn verify_persistence(
975 &self,
976 transaction: &Transaction,
977 expected_nonce: &str,
978 ) -> Result<bool, ContextManagerError> {
979 let chunk_0_id = generate_chunk_id(
981 transaction.agent_id,
982 transaction.context_seq_no,
983 transaction.transaction_seq_no,
984 0,
985 );
986
987 let include = IncludeList(vec![Include::Metadata]);
989
990 let result = self
991 .collection
992 .get(
993 Some(vec![chunk_0_id]),
994 None,
995 Some(1),
996 Some(0),
997 Some(include),
998 )
999 .await
1000 .map_err(|e| ContextManagerError::ChromaError(e.to_string()))?;
1001
1002 if result.ids.len() != 1 {
1004 return Ok(false);
1005 }
1006
1007 if let Some(metadatas) = result.metadatas
1009 && let Some(Some(metadata)) = metadatas.first()
1010 && let Some(MetadataValue::Str(nonce_str)) = metadata.get("nonce")
1011 {
1012 return Ok(nonce_str == expected_nonce);
1013 }
1014
1015 Ok(false)
1016 }
1017
1018 pub async fn load_agent(&self, agent_id: AgentID) -> Result<AgentData, ContextManagerError> {
1026 let mut all_chunks = Vec::new();
1027 let batch_size: u32 = 300; let mut offset: u32 = 0;
1029
1030 loop {
1031 let agent_filter = metadata_equals("agent_id", agent_id.to_string());
1033 let include = IncludeList(vec![Include::Metadata, Include::Document]);
1034
1035 let result = self
1036 .collection
1037 .get(
1038 None,
1039 Some(agent_filter),
1040 Some(batch_size),
1041 Some(offset),
1042 Some(include),
1043 )
1044 .await
1045 .map_err(|e| ContextManagerError::ChromaError(e.to_string()))?;
1046
1047 let batch_chunks = self.convert_chroma_result_to_chunks(result)?;
1049
1050 let batch_size_returned = batch_chunks.len();
1051 all_chunks.extend(batch_chunks);
1052
1053 if batch_size_returned < batch_size as usize {
1055 break;
1056 }
1057
1058 offset = offset.saturating_add(batch_size);
1059 }
1060
1061 let agent_data = self.assemble_agent_data(agent_id, all_chunks)?;
1063
1064 Ok(agent_data)
1065 }
1066
1067 fn convert_chroma_result_to_chunks(
1069 &self,
1070 result: GetResponse,
1071 ) -> Result<Vec<TransactionChunk>, ContextManagerError> {
1072 let GetResponse {
1073 ids,
1074 metadatas,
1075 documents,
1076 ..
1077 } = result;
1078
1079 let Some(metadatas) = metadatas else {
1080 return Err(ContextManagerError::LoadAgentError(
1081 "No metadata returned from Chroma".to_string(),
1082 ));
1083 };
1084
1085 let Some(documents) = documents else {
1086 return Err(ContextManagerError::LoadAgentError(
1087 "No documents returned from Chroma".to_string(),
1088 ));
1089 };
1090
1091 let mut chunks = Vec::new();
1092 for (i, id) in ids.iter().enumerate() {
1093 let metadata = metadatas.get(i).and_then(|m| m.as_ref()).ok_or_else(|| {
1094 ContextManagerError::LoadAgentError(format!("Missing metadata for chunk {}", id))
1095 })?;
1096
1097 let document = documents.get(i).and_then(|d| d.as_ref()).ok_or_else(|| {
1098 ContextManagerError::LoadAgentError(format!("Missing document for chunk {}", id))
1099 })?;
1100
1101 let agent_id_str = extract_metadata_string(metadata, "agent_id", id)?;
1103
1104 let agent_id = AgentID::from_human_readable(&agent_id_str).ok_or_else(|| {
1105 ContextManagerError::LoadAgentError(format!(
1106 "Invalid agent_id format in chunk {}: {}",
1107 id, agent_id_str
1108 ))
1109 })?;
1110
1111 let context_seq_no = extract_metadata_u32(metadata, "context_seq_no", id)?;
1112 let transaction_seq_no = extract_metadata_u64(metadata, "transaction_seq_no", id)?;
1113 let chunk_seq_no = extract_metadata_u32(metadata, "chunk_seq_no", id)?;
1114 let total_chunks = extract_metadata_u32(metadata, "total_chunks", id)?;
1115
1116 chunks.push(TransactionChunk {
1117 agent_id,
1118 context_seq_no,
1119 transaction_seq_no,
1120 chunk_seq_no,
1121 total_chunks,
1122 data: document.clone(),
1123 });
1124 }
1125
1126 Ok(chunks)
1127 }
1128
1129 fn assemble_agent_data(
1131 &self,
1132 agent_id: AgentID,
1133 chunks: Vec<TransactionChunk>,
1134 ) -> Result<AgentData, ContextManagerError> {
1135 use std::collections::BTreeMap;
1136
1137 let mut context_transaction_chunks: BTreeMap<(u32, u64), Vec<TransactionChunk>> =
1139 BTreeMap::new();
1140
1141 for chunk in chunks {
1142 let key = (chunk.context_seq_no, chunk.transaction_seq_no);
1143 context_transaction_chunks
1144 .entry(key)
1145 .or_default()
1146 .push(chunk);
1147 }
1148
1149 let mut contexts_map: BTreeMap<u32, Vec<Transaction>> = BTreeMap::new();
1151
1152 for ((context_seq_no, _transaction_seq_no), mut transaction_chunks) in
1153 context_transaction_chunks
1154 {
1155 transaction_chunks.sort_by_key(|c| c.chunk_seq_no);
1157
1158 let transaction = Transaction::from_chunks(transaction_chunks).map_err(|e| {
1160 ContextManagerError::LoadAgentError(format!(
1161 "Failed to assemble transaction: {}",
1162 e
1163 ))
1164 })?;
1165
1166 contexts_map
1167 .entry(context_seq_no)
1168 .or_default()
1169 .push(transaction);
1170 }
1171
1172 let mut contexts = Vec::new();
1174 for (context_seq_no, mut transactions) in contexts_map {
1175 transactions.sort_by_key(|t| t.transaction_seq_no);
1177
1178 contexts.push(AgentContext {
1179 agent_id,
1180 context_seq_no,
1181 transactions,
1182 });
1183 }
1184
1185 contexts.sort_by_key(|c| c.context_seq_no);
1187
1188 Ok(AgentData { agent_id, contexts })
1189 }
1190}
1191
1192#[cfg(test)]
1195mod tests {
1196 use super::*;
1197 use crate::AgentID;
1198 use chroma::ChromaHttpClient;
1199 use claudius::{MessageParam, MessageRole};
1200
1201 async fn create_test_client() -> ChromaHttpClient {
1202 ChromaHttpClient::cloud().expect("Failed to construct Chroma client")
1203 }
1204
1205 fn create_test_transaction() -> Transaction {
1206 Transaction {
1207 agent_id: AgentID::generate().unwrap(),
1208 context_seq_no: 1,
1209 transaction_seq_no: 42,
1210 msgs: vec![MessageParam {
1211 role: MessageRole::User,
1212 content: "Test message".into(),
1213 }],
1214 writes: vec![],
1215 }
1216 }
1217
1218 async fn create_test_context_manager() -> ContextManager {
1219 let client = create_test_client().await;
1220 let collection = client
1221 .get_or_create_collection("test_transactions", None, None)
1222 .await
1223 .expect("Failed to create Chroma collection");
1224 ContextManager::new(collection).expect("Failed to create ContextManager")
1225 }
1226
1227 #[tokio::test]
1228 async fn context_manager_creation() {
1229 let _context_manager = create_test_context_manager().await;
1230 }
1232
1233 #[tokio::test]
1234 async fn persist_and_verify_transaction() {
1235 let context_manager = create_test_context_manager().await;
1236 let transaction = create_test_transaction();
1237
1238 let nonce_result = context_manager.persist_transaction(&transaction).await;
1240 assert!(nonce_result.is_ok());
1241
1242 let nonce = nonce_result.unwrap();
1243 assert!(!nonce.is_empty());
1244
1245 let verification_result = context_manager
1247 .verify_persistence(&transaction, &nonce)
1248 .await;
1249 assert!(verification_result.is_ok());
1250 assert!(verification_result.unwrap());
1251
1252 let wrong_nonce = TransactionID::generate().unwrap().to_string();
1254 let wrong_verification = context_manager
1255 .verify_persistence(&transaction, &wrong_nonce)
1256 .await;
1257 assert!(wrong_verification.is_ok());
1258 assert!(!wrong_verification.unwrap());
1259 }
1260
1261 #[tokio::test]
1262 async fn persist_transaction_with_multiple_chunks() {
1263 let client = create_test_client().await;
1264 let collection = client
1265 .get_or_create_collection("test_transactions", None, None)
1266 .await
1267 .expect("Failed to create Chroma collection");
1268 let context_manager =
1269 ContextManager::new(collection).expect("Failed to create ContextManager");
1270
1271 let large_content = "x".repeat(crate::CHUNK_SIZE_LIMIT * 2);
1273 let mut transaction = create_test_transaction();
1274 transaction.msgs.push(MessageParam {
1275 role: MessageRole::Assistant,
1276 content: large_content.into(),
1277 });
1278
1279 let nonce = context_manager
1280 .persist_transaction(&transaction)
1281 .await
1282 .unwrap();
1283 let verification = context_manager
1284 .verify_persistence(&transaction, &nonce)
1285 .await
1286 .unwrap();
1287 assert!(verification);
1288 }
1289
1290 #[tokio::test]
1291 async fn verify_nonexistent_transaction() {
1292 let client = create_test_client().await;
1293 let collection = client
1294 .get_or_create_collection("test_transactions", None, None)
1295 .await
1296 .expect("Failed to create Chroma collection");
1297 let context_manager =
1298 ContextManager::new(collection).expect("Failed to create ContextManager");
1299 let transaction = create_test_transaction();
1300 let fake_nonce = TransactionID::generate().unwrap().to_string();
1301
1302 let verification = context_manager
1303 .verify_persistence(&transaction, &fake_nonce)
1304 .await
1305 .unwrap();
1306 assert!(!verification);
1307 }
1308
1309 #[tokio::test]
1310 async fn load_agent_single_context_single_transaction() {
1311 let client = create_test_client().await;
1312 let collection = client
1313 .get_or_create_collection("test_transactions", None, None)
1314 .await
1315 .expect("Failed to create Chroma collection");
1316 let context_manager =
1317 ContextManager::new(collection).expect("Failed to create ContextManager");
1318
1319 let agent_id = AgentID::generate().unwrap();
1320 let transaction = Transaction {
1321 agent_id,
1322 context_seq_no: 1,
1323 transaction_seq_no: 1,
1324 msgs: vec![MessageParam {
1325 role: MessageRole::User,
1326 content: "Test message for load_agent".into(),
1327 }],
1328 writes: vec![],
1329 };
1330
1331 let _nonce = context_manager
1333 .persist_transaction(&transaction)
1334 .await
1335 .unwrap();
1336
1337 let agent_data = context_manager.load_agent(agent_id).await.unwrap();
1339
1340 assert_eq!(agent_data.agent_id, agent_id);
1342 assert_eq!(agent_data.contexts.len(), 1);
1343
1344 let context = &agent_data.contexts[0];
1345 assert_eq!(context.agent_id, agent_id);
1346 assert_eq!(context.context_seq_no, 1);
1347 assert_eq!(context.transactions.len(), 1);
1348
1349 let loaded_transaction = &context.transactions[0];
1350 assert_eq!(loaded_transaction.agent_id, transaction.agent_id);
1351 assert_eq!(
1352 loaded_transaction.context_seq_no,
1353 transaction.context_seq_no
1354 );
1355 assert_eq!(
1356 loaded_transaction.transaction_seq_no,
1357 transaction.transaction_seq_no
1358 );
1359 assert_eq!(loaded_transaction.msgs.len(), transaction.msgs.len());
1360 }
1361
1362 #[tokio::test]
1363 async fn load_agent_multiple_contexts_multiple_transactions() {
1364 let client = create_test_client().await;
1365 let collection = client
1366 .get_or_create_collection("test_transactions", None, None)
1367 .await
1368 .expect("Failed to create Chroma collection");
1369 let context_manager =
1370 ContextManager::new(collection).expect("Failed to create ContextManager");
1371
1372 let agent_id = AgentID::generate().unwrap();
1373
1374 let transactions = vec![
1376 Transaction {
1377 agent_id,
1378 context_seq_no: 1,
1379 transaction_seq_no: 1,
1380 msgs: vec![MessageParam {
1381 role: MessageRole::User,
1382 content: "Context 1, Transaction 1".into(),
1383 }],
1384 writes: vec![],
1385 },
1386 Transaction {
1387 agent_id,
1388 context_seq_no: 1,
1389 transaction_seq_no: 2,
1390 msgs: vec![MessageParam {
1391 role: MessageRole::Assistant,
1392 content: "Context 1, Transaction 2".into(),
1393 }],
1394 writes: vec![],
1395 },
1396 Transaction {
1397 agent_id,
1398 context_seq_no: 2,
1399 transaction_seq_no: 1,
1400 msgs: vec![MessageParam {
1401 role: MessageRole::User,
1402 content: "Context 2, Transaction 1".into(),
1403 }],
1404 writes: vec![],
1405 },
1406 ];
1407
1408 for transaction in &transactions {
1410 let _nonce = context_manager
1411 .persist_transaction(transaction)
1412 .await
1413 .unwrap();
1414 }
1415
1416 let agent_data = context_manager.load_agent(agent_id).await.unwrap();
1418
1419 assert_eq!(agent_data.agent_id, agent_id);
1421 assert_eq!(agent_data.contexts.len(), 2);
1422
1423 let context1 = &agent_data.contexts[0];
1425 assert_eq!(context1.context_seq_no, 1);
1426 assert_eq!(context1.transactions.len(), 2);
1427 assert_eq!(context1.transactions[0].transaction_seq_no, 1);
1428 assert_eq!(context1.transactions[1].transaction_seq_no, 2);
1429
1430 let context2 = &agent_data.contexts[1];
1432 assert_eq!(context2.context_seq_no, 2);
1433 assert_eq!(context2.transactions.len(), 1);
1434 assert_eq!(context2.transactions[0].transaction_seq_no, 1);
1435
1436 let all_transactions = agent_data.all_transactions();
1438 assert_eq!(all_transactions.len(), 3);
1439
1440 let latest_context = agent_data.latest_context().unwrap();
1441 assert_eq!(latest_context.context_seq_no, 2);
1442
1443 let specific_context = agent_data.get_context(1).unwrap();
1444 assert_eq!(specific_context.transactions.len(), 2);
1445 }
1446
1447 #[tokio::test]
1448 async fn load_agent_with_chunked_transactions() {
1449 let client = create_test_client().await;
1450 let collection = client
1451 .get_or_create_collection("test_transactions", None, None)
1452 .await
1453 .expect("Failed to create Chroma collection");
1454 let context_manager =
1455 ContextManager::new(collection).expect("Failed to create ContextManager");
1456
1457 let agent_id = AgentID::generate().unwrap();
1458
1459 let large_content = "x".repeat(crate::CHUNK_SIZE_LIMIT * 2);
1461 let transaction = Transaction {
1462 agent_id,
1463 context_seq_no: 1,
1464 transaction_seq_no: 1,
1465 msgs: vec![MessageParam {
1466 role: MessageRole::User,
1467 content: large_content.into(),
1468 }],
1469 writes: vec![],
1470 };
1471
1472 let _nonce = context_manager
1474 .persist_transaction(&transaction)
1475 .await
1476 .unwrap();
1477
1478 let agent_data = context_manager.load_agent(agent_id).await.unwrap();
1480
1481 assert_eq!(agent_data.contexts.len(), 1);
1483 let context = &agent_data.contexts[0];
1484 assert_eq!(context.transactions.len(), 1);
1485
1486 let loaded_transaction = &context.transactions[0];
1487 assert_eq!(loaded_transaction.msgs.len(), transaction.msgs.len());
1488 assert_eq!(
1489 loaded_transaction.msgs[0].content,
1490 transaction.msgs[0].content
1491 );
1492 }
1493
1494 #[tokio::test]
1495 async fn load_nonexistent_agent() {
1496 let client = create_test_client().await;
1497 let collection = client
1498 .get_or_create_collection("test_transactions", None, None)
1499 .await
1500 .expect("Failed to create Chroma collection");
1501 let context_manager =
1502 ContextManager::new(collection).expect("Failed to create ContextManager");
1503
1504 let nonexistent_agent_id = AgentID::generate().unwrap();
1505
1506 let agent_data = context_manager
1508 .load_agent(nonexistent_agent_id)
1509 .await
1510 .unwrap();
1511
1512 assert_eq!(agent_data.agent_id, nonexistent_agent_id);
1514 assert!(agent_data.contexts.is_empty());
1515 assert!(agent_data.all_transactions().is_empty());
1516 assert!(agent_data.latest_context().is_none());
1517 }
1518
1519 #[tokio::test]
1520 async fn fluent_transaction_building_next_transaction() {
1521 let client = create_test_client().await;
1522 let collection = client
1523 .get_or_create_collection("test_transactions", None, None)
1524 .await
1525 .expect("Failed to create Chroma collection");
1526 let context_manager =
1527 ContextManager::new(collection).expect("Failed to create ContextManager");
1528
1529 let agent_id = AgentID::generate().unwrap();
1530
1531 let transaction = Transaction {
1533 agent_id,
1534 context_seq_no: 1,
1535 transaction_seq_no: 1,
1536 msgs: vec![MessageParam {
1537 role: MessageRole::User,
1538 content: "Initial message".into(),
1539 }],
1540 writes: vec![],
1541 };
1542
1543 context_manager
1544 .persist_transaction(&transaction)
1545 .await
1546 .unwrap();
1547
1548 let mut agent_data = context_manager.load_agent(agent_id).await.unwrap();
1550
1551 let nonce = agent_data
1553 .next_transaction(&context_manager)
1554 .message(MessageParam {
1555 role: MessageRole::Assistant,
1556 content: "Response message".into(),
1557 })
1558 .save()
1559 .await
1560 .unwrap();
1561
1562 assert!(!nonce.is_empty());
1563
1564 assert_eq!(agent_data.contexts.len(), 1);
1566 let context = &agent_data.contexts[0];
1567 assert_eq!(context.transactions.len(), 2);
1568 assert_eq!(context.transactions[1].transaction_seq_no, 2);
1569 assert_eq!(context.transactions[1].msgs.len(), 1);
1570
1571 let reloaded_data = context_manager.load_agent(agent_id).await.unwrap();
1573 assert_eq!(reloaded_data.contexts[0].transactions.len(), 2);
1574 }
1575
1576 #[tokio::test]
1577 async fn fluent_transaction_building_new_context() {
1578 let client = create_test_client().await;
1579 let collection = client
1580 .get_or_create_collection("test_transactions", None, None)
1581 .await
1582 .expect("Failed to create Chroma collection");
1583 let context_manager =
1584 ContextManager::new(collection).expect("Failed to create ContextManager");
1585
1586 let agent_id = AgentID::generate().unwrap();
1587
1588 let transaction = Transaction {
1590 agent_id,
1591 context_seq_no: 1,
1592 transaction_seq_no: 1,
1593 msgs: vec![MessageParam {
1594 role: MessageRole::User,
1595 content: "Initial message".into(),
1596 }],
1597 writes: vec![],
1598 };
1599
1600 context_manager
1601 .persist_transaction(&transaction)
1602 .await
1603 .unwrap();
1604
1605 let mut agent_data = context_manager.load_agent(agent_id).await.unwrap();
1607
1608 let nonce = agent_data
1610 .new_context(&context_manager)
1611 .message(MessageParam {
1612 role: MessageRole::User,
1613 content: "New context message".into(),
1614 })
1615 .save()
1616 .await
1617 .unwrap();
1618
1619 assert!(!nonce.is_empty());
1620
1621 assert_eq!(agent_data.contexts.len(), 2);
1623 let new_context = &agent_data.contexts[1];
1624 assert_eq!(new_context.context_seq_no, 2);
1625 assert_eq!(new_context.transactions.len(), 1);
1626 assert_eq!(new_context.transactions[0].transaction_seq_no, 1);
1627
1628 let reloaded_data = context_manager.load_agent(agent_id).await.unwrap();
1630 assert_eq!(reloaded_data.contexts.len(), 2);
1631 assert_eq!(reloaded_data.contexts[1].context_seq_no, 2);
1632 }
1633
1634 #[tokio::test]
1635 async fn fluent_transaction_building_with_file_writes() {
1636 let client = create_test_client().await;
1637 let collection = client
1638 .get_or_create_collection("test_transactions", None, None)
1639 .await
1640 .expect("Failed to create Chroma collection");
1641 let context_manager =
1642 ContextManager::new(collection).expect("Failed to create ContextManager");
1643
1644 let agent_id = AgentID::generate().unwrap();
1645 let mount_id = crate::MountID::generate().unwrap();
1646
1647 let mut agent_data = AgentData {
1649 agent_id,
1650 contexts: vec![],
1651 };
1652
1653 let nonce = agent_data
1655 .next_transaction(&context_manager)
1656 .message(MessageParam {
1657 role: MessageRole::User,
1658 content: "Create some files".into(),
1659 })
1660 .write_file(mount_id, "/test.txt", "Hello, world!")
1661 .unwrap()
1662 .write_file(mount_id, "/config.json", r#"{"setting": "value"}"#)
1663 .unwrap()
1664 .save()
1665 .await
1666 .unwrap();
1667
1668 assert!(!nonce.is_empty());
1669
1670 assert_eq!(agent_data.contexts.len(), 1);
1672 let context = &agent_data.contexts[0];
1673 assert_eq!(context.transactions.len(), 1);
1674 let transaction = &context.transactions[0];
1675 assert_eq!(transaction.writes.len(), 2);
1676 assert_eq!(transaction.writes[0].path, "/test.txt");
1677 assert_eq!(transaction.writes[0].data, "Hello, world!");
1678 assert_eq!(transaction.writes[1].path, "/config.json");
1679
1680 let reloaded_data = context_manager.load_agent(agent_id).await.unwrap();
1682 let reloaded_transaction = &reloaded_data.contexts[0].transactions[0];
1683 assert_eq!(reloaded_transaction.writes.len(), 2);
1684 }
1685
1686 #[tokio::test]
1687 async fn fluent_transaction_building_multiple_messages() {
1688 let client = create_test_client().await;
1689 let collection = client
1690 .get_or_create_collection("test_transactions", None, None)
1691 .await
1692 .expect("Failed to create Chroma collection");
1693 let context_manager =
1694 ContextManager::new(collection).expect("Failed to create ContextManager");
1695
1696 let agent_id = AgentID::generate().unwrap();
1697
1698 let mut agent_data = AgentData {
1700 agent_id,
1701 contexts: vec![],
1702 };
1703
1704 let messages = vec![
1705 MessageParam {
1706 role: MessageRole::User,
1707 content: "First message".into(),
1708 },
1709 MessageParam {
1710 role: MessageRole::Assistant,
1711 content: "Second message".into(),
1712 },
1713 ];
1714
1715 let nonce = agent_data
1717 .next_transaction(&context_manager)
1718 .messages(messages.clone())
1719 .message(MessageParam {
1720 role: MessageRole::User,
1721 content: "Third message".into(),
1722 })
1723 .save()
1724 .await
1725 .unwrap();
1726
1727 assert!(!nonce.is_empty());
1728
1729 let context = &agent_data.contexts[0];
1731 let transaction = &context.transactions[0];
1732 assert_eq!(transaction.msgs.len(), 3);
1733
1734 assert_eq!(transaction.msgs[0].role, MessageRole::User);
1736 assert_eq!(transaction.msgs[1].role, MessageRole::Assistant);
1737 assert_eq!(transaction.msgs[2].role, MessageRole::User);
1738
1739 assert_eq!(transaction.msgs[0].content, messages[0].content);
1741 assert_eq!(transaction.msgs[1].content, messages[1].content);
1742 }
1743
1744 #[tokio::test]
1745 async fn fluent_transaction_building_build_without_save() {
1746 let client = create_test_client().await;
1747 let collection = client
1748 .get_or_create_collection("test_transactions", None, None)
1749 .await
1750 .expect("Failed to create Chroma collection");
1751 let context_manager =
1752 ContextManager::new(collection).expect("Failed to create ContextManager");
1753
1754 let agent_id = AgentID::generate().unwrap();
1755 let mut agent_data = AgentData {
1756 agent_id,
1757 contexts: vec![],
1758 };
1759
1760 let transaction = agent_data
1762 .next_transaction(&context_manager)
1763 .message(MessageParam {
1764 role: MessageRole::User,
1765 content: "Test message".into(),
1766 })
1767 .build();
1768
1769 assert_eq!(transaction.agent_id, agent_id);
1771 assert_eq!(transaction.context_seq_no, 1);
1772 assert_eq!(transaction.transaction_seq_no, 1);
1773 assert_eq!(transaction.msgs.len(), 1);
1774
1775 assert!(agent_data.contexts.is_empty());
1777
1778 let loaded_data = context_manager.load_agent(agent_id).await.unwrap();
1780 assert!(loaded_data.contexts.is_empty());
1781 }
1782
1783 #[tokio::test]
1784 async fn fluent_transaction_building_sequence_numbers() {
1785 let client = create_test_client().await;
1786 let collection = client
1787 .get_or_create_collection("test_transactions", None, None)
1788 .await
1789 .expect("Failed to create Chroma collection");
1790 let context_manager =
1791 ContextManager::new(collection).expect("Failed to create ContextManager");
1792
1793 let agent_id = AgentID::generate().unwrap();
1794 let mut agent_data = AgentData {
1795 agent_id,
1796 contexts: vec![],
1797 };
1798
1799 let tx1 = agent_data
1801 .next_transaction(&context_manager)
1802 .message(MessageParam {
1803 role: MessageRole::User,
1804 content: "Message 1".into(),
1805 })
1806 .build();
1807
1808 assert_eq!(tx1.context_seq_no, 1);
1809 assert_eq!(tx1.transaction_seq_no, 1);
1810
1811 agent_data.contexts.push(AgentContext {
1813 agent_id,
1814 context_seq_no: 1,
1815 transactions: vec![tx1],
1816 });
1817
1818 let tx2 = agent_data
1820 .next_transaction(&context_manager)
1821 .message(MessageParam {
1822 role: MessageRole::Assistant,
1823 content: "Message 2".into(),
1824 })
1825 .build();
1826
1827 assert_eq!(tx2.context_seq_no, 1);
1828 assert_eq!(tx2.transaction_seq_no, 2);
1829
1830 let tx3 = agent_data
1832 .new_context(&context_manager)
1833 .message(MessageParam {
1834 role: MessageRole::User,
1835 content: "Message 3".into(),
1836 })
1837 .build();
1838
1839 assert_eq!(tx3.context_seq_no, 2);
1840 assert_eq!(tx3.transaction_seq_no, 1);
1841 }
1842
1843 #[tokio::test]
1844 async fn agent_data_file_reading() {
1845 let client = create_test_client().await;
1846
1847 client
1848 .heartbeat()
1849 .await
1850 .expect("Chroma heartbeat failed; ensure environment is configured");
1851
1852 let collection_name = format!("test_agent_file_reading_{}", rand::random::<u32>());
1853 let collection = client
1854 .get_or_create_collection(&collection_name, None, None)
1855 .await
1856 .unwrap();
1857
1858 let context_manager = ContextManager::new(collection).unwrap();
1859 let agent_id = AgentID::generate().unwrap();
1860 let mount_id = crate::MountID::generate().unwrap();
1861
1862 let mut agent_data = AgentData {
1864 agent_id,
1865 contexts: Vec::new(),
1866 };
1867
1868 let _nonce = agent_data
1869 .new_context(&context_manager)
1870 .write_file(mount_id, "/file1.txt", "First file content")
1871 .unwrap()
1872 .write_file(mount_id, "/file2.txt", "Second file content")
1873 .unwrap()
1874 .write_file(mount_id, "/subdir/file3.txt", "Third file content")
1875 .unwrap()
1876 .save()
1877 .await
1878 .unwrap();
1879
1880 assert_eq!(
1882 agent_data.get_file_content(mount_id, "/file1.txt").unwrap(),
1883 Some("First file content".to_string())
1884 );
1885 assert_eq!(
1886 agent_data
1887 .get_file_content(mount_id, "/nonexistent.txt")
1888 .unwrap(),
1889 None
1890 );
1891
1892 let files = agent_data.list_files(mount_id);
1894 assert_eq!(files.len(), 3);
1895 assert!(files.contains(&"/file1.txt".to_string()));
1896 assert!(files.contains(&"/file2.txt".to_string()));
1897 assert!(files.contains(&"/subdir/file3.txt".to_string()));
1898
1899 let matches = agent_data.search_file_contents(mount_id, "file").unwrap();
1901 assert_eq!(matches.len(), 3);
1902 assert!(matches.iter().any(|(path, _)| path == "/file1.txt"));
1903
1904 let matches = agent_data.search_file_contents(mount_id, "Third").unwrap();
1905 assert_eq!(matches.len(), 1);
1906 assert_eq!(matches[0].0, "/subdir/file3.txt");
1907 }
1908
1909 #[tokio::test]
1910 async fn transaction_builder_filesystem_methods() {
1911 let client = create_test_client().await;
1912
1913 client
1914 .heartbeat()
1915 .await
1916 .expect("Chroma heartbeat failed; ensure environment is configured");
1917
1918 let collection_name = format!("test_builder_fs_{}", rand::random::<u32>());
1919 let collection = client
1920 .get_or_create_collection(&collection_name, None, None)
1921 .await
1922 .unwrap();
1923
1924 let context_manager = ContextManager::new(collection).unwrap();
1925 let agent_id = AgentID::generate().unwrap();
1926 let mount_id = crate::MountID::generate().unwrap();
1927
1928 let mut agent_data = AgentData {
1930 agent_id,
1931 contexts: Vec::new(),
1932 };
1933
1934 let _nonce = agent_data
1935 .new_context(&context_manager)
1936 .write_file(mount_id, "/original.txt", "Original content")
1937 .unwrap()
1938 .save()
1939 .await
1940 .unwrap();
1941
1942 let builder = agent_data.next_transaction(&context_manager);
1944
1945 assert_eq!(
1947 builder.view_file(mount_id, "/original.txt"),
1948 Some("Original content".to_string())
1949 );
1950 assert_eq!(builder.view_file(mount_id, "/nonexistent.txt"), None);
1951
1952 let files = builder.list_files(mount_id);
1954 assert_eq!(files.len(), 1);
1955 assert!(files.contains(&"/original.txt".to_string()));
1956
1957 let matches = builder.search_files(mount_id, "Original");
1959 assert_eq!(matches.len(), 1);
1960 assert_eq!(matches[0].0, "/original.txt");
1961
1962 let builder = builder
1964 .str_replace_file(mount_id, "/original.txt", "Original", "Modified")
1965 .unwrap();
1966
1967 assert_eq!(
1969 builder
1970 .get_buffered_content(mount_id, "/original.txt")
1971 .unwrap(),
1972 Some("Modified content".to_string())
1973 );
1974
1975 let builder2 = agent_data.next_transaction(&context_manager);
1977 let result = builder2.str_replace_file(mount_id, "/nonexistent.txt", "old", "new");
1978 assert!(result.is_err());
1979 if let Err(error) = result {
1980 assert!(error.to_string().contains("not found"));
1981 }
1982
1983 let builder3 = agent_data.next_transaction(&context_manager);
1984 let result = builder3.str_replace_file(mount_id, "/original.txt", "nonexistent", "new");
1985 assert!(result.is_err());
1986 if let Err(error) = result {
1987 assert!(error.to_string().contains("not found"));
1988 }
1989
1990 let builder4 = agent_data
1992 .next_transaction(&context_manager)
1993 .insert_file(mount_id, "/new_file.txt", "New file content")
1994 .unwrap();
1995
1996 assert_eq!(
1997 builder4
1998 .get_buffered_content(mount_id, "/new_file.txt")
1999 .unwrap(),
2000 Some("New file content".to_string())
2001 );
2002 }
2003
2004 #[tokio::test]
2005 async fn write_buffering_latest_wins() {
2006 let client = create_test_client().await;
2007
2008 client
2009 .heartbeat()
2010 .await
2011 .expect("Chroma heartbeat failed; ensure environment is configured");
2012
2013 let collection_name = format!("test_write_buffering_{}", rand::random::<u32>());
2014 let collection = client
2015 .get_or_create_collection(&collection_name, None, None)
2016 .await
2017 .unwrap();
2018
2019 let context_manager = ContextManager::new(collection).unwrap();
2020 let agent_id = AgentID::generate().unwrap();
2021 let mount_id = crate::MountID::generate().unwrap();
2022
2023 let mut agent_data = AgentData {
2024 agent_id,
2025 contexts: Vec::new(),
2026 };
2027
2028 let builder = agent_data
2030 .new_context(&context_manager)
2031 .write_file(mount_id, "/multi_write.txt", "First write")
2032 .unwrap()
2033 .write_file(mount_id, "/multi_write.txt", "Second write")
2034 .unwrap()
2035 .write_file(mount_id, "/multi_write.txt", "Third write")
2036 .unwrap();
2037
2038 assert_eq!(
2040 builder
2041 .get_buffered_content(mount_id, "/multi_write.txt")
2042 .unwrap(),
2043 Some("Third write".to_string())
2044 );
2045
2046 let summary = builder.get_write_summary();
2048 assert_eq!(summary.len(), 3); assert!(
2050 summary
2051 .iter()
2052 .all(|(_, path, _)| path == "/multi_write.txt")
2053 );
2054
2055 let _nonce = builder.save().await.unwrap();
2057
2058 assert_eq!(
2060 agent_data
2061 .get_file_content(mount_id, "/multi_write.txt")
2062 .unwrap(),
2063 Some("Third write".to_string())
2064 );
2065 }
2066
2067 #[tokio::test]
2068 async fn multiple_writes_same_file_appears_once() {
2069 let client = create_test_client().await;
2070
2071 client
2072 .heartbeat()
2073 .await
2074 .expect("Chroma heartbeat failed; ensure environment is configured");
2075
2076 let collection_name = format!("test_multiple_writes_{}", rand::random::<u32>());
2077 let collection = client
2078 .get_or_create_collection(&collection_name, None, None)
2079 .await
2080 .unwrap();
2081
2082 let context_manager = ContextManager::new(collection).unwrap();
2083 let agent_id = AgentID::generate().unwrap();
2084 let mount_id = crate::MountID::generate().unwrap();
2085
2086 let mut agent_data = AgentData {
2087 agent_id,
2088 contexts: Vec::new(),
2089 };
2090
2091 let _nonce1 = agent_data
2093 .new_context(&context_manager)
2094 .write_file(mount_id, "/config.txt", "Version 1 content")
2095 .unwrap()
2096 .write_file(mount_id, "/other.txt", "Other file content")
2097 .unwrap()
2098 .save()
2099 .await
2100 .unwrap();
2101
2102 let _nonce2 = agent_data
2103 .next_transaction(&context_manager)
2104 .write_file(mount_id, "/config.txt", "Version 2 content")
2105 .unwrap()
2106 .save()
2107 .await
2108 .unwrap();
2109
2110 let _nonce3 = agent_data
2111 .next_transaction(&context_manager)
2112 .write_file(mount_id, "/config.txt", "Version 3 final content")
2113 .unwrap()
2114 .save()
2115 .await
2116 .unwrap();
2117
2118 let files = agent_data.list_files(mount_id);
2120 assert_eq!(files.len(), 2); assert!(files.contains(&"/config.txt".to_string()));
2122 assert!(files.contains(&"/other.txt".to_string()));
2123
2124 let config_count = files.iter().filter(|&f| f == "/config.txt").count();
2126 let other_count = files.iter().filter(|&f| f == "/other.txt").count();
2127 assert_eq!(
2128 config_count, 1,
2129 "config.txt should appear exactly once in file list"
2130 );
2131 assert_eq!(
2132 other_count, 1,
2133 "other.txt should appear exactly once in file list"
2134 );
2135
2136 let content = agent_data
2138 .get_file_content(mount_id, "/config.txt")
2139 .unwrap();
2140 assert_eq!(content, Some("Version 3 final content".to_string()));
2141
2142 let other_content = agent_data.get_file_content(mount_id, "/other.txt").unwrap();
2143 assert_eq!(other_content, Some("Other file content".to_string()));
2144
2145 let matches = agent_data.search_file_contents(mount_id, "final").unwrap();
2147 assert_eq!(matches.len(), 1, "Should find config.txt exactly once");
2148 assert_eq!(matches[0].0, "/config.txt");
2149 assert!(matches[0].1[0].contains("Version 3 final content"));
2150
2151 let old_matches = agent_data
2153 .search_file_contents(mount_id, "Version 1")
2154 .unwrap();
2155 assert_eq!(
2156 old_matches.len(),
2157 0,
2158 "Should not find old Version 1 content"
2159 );
2160
2161 let old_matches2 = agent_data
2162 .search_file_contents(mount_id, "Version 2")
2163 .unwrap();
2164 assert_eq!(
2165 old_matches2.len(),
2166 0,
2167 "Should not find old Version 2 content"
2168 );
2169
2170 let builder = agent_data
2172 .next_transaction(&context_manager)
2173 .write_file(mount_id, "/config.txt", "Pending version")
2174 .unwrap();
2175
2176 let builder_files = builder.list_files(mount_id);
2178 let config_count_builder = builder_files.iter().filter(|&f| f == "/config.txt").count();
2179 assert_eq!(
2180 config_count_builder, 1,
2181 "TransactionBuilder should show config.txt exactly once"
2182 );
2183
2184 assert_eq!(
2186 builder
2187 .get_buffered_content(mount_id, "/config.txt")
2188 .unwrap(),
2189 Some("Pending version".to_string())
2190 );
2191
2192 let builder_matches = builder.search_files(mount_id, "Pending");
2194 assert_eq!(
2195 builder_matches.len(),
2196 1,
2197 "Should find file with pending write exactly once"
2198 );
2199 assert_eq!(builder_matches[0].0, "/config.txt");
2200 }
2201}