use std::sync::Arc;
use serde_json::Value;
use super::host::{RlmHost, RlmHostApi};
use super::session::RlmSession;
use super::templates;
use super::types::{RlmConfig, RlmOutcome, RlmStep, RlmStopReason};
use crate::error::{Result, TinyAgentsError};
use crate::harness::message::Message;
use crate::harness::model::ModelRequest;
use crate::registry::CapabilityRegistry;
pub fn extract_code_cell(reply: &str) -> Option<String> {
let fence_start = reply.find("```")?;
let after_fence = &reply[fence_start + 3..];
let newline = after_fence.find('\n')?;
let body = &after_fence[newline + 1..];
let fence_end = body.find("```")?;
let code = body[..fence_end].trim_end();
(!code.trim().is_empty()).then(|| code.to_string())
}
fn render_observation(outcome: &super::types::CellOutcome) -> String {
let mut out = String::new();
if !outcome.stdout.is_empty() {
out.push_str("stdout:\n");
out.push_str(&outcome.stdout);
if !outcome.stdout.ends_with('\n') {
out.push('\n');
}
}
if let Some(value) = &outcome.value {
out.push_str(&format!("value: {value}\n"));
}
if let Some(error) = &outcome.error {
out.push_str(&format!("error: {error}\n"));
}
if out.is_empty() {
out.push_str("(cell produced no output)\n");
}
out.push_str("Continue. Reply with the next code cell, or call final_answer(...) when done.");
out
}
pub struct RlmRunner<State: Send + Sync + 'static> {
registry: Arc<CapabilityRegistry<State>>,
config: RlmConfig,
session: RlmSession<State>,
driver_model: String,
system_prompt: String,
}
impl<State: Send + Sync + 'static> RlmRunner<State> {
pub fn from_config(
config: RlmConfig,
registry: Arc<CapabilityRegistry<State>>,
state: Arc<State>,
) -> Result<Self> {
let driver_model = config
.driver_model
.clone()
.or_else(|| {
registry
.names(crate::registry::ComponentKind::Model)
.into_iter()
.next()
})
.ok_or_else(|| {
TinyAgentsError::Validation(
"rlm: no driver model configured and no model registered".to_string(),
)
})?;
let sub_model = config
.sub_model
.clone()
.unwrap_or_else(|| driver_model.clone());
let host = Arc::new(
RlmHost::new(registry.clone(), state)
.with_policy(config.policy.clone())
.with_default_model(sub_model),
);
let session = RlmSession::new(&config.interpreter, host)?;
let template = templates::resolve(&config.template)?;
let system_prompt = templates::render_system_prompt(
&template,
&session.language(),
&session.usage_guide(),
&session.host().capabilities(),
&config.policy,
);
Ok(Self {
registry,
config,
session,
driver_model,
system_prompt,
})
}
pub fn session_mut(&mut self) -> &mut RlmSession<State> {
&mut self.session
}
pub fn system_prompt(&self) -> &str {
&self.system_prompt
}
pub async fn set_context(&mut self, context: Value) -> Result<()> {
self.session.set_variable("context", context).await
}
pub async fn run(&mut self, task: impl Into<String>) -> Result<RlmOutcome> {
let driver = self
.registry
.model(&self.driver_model)
.ok_or_else(|| TinyAgentsError::ModelNotFound(self.driver_model.clone()))?;
let state = self.session.host().app_state();
let mut messages = vec![
Message::system(self.system_prompt.clone()),
Message::user(task.into()),
];
let mut steps: Vec<RlmStep> = Vec::new();
let mut driver_calls = 0usize;
let mut nudged = false;
let outcome = loop {
if steps.len() >= self.config.policy.max_cells {
break RlmOutcome {
answer: None,
stop_reason: RlmStopReason::CellBudgetExhausted,
steps,
driver_calls,
sub_llm_calls: 0,
tool_calls: 0,
agent_calls: 0,
};
}
let request = ModelRequest {
messages: messages.clone(),
..Default::default()
};
driver_calls += 1;
let response = driver.invoke(&state, request).await?;
let reply_text = Message::Assistant(response.message.clone()).text();
messages.push(Message::Assistant(response.message));
let Some(code) = extract_code_cell(&reply_text) else {
if !nudged {
nudged = true;
messages.push(Message::user(
"Your reply contained no fenced code block, so nothing was executed. \
Reply with exactly one fenced code block, or — if you are done — call \
final_answer(...) from code. If you truly have nothing to run, repeat \
your final answer in plain prose.",
));
continue;
}
break RlmOutcome {
answer: Some(reply_text.trim().to_string()),
stop_reason: RlmStopReason::ModelAnswered,
steps,
driver_calls,
sub_llm_calls: 0,
tool_calls: 0,
agent_calls: 0,
};
};
nudged = false;
let cell = self.session.eval(&code).await?;
let answered = cell.final_answer.clone();
let observation = render_observation(&cell);
steps.push(RlmStep {
code,
outcome: cell,
});
if let Some(answer) = answered {
break RlmOutcome {
answer: Some(answer),
stop_reason: RlmStopReason::Answered,
steps,
driver_calls,
sub_llm_calls: 0,
tool_calls: 0,
agent_calls: 0,
};
}
messages.push(Message::user(observation));
};
let (llm, tool, agent) = self.session.host().call_counts();
Ok(RlmOutcome {
sub_llm_calls: llm,
tool_calls: tool,
agent_calls: agent,
..outcome
})
}
pub async fn shutdown(&mut self) -> Result<()> {
self.session.shutdown().await
}
}