use std::time::Instant;
use crate::chat::{ChatMessage, ChatResponse, ContentBlock, StopReason, ToolCall, ToolResult};
use crate::error::LlmError;
use crate::provider::{ChatParams, DynProvider};
use crate::usage::Usage;
use super::LoopDepth;
use super::ToolRegistry;
use super::approval::approve_calls;
use super::config::{
StopContext, StopDecision, TerminationReason, ToolLoopConfig, ToolLoopEvent, ToolLoopResult,
};
use super::execution::execute_with_events;
use super::loop_detection::{LoopDetectionState, handle_loop_detection_refs};
pub async fn tool_loop<Ctx: LoopDepth + Send + Sync + 'static>(
provider: &dyn DynProvider,
registry: &ToolRegistry<Ctx>,
mut params: ChatParams,
config: ToolLoopConfig,
ctx: &Ctx,
) -> Result<ToolLoopResult, LlmError> {
let current_depth = ctx.loop_depth();
if let Some(max_depth) = config.max_depth {
if current_depth >= max_depth {
return Err(LlmError::MaxDepthExceeded {
current: current_depth,
limit: max_depth,
});
}
}
let nested_ctx = ctx.with_depth(current_depth + 1);
let mut total_usage = Usage::default();
let mut iterations = 0u32;
let mut tool_calls_executed = 0usize;
let mut last_tool_results: Vec<ToolResult> = Vec::new();
let mut loop_state = LoopDetectionState::default();
let start_time = Instant::now();
let timeout_limit = config.timeout;
loop {
if let Some(limit) = timeout_limit {
if start_time.elapsed() >= limit {
return Ok(ToolLoopResult {
response: ChatResponse::empty(),
iterations,
total_usage,
termination_reason: TerminationReason::Timeout { limit },
});
}
}
iterations += 1;
let msg_count = params.messages.len();
emit_event(&config, || ToolLoopEvent::IterationStart {
iteration: iterations,
message_count: msg_count,
});
let response = provider.generate_boxed(¶ms).await?;
total_usage += &response.usage;
let call_refs: Vec<&ToolCall> = response.tool_calls();
let text_length = response.text().map_or(0, str::len);
let has_tool_calls = !call_refs.is_empty();
emit_event(&config, || ToolLoopEvent::LlmResponseReceived {
iteration: iterations,
has_tool_calls,
text_length,
});
if let Some(result) = check_stop_condition_refs(
&config,
&response,
iterations,
&total_usage,
tool_calls_executed,
&last_tool_results,
&call_refs,
) {
return Ok(result);
}
if iterations > config.max_iterations {
return Ok(ToolLoopResult {
response,
iterations,
total_usage,
termination_reason: TerminationReason::MaxIterations {
limit: config.max_iterations,
},
});
}
if let Some(result) = handle_loop_detection_refs(
&mut loop_state,
&call_refs,
config.loop_detection.as_ref(),
&config,
&mut params.messages,
&response,
iterations,
&total_usage,
) {
return Ok(result);
}
let (calls, other_content) = response.partition_content();
let (approved_calls, denied_results) = approve_calls(&calls, &config);
let results = execute_with_events(
registry,
&approved_calls,
denied_results,
config.parallel_tool_execution,
&config,
&nested_ctx,
)
.await;
tool_calls_executed += results.len();
last_tool_results.clone_from(&results);
params.messages.push(ChatMessage {
role: crate::chat::ChatRole::Assistant,
content: other_content,
});
for result in results {
params.messages.push(ChatMessage::tool_result_full(result));
}
}
}
#[inline]
pub(crate) fn emit_event<F>(config: &ToolLoopConfig, event_fn: F)
where
F: FnOnce() -> ToolLoopEvent,
{
if let Some(ref callback) = config.on_event {
callback(event_fn());
}
}
#[allow(clippy::too_many_arguments)]
fn check_stop_condition_refs(
config: &ToolLoopConfig,
response: &ChatResponse,
iterations: u32,
total_usage: &Usage,
tool_calls_executed: usize,
last_tool_results: &[ToolResult],
call_refs: &[&ToolCall],
) -> Option<ToolLoopResult> {
if let Some(ref stop_fn) = config.stop_when {
let ctx = StopContext {
iteration: iterations,
response,
total_usage,
tool_calls_executed,
last_tool_results,
};
match stop_fn(&ctx) {
StopDecision::Continue => {}
StopDecision::Stop => {
return Some(ToolLoopResult {
response: response.clone(),
iterations,
total_usage: total_usage.clone(),
termination_reason: TerminationReason::StopCondition { reason: None },
});
}
StopDecision::StopWithReason(reason) => {
return Some(ToolLoopResult {
response: response.clone(),
iterations,
total_usage: total_usage.clone(),
termination_reason: TerminationReason::StopCondition {
reason: Some(reason),
},
});
}
}
}
if call_refs.is_empty() || response.stop_reason != StopReason::ToolUse {
return Some(ToolLoopResult {
response: response.clone(),
iterations,
total_usage: total_usage.clone(),
termination_reason: TerminationReason::Complete,
});
}
None
}
impl ChatMessage {
pub fn tool_result_full(result: ToolResult) -> Self {
Self {
role: crate::chat::ChatRole::Tool,
content: vec![ContentBlock::ToolResult(result)],
}
}
}