1use anyhow::Result;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::process::Stdio;
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
15use tokio::process::{Child, Command};
16use tokio::time::timeout;
17
18use crate::provider::{
19 CompletionRequest, ContentPart, Message, Provider, Role,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
24#[serde(rename_all = "lowercase")]
25pub enum ReplRuntime {
26 #[default]
28 Rust,
29 Bun,
31 Python,
33}
34
35pub struct RlmRepl {
37 runtime: ReplRuntime,
38 context: String,
39 context_lines: Vec<String>,
40 variables: HashMap<String, String>,
41}
42
43#[derive(Debug, Clone)]
45pub struct ReplResult {
46 pub stdout: String,
47 pub stderr: String,
48 pub final_answer: Option<String>,
49}
50
51impl RlmRepl {
52 pub fn new(context: String, runtime: ReplRuntime) -> Self {
54 let context_lines = context.lines().map(|s| s.to_string()).collect();
55 Self {
56 runtime,
57 context,
58 context_lines,
59 variables: HashMap::new(),
60 }
61 }
62
63 pub fn context(&self) -> &str {
65 &self.context
66 }
67
68 pub fn lines(&self) -> &[String] {
70 &self.context_lines
71 }
72
73 pub fn head(&self, n: usize) -> Vec<&str> {
75 self.context_lines.iter().take(n).map(|s| s.as_str()).collect()
76 }
77
78 pub fn tail(&self, n: usize) -> Vec<&str> {
80 let start = self.context_lines.len().saturating_sub(n);
81 self.context_lines.iter().skip(start).map(|s| s.as_str()).collect()
82 }
83
84 pub fn grep(&self, pattern: &str) -> Vec<(usize, &str)> {
86 let re = match regex::Regex::new(pattern) {
87 Ok(r) => r,
88 Err(_) => {
89 return self.context_lines
91 .iter()
92 .enumerate()
93 .filter(|(_, line)| line.contains(pattern))
94 .map(|(i, line)| (i + 1, line.as_str()))
95 .collect();
96 }
97 };
98
99 self.context_lines
100 .iter()
101 .enumerate()
102 .filter(|(_, line)| re.is_match(line))
103 .map(|(i, line)| (i + 1, line.as_str()))
104 .collect()
105 }
106
107 pub fn count(&self, pattern: &str) -> usize {
109 let re = match regex::Regex::new(pattern) {
110 Ok(r) => r,
111 Err(_) => return self.context.matches(pattern).count(),
112 };
113 re.find_iter(&self.context).count()
114 }
115
116 pub fn slice(&self, start: usize, end: usize) -> &str {
118 let end = end.min(self.context.len());
119 let start = start.min(end);
120 &self.context[start..end]
121 }
122
123 pub fn chunks(&self, n: usize) -> Vec<String> {
125 if n == 0 {
126 return vec![self.context.clone()];
127 }
128
129 let chunk_size = self.context_lines.len().div_ceil(n);
130 self.context_lines
131 .chunks(chunk_size)
132 .map(|chunk| chunk.join("\n"))
133 .collect()
134 }
135
136 pub fn set_var(&mut self, name: &str, value: String) {
138 self.variables.insert(name.to_string(), value);
139 }
140
141 pub fn get_var(&self, name: &str) -> Option<&str> {
143 self.variables.get(name).map(|s| s.as_str())
144 }
145
146 pub fn execute(&mut self, code: &str) -> ReplResult {
157 match self.runtime {
158 ReplRuntime::Rust => self.execute_rust_dsl(code),
159 ReplRuntime::Bun | ReplRuntime::Python => {
160 self.execute_rust_dsl(code)
163 }
164 }
165 }
166
167 fn execute_rust_dsl(&mut self, code: &str) -> ReplResult {
168 let mut stdout = Vec::new();
169 let mut final_answer = None;
170
171 for line in code.lines() {
172 let line = line.trim();
173 if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
174 continue;
175 }
176
177 if let Some(result) = self.execute_dsl_line(line) {
179 match result {
180 DslResult::Output(s) => stdout.push(s),
181 DslResult::Final(s) => {
182 final_answer = Some(s);
183 break;
184 }
185 DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
186 }
187 }
188 }
189
190 ReplResult {
191 stdout: stdout.join("\n"),
192 stderr: String::new(),
193 final_answer,
194 }
195 }
196
197 pub fn execute_dsl_line(&mut self, line: &str) -> Option<DslResult> {
198 if line.starts_with("FINAL(") || line.starts_with("FINAL!(") {
200 let start = line.find('(').unwrap() + 1;
201 let end = line.rfind(')').unwrap_or(line.len());
202 let answer = line[start..end].trim().trim_matches(|c| c == '"' || c == '\'' || c == '`');
203 return Some(DslResult::Final(answer.to_string()));
204 }
205
206 if line.starts_with("print(") || line.starts_with("println!(") || line.starts_with("console.log(") {
208 let start = line.find('(').unwrap() + 1;
209 let end = line.rfind(')').unwrap_or(line.len());
210 let content = line[start..end].trim().trim_matches(|c| c == '"' || c == '\'' || c == '`');
211
212 let expanded = self.expand_expression(content);
214 return Some(DslResult::Output(expanded));
215 }
216
217 if let Some(eq_pos) = line.find('=') {
219 if !line.contains("==") && !line.starts_with("if ") {
220 let var_name = line[..eq_pos].trim()
221 .trim_start_matches("let ")
222 .trim_start_matches("const ")
223 .trim_start_matches("var ")
224 .trim();
225 let expr = line[eq_pos + 1..].trim().trim_end_matches(';');
226
227 let value = self.evaluate_expression(expr);
228 self.set_var(var_name, value);
229 return None;
230 }
231 }
232
233 if line.starts_with("head(") || line.starts_with("tail(") ||
235 line.starts_with("grep(") || line.starts_with("count(") ||
236 line.starts_with("lines()") || line.starts_with("slice(") ||
237 line.starts_with("chunks(") || line.starts_with("context") {
238 let result = self.evaluate_expression(line);
239 return Some(DslResult::Output(result));
240 }
241
242 None
243 }
244
245 fn expand_expression(&self, expr: &str) -> String {
246 let mut result = expr.to_string();
248
249 for (name, value) in &self.variables {
250 let patterns = [
251 format!("${{{}}}", name),
252 format!("${}", name),
253 format!("{{{}}}", name),
254 ];
255 for p in patterns {
256 result = result.replace(&p, value);
257 }
258 }
259
260 if result.contains("context.len()") || result.contains("context.length") {
262 result = result
263 .replace("context.len()", &self.context.len().to_string())
264 .replace("context.length", &self.context.len().to_string());
265 }
266
267 if result.contains("lines().len()") || result.contains("lines().length") {
268 result = result
269 .replace("lines().len()", &self.context_lines.len().to_string())
270 .replace("lines().length", &self.context_lines.len().to_string());
271 }
272
273 result
274 }
275
276 pub fn evaluate_expression(&mut self, expr: &str) -> String {
277 let expr = expr.trim().trim_end_matches(';');
278
279 if expr.starts_with("head(") {
281 let n = self.extract_number(expr).unwrap_or(10);
282 return self.head(n).join("\n");
283 }
284
285 if expr.starts_with("tail(") {
287 let n = self.extract_number(expr).unwrap_or(10);
288 return self.tail(n).join("\n");
289 }
290
291 if expr.starts_with("grep(") {
293 let pattern = self.extract_string(expr).unwrap_or_default();
294 let matches = self.grep(&pattern);
295 return matches
296 .iter()
297 .map(|(i, line)| format!("{}:{}", i, line))
298 .collect::<Vec<_>>()
299 .join("\n");
300 }
301
302 if expr.starts_with("count(") {
304 let pattern = self.extract_string(expr).unwrap_or_default();
305 return self.count(&pattern).to_string();
306 }
307
308 if expr == "lines()" || expr == "lines" {
310 return format!("Lines: {}", self.context_lines.len());
311 }
312
313 if expr.starts_with("slice(") {
315 let nums = self.extract_numbers(expr);
316 if nums.len() >= 2 {
317 return self.slice(nums[0], nums[1]).to_string();
318 }
319 }
320
321 if expr.starts_with("chunks(") || expr.starts_with("chunk(") {
323 let n = self.extract_number(expr).unwrap_or(5);
324 let chunks = self.chunks(n);
325 return format!("[{} chunks of {} lines each]", chunks.len(),
326 chunks.first().map(|c| c.lines().count()).unwrap_or(0));
327 }
328
329 if expr == "context" || expr.starts_with("context.slice") || expr.starts_with("context[") {
331 return format!("[Context: {} chars, {} lines]", self.context.len(), self.context_lines.len());
332 }
333
334 if let Some(val) = self.get_var(expr) {
336 return val.to_string();
337 }
338
339 if (expr.starts_with('"') && expr.ends_with('"')) ||
341 (expr.starts_with('\'') && expr.ends_with('\'')) {
342 return expr[1..expr.len()-1].to_string();
343 }
344
345 expr.to_string()
346 }
347
348 fn extract_number(&self, expr: &str) -> Option<usize> {
349 let start = expr.find('(')?;
350 let end = expr.find(')')?;
351 let inner = expr[start + 1..end].trim();
352 inner.parse().ok()
353 }
354
355 fn extract_numbers(&self, expr: &str) -> Vec<usize> {
356 let start = expr.find('(').unwrap_or(0);
357 let end = expr.find(')').unwrap_or(expr.len());
358 let inner = &expr[start + 1..end];
359
360 inner.split(',')
361 .filter_map(|s| s.trim().parse().ok())
362 .collect()
363 }
364
365 fn extract_string(&self, expr: &str) -> Option<String> {
366 let start = expr.find('(')?;
367 let end = expr.rfind(')')?;
368 let inner = expr[start + 1..end].trim();
369
370 let unquoted = inner
372 .trim_start_matches(['"', '\'', '`', '/'])
373 .trim_end_matches(['"', '\'', '`', '/']);
374
375 Some(unquoted.to_string())
376 }
377}
378
379pub enum DslResult {
380 Output(String),
381 Final(String),
382 #[allow(dead_code)]
383 Error(String),
384}
385
386pub struct RlmExecutor {
394 repl: RlmRepl,
395 provider: Arc<dyn Provider>,
396 model: String,
397 max_iterations: usize,
398 sub_queries: Vec<SubQuery>,
399 verbose: bool,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct SubQuery {
405 pub query: String,
406 pub context_slice: Option<String>,
407 pub response: String,
408 pub tokens_used: usize,
409}
410
411impl RlmExecutor {
412 pub fn new(context: String, provider: Arc<dyn Provider>, model: String) -> Self {
414 Self {
415 repl: RlmRepl::new(context, ReplRuntime::Rust),
416 provider,
417 model,
418 max_iterations: 5, sub_queries: Vec::new(),
420 verbose: false,
421 }
422 }
423
424 pub fn with_max_iterations(mut self, max: usize) -> Self {
426 self.max_iterations = max;
427 self
428 }
429
430 pub fn with_verbose(mut self, verbose: bool) -> Self {
435 self.verbose = verbose;
436 self
437 }
438
439 pub async fn analyze(&mut self, query: &str) -> Result<RlmAnalysisResult> {
441 let start = std::time::Instant::now();
442 let mut iterations = 0;
443 let mut total_input_tokens = 0;
444 let mut total_output_tokens = 0;
445
446 let context_summary = format!(
448 "=== CONTEXT LOADED ===\n\
449 Total: {} chars, {} lines\n\
450 Available functions:\n\
451 - head(n) - first n lines\n\
452 - tail(n) - last n lines\n\
453 - grep(\"pattern\") - find lines matching regex\n\
454 - count(\"pattern\") - count regex matches\n\
455 - slice(start, end) - slice by char position\n\
456 - chunks(n) - split into n chunks\n\
457 - llm_query(\"question\", context?) - ask sub-LM a question\n\
458 - FINAL(\"answer\") - return final answer\n\
459 === END CONTEXT INFO ===",
460 self.repl.context().len(),
461 self.repl.lines().len()
462 );
463
464 if self.verbose {
466 tracing::info!("RLM Context Summary:\n{}", context_summary);
467 println!("[RLM] Context loaded: {} chars, {} lines",
468 self.repl.context().len(),
469 self.repl.lines().len()
470 );
471 }
472
473 let system_prompt = format!(
474 "You are a code analysis assistant. Answer questions by examining the provided context.\n\n\
475 IMPORTANT: You MUST end your response with FINAL(\"your answer\") in 1-3 iterations.\n\n\
476 Available commands:\n\
477 - head(n), tail(n): See first/last n lines\n\
478 - grep(\"pattern\"): Search for patterns\n\
479 - llm_query(\"question\"): Ask a focused sub-question\n\
480 - FINAL(\"answer\"): Return your final answer (REQUIRED)\n\n\
481 The context has {} chars across {} lines. A preview follows:\n\n\
482 {}\n\n\
483 Now analyze the context. Use 1-2 commands if needed, then call FINAL() with your answer.",
484 self.repl.context().len(),
485 self.repl.lines().len(),
486 self.repl.head(25).join("\n")
487 );
488
489 let mut messages = vec![
490 Message {
491 role: Role::System,
492 content: vec![ContentPart::Text { text: system_prompt }],
493 },
494 Message {
495 role: Role::User,
496 content: vec![ContentPart::Text {
497 text: format!("Analyze and answer: {}", query),
498 }],
499 },
500 ];
501
502 let mut final_answer = None;
503
504 while iterations < self.max_iterations {
505 iterations += 1;
506 tracing::info!("RLM iteration {}", iterations);
507
508 tracing::debug!("Sending LLM request...");
510 let response = match tokio::time::timeout(
511 std::time::Duration::from_secs(60),
512 self.provider.complete(CompletionRequest {
513 messages: messages.clone(),
514 tools: vec![],
515 model: self.model.clone(),
516 temperature: Some(0.3),
517 top_p: None,
518 max_tokens: Some(2000),
519 stop: vec![],
520 })
521 ).await {
522 Ok(Ok(r)) => {
523 tracing::debug!("LLM response received");
524 r
525 }
526 Ok(Err(e)) => return Err(e),
527 Err(_) => return Err(anyhow::anyhow!("LLM request timed out after 60 seconds")),
528 };
529
530 total_input_tokens += response.usage.prompt_tokens;
531 total_output_tokens += response.usage.completion_tokens;
532
533 let assistant_text = response.message.content.iter()
535 .filter_map(|p| match p {
536 ContentPart::Text { text } => Some(text.as_str()),
537 _ => None,
538 })
539 .collect::<Vec<_>>()
540 .join("");
541
542 messages.push(Message {
544 role: Role::Assistant,
545 content: vec![ContentPart::Text { text: assistant_text.clone() }],
546 });
547
548 let code = self.extract_code(&assistant_text);
550
551 if self.verbose {
553 println!("[RLM] Iteration {}: Executing code:\n{}", iterations, code);
554 }
555
556 let execution_result = self.execute_with_llm_query(&code).await?;
557
558 if self.verbose {
560 if let Some(ref answer) = execution_result.final_answer {
561 println!("[RLM] Final answer received: {}", answer);
562 } else if !execution_result.stdout.is_empty() {
563 let preview = if execution_result.stdout.len() > 200 {
564 format!("{}...", &execution_result.stdout[..200])
565 } else {
566 execution_result.stdout.clone()
567 };
568 println!("[RLM] Execution output:\n{}", preview);
569 }
570 }
571
572 if let Some(answer) = &execution_result.final_answer {
574 final_answer = Some(answer.clone());
575 break;
576 }
577
578 let result_text = if execution_result.stdout.is_empty() {
580 "[No output]".to_string()
581 } else {
582 format!("Execution result:\n{}", execution_result.stdout)
583 };
584
585 messages.push(Message {
586 role: Role::User,
587 content: vec![ContentPart::Text { text: result_text }],
588 });
589 }
590
591 let elapsed = start.elapsed();
592
593 Ok(RlmAnalysisResult {
594 answer: final_answer.unwrap_or_else(|| "Analysis incomplete".to_string()),
595 iterations,
596 sub_queries: self.sub_queries.clone(),
597 stats: super::RlmStats {
598 input_tokens: total_input_tokens,
599 output_tokens: total_output_tokens,
600 iterations,
601 subcalls: self.sub_queries.len(),
602 elapsed_ms: elapsed.as_millis() as u64,
603 compression_ratio: 1.0,
604 },
605 })
606 }
607
608 fn extract_code(&self, text: &str) -> String {
610 let mut code_lines = Vec::new();
612 let mut in_code_block = false;
613
614 for line in text.lines() {
615 if line.starts_with("```") {
616 in_code_block = !in_code_block;
617 continue;
618 }
619 if in_code_block {
620 code_lines.push(line);
621 }
622 }
623
624 if !code_lines.is_empty() {
625 return code_lines.join("\n");
626 }
627
628 text.lines()
630 .filter(|line| {
631 let l = line.trim();
632 l.starts_with("head(") || l.starts_with("tail(") ||
633 l.starts_with("grep(") || l.starts_with("count(") ||
634 l.starts_with("llm_query(") || l.starts_with("FINAL(") ||
635 l.starts_with("let ") || l.starts_with("const ") ||
636 l.starts_with("print") || l.starts_with("console.")
637 })
638 .collect::<Vec<_>>()
639 .join("\n")
640 }
641
642 async fn execute_with_llm_query(&mut self, code: &str) -> Result<ReplResult> {
644 let mut stdout = Vec::new();
645 let mut final_answer = None;
646
647 for line in code.lines() {
648 let line = line.trim();
649 if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
650 continue;
651 }
652
653 if line.starts_with("llm_query(") || line.contains("= llm_query(") {
655 let result = self.handle_llm_query(line).await?;
656 stdout.push(result);
657 continue;
658 }
659
660 if let Some(result) = self.repl.execute_dsl_line(line) {
662 match result {
663 DslResult::Output(s) => stdout.push(s),
664 DslResult::Final(s) => {
665 final_answer = Some(s);
666 break;
667 }
668 DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
669 }
670 }
671 }
672
673 Ok(ReplResult {
674 stdout: stdout.join("\n"),
675 stderr: String::new(),
676 final_answer,
677 })
678 }
679
680 async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
682 let (query, context_slice) = self.parse_llm_query(line);
684
685 let context_to_analyze = context_slice.clone()
687 .unwrap_or_else(|| self.repl.context().to_string());
688
689 let truncated_context = if context_to_analyze.len() > 8000 {
691 format!(
692 "{}...\n[truncated, {} chars total]",
693 &context_to_analyze[..7500],
694 context_to_analyze.len()
695 )
696 } else {
697 context_to_analyze.clone()
698 };
699
700 let messages = vec![
702 Message {
703 role: Role::System,
704 content: vec![ContentPart::Text {
705 text: "You are a focused analysis assistant. Answer the question based on the provided context. Be concise.".to_string(),
706 }],
707 },
708 Message {
709 role: Role::User,
710 content: vec![ContentPart::Text {
711 text: format!("Context:\n{}\n\nQuestion: {}", truncated_context, query),
712 }],
713 },
714 ];
715
716 let response = self.provider.complete(CompletionRequest {
717 messages,
718 tools: vec![],
719 model: self.model.clone(),
720 temperature: Some(0.3),
721 top_p: None,
722 max_tokens: Some(500),
723 stop: vec![],
724 }).await?;
725
726 let answer = response.message.content.iter()
727 .filter_map(|p| match p {
728 ContentPart::Text { text } => Some(text.as_str()),
729 _ => None,
730 })
731 .collect::<Vec<_>>()
732 .join("");
733
734 self.sub_queries.push(SubQuery {
736 query: query.clone(),
737 context_slice,
738 response: answer.clone(),
739 tokens_used: response.usage.total_tokens,
740 });
741
742 Ok(format!("llm_query result: {}", answer))
743 }
744
745 fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
747 let start = line.find('(').unwrap_or(0) + 1;
749 let end = line.rfind(')').unwrap_or(line.len());
750 let args = &line[start..end];
751
752 let mut query = String::new();
754 let mut context = None;
755 let mut in_quotes = false;
756 let mut current = String::new();
757 let mut parts = Vec::new();
758
759 for c in args.chars() {
760 if c == '"' || c == '\'' {
761 in_quotes = !in_quotes;
762 } else if c == ',' && !in_quotes {
763 parts.push(current.trim().to_string());
764 current = String::new();
765 continue;
766 }
767 current.push(c);
768 }
769 if !current.is_empty() {
770 parts.push(current.trim().to_string());
771 }
772
773 if let Some(q) = parts.first() {
775 query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
776 }
777
778 if let Some(ctx_expr) = parts.get(1) {
780 let ctx = self.repl.evaluate_expression(ctx_expr);
782 if !ctx.is_empty() && !ctx.starts_with('[') {
783 context = Some(ctx);
784 }
785 }
786
787 (query, context)
788 }
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct RlmAnalysisResult {
794 pub answer: String,
795 pub iterations: usize,
796 pub sub_queries: Vec<SubQuery>,
797 pub stats: super::RlmStats,
798}
799
800pub struct ExternalRepl {
802 child: Child,
803 #[allow(dead_code)]
804 runtime: ReplRuntime,
805}
806
807impl ExternalRepl {
808 pub async fn spawn_bun(context: &str) -> Result<Self> {
810 let init_script = Self::generate_bun_init(context);
811
812 let temp_dir = std::env::temp_dir().join("rlm-repl");
814 tokio::fs::create_dir_all(&temp_dir).await?;
815 let script_path = temp_dir.join(format!("init_{}.js", std::process::id()));
816 tokio::fs::write(&script_path, init_script).await?;
817
818 let runtime = if Self::is_bun_available().await { "bun" } else { "node" };
820
821 let child = Command::new(runtime)
822 .arg(&script_path)
823 .stdin(Stdio::piped())
824 .stdout(Stdio::piped())
825 .stderr(Stdio::piped())
826 .spawn()?;
827
828 Ok(Self {
829 child,
830 runtime: ReplRuntime::Bun,
831 })
832 }
833
834 async fn is_bun_available() -> bool {
835 Command::new("bun")
836 .arg("--version")
837 .output()
838 .await
839 .map(|o| o.status.success())
840 .unwrap_or(false)
841 }
842
843 fn generate_bun_init(context: &str) -> String {
844 let escaped = context.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
845
846 format!(r#"
847const readline = require('readline');
848const rl = readline.createInterface({{ input: process.stdin, output: process.stdout, terminal: false }});
849
850const context = "{escaped}";
851
852function lines() {{ return context.split("\n"); }}
853function head(n = 10) {{ return lines().slice(0, n).join("\n"); }}
854function tail(n = 10) {{ return lines().slice(-n).join("\n"); }}
855function grep(pattern) {{
856 const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
857 return lines().filter(l => re.test(l));
858}}
859function count(pattern) {{
860 const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
861 return (context.match(re) || []).length;
862}}
863function FINAL(answer) {{
864 console.log("__FINAL__" + String(answer) + "__FINAL_END__");
865}}
866
867console.log("READY");
868
869rl.on('line', async (line) => {{
870 try {{
871 const result = eval(line);
872 if (result !== undefined) console.log(result);
873 }} catch (e) {{
874 console.error("Error:", e.message);
875 }}
876 console.log("__DONE__");
877}});
878"#)
879 }
880
881 pub async fn execute(&mut self, code: &str) -> Result<ReplResult> {
883 let stdin = self.child.stdin.as_mut().ok_or_else(|| anyhow::anyhow!("No stdin"))?;
884 let stdout = self.child.stdout.as_mut().ok_or_else(|| anyhow::anyhow!("No stdout"))?;
885
886 stdin.write_all(code.as_bytes()).await?;
887 stdin.write_all(b"\n").await?;
888 stdin.flush().await?;
889
890 let mut reader = BufReader::new(stdout);
891 let mut output = Vec::new();
892 let mut final_answer = None;
893
894 loop {
895 let mut line = String::new();
896 match timeout(Duration::from_secs(30), reader.read_line(&mut line)).await {
897 Ok(Ok(0)) | Err(_) => break, Ok(Ok(_)) => {
899 let line = line.trim();
900 if line == "__DONE__" {
901 break;
902 }
903 if let Some(answer) = Self::extract_final(line) {
904 final_answer = Some(answer);
905 break;
906 }
907 output.push(line.to_string());
908 }
909 Ok(Err(e)) => return Err(anyhow::anyhow!("Read error: {}", e)),
910 }
911 }
912
913 Ok(ReplResult {
914 stdout: output.join("\n"),
915 stderr: String::new(),
916 final_answer,
917 })
918 }
919
920 fn extract_final(line: &str) -> Option<String> {
921 if line.contains("__FINAL__") {
922 let start = line.find("__FINAL__")? + 9;
923 let end = line.find("__FINAL_END__")?;
924 return Some(line[start..end].to_string());
925 }
926 None
927 }
928
929 pub async fn destroy(&mut self) -> Result<()> {
931 tracing::debug!(runtime = ?self.runtime, "Destroying external REPL");
932 self.child.kill().await?;
933 Ok(())
934 }
935
936 pub fn runtime(&self) -> ReplRuntime {
938 self.runtime
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945
946 #[test]
947 fn test_repl_head_tail() {
948 let context = (1..=100).map(|i| format!("line {}", i)).collect::<Vec<_>>().join("\n");
949 let repl = RlmRepl::new(context, ReplRuntime::Rust);
950
951 let head = repl.head(5);
952 assert_eq!(head.len(), 5);
953 assert_eq!(head[0], "line 1");
954
955 let tail = repl.tail(5);
956 assert_eq!(tail.len(), 5);
957 assert_eq!(tail[4], "line 100");
958 }
959
960 #[test]
961 fn test_repl_grep() {
962 let context = "error: something failed\ninfo: all good\nerror: another failure".to_string();
963 let repl = RlmRepl::new(context, ReplRuntime::Rust);
964
965 let matches = repl.grep("error");
966 assert_eq!(matches.len(), 2);
967 }
968
969 #[test]
970 fn test_repl_execute_final() {
971 let context = "test content".to_string();
972 let mut repl = RlmRepl::new(context, ReplRuntime::Rust);
973
974 let result = repl.execute(r#"FINAL("This is the answer")"#);
975 assert_eq!(result.final_answer, Some("This is the answer".to_string()));
976 }
977
978 #[test]
979 fn test_repl_chunks() {
980 let context = (1..=100).map(|i| format!("line {}", i)).collect::<Vec<_>>().join("\n");
981 let repl = RlmRepl::new(context, ReplRuntime::Rust);
982
983 let chunks = repl.chunks(5);
984 assert_eq!(chunks.len(), 5);
985 }
986}