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
33pub fn parse_rate(s: &str) -> Result<(u32, f64), String> {
37 let (b, p) = s
38 .split_once('/')
39 .ok_or_else(|| format!("want \"<burst>/<per>s\" (e.g. \"20/1s\"), got {s:?}"))?;
40 let burst: u32 = b
41 .trim()
42 .parse()
43 .map_err(|_| format!("burst must be a count, got {b:?}"))?;
44 let per = p.trim();
45 let secs: f64 = match per
50 .strip_suffix("sec")
51 .or_else(|| per.strip_suffix('s').filter(|v| !v.ends_with('m')))
52 .unwrap_or(per)
53 .trim()
54 .parse()
55 {
56 Ok(v) => v,
57 Err(_) => crate::config::parse_duration(per)
58 .map_err(|_| format!("window must be a duration, got {p:?}"))?
59 .as_secs_f64(),
60 };
61 if burst == 0 || !secs.is_finite() || secs <= 0.0 {
62 return Err(format!("burst and window must be positive, got {s:?}"));
63 }
64 Ok((burst, secs))
65}
66
67impl TokenBucket {
68 pub fn new(burst: u32, per_sec: f64) -> TokenBucket {
71 let capacity = f64::from(burst);
72 TokenBucket {
73 capacity,
74 tokens: capacity,
75 refill_per_sec: per_sec,
76 last: Instant::now(),
77 }
78 }
79
80 pub fn try_take_at(&mut self, now: Instant) -> bool {
85 let elapsed = now.saturating_duration_since(self.last).as_secs_f64();
86 self.tokens = (self.tokens + elapsed * self.refill_per_sec).min(self.capacity);
87 self.last = now;
88 if self.tokens >= 1.0 {
89 self.tokens -= 1.0;
90 true
91 } else {
92 false
93 }
94 }
95
96 pub fn try_take(&mut self) -> bool {
98 self.try_take_at(Instant::now())
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
104pub struct NodeId(pub u64);
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum NodeStatus {
108 Spawning,
110 Running,
111 Done,
113 Failed,
115}
116
117#[derive(Debug, Clone)]
118pub struct Node {
119 pub id: NodeId,
120 pub parent: Option<NodeId>,
121 pub depth: u32,
122 pub agent_path: String,
124 pub status: NodeStatus,
125 pub tokens: u64,
127 pub children: Vec<NodeId>,
128}
129
130#[derive(Debug, Clone, Copy)]
135pub struct Caps {
136 pub max_depth: u32,
137 pub max_children: u32,
138 pub max_total: u32,
139 pub tree_token_ceiling: u64,
140 pub spawn_rate_burst: u32,
142 pub spawn_rate_per_sec: f64,
144}
145
146impl Default for Caps {
147 fn default() -> Self {
148 Caps {
149 max_depth: 4,
150 max_children: 8,
151 max_total: 64,
152 tree_token_ceiling: 2_000_000,
153 spawn_rate_burst: 8,
154 spawn_rate_per_sec: 2.0,
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum SpawnRefused {
163 Draining,
164 MaxDepth,
165 MaxChildren,
166 MaxTotal,
167 RateExceeded,
168 TreeBudget,
169 UnknownParent,
170}
171
172impl SpawnRefused {
173 pub fn as_str(self) -> &'static str {
174 match self {
175 SpawnRefused::Draining => "tree is draining; no new subagents",
176 SpawnRefused::MaxDepth => "max subagent depth reached",
177 SpawnRefused::MaxChildren => "parent has too many children",
178 SpawnRefused::MaxTotal => "max total subagents reached",
179 SpawnRefused::RateExceeded => "spawn rate exceeded",
180 SpawnRefused::TreeBudget => "tree token budget exhausted",
181 SpawnRefused::UnknownParent => "unknown parent handle",
182 }
183 }
184}
185
186pub struct Tree {
187 nodes: HashMap<NodeId, Node>,
188 next_id: u64,
189 root: Option<NodeId>,
190 draining: bool,
191 total_tokens: u64,
193 spawn_bucket: TokenBucket,
195 caps: Caps,
196}
197
198impl Tree {
199 pub fn new(caps: Caps) -> Tree {
200 Tree {
201 nodes: HashMap::new(),
202 next_id: 0,
203 root: None,
204 draining: false,
205 total_tokens: 0,
206 spawn_bucket: TokenBucket::new(caps.spawn_rate_burst, caps.spawn_rate_per_sec),
207 caps,
208 }
209 }
210
211 pub fn caps(&self) -> Caps {
212 self.caps
213 }
214 pub fn is_draining(&self) -> bool {
215 self.draining
216 }
217 pub fn total_tokens(&self) -> u64 {
218 self.total_tokens
219 }
220 pub fn len(&self) -> usize {
221 self.nodes.len()
222 }
223 pub fn is_empty(&self) -> bool {
224 self.nodes.is_empty()
225 }
226 pub fn get(&self, id: NodeId) -> Option<&Node> {
227 self.nodes.get(&id)
228 }
229 pub fn root(&self) -> Option<NodeId> {
230 self.root
231 }
232
233 pub fn mint_root(&mut self) -> Result<NodeId, SpawnRefused> {
235 if self.draining {
236 return Err(SpawnRefused::Draining);
237 }
238 let id = self.alloc(None, 0, "0".to_string());
239 self.root = Some(id);
240 Ok(id)
241 }
242
243 pub fn mint_child(&mut self, parent: NodeId) -> Result<NodeId, SpawnRefused> {
248 if self.draining {
249 return Err(SpawnRefused::Draining);
250 }
251 if self.total_tokens >= self.caps.tree_token_ceiling {
252 return Err(SpawnRefused::TreeBudget);
253 }
254 if self.nodes.len() as u32 >= self.caps.max_total {
255 return Err(SpawnRefused::MaxTotal);
256 }
257 let (depth, child_index, parent_path) = {
258 let p = self.nodes.get(&parent).ok_or(SpawnRefused::UnknownParent)?;
259 if p.depth + 1 > self.caps.max_depth {
260 return Err(SpawnRefused::MaxDepth);
261 }
262 if p.children.len() as u32 >= self.caps.max_children {
263 return Err(SpawnRefused::MaxChildren);
264 }
265 (p.depth + 1, p.children.len(), p.agent_path.clone())
266 };
267 if !self.spawn_bucket.try_take() {
271 return Err(SpawnRefused::RateExceeded);
272 }
273 let path = format!("{parent_path}.{child_index}");
274 let id = self.alloc(Some(parent), depth, path);
275 if let Some(p) = self.nodes.get_mut(&parent) {
276 p.children.push(id);
277 }
278 Ok(id)
279 }
280
281 fn alloc(&mut self, parent: Option<NodeId>, depth: u32, agent_path: String) -> NodeId {
282 let id = NodeId(self.next_id);
283 self.next_id += 1;
284 self.nodes.insert(
285 id,
286 Node {
287 id,
288 parent,
289 depth,
290 agent_path,
291 status: NodeStatus::Spawning,
292 tokens: 0,
293 children: Vec::new(),
294 },
295 );
296 id
297 }
298
299 pub fn set_status(&mut self, id: NodeId, status: NodeStatus) {
300 if let Some(n) = self.nodes.get_mut(&id) {
301 n.status = status;
302 }
303 }
304
305 pub fn charge_tokens(&mut self, id: NodeId, tokens: u64) -> bool {
308 if let Some(n) = self.nodes.get_mut(&id) {
309 n.tokens = n.tokens.saturating_add(tokens);
310 }
311 self.total_tokens = self.total_tokens.saturating_add(tokens);
312 self.total_tokens >= self.caps.tree_token_ceiling
313 }
314
315 pub fn set_draining(&mut self) {
319 self.draining = true;
320 }
321
322 pub fn deepest_first(&self) -> Vec<NodeId> {
326 let mut ids: Vec<NodeId> = self.nodes.keys().copied().collect();
327 ids.sort_by(|a, b| {
328 let da = self.nodes[a].depth;
329 let db = self.nodes[b].depth;
330 db.cmp(&da).then(b.cmp(a))
331 });
332 ids
333 }
334
335 pub fn remove(&mut self, id: NodeId) -> Option<Node> {
336 let node = self.nodes.remove(&id)?;
337 if let Some(p) = node.parent.and_then(|parent| self.nodes.get_mut(&parent)) {
338 p.children.retain(|c| *c != id);
339 }
340 Some(node)
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn root_then_child_depth_and_path() {
350 let mut t = Tree::new(Caps::default());
351 let root = t.mint_root().unwrap();
352 assert_eq!(t.get(root).unwrap().depth, 0);
353 assert_eq!(t.get(root).unwrap().agent_path, "0");
354 let c0 = t.mint_child(root).unwrap();
355 let c1 = t.mint_child(root).unwrap();
356 assert_eq!(t.get(c0).unwrap().depth, 1);
357 assert_eq!(t.get(c0).unwrap().agent_path, "0.0");
358 assert_eq!(t.get(c1).unwrap().agent_path, "0.1");
359 assert_eq!(t.get(root).unwrap().children.len(), 2);
360 }
361
362 #[test]
363 fn depth_cap_refuses() {
364 let caps = Caps {
365 max_depth: 2,
366 ..Caps::default()
367 };
368 let mut t = Tree::new(caps);
369 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);
373 }
374
375 #[test]
376 fn children_cap_refuses() {
377 let caps = Caps {
378 max_children: 2,
379 ..Caps::default()
380 };
381 let mut t = Tree::new(caps);
382 let root = t.mint_root().unwrap();
383 t.mint_child(root).unwrap();
384 t.mint_child(root).unwrap();
385 assert_eq!(t.mint_child(root).unwrap_err(), SpawnRefused::MaxChildren);
386 }
387
388 #[test]
389 fn total_cap_refuses() {
390 let caps = Caps {
391 max_total: 2,
392 max_children: 10,
393 ..Caps::default()
394 };
395 let mut t = Tree::new(caps);
396 let root = t.mint_root().unwrap(); t.mint_child(root).unwrap(); assert_eq!(t.mint_child(root).unwrap_err(), SpawnRefused::MaxTotal);
399 }
400
401 #[test]
402 fn draining_refuses_new_spawns() {
403 let mut t = Tree::new(Caps::default());
404 let root = t.mint_root().unwrap();
405 t.set_draining();
406 assert_eq!(t.mint_child(root).unwrap_err(), SpawnRefused::Draining);
407 }
408
409 #[test]
410 fn token_accounting_rolls_to_root_and_trips_ceiling() {
411 let caps = Caps {
412 tree_token_ceiling: 100,
413 ..Caps::default()
414 };
415 let mut t = Tree::new(caps);
416 let root = t.mint_root().unwrap();
417 let c = t.mint_child(root).unwrap();
418 assert!(!t.charge_tokens(c, 60));
419 assert_eq!(t.total_tokens(), 60);
420 assert!(t.charge_tokens(c, 40)); assert_eq!(t.get(c).unwrap().tokens, 100);
422 }
423
424 #[test]
425 fn token_bucket_burst_then_refill() {
426 use std::time::Duration;
427 let mut b = TokenBucket::new(8, 2.0);
429 let t0 = Instant::now();
430 for i in 0..8 {
432 assert!(b.try_take_at(t0), "burst token {i} should be available");
433 }
434 assert!(
436 !b.try_take_at(t0),
437 "9th take with no refill must be refused"
438 );
439 assert!(!b.try_take_at(t0 + Duration::from_millis(400)));
441 let t1 = t0 + Duration::from_millis(500);
444 assert!(b.try_take_at(t1), "one token refills after the interval");
445 assert!(!b.try_take_at(t1), "and only one — the refill is metered");
446 }
447
448 #[test]
449 fn token_bucket_caps_at_burst() {
450 use std::time::Duration;
451 let mut b = TokenBucket::new(8, 2.0);
453 let t0 = Instant::now();
454 let far = t0 + Duration::from_secs(3600);
455 for _ in 0..8 {
456 assert!(b.try_take_at(far));
457 }
458 assert!(
459 !b.try_take_at(far),
460 "no more than `burst` tokens ever accrue"
461 );
462 }
463
464 #[test]
465 fn spawn_rate_cap_refuses_after_burst() {
466 let caps = Caps {
470 max_children: 100,
471 max_total: 100,
472 spawn_rate_burst: 3,
473 spawn_rate_per_sec: 1000.0, ..Caps::default()
475 };
476 let mut t = Tree::new(caps);
477 let root = t.mint_root().unwrap();
478 t.mint_child(root).unwrap();
479 t.mint_child(root).unwrap();
480 t.mint_child(root).unwrap();
481 assert_eq!(
482 t.mint_child(root).unwrap_err(),
483 SpawnRefused::RateExceeded,
484 "the 4th rapid spawn is rate-limited, not breadth-limited"
485 );
486 std::thread::sleep(std::time::Duration::from_millis(5));
488 assert!(
489 t.mint_child(root).is_ok(),
490 "a refilled token re-admits a spawn"
491 );
492 }
493
494 #[test]
495 fn deepest_first_orders_children_before_parents() {
496 let mut t = Tree::new(Caps::default());
497 let root = t.mint_root().unwrap();
498 let a = t.mint_child(root).unwrap();
499 let b = t.mint_child(a).unwrap();
500 let order = t.deepest_first();
501 let pos = |id: NodeId| order.iter().position(|x| *x == id).unwrap();
502 assert!(pos(b) < pos(a));
503 assert!(pos(a) < pos(root));
504 }
505}