Skip to main content

codetether_agent/rlm/
repl.rs

1//! RLM REPL - Execution environment for RLM processing
2//!
3//! Provides a REPL-like environment where context is loaded as a variable
4//! and the LLM can execute code to analyze it.
5//!
6//! Key feature: llm_query() function for recursive sub-LM calls.
7
8use 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/// REPL runtime options
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
24#[serde(rename_all = "lowercase")]
25pub enum ReplRuntime {
26    /// Native Rust REPL (fastest, uses rhai scripting)
27    #[default]
28    Rust,
29    /// Bun/Node.js JavaScript REPL
30    Bun,
31    /// Python REPL
32    Python,
33}
34
35/// REPL instance for RLM processing
36pub struct RlmRepl {
37    runtime: ReplRuntime,
38    context: String,
39    context_lines: Vec<String>,
40    variables: HashMap<String, String>,
41}
42
43/// Result of REPL execution
44#[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    /// Create a new REPL with the given context
53    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    /// Get the context
64    pub fn context(&self) -> &str {
65        &self.context
66    }
67
68    /// Get context as lines
69    pub fn lines(&self) -> &[String] {
70        &self.context_lines
71    }
72
73    /// Get first n lines
74    pub fn head(&self, n: usize) -> Vec<&str> {
75        self.context_lines.iter().take(n).map(|s| s.as_str()).collect()
76    }
77
78    /// Get last n lines
79    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    /// Search for lines matching a pattern
85    pub fn grep(&self, pattern: &str) -> Vec<(usize, &str)> {
86        let re = match regex::Regex::new(pattern) {
87            Ok(r) => r,
88            Err(_) => {
89                // Fall back to simple contains
90                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    /// Count occurrences of a pattern
108    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    /// Slice context by character positions
117    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    /// Split context into n chunks
124    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    /// Set a variable
137    pub fn set_var(&mut self, name: &str, value: String) {
138        self.variables.insert(name.to_string(), value);
139    }
140
141    /// Get a variable
142    pub fn get_var(&self, name: &str) -> Option<&str> {
143        self.variables.get(name).map(|s| s.as_str())
144    }
145
146    /// Execute analysis code (interpreted based on runtime)
147    /// 
148    /// For Rust runtime, this uses a simple DSL:
149    /// - head(n) - first n lines
150    /// - tail(n) - last n lines
151    /// - grep("pattern") - search for pattern
152    /// - count("pattern") - count matches
153    /// - slice(start, end) - slice by chars
154    /// - chunks(n) - split into n chunks
155    /// - FINAL("answer") - return final answer
156    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                // For external runtimes, we'd spawn processes
161                // For now, fall back to DSL
162                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            // Parse and execute commands
178            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        // Check for FINAL
199        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        // Check for print/console.log
207        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            // Expand variables
213            let expanded = self.expand_expression(content);
214            return Some(DslResult::Output(expanded));
215        }
216
217        // Check for variable assignment
218        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        // Check for function calls that should output
234        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        // Simple variable expansion
247        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        // Evaluate embedded expressions
261        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        // head(n)
280        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        // tail(n)
286        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        // grep("pattern")
292        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        // count("pattern")
303        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        // lines()
309        if expr == "lines()" || expr == "lines" {
310            return format!("Lines: {}", self.context_lines.len());
311        }
312
313        // slice(start, end)
314        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        // chunks(n)
322        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        // context
330        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        // Variable reference
335        if let Some(val) = self.get_var(expr) {
336            return val.to_string();
337        }
338
339        // String literal
340        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        // Remove quotes
371        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
386/// LLM-powered RLM executor
387/// 
388/// This is the main entry point for RLM processing. It:
389/// 1. Loads context into a REPL environment
390/// 2. Lets the LLM write analysis code
391/// 3. Executes the code and provides llm_query() for semantic sub-calls
392/// 4. Iterates until the LLM returns a FINAL answer
393pub 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/// Record of a sub-LM call
403#[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    /// Create a new RLM executor
413    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, // Keep iterations limited for speed
419            sub_queries: Vec::new(),
420            verbose: false,
421        }
422    }
423
424    /// Set maximum iterations
425    pub fn with_max_iterations(mut self, max: usize) -> Self {
426        self.max_iterations = max;
427        self
428    }
429
430    /// Enable or disable verbose mode
431    /// 
432    /// When verbose is true, the context summary will be displayed
433    /// at the start of analysis to help users understand what's being analyzed.
434    pub fn with_verbose(mut self, verbose: bool) -> Self {
435        self.verbose = verbose;
436        self
437    }
438
439    /// Execute RLM analysis with the given query
440    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        // Build and optionally display context summary
447        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        // Display context summary at the start in verbose mode
465        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            // Get LLM response with code to execute (with timeout)
509            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            // Extract code from response
534            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            // Add assistant message
543            messages.push(Message {
544                role: Role::Assistant,
545                content: vec![ContentPart::Text { text: assistant_text.clone() }],
546            });
547
548            // Extract and execute code blocks
549            let code = self.extract_code(&assistant_text);
550            
551            // Display execution details in verbose mode
552            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            // Display execution results in verbose mode
559            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            // Check for final answer
573            if let Some(answer) = &execution_result.final_answer {
574                final_answer = Some(answer.clone());
575                break;
576            }
577
578            // Add execution result as user message for next iteration
579            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    /// Extract code from LLM response
609    fn extract_code(&self, text: &str) -> String {
610        // Look for fenced code blocks
611        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        // If no code blocks, look for lines that look like code
629        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    /// Execute code with llm_query() support
643    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            // Handle llm_query calls specially
654            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            // Handle regular REPL commands
661            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    /// Handle llm_query() calls
681    async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
682        // Extract query and optional context slice
683        let (query, context_slice) = self.parse_llm_query(line);
684
685        // Get the context to send
686        let context_to_analyze = context_slice.clone()
687            .unwrap_or_else(|| self.repl.context().to_string());
688
689        // Truncate context for sub-query to avoid overwhelming the LLM
690        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        // Make sub-LM call
701        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        // Record the sub-query
735        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    /// Parse llm_query("question", context?) call
746    fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
747        // Find the query string
748        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        // Split by comma, but respect quotes
753        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        // First part is the query
774        if let Some(q) = parts.first() {
775            query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
776        }
777
778        // Second part (if present) is context expression
779        if let Some(ctx_expr) = parts.get(1) {
780            // Evaluate the context expression
781            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/// Result of RLM analysis
792#[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
800/// Spawn an external REPL process for Python or Bun
801pub struct ExternalRepl {
802    child: Child,
803    #[allow(dead_code)]
804    runtime: ReplRuntime,
805}
806
807impl ExternalRepl {
808    /// Create a Bun/Node.js REPL
809    pub async fn spawn_bun(context: &str) -> Result<Self> {
810        let init_script = Self::generate_bun_init(context);
811        
812        // Write init script to temp file
813        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        // Try bun first, fall back to node
819        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    /// Execute code and get result
882    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, // EOF or timeout
898                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    /// Kill the REPL process
930    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    /// Get the runtime type used by this REPL
937    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}