use lellm_core::{ChatResponse, LlmError, Message};
use lellm_provider::ResolvedModel;
use std::sync::Arc;
use super::config::{ToolUseConfig, ToolUseDeps, build_request_messages_inner, empty_response};
use super::context::LocalCompactor;
use super::event::{AgentEvent, AgentStream, StopReason};
use super::tools::{ToolExecutor, ToolSnapshot};
#[derive(Clone)]
pub struct ResolvedRound {
pub snapshot: Arc<ToolSnapshot>,
pub definitions: Vec<lellm_core::ToolDefinition>,
}
impl ResolvedRound {
pub fn new(snapshot: Arc<ToolSnapshot>) -> Self {
Self {
definitions: snapshot.definitions().to_vec(),
snapshot,
}
}
}
#[derive(Debug, Clone)]
pub struct ToolUseResult {
pub stop_reason: StopReason,
pub response: ChatResponse,
pub messages: Vec<Message>,
pub iterations: usize,
pub tool_calls_executed: usize,
}
impl ToolUseResult {
pub fn is_success(&self) -> bool {
matches!(self.stop_reason, StopReason::Complete)
}
}
#[derive(Clone)]
pub struct ToolUseLoop {
model: ResolvedModel,
executor: ToolExecutor,
config: ToolUseConfig,
deps: ToolUseDeps,
}
impl ToolUseLoop {
pub fn new(
model: ResolvedModel,
executor: ToolExecutor,
config: ToolUseConfig,
deps: ToolUseDeps,
) -> Self {
if config.stream_thinking {
let caps = model.provider.capabilities_for(&model.model);
if !caps.supports_stream_thinking {
tracing::warn!(
provider = %model.provider.provider_id(),
model = %model.model,
"stream_thinking=true but provider does not support thinking deltas; \
reasoning content will only be available in the final response"
);
}
}
Self {
model,
executor,
config,
deps,
}
}
pub fn simple(model: ResolvedModel, executor: ToolExecutor) -> Self {
Self::new(
model,
executor,
ToolUseConfig::default(),
ToolUseDeps::default(),
)
}
pub fn model(&self) -> &ResolvedModel {
&self.model
}
pub fn executor(&self) -> &ToolExecutor {
&self.executor
}
pub fn config(&self) -> &ToolUseConfig {
&self.config
}
pub fn deps(&self) -> &ToolUseDeps {
&self.deps
}
fn build_invoker(&self) -> Arc<super::invoker::LlmInvoker> {
Arc::new(super::invoker::LlmInvoker::from_config(
self.model.clone(),
&self.config,
self.deps.fallback.clone(),
))
}
pub async fn execute(&self, messages: Vec<Message>) -> Result<ToolUseResult, LlmError> {
let initial_messages = build_request_messages_inner(&self.config, &messages)?;
let invoker = self.build_invoker();
let llm_node =
super::react::LLMNode::new("llm", invoker, self.executor.clone(), self.config.clone());
let tool_node =
super::react::ToolNode::new("tool", self.executor.clone(), self.config.clone());
let compactor_node = super::react::CompactorNode::new(
"compactor",
Arc::new(LocalCompactor::new()),
self.config.context_budget.clone(),
);
let graph = super::react::build_react_graph(llm_node, tool_node, compactor_node);
let max_steps = self.config.max_iterations * 4 + 1;
let agent_state_init = super::typed_state::AgentState::from_messages(initial_messages);
let mut exec_ctx = lellm_graph::node_context::ExecutionContext::new(
agent_state_init,
None,
lellm_graph::CancellationToken::new(),
);
graph
.run_inline(&mut exec_ctx, max_steps)
.await
.map_err(|e| lellm_core::LlmError::Provider {
provider: "react_graph".into(),
status: None,
code: None,
message: e.to_string(),
})?;
let agent_state = exec_ctx.state();
let stop_reason = agent_state
.stop_reason
.clone()
.unwrap_or(StopReason::Complete);
let last_response = agent_state
.last_response
.clone()
.unwrap_or_else(empty_response);
Ok(ToolUseResult {
stop_reason,
response: last_response,
messages: agent_state.messages.clone(),
iterations: agent_state.iterations,
tool_calls_executed: agent_state.total_tool_calls,
})
}
pub fn execute_stream(&self, messages: Vec<Message>) -> AgentStream {
let (tx, rx) = tokio::sync::mpsc::channel(32);
let invoker = self.build_invoker();
let executor = self.executor.clone();
let config = self.config.clone();
tokio::spawn(async move {
let initial_messages = match build_request_messages_inner(&config, &messages) {
Ok(m) => m,
Err(e) => {
let _ = tx
.send(AgentEvent::LoopError {
error: e,
iterations: 0,
})
.await;
return;
}
};
let llm_node =
super::react::LLMNode::new("llm", invoker, executor.clone(), config.clone());
let tool_node = super::react::ToolNode::new("tool", executor.clone(), config.clone());
let compactor_node = super::react::CompactorNode::new(
"compactor",
Arc::new(LocalCompactor::new()),
config.context_budget.clone(),
);
let graph = super::react::build_react_graph(llm_node, tool_node, compactor_node);
let max_steps = config.max_iterations * 4 + 1;
let agent_state = super::typed_state::AgentState::from_messages(initial_messages);
let event_sink = super::event_bridge::AgentEventSink::new(tx.clone());
let sink: std::sync::Arc<dyn lellm_graph::StreamSink> = std::sync::Arc::new(event_sink);
let mut exec_ctx = lellm_graph::node_context::ExecutionContext::new(
agent_state,
Some(sink),
lellm_graph::CancellationToken::new(),
);
match graph.run_inline(&mut exec_ctx, max_steps).await {
Ok(()) => {
let state = exec_ctx.state();
let stop_reason = state.stop_reason.clone().unwrap_or(StopReason::Complete);
let last_response = state.last_response.clone().unwrap_or_else(empty_response);
let _ = tx
.send(AgentEvent::LoopEnd {
result: ToolUseResult {
stop_reason,
response: last_response,
messages: state.messages.clone(),
iterations: state.iterations,
tool_calls_executed: state.total_tool_calls,
},
})
.await;
}
Err(e) => {
let state = exec_ctx.state();
let error = LlmError::Provider {
provider: "react_graph".into(),
status: None,
code: None,
message: e.to_string(),
};
let _ = tx
.send(AgentEvent::LoopError {
error,
iterations: state.iterations,
})
.await;
}
}
});
rx
}
}