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).await
607 } else {
608 response
609 };
610
611 total_input_tokens += response.usage.prompt_tokens;
612 total_output_tokens += response.usage.completion_tokens;
613
614 let tool_calls: Vec<(String, String, String)> = response
618 .message
619 .content
620 .iter()
621 .filter_map(|p| match p {
622 ContentPart::ToolCall {
623 id,
624 name,
625 arguments,
626 } => Some((id.clone(), name.clone(), arguments.clone())),
627 _ => None,
628 })
629 .collect();
630
631 if !tool_calls.is_empty() {
632 tracing::info!(
633 count = tool_calls.len(),
634 "RLM: dispatching structured tool calls"
635 );
636
637 messages.push(Message {
639 role: Role::Assistant,
640 content: response.message.content.clone(),
641 });
642
643 let mut tool_results: Vec<ContentPart> = Vec::new();
644
645 for (call_id, name, arguments) in &tool_calls {
646 match dispatch_tool_call(name, arguments, &mut self.repl) {
647 Some(RlmToolResult::Final(answer)) => {
648 if self.verbose {
649 println!("[RLM] Final answer received via tool call");
650 }
651 final_answer = Some(answer.clone());
652 tool_results.push(ContentPart::ToolResult {
653 tool_call_id: call_id.clone(),
654 content: format!("FINAL: {answer}"),
655 });
656 break;
657 }
658 Some(RlmToolResult::Output(output)) => {
659 if let Ok(sentinel) = serde_json::from_str::<serde_json::Value>(&output)
661 {
662 if sentinel
663 .get("__rlm_llm_query")
664 .and_then(|v| v.as_bool())
665 .unwrap_or(false)
666 {
667 let q = sentinel
668 .get("query")
669 .and_then(|v| v.as_str())
670 .unwrap_or("");
671 let ctx_slice = sentinel
672 .get("context_slice")
673 .and_then(|v| v.as_str())
674 .map(|s| s.to_string());
675 let llm_result =
676 self.handle_llm_query_direct(q, ctx_slice).await?;
677 tool_results.push(ContentPart::ToolResult {
678 tool_call_id: call_id.clone(),
679 content: llm_result,
680 });
681 continue;
682 }
683 }
684 if self.verbose {
686 let preview = truncate_with_ellipsis(&output, 200);
687 println!("[RLM] Tool {name} → {}", preview);
688 }
689 tool_results.push(ContentPart::ToolResult {
690 tool_call_id: call_id.clone(),
691 content: output,
692 });
693 }
694 None => {
695 tool_results.push(ContentPart::ToolResult {
696 tool_call_id: call_id.clone(),
697 content: format!("Unknown tool: {name}"),
698 });
699 }
700 }
701 }
702
703 if !tool_results.is_empty() {
705 messages.push(Message {
706 role: Role::Tool,
707 content: tool_results,
708 });
709 }
710
711 if final_answer.is_some() {
712 break;
713 }
714 continue;
715 }
716
717 let assistant_text = response
721 .message
722 .content
723 .iter()
724 .filter_map(|p| match p {
725 ContentPart::Text { text } => Some(text.as_str()),
726 _ => None,
727 })
728 .collect::<Vec<_>>()
729 .join("");
730
731 messages.push(Message {
733 role: Role::Assistant,
734 content: vec![ContentPart::Text {
735 text: assistant_text.clone(),
736 }],
737 });
738
739 let code = self.extract_code(&assistant_text);
741
742 if self.verbose {
744 println!("[RLM] Iteration {}: Executing code:\n{}", iterations, code);
745 }
746
747 let execution_result = self.execute_with_llm_query(&code).await?;
748
749 if self.verbose {
751 if let Some(ref answer) = execution_result.final_answer {
752 println!("[RLM] Final answer received: {}", answer);
753 } else if !execution_result.stdout.is_empty() {
754 let preview = truncate_with_ellipsis(&execution_result.stdout, 200);
755 println!("[RLM] Execution output:\n{}", preview);
756 }
757 }
758
759 if let Some(answer) = &execution_result.final_answer {
761 final_answer = Some(answer.clone());
762 break;
763 }
764
765 let result_text = if execution_result.stdout.is_empty() {
767 "[No output]".to_string()
768 } else {
769 format!("Execution result:\n{}", execution_result.stdout)
770 };
771
772 messages.push(Message {
773 role: Role::User,
774 content: vec![ContentPart::Text { text: result_text }],
775 });
776 }
777
778 let elapsed = start.elapsed();
779
780 Ok(RlmAnalysisResult {
781 answer: final_answer.unwrap_or_else(|| "Analysis incomplete".to_string()),
782 iterations,
783 sub_queries: self.sub_queries.clone(),
784 stats: super::RlmStats {
785 input_tokens: total_input_tokens,
786 output_tokens: total_output_tokens,
787 iterations,
788 subcalls: self.sub_queries.len(),
789 elapsed_ms: elapsed.as_millis() as u64,
790 compression_ratio: 1.0,
791 },
792 })
793 }
794
795 fn extract_code(&self, text: &str) -> String {
797 let mut code_lines = Vec::new();
799 let mut in_code_block = false;
800
801 for line in text.lines() {
802 if line.starts_with("```") {
803 in_code_block = !in_code_block;
804 continue;
805 }
806 if in_code_block {
807 code_lines.push(line);
808 }
809 }
810
811 if !code_lines.is_empty() {
812 return code_lines.join("\n");
813 }
814
815 text.lines()
817 .filter(|line| {
818 let l = line.trim();
819 l.starts_with("head(")
820 || l.starts_with("tail(")
821 || l.starts_with("grep(")
822 || l.starts_with("count(")
823 || l.starts_with("llm_query(")
824 || l.starts_with("FINAL(")
825 || l.starts_with("let ")
826 || l.starts_with("const ")
827 || l.starts_with("print")
828 || l.starts_with("console.")
829 })
830 .collect::<Vec<_>>()
831 .join("\n")
832 }
833
834 async fn execute_with_llm_query(&mut self, code: &str) -> Result<ReplResult> {
836 let mut stdout = Vec::new();
837 let mut final_answer = None;
838
839 for line in code.lines() {
840 let line = line.trim();
841 if line.is_empty() || line.starts_with("//") || line.starts_with('#') {
842 continue;
843 }
844
845 if line.starts_with("llm_query(") || line.contains("= llm_query(") {
847 let result = self.handle_llm_query(line).await?;
848 stdout.push(result);
849 continue;
850 }
851
852 if let Some(result) = self.repl.execute_dsl_line(line) {
854 match result {
855 DslResult::Output(s) => stdout.push(s),
856 DslResult::Final(s) => {
857 final_answer = Some(s);
858 break;
859 }
860 DslResult::Error(s) => stdout.push(format!("Error: {}", s)),
861 }
862 }
863 }
864
865 Ok(ReplResult {
866 stdout: stdout.join("\n"),
867 stderr: String::new(),
868 final_answer,
869 })
870 }
871
872 async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
874 let (query, context_slice) = self.parse_llm_query(line);
876
877 let context_to_analyze = context_slice
879 .clone()
880 .unwrap_or_else(|| self.repl.context().to_string());
881
882 let context_chars = context_to_analyze.chars().count();
884 let truncated_context = if context_chars > 8000 {
885 format!(
886 "{}\n[truncated, {} chars total]",
887 truncate_with_ellipsis(&context_to_analyze, 7500),
888 context_chars
889 )
890 } else {
891 context_to_analyze.clone()
892 };
893
894 let messages = vec![
896 Message {
897 role: Role::System,
898 content: vec![ContentPart::Text {
899 text: "You are a focused analysis assistant. Answer the question based on the provided context. Be concise.".to_string(),
900 }],
901 },
902 Message {
903 role: Role::User,
904 content: vec![ContentPart::Text {
905 text: format!("Context:\n{}\n\nQuestion: {}", truncated_context, query),
906 }],
907 },
908 ];
909
910 let response = self
911 .provider
912 .complete(CompletionRequest {
913 messages,
914 tools: vec![],
915 model: self.model.clone(),
916 temperature: Some(0.3),
917 top_p: None,
918 max_tokens: Some(500),
919 stop: vec![],
920 })
921 .await?;
922
923 let answer = response
924 .message
925 .content
926 .iter()
927 .filter_map(|p| match p {
928 ContentPart::Text { text } => Some(text.as_str()),
929 _ => None,
930 })
931 .collect::<Vec<_>>()
932 .join("");
933
934 self.sub_queries.push(SubQuery {
936 query: query.clone(),
937 context_slice,
938 response: answer.clone(),
939 tokens_used: response.usage.total_tokens,
940 });
941
942 Ok(format!("llm_query result: {}", answer))
943 }
944
945 async fn handle_llm_query_direct(
950 &mut self,
951 query: &str,
952 context_slice: Option<String>,
953 ) -> Result<String> {
954 let context_to_analyze = context_slice
955 .clone()
956 .unwrap_or_else(|| self.repl.context().to_string());
957
958 let context_chars = context_to_analyze.chars().count();
959 let truncated_context = if context_chars > 8000 {
960 format!(
961 "{}\n[truncated, {} chars total]",
962 truncate_with_ellipsis(&context_to_analyze, 7500),
963 context_chars
964 )
965 } else {
966 context_to_analyze.clone()
967 };
968
969 let messages = vec![
970 Message {
971 role: Role::System,
972 content: vec![ContentPart::Text {
973 text: "You are a focused analysis assistant. Answer the question based on the provided context. Be concise.".to_string(),
974 }],
975 },
976 Message {
977 role: Role::User,
978 content: vec![ContentPart::Text {
979 text: format!("Context:\n{}\n\nQuestion: {}", truncated_context, query),
980 }],
981 },
982 ];
983
984 let response = self
985 .provider
986 .complete(CompletionRequest {
987 messages,
988 tools: vec![],
989 model: self.model.clone(),
990 temperature: Some(0.3),
991 top_p: None,
992 max_tokens: Some(500),
993 stop: vec![],
994 })
995 .await?;
996
997 let answer = response
998 .message
999 .content
1000 .iter()
1001 .filter_map(|p| match p {
1002 ContentPart::Text { text } => Some(text.as_str()),
1003 _ => None,
1004 })
1005 .collect::<Vec<_>>()
1006 .join("");
1007
1008 self.sub_queries.push(SubQuery {
1009 query: query.to_string(),
1010 context_slice,
1011 response: answer.clone(),
1012 tokens_used: response.usage.total_tokens,
1013 });
1014
1015 Ok(format!("llm_query result: {}", answer))
1016 }
1017
1018 fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
1020 let start = line.find('(').unwrap_or(0) + 1;
1022 let end = line.rfind(')').unwrap_or(line.len());
1023 let args = &line[start..end];
1024
1025 let mut query = String::new();
1027 let mut context = None;
1028 let mut in_quotes = false;
1029 let mut current = String::new();
1030 let mut parts = Vec::new();
1031
1032 for c in args.chars() {
1033 if c == '"' || c == '\'' {
1034 in_quotes = !in_quotes;
1035 } else if c == ',' && !in_quotes {
1036 parts.push(current.trim().to_string());
1037 current = String::new();
1038 continue;
1039 }
1040 current.push(c);
1041 }
1042 if !current.is_empty() {
1043 parts.push(current.trim().to_string());
1044 }
1045
1046 if let Some(q) = parts.first() {
1048 query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
1049 }
1050
1051 if let Some(ctx_expr) = parts.get(1) {
1053 let ctx = self.repl.evaluate_expression(ctx_expr);
1055 if !ctx.is_empty() && !ctx.starts_with('[') {
1056 context = Some(ctx);
1057 }
1058 }
1059
1060 (query, context)
1061 }
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize)]
1066pub struct RlmAnalysisResult {
1067 pub answer: String,
1068 pub iterations: usize,
1069 pub sub_queries: Vec<SubQuery>,
1070 pub stats: super::RlmStats,
1071}
1072
1073pub struct ExternalRepl {
1075 child: Child,
1076 #[allow(dead_code)]
1077 runtime: ReplRuntime,
1078}
1079
1080impl ExternalRepl {
1081 pub async fn spawn_bun(context: &str) -> Result<Self> {
1083 let init_script = Self::generate_bun_init(context);
1084
1085 let temp_dir = std::env::temp_dir().join("rlm-repl");
1087 tokio::fs::create_dir_all(&temp_dir).await?;
1088 let script_path = temp_dir.join(format!("init_{}.js", std::process::id()));
1089 tokio::fs::write(&script_path, init_script).await?;
1090
1091 let runtime = if Self::is_bun_available().await {
1093 "bun"
1094 } else {
1095 "node"
1096 };
1097
1098 let child = Command::new(runtime)
1099 .arg(&script_path)
1100 .stdin(Stdio::piped())
1101 .stdout(Stdio::piped())
1102 .stderr(Stdio::piped())
1103 .spawn()?;
1104
1105 Ok(Self {
1106 child,
1107 runtime: ReplRuntime::Bun,
1108 })
1109 }
1110
1111 async fn is_bun_available() -> bool {
1112 Command::new("bun")
1113 .arg("--version")
1114 .output()
1115 .await
1116 .map(|o| o.status.success())
1117 .unwrap_or(false)
1118 }
1119
1120 fn generate_bun_init(context: &str) -> String {
1121 let escaped = context
1122 .replace('\\', "\\\\")
1123 .replace('"', "\\\"")
1124 .replace('\n', "\\n");
1125
1126 format!(
1127 r#"
1128const readline = require('readline');
1129const rl = readline.createInterface({{ input: process.stdin, output: process.stdout, terminal: false }});
1130
1131const context = "{escaped}";
1132
1133function lines() {{ return context.split("\n"); }}
1134function head(n = 10) {{ return lines().slice(0, n).join("\n"); }}
1135function tail(n = 10) {{ return lines().slice(-n).join("\n"); }}
1136function grep(pattern) {{
1137 const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
1138 return lines().filter(l => re.test(l));
1139}}
1140function count(pattern) {{
1141 const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'gi');
1142 return (context.match(re) || []).length;
1143}}
1144function FINAL(answer) {{
1145 console.log("__FINAL__" + String(answer) + "__FINAL_END__");
1146}}
1147
1148console.log("READY");
1149
1150rl.on('line', async (line) => {{
1151 try {{
1152 const result = eval(line);
1153 if (result !== undefined) console.log(result);
1154 }} catch (e) {{
1155 console.error("Error:", e.message);
1156 }}
1157 console.log("__DONE__");
1158}});
1159"#
1160 )
1161 }
1162
1163 pub async fn execute(&mut self, code: &str) -> Result<ReplResult> {
1165 let stdin = self
1166 .child
1167 .stdin
1168 .as_mut()
1169 .ok_or_else(|| anyhow::anyhow!("No stdin"))?;
1170 let stdout = self
1171 .child
1172 .stdout
1173 .as_mut()
1174 .ok_or_else(|| anyhow::anyhow!("No stdout"))?;
1175
1176 stdin.write_all(code.as_bytes()).await?;
1177 stdin.write_all(b"\n").await?;
1178 stdin.flush().await?;
1179
1180 let mut reader = BufReader::new(stdout);
1181 let mut output = Vec::new();
1182 let mut final_answer = None;
1183
1184 loop {
1185 let mut line = String::new();
1186 match timeout(Duration::from_secs(30), reader.read_line(&mut line)).await {
1187 Ok(Ok(0)) | Err(_) => break, Ok(Ok(_)) => {
1189 let line = line.trim();
1190 if line == "__DONE__" {
1191 break;
1192 }
1193 if let Some(answer) = Self::extract_final(line) {
1194 final_answer = Some(answer);
1195 break;
1196 }
1197 output.push(line.to_string());
1198 }
1199 Ok(Err(e)) => return Err(anyhow::anyhow!("Read error: {}", e)),
1200 }
1201 }
1202
1203 Ok(ReplResult {
1204 stdout: output.join("\n"),
1205 stderr: String::new(),
1206 final_answer,
1207 })
1208 }
1209
1210 fn extract_final(line: &str) -> Option<String> {
1211 if line.contains("__FINAL__") {
1212 let start = line.find("__FINAL__")? + 9;
1213 let end = line.find("__FINAL_END__")?;
1214 return Some(line[start..end].to_string());
1215 }
1216 None
1217 }
1218
1219 pub async fn destroy(&mut self) -> Result<()> {
1221 tracing::debug!(runtime = ?self.runtime, "Destroying external REPL");
1222 self.child.kill().await?;
1223 Ok(())
1224 }
1225
1226 pub fn runtime(&self) -> ReplRuntime {
1228 self.runtime
1229 }
1230}
1231
1232fn char_index_to_byte_index(value: &str, char_index: usize) -> usize {
1233 if char_index == 0 {
1234 return 0;
1235 }
1236
1237 value
1238 .char_indices()
1239 .nth(char_index)
1240 .map(|(idx, _)| idx)
1241 .unwrap_or(value.len())
1242}
1243
1244fn truncate_with_ellipsis(value: &str, max_chars: usize) -> String {
1245 if max_chars == 0 {
1246 return String::new();
1247 }
1248
1249 let mut chars = value.chars();
1250 let mut output = String::new();
1251 for _ in 0..max_chars {
1252 if let Some(ch) = chars.next() {
1253 output.push(ch);
1254 } else {
1255 return value.to_string();
1256 }
1257 }
1258
1259 if chars.next().is_some() {
1260 format!("{output}...")
1261 } else {
1262 output
1263 }
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268 use super::*;
1269
1270 #[test]
1271 fn test_repl_head_tail() {
1272 let context = (1..=100)
1273 .map(|i| format!("line {}", i))
1274 .collect::<Vec<_>>()
1275 .join("\n");
1276 let repl = RlmRepl::new(context, ReplRuntime::Rust);
1277
1278 let head = repl.head(5);
1279 assert_eq!(head.len(), 5);
1280 assert_eq!(head[0], "line 1");
1281
1282 let tail = repl.tail(5);
1283 assert_eq!(tail.len(), 5);
1284 assert_eq!(tail[4], "line 100");
1285 }
1286
1287 #[test]
1288 fn test_repl_grep() {
1289 let context = "error: something failed\ninfo: all good\nerror: another failure".to_string();
1290 let repl = RlmRepl::new(context, ReplRuntime::Rust);
1291
1292 let matches = repl.grep("error");
1293 assert_eq!(matches.len(), 2);
1294 }
1295
1296 #[test]
1297 fn test_repl_execute_final() {
1298 let context = "test content".to_string();
1299 let mut repl = RlmRepl::new(context, ReplRuntime::Rust);
1300
1301 let result = repl.execute(r#"FINAL("This is the answer")"#);
1302 assert_eq!(result.final_answer, Some("This is the answer".to_string()));
1303 }
1304
1305 #[test]
1306 fn test_repl_chunks() {
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 chunks = repl.chunks(5);
1314 assert_eq!(chunks.len(), 5);
1315 }
1316}