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}