1use 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
25#[cfg(feature = "functiongemma")]
26use crate::cognition::tool_router::{ToolCallRouter, ToolRouterConfig};
27
28use super::tools::{RlmToolResult, dispatch_tool_call, rlm_tool_definitions};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
32#[serde(rename_all = "lowercase")]
33pub enum ReplRuntime {
34 #[default]
36 Rust,
37 Bun,
39 Python,
41}
42
43pub struct RlmRepl {
45 runtime: ReplRuntime,
46 context: String,
47 context_lines: Vec<String>,
48 variables: HashMap<String, String>,
49}
50
51#[derive(Debug, Clone)]
53pub struct ReplResult {
54 pub stdout: String,
55 pub stderr: String,
56 pub final_answer: Option<String>,
57}
58
59impl RlmRepl {
60 pub fn new(context: String, runtime: ReplRuntime) -> Self {
62 let context_lines = context.lines().map(|s| s.to_string()).collect();
63 Self {
64 runtime,
65 context,
66 context_lines,
67 variables: HashMap::new(),
68 }
69 }
70
71 pub fn context(&self) -> &str {
73 &self.context
74 }
75
76 pub fn lines(&self) -> &[String] {
78 &self.context_lines
79 }
80
81 pub fn head(&self, n: usize) -> Vec<&str> {
83 self.context_lines
84 .iter()
85 .take(n)
86 .map(|s| s.as_str())
87 .collect()
88 }
89
90 pub fn tail(&self, n: usize) -> Vec<&str> {
92 let start = self.context_lines.len().saturating_sub(n);
93 self.context_lines
94 .iter()
95 .skip(start)
96 .map(|s| s.as_str())
97 .collect()
98 }
99
100 pub fn grep(&self, pattern: &str) -> Vec<(usize, &str)> {
102 let re = match regex::Regex::new(pattern) {
103 Ok(r) => r,
104 Err(_) => {
105 return self
107 .context_lines
108 .iter()
109 .enumerate()
110 .filter(|(_, line)| line.contains(pattern))
111 .map(|(i, line)| (i + 1, line.as_str()))
112 .collect();
113 }
114 };
115
116 self.context_lines
117 .iter()
118 .enumerate()
119 .filter(|(_, line)| re.is_match(line))
120 .map(|(i, line)| (i + 1, line.as_str()))
121 .collect()
122 }
123
124 pub fn count(&self, pattern: &str) -> usize {
126 let re = match regex::Regex::new(pattern) {
127 Ok(r) => r,
128 Err(_) => return self.context.matches(pattern).count(),
129 };
130 re.find_iter(&self.context).count()
131 }
132
133 pub fn slice(&self, start: usize, end: usize) -> &str {
135 let total_chars = self.context.chars().count();
136 let end = end.min(total_chars);
137 let start = start.min(end);
138 let start_byte = char_index_to_byte_index(&self.context, start);
139 let end_byte = char_index_to_byte_index(&self.context, end);
140 &self.context[start_byte..end_byte]
141 }
142
143 pub fn chunks(&self, n: usize) -> Vec<String> {
145 if n == 0 {
146 return vec![self.context.clone()];
147 }
148
149 let chunk_size = self.context_lines.len().div_ceil(n);
150 self.context_lines
151 .chunks(chunk_size)
152 .map(|chunk| chunk.join("\n"))
153 .collect()
154 }
155
156 pub fn set_var(&mut self, name: &str, value: String) {
158 self.variables.insert(name.to_string(), value);
159 }
160
161 pub fn get_var(&self, name: &str) -> Option<&str> {
163 self.variables.get(name).map(|s| s.as_str())
164 }
165
166 pub fn execute(&mut self, code: &str) -> ReplResult {
177 match self.runtime {
178 ReplRuntime::Rust => self.execute_rust_dsl(code),
179 ReplRuntime::Bun | ReplRuntime::Python => {
180 self.execute_rust_dsl(code)
183 }
184 }
185 }
186
187 fn execute_rust_dsl(&mut self, code: &str) -> ReplResult {
188 let mut stdout = Vec::new();
189 let mut final_answer = None;
190
191 for line in code.lines() {
192 let line = line.trim();
193 if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
194 continue;
195 }
196
197 if let Some(result) = self.execute_dsl_line(line) {
199 match result {
200 DslResult::Output(s) => stdout.push(s),
201 DslResult::Final(s) => {
202 final_answer = Some(s);
203 break;
204 }
205 DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
206 }
207 }
208 }
209
210 ReplResult {
211 stdout: stdout.join("\n"),
212 stderr: String::new(),
213 final_answer,
214 }
215 }
216
217 pub fn execute_dsl_line(&mut self, line: &str) -> Option<DslResult> {
218 if line.starts_with("FINAL(") || line.starts_with("FINAL!(") {
220 let start = line.find('(').unwrap() + 1;
221 let end = line.rfind(')').unwrap_or(line.len());
222 let answer = line[start..end]
223 .trim()
224 .trim_matches(|c| c == '"' || c == '\'' || c == '`');
225 return Some(DslResult::Final(answer.to_string()));
226 }
227
228 if line.starts_with("print(")
230 || line.starts_with("println!(")
231 || line.starts_with("console.log(")
232 {
233 let start = line.find('(').unwrap() + 1;
234 let end = line.rfind(')').unwrap_or(line.len());
235 let content = line[start..end]
236 .trim()
237 .trim_matches(|c| c == '"' || c == '\'' || c == '`');
238
239 let expanded = self.expand_expression(content);
241 return Some(DslResult::Output(expanded));
242 }
243
244 if let Some(eq_pos) = line.find('=') {
246 if !line.contains("==") && !line.starts_with("if ") {
247 let var_name = line[..eq_pos]
248 .trim()
249 .trim_start_matches("let ")
250 .trim_start_matches("const ")
251 .trim_start_matches("var ")
252 .trim();
253 let expr = line[eq_pos + 1..].trim().trim_end_matches(';');
254
255 let value = self.evaluate_expression(expr);
256 self.set_var(var_name, value);
257 return None;
258 }
259 }
260
261 if line.starts_with("head(")
263 || line.starts_with("tail(")
264 || line.starts_with("grep(")
265 || line.starts_with("count(")
266 || line.starts_with("lines()")
267 || line.starts_with("slice(")
268 || line.starts_with("chunks(")
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 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 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 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 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 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 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 if expr == "lines()" || expr == "lines" {
343 return format!("Lines: {}", self.context_lines.len());
344 }
345
346 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 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 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 if let Some(val) = self.get_var(expr) {
376 return val.to_string();
377 }
378
379 if (expr.starts_with('"') && expr.ends_with('"'))
381 || (expr.starts_with('\'') && expr.ends_with('\''))
382 {
383 let mut chars = expr.chars();
384 let _ = chars.next();
385 let _ = chars.next_back();
386 return chars.collect();
387 }
388
389 expr.to_string()
390 }
391
392 fn extract_number(&self, expr: &str) -> Option<usize> {
393 let start = expr.find('(')?;
394 let end = expr.find(')')?;
395 let inner = expr[start + 1..end].trim();
396 inner.parse().ok()
397 }
398
399 fn extract_numbers(&self, expr: &str) -> Vec<usize> {
400 let start = expr.find('(').unwrap_or(0);
401 let end = expr.find(')').unwrap_or(expr.len());
402 let inner = &expr[start + 1..end];
403
404 inner
405 .split(',')
406 .filter_map(|s| s.trim().parse().ok())
407 .collect()
408 }
409
410 fn extract_string(&self, expr: &str) -> Option<String> {
411 let start = expr.find('(')?;
412 let end = expr.rfind(')')?;
413 let inner = expr[start + 1..end].trim();
414
415 let unquoted = inner
417 .trim_start_matches(['"', '\'', '`', '/'])
418 .trim_end_matches(['"', '\'', '`', '/']);
419
420 Some(unquoted.to_string())
421 }
422}
423
424pub enum DslResult {
425 Output(String),
426 Final(String),
427 #[allow(dead_code)]
428 Error(String),
429}
430
431pub struct RlmExecutor {
443 repl: RlmRepl,
444 provider: Arc<dyn Provider>,
445 model: String,
446 max_iterations: usize,
447 sub_queries: Vec<SubQuery>,
448 verbose: bool,
449 #[cfg(feature = "functiongemma")]
450 tool_router: Option<ToolCallRouter>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct SubQuery {
456 pub query: String,
457 pub context_slice: Option<String>,
458 pub response: String,
459 pub tokens_used: usize,
460}
461
462impl RlmExecutor {
463 pub fn new(context: String, provider: Arc<dyn Provider>, model: String) -> Self {
465 #[cfg(feature = "functiongemma")]
466 let tool_router = {
467 let cfg = ToolRouterConfig::from_env();
468 ToolCallRouter::from_config(&cfg)
469 .inspect_err(|e| {
470 tracing::debug!(error = %e, "FunctionGemma router unavailable for RLM");
471 })
472 .ok()
473 .flatten()
474 };
475
476 Self {
477 repl: RlmRepl::new(context, ReplRuntime::Rust),
478 provider,
479 model,
480 max_iterations: 5, sub_queries: Vec::new(),
482 verbose: false,
483 #[cfg(feature = "functiongemma")]
484 tool_router,
485 }
486 }
487
488 pub fn with_max_iterations(mut self, max: usize) -> Self {
490 self.max_iterations = max;
491 self
492 }
493
494 pub fn with_verbose(mut self, verbose: bool) -> Self {
499 self.verbose = verbose;
500 self
501 }
502
503 pub async fn analyze(&mut self, query: &str) -> Result<RlmAnalysisResult> {
505 let start = std::time::Instant::now();
506 let mut iterations = 0;
507 let mut total_input_tokens = 0;
508 let mut total_output_tokens = 0;
509
510 let tools = rlm_tool_definitions();
512
513 let context_summary = format!(
515 "=== CONTEXT LOADED ===\n\
516 Total: {} chars, {} lines\n\
517 Available functions:\n\
518 - head(n) - first n lines\n\
519 - tail(n) - last n lines\n\
520 - grep(\"pattern\") - find lines matching regex\n\
521 - count(\"pattern\") - count regex matches\n\
522 - slice(start, end) - slice by char position\n\
523 - chunks(n) - split into n chunks\n\
524 - llm_query(\"question\", context?) - ask sub-LM a question\n\
525 - FINAL(\"answer\") - return final answer\n\
526 === END CONTEXT INFO ===",
527 self.repl.context().len(),
528 self.repl.lines().len()
529 );
530
531 if self.verbose {
533 tracing::info!("RLM Context Summary:\n{}", context_summary);
534 println!(
535 "[RLM] Context loaded: {} chars, {} lines",
536 self.repl.context().len(),
537 self.repl.lines().len()
538 );
539 }
540
541 let system_prompt = format!(
542 "You are a code analysis assistant. Answer questions by examining the provided context.\n\n\
543 IMPORTANT: You MUST end your response with FINAL(\"your answer\") in 1-3 iterations.\n\n\
544 Available commands:\n\
545 - head(n), tail(n): See first/last n lines\n\
546 - grep(\"pattern\"): Search for patterns\n\
547 - llm_query(\"question\"): Ask a focused sub-question\n\
548 - FINAL(\"answer\"): Return your final answer (REQUIRED)\n\n\
549 The context has {} chars across {} lines. A preview follows:\n\n\
550 {}\n\n\
551 Now analyze the context. Use 1-2 commands if needed, then call FINAL() with your answer.",
552 self.repl.context().len(),
553 self.repl.lines().len(),
554 self.repl.head(25).join("\n")
555 );
556
557 let mut messages = vec![
558 Message {
559 role: Role::System,
560 content: vec![ContentPart::Text {
561 text: system_prompt,
562 }],
563 },
564 Message {
565 role: Role::User,
566 content: vec![ContentPart::Text {
567 text: format!("Analyze and answer: {}", query),
568 }],
569 },
570 ];
571
572 let mut final_answer = None;
573
574 while iterations < self.max_iterations {
575 iterations += 1;
576 tracing::info!("RLM iteration {}", iterations);
577
578 tracing::debug!("Sending LLM request...");
580 let response = match tokio::time::timeout(
581 std::time::Duration::from_secs(60),
582 self.provider.complete(CompletionRequest {
583 messages: messages.clone(),
584 tools: tools.clone(),
585 model: self.model.clone(),
586 temperature: Some(0.3),
587 top_p: None,
588 max_tokens: Some(2000),
589 stop: vec![],
590 }),
591 )
592 .await
593 {
594 Ok(Ok(r)) => {
595 tracing::debug!("LLM response received");
596 r
597 }
598 Ok(Err(e)) => return Err(e),
599 Err(_) => return Err(anyhow::anyhow!("LLM request timed out after 60 seconds")),
600 };
601
602 #[cfg(feature = "functiongemma")]
605 let response = if let Some(ref router) = self.tool_router {
606 router.maybe_reformat(response, &tools, true).await
610 } else {
611 response
612 };
613
614 total_input_tokens += response.usage.prompt_tokens;
615 total_output_tokens += response.usage.completion_tokens;
616
617 let tool_calls: Vec<(String, String, String)> = response
621 .message
622 .content
623 .iter()
624 .filter_map(|p| match p {
625 ContentPart::ToolCall {
626 id,
627 name,
628 arguments,
629 } => Some((id.clone(), name.clone(), arguments.clone())),
630 _ => None,
631 })
632 .collect();
633
634 if !tool_calls.is_empty() {
635 tracing::info!(
636 count = tool_calls.len(),
637 "RLM: dispatching structured tool calls"
638 );
639
640 messages.push(Message {
642 role: Role::Assistant,
643 content: response.message.content.clone(),
644 });
645
646 let mut tool_results: Vec<ContentPart> = Vec::new();
647
648 for (call_id, name, arguments) in &tool_calls {
649 match dispatch_tool_call(name, arguments, &mut self.repl) {
650 Some(RlmToolResult::Final(answer)) => {
651 if self.verbose {
652 println!("[RLM] Final answer received via tool call");
653 }
654 final_answer = Some(answer.clone());
655 tool_results.push(ContentPart::ToolResult {
656 tool_call_id: call_id.clone(),
657 content: format!("FINAL: {answer}"),
658 });
659 break;
660 }
661 Some(RlmToolResult::Output(output)) => {
662 if let Ok(sentinel) = serde_json::from_str::<serde_json::Value>(&output)
664 {
665 if sentinel
666 .get("__rlm_llm_query")
667 .and_then(|v| v.as_bool())
668 .unwrap_or(false)
669 {
670 let q = sentinel
671 .get("query")
672 .and_then(|v| v.as_str())
673 .unwrap_or("");
674 let ctx_slice = sentinel
675 .get("context_slice")
676 .and_then(|v| v.as_str())
677 .map(|s| s.to_string());
678 let llm_result =
679 self.handle_llm_query_direct(q, ctx_slice).await?;
680 tool_results.push(ContentPart::ToolResult {
681 tool_call_id: call_id.clone(),
682 content: llm_result,
683 });
684 continue;
685 }
686 }
687 if self.verbose {
689 let preview = truncate_with_ellipsis(&output, 200);
690 println!("[RLM] Tool {name} → {}", preview);
691 }
692 tool_results.push(ContentPart::ToolResult {
693 tool_call_id: call_id.clone(),
694 content: output,
695 });
696 }
697 None => {
698 tool_results.push(ContentPart::ToolResult {
699 tool_call_id: call_id.clone(),
700 content: format!("Unknown tool: {name}"),
701 });
702 }
703 }
704 }
705
706 if !tool_results.is_empty() {
708 messages.push(Message {
709 role: Role::Tool,
710 content: tool_results,
711 });
712 }
713
714 if final_answer.is_some() {
715 break;
716 }
717 continue;
718 }
719
720 let assistant_text = response
724 .message
725 .content
726 .iter()
727 .filter_map(|p| match p {
728 ContentPart::Text { text } => Some(text.as_str()),
729 _ => None,
730 })
731 .collect::<Vec<_>>()
732 .join("");
733
734 messages.push(Message {
736 role: Role::Assistant,
737 content: vec![ContentPart::Text {
738 text: assistant_text.clone(),
739 }],
740 });
741
742 let code = self.extract_code(&assistant_text);
744
745 if self.verbose {
747 println!("[RLM] Iteration {}: Executing code:\n{}", iterations, code);
748 }
749
750 let execution_result = self.execute_with_llm_query(&code).await?;
751
752 if self.verbose {
754 if let Some(ref answer) = execution_result.final_answer {
755 println!("[RLM] Final answer received: {}", answer);
756 } else if !execution_result.stdout.is_empty() {
757 let preview = truncate_with_ellipsis(&execution_result.stdout, 200);
758 println!("[RLM] Execution output:\n{}", preview);
759 }
760 }
761
762 if let Some(answer) = &execution_result.final_answer {
764 final_answer = Some(answer.clone());
765 break;
766 }
767
768 let result_text = if execution_result.stdout.is_empty() {
770 "[No output]".to_string()
771 } else {
772 format!("Execution result:\n{}", execution_result.stdout)
773 };
774
775 messages.push(Message {
776 role: Role::User,
777 content: vec![ContentPart::Text { text: result_text }],
778 });
779 }
780
781 let elapsed = start.elapsed();
782
783 Ok(RlmAnalysisResult {
784 answer: final_answer.unwrap_or_else(|| "Analysis incomplete".to_string()),
785 iterations,
786 sub_queries: self.sub_queries.clone(),
787 stats: super::RlmStats {
788 input_tokens: total_input_tokens,
789 output_tokens: total_output_tokens,
790 iterations,
791 subcalls: self.sub_queries.len(),
792 elapsed_ms: elapsed.as_millis() as u64,
793 compression_ratio: 1.0,
794 },
795 })
796 }
797
798 fn extract_code(&self, text: &str) -> String {
800 let mut code_lines = Vec::new();
802 let mut in_code_block = false;
803
804 for line in text.lines() {
805 if line.starts_with("```") {
806 in_code_block = !in_code_block;
807 continue;
808 }
809 if in_code_block {
810 code_lines.push(line);
811 }
812 }
813
814 if !code_lines.is_empty() {
815 return code_lines.join("\n");
816 }
817
818 text.lines()
820 .filter(|line| {
821 let l = line.trim();
822 l.starts_with("head(")
823 || l.starts_with("tail(")
824 || l.starts_with("grep(")
825 || l.starts_with("count(")
826 || l.starts_with("llm_query(")
827 || l.starts_with("FINAL(")
828 || l.starts_with("let ")
829 || l.starts_with("const ")
830 || l.starts_with("print")
831 || l.starts_with("console.")
832 })
833 .collect::<Vec<_>>()
834 .join("\n")
835 }
836
837 async fn execute_with_llm_query(&mut self, code: &str) -> Result<ReplResult> {
839 let mut stdout = Vec::new();
840 let mut final_answer = None;
841
842 for line in code.lines() {
843 let line = line.trim();
844 if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
845 continue;
846 }
847
848 if line.starts_with("llm_query(") || line.contains("= llm_query(") {
850 let result = self.handle_llm_query(line).await?;
851 stdout.push(result);
852 continue;
853 }
854
855 if let Some(result) = self.repl.execute_dsl_line(line) {
857 match result {
858 DslResult::Output(s) => stdout.push(s),
859 DslResult::Final(s) => {
860 final_answer = Some(s);
861 break;
862 }
863 DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
864 }
865 }
866 }
867
868 Ok(ReplResult {
869 stdout: stdout.join("\n"),
870 stderr: String::new(),
871 final_answer,
872 })
873 }
874
875 async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
877 let (query, context_slice) = self.parse_llm_query(line);
879
880 let context_to_analyze = context_slice
882 .clone()
883 .unwrap_or_else(|| self.repl.context().to_string());
884
885 let context_chars = context_to_analyze.chars().count();
887 let truncated_context = if context_chars > 8000 {
888 format!(
889 "{}\n[truncated, {} chars total]",
890 truncate_with_ellipsis(&context_to_analyze, 7500),
891 context_chars
892 )
893 } else {
894 context_to_analyze.clone()
895 };
896
897 let messages = vec![
899 Message {
900 role: Role::System,
901 content: vec![ContentPart::Text {
902 text: "You are a focused analysis assistant. Answer the question based on the provided context. Be concise.".to_string(),
903 }],
904 },
905 Message {
906 role: Role::User,
907 content: vec![ContentPart::Text {
908 text: format!("Context:\n{}\n\nQuestion: {}", truncated_context, query),
909 }],
910 },
911 ];
912
913 let response = self
914 .provider
915 .complete(CompletionRequest {
916 messages,
917 tools: vec![],
918 model: self.model.clone(),
919 temperature: Some(0.3),
920 top_p: None,
921 max_tokens: Some(500),
922 stop: vec![],
923 })
924 .await?;
925
926 let answer = response
927 .message
928 .content
929 .iter()
930 .filter_map(|p| match p {
931 ContentPart::Text { text } => Some(text.as_str()),
932 _ => None,
933 })
934 .collect::<Vec<_>>()
935 .join("");
936
937 self.sub_queries.push(SubQuery {
939 query: query.clone(),
940 context_slice,
941 response: answer.clone(),
942 tokens_used: response.usage.total_tokens,
943 });
944
945 Ok(format!("llm_query result: {}", answer))
946 }
947
948 async fn handle_llm_query_direct(
953 &mut self,
954 query: &str,
955 context_slice: Option<String>,
956 ) -> Result<String> {
957 let context_to_analyze = context_slice
958 .clone()
959 .unwrap_or_else(|| self.repl.context().to_string());
960
961 let context_chars = context_to_analyze.chars().count();
962 let truncated_context = if context_chars > 8000 {
963 format!(
964 "{}\n[truncated, {} chars total]",
965 truncate_with_ellipsis(&context_to_analyze, 7500),
966 context_chars
967 )
968 } else {
969 context_to_analyze.clone()
970 };
971
972 let messages = vec![
973 Message {
974 role: Role::System,
975 content: vec![ContentPart::Text {
976 text: "You are a focused analysis assistant. Answer the question based on the provided context. Be concise.".to_string(),
977 }],
978 },
979 Message {
980 role: Role::User,
981 content: vec![ContentPart::Text {
982 text: format!("Context:\n{}\n\nQuestion: {}", truncated_context, query),
983 }],
984 },
985 ];
986
987 let response = self
988 .provider
989 .complete(CompletionRequest {
990 messages,
991 tools: vec![],
992 model: self.model.clone(),
993 temperature: Some(0.3),
994 top_p: None,
995 max_tokens: Some(500),
996 stop: vec![],
997 })
998 .await?;
999
1000 let answer = response
1001 .message
1002 .content
1003 .iter()
1004 .filter_map(|p| match p {
1005 ContentPart::Text { text } => Some(text.as_str()),
1006 _ => None,
1007 })
1008 .collect::<Vec<_>>()
1009 .join("");
1010
1011 self.sub_queries.push(SubQuery {
1012 query: query.to_string(),
1013 context_slice,
1014 response: answer.clone(),
1015 tokens_used: response.usage.total_tokens,
1016 });
1017
1018 Ok(format!("llm_query result: {}", answer))
1019 }
1020
1021 fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
1023 let start = line.find('(').unwrap_or(0) + 1;
1025 let end = line.rfind(')').unwrap_or(line.len());
1026 let args = &line[start..end];
1027
1028 let mut query = String::new();
1030 let mut context = None;
1031 let mut in_quotes = false;
1032 let mut current = String::new();
1033 let mut parts = Vec::new();
1034
1035 for c in args.chars() {
1036 if c == '"' || c == '\'' {
1037 in_quotes = !in_quotes;
1038 } else if c == ',' && !in_quotes {
1039 parts.push(current.trim().to_string());
1040 current = String::new();
1041 continue;
1042 }
1043 current.push(c);
1044 }
1045 if !current.is_empty() {
1046 parts.push(current.trim().to_string());
1047 }
1048
1049 if let Some(q) = parts.first() {
1051 query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
1052 }
1053
1054 if let Some(ctx_expr) = parts.get(1) {
1056 let ctx = self.repl.evaluate_expression(ctx_expr);
1058 if !ctx.is_empty() && !ctx.starts_with('[') {
1059 context = Some(ctx);
1060 }
1061 }
1062
1063 (query, context)
1064 }
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1069pub struct RlmAnalysisResult {
1070 pub answer: String,
1071 pub iterations: usize,
1072 pub sub_queries: Vec<SubQuery>,
1073 pub stats: super::RlmStats,
1074}
1075
1076pub struct ExternalRepl {
1078 child: Child,
1079 #[allow(dead_code)]
1080 runtime: ReplRuntime,
1081}
1082
1083impl ExternalRepl {
1084 pub async fn spawn_bun(context: &str) -> Result<Self> {
1086 let init_script = Self::generate_bun_init(context);
1087
1088 let temp_dir = std::env::temp_dir().join("rlm-repl");
1090 tokio::fs::create_dir_all(&temp_dir).await?;
1091 let script_path = temp_dir.join(format!("init_{}.js", std::process::id()));
1092 tokio::fs::write(&script_path, init_script).await?;
1093
1094 let runtime = if Self::is_bun_available().await {
1096 "bun"
1097 } else {
1098 "node"
1099 };
1100
1101 let child = Command::new(runtime)
1102 .arg(&script_path)
1103 .stdin(Stdio::piped())
1104 .stdout(Stdio::piped())
1105 .stderr(Stdio::piped())
1106 .spawn()?;
1107
1108 Ok(Self {
1109 child,
1110 runtime: ReplRuntime::Bun,
1111 })
1112 }
1113
1114 async fn is_bun_available() -> bool {
1115 Command::new("bun")
1116 .arg("--version")
1117 .output()
1118 .await
1119 .map(|o| o.status.success())
1120 .unwrap_or(false)
1121 }
1122
1123 fn generate_bun_init(context: &str) -> String {
1124 let escaped = context
1125 .replace('\\', "\\\\")
1126 .replace('"', "\\\"")
1127 .replace('\n', "\\n");
1128
1129 format!(
1130 r#"
1131const readline = require('readline');
1132const rl = readline.createInterface({{ input: process.stdin, output: process.stdout, terminal: false }});
1133
1134const context = "{escaped}";
1135
1136function lines() {{ return context.split("\n"); }}
1137function head(n = 10) {{ return lines().slice(0, n).join("\n"); }}
1138function tail(n = 10) {{ return lines().slice(-n).join("\n"); }}
1139function grep(pattern) {{
1140 const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
1141 return lines().filter(l => re.test(l));
1142}}
1143function count(pattern) {{
1144 const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
1145 return (context.match(re) || []).length;
1146}}
1147function FINAL(answer) {{
1148 console.log("__FINAL__" + String(answer) + "__FINAL_END__");
1149}}
1150
1151console.log("READY");
1152
1153rl.on('line', async (line) => {{
1154 try {{
1155 const result = eval(line);
1156 if (result !== undefined) console.log(result);
1157 }} catch (e) {{
1158 console.error("Error:", e.message);
1159 }}
1160 console.log("__DONE__");
1161}});
1162"#
1163 )
1164 }
1165
1166 pub async fn execute(&mut self, code: &str) -> Result<ReplResult> {
1168 let stdin = self
1169 .child
1170 .stdin
1171 .as_mut()
1172 .ok_or_else(|| anyhow::anyhow!("No stdin"))?;
1173 let stdout = self
1174 .child
1175 .stdout
1176 .as_mut()
1177 .ok_or_else(|| anyhow::anyhow!("No stdout"))?;
1178
1179 stdin.write_all(code.as_bytes()).await?;
1180 stdin.write_all(b"\n").await?;
1181 stdin.flush().await?;
1182
1183 let mut reader = BufReader::new(stdout);
1184 let mut output = Vec::new();
1185 let mut final_answer = None;
1186
1187 loop {
1188 let mut line = String::new();
1189 match timeout(Duration::from_secs(30), reader.read_line(&mut line)).await {
1190 Ok(Ok(0)) | Err(_) => break, Ok(Ok(_)) => {
1192 let line = line.trim();
1193 if line == "__DONE__" {
1194 break;
1195 }
1196 if let Some(answer) = Self::extract_final(line) {
1197 final_answer = Some(answer);
1198 break;
1199 }
1200 output.push(line.to_string());
1201 }
1202 Ok(Err(e)) => return Err(anyhow::anyhow!("Read error: {}", e)),
1203 }
1204 }
1205
1206 Ok(ReplResult {
1207 stdout: output.join("\n"),
1208 stderr: String::new(),
1209 final_answer,
1210 })
1211 }
1212
1213 fn extract_final(line: &str) -> Option<String> {
1214 if line.contains("__FINAL__") {
1215 let start = line.find("__FINAL__")? + 9;
1216 let end = line.find("__FINAL_END__")?;
1217 return Some(line[start..end].to_string());
1218 }
1219 None
1220 }
1221
1222 pub async fn destroy(&mut self) -> Result<()> {
1224 tracing::debug!(runtime = ?self.runtime, "Destroying external REPL");
1225 self.child.kill().await?;
1226 Ok(())
1227 }
1228
1229 pub fn runtime(&self) -> ReplRuntime {
1231 self.runtime
1232 }
1233}
1234
1235fn char_index_to_byte_index(value: &str, char_index: usize) -> usize {
1236 if char_index == 0 {
1237 return 0;
1238 }
1239
1240 value
1241 .char_indices()
1242 .nth(char_index)
1243 .map(|(idx, _)| idx)
1244 .unwrap_or(value.len())
1245}
1246
1247fn truncate_with_ellipsis(value: &str, max_chars: usize) -> String {
1248 if max_chars == 0 {
1249 return String::new();
1250 }
1251
1252 let mut chars = value.chars();
1253 let mut output = String::new();
1254 for _ in 0..max_chars {
1255 if let Some(ch) = chars.next() {
1256 output.push(ch);
1257 } else {
1258 return value.to_string();
1259 }
1260 }
1261
1262 if chars.next().is_some() {
1263 format!("{output}...")
1264 } else {
1265 output
1266 }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272
1273 #[test]
1274 fn test_repl_head_tail() {
1275 let context = (1..=100)
1276 .map(|i| format!("line {}", i))
1277 .collect::<Vec<_>>()
1278 .join("\n");
1279 let repl = RlmRepl::new(context, ReplRuntime::Rust);
1280
1281 let head = repl.head(5);
1282 assert_eq!(head.len(), 5);
1283 assert_eq!(head[0], "line 1");
1284
1285 let tail = repl.tail(5);
1286 assert_eq!(tail.len(), 5);
1287 assert_eq!(tail[4], "line 100");
1288 }
1289
1290 #[test]
1291 fn test_repl_grep() {
1292 let context = "error: something failed\ninfo: all good\nerror: another failure".to_string();
1293 let repl = RlmRepl::new(context, ReplRuntime::Rust);
1294
1295 let matches = repl.grep("error");
1296 assert_eq!(matches.len(), 2);
1297 }
1298
1299 #[test]
1300 fn test_repl_execute_final() {
1301 let context = "test content".to_string();
1302 let mut repl = RlmRepl::new(context, ReplRuntime::Rust);
1303
1304 let result = repl.execute(r#"FINAL("This is the answer")"#);
1305 assert_eq!(result.final_answer, Some("This is the answer".to_string()));
1306 }
1307
1308 #[test]
1309 fn test_repl_chunks() {
1310 let context = (1..=100)
1311 .map(|i| format!("line {}", i))
1312 .collect::<Vec<_>>()
1313 .join("\n");
1314 let repl = RlmRepl::new(context, ReplRuntime::Rust);
1315
1316 let chunks = repl.chunks(5);
1317 assert_eq!(chunks.len(), 5);
1318 }
1319}