1use std::collections::HashMap;
50use std::sync::{Arc, Mutex, MutexGuard};
51
52use behest_provider::ToolSpec;
53use behest_tool::{Tool, ToolRegistry};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct ScopeId(pub(crate) usize);
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum ScopeLevel {
66 Global = 0,
68 Agent = 1,
70 Run = 2,
72 Turn = 3,
74}
75
76#[derive(Clone)]
78struct ScopeLayer {
79 tools: HashMap<String, Arc<dyn Tool>>,
80}
81
82struct ScopeState {
84 layers: Vec<(ScopeId, ScopeLayer)>,
85 next_id: usize,
86}
87
88impl Clone for ScopeState {
89 fn clone(&self) -> Self {
90 Self {
91 layers: self.layers.clone(),
92 next_id: self.next_id,
93 }
94 }
95}
96
97type RegisterArcResult = Result<Option<Arc<dyn Tool>>, (String, Arc<dyn Tool>)>;
99
100pub struct ScopedToolRegistry {
109 base: ToolRegistry,
110 state: Mutex<ScopeState>,
111}
112
113impl Clone for ScopedToolRegistry {
114 fn clone(&self) -> Self {
115 let state = self.lock_state_poison_safe();
116 Self {
117 base: self.base.clone(),
118 state: Mutex::new(state.clone()),
119 }
120 }
121}
122
123impl ScopedToolRegistry {
124 #[must_use]
128 pub fn new(base: ToolRegistry) -> Self {
129 Self {
130 base,
131 state: Mutex::new(ScopeState {
132 layers: Vec::new(),
133 next_id: 0,
134 }),
135 }
136 }
137
138 #[must_use]
140 pub fn push_scope(&self) -> ScopeId {
141 let mut state = self.lock_state_poison_safe();
142 let id = ScopeId(state.next_id);
143 state.next_id += 1;
144 state.layers.push((
145 id,
146 ScopeLayer {
147 tools: HashMap::new(),
148 },
149 ));
150 id
151 }
152
153 pub fn pop_scope(&self, id: ScopeId) -> bool {
159 let mut state = self.lock_state_poison_safe();
160 if let Some(&(top_id, _)) = state.layers.last()
161 && top_id == id
162 {
163 state.layers.pop();
164 return true;
165 }
166 false
167 }
168
169 pub fn register_in_scope<T: Tool + 'static>(
179 &self,
180 scope: ScopeId,
181 tool: T,
182 ) -> Result<Option<Arc<dyn Tool>>, T> {
183 let name = tool.name().to_owned();
184 let mut state = self.lock_state_poison_safe();
185 for (id, layer) in &mut state.layers {
186 if *id == scope {
187 let arc: Arc<dyn Tool> = Arc::new(tool);
188 return Ok(layer.tools.insert(name, arc));
189 }
190 }
191 Err(tool)
192 }
193
194 pub fn register_arc_in_scope(
204 &self,
205 scope: ScopeId,
206 name: String,
207 tool: Arc<dyn Tool>,
208 ) -> RegisterArcResult {
209 let mut state = self.lock_state_poison_safe();
210 for (id, layer) in &mut state.layers {
211 if *id == scope {
212 return Ok(layer.tools.insert(name, tool));
213 }
214 }
215 Err((name, tool))
216 }
217
218 #[must_use]
220 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
221 let state = self.lock_state_poison_safe();
222 for (_, layer) in state.layers.iter().rev() {
224 if let Some(tool) = layer.tools.get(name) {
225 return Some(Arc::clone(tool));
226 }
227 }
228 self.base.get(name)
230 }
231
232 #[must_use]
242 pub fn specs(&self) -> Vec<ToolSpec> {
243 let state = self.lock_state_poison_safe();
244 let mut merged: HashMap<String, Arc<dyn Tool>> = HashMap::new();
245
246 for spec in self.base.specs() {
248 if let Some(tool) = self.base.get(&spec.name) {
249 merged.insert(spec.name, tool);
250 }
251 }
252
253 for (_, layer) in &state.layers {
255 for (name, tool) in &layer.tools {
256 merged.insert(name.clone(), Arc::clone(tool));
257 }
258 }
259
260 let mut specs: Vec<ToolSpec> = merged.values().map(|t| t.to_spec()).collect();
261 specs.sort_by(|a, b| a.name.cmp(&b.name));
262 specs
263 }
264
265 #[must_use]
267 pub fn scope_depth(&self) -> usize {
268 let state = self.lock_state_poison_safe();
269 state.layers.len()
270 }
271
272 #[must_use]
274 pub fn base(&self) -> &ToolRegistry {
275 &self.base
276 }
277
278 pub fn base_mut(&mut self) -> &mut ToolRegistry {
280 &mut self.base
281 }
282
283 pub fn unregister_from_base(&self, name: &str) -> Option<Arc<dyn Tool>> {
285 self.base.unregister(name)
286 }
287
288 #[must_use]
290 pub fn len(&self) -> usize {
291 self.specs().len()
292 }
293
294 #[must_use]
296 pub fn is_empty(&self) -> bool {
297 self.specs().is_empty()
298 }
299
300 pub async fn execute(
309 &self,
310 call: &behest_provider::ToolCall,
311 ) -> behest_tool::ToolResult<behest_tool::ToolOutput> {
312 let tool = self
313 .get(&call.name)
314 .ok_or_else(|| behest_core::error::ToolError::NotFound {
315 name: call.name.clone(),
316 })?;
317 tool.execute(call.arguments.clone()).await
318 }
319
320 #[must_use]
327 pub fn push_scope_guarded(self: &Arc<Self>) -> ScopeGuard {
328 let id = self.push_scope();
329 ScopeGuard {
330 scope: Arc::clone(self),
331 id,
332 }
333 }
334
335 fn lock_state_poison_safe(&self) -> MutexGuard<'_, ScopeState> {
336 match self.state.lock() {
337 Ok(guard) => guard,
338 Err(poisoned) => poisoned.into_inner(),
339 }
340 }
341}
342
343pub struct ScopeGuard {
350 scope: Arc<ScopedToolRegistry>,
351 id: ScopeId,
352}
353
354impl ScopeGuard {
355 #[must_use]
357 pub fn id(&self) -> ScopeId {
358 self.id
359 }
360
361 #[must_use]
365 pub fn into_inner(self) -> Arc<ScopedToolRegistry> {
366 let scope = Arc::clone(&self.scope);
367 std::mem::forget(self);
368 scope
369 }
370}
371
372impl Drop for ScopeGuard {
373 fn drop(&mut self) {
374 self.scope.pop_scope(self.id);
375 }
376}
377
378impl std::fmt::Debug for ScopeGuard {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 f.debug_struct("ScopeGuard")
381 .field("id", &self.id)
382 .finish_non_exhaustive()
383 }
384}
385
386#[cfg(test)]
387#[allow(clippy::unwrap_used, clippy::type_complexity, clippy::expect_used)]
388mod tests {
389 use super::*;
390 use behest_tool::FunctionTool;
391 use serde_json::{Value, json};
392
393 fn make_tool(name: &'static str) -> FunctionTool {
394 FunctionTool::new(
395 name,
396 format!("{name} desc"),
397 json!({"type": "object"}),
398 |args| -> std::pin::Pin<
399 Box<dyn std::future::Future<Output = behest_tool::ToolResult<Value>> + Send>,
400 > { Box::pin(async move { Ok(args) }) },
401 )
402 }
403
404 #[test]
405 fn new_should_have_base_tools() {
406 let base = ToolRegistry::new();
407 base.register(make_tool("base_tool"));
408 let scoped = ScopedToolRegistry::new(base);
409
410 assert!(scoped.get("base_tool").is_some());
411 assert!(scoped.get("missing").is_none());
412 assert_eq!(scoped.scope_depth(), 0);
413 }
414
415 #[test]
416 fn push_scope_should_create_empty_scope() {
417 let base = ToolRegistry::new();
418 let scoped = ScopedToolRegistry::new(base);
419 let scope = scoped.push_scope();
420
421 assert_eq!(scoped.scope_depth(), 1);
422 assert!(scoped.get("anything").is_none());
423 let _ = scope;
424 }
425
426 #[test]
427 fn register_in_scope_should_shadow_base() {
428 let base = ToolRegistry::new();
429 base.register(make_tool("shared"));
430 let scoped = ScopedToolRegistry::new(base);
431
432 let scope = scoped.push_scope();
433 scoped
434 .register_in_scope(scope, make_tool("shared"))
435 .ok()
436 .unwrap();
437
438 assert!(scoped.get("shared").is_some());
439 assert_eq!(scoped.specs().len(), 1);
440 }
441
442 #[test]
443 fn pop_scope_should_unshadow_base_tool() {
444 let base = ToolRegistry::new();
445 base.register(make_tool("shared"));
446 let scoped = ScopedToolRegistry::new(base);
447
448 let scope = scoped.push_scope();
449 scoped
450 .register_in_scope(scope, make_tool("shared"))
451 .ok()
452 .unwrap();
453 assert!(scoped.get("shared").is_some());
454
455 scoped.pop_scope(scope);
456 assert!(scoped.get("shared").is_some());
457 assert_eq!(scoped.scope_depth(), 0);
458 }
459
460 #[test]
461 fn pop_scope_should_remove_scope_only_tools() {
462 let base = ToolRegistry::new();
463 let scoped = ScopedToolRegistry::new(base);
464
465 let scope = scoped.push_scope();
466 scoped
467 .register_in_scope(scope, make_tool("scope_only"))
468 .ok()
469 .unwrap();
470 assert!(scoped.get("scope_only").is_some());
471
472 scoped.pop_scope(scope);
473 assert!(scoped.get("scope_only").is_none());
474 }
475
476 #[test]
477 fn pop_scope_non_top_should_return_false() {
478 let base = ToolRegistry::new();
479 let registry = ScopedToolRegistry::new(base);
480
481 let scope1 = registry.push_scope();
482 let scope2 = registry.push_scope();
483
484 assert!(!registry.pop_scope(scope1));
485 assert_eq!(registry.scope_depth(), 2);
486
487 assert!(registry.pop_scope(scope2));
488 assert_eq!(registry.scope_depth(), 1);
489 }
490
491 #[test]
492 fn pop_nonexistent_scope_should_return_false() {
493 let base = ToolRegistry::new();
494 let scoped = ScopedToolRegistry::new(base);
495 assert!(!scoped.pop_scope(ScopeId(999)));
496 }
497
498 #[test]
499 fn multiple_scopes_shadow_correctly() {
500 let base = ToolRegistry::new();
501 let registry = ScopedToolRegistry::new(base);
502
503 let scope1 = registry.push_scope();
504 registry
505 .register_in_scope(scope1, make_tool("tool"))
506 .ok()
507 .unwrap();
508
509 let scope2 = registry.push_scope();
510 registry
511 .register_in_scope(scope2, make_tool("tool"))
512 .ok()
513 .unwrap();
514
515 assert_eq!(registry.specs().len(), 1);
516
517 registry.pop_scope(scope2);
518 assert!(registry.get("tool").is_some());
519 assert_eq!(registry.specs().len(), 1);
520
521 registry.pop_scope(scope1);
522 assert!(registry.get("tool").is_none());
523 }
524
525 #[test]
526 fn register_in_nonexistent_scope_returns_error() {
527 let base = ToolRegistry::new();
528 let scoped = ScopedToolRegistry::new(base);
529 let result = scoped.register_in_scope(ScopeId(999), make_tool("nope"));
530 assert!(result.is_err());
531 }
532
533 #[test]
534 fn register_replaces_within_same_scope() {
535 let base = ToolRegistry::new();
536 let scoped = ScopedToolRegistry::new(base);
537
538 let scope = scoped.push_scope();
539 let old = scoped
540 .register_in_scope(scope, make_tool("tool"))
541 .ok()
542 .unwrap();
543 assert!(old.is_none());
544
545 let old = scoped
546 .register_in_scope(scope, make_tool("tool"))
547 .ok()
548 .unwrap();
549 assert!(old.is_some());
550 assert_eq!(scoped.specs().len(), 1);
551 }
552
553 #[tokio::test]
554 async fn execute_should_resolve_from_scoped_registry() {
555 let base = ToolRegistry::new();
556 let scoped = ScopedToolRegistry::new(base);
557
558 let scope = scoped.push_scope();
559 scoped
560 .register_in_scope(scope, make_tool("tool"))
561 .ok()
562 .unwrap();
563
564 let call = behest_provider::ToolCall::new("c1", "tool", json!({"x": 1}));
565 let output = scoped.execute(&call).await.unwrap();
566 assert_eq!(output.value, json!({"x": 1}));
567 }
568
569 #[tokio::test]
570 async fn execute_unknown_tool_returns_not_found() {
571 let base = ToolRegistry::new();
572 let scoped = ScopedToolRegistry::new(base);
573
574 let call = behest_provider::ToolCall::new("c1", "missing", json!({}));
575 let result = scoped.execute(&call).await;
576 assert!(result.is_err());
577 }
578
579 #[test]
580 fn scope_id_increments_monotonically() {
581 let base = ToolRegistry::new();
582 let scoped = ScopedToolRegistry::new(base);
583 let s1 = scoped.push_scope();
584 let s2 = scoped.push_scope();
585 assert!(s2.0 > s1.0);
586 }
587
588 #[test]
589 fn base_mut_allows_modifying_base() {
590 let base = ToolRegistry::new();
591 let mut scoped = ScopedToolRegistry::new(base);
592 scoped.base_mut().register(make_tool("new_base"));
593 assert!(scoped.get("new_base").is_some());
594 }
595
596 #[test]
597 fn clone_captures_scope_snapshot() {
598 let base = ToolRegistry::new();
599 let scoped = ScopedToolRegistry::new(base);
600
601 let scope = scoped.push_scope();
602 scoped
603 .register_in_scope(scope, make_tool("tool"))
604 .ok()
605 .unwrap();
606
607 let cloned = scoped.clone();
608 assert!(cloned.get("tool").is_some());
609
610 scoped.pop_scope(scope);
612 assert!(scoped.get("tool").is_none());
613 assert!(cloned.get("tool").is_some());
614 }
615
616 #[test]
617 fn scope_guard_pops_on_drop() {
618 let base = ToolRegistry::new();
619 let scoped = Arc::new(ScopedToolRegistry::new(base));
620
621 {
622 let _guard = scoped.push_scope_guarded();
623 assert_eq!(scoped.scope_depth(), 1);
624 }
625 assert_eq!(scoped.scope_depth(), 0);
627 }
628
629 #[test]
630 fn scope_guard_id_matches_pushed_scope() {
631 let base = ToolRegistry::new();
632 let scoped = Arc::new(ScopedToolRegistry::new(base));
633
634 let guard = scoped.push_scope_guarded();
635 let id = guard.id();
636 assert_eq!(scoped.scope_depth(), 1);
638 assert!(scoped.pop_scope(id));
640 }
641
642 #[test]
643 fn scope_guard_into_inner_prevents_drop_cleanup() {
644 let base = ToolRegistry::new();
645 let scoped = Arc::new(ScopedToolRegistry::new(base));
646
647 let guard = scoped.push_scope_guarded();
648 let id = guard.id();
649
650 let _ = guard.into_inner();
652 assert_eq!(scoped.scope_depth(), 1);
654 scoped.pop_scope(id);
656 }
657
658 #[test]
659 fn scope_guard_tools_within_scope_are_visible() {
660 let base = ToolRegistry::new();
661 let scoped = Arc::new(ScopedToolRegistry::new(base));
662
663 let guard = scoped.push_scope_guarded();
664 scoped
665 .register_in_scope(guard.id(), make_tool("scoped_tool"))
666 .ok()
667 .unwrap();
668
669 assert!(scoped.get("scoped_tool").is_some());
670 assert_eq!(scoped.specs().len(), 1);
671
672 drop(guard);
673 assert!(scoped.get("scoped_tool").is_none());
675 assert_eq!(scoped.specs().len(), 0);
676 }
677
678 #[test]
679 fn push_scope_is_thread_safe() {
680 use std::thread;
681
682 let base = ToolRegistry::new();
683 let scoped = Arc::new(ScopedToolRegistry::new(base));
684
685 let handles: Vec<_> = (0..4)
686 .map(|_| {
687 let s = Arc::clone(&scoped);
688 thread::spawn(move || {
689 let _scope = s.push_scope();
690 })
691 })
692 .collect();
693
694 for h in handles {
695 h.join().unwrap();
696 }
697
698 assert_eq!(scoped.scope_depth(), 4);
699 }
700
701 #[test]
702 fn specs_should_return_sorted_by_name() {
703 let base = ToolRegistry::new();
704 base.register(make_tool("zebra"));
705 base.register(make_tool("alpha"));
706 let scoped = ScopedToolRegistry::new(base);
707
708 let scope = scoped.push_scope();
709 scoped
710 .register_in_scope(scope, make_tool("mike"))
711 .ok()
712 .unwrap();
713
714 let specs = scoped.specs();
715 assert_eq!(specs.len(), 3);
716 assert_eq!(specs[0].name, "alpha");
717 assert_eq!(specs[1].name, "mike");
718 assert_eq!(specs[2].name, "zebra");
719 }
720}