codetether_agent/rlm/oracle/
mod.rs1mod ast_validation;
29mod batch;
30mod batch_write;
31mod consensus;
32mod consensus_helpers;
33mod grep_oracle;
34mod grep_validation;
35mod query_type;
36mod record;
37mod schema;
38#[path = "storage/mod.rs"]
39mod storage;
40mod templates;
41mod trace_types;
42mod tree_sitter_oracle;
43mod types;
44#[path = "validator/mod.rs"]
45mod validator;
46
47pub use grep_oracle::{GrepOracle, GrepVerification};
48pub use record::OracleTraceRecord;
49pub use schema::{AstPayload, AstResult, FinalPayload, GrepMatch, GrepPayload, SemanticPayload};
50pub use storage::{
51 OracleTracePersistResult, OracleTraceStorage, OracleTraceSyncStats, default_spool_dir,
52};
53pub use templates::{GeneratedQuery, QueryTemplate, TemplateKind};
54pub use trace_types::{OracleResult, ValidatedTrace};
55pub use tree_sitter_oracle::{TreeSitterOracle, TreeSitterVerification};
56pub use types::{TraceStep, VerificationMethod};
57pub use validator::{BatchValidationStats, SplitWriteStats, TraceValidator};
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum QueryType {
62 PatternMatch,
64 Structural,
66 Semantic,
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub enum FinalAnswerFormat {
73 LineNumberedMatches { matches: Vec<(usize, String)> },
75 CountResult { count: usize },
77 StructuredData { data: serde_json::Value },
79 FreeFormText { text: String },
81}
82
83impl FinalAnswerFormat {
84 pub fn parse(answer: &str) -> Self {
86 let lines: Vec<&str> = answer.lines().collect();
88 let mut numbered_matches = Vec::new();
89 let mut all_valid = true;
90
91 for line in &lines {
92 let trimmed = line.trim();
94 if let Some(colon_pos) = trimmed.find(':') {
95 let num_part = trimmed[..colon_pos].trim().trim_start_matches('L').trim();
96 if let Ok(line_num) = num_part.parse::<usize>() {
97 let text_part = trimmed[colon_pos + 1..].trim().to_string();
98 numbered_matches.push((line_num, text_part));
99 } else {
100 all_valid = false;
101 break;
102 }
103 } else if !trimmed.is_empty() {
104 all_valid = false;
106 break;
107 }
108 }
109
110 if all_valid && !numbered_matches.is_empty() {
111 return Self::LineNumberedMatches {
112 matches: numbered_matches,
113 };
114 }
115
116 let lower = answer.to_lowercase();
118 if lower.contains("found") || lower.contains("count:") || lower.contains("occurrences") {
119 if let Some(count) = extract_count_from_text(answer) {
121 return Self::CountResult { count };
122 }
123 }
124
125 if (answer.trim().starts_with('{') || answer.trim().starts_with('['))
127 && let Ok(data) = serde_json::from_str::<serde_json::Value>(answer)
128 {
129 return Self::StructuredData { data };
130 }
131
132 Self::FreeFormText {
134 text: answer.to_string(),
135 }
136 }
137}
138
139fn extract_count_from_text(text: &str) -> Option<usize> {
141 let re = regex::Regex::new(r"(?i)(?:found|count:?\s*)\s*(\d+)|(\d+)\s+(?:functions?|matches?|occurrences?|items?|results?)").ok()?;
143
144 for cap in re.captures_iter(text) {
145 if let Some(m) = cap.get(1)
147 && let Ok(n) = m.as_str().parse()
148 {
149 return Some(n);
150 }
151 if let Some(m) = cap.get(2)
153 && let Ok(n) = m.as_str().parse()
154 {
155 return Some(n);
156 }
157 }
158
159 None
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn parse_line_numbered_matches() {
168 let answer = "42:async fn foo()\n100:pub struct Bar\n";
169 let format = FinalAnswerFormat::parse(answer);
170 match format {
171 FinalAnswerFormat::LineNumberedMatches { matches } => {
172 assert_eq!(matches.len(), 2);
173 assert_eq!(matches[0], (42, "async fn foo()".to_string()));
174 assert_eq!(matches[1], (100, "pub struct Bar".to_string()));
175 }
176 _ => panic!("Expected LineNumberedMatches"),
177 }
178 }
179
180 #[test]
181 fn parse_count_result() {
182 let answer = "Found 15 async functions";
183 let format = FinalAnswerFormat::parse(answer);
184 match format {
185 FinalAnswerFormat::CountResult { count } => assert_eq!(count, 15),
186 _ => panic!("Expected CountResult"),
187 }
188 }
189
190 #[test]
191 fn parse_structured_data() {
192 let answer = r#"{"name": "foo", "args": ["x", "y"]}"#;
193 let format = FinalAnswerFormat::parse(answer);
194 match format {
195 FinalAnswerFormat::StructuredData { data } => {
196 assert_eq!(data["name"], "foo");
197 }
198 _ => panic!("Expected StructuredData"),
199 }
200 }
201
202 #[test]
203 fn parse_free_form_text() {
204 let answer = "This function handles error cases by using the ? operator";
205 let format = FinalAnswerFormat::parse(answer);
206 match format {
207 FinalAnswerFormat::FreeFormText { text } => {
208 assert!(text.contains("error cases"));
209 }
210 _ => panic!("Expected FreeFormText"),
211 }
212 }
213}