Skip to main content

llm_stack/tool/
depth.rs

1//! Loop depth tracking for nested tool loops.
2//!
3//! When tools spawn sub-agents (nested `tool_loop` calls), depth tracking
4//! prevents runaway recursion. Use [`LoopContext`] for built-in depth
5//! management, or implement [`LoopDepth`] manually on your own type.
6//!
7//! # Depth semantics
8//!
9//! - **Depth 0** = the outermost (top-level) loop. This is the initial
10//!   context created by the application before entering any tool loop.
11//! - **Depth N** = the context is inside N levels of nested tool loops.
12//! - `LoopCore::new()` auto-increments: if you pass a context at depth 0,
13//!   tools inside that loop receive a context at depth 1. Tools that spawn
14//!   their own `tool_loop` will pass depth 1 in, and their inner tools
15//!   see depth 2, etc.
16//! - `max_depth` is checked *before* incrementing: if `ctx.loop_depth() >= max_depth`,
17//!   the loop immediately returns `LlmError::MaxDepthExceeded`. With the default
18//!   `max_depth = Some(3)`, the deepest allowed nesting is master(0) → worker(1) → sub-worker(2).
19//!   A tool at depth 2 trying to spawn another loop would be rejected (2 >= 3 is false,
20//!   but a tool at depth 3 would be: 3 >= 3 is true).
21//!
22//! # Using `LoopContext` (recommended)
23//!
24//! ```rust
25//! use llm_stack::tool::LoopContext;
26//!
27//! // Wrap your application state — depth tracking is automatic
28//! let ctx = LoopContext::new(MyState { user_id: "u123".into() });
29//! # #[derive(Clone)] struct MyState { user_id: String }
30//! ```
31//!
32//! # Manual implementation
33//!
34//! ```rust
35//! use llm_stack::tool::LoopDepth;
36//!
37//! #[derive(Clone)]
38//! struct AgentContext {
39//!     user_id: String,
40//!     depth: u32,
41//! }
42//!
43//! impl LoopDepth for AgentContext {
44//!     fn loop_depth(&self) -> u32 {
45//!         self.depth
46//!     }
47//!
48//!     fn with_depth(&self, depth: u32) -> Self {
49//!         Self {
50//!             depth,
51//!             ..self.clone()
52//!         }
53//!     }
54//! }
55//! ```
56
57/// Trait for contexts that support automatic depth tracking in nested tool loops.
58///
59/// When `tool_loop` executes tools, `LoopCore` calls
60/// `ctx.with_depth(ctx.loop_depth() + 1)` and passes the result to tool
61/// handlers. If a tool handler then enters its own `tool_loop`, the depth
62/// is checked against [`ToolLoopConfig::max_depth`](super::ToolLoopConfig::max_depth)
63/// and the loop is rejected if the limit is reached.
64///
65/// # Blanket Implementation
66///
67/// The unit type `()` has a blanket implementation that always returns depth 0
68/// and ignores `with_depth`. Use this for simple cases where depth tracking
69/// isn't needed:
70///
71/// ```rust
72/// use llm_stack::tool::LoopDepth;
73///
74/// assert_eq!(().loop_depth(), 0);
75/// assert_eq!(().with_depth(5), ());
76/// ```
77///
78/// # Custom Implementation
79///
80/// For agent systems with nesting, implement this on your context type:
81///
82/// ```rust
83/// use llm_stack::tool::LoopDepth;
84///
85/// #[derive(Clone)]
86/// struct MyContext {
87///     session_id: String,
88///     loop_depth: u32,
89/// }
90///
91/// impl LoopDepth for MyContext {
92///     fn loop_depth(&self) -> u32 {
93///         self.loop_depth
94///     }
95///
96///     fn with_depth(&self, depth: u32) -> Self {
97///         Self {
98///             loop_depth: depth,
99///             ..self.clone()
100///         }
101///     }
102/// }
103/// ```
104pub trait LoopDepth: Clone + Send + Sync {
105    /// Returns the current nesting depth (0 = top-level, not nested).
106    fn loop_depth(&self) -> u32;
107
108    /// Returns a new context with the specified depth.
109    ///
110    /// Called internally by `LoopCore::new()` as
111    /// `ctx.with_depth(ctx.loop_depth() + 1)` — tool handlers receive
112    /// a context one level deeper than their parent loop.
113    #[must_use]
114    fn with_depth(&self, depth: u32) -> Self;
115}
116
117/// Blanket implementation for unit type — always depth 0, no tracking.
118///
119/// This allows simple use cases to work without implementing the trait:
120///
121/// ```rust
122/// use llm_stack::tool::{ToolLoopConfig, ToolRegistry};
123///
124/// // Works with () context, no depth tracking
125/// let registry: ToolRegistry<()> = ToolRegistry::new();
126/// ```
127impl LoopDepth for () {
128    fn loop_depth(&self) -> u32 {
129        0
130    }
131
132    fn with_depth(&self, _depth: u32) -> Self {}
133}
134
135// ── LoopContext ──────────────────────────────────────────────────────
136
137/// Generic context wrapper with built-in depth tracking.
138///
139/// Wraps any `Clone + Send + Sync` state and automatically implements
140/// [`LoopDepth`], eliminating the boilerplate of storing a `depth` field
141/// and writing the trait impl yourself.
142///
143/// # Examples
144///
145/// ```rust
146/// use llm_stack::tool::{LoopContext, LoopDepth, ToolRegistry};
147///
148/// #[derive(Clone)]
149/// struct AppState {
150///     user_id: String,
151///     api_key: String,
152/// }
153///
154/// let ctx = LoopContext::new(AppState {
155///     user_id: "user_123".into(),
156///     api_key: "sk-secret".into(),
157/// });
158///
159/// assert_eq!(ctx.loop_depth(), 0);
160/// assert_eq!(ctx.state.user_id, "user_123");
161///
162/// // Use with a typed registry
163/// let registry: ToolRegistry<LoopContext<AppState>> = ToolRegistry::new();
164/// ```
165///
166/// For the zero-state case, use `LoopContext<()>`:
167///
168/// ```rust
169/// use llm_stack::tool::{LoopContext, LoopDepth};
170///
171/// let ctx = LoopContext::empty();
172/// assert_eq!(ctx.loop_depth(), 0);
173///
174/// let nested = ctx.with_depth(1);
175/// assert_eq!(nested.loop_depth(), 1);
176/// ```
177#[derive(Clone, Debug)]
178pub struct LoopContext<T: Clone + Send + Sync = ()> {
179    /// The application state accessible from tool handlers.
180    pub state: T,
181    depth: u32,
182}
183
184impl<T: Clone + Send + Sync> LoopContext<T> {
185    /// Create a new context wrapping the given state at depth 0.
186    pub fn new(state: T) -> Self {
187        Self { state, depth: 0 }
188    }
189}
190
191impl LoopContext<()> {
192    /// Create a stateless context at depth 0.
193    pub fn empty() -> Self {
194        Self {
195            state: (),
196            depth: 0,
197        }
198    }
199}
200
201impl<T: Clone + Send + Sync> LoopDepth for LoopContext<T> {
202    fn loop_depth(&self) -> u32 {
203        self.depth
204    }
205
206    fn with_depth(&self, depth: u32) -> Self {
207        Self {
208            state: self.state.clone(),
209            depth,
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_unit_loop_depth() {
220        assert_eq!(().loop_depth(), 0);
221    }
222
223    #[test]
224    #[allow(clippy::let_unit_value)]
225    fn test_unit_with_depth_ignores_value() {
226        let nested = ().with_depth(5);
227        assert_eq!(nested.loop_depth(), 0);
228    }
229
230    #[derive(Clone)]
231    struct TestContext {
232        name: String,
233        depth: u32,
234    }
235
236    impl LoopDepth for TestContext {
237        fn loop_depth(&self) -> u32 {
238            self.depth
239        }
240
241        fn with_depth(&self, depth: u32) -> Self {
242            Self {
243                depth,
244                ..self.clone()
245            }
246        }
247    }
248
249    #[test]
250    fn test_custom_context_depth() {
251        let ctx = TestContext {
252            name: "test".into(),
253            depth: 0,
254        };
255        assert_eq!(ctx.loop_depth(), 0);
256
257        let nested = ctx.with_depth(1);
258        assert_eq!(nested.loop_depth(), 1);
259        assert_eq!(nested.name, "test");
260    }
261
262    #[test]
263    fn test_depth_increments() {
264        let ctx = TestContext {
265            name: "agent".into(),
266            depth: 0,
267        };
268
269        let level1 = ctx.with_depth(ctx.loop_depth() + 1);
270        assert_eq!(level1.loop_depth(), 1);
271
272        let level2 = level1.with_depth(level1.loop_depth() + 1);
273        assert_eq!(level2.loop_depth(), 2);
274    }
275
276    #[test]
277    fn test_loop_context_new() {
278        #[derive(Clone, Debug, PartialEq)]
279        struct State {
280            name: String,
281        }
282
283        let ctx = LoopContext::new(State {
284            name: "test".into(),
285        });
286        assert_eq!(ctx.loop_depth(), 0);
287        assert_eq!(ctx.state.name, "test");
288    }
289
290    #[test]
291    fn test_loop_context_with_depth_preserves_state() {
292        #[derive(Clone, Debug, PartialEq)]
293        struct State {
294            user_id: String,
295            api_key: String,
296        }
297
298        let ctx = LoopContext::new(State {
299            user_id: "u1".into(),
300            api_key: "k1".into(),
301        });
302
303        let nested = ctx.with_depth(3);
304        assert_eq!(nested.loop_depth(), 3);
305        assert_eq!(nested.state.user_id, "u1");
306        assert_eq!(nested.state.api_key, "k1");
307    }
308
309    #[test]
310    fn test_loop_context_empty() {
311        let ctx = LoopContext::empty();
312        assert_eq!(ctx.loop_depth(), 0);
313
314        let nested = ctx.with_depth(2);
315        assert_eq!(nested.loop_depth(), 2);
316    }
317
318    #[test]
319    fn test_loop_context_depth_chain() {
320        let ctx = LoopContext::new("agent");
321        let l1 = ctx.with_depth(ctx.loop_depth() + 1);
322        let l2 = l1.with_depth(l1.loop_depth() + 1);
323        let l3 = l2.with_depth(l2.loop_depth() + 1);
324        assert_eq!(l3.loop_depth(), 3);
325        assert_eq!(l3.state, "agent");
326    }
327}