Skip to main content

reliary_core/
ingest.rs

1use crate::session::{SessionState, ReadRecord, EditRecord, ErrorRecord};
2use rustc_hash::FxHashMap;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5
6pub fn parse_session_file(path: &str) -> Result<SessionState, String> {
7    let file = File::open(path).map_err(|e| format!("Cannot open {}: {}", path, e))?;
8    let reader = BufReader::new(file);
9    let mut state = SessionState::default();
10    let mut tool_calls: FxHashMap<String, (String, serde_json::Value)> = FxHashMap::default();
11    let mut edit_counter: FxHashMap<String, usize> = FxHashMap::default();
12    let mut read_counter: FxHashMap<String, usize> = FxHashMap::default();
13    // Track last edit file path per toolCallId
14    let mut edit_files: FxHashMap<String, String> = FxHashMap::default();
15
16    for line in reader.lines() {
17        let line = line.map_err(|e| format!("Read error: {}", e))?;
18        let v: serde_json::Value = match serde_json::from_str(&line) { Ok(v) => v, Err(_) => continue };
19        let typ = match v.get("type").and_then(|t| t.as_str()) { Some(t) => t, None => continue };
20        if typ == "message" {
21            let msg = match v.get("message") { Some(m) => m, None => continue };
22            let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
23
24            // Record tool calls from assistant messages
25            if role == "assistant" {
26                if let Some(content) = msg.get("content").and_then(|c| c.as_array()) {
27                    for item in content {
28                        if item.get("type").and_then(|t| t.as_str()) == Some("toolCall") {
29                            let tool_id = item.get("id").and_then(|s| s.as_str()).unwrap_or("").to_string();
30                            let tool_name = item.get("name").and_then(|s| s.as_str()).unwrap_or("").to_string();
31                            let args = item.get("arguments").cloned().unwrap_or(serde_json::Value::Null);
32                            tool_calls.insert(tool_id.clone(), (tool_name.clone(), args.clone()));
33                            // Pre-extract edit file
34                            if tool_name == "edit" {
35                                if let Some(obj) = args.as_object() {
36                                    if let Some(file) = obj.get("file").and_then(|f| f.as_str()) {
37                                        edit_files.insert(tool_id.clone(), file.to_string());
38                                    }
39                                }
40                            }
41                        }
42                    }
43                }
44            }
45
46            if role == "user" {
47                // Track user turns
48                state.turn_count += 1;
49            }
50
51            if role == "toolResult" {
52                let call_id = msg.get("toolCallId").and_then(|s| s.as_str()).unwrap_or("");
53                let tool_name = msg.get("toolName").and_then(|s| s.as_str()).unwrap_or("");
54
55                let has_content = msg.get("content").and_then(|c| c.as_array()).map(|a| !a.is_empty()).unwrap_or(false);
56
57                // Match tool call to tool result
58                if let Some((_name, args)) = tool_calls.remove(call_id) {
59                    match tool_name {
60                        "read" => {
61                            let path = args.get("path").and_then(|p| p.as_str()).unwrap_or("").to_string();
62                            if !path.is_empty() && has_content {
63                                *read_counter.entry(path.clone()).or_insert(0) += 1;
64                                let is_rerun = read_counter.get(&path).copied().unwrap_or(0) > 1;
65                                let content = msg.get("content").and_then(|c| c.as_array());
66                                let total_size: usize = content.map(|arr| {
67                                    arr.iter().filter_map(|b| b.get("text").and_then(|t| t.as_str()).map(|s| s.len())).sum()
68                                }).unwrap_or(0);
69                                state.reads.push(ReadRecord { path, size: total_size, hash: format!("{:x}", total_size), is_rerun });
70                            }
71                        }
72                        "bash" => {
73                            let cmd = args.get("command").and_then(|c| c.as_str()).unwrap_or("");
74                            if cmd.contains("cargo test") || cmd.contains("pytest") {
75                                if let Some(content) = msg.get("content").and_then(|c| c.as_array()) {
76                                    for block in content {
77                                        if block.get("type").and_then(|t| t.as_str()) == Some("text") {
78                                            let text = block.get("text").and_then(|t| t.as_str()).unwrap_or("");
79                                            // Detect pass/fail from test output directly
80                                            let is_fail = text.contains("FAILED") || text.contains("failures:");
81                                            let is_pass = !is_fail && text.contains("test result:");
82                                            if is_fail || is_pass {
83                                                state.last_test_output = Some(text.chars().take(200).collect());
84                                                state.last_test_pass = is_pass;
85                                            }
86                                            if is_fail {
87                                                state.errors.push(ErrorRecord {
88                                                    turn: state.turn_count,
89                                                    summary: extract_error_summary(text),
90                                                });
91                                            }
92                                        }
93                                    }
94                                }
95                            }
96                        }
97                        "edit" => {
98                            let file = edit_files.remove(call_id).unwrap_or_default();
99                            if let Some(edits_arr) = args.get("edits").and_then(|e| e.as_array()) {
100                                for edit in edits_arr {
101                                    let old = edit.get("oldText").and_then(|e| e.as_str()).unwrap_or("").to_string();
102                                    let new = edit.get("newText").and_then(|e| e.as_str()).unwrap_or("").to_string();
103                                    let cnt = edit_counter.entry(file.clone()).or_insert(0);
104                                    *cnt += 1;
105                                    state.edits.push(EditRecord {
106                                        file: file.clone(),
107                                        line: old.lines().next().unwrap_or("").to_string(),
108                                        attempt: *cnt,
109                                        old_snippet: old.chars().take(40).collect(),
110                                        new_snippet: new.chars().take(40).collect(),
111                                        success: false,
112                                    });
113                                }
114                            }
115                        }
116                        _ => {}
117                    }
118                }
119            }
120        }
121    }
122
123    // Mark last edit as successful if last test passed
124    if state.last_test_pass {
125        if let Some(last) = state.edits.last_mut() {
126            last.success = true;
127        }
128    }
129
130    Ok(state)
131}
132
133fn extract_error_summary(output: &str) -> String {
134    for line in output.lines() {
135        let t = line.trim();
136        if t.contains("FAILED") || t.contains("panicked at") || t.contains("assertion") {
137            return t.chars().take(120).collect();
138        }
139        if t.contains("expected `true`, got `false`") {
140            return t.chars().take(120).collect();
141        }
142    }
143    // Return first line that mentions "failure" or "failures:"
144    for line in output.lines() {
145        if line.contains("failure") {
146            return line.chars().take(120).collect();
147        }
148    }
149    output.lines().next().unwrap_or("").chars().take(120).collect()
150}