use anyhow::Result;
use std::sync::Arc;
use tracing::{debug, warn};
use crate::{
env::{build_engine, execute_script},
error::RlmError,
protocol::{Notebook, StepResult},
};
#[async_trait::async_trait]
pub trait LlmProvider: Send + Sync {
async fn complete(&self, messages: Vec<Message>) -> Result<String>;
fn model_id(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct Message {
pub role: Role,
pub content: String,
}
#[derive(Debug, Clone)]
pub enum Role {
System,
User,
Assistant,
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::System => write!(f, "system"),
Role::User => write!(f, "user"),
Role::Assistant => write!(f, "assistant"),
}
}
}
const SYSTEM_PROMPT: &str = r#"You are an RLM (Recursive Language Model) agent. You never see the
full context directly. Instead, you write Rhai scripts that interact with the context via
registered functions, and the results are returned to you as cell outputs.
Available Rhai functions:
- ctx_len() -> int — byte length of the full context
- ctx_slice(start, end) -> String — byte-range slice of context
- ctx_grep(pattern) -> String — regex search; returns matching lines
- print_cell(msg) — append a message to this cell's output log
- done(answer) — signal that you have the final answer; exits the loop
Rules:
- Write ONE Rhai script per response. No prose before or after the script.
- To exit, call done("your answer here") in the script.
- If a script errors, you will see the error as cell output — self-correct and try again.
- You may call rlm_call(query, ctx_fragment) to recursively process a sub-context.
"#;
pub struct Rlm {
provider: Arc<dyn LlmProvider>,
pub depth: usize,
pub max_depth: usize,
pub max_iterations: usize,
pub max_retries_per_cell: usize,
pub verbose: bool,
}
impl Rlm {
pub fn new(provider: Arc<dyn LlmProvider>) -> Self {
Self {
provider,
depth: 0,
max_depth: 5,
max_iterations: 20,
max_retries_per_cell: 3,
verbose: false,
}
}
pub fn with_depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
pub fn with_max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub async fn run(&self, query: &str, context: &str) -> Result<String, RlmError> {
if self.depth >= self.max_depth {
return Err(RlmError::MaxDepthExceeded(self.depth));
}
let (engine, mut scope) = build_engine(context.to_string());
let mut notebook = Notebook::default();
let mut iterations = 0;
loop {
if iterations >= self.max_iterations {
return Err(RlmError::MaxIterationsExceeded(self.max_iterations));
}
iterations += 1;
let messages = self.build_messages(query, ¬ebook);
let script = self
.provider
.complete(messages)
.await
.map_err(|e| RlmError::ProviderError(e))?;
let script = extract_script(&script);
if self.verbose {
eprintln!("[depth={}] >> {}", self.depth, script);
}
debug!(
depth = self.depth,
iteration = iterations,
"rlm: executing cell"
);
let mut retries: usize = 0;
let step = loop {
match execute_script(&engine, &mut scope, &script)
.map_err(|e| RlmError::ScriptError(e.to_string()))?
{
StepResult::Continue(out)
if out.starts_with("script error:")
&& retries < self.max_retries_per_cell =>
{
retries += 1;
warn!(depth = self.depth, retry = retries, error = %out, "rlm: script error, retrying");
break StepResult::Continue(out);
}
step => break step,
}
};
if self.verbose {
eprintln!(
"[depth={}] << {}",
self.depth,
match &step {
StepResult::Continue(o) => o.as_str(),
StepResult::Final(a) => a.as_str(),
}
);
}
match step {
StepResult::Final(answer) => {
debug!(depth = self.depth, "rlm: final answer received");
return Ok(answer);
}
StepResult::Continue(output) => {
notebook.push(script.to_string(), output);
}
}
}
}
fn build_messages(&self, query: &str, notebook: &Notebook) -> Vec<Message> {
let mut messages = vec![
Message {
role: Role::System,
content: SYSTEM_PROMPT.to_string(),
},
Message {
role: Role::User,
content: format!("Query: {query}"),
},
];
if !notebook.cells.is_empty() {
messages.push(Message {
role: Role::Assistant,
content: notebook.as_history(),
});
messages.push(Message {
role: Role::User,
content:
"Continue. Write the next Rhai script or call done() if you have the answer."
.to_string(),
});
}
messages
}
}
fn extract_script(raw: &str) -> &str {
let trimmed = raw.trim();
if let Some(inner) = trimmed.strip_prefix("```rhai") {
inner.trim_end_matches("```").trim()
} else if let Some(inner) = trimmed.strip_prefix("```") {
inner.trim_end_matches("```").trim()
} else {
trimmed
}
}