llm_stack/tool/
loop_sync.rs1use 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::{IterationSnapshot, LoopDetectionState, handle_loop_detection};
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 let snap = IterationSnapshot {
120 response: &response,
121 call_refs: &call_refs,
122 iterations,
123 total_usage: &total_usage,
124 tool_calls_executed,
125 last_tool_results: &last_tool_results,
126 config: &config,
127 };
128
129 if let Some(result) = check_stop_condition(&snap) {
131 return Ok(result);
132 }
133
134 if iterations > config.max_iterations {
135 return Ok(ToolLoopResult {
136 response,
137 iterations,
138 total_usage,
139 termination_reason: TerminationReason::MaxIterations {
140 limit: config.max_iterations,
141 },
142 });
143 }
144
145 if let Some(result) = handle_loop_detection(&mut loop_state, &snap, &mut params.messages) {
147 return Ok(result);
148 }
149
150 let (calls, other_content) = response.partition_content();
152
153 let (approved_calls, denied_results) = approve_calls(calls, &config);
156 let results = execute_with_events(
157 registry,
158 approved_calls,
159 denied_results,
160 config.parallel_tool_execution,
161 &config,
162 &nested_ctx,
163 )
164 .await;
165
166 tool_calls_executed += results.len();
168 last_tool_results.clone_from(&results);
169
170 params.messages.push(ChatMessage {
173 role: crate::chat::ChatRole::Assistant,
174 content: other_content,
175 });
176
177 for result in results {
178 params.messages.push(ChatMessage::tool_result_full(result));
179 }
180 }
181}
182
183#[inline]
187pub(crate) fn emit_event<F>(config: &ToolLoopConfig, event_fn: F)
188where
189 F: FnOnce() -> ToolLoopEvent,
190{
191 if let Some(ref callback) = config.on_event {
192 callback(event_fn());
193 }
194}
195
196fn check_stop_condition(snap: &IterationSnapshot<'_>) -> Option<ToolLoopResult> {
198 if let Some(ref stop_fn) = snap.config.stop_when {
200 let ctx = StopContext {
201 iteration: snap.iterations,
202 response: snap.response,
203 total_usage: snap.total_usage,
204 tool_calls_executed: snap.tool_calls_executed,
205 last_tool_results: snap.last_tool_results,
206 };
207 match stop_fn(&ctx) {
208 StopDecision::Continue => {}
209 StopDecision::Stop => {
210 return Some(ToolLoopResult {
211 response: snap.response.clone(),
212 iterations: snap.iterations,
213 total_usage: snap.total_usage.clone(),
214 termination_reason: TerminationReason::StopCondition { reason: None },
215 });
216 }
217 StopDecision::StopWithReason(reason) => {
218 return Some(ToolLoopResult {
219 response: snap.response.clone(),
220 iterations: snap.iterations,
221 total_usage: snap.total_usage.clone(),
222 termination_reason: TerminationReason::StopCondition {
223 reason: Some(reason),
224 },
225 });
226 }
227 }
228 }
229
230 if snap.call_refs.is_empty() || snap.response.stop_reason != StopReason::ToolUse {
232 return Some(ToolLoopResult {
233 response: snap.response.clone(),
234 iterations: snap.iterations,
235 total_usage: snap.total_usage.clone(),
236 termination_reason: TerminationReason::Complete,
237 });
238 }
239
240 None
241}
242
243impl ChatMessage {
246 pub fn tool_result_full(result: ToolResult) -> Self {
248 Self {
249 role: crate::chat::ChatRole::Tool,
250 content: vec![ContentBlock::ToolResult(result)],
251 }
252 }
253}