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}