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::{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
115impl_yielded_methods!(OwnedYielded<'h>);
116
117// ── OwnedToolLoopHandle ─────────────────────────────────────────────
118
119/// Arc-owned resumable tool loop.
120///
121/// Identical in behavior to [`ToolLoopHandle`](super::ToolLoopHandle) but
122/// owns provider and registry via `Arc`, making it `Send + 'static`.
123///
124/// # Lifecycle
125///
126/// Same as `ToolLoopHandle`:
127///
128/// 1. Create with [`new()`](Self::new)
129/// 2. Call [`next_turn()`](Self::next_turn)
130/// 3. If `Yielded`, consume via `resume()` / `continue_loop()` / etc.
131/// 4. Repeat until `Completed` or `Error`
132/// 5. Optionally call [`into_result()`](Self::into_result)
133pub struct OwnedToolLoopHandle<Ctx: LoopDepth + Send + Sync + 'static> {
134    provider: Arc<dyn DynProvider>,
135    registry: Arc<ToolRegistry<Ctx>>,
136    core: LoopCore<Ctx>,
137}
138
139impl<Ctx: LoopDepth + Send + Sync + 'static> OwnedToolLoopHandle<Ctx> {
140    /// Create a new owned resumable tool loop.
141    ///
142    /// Takes `Arc`-wrapped provider and registry so the handle can be
143    /// moved into `tokio::spawn`.
144    pub fn new(
145        provider: Arc<dyn DynProvider>,
146        registry: Arc<ToolRegistry<Ctx>>,
147        params: ChatParams,
148        config: ToolLoopConfig,
149        ctx: &Ctx,
150    ) -> Self {
151        Self {
152            provider,
153            registry,
154            core: LoopCore::new(params, config, ctx),
155        }
156    }
157
158    /// Internal constructor from a pre-built `LoopCore`.
159    ///
160    /// Used by `ToolLoopHandle::into_owned()`.
161    pub(crate) fn from_core(
162        provider: Arc<dyn DynProvider>,
163        registry: Arc<ToolRegistry<Ctx>>,
164        core: LoopCore<Ctx>,
165    ) -> Self {
166        Self {
167            provider,
168            registry,
169            core,
170        }
171    }
172
173    /// Advance the loop and return the result of this turn.
174    ///
175    /// Identical semantics to [`ToolLoopHandle::next_turn()`](super::ToolLoopHandle::next_turn).
176    pub async fn next_turn(&mut self) -> OwnedTurnResult<'_, Ctx> {
177        let outcome = self
178            .core
179            .do_iteration(&*self.provider, &self.registry)
180            .await;
181        outcome_to_turn_result!(outcome, self, OwnedTurnResult, OwnedYielded)
182    }
183
184    /// Tell the loop how to proceed before the next `next_turn()` call.
185    pub fn resume(&mut self, command: LoopCommand) {
186        self.core.resume(command);
187    }
188
189    /// Get a snapshot of the current conversation messages.
190    pub fn messages(&self) -> &[ChatMessage] {
191        self.core.messages()
192    }
193
194    /// Get a mutable reference to the conversation messages.
195    pub fn messages_mut(&mut self) -> &mut Vec<ChatMessage> {
196        self.core.messages_mut()
197    }
198
199    /// Get the accumulated usage across all iterations so far.
200    pub fn total_usage(&self) -> &Usage {
201        self.core.total_usage()
202    }
203
204    /// Get the current iteration count.
205    pub fn iterations(&self) -> u32 {
206        self.core.iterations()
207    }
208
209    /// Whether the loop has finished (returned Completed or Error).
210    pub fn is_finished(&self) -> bool {
211        self.core.is_finished()
212    }
213
214    /// Consume the handle and return a `ToolLoopResult`.
215    pub fn into_result(self) -> ToolLoopResult {
216        self.core.into_result()
217    }
218}
219
220impl<Ctx: LoopDepth + Send + Sync + 'static> std::fmt::Debug for OwnedToolLoopHandle<Ctx> {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.debug_struct("OwnedToolLoopHandle")
223            .field("core", &self.core)
224            .finish_non_exhaustive()
225    }
226}