memories/read/src/citations.rs
85 lines
use codex_protocol::ThreadId;use codex_protocol::memory_citation::MemoryCitation;use codex_protocol::memory_citation::MemoryCitationEntry;use std::collections::HashSet;pub fn parse_memory_citation(citations: Vec<String>) -> Option<MemoryCitation> { let mut entries = Vec::new(); let mut rollout_ids = Vec::new(); let mut seen_rollout_ids = HashSet::new(); for citation in citations { if let Some(entries_block) = extract_block(&citation, "<citation_entries>", "</citation_entries>") { entries.extend( entries_block .lines() .filter_map(parse_memory_citation_entry), ); } if let Some(ids_block) = extract_ids_block(&citation) { for id in ids_block .lines() .map(str::trim) .filter(|line| !line.is_empty()) { if seen_rollout_ids.insert(id.to_string()) { rollout_ids.push(id.to_string()); } } } } if entries.is_empty() && rollout_ids.is_empty() { None } else { Some(MemoryCitation { entries, rollout_ids, }) }}pub fn thread_ids_from_memory_citation(memory_citation: &MemoryCitation) -> Vec<ThreadId> { memory_citation .rollout_ids .iter() .filter_map(|id| ThreadId::try_from(id.as_str()).ok()) .collect()}fn parse_memory_citation_entry(line: &str) -> Option<MemoryCitationEntry> { let line = line.trim(); if line.is_empty() { return None; } let (location, note) = line.rsplit_once("|note=[")?; let note = note.strip_suffix(']')?.trim().to_string(); let (path, line_range) = location.rsplit_once(':')?; let (line_start, line_end) = line_range.split_once('-')?; Some(MemoryCitationEntry { path: path.trim().to_string(), line_start: line_start.trim().parse().ok()?, line_end: line_end.trim().parse().ok()?, note, })}fn extract_block<'a>(text: &'a str, open: &str, close: &str) -> Option<&'a str> { let (_, rest) = text.split_once(open)?; let (body, _) = rest.split_once(close)?; Some(body)}fn extract_ids_block(text: &str) -> Option<&str> { extract_block(text, "<rollout_ids>", "</rollout_ids>") .or_else(|| extract_block(text, "<thread_ids>", "</thread_ids>"))}#[cfg(test)]#[path = "citations_tests.rs"]mod tests;