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}