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. Implement [`LoopDepth`] on your context type
5//! to enable automatic depth management.
6//!
7//! # Example
8//!
9//! ```rust
10//! use llm_stack::tool::LoopDepth;
11//!
12//! #[derive(Clone)]
13//! struct AgentContext {
14//!     user_id: String,
15//!     depth: u32,
16//! }
17//!
18//! impl LoopDepth for AgentContext {
19//!     fn loop_depth(&self) -> u32 {
20//!         self.depth
21//!     }
22//!
23//!     fn with_depth(&self, depth: u32) -> Self {
24//!         Self {
25//!             depth,
26//!             ..self.clone()
27//!         }
28//!     }
29//! }
30//! ```
31
32/// Trait for contexts that support automatic depth tracking in nested tool loops.
33///
34/// When `tool_loop` executes tools, it passes a context with incremented depth
35/// so that nested loops can enforce depth limits via `max_depth` in
36/// [`ToolLoopConfig`](super::ToolLoopConfig).
37///
38/// # Blanket Implementation
39///
40/// The unit type `()` has a blanket implementation that always returns depth 0.
41/// Use this for simple cases where depth tracking isn't needed:
42///
43/// ```rust
44/// use llm_stack::tool::LoopDepth;
45///
46/// // () always returns 0, ignores depth changes
47/// assert_eq!(().loop_depth(), 0);
48/// assert_eq!(().with_depth(5), ());
49/// ```
50///
51/// # Custom Implementation
52///
53/// For agent systems with nesting, implement this on your context type:
54///
55/// ```rust
56/// use llm_stack::tool::LoopDepth;
57///
58/// #[derive(Clone)]
59/// struct MyContext {
60///     session_id: String,
61///     loop_depth: u32,
62/// }
63///
64/// impl LoopDepth for MyContext {
65///     fn loop_depth(&self) -> u32 {
66///         self.loop_depth
67///     }
68///
69///     fn with_depth(&self, depth: u32) -> Self {
70///         Self {
71///             loop_depth: depth,
72///             ..self.clone()
73///         }
74///     }
75/// }
76/// ```
77pub trait LoopDepth: Clone + Send + Sync {
78    /// Returns the current nesting depth.
79    ///
80    /// A depth of 0 means this is the top-level loop (not nested).
81    fn loop_depth(&self) -> u32;
82
83    /// Returns a new context with the specified depth.
84    ///
85    /// Called by `tool_loop` when passing context to tool handlers,
86    /// incrementing depth for any nested loops.
87    #[must_use]
88    fn with_depth(&self, depth: u32) -> Self;
89}
90
91/// Blanket implementation for unit type — always depth 0, no tracking.
92///
93/// This allows simple use cases to work without implementing the trait:
94///
95/// ```rust
96/// use llm_stack::tool::{ToolLoopConfig, ToolRegistry};
97///
98/// // Works with () context, no depth tracking
99/// let registry: ToolRegistry<()> = ToolRegistry::new();
100/// ```
101impl LoopDepth for () {
102    fn loop_depth(&self) -> u32 {
103        0
104    }
105
106    fn with_depth(&self, _depth: u32) -> Self {}
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_unit_loop_depth() {
115        assert_eq!(().loop_depth(), 0);
116    }
117
118    #[test]
119    #[allow(clippy::let_unit_value)]
120    fn test_unit_with_depth_ignores_value() {
121        // with_depth on () returns (), which still has depth 0
122        let nested = ().with_depth(5);
123        // Even after "nesting", unit context still reports depth 0
124        assert_eq!(nested.loop_depth(), 0);
125    }
126
127    #[derive(Clone)]
128    struct TestContext {
129        name: String,
130        depth: u32,
131    }
132
133    impl LoopDepth for TestContext {
134        fn loop_depth(&self) -> u32 {
135            self.depth
136        }
137
138        fn with_depth(&self, depth: u32) -> Self {
139            Self {
140                depth,
141                ..self.clone()
142            }
143        }
144    }
145
146    #[test]
147    fn test_custom_context_depth() {
148        let ctx = TestContext {
149            name: "test".into(),
150            depth: 0,
151        };
152        assert_eq!(ctx.loop_depth(), 0);
153
154        let nested = ctx.with_depth(1);
155        assert_eq!(nested.loop_depth(), 1);
156        assert_eq!(nested.name, "test"); // Other fields preserved
157    }
158
159    #[test]
160    fn test_depth_increments() {
161        let ctx = TestContext {
162            name: "agent".into(),
163            depth: 0,
164        };
165
166        let level1 = ctx.with_depth(ctx.loop_depth() + 1);
167        assert_eq!(level1.loop_depth(), 1);
168
169        let level2 = level1.with_depth(level1.loop_depth() + 1);
170        assert_eq!(level2.loop_depth(), 2);
171    }
172}