1use anyhow::Result;
2use std::sync::Arc;
3use tracing::{debug, warn};
4
5use crate::{
6 env::{build_engine, execute_script},
7 error::RlmError,
8 protocol::{Notebook, StepResult},
9};
10
11#[async_trait::async_trait]
13pub trait LlmProvider: Send + Sync {
14 async fn complete(&self, messages: Vec<Message>) -> Result<String>;
15 fn model_id(&self) -> &str;
16}
17
18#[derive(Debug, Clone)]
19pub struct Message {
20 pub role: Role,
21 pub content: String,
22}
23
24#[derive(Debug, Clone)]
25pub enum Role {
26 System,
27 User,
28 Assistant,
29}
30
31impl std::fmt::Display for Role {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Role::System => write!(f, "system"),
35 Role::User => write!(f, "user"),
36 Role::Assistant => write!(f, "assistant"),
37 }
38 }
39}
40
41const SYSTEM_PROMPT: &str = r#"You are an RLM (Recursive Language Model) agent. You never see the
42full context directly. Instead, you write Rhai scripts that interact with the context via
43registered functions, and the results are returned to you as cell outputs.
44
45Available Rhai functions:
46- ctx_len() -> int — byte length of the full context
47- ctx_slice(start, end) -> String — byte-range slice of context
48- ctx_grep(pattern) -> String — regex search; returns matching lines
49- print_cell(msg) — append a message to this cell's output log
50- done(answer) — signal that you have the final answer; exits the loop
51
52Rules:
53- Write ONE Rhai script per response. No prose before or after the script.
54- To exit, call done("your answer here") in the script.
55- If a script errors, you will see the error as cell output — self-correct and try again.
56- You may call rlm_call(query, ctx_fragment) to recursively process a sub-context.
57"#;
58
59pub struct Rlm {
61 provider: Arc<dyn LlmProvider>,
62 pub depth: usize,
63 pub max_depth: usize,
64 pub max_iterations: usize,
65 pub max_retries_per_cell: usize,
66 pub verbose: bool,
67}
68
69impl Rlm {
70 pub fn new(provider: Arc<dyn LlmProvider>) -> Self {
71 Self {
72 provider,
73 depth: 0,
74 max_depth: 5,
75 max_iterations: 20,
76 max_retries_per_cell: 3,
77 verbose: false,
78 }
79 }
80
81 pub fn with_depth(mut self, depth: usize) -> Self {
82 self.depth = depth;
83 self
84 }
85
86 pub fn with_max_depth(mut self, max_depth: usize) -> Self {
87 self.max_depth = max_depth;
88 self
89 }
90
91 pub fn with_verbose(mut self, verbose: bool) -> Self {
92 self.verbose = verbose;
93 self
94 }
95
96 pub async fn run(&self, query: &str, context: &str) -> Result<String, RlmError> {
98 if self.depth >= self.max_depth {
99 return Err(RlmError::MaxDepthExceeded(self.depth));
100 }
101
102 let (engine, mut scope) = build_engine(context.to_string());
103 let mut notebook = Notebook::default();
104 let mut iterations = 0;
105
106 loop {
107 if iterations >= self.max_iterations {
108 return Err(RlmError::MaxIterationsExceeded(self.max_iterations));
109 }
110 iterations += 1;
111
112 let messages = self.build_messages(query, ¬ebook);
113 let script = self
114 .provider
115 .complete(messages)
116 .await
117 .map_err(|e| RlmError::ProviderError(e))?;
118
119 let script = extract_script(&script);
120
121 if self.verbose {
122 eprintln!("[depth={}] >> {}", self.depth, script);
123 }
124
125 debug!(
126 depth = self.depth,
127 iteration = iterations,
128 "rlm: executing cell"
129 );
130
131 let mut retries: usize = 0;
132 let step = loop {
133 match execute_script(&engine, &mut scope, &script)
134 .map_err(|e| RlmError::ScriptError(e.to_string()))?
135 {
136 StepResult::Continue(out)
137 if out.starts_with("script error:")
138 && retries < self.max_retries_per_cell =>
139 {
140 retries += 1;
141 warn!(depth = self.depth, retry = retries, error = %out, "rlm: script error, retrying");
142 break StepResult::Continue(out);
143 }
144 step => break step,
145 }
146 };
147
148 if self.verbose {
149 eprintln!(
150 "[depth={}] << {}",
151 self.depth,
152 match &step {
153 StepResult::Continue(o) => o.as_str(),
154 StepResult::Final(a) => a.as_str(),
155 }
156 );
157 }
158
159 match step {
160 StepResult::Final(answer) => {
161 debug!(depth = self.depth, "rlm: final answer received");
162 return Ok(answer);
163 }
164 StepResult::Continue(output) => {
165 notebook.push(script.to_string(), output);
166 }
167 }
168 }
169 }
170
171 fn build_messages(&self, query: &str, notebook: &Notebook) -> Vec<Message> {
172 let mut messages = vec![
173 Message {
174 role: Role::System,
175 content: SYSTEM_PROMPT.to_string(),
176 },
177 Message {
178 role: Role::User,
179 content: format!("Query: {query}"),
180 },
181 ];
182
183 if !notebook.cells.is_empty() {
184 messages.push(Message {
185 role: Role::Assistant,
186 content: notebook.as_history(),
187 });
188 messages.push(Message {
189 role: Role::User,
190 content:
191 "Continue. Write the next Rhai script or call done() if you have the answer."
192 .to_string(),
193 });
194 }
195
196 messages
197 }
198}
199
200fn extract_script(raw: &str) -> &str {
202 let trimmed = raw.trim();
203 if let Some(inner) = trimmed.strip_prefix("```rhai") {
204 inner.trim_end_matches("```").trim()
205 } else if let Some(inner) = trimmed.strip_prefix("```") {
206 inner.trim_end_matches("```").trim()
207 } else {
208 trimmed
209 }
210}