1use std::time::Instant;
4
5use crate::chat::{ChatMessage, ChatResponse, ContentBlock, StopReason, ToolCall, ToolResult};
6use crate::error::LlmError;
7use crate::provider::{ChatParams, DynProvider};
8use crate::usage::Usage;
9
10use super::LoopDepth;
11use super::ToolRegistry;
12use super::approval::approve_calls;
13use super::config::{
14 StopContext, StopDecision, TerminationReason, ToolLoopConfig, ToolLoopEvent, ToolLoopResult,
15};
16use super::execution::execute_with_events;
17use super::loop_detection::{LoopDetectionState, handle_loop_detection_refs};
18
19pub async fn tool_loop<Ctx: LoopDepth + Send + Sync + 'static>(
50 provider: &dyn DynProvider,
51 registry: &ToolRegistry<Ctx>,
52 mut params: ChatParams,
53 config: ToolLoopConfig,
54 ctx: &Ctx,
55) -> Result<ToolLoopResult, LlmError> {
56 let current_depth = ctx.loop_depth();
58 if let Some(max_depth) = config.max_depth {
59 if current_depth >= max_depth {
60 return Err(LlmError::MaxDepthExceeded {
61 current: current_depth,
62 limit: max_depth,
63 });
64 }
65 }
66
67 let nested_ctx = ctx.with_depth(current_depth + 1);
69
70 let mut total_usage = Usage::default();
71 let mut iterations = 0u32;
72 let mut tool_calls_executed = 0usize;
73 let mut last_tool_results: Vec<ToolResult> = Vec::new();
74 let mut loop_state = LoopDetectionState::default();
75
76 let start_time = Instant::now();
78 let timeout_limit = config.timeout;
79
80 loop {
81 if let Some(limit) = timeout_limit {
83 if start_time.elapsed() >= limit {
84 return Ok(ToolLoopResult {
86 response: ChatResponse::empty(),
87 iterations,
88 total_usage,
89 termination_reason: TerminationReason::Timeout { limit },
90 });
91 }
92 }
93
94 iterations += 1;
95
96 let msg_count = params.messages.len();
98 emit_event(&config, || ToolLoopEvent::IterationStart {
99 iteration: iterations,
100 message_count: msg_count,
101 });
102
103 let response = provider.generate_boxed(¶ms).await?;
104 total_usage += &response.usage;
105
106 let call_refs: Vec<&ToolCall> = response.tool_calls();
108 let text_length = response.text().map_or(0, str::len);
109 let has_tool_calls = !call_refs.is_empty();
110
111 emit_event(&config, || ToolLoopEvent::LlmResponseReceived {
113 iteration: iterations,
114 has_tool_calls,
115 text_length,
116 });
117
118 if let Some(result) = check_stop_condition_refs(
120 &config,
121 &response,
122 iterations,
123 &total_usage,
124 tool_calls_executed,
125 &last_tool_results,
126 &call_refs,
127 ) {
128 return Ok(result);
129 }
130
131 if iterations > config.max_iterations {
132 return Ok(ToolLoopResult {
133 response,
134 iterations,
135 total_usage,
136 termination_reason: TerminationReason::MaxIterations {
137 limit: config.max_iterations,
138 },
139 });
140 }
141
142 if let Some(result) = handle_loop_detection_refs(
144 &mut loop_state,
145 &call_refs,
146 config.loop_detection.as_ref(),
147 &config,
148 &mut params.messages,
149 &response,
150 iterations,
151 &total_usage,
152 ) {
153 return Ok(result);
154 }
155
156 let (calls, other_content) = response.partition_content();
158
159 let (approved_calls, denied_results) = approve_calls(&calls, &config);
162 let results = execute_with_events(
163 registry,
164 &approved_calls,
165 denied_results,
166 config.parallel_tool_execution,
167 &config,
168 &nested_ctx,
169 )
170 .await;
171
172 tool_calls_executed += results.len();
174 last_tool_results.clone_from(&results);
175
176 params.messages.push(ChatMessage {
179 role: crate::chat::ChatRole::Assistant,
180 content: other_content,
181 });
182
183 for result in results {
184 params.messages.push(ChatMessage::tool_result_full(result));
185 }
186 }
187}
188
189#[inline]
193pub(crate) fn emit_event<F>(config: &ToolLoopConfig, event_fn: F)
194where
195 F: FnOnce() -> ToolLoopEvent,
196{
197 if let Some(ref callback) = config.on_event {
198 callback(event_fn());
199 }
200}
201
202#[allow(clippy::too_many_arguments)]
205fn check_stop_condition_refs(
206 config: &ToolLoopConfig,
207 response: &ChatResponse,
208 iterations: u32,
209 total_usage: &Usage,
210 tool_calls_executed: usize,
211 last_tool_results: &[ToolResult],
212 call_refs: &[&ToolCall],
213) -> Option<ToolLoopResult> {
214 if let Some(ref stop_fn) = config.stop_when {
216 let ctx = StopContext {
217 iteration: iterations,
218 response,
219 total_usage,
220 tool_calls_executed,
221 last_tool_results,
222 };
223 match stop_fn(&ctx) {
224 StopDecision::Continue => {}
225 StopDecision::Stop => {
226 return Some(ToolLoopResult {
227 response: response.clone(),
228 iterations,
229 total_usage: total_usage.clone(),
230 termination_reason: TerminationReason::StopCondition { reason: None },
231 });
232 }
233 StopDecision::StopWithReason(reason) => {
234 return Some(ToolLoopResult {
235 response: response.clone(),
236 iterations,
237 total_usage: total_usage.clone(),
238 termination_reason: TerminationReason::StopCondition {
239 reason: Some(reason),
240 },
241 });
242 }
243 }
244 }
245
246 if call_refs.is_empty() || response.stop_reason != StopReason::ToolUse {
248 return Some(ToolLoopResult {
249 response: response.clone(),
250 iterations,
251 total_usage: total_usage.clone(),
252 termination_reason: TerminationReason::Complete,
253 });
254 }
255
256 None
257}
258
259impl ChatMessage {
262 pub fn tool_result_full(result: ToolResult) -> Self {
264 Self {
265 role: crate::chat::ChatRole::Tool,
266 content: vec![ContentBlock::ToolResult(result)],
267 }
268 }
269}