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::{CompletionRequest, ContentPart, Message, Provider, Role};
19
20/// REPL runtime options
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22#[serde(rename_all = "lowercase")]
23pub enum ReplRuntime {
24    /// Native Rust REPL (fastest, uses rhai scripting)
25    #[default]
26    Rust,
27    /// Bun/Node.js JavaScript REPL
28    Bun,
29    /// Python REPL
30    Python,
31}
32
33/// REPL instance for RLM processing
34pub struct RlmRepl {
35    runtime: ReplRuntime,
36    context: String,
37    context_lines: Vec<String>,
38    variables: HashMap<String, String>,
39}
40
41/// Result of REPL execution
42#[derive(Debug, Clone)]
43pub struct ReplResult {
44    pub stdout: String,
45    pub stderr: String,
46    pub final_answer: Option<String>,
47}
48
49impl RlmRepl {
50    /// Create a new REPL with the given context
51    pub fn new(context: String, runtime: ReplRuntime) -> Self {
52        let context_lines = context.lines().map(|s| s.to_string()).collect();
53        Self {
54            runtime,
55            context,
56            context_lines,
57            variables: HashMap::new(),
58        }
59    }
60
61    /// Get the context
62    pub fn context(&self) -> &str {
63        &self.context
64    }
65
66    /// Get context as lines
67    pub fn lines(&self) -> &[String] {
68        &self.context_lines
69    }
70
71    /// Get first n lines
72    pub fn head(&self, n: usize) -> Vec<&str> {
73        self.context_lines
74            .iter()
75            .take(n)
76            .map(|s| s.as_str())
77            .collect()
78    }
79
80    /// Get last n lines
81    pub fn tail(&self, n: usize) -> Vec<&str> {
82        let start = self.context_lines.len().saturating_sub(n);
83        self.context_lines
84            .iter()
85            .skip(start)
86            .map(|s| s.as_str())
87            .collect()
88    }
89
90    /// Search for lines matching a pattern
91    pub fn grep(&self, pattern: &str) -> Vec<(usize, &str)> {
92        let re = match regex::Regex::new(pattern) {
93            Ok(r) => r,
94            Err(_) => {
95                // Fall back to simple contains
96                return self
97                    .context_lines
98                    .iter()
99                    .enumerate()
100                    .filter(|(_, line)| line.contains(pattern))
101                    .map(|(i, line)| (i + 1, line.as_str()))
102                    .collect();
103            }
104        };
105
106        self.context_lines
107            .iter()
108            .enumerate()
109            .filter(|(_, line)| re.is_match(line))
110            .map(|(i, line)| (i + 1, line.as_str()))
111            .collect()
112    }
113
114    /// Count occurrences of a pattern
115    pub fn count(&self, pattern: &str) -> usize {
116        let re = match regex::Regex::new(pattern) {
117            Ok(r) => r,
118            Err(_) => return self.context.matches(pattern).count(),
119        };
120        re.find_iter(&self.context).count()
121    }
122
123    /// Slice context by character positions
124    pub fn slice(&self, start: usize, end: usize) -> &str {
125        let total_chars = self.context.chars().count();
126        let end = end.min(total_chars);
127        let start = start.min(end);
128        let start_byte = char_index_to_byte_index(&self.context, start);
129        let end_byte = char_index_to_byte_index(&self.context, end);
130        &self.context[start_byte..end_byte]
131    }
132
133    /// Split context into n chunks
134    pub fn chunks(&self, n: usize) -> Vec<String> {
135        if n == 0 {
136            return vec![self.context.clone()];
137        }
138
139        let chunk_size = self.context_lines.len().div_ceil(n);
140        self.context_lines
141            .chunks(chunk_size)
142            .map(|chunk| chunk.join("\n"))
143            .collect()
144    }
145
146    /// Set a variable
147    pub fn set_var(&mut self, name: &str, value: String) {
148        self.variables.insert(name.to_string(), value);
149    }
150
151    /// Get a variable
152    pub fn get_var(&self, name: &str) -> Option<&str> {
153        self.variables.get(name).map(|s| s.as_str())
154    }
155
156    /// Execute analysis code (interpreted based on runtime)
157    ///
158    /// For Rust runtime, this uses a simple DSL:
159    /// - head(n) - first n lines
160    /// - tail(n) - last n lines
161    /// - grep("pattern") - search for pattern
162    /// - count("pattern") - count matches
163    /// - slice(start, end) - slice by chars
164    /// - chunks(n) - split into n chunks
165    /// - FINAL("answer") - return final answer
166    pub fn execute(&mut self, code: &str) -> ReplResult {
167        match self.runtime {
168            ReplRuntime::Rust => self.execute_rust_dsl(code),
169            ReplRuntime::Bun | ReplRuntime::Python => {
170                // For external runtimes, we'd spawn processes
171                // For now, fall back to DSL
172                self.execute_rust_dsl(code)
173            }
174        }
175    }
176
177    fn execute_rust_dsl(&mut self, code: &str) -> ReplResult {
178        let mut stdout = Vec::new();
179        let mut final_answer = None;
180
181        for line in code.lines() {
182            let line = line.trim();
183            if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
184                continue;
185            }
186
187            // Parse and execute commands
188            if let Some(result) = self.execute_dsl_line(line) {
189                match result {
190                    DslResult::Output(s) => stdout.push(s),
191                    DslResult::Final(s) => {
192                        final_answer = Some(s);
193                        break;
194                    }
195                    DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
196                }
197            }
198        }
199
200        ReplResult {
201            stdout: stdout.join("\n"),
202            stderr: String::new(),
203            final_answer,
204        }
205    }
206
207    pub fn execute_dsl_line(&mut self, line: &str) -> Option<DslResult> {
208        // Check for FINAL
209        if line.starts_with("FINAL(") || line.starts_with("FINAL!(") {
210            let start = line.find('(').unwrap() + 1;
211            let end = line.rfind(')').unwrap_or(line.len());
212            let answer = line[start..end]
213                .trim()
214                .trim_matches(|c| c == '"' || c == '\'' || c == '`');
215            return Some(DslResult::Final(answer.to_string()));
216        }
217
218        // Check for print/console.log
219        if line.starts_with("print(")
220            || line.starts_with("println!(")
221            || line.starts_with("console.log(")
222        {
223            let start = line.find('(').unwrap() + 1;
224            let end = line.rfind(')').unwrap_or(line.len());
225            let content = line[start..end]
226                .trim()
227                .trim_matches(|c| c == '"' || c == '\'' || c == '`');
228
229            // Expand variables
230            let expanded = self.expand_expression(content);
231            return Some(DslResult::Output(expanded));
232        }
233
234        // Check for variable assignment
235        if let Some(eq_pos) = line.find('=') {
236            if !line.contains("==") && !line.starts_with("if ") {
237                let var_name = line[..eq_pos]
238                    .trim()
239                    .trim_start_matches("let ")
240                    .trim_start_matches("const ")
241                    .trim_start_matches("var ")
242                    .trim();
243                let expr = line[eq_pos + 1..].trim().trim_end_matches(';');
244
245                let value = self.evaluate_expression(expr);
246                self.set_var(var_name, value);
247                return None;
248            }
249        }
250
251        // Check for function calls that should output
252        if line.starts_with("head(")
253            || line.starts_with("tail(")
254            || line.starts_with("grep(")
255            || line.starts_with("count(")
256            || line.starts_with("lines()")
257            || line.starts_with("slice(")
258            || line.starts_with("chunks(")
259            || line.starts_with("context")
260        {
261            let result = self.evaluate_expression(line);
262            return Some(DslResult::Output(result));
263        }
264
265        None
266    }
267
268    fn expand_expression(&self, expr: &str) -> String {
269        // Simple variable expansion
270        let mut result = expr.to_string();
271
272        for (name, value) in &self.variables {
273            let patterns = [
274                format!("${{{}}}", name),
275                format!("${}", name),
276                format!("{{{}}}", name),
277            ];
278            for p in patterns {
279                result = result.replace(&p, value);
280            }
281        }
282
283        // Evaluate embedded expressions
284        if result.contains("context.len()") || result.contains("context.length") {
285            result = result
286                .replace("context.len()", &self.context.len().to_string())
287                .replace("context.length", &self.context.len().to_string());
288        }
289
290        if result.contains("lines().len()") || result.contains("lines().length") {
291            result = result
292                .replace("lines().len()", &self.context_lines.len().to_string())
293                .replace("lines().length", &self.context_lines.len().to_string());
294        }
295
296        result
297    }
298
299    pub fn evaluate_expression(&mut self, expr: &str) -> String {
300        let expr = expr.trim().trim_end_matches(';');
301
302        // head(n)
303        if expr.starts_with("head(") {
304            let n = self.extract_number(expr).unwrap_or(10);
305            return self.head(n).join("\n");
306        }
307
308        // tail(n)
309        if expr.starts_with("tail(") {
310            let n = self.extract_number(expr).unwrap_or(10);
311            return self.tail(n).join("\n");
312        }
313
314        // grep("pattern")
315        if expr.starts_with("grep(") {
316            let pattern = self.extract_string(expr).unwrap_or_default();
317            let matches = self.grep(&pattern);
318            return matches
319                .iter()
320                .map(|(i, line)| format!("{}:{}", i, line))
321                .collect::<Vec<_>>()
322                .join("\n");
323        }
324
325        // count("pattern")
326        if expr.starts_with("count(") {
327            let pattern = self.extract_string(expr).unwrap_or_default();
328            return self.count(&pattern).to_string();
329        }
330
331        // lines()
332        if expr == "lines()" || expr == "lines" {
333            return format!("Lines: {}", self.context_lines.len());
334        }
335
336        // slice(start, end)
337        if expr.starts_with("slice(") {
338            let nums = self.extract_numbers(expr);
339            if nums.len() >= 2 {
340                return self.slice(nums[0], nums[1]).to_string();
341            }
342        }
343
344        // chunks(n)
345        if expr.starts_with("chunks(") || expr.starts_with("chunk(") {
346            let n = self.extract_number(expr).unwrap_or(5);
347            let chunks = self.chunks(n);
348            return format!(
349                "[{} chunks of {} lines each]",
350                chunks.len(),
351                chunks.first().map(|c| c.lines().count()).unwrap_or(0)
352            );
353        }
354
355        // context
356        if expr == "context" || expr.starts_with("context.slice") || expr.starts_with("context[") {
357            return format!(
358                "[Context: {} chars, {} lines]",
359                self.context.len(),
360                self.context_lines.len()
361            );
362        }
363
364        // Variable reference
365        if let Some(val) = self.get_var(expr) {
366            return val.to_string();
367        }
368
369        // String literal
370        if (expr.starts_with('"') && expr.ends_with('"'))
371            || (expr.starts_with('\'') && expr.ends_with('\''))
372        {
373            let mut chars = expr.chars();
374            let _ = chars.next();
375            let _ = chars.next_back();
376            return chars.collect();
377        }
378
379        expr.to_string()
380    }
381
382    fn extract_number(&self, expr: &str) -> Option<usize> {
383        let start = expr.find('(')?;
384        let end = expr.find(')')?;
385        let inner = expr[start + 1..end].trim();
386        inner.parse().ok()
387    }
388
389    fn extract_numbers(&self, expr: &str) -> Vec<usize> {
390        let start = expr.find('(').unwrap_or(0);
391        let end = expr.find(')').unwrap_or(expr.len());
392        let inner = &expr[start + 1..end];
393
394        inner
395            .split(',')
396            .filter_map(|s| s.trim().parse().ok())
397            .collect()
398    }
399
400    fn extract_string(&self, expr: &str) -> Option<String> {
401        let start = expr.find('(')?;
402        let end = expr.rfind(')')?;
403        let inner = expr[start + 1..end].trim();
404
405        // Remove quotes
406        let unquoted = inner
407            .trim_start_matches(['"', '\'', '`', '/'])
408            .trim_end_matches(['"', '\'', '`', '/']);
409
410        Some(unquoted.to_string())
411    }
412}
413
414pub enum DslResult {
415    Output(String),
416    Final(String),
417    #[allow(dead_code)]
418    Error(String),
419}
420
421/// LLM-powered RLM executor
422///
423/// This is the main entry point for RLM processing. It:
424/// 1. Loads context into a REPL environment
425/// 2. Lets the LLM write analysis code
426/// 3. Executes the code and provides llm_query() for semantic sub-calls
427/// 4. Iterates until the LLM returns a FINAL answer
428pub struct RlmExecutor {
429    repl: RlmRepl,
430    provider: Arc<dyn Provider>,
431    model: String,
432    max_iterations: usize,
433    sub_queries: Vec<SubQuery>,
434    verbose: bool,
435}
436
437/// Record of a sub-LM call
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct SubQuery {
440    pub query: String,
441    pub context_slice: Option<String>,
442    pub response: String,
443    pub tokens_used: usize,
444}
445
446impl RlmExecutor {
447    /// Create a new RLM executor
448    pub fn new(context: String, provider: Arc<dyn Provider>, model: String) -> Self {
449        Self {
450            repl: RlmRepl::new(context, ReplRuntime::Rust),
451            provider,
452            model,
453            max_iterations: 5, // Keep iterations limited for speed
454            sub_queries: Vec::new(),
455            verbose: false,
456        }
457    }
458
459    /// Set maximum iterations
460    pub fn with_max_iterations(mut self, max: usize) -> Self {
461        self.max_iterations = max;
462        self
463    }
464
465    /// Enable or disable verbose mode
466    ///
467    /// When verbose is true, the context summary will be displayed
468    /// at the start of analysis to help users understand what's being analyzed.
469    pub fn with_verbose(mut self, verbose: bool) -> Self {
470        self.verbose = verbose;
471        self
472    }
473
474    /// Execute RLM analysis with the given query
475    pub async fn analyze(&mut self, query: &str) -> Result<RlmAnalysisResult> {
476        let start = std::time::Instant::now();
477        let mut iterations = 0;
478        let mut total_input_tokens = 0;
479        let mut total_output_tokens = 0;
480
481        // Build and optionally display context summary
482        let context_summary = format!(
483            "=== CONTEXT LOADED ===\n\
484             Total: {} chars, {} lines\n\
485             Available functions:\n\
486             - head(n) - first n lines\n\
487             - tail(n) - last n lines\n\
488             - grep(\"pattern\") - find lines matching regex\n\
489             - count(\"pattern\") - count regex matches\n\
490             - slice(start, end) - slice by char position\n\
491             - chunks(n) - split into n chunks\n\
492             - llm_query(\"question\", context?) - ask sub-LM a question\n\
493             - FINAL(\"answer\") - return final answer\n\
494             === END CONTEXT INFO ===",
495            self.repl.context().len(),
496            self.repl.lines().len()
497        );
498
499        // Display context summary at the start in verbose mode
500        if self.verbose {
501            tracing::info!("RLM Context Summary:\n{}", context_summary);
502            println!(
503                "[RLM] Context loaded: {} chars, {} lines",
504                self.repl.context().len(),
505                self.repl.lines().len()
506            );
507        }
508
509        let system_prompt = format!(
510            "You are a code analysis assistant. Answer questions by examining the provided context.\n\n\
511             IMPORTANT: You MUST end your response with FINAL(\"your answer\") in 1-3 iterations.\n\n\
512             Available commands:\n\
513             - head(n), tail(n): See first/last n lines\n\
514             - grep(\"pattern\"): Search for patterns\n\
515             - llm_query(\"question\"): Ask a focused sub-question\n\
516             - FINAL(\"answer\"): Return your final answer (REQUIRED)\n\n\
517             The context has {} chars across {} lines. A preview follows:\n\n\
518             {}\n\n\
519             Now analyze the context. Use 1-2 commands if needed, then call FINAL() with your answer.",
520            self.repl.context().len(),
521            self.repl.lines().len(),
522            self.repl.head(25).join("\n")
523        );
524
525        let mut messages = vec![
526            Message {
527                role: Role::System,
528                content: vec![ContentPart::Text {
529                    text: system_prompt,
530                }],
531            },
532            Message {
533                role: Role::User,
534                content: vec![ContentPart::Text {
535                    text: format!("Analyze and answer: {}", query),
536                }],
537            },
538        ];
539
540        let mut final_answer = None;
541
542        while iterations < self.max_iterations {
543            iterations += 1;
544            tracing::info!("RLM iteration {}", iterations);
545
546            // Get LLM response with code to execute (with timeout)
547            tracing::debug!("Sending LLM request...");
548            let response = match tokio::time::timeout(
549                std::time::Duration::from_secs(60),
550                self.provider.complete(CompletionRequest {
551                    messages: messages.clone(),
552                    tools: vec![],
553                    model: self.model.clone(),
554                    temperature: Some(0.3),
555                    top_p: None,
556                    max_tokens: Some(2000),
557                    stop: vec![],
558                }),
559            )
560            .await
561            {
562                Ok(Ok(r)) => {
563                    tracing::debug!("LLM response received");
564                    r
565                }
566                Ok(Err(e)) => return Err(e),
567                Err(_) => return Err(anyhow::anyhow!("LLM request timed out after 60 seconds")),
568            };
569
570            total_input_tokens += response.usage.prompt_tokens;
571            total_output_tokens += response.usage.completion_tokens;
572
573            // Extract code from response
574            let assistant_text = response
575                .message
576                .content
577                .iter()
578                .filter_map(|p| match p {
579                    ContentPart::Text { text } => Some(text.as_str()),
580                    _ => None,
581                })
582                .collect::<Vec<_>>()
583                .join("");
584
585            // Add assistant message
586            messages.push(Message {
587                role: Role::Assistant,
588                content: vec![ContentPart::Text {
589                    text: assistant_text.clone(),
590                }],
591            });
592
593            // Extract and execute code blocks
594            let code = self.extract_code(&assistant_text);
595
596            // Display execution details in verbose mode
597            if self.verbose {
598                println!("[RLM] Iteration {}: Executing code:\n{}", iterations, code);
599            }
600
601            let execution_result = self.execute_with_llm_query(&code).await?;
602
603            // Display execution results in verbose mode
604            if self.verbose {
605                if let Some(ref answer) = execution_result.final_answer {
606                    println!("[RLM] Final answer received: {}", answer);
607                } else if !execution_result.stdout.is_empty() {
608                    let preview = truncate_with_ellipsis(&execution_result.stdout, 200);
609                    println!("[RLM] Execution output:\n{}", preview);
610                }
611            }
612
613            // Check for final answer
614            if let Some(answer) = &execution_result.final_answer {
615                final_answer = Some(answer.clone());
616                break;
617            }
618
619            // Add execution result as user message for next iteration
620            let result_text = if execution_result.stdout.is_empty() {
621                "[No output]".to_string()
622            } else {
623                format!("Execution result:\n{}", execution_result.stdout)
624            };
625
626            messages.push(Message {
627                role: Role::User,
628                content: vec![ContentPart::Text { text: result_text }],
629            });
630        }
631
632        let elapsed = start.elapsed();
633
634        Ok(RlmAnalysisResult {
635            answer: final_answer.unwrap_or_else(|| "Analysis incomplete".to_string()),
636            iterations,
637            sub_queries: self.sub_queries.clone(),
638            stats: super::RlmStats {
639                input_tokens: total_input_tokens,
640                output_tokens: total_output_tokens,
641                iterations,
642                subcalls: self.sub_queries.len(),
643                elapsed_ms: elapsed.as_millis() as u64,
644                compression_ratio: 1.0,
645            },
646        })
647    }
648
649    /// Extract code from LLM response
650    fn extract_code(&self, text: &str) -> String {
651        // Look for fenced code blocks
652        let mut code_lines = Vec::new();
653        let mut in_code_block = false;
654
655        for line in text.lines() {
656            if line.starts_with("```") {
657                in_code_block = !in_code_block;
658                continue;
659            }
660            if in_code_block {
661                code_lines.push(line);
662            }
663        }
664
665        if !code_lines.is_empty() {
666            return code_lines.join("\n");
667        }
668
669        // If no code blocks, look for lines that look like code
670        text.lines()
671            .filter(|line| {
672                let l = line.trim();
673                l.starts_with("head(")
674                    || l.starts_with("tail(")
675                    || l.starts_with("grep(")
676                    || l.starts_with("count(")
677                    || l.starts_with("llm_query(")
678                    || l.starts_with("FINAL(")
679                    || l.starts_with("let ")
680                    || l.starts_with("const ")
681                    || l.starts_with("print")
682                    || l.starts_with("console.")
683            })
684            .collect::<Vec<_>>()
685            .join("\n")
686    }
687
688    /// Execute code with llm_query() support
689    async fn execute_with_llm_query(&mut self, code: &str) -> Result<ReplResult> {
690        let mut stdout = Vec::new();
691        let mut final_answer = None;
692
693        for line in code.lines() {
694            let line = line.trim();
695            if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
696                continue;
697            }
698
699            // Handle llm_query calls specially
700            if line.starts_with("llm_query(") || line.contains("= llm_query(") {
701                let result = self.handle_llm_query(line).await?;
702                stdout.push(result);
703                continue;
704            }
705
706            // Handle regular REPL commands
707            if let Some(result) = self.repl.execute_dsl_line(line) {
708                match result {
709                    DslResult::Output(s) => stdout.push(s),
710                    DslResult::Final(s) => {
711                        final_answer = Some(s);
712                        break;
713                    }
714                    DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
715                }
716            }
717        }
718
719        Ok(ReplResult {
720            stdout: stdout.join("\n"),
721            stderr: String::new(),
722            final_answer,
723        })
724    }
725
726    /// Handle llm_query() calls
727    async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
728        // Extract query and optional context slice
729        let (query, context_slice) = self.parse_llm_query(line);
730
731        // Get the context to send
732        let context_to_analyze = context_slice
733            .clone()
734            .unwrap_or_else(|| self.repl.context().to_string());
735
736        // Truncate context for sub-query to avoid overwhelming the LLM
737        let context_chars = context_to_analyze.chars().count();
738        let truncated_context = if context_chars > 8000 {
739            format!(
740                "{}\n[truncated, {} chars total]",
741                truncate_with_ellipsis(&context_to_analyze, 7500),
742                context_chars
743            )
744        } else {
745            context_to_analyze.clone()
746        };
747
748        // Make sub-LM call
749        let messages = vec![
750            Message {
751                role: Role::System,
752                content: vec![ContentPart::Text {
753                    text: "You are a focused analysis assistant. Answer the question based on the provided context. Be concise.".to_string(),
754                }],
755            },
756            Message {
757                role: Role::User,
758                content: vec![ContentPart::Text {
759                    text: format!("Context:\n{}\n\nQuestion: {}", truncated_context, query),
760                }],
761            },
762        ];
763
764        let response = self
765            .provider
766            .complete(CompletionRequest {
767                messages,
768                tools: vec![],
769                model: self.model.clone(),
770                temperature: Some(0.3),
771                top_p: None,
772                max_tokens: Some(500),
773                stop: vec![],
774            })
775            .await?;
776
777        let answer = response
778            .message
779            .content
780            .iter()
781            .filter_map(|p| match p {
782                ContentPart::Text { text } => Some(text.as_str()),
783                _ => None,
784            })
785            .collect::<Vec<_>>()
786            .join("");
787
788        // Record the sub-query
789        self.sub_queries.push(SubQuery {
790            query: query.clone(),
791            context_slice,
792            response: answer.clone(),
793            tokens_used: response.usage.total_tokens,
794        });
795
796        Ok(format!("llm_query result: {}", answer))
797    }
798
799    /// Parse llm_query("question", context?) call
800    fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
801        // Find the query string
802        let start = line.find('(').unwrap_or(0) + 1;
803        let end = line.rfind(')').unwrap_or(line.len());
804        let args = &line[start..end];
805
806        // Split by comma, but respect quotes
807        let mut query = String::new();
808        let mut context = None;
809        let mut in_quotes = false;
810        let mut current = String::new();
811        let mut parts = Vec::new();
812
813        for c in args.chars() {
814            if c == '"' || c == '\'' {
815                in_quotes = !in_quotes;
816            } else if c == ',' && !in_quotes {
817                parts.push(current.trim().to_string());
818                current = String::new();
819                continue;
820            }
821            current.push(c);
822        }
823        if !current.is_empty() {
824            parts.push(current.trim().to_string());
825        }
826
827        // First part is the query
828        if let Some(q) = parts.first() {
829            query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
830        }
831
832        // Second part (if present) is context expression
833        if let Some(ctx_expr) = parts.get(1) {
834            // Evaluate the context expression
835            let ctx = self.repl.evaluate_expression(ctx_expr);
836            if !ctx.is_empty() && !ctx.starts_with('[') {
837                context = Some(ctx);
838            }
839        }
840
841        (query, context)
842    }
843}
844
845/// Result of RLM analysis
846#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct RlmAnalysisResult {
848    pub answer: String,
849    pub iterations: usize,
850    pub sub_queries: Vec<SubQuery>,
851    pub stats: super::RlmStats,
852}
853
854/// Spawn an external REPL process for Python or Bun
855pub struct ExternalRepl {
856    child: Child,
857    #[allow(dead_code)]
858    runtime: ReplRuntime,
859}
860
861impl ExternalRepl {
862    /// Create a Bun/Node.js REPL
863    pub async fn spawn_bun(context: &str) -> Result<Self> {
864        let init_script = Self::generate_bun_init(context);
865
866        // Write init script to temp file
867        let temp_dir = std::env::temp_dir().join("rlm-repl");
868        tokio::fs::create_dir_all(&temp_dir).await?;
869        let script_path = temp_dir.join(format!("init_{}.js", std::process::id()));
870        tokio::fs::write(&script_path, init_script).await?;
871
872        // Try bun first, fall back to node
873        let runtime = if Self::is_bun_available().await {
874            "bun"
875        } else {
876            "node"
877        };
878
879        let child = Command::new(runtime)
880            .arg(&script_path)
881            .stdin(Stdio::piped())
882            .stdout(Stdio::piped())
883            .stderr(Stdio::piped())
884            .spawn()?;
885
886        Ok(Self {
887            child,
888            runtime: ReplRuntime::Bun,
889        })
890    }
891
892    async fn is_bun_available() -> bool {
893        Command::new("bun")
894            .arg("--version")
895            .output()
896            .await
897            .map(|o| o.status.success())
898            .unwrap_or(false)
899    }
900
901    fn generate_bun_init(context: &str) -> String {
902        let escaped = context
903            .replace('\\', "\\\\")
904            .replace('"', "\\\"")
905            .replace('\n', "\\n");
906
907        format!(
908            r#"
909const readline = require('readline');
910const rl = readline.createInterface({{ input: process.stdin, output: process.stdout, terminal: false }});
911
912const context = "{escaped}";
913
914function lines() {{ return context.split("\n"); }}
915function head(n = 10) {{ return lines().slice(0, n).join("\n"); }}
916function tail(n = 10) {{ return lines().slice(-n).join("\n"); }}
917function grep(pattern) {{
918    const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
919    return lines().filter(l => re.test(l));
920}}
921function count(pattern) {{
922    const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
923    return (context.match(re) || []).length;
924}}
925function FINAL(answer) {{
926    console.log("__FINAL__" + String(answer) + "__FINAL_END__");
927}}
928
929console.log("READY");
930
931rl.on('line', async (line) => {{
932    try {{
933        const result = eval(line);
934        if (result !== undefined) console.log(result);
935    }} catch (e) {{
936        console.error("Error:", e.message);
937    }}
938    console.log("__DONE__");
939}});
940"#
941        )
942    }
943
944    /// Execute code and get result
945    pub async fn execute(&mut self, code: &str) -> Result<ReplResult> {
946        let stdin = self
947            .child
948            .stdin
949            .as_mut()
950            .ok_or_else(|| anyhow::anyhow!("No stdin"))?;
951        let stdout = self
952            .child
953            .stdout
954            .as_mut()
955            .ok_or_else(|| anyhow::anyhow!("No stdout"))?;
956
957        stdin.write_all(code.as_bytes()).await?;
958        stdin.write_all(b"\n").await?;
959        stdin.flush().await?;
960
961        let mut reader = BufReader::new(stdout);
962        let mut output = Vec::new();
963        let mut final_answer = None;
964
965        loop {
966            let mut line = String::new();
967            match timeout(Duration::from_secs(30), reader.read_line(&mut line)).await {
968                Ok(Ok(0)) | Err(_) => break, // EOF or timeout
969                Ok(Ok(_)) => {
970                    let line = line.trim();
971                    if line == "__DONE__" {
972                        break;
973                    }
974                    if let Some(answer) = Self::extract_final(line) {
975                        final_answer = Some(answer);
976                        break;
977                    }
978                    output.push(line.to_string());
979                }
980                Ok(Err(e)) => return Err(anyhow::anyhow!("Read error: {}", e)),
981            }
982        }
983
984        Ok(ReplResult {
985            stdout: output.join("\n"),
986            stderr: String::new(),
987            final_answer,
988        })
989    }
990
991    fn extract_final(line: &str) -> Option<String> {
992        if line.contains("__FINAL__") {
993            let start = line.find("__FINAL__")? + 9;
994            let end = line.find("__FINAL_END__")?;
995            return Some(line[start..end].to_string());
996        }
997        None
998    }
999
1000    /// Kill the REPL process
1001    pub async fn destroy(&mut self) -> Result<()> {
1002        tracing::debug!(runtime = ?self.runtime, "Destroying external REPL");
1003        self.child.kill().await?;
1004        Ok(())
1005    }
1006
1007    /// Get the runtime type used by this REPL
1008    pub fn runtime(&self) -> ReplRuntime {
1009        self.runtime
1010    }
1011}
1012
1013fn char_index_to_byte_index(value: &str, char_index: usize) -> usize {
1014    if char_index == 0 {
1015        return 0;
1016    }
1017
1018    value
1019        .char_indices()
1020        .nth(char_index)
1021        .map(|(idx, _)| idx)
1022        .unwrap_or(value.len())
1023}
1024
1025fn truncate_with_ellipsis(value: &str, max_chars: usize) -> String {
1026    if max_chars == 0 {
1027        return String::new();
1028    }
1029
1030    let mut chars = value.chars();
1031    let mut output = String::new();
1032    for _ in 0..max_chars {
1033        if let Some(ch) = chars.next() {
1034            output.push(ch);
1035        } else {
1036            return value.to_string();
1037        }
1038    }
1039
1040    if chars.next().is_some() {
1041        format!("{output}...")
1042    } else {
1043        output
1044    }
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049    use super::*;
1050
1051    #[test]
1052    fn test_repl_head_tail() {
1053        let context = (1..=100)
1054            .map(|i| format!("line {}", i))
1055            .collect::<Vec<_>>()
1056            .join("\n");
1057        let repl = RlmRepl::new(context, ReplRuntime::Rust);
1058
1059        let head = repl.head(5);
1060        assert_eq!(head.len(), 5);
1061        assert_eq!(head[0], "line 1");
1062
1063        let tail = repl.tail(5);
1064        assert_eq!(tail.len(), 5);
1065        assert_eq!(tail[4], "line 100");
1066    }
1067
1068    #[test]
1069    fn test_repl_grep() {
1070        let context = "error: something failed\ninfo: all good\nerror: another failure".to_string();
1071        let repl = RlmRepl::new(context, ReplRuntime::Rust);
1072
1073        let matches = repl.grep("error");
1074        assert_eq!(matches.len(), 2);
1075    }
1076
1077    #[test]
1078    fn test_repl_execute_final() {
1079        let context = "test content".to_string();
1080        let mut repl = RlmRepl::new(context, ReplRuntime::Rust);
1081
1082        let result = repl.execute(r#"FINAL("This is the answer")"#);
1083        assert_eq!(result.final_answer, Some("This is the answer".to_string()));
1084    }
1085
1086    #[test]
1087    fn test_repl_chunks() {
1088        let context = (1..=100)
1089            .map(|i| format!("line {}", i))
1090            .collect::<Vec<_>>()
1091            .join("\n");
1092        let repl = RlmRepl::new(context, ReplRuntime::Rust);
1093
1094        let chunks = repl.chunks(5);
1095        assert_eq!(chunks.len(), 5);
1096    }
1097}