Skip to main content

llm_stack/tool/
loop_sync.rs

1//! Synchronous (non-streaming) tool loop implementation.
2//!
3//! Thin wrapper around [`ToolLoopHandle`](super::ToolLoopHandle) that
4//! auto-continues on every `Yielded` result, running the loop to completion.
5
6use crate::error::LlmError;
7use crate::provider::{ChatParams, DynProvider};
8
9use super::LoopDepth;
10use super::ToolRegistry;
11use super::config::{ToolLoopConfig, ToolLoopResult};
12use super::loop_resumable::{ToolLoopHandle, TurnResult};
13
14/// Runs the LLM in a tool-calling loop until completion.
15///
16/// Each iteration:
17/// 1. Calls `provider.stream_boxed()` with the current messages
18/// 2. If the response contains tool calls, executes them via the registry
19/// 3. Appends tool results as messages and repeats
20/// 4. Stops when the LLM returns without tool calls, or max iterations
21///    is reached
22///
23/// # Depth Tracking
24///
25/// If `Ctx` implements [`LoopDepth`], nested calls are tracked automatically.
26/// When `config.max_depth` is set and the context's depth exceeds the limit,
27/// returns `Err(LlmError::MaxDepthExceeded)`.
28///
29/// # Errors
30///
31/// Returns `LlmError` if:
32/// - The provider returns an error
33/// - Max depth is exceeded (returns `LlmError::MaxDepthExceeded`)
34/// - Max iterations is exceeded (returns in result with `TerminationReason::MaxIterations`)
35pub async fn tool_loop<Ctx: LoopDepth + Send + Sync + 'static>(
36    provider: &dyn DynProvider,
37    registry: &ToolRegistry<Ctx>,
38    params: ChatParams,
39    config: ToolLoopConfig,
40    ctx: &Ctx,
41) -> Result<ToolLoopResult, LlmError> {
42    let mut handle = ToolLoopHandle::new(provider, registry, params, config, ctx);
43    loop {
44        match handle.next_turn().await {
45            TurnResult::Yielded(turn) => turn.continue_loop(),
46            TurnResult::Completed(done) => {
47                return Ok(ToolLoopResult {
48                    response: done.response,
49                    iterations: done.iterations,
50                    total_usage: done.total_usage,
51                    termination_reason: done.termination_reason,
52                    // Events silently dropped — tool_loop is fire-and-forget.
53                    // Use ToolLoopHandle or tool_loop_stream for event access.
54                });
55            }
56            TurnResult::Error(err) => return Err(err.error),
57        }
58    }
59}