1use std::collections::HashMap;
13use std::time::Instant;
14
15#[derive(Debug, Clone, Copy)]
22pub struct TokenBucket {
23 capacity: f64,
25 tokens: f64,
27 refill_per_sec: f64,
29 last: Instant,
31}
32
33impl TokenBucket {
34 pub fn new(burst: u32, per_sec: f64) -> TokenBucket {
37 let capacity = f64::from(burst);
38 TokenBucket {
39 capacity,
40 tokens: capacity,
41 refill_per_sec: per_sec,
42 last: Instant::now(),
43 }
44 }
45
46 pub fn try_take_at(&mut self, now: Instant) -> bool {
51 let elapsed = now.saturating_duration_since(self.last).as_secs_f64();
52 self.tokens = (self.tokens + elapsed * self.refill_per_sec).min(self.capacity);
53 self.last = now;
54 if self.tokens >= 1.0 {
55 self.tokens -= 1.0;
56 true
57 } else {
58 false
59 }
60 }
61
62 pub fn try_take(&mut self) -> bool {
64 self.try_take_at(Instant::now())
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
70pub struct NodeId(pub u64);
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum NodeStatus {
74 Spawning,
76 Running,
77 Done,
79 Failed,
81}
82
83#[derive(Debug, Clone)]
84pub struct Node {
85 pub id: NodeId,
86 pub parent: Option<NodeId>,
87 pub depth: u32,
88 pub agent_path: String,
90 pub status: NodeStatus,
91 pub tokens: u64,
93 pub children: Vec<NodeId>,
94}
95
96#[derive(Debug, Clone, Copy)]
100pub struct Caps {
101 pub max_depth: u32,
102 pub max_children: u32,
103 pub max_total: u32,
104 pub tree_token_ceiling: u64,
105 pub spawn_rate_burst: u32,
107 pub spawn_rate_per_sec: f64,
109}
110
111impl Default for Caps {
112 fn default() -> Self {
113 Caps {
114 max_depth: 4,
115 max_children: 8,
116 max_total: 64,
117 tree_token_ceiling: 2_000_000,
118 spawn_rate_burst: 8,
119 spawn_rate_per_sec: 2.0,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum SpawnRefused {
128 Draining,
129 MaxDepth,
130 MaxChildren,
131 MaxTotal,
132 RateExceeded,
133 TreeBudget,
134 UnknownParent,
135}
136
137impl SpawnRefused {
138 pub fn as_str(self) -> &'static str {
139 match self {
140 SpawnRefused::Draining => "tree is draining; no new subagents",
141 SpawnRefused::MaxDepth => "max subagent depth reached",
142 SpawnRefused::MaxChildren => "parent has too many children",
143 SpawnRefused::MaxTotal => "max total subagents reached",
144 SpawnRefused::RateExceeded => "spawn rate exceeded",
145 SpawnRefused::TreeBudget => "tree token budget exhausted",
146 SpawnRefused::UnknownParent => "unknown parent handle",
147 }
148 }
149}
150
151pub struct Tree {
152 nodes: HashMap<NodeId, Node>,
153 next_id: u64,
154 root: Option<NodeId>,
155 draining: bool,
156 total_tokens: u64,
158 spawn_bucket: TokenBucket,
160 caps: Caps,
161}
162
163impl Tree {
164 pub fn new(caps: Caps) -> Tree {
165 Tree {
166 nodes: HashMap::new(),
167 next_id: 0,
168 root: None,
169 draining: false,
170 total_tokens: 0,
171 spawn_bucket: TokenBucket::new(caps.spawn_rate_burst, caps.spawn_rate_per_sec),
172 caps,
173 }
174 }
175
176 pub fn caps(&self) -> Caps {
177 self.caps
178 }
179 pub fn is_draining(&self) -> bool {
180 self.draining
181 }
182 pub fn total_tokens(&self) -> u64 {
183 self.total_tokens
184 }
185 pub fn len(&self) -> usize {
186 self.nodes.len()
187 }
188 pub fn is_empty(&self) -> bool {
189 self.nodes.is_empty()
190 }
191 pub fn get(&self, id: NodeId) -> Option<&Node> {
192 self.nodes.get(&id)
193 }
194 pub fn root(&self) -> Option<NodeId> {
195 self.root
196 }
197
198 pub fn mint_root(&mut self) -> Result<NodeId, SpawnRefused> {
200 if self.draining {
201 return Err(SpawnRefused::Draining);
202 }
203 let id = self.alloc(None, 0, "0".to_string());
204 self.root = Some(id);
205 Ok(id)
206 }
207
208 pub fn mint_child(&mut self, parent: NodeId) -> Result<NodeId, SpawnRefused> {
212 if self.draining {
213 return Err(SpawnRefused::Draining);
214 }
215 if self.total_tokens >= self.caps.tree_token_ceiling {
216 return Err(SpawnRefused::TreeBudget);
217 }
218 if self.nodes.len() as u32 >= self.caps.max_total {
219 return Err(SpawnRefused::MaxTotal);
220 }
221 let (depth, child_index, parent_path) = {
222 let p = self.nodes.get(&parent).ok_or(SpawnRefused::UnknownParent)?;
223 if p.depth + 1 > self.caps.max_depth {
224 return Err(SpawnRefused::MaxDepth);
225 }
226 if p.children.len() as u32 >= self.caps.max_children {
227 return Err(SpawnRefused::MaxChildren);
228 }
229 (p.depth + 1, p.children.len(), p.agent_path.clone())
230 };
231 if !self.spawn_bucket.try_take() {
235 return Err(SpawnRefused::RateExceeded);
236 }
237 let path = format!("{parent_path}.{child_index}");
238 let id = self.alloc(Some(parent), depth, path);
239 if let Some(p) = self.nodes.get_mut(&parent) {
240 p.children.push(id);
241 }
242 Ok(id)
243 }
244
245 fn alloc(&mut self, parent: Option<NodeId>, depth: u32, agent_path: String) -> NodeId {
246 let id = NodeId(self.next_id);
247 self.next_id += 1;
248 self.nodes.insert(
249 id,
250 Node {
251 id,
252 parent,
253 depth,
254 agent_path,
255 status: NodeStatus::Spawning,
256 tokens: 0,
257 children: Vec::new(),
258 },
259 );
260 id
261 }
262
263 pub fn set_status(&mut self, id: NodeId, status: NodeStatus) {
264 if let Some(n) = self.nodes.get_mut(&id) {
265 n.status = status;
266 }
267 }
268
269 pub fn charge_tokens(&mut self, id: NodeId, tokens: u64) -> bool {
272 if let Some(n) = self.nodes.get_mut(&id) {
273 n.tokens = n.tokens.saturating_add(tokens);
274 }
275 self.total_tokens = self.total_tokens.saturating_add(tokens);
276 self.total_tokens >= self.caps.tree_token_ceiling
277 }
278
279 pub fn set_draining(&mut self) {
283 self.draining = true;
284 }
285
286 pub fn deepest_first(&self) -> Vec<NodeId> {
289 let mut ids: Vec<NodeId> = self.nodes.keys().copied().collect();
290 ids.sort_by(|a, b| {
291 let da = self.nodes[a].depth;
292 let db = self.nodes[b].depth;
293 db.cmp(&da).then(b.cmp(a))
294 });
295 ids
296 }
297
298 pub fn remove(&mut self, id: NodeId) -> Option<Node> {
299 let node = self.nodes.remove(&id)?;
300 if let Some(p) = node.parent.and_then(|parent| self.nodes.get_mut(&parent)) {
301 p.children.retain(|c| *c != id);
302 }
303 Some(node)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn root_then_child_depth_and_path() {
313 let mut t = Tree::new(Caps::default());
314 let root = t.mint_root().unwrap();
315 assert_eq!(t.get(root).unwrap().depth, 0);
316 assert_eq!(t.get(root).unwrap().agent_path, "0");
317 let c0 = t.mint_child(root).unwrap();
318 let c1 = t.mint_child(root).unwrap();
319 assert_eq!(t.get(c0).unwrap().depth, 1);
320 assert_eq!(t.get(c0).unwrap().agent_path, "0.0");
321 assert_eq!(t.get(c1).unwrap().agent_path, "0.1");
322 assert_eq!(t.get(root).unwrap().children.len(), 2);
323 }
324
325 #[test]
326 fn depth_cap_refuses() {
327 let caps = Caps {
328 max_depth: 2,
329 ..Caps::default()
330 };
331 let mut t = Tree::new(caps);
332 let root = t.mint_root().unwrap(); let a = t.mint_child(root).unwrap(); let b = t.mint_child(a).unwrap(); assert_eq!(t.mint_child(b).unwrap_err(), SpawnRefused::MaxDepth);
336 }
337
338 #[test]
339 fn children_cap_refuses() {
340 let caps = Caps {
341 max_children: 2,
342 ..Caps::default()
343 };
344 let mut t = Tree::new(caps);
345 let root = t.mint_root().unwrap();
346 t.mint_child(root).unwrap();
347 t.mint_child(root).unwrap();
348 assert_eq!(t.mint_child(root).unwrap_err(), SpawnRefused::MaxChildren);
349 }
350
351 #[test]
352 fn total_cap_refuses() {
353 let caps = Caps {
354 max_total: 2,
355 max_children: 10,
356 ..Caps::default()
357 };
358 let mut t = Tree::new(caps);
359 let root = t.mint_root().unwrap(); t.mint_child(root).unwrap(); assert_eq!(t.mint_child(root).unwrap_err(), SpawnRefused::MaxTotal);
362 }
363
364 #[test]
365 fn draining_refuses_new_spawns() {
366 let mut t = Tree::new(Caps::default());
367 let root = t.mint_root().unwrap();
368 t.set_draining();
369 assert_eq!(t.mint_child(root).unwrap_err(), SpawnRefused::Draining);
370 }
371
372 #[test]
373 fn token_accounting_rolls_to_root_and_trips_ceiling() {
374 let caps = Caps {
375 tree_token_ceiling: 100,
376 ..Caps::default()
377 };
378 let mut t = Tree::new(caps);
379 let root = t.mint_root().unwrap();
380 let c = t.mint_child(root).unwrap();
381 assert!(!t.charge_tokens(c, 60));
382 assert_eq!(t.total_tokens(), 60);
383 assert!(t.charge_tokens(c, 40)); assert_eq!(t.get(c).unwrap().tokens, 100);
385 }
386
387 #[test]
388 fn token_bucket_burst_then_refill() {
389 use std::time::Duration;
390 let mut b = TokenBucket::new(8, 2.0);
392 let t0 = Instant::now();
393 for i in 0..8 {
395 assert!(b.try_take_at(t0), "burst token {i} should be available");
396 }
397 assert!(
399 !b.try_take_at(t0),
400 "9th take with no refill must be refused"
401 );
402 assert!(!b.try_take_at(t0 + Duration::from_millis(400)));
404 let t1 = t0 + Duration::from_millis(500);
407 assert!(b.try_take_at(t1), "one token refills after the interval");
408 assert!(!b.try_take_at(t1), "and only one — the refill is metered");
409 }
410
411 #[test]
412 fn token_bucket_caps_at_burst() {
413 use std::time::Duration;
414 let mut b = TokenBucket::new(8, 2.0);
416 let t0 = Instant::now();
417 let far = t0 + Duration::from_secs(3600);
418 for _ in 0..8 {
419 assert!(b.try_take_at(far));
420 }
421 assert!(
422 !b.try_take_at(far),
423 "no more than `burst` tokens ever accrue"
424 );
425 }
426
427 #[test]
428 fn spawn_rate_cap_refuses_after_burst() {
429 let caps = Caps {
433 max_children: 100,
434 max_total: 100,
435 spawn_rate_burst: 3,
436 spawn_rate_per_sec: 1000.0, ..Caps::default()
438 };
439 let mut t = Tree::new(caps);
440 let root = t.mint_root().unwrap();
441 t.mint_child(root).unwrap();
442 t.mint_child(root).unwrap();
443 t.mint_child(root).unwrap();
444 assert_eq!(
445 t.mint_child(root).unwrap_err(),
446 SpawnRefused::RateExceeded,
447 "the 4th rapid spawn is rate-limited, not breadth-limited"
448 );
449 std::thread::sleep(std::time::Duration::from_millis(5));
451 assert!(
452 t.mint_child(root).is_ok(),
453 "a refilled token re-admits a spawn"
454 );
455 }
456
457 #[test]
458 fn deepest_first_orders_children_before_parents() {
459 let mut t = Tree::new(Caps::default());
460 let root = t.mint_root().unwrap();
461 let a = t.mint_child(root).unwrap();
462 let b = t.mint_child(a).unwrap();
463 let order = t.deepest_first();
464 let pos = |id: NodeId| order.iter().position(|x| *x == id).unwrap();
465 assert!(pos(b) < pos(a));
466 assert!(pos(a) < pos(root));
467 }
468}