1use lellm_core::{ChatResponse, LlmError, Message};
17use lellm_provider::ResolvedModel;
18use std::sync::Arc;
19
20use super::config::{ToolUseConfig, ToolUseDeps, build_request_messages_inner, empty_response};
21use super::context::LocalCompactor;
22use super::event::{AgentEvent, AgentStream, StopReason};
23use super::tools::{ToolExecutor, ToolSnapshot};
24
25#[derive(Clone)]
31pub struct ResolvedRound {
32 pub snapshot: Arc<ToolSnapshot>,
34 pub definitions: Vec<lellm_core::ToolDefinition>,
36}
37
38impl ResolvedRound {
39 pub fn new(snapshot: Arc<ToolSnapshot>) -> Self {
40 Self {
41 definitions: snapshot.definitions().to_vec(),
42 snapshot,
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
51pub struct ToolUseResult {
52 pub stop_reason: StopReason,
53 pub response: ChatResponse,
54 pub messages: Vec<Message>,
55 pub iterations: usize,
56 pub tool_calls_executed: usize,
57}
58
59impl ToolUseResult {
60 pub fn is_success(&self) -> bool {
61 matches!(self.stop_reason, StopReason::Complete)
62 }
63}
64
65#[derive(Clone)]
71pub struct ToolUseLoop {
72 model: ResolvedModel,
73 executor: ToolExecutor,
74 config: ToolUseConfig,
75 deps: ToolUseDeps,
76}
77
78impl ToolUseLoop {
79 pub fn new(
80 model: ResolvedModel,
81 executor: ToolExecutor,
82 config: ToolUseConfig,
83 deps: ToolUseDeps,
84 ) -> Self {
85 if config.stream_thinking {
86 let caps = model.provider.capabilities_for(&model.model);
87 if !caps.supports_stream_thinking {
88 tracing::warn!(
89 provider = %model.provider.provider_id(),
90 model = %model.model,
91 "stream_thinking=true but provider does not support thinking deltas; \
92 reasoning content will only be available in the final response"
93 );
94 }
95 }
96
97 Self {
98 model,
99 executor,
100 config,
101 deps,
102 }
103 }
104
105 pub fn simple(model: ResolvedModel, executor: ToolExecutor) -> Self {
107 Self::new(
108 model,
109 executor,
110 ToolUseConfig::default(),
111 ToolUseDeps::default(),
112 )
113 }
114
115 pub fn model(&self) -> &ResolvedModel {
117 &self.model
118 }
119
120 pub fn executor(&self) -> &ToolExecutor {
122 &self.executor
123 }
124
125 pub fn config(&self) -> &ToolUseConfig {
127 &self.config
128 }
129
130 pub fn deps(&self) -> &ToolUseDeps {
132 &self.deps
133 }
134
135 fn build_invoker(&self) -> Arc<super::invoker::LlmInvoker> {
137 Arc::new(super::invoker::LlmInvoker::from_config(
138 self.model.clone(),
139 &self.config,
140 self.deps.fallback.clone(),
141 ))
142 }
143
144 pub async fn execute(&self, messages: Vec<Message>) -> Result<ToolUseResult, LlmError> {
149 let initial_messages = build_request_messages_inner(&self.config, &messages)?;
150
151 let invoker = self.build_invoker();
153 let llm_node =
154 super::react::LLMNode::new("llm", invoker, self.executor.clone(), self.config.clone());
155 let tool_node =
156 super::react::ToolNode::new("tool", self.executor.clone(), self.config.clone());
157 let compactor_node = super::react::CompactorNode::new(
158 "compactor",
159 Arc::new(LocalCompactor::new()),
160 self.config.context_budget.clone(),
161 );
162 let graph = super::react::build_react_graph(llm_node, tool_node, compactor_node);
163 let max_steps = self.config.max_iterations * 4 + 1;
167
168 let agent_state_init = super::typed_state::AgentState::from_messages(initial_messages);
170
171 let mut exec_ctx = lellm_graph::node_context::ExecutionContext::new(
173 agent_state_init,
174 None,
175 lellm_graph::CancellationToken::new(),
176 );
177
178 graph
179 .run_inline(&mut exec_ctx, max_steps)
180 .await
181 .map_err(|e| lellm_core::LlmError::Provider {
182 provider: "react_graph".into(),
183 status: None,
184 code: None,
185 message: e.to_string(),
186 })?;
187
188 let agent_state = exec_ctx.state();
189 let stop_reason = agent_state
190 .stop_reason
191 .clone()
192 .unwrap_or(StopReason::Complete);
193 let last_response = agent_state
194 .last_response
195 .clone()
196 .unwrap_or_else(empty_response);
197
198 Ok(ToolUseResult {
199 stop_reason,
200 response: last_response,
201 messages: agent_state.messages.clone(),
202 iterations: agent_state.iterations,
203 tool_calls_executed: agent_state.total_tool_calls,
204 })
205 }
206
207 pub fn execute_stream(&self, messages: Vec<Message>) -> AgentStream {
224 let (tx, rx) = tokio::sync::mpsc::channel(32);
225 let invoker = self.build_invoker();
226 let executor = self.executor.clone();
227 let config = self.config.clone();
228
229 tokio::spawn(async move {
230 let initial_messages = match build_request_messages_inner(&config, &messages) {
232 Ok(m) => m,
233 Err(e) => {
234 let _ = tx
235 .send(AgentEvent::LoopError {
236 error: e,
237 iterations: 0,
238 })
239 .await;
240 return;
241 }
242 };
243
244 let llm_node =
246 super::react::LLMNode::new("llm", invoker, executor.clone(), config.clone());
247 let tool_node = super::react::ToolNode::new("tool", executor.clone(), config.clone());
248 let compactor_node = super::react::CompactorNode::new(
249 "compactor",
250 Arc::new(LocalCompactor::new()),
251 config.context_budget.clone(),
252 );
253 let graph = super::react::build_react_graph(llm_node, tool_node, compactor_node);
254
255 let max_steps = config.max_iterations * 4 + 1;
257
258 let agent_state = super::typed_state::AgentState::from_messages(initial_messages);
260
261 let event_sink = super::event_bridge::AgentEventSink::new(tx.clone());
263 let sink: std::sync::Arc<dyn lellm_graph::StreamSink> = std::sync::Arc::new(event_sink);
264
265 let mut exec_ctx = lellm_graph::node_context::ExecutionContext::new(
267 agent_state,
268 Some(sink),
269 lellm_graph::CancellationToken::new(),
270 );
271
272 match graph.run_inline(&mut exec_ctx, max_steps).await {
273 Ok(()) => {
274 let state = exec_ctx.state();
276 let stop_reason = state.stop_reason.clone().unwrap_or(StopReason::Complete);
277 let last_response = state.last_response.clone().unwrap_or_else(empty_response);
278
279 let _ = tx
280 .send(AgentEvent::LoopEnd {
281 result: ToolUseResult {
282 stop_reason,
283 response: last_response,
284 messages: state.messages.clone(),
285 iterations: state.iterations,
286 tool_calls_executed: state.total_tool_calls,
287 },
288 })
289 .await;
290 }
291 Err(e) => {
292 let state = exec_ctx.state();
294 let error = LlmError::Provider {
295 provider: "react_graph".into(),
296 status: None,
297 code: None,
298 message: e.to_string(),
299 };
300 let _ = tx
301 .send(AgentEvent::LoopError {
302 error,
303 iterations: state.iterations,
304 })
305 .await;
306 }
307 }
308 });
309
310 rx
311 }
312}