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