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
25use crate::cognition::tool_router::{ToolCallRouter, ToolRouterConfig};
26
27use super::tools::{RlmToolResult, dispatch_tool_call, rlm_tool_definitions};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[serde(rename_all = "lowercase")]
32pub enum ReplRuntime {
33 #[default]
35 Rust,
36 Bun,
38 Python,
40}
41
42pub struct RlmRepl {
44 runtime: ReplRuntime,
45 context: String,
46 context_lines: Vec<String>,
47 variables: HashMap<String, String>,
48}
49
50#[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 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 pub fn context(&self) -> &str {
72 &self.context
73 }
74
75 pub fn lines(&self) -> &[String] {
77 &self.context_lines
78 }
79
80 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 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 pub fn grep(&self, pattern: &str) -> Vec<(usize, &str)> {
101 let re = match regex::Regex::new(pattern) {
102 Ok(r) => r,
103 Err(_) => {
104 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 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 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 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 pub fn set_var(&mut self, name: &str, value: String) {
157 self.variables.insert(name.to_string(), value);
158 }
159
160 pub fn get_var(&self, name: &str) -> Option<&str> {
162 self.variables.get(name).map(|s| s.as_str())
163 }
164
165 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 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 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 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 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 let expanded = self.expand_expression(content);
240 return Some(DslResult::Output(expanded));
241 }
242
243 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 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 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 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 if let Some(val) = self.get_var(expr) {
383 return val.to_string();
384 }
385
386 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 let unquoted = inner
424 .trim_start_matches(['"', '\'', '`', '/'])
425 .trim_end_matches(['"', '\'', '`', '/']);
426
427 Some(unquoted.to_string())
428 }
429
430 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
460pub 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#[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 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, sub_queries: Vec::new(),
510 verbose: false,
511
512 tool_router,
513 }
514 }
515
516 pub fn with_max_iterations(mut self, max: usize) -> Self {
518 self.max_iterations = max;
519 self
520 }
521
522 pub fn with_verbose(mut self, verbose: bool) -> Self {
527 self.verbose = verbose;
528 self
529 }
530
531 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 let tools = rlm_tool_definitions();
540
541 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 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 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 let response = if let Some(ref router) = self.tool_router {
636 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 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 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 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 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 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 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 messages.push(Message {
767 role: Role::Assistant,
768 content: vec![ContentPart::Text {
769 text: assistant_text.clone(),
770 }],
771 });
772
773 let code = self.extract_code(&assistant_text);
775
776 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 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 if let Some(answer) = &execution_result.final_answer {
795 final_answer = Some(answer.clone());
796 break;
797 }
798
799 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 fn extract_code(&self, text: &str) -> String {
831 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 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 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 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 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 async fn handle_llm_query(&mut self, line: &str) -> Result<String> {
909 let (query, context_slice) = self.parse_llm_query(line);
911
912 let context_to_analyze = context_slice
914 .clone()
915 .unwrap_or_else(|| self.repl.context().to_string());
916
917 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 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 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 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 fn parse_llm_query(&mut self, line: &str) -> (String, Option<String>) {
1055 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 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 if let Some(q) = parts.first() {
1083 query = q.trim_matches(|c| c == '"' || c == '\'').to_string();
1084 }
1085
1086 if let Some(ctx_expr) = parts.get(1) {
1088 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#[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
1108pub struct ExternalRepl {
1110 child: Child,
1111 #[allow(dead_code)]
1112 runtime: ReplRuntime,
1113}
1114
1115impl ExternalRepl {
1116 pub async fn spawn_bun(context: &str) -> Result<Self> {
1118 let init_script = Self::generate_bun_init(context);
1119
1120 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 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 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, 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 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 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}