1use anyhow::Result;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::process::Stdio;
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
15use tokio::process::{Child, Command};
16use tokio::time::timeout;
17
18use crate::provider::{CompletionRequest, ContentPart, Message, Provider, Role};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22#[serde(rename_all = "lowercase")]
23pub enum ReplRuntime {
24 #[default]
26 Rust,
27 Bun,
29 Python,
31}
32
33pub struct RlmRepl {
35 runtime: ReplRuntime,
36 context: String,
37 context_lines: Vec<String>,
38 variables: HashMap<String, String>,
39}
40
41#[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 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 pub fn context(&self) -> &str {
63 &self.context
64 }
65
66 pub fn lines(&self) -> &[String] {
68 &self.context_lines
69 }
70
71 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 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 pub fn grep(&self, pattern: &str) -> Vec<(usize, &str)> {
92 let re = match regex::Regex::new(pattern) {
93 Ok(r) => r,
94 Err(_) => {
95 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 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 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 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 pub fn set_var(&mut self, name: &str, value: String) {
148 self.variables.insert(name.to_string(), value);
149 }
150
151 pub fn get_var(&self, name: &str) -> Option<&str> {
153 self.variables.get(name).map(|s| s.as_str())
154 }
155
156 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 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 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 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 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 let expanded = self.expand_expression(content);
231 return Some(DslResult::Output(expanded));
232 }
233
234 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 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 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 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 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 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 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 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 if expr == "lines()" || expr == "lines" {
333 return format!("Lines: {}", self.context_lines.len());
334 }
335
336 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 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 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 if let Some(val) = self.get_var(expr) {
366 return val.to_string();
367 }
368
369 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 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
421pub 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#[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 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, sub_queries: Vec::new(),
455 verbose: false,
456 }
457 }
458
459 pub fn with_max_iterations(mut self, max: usize) -> Self {
461 self.max_iterations = max;
462 self
463 }
464
465 pub fn with_verbose(mut self, verbose: bool) -> Self {
470 self.verbose = verbose;
471 self
472 }
473
474 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 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 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 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 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 messages.push(Message {
587 role: Role::Assistant,
588 content: vec![ContentPart::Text {
589 text: assistant_text.clone(),
590 }],
591 });
592
593 let code = self.extract_code(&assistant_text);
595
596 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 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 if let Some(answer) = &execution_result.final_answer {
615 final_answer = Some(answer.clone());
616 break;
617 }
618
619 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 fn extract_code(&self, text: &str) -> String {
651 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 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 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 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 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 async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
728 let (query, context_slice) = self.parse_llm_query(line);
730
731 let context_to_analyze = context_slice
733 .clone()
734 .unwrap_or_else(|| self.repl.context().to_string());
735
736 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 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 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 fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
801 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 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 if let Some(q) = parts.first() {
829 query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
830 }
831
832 if let Some(ctx_expr) = parts.get(1) {
834 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#[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
854pub struct ExternalRepl {
856 child: Child,
857 #[allow(dead_code)]
858 runtime: ReplRuntime,
859}
860
861impl ExternalRepl {
862 pub async fn spawn_bun(context: &str) -> Result<Self> {
864 let init_script = Self::generate_bun_init(context);
865
866 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 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 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, 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 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 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}