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}