use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use async_trait::async_trait;
use serde_json::{Value, json};
use super::types::{HostCall, RlmCallRecord, RlmCancelFlag, RlmPolicy};
use crate::error::{Result, TinyAgentsError};
use crate::graph::subagent_node::SubAgentInput;
use crate::harness::events::EventSink;
use crate::harness::message::Message;
use crate::harness::model::ModelRequest;
use crate::harness::tool::ToolCall;
use crate::registry::{CapabilityRegistry, ComponentKind};
pub fn is_fatal(err: &TinyAgentsError) -> bool {
matches!(
err,
TinyAgentsError::LimitExceeded(_)
| TinyAgentsError::Timeout(_)
| TinyAgentsError::Cancelled
| TinyAgentsError::SubAgentDepth(_)
)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CapabilityListing {
pub models: Vec<String>,
pub tools: Vec<(String, String)>,
pub agents: Vec<String>,
}
#[async_trait]
pub trait RlmHostApi: Send + Sync {
async fn handle(&self, call: HostCall) -> Result<Value>;
fn capabilities(&self) -> CapabilityListing;
fn deadline(&self) -> Option<Instant>;
fn cancel_flag(&self) -> RlmCancelFlag;
}
#[derive(Debug, Default, Clone, Copy)]
struct CallCounters {
llm: usize,
tool: usize,
agent: usize,
}
#[derive(Debug, Default)]
struct CellBuffers {
deadline: Option<Instant>,
calls: Vec<RlmCallRecord>,
final_answer: Option<String>,
}
pub struct RlmHost<State: Send + Sync> {
registry: Arc<CapabilityRegistry<State>>,
state: Arc<State>,
policy: RlmPolicy,
default_model: Option<String>,
run_depth: usize,
events: EventSink,
cancel: RlmCancelFlag,
counters: Mutex<CallCounters>,
cell: Mutex<CellBuffers>,
}
impl<State: Send + Sync + 'static> RlmHost<State> {
pub fn new(registry: Arc<CapabilityRegistry<State>>, state: Arc<State>) -> Self {
Self {
registry,
state,
policy: RlmPolicy::default(),
default_model: None,
run_depth: 0,
events: EventSink::default(),
cancel: RlmCancelFlag::new(),
counters: Mutex::new(CallCounters::default()),
cell: Mutex::new(CellBuffers::default()),
}
}
pub fn with_policy(mut self, policy: RlmPolicy) -> Self {
self.policy = policy;
self
}
pub fn with_default_model(mut self, model: impl Into<String>) -> Self {
self.default_model = Some(model.into());
self
}
pub fn with_run_depth(mut self, depth: usize) -> Self {
self.run_depth = depth;
self
}
pub fn with_events(mut self, events: EventSink) -> Self {
self.events = events;
self
}
pub fn with_cancel_flag(mut self, cancel: RlmCancelFlag) -> Self {
self.cancel = cancel;
self
}
pub fn policy(&self) -> &RlmPolicy {
&self.policy
}
pub fn app_state(&self) -> Arc<State> {
self.state.clone()
}
pub(super) fn begin_cell(&self) {
let mut cell = self.cell.lock().expect("cell buffers poisoned");
cell.deadline = self.policy.cell_timeout.map(|t| Instant::now() + t);
cell.calls.clear();
cell.final_answer = None;
}
pub(super) fn end_cell(&self) -> (Vec<RlmCallRecord>, Option<String>) {
let mut cell = self.cell.lock().expect("cell buffers poisoned");
cell.deadline = None;
(std::mem::take(&mut cell.calls), cell.final_answer.take())
}
fn record(&self, record: RlmCallRecord) {
self.cell
.lock()
.expect("cell buffers poisoned")
.calls
.push(record);
}
fn bump(&self, call: &HostCall) -> Result<()> {
let mut counters = self.counters.lock().expect("counters poisoned");
match call {
HostCall::Llm { .. } => {
if counters.llm >= self.policy.max_llm_calls {
return Err(TinyAgentsError::LimitExceeded(format!(
"llm call limit ({}) exceeded",
self.policy.max_llm_calls
)));
}
counters.llm += 1;
}
HostCall::Tool { .. } => {
if counters.tool >= self.policy.max_tool_calls {
return Err(TinyAgentsError::LimitExceeded(format!(
"tool call limit ({}) exceeded",
self.policy.max_tool_calls
)));
}
counters.tool += 1;
}
HostCall::Agent { .. } => {
if counters.agent >= self.policy.max_agent_calls {
return Err(TinyAgentsError::LimitExceeded(format!(
"agent call limit ({}) exceeded",
self.policy.max_agent_calls
)));
}
counters.agent += 1;
}
HostCall::FinalAnswer { .. } => {}
}
Ok(())
}
pub fn call_counts(&self) -> (usize, usize, usize) {
let counters = self.counters.lock().expect("counters poisoned");
(counters.llm, counters.tool, counters.agent)
}
async fn handle_llm(
&self,
model: Option<String>,
prompt: String,
system: Option<String>,
) -> Result<Value> {
let model_name = model
.or_else(|| self.default_model.clone())
.ok_or_else(|| {
TinyAgentsError::Validation(
"llm: no model named and the session has no default model".to_string(),
)
})?;
let model = self
.registry
.model(&model_name)
.ok_or_else(|| TinyAgentsError::ModelNotFound(model_name.clone()))?;
let mut messages = Vec::new();
if let Some(system) = system {
messages.push(Message::system(system));
}
messages.push(Message::user(prompt));
let request = ModelRequest {
messages,
..Default::default()
};
let start = Instant::now();
let response = model.invoke(&self.state, request).await?;
let text = Message::Assistant(response.message).text();
self.record(RlmCallRecord {
kind: super::types::RlmCallKind::Llm,
name: model_name,
detail: json!({ "chars": text.len() }),
elapsed: start.elapsed(),
});
Ok(Value::String(text))
}
async fn handle_tool(&self, tool_name: String, arguments: Value) -> Result<Value> {
let tool = self
.registry
.tool(&tool_name)
.ok_or_else(|| TinyAgentsError::ToolNotFound(tool_name.clone()))?;
let call = ToolCall::new(
crate::harness::ids::new_call_id().as_str().to_string(),
tool_name.clone(),
arguments.clone(),
);
tool.schema().validate_call(&call)?;
let start = Instant::now();
let result = tool.call(&self.state, call).await?;
self.record(RlmCallRecord {
kind: super::types::RlmCallKind::Tool,
name: tool_name,
detail: json!({ "arguments": arguments }),
elapsed: start.elapsed(),
});
if let Some(error) = result.error {
return Err(TinyAgentsError::Tool(error));
}
match result.raw {
Some(raw) => Ok(raw),
None => Ok(Value::String(result.content)),
}
}
async fn handle_agent(
&self,
agent_name: String,
input: String,
data: Option<Value>,
) -> Result<Value> {
crate::harness::context::RunConfig::checked_child_depth(
self.run_depth,
self.policy.max_depth,
)?;
let agent = self.registry.agent(&agent_name).ok_or_else(|| {
TinyAgentsError::Capability(format!("agent `{agent_name}` is not registered"))
})?;
let mut sub_input = SubAgentInput::prompt(input);
if let Some(data) = data {
sub_input = sub_input.with_data(data);
}
let start = Instant::now();
let output = agent.run(sub_input, self.events.clone()).await?;
self.record(RlmCallRecord {
kind: super::types::RlmCallKind::Agent,
name: agent_name,
detail: json!({
"model_calls": output.model_calls,
"tool_calls": output.tool_calls,
}),
elapsed: start.elapsed(),
});
Ok(Value::String(output.text))
}
}
#[async_trait]
impl<State: Send + Sync + 'static> RlmHostApi for RlmHost<State> {
async fn handle(&self, call: HostCall) -> Result<Value> {
if self.cancel.is_cancelled() {
return Err(TinyAgentsError::Cancelled);
}
if let Some(deadline) = self.deadline()
&& Instant::now() >= deadline
{
return Err(TinyAgentsError::Timeout(
"rlm cell exceeded its wall-clock timeout".to_string(),
));
}
self.bump(&call)?;
match call {
HostCall::Llm {
model,
prompt,
system,
} => self.handle_llm(model, prompt, system).await,
HostCall::Tool { tool, arguments } => self.handle_tool(tool, arguments).await,
HostCall::Agent { agent, input, data } => self.handle_agent(agent, input, data).await,
HostCall::FinalAnswer { answer } => {
let mut cell = self.cell.lock().expect("cell buffers poisoned");
cell.final_answer = Some(answer);
cell.calls.push(RlmCallRecord {
kind: super::types::RlmCallKind::FinalAnswer,
name: "final_answer".to_string(),
detail: Value::Null,
elapsed: Duration::default(),
});
Ok(Value::Null)
}
}
}
fn capabilities(&self) -> CapabilityListing {
let tools = self
.registry
.names(ComponentKind::Tool)
.into_iter()
.map(|name| {
let description = self
.registry
.tool(&name)
.map(|tool| tool.description().to_string())
.unwrap_or_default();
(name, description)
})
.collect();
CapabilityListing {
models: self.registry.names(ComponentKind::Model),
tools,
agents: self.registry.names(ComponentKind::Agent),
}
}
fn deadline(&self) -> Option<Instant> {
self.cell.lock().expect("cell buffers poisoned").deadline
}
fn cancel_flag(&self) -> RlmCancelFlag {
self.cancel.clone()
}
}