Skip to main content

llm_stack/tool/
loop_owned.rs

1//! Arc-owned resumable tool loop that is `Send + 'static`.
2//!
3//! [`OwnedToolLoopHandle`] is identical in behavior to
4//! [`ToolLoopHandle`](super::ToolLoopHandle) but owns its provider and
5//! registry via `Arc`, making it safe to move into `tokio::spawn` or any
6//! context requiring `Send + 'static`.
7//!
8//! # When to use
9//!
10//! Use `OwnedToolLoopHandle` when the loop must outlive its creator:
11//! - Task agents spawned via `tokio::spawn`
12//! - Holding the handle across an `await` point that requires `'static`
13//! - Sending the handle to another thread
14//!
15//! Use [`ToolLoopHandle`](super::ToolLoopHandle) when the loop lives on the
16//! caller's stack (e.g., a master orchestrator driving the loop directly).
17//!
18//! # Example
19//!
20//! ```rust,no_run
21//! use std::sync::Arc;
22//! use llm_stack::tool::{ToolLoopConfig, ToolRegistry, OwnedToolLoopHandle, OwnedTurnResult};
23//! use llm_stack::{ChatParams, ChatMessage};
24//!
25//! # async fn example(provider: Arc<dyn llm_stack::DynProvider>) {
26//! let registry = Arc::new(ToolRegistry::<()>::new());
27//! let params = ChatParams {
28//!     messages: vec![ChatMessage::user("Hello")],
29//!     ..Default::default()
30//! };
31//!
32//! let mut handle = OwnedToolLoopHandle::new(
33//!     provider,
34//!     registry,
35//!     params,
36//!     ToolLoopConfig::default(),
37//!     &(),
38//! );
39//!
40//! // Safe to spawn because OwnedToolLoopHandle is Send + 'static
41//! tokio::spawn(async move {
42//!     loop {
43//!         match handle.next_turn().await {
44//!             OwnedTurnResult::Yielded(turn) => turn.continue_loop(),
45//!             OwnedTurnResult::Completed(done) => {
46//!                 println!("Done: {:?}", done.response.text());
47//!                 break;
48//!             }
49//!             OwnedTurnResult::Error(err) => {
50//!                 eprintln!("Error: {}", err.error);
51//!                 break;
52//!             }
53//!         }
54//!     }
55//! });
56//! # }
57//! ```
58
59use std::sync::Arc;
60
61use crate::chat::{ChatMessage, ContentBlock, ToolCall, ToolResult};
62use crate::provider::{ChatParams, DynProvider};
63use crate::usage::Usage;
64
65use super::LoopDepth;
66use super::ToolRegistry;
67use super::config::{LoopEvent, ToolLoopConfig, ToolLoopResult};
68use super::loop_core::{CompletedData, ErrorData, IterationOutcome, LoopCore};
69use super::loop_resumable::{
70    Completed, LoopCommand, TurnError, impl_yielded_methods, outcome_to_turn_result,
71};
72
73/// Result of one turn of the owned tool loop.
74///
75/// Same semantics as [`TurnResult`](super::TurnResult) but without the
76/// provider lifetime parameter — this makes `OwnedToolLoopHandle` fully
77/// `Send + 'static`.
78#[must_use = "an OwnedTurnResult must be matched — Yielded requires resume() to continue"]
79pub enum OwnedTurnResult<'h, Ctx: LoopDepth + Send + Sync + 'static> {
80    /// Tools were executed. Consume via `resume()`, `continue_loop()`,
81    /// `inject_and_continue()`, or `stop()`.
82    Yielded(OwnedYielded<'h, Ctx>),
83
84    /// The loop completed.
85    Completed(Completed),
86
87    /// An unrecoverable error occurred.
88    Error(TurnError),
89}
90
91/// Handle returned when tools were executed on an [`OwnedToolLoopHandle`].
92///
93/// Same API as [`Yielded`](super::Yielded) but borrows an
94/// `OwnedToolLoopHandle` instead of a `ToolLoopHandle`.
95#[must_use = "must call .resume(), .continue_loop(), .inject_and_continue(), or .stop() to continue"]
96pub struct OwnedYielded<'h, Ctx: LoopDepth + Send + Sync + 'static> {
97    handle: &'h mut OwnedToolLoopHandle<Ctx>,
98
99    /// The tool calls the LLM requested.
100    pub tool_calls: Vec<ToolCall>,
101
102    /// Results from executing those tool calls.
103    pub results: Vec<ToolResult>,
104
105    /// Text content from the LLM's response alongside the tool calls.
106    pub assistant_content: Vec<ContentBlock>,
107
108    /// Current iteration number (1-indexed).
109    pub iteration: u32,
110
111    /// Accumulated usage across all iterations so far.
112    pub total_usage: Usage,
113
114    /// Lifecycle events from this turn.
115    ///
116    /// See [`Yielded::events`](super::Yielded::events) for details.
117    pub events: Vec<LoopEvent>,
118}
119
120impl_yielded_methods!(OwnedYielded<'h>);
121
122// ── OwnedToolLoopHandle ─────────────────────────────────────────────
123
124/// Arc-owned resumable tool loop.
125///
126/// Identical in behavior to [`ToolLoopHandle`](super::ToolLoopHandle) but
127/// owns provider and registry via `Arc`, making it `Send + 'static`.
128///
129/// # Lifecycle
130///
131/// Same as `ToolLoopHandle`:
132///
133/// 1. Create with [`new()`](Self::new)
134/// 2. Call [`next_turn()`](Self::next_turn)
135/// 3. If `Yielded`, consume via `resume()` / `continue_loop()` / etc.
136/// 4. Repeat until `Completed` or `Error`
137/// 5. Optionally call [`into_result()`](Self::into_result)
138pub struct OwnedToolLoopHandle<Ctx: LoopDepth + Send + Sync + 'static> {
139    provider: Arc<dyn DynProvider>,
140    registry: Arc<ToolRegistry<Ctx>>,
141    core: LoopCore<Ctx>,
142}
143
144impl<Ctx: LoopDepth + Send + Sync + 'static> OwnedToolLoopHandle<Ctx> {
145    /// Create a new owned resumable tool loop.
146    ///
147    /// Takes `Arc`-wrapped provider and registry so the handle can be
148    /// moved into `tokio::spawn`.
149    pub fn new(
150        provider: Arc<dyn DynProvider>,
151        registry: Arc<ToolRegistry<Ctx>>,
152        params: ChatParams,
153        config: ToolLoopConfig,
154        ctx: &Ctx,
155    ) -> Self {
156        Self {
157            provider,
158            registry,
159            core: LoopCore::new(params, config, ctx),
160        }
161    }
162
163    /// Internal constructor from a pre-built `LoopCore`.
164    ///
165    /// Used by `ToolLoopHandle::into_owned()`.
166    pub(crate) fn from_core(
167        provider: Arc<dyn DynProvider>,
168        registry: Arc<ToolRegistry<Ctx>>,
169        core: LoopCore<Ctx>,
170    ) -> Self {
171        Self {
172            provider,
173            registry,
174            core,
175        }
176    }
177
178    /// Advance the loop and return the result of this turn.
179    ///
180    /// Identical semantics to [`ToolLoopHandle::next_turn()`](super::ToolLoopHandle::next_turn).
181    pub async fn next_turn(&mut self) -> OwnedTurnResult<'_, Ctx> {
182        let outcome = self
183            .core
184            .do_iteration(&*self.provider, &self.registry)
185            .await;
186        outcome_to_turn_result!(outcome, self, OwnedTurnResult, OwnedYielded)
187    }
188
189    /// Tell the loop how to proceed before the next `next_turn()` call.
190    pub fn resume(&mut self, command: LoopCommand) {
191        self.core.resume(command);
192    }
193
194    /// Get a snapshot of the current conversation messages.
195    pub fn messages(&self) -> &[ChatMessage] {
196        self.core.messages()
197    }
198
199    /// Get a mutable reference to the conversation messages.
200    pub fn messages_mut(&mut self) -> &mut Vec<ChatMessage> {
201        self.core.messages_mut()
202    }
203
204    /// Get the accumulated usage across all iterations so far.
205    pub fn total_usage(&self) -> &Usage {
206        self.core.total_usage()
207    }
208
209    /// Get the current iteration count.
210    pub fn iterations(&self) -> u32 {
211        self.core.iterations()
212    }
213
214    /// Whether the loop has finished (returned Completed or Error).
215    pub fn is_finished(&self) -> bool {
216        self.core.is_finished()
217    }
218
219    /// Drain any remaining buffered [`LoopEvent`]s.
220    ///
221    /// See [`ToolLoopHandle::drain_events`](super::ToolLoopHandle::drain_events)
222    /// for full documentation.
223    pub fn drain_events(&mut self) -> Vec<LoopEvent> {
224        self.core.drain_events()
225    }
226
227    /// Consume the handle and return a `ToolLoopResult`.
228    pub fn into_result(self) -> ToolLoopResult {
229        self.core.into_result()
230    }
231}
232
233impl<Ctx: LoopDepth + Send + Sync + 'static> std::fmt::Debug for OwnedToolLoopHandle<Ctx> {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        f.debug_struct("OwnedToolLoopHandle")
236            .field("core", &self.core)
237            .finish_non_exhaustive()
238    }
239}