1#![allow(clippy::items_after_test_module)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::sync::{Arc, RwLock};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 PreparedToolCall, ProgressSender, ToolCall, ToolContext, ToolContract, ToolManifest,
10 ToolPrepareCall, ToolProvider, ToolResult,
11};
12
13const PLUGIN_SOURCE_ID: &str = "plugins";
14
15#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16#[serde(transparent)]
17pub struct ToolSourceHandle {
18 id: String,
19}
20
21impl ToolSourceHandle {
22 pub(crate) fn new(id: impl Into<String>) -> Self {
23 Self { id: id.into() }
24 }
25
26 pub(crate) fn as_str(&self) -> &str {
27 &self.id
28 }
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct ToolStateEntry {
33 manifest: ToolManifest,
34}
35
36impl ToolStateEntry {
37 pub(crate) fn new(manifest: ToolManifest) -> Self {
38 Self { manifest }
39 }
40
41 pub fn manifest(&self) -> &ToolManifest {
42 &self.manifest
43 }
44
45 pub fn manifest_mut(&mut self) -> &mut ToolManifest {
46 &mut self.manifest
47 }
48}
49
50#[derive(Clone, Debug, Default)]
51pub struct ToolState {
52 generation: u64,
53 tools: Arc<BTreeMap<String, ToolStateEntry>>,
54}
55
56impl ToolState {
57 pub(crate) fn new(generation: u64, tools: BTreeMap<String, ToolStateEntry>) -> Self {
58 Self {
59 generation,
60 tools: Arc::new(tools),
61 }
62 }
63
64 pub fn generation(&self) -> u64 {
65 self.generation
66 }
67
68 pub fn with_generation(mut self, generation: u64) -> Self {
69 self.generation = generation;
70 self
71 }
72
73 pub fn tool_manifests(&self) -> Vec<ToolManifest> {
74 self.tools
75 .values()
76 .map(|entry| entry.manifest.clone())
77 .collect()
78 }
79
80 pub fn get(&self, name: &str) -> Option<&ToolStateEntry> {
81 self.tools.get(name)
82 }
83
84 pub fn manifest_mut(&mut self, name: &str) -> Option<&mut ToolManifest> {
85 Arc::make_mut(&mut self.tools)
86 .get_mut(name)
87 .map(|entry| &mut entry.manifest)
88 }
89
90 pub fn contains(&self, name: &str) -> bool {
91 self.tools.contains_key(name)
92 }
93
94 pub fn is_empty(&self) -> bool {
95 self.tools.is_empty()
96 }
97
98 pub fn len(&self) -> usize {
99 self.tools.len()
100 }
101
102 pub fn iter(&self) -> impl Iterator<Item = (&str, &ToolStateEntry)> {
103 self.tools
104 .iter()
105 .map(|(name, entry)| (name.as_str(), entry))
106 }
107
108 pub fn set_availability(
109 &mut self,
110 name: &str,
111 availability: Option<crate::ToolAvailability>,
112 ) -> Result<(), ReconfigureError> {
113 let Some(entry) = Arc::make_mut(&mut self.tools).get_mut(name) else {
114 return Err(ReconfigureError::Validation(format!(
115 "unknown tool `{name}`"
116 )));
117 };
118 entry.manifest.availability_override = availability;
119 Ok(())
120 }
121
122 pub fn retain(&mut self, mut keep: impl FnMut(&str, &ToolStateEntry) -> bool) {
123 Arc::make_mut(&mut self.tools).retain(|name, entry| keep(name, entry));
124 }
125
126 pub fn remove(&mut self, name: &str) -> Option<ToolStateEntry> {
127 Arc::make_mut(&mut self.tools).remove(name)
128 }
129
130 pub(crate) fn entries(&self) -> &BTreeMap<String, ToolStateEntry> {
131 self.tools.as_ref()
132 }
133}
134
135impl Serialize for ToolState {
136 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
137 where
138 S: serde::Serializer,
139 {
140 #[derive(Serialize)]
141 struct ToolStateRef<'a> {
142 generation: u64,
143 tools: &'a BTreeMap<String, ToolStateEntry>,
144 }
145
146 ToolStateRef {
147 generation: self.generation,
148 tools: self.tools.as_ref(),
149 }
150 .serialize(serializer)
151 }
152}
153
154impl<'de> Deserialize<'de> for ToolState {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: serde::Deserializer<'de>,
158 {
159 #[derive(Deserialize)]
160 struct ToolStateOwned {
161 generation: u64,
162 tools: BTreeMap<String, ToolStateEntry>,
163 }
164
165 let owned = ToolStateOwned::deserialize(deserializer)?;
166 Ok(Self {
167 generation: owned.generation,
168 tools: Arc::new(owned.tools),
169 })
170 }
171}
172
173#[async_trait::async_trait]
174pub(crate) trait ToolSourceExecutor: Send + Sync + 'static {
175 fn id(&self) -> &str;
176 fn advertised_tools(&self) -> Vec<ToolManifest>;
177 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
178 self.advertised_tools()
179 .into_iter()
180 .find(|manifest| manifest.name == name)
181 }
182 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>>;
183 async fn prepare_tool_call(
184 &self,
185 call: ToolPrepareCall<'_>,
186 ) -> Result<PreparedToolCall, ToolResult> {
187 Ok(PreparedToolCall::identity(call.pending))
188 }
189 async fn execute(
190 &self,
191 tool: &str,
192 args: &serde_json::Value,
193 context: &ToolContext<'_>,
194 progress: Option<&ProgressSender>,
195 ) -> ToolResult;
196}
197
198struct ToolProviderSource {
199 id: String,
200 provider: Arc<dyn ToolProvider>,
201}
202
203impl ToolProviderSource {
204 fn new(id: impl Into<String>, provider: Arc<dyn ToolProvider>) -> Self {
205 Self {
206 id: id.into(),
207 provider,
208 }
209 }
210}
211
212struct ToolProviderGroupSource {
213 id: String,
214 tools: RwLock<BTreeMap<String, (ToolManifest, usize)>>,
215 providers: Vec<Arc<dyn ToolProvider>>,
216}
217
218impl ToolProviderGroupSource {
219 fn new(id: impl Into<String>, providers: Vec<Arc<dyn ToolProvider>>) -> Self {
220 let mut tools = BTreeMap::new();
221 for (provider_idx, provider) in providers.iter().enumerate() {
222 for manifest in provider.tool_manifests() {
223 tools.insert(manifest.name.clone(), (manifest, provider_idx));
224 }
225 }
226 Self {
227 id: id.into(),
228 tools: RwLock::new(tools),
229 providers,
230 }
231 }
232
233 fn read_advertised_tools(&self) -> Vec<ToolManifest> {
234 let mut tools = BTreeMap::new();
235 for (provider_idx, provider) in self.providers.iter().enumerate() {
236 for manifest in provider.tool_manifests() {
237 tools.insert(manifest.name.clone(), (manifest, provider_idx));
238 }
239 }
240 let manifests = tools
241 .values()
242 .map(|(manifest, _)| manifest.clone())
243 .collect::<Vec<_>>();
244 *self
245 .tools
246 .write()
247 .expect("tool provider group lock poisoned") = tools;
248 manifests
249 }
250
251 fn provider_index_for(&self, name: &str) -> Option<usize> {
252 self.resolve_manifest(name).and_then(|_| {
253 self.tools
254 .read()
255 .expect("tool provider group lock poisoned")
256 .get(name)
257 .map(|(_, provider_idx)| *provider_idx)
258 })
259 }
260}
261
262#[async_trait::async_trait]
263impl ToolSourceExecutor for ToolProviderGroupSource {
264 fn id(&self) -> &str {
265 &self.id
266 }
267
268 fn advertised_tools(&self) -> Vec<ToolManifest> {
269 self.read_advertised_tools()
270 }
271
272 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
273 if let Some((manifest, _)) = self
274 .tools
275 .read()
276 .expect("tool provider group lock poisoned")
277 .get(name)
278 {
279 return Some(manifest.clone());
280 }
281 for (provider_idx, provider) in self.providers.iter().enumerate() {
282 if let Some(manifest) = provider.resolve_manifest(name) {
283 self.tools
284 .write()
285 .expect("tool provider group lock poisoned")
286 .insert(name.to_string(), (manifest.clone(), provider_idx));
287 return Some(manifest);
288 }
289 }
290 None
291 }
292
293 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
294 let provider_idx = self.provider_index_for(name)?;
295 self.providers[provider_idx].resolve_contract(name)
296 }
297
298 async fn prepare_tool_call(
299 &self,
300 call: ToolPrepareCall<'_>,
301 ) -> Result<PreparedToolCall, ToolResult> {
302 let name = call.pending.tool_name.clone();
303 let Some(provider_idx) = self.provider_index_for(&name) else {
304 return Err(ToolResult::err_fmt(format_args!("Unknown tool: {name}")));
305 };
306 self.providers[provider_idx].prepare_tool_call(call).await
307 }
308
309 async fn execute(
310 &self,
311 tool: &str,
312 args: &serde_json::Value,
313 context: &ToolContext<'_>,
314 progress: Option<&ProgressSender>,
315 ) -> ToolResult {
316 let Some(provider_idx) = self.provider_index_for(tool) else {
317 return ToolResult::err_fmt(format_args!("Unknown tool: {tool}"));
318 };
319 self.providers[provider_idx]
320 .execute(ToolCall {
321 name: tool,
322 args,
323 context,
324 progress,
325 })
326 .await
327 }
328}
329
330#[async_trait::async_trait]
331impl ToolSourceExecutor for ToolProviderSource {
332 fn id(&self) -> &str {
333 &self.id
334 }
335
336 fn advertised_tools(&self) -> Vec<ToolManifest> {
337 self.provider.tool_manifests()
338 }
339
340 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
341 self.provider.resolve_manifest(name)
342 }
343
344 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
345 self.provider.resolve_contract(name)
346 }
347
348 async fn prepare_tool_call(
349 &self,
350 call: ToolPrepareCall<'_>,
351 ) -> Result<PreparedToolCall, ToolResult> {
352 self.provider.prepare_tool_call(call).await
353 }
354
355 async fn execute(
356 &self,
357 tool: &str,
358 args: &serde_json::Value,
359 context: &ToolContext<'_>,
360 progress: Option<&ProgressSender>,
361 ) -> ToolResult {
362 self.provider
363 .execute(ToolCall {
364 name: tool,
365 args,
366 context,
367 progress,
368 })
369 .await
370 }
371}
372
373#[derive(Clone)]
374struct ToolRegistryEntry {
375 manifest: ToolManifest,
376 source_id: String,
377}
378
379impl ToolRegistryEntry {
380 fn new(manifest: ToolManifest, source_id: impl Into<String>) -> Self {
381 Self {
382 manifest,
383 source_id: source_id.into(),
384 }
385 }
386
387 fn export(&self) -> ToolStateEntry {
388 ToolStateEntry::new(self.manifest.clone())
389 }
390}
391
392#[derive(Clone)]
393struct ToolRegistryState {
394 generation: u64,
395 tools: BTreeMap<String, ToolRegistryEntry>,
396 next_live_source_id: u64,
397}
398
399#[derive(Debug, thiserror::Error)]
400pub enum ReconfigureError {
401 #[error("validation error: {0}")]
402 Validation(String),
403 #[error("unknown tool source: {0}")]
404 UnknownSource(String),
405 #[error("generation mismatch: expected {expected}, actual {actual}")]
406 GenerationMismatch { expected: u64, actual: u64 },
407}
408
409#[derive(Clone)]
410pub struct ToolRegistry {
411 sources: Arc<RwLock<BTreeMap<String, Arc<dyn ToolSourceExecutor>>>>,
412 state: Arc<RwLock<ToolRegistryState>>,
413}
414
415#[derive(Clone, Copy, Debug, PartialEq, Eq)]
416enum SourceReconcilePolicy {
417 RejectExternalConflicts,
418 OverlayReplacingConflicts,
419}
420
421impl ToolRegistry {
422 pub fn from_tool_provider(provider: Arc<dyn ToolProvider>) -> Result<Self, ReconfigureError> {
423 let registry = Self::empty();
424 registry.upsert_source(Arc::new(ToolProviderSource::new(
425 PLUGIN_SOURCE_ID,
426 provider,
427 )))?;
428 Ok(registry)
429 }
430
431 pub(crate) fn from_tool_providers(
432 providers: Vec<Arc<dyn ToolProvider>>,
433 ) -> Result<Self, ReconfigureError> {
434 let registry = Self::empty();
435 registry.upsert_source(Arc::new(ToolProviderGroupSource::new(
436 PLUGIN_SOURCE_ID,
437 providers,
438 )))?;
439 Ok(registry)
440 }
441
442 pub(crate) fn empty() -> Self {
443 Self {
444 sources: Arc::new(RwLock::new(BTreeMap::new())),
445 state: Arc::new(RwLock::new(ToolRegistryState {
446 generation: 0,
447 tools: BTreeMap::new(),
448 next_live_source_id: 0,
449 })),
450 }
451 }
452
453 pub fn generation(&self) -> u64 {
454 self.state
455 .read()
456 .expect("tool registry state lock poisoned")
457 .generation
458 }
459
460 pub fn export_state(&self) -> ToolState {
461 let state = self
462 .state
463 .read()
464 .expect("tool registry state lock poisoned");
465 ToolState::new(state.generation, export_tool_state_entries(&state.tools))
466 }
467
468 pub fn apply_state(&self, next: ToolState) -> Result<u64, ReconfigureError> {
469 let current_generation = self.generation();
470 if next.generation != current_generation {
471 return Err(ReconfigureError::GenerationMismatch {
472 expected: next.generation,
473 actual: current_generation,
474 });
475 }
476
477 validate_unique_manifest_entries(next.entries().values())?;
478 let rebound_tools = {
479 let sources = self.sources.read().expect("tool source lock poisoned");
480 rebind_tool_state_entries(next.entries(), &sources)?
481 };
482
483 let mut state = self
484 .state
485 .write()
486 .expect("tool registry state lock poisoned");
487 if state.generation != next.generation {
488 return Err(ReconfigureError::GenerationMismatch {
489 expected: next.generation,
490 actual: state.generation,
491 });
492 }
493 state.tools = rebound_tools;
494 state.generation += 1;
495 Ok(state.generation)
496 }
497
498 pub fn restore_state(&self, snapshot: ToolState) -> Result<u64, ReconfigureError> {
512 validate_unique_manifest_entries(snapshot.entries().values())?;
513 let rebound_tools = {
514 let sources = self.sources.read().expect("tool source lock poisoned");
515 rebind_tool_state_entries(snapshot.entries(), &sources)?
516 };
517
518 let mut state = self
519 .state
520 .write()
521 .expect("tool registry state lock poisoned");
522 state.tools = rebound_tools;
523 state.generation = snapshot.generation();
524 Ok(state.generation)
525 }
526
527 pub fn add_tool_provider(
528 &self,
529 provider: Arc<dyn ToolProvider>,
530 ) -> Result<ToolSourceHandle, ReconfigureError> {
531 let source_id = {
532 let mut state = self
533 .state
534 .write()
535 .expect("tool registry state lock poisoned");
536 state.next_live_source_id += 1;
537 format!("live:{}", state.next_live_source_id)
538 };
539 self.upsert_source(Arc::new(ToolProviderSource::new(
540 source_id.clone(),
541 provider,
542 )))?;
543 Ok(ToolSourceHandle::new(source_id))
544 }
545
546 pub(crate) fn compose_session_surface(
547 &self,
548 include_base_tools: bool,
549 context_providers: Vec<Arc<dyn ToolProvider>>,
550 ) -> Result<Self, ReconfigureError> {
551 let registry = if include_base_tools {
552 self.fork_with_state(self.export_state())?
553 } else {
554 Self::empty()
555 };
556 registry.upsert_overlay_source(Arc::new(ToolProviderGroupSource::new(
557 "context",
558 context_providers,
559 )))?;
560 Ok(registry)
561 }
562
563 pub(crate) fn upsert_source(
564 &self,
565 source: Arc<dyn ToolSourceExecutor>,
566 ) -> Result<u64, ReconfigureError> {
567 self.reconcile_source(source, SourceReconcilePolicy::RejectExternalConflicts)
568 }
569
570 fn upsert_overlay_source(
571 &self,
572 source: Arc<dyn ToolSourceExecutor>,
573 ) -> Result<u64, ReconfigureError> {
574 self.reconcile_source(source, SourceReconcilePolicy::OverlayReplacingConflicts)
575 }
576
577 fn reconcile_source(
578 &self,
579 source: Arc<dyn ToolSourceExecutor>,
580 policy: SourceReconcilePolicy,
581 ) -> Result<u64, ReconfigureError> {
582 let source_id = source.id().to_string();
583 let advertised_tools = source
584 .advertised_tools()
585 .into_iter()
586 .map(|manifest| manifest_with_compact_contract(source.as_ref(), manifest))
587 .collect::<Vec<_>>();
588 validate_unique_manifests(&advertised_tools)?;
589
590 let advertised_names = advertised_tools
591 .iter()
592 .map(|manifest| manifest.name.clone())
593 .collect::<BTreeSet<_>>();
594 let advertised_ids = advertised_tools
595 .iter()
596 .map(|manifest| manifest.id.clone())
597 .collect::<BTreeSet<_>>();
598 let mut state = self
599 .state
600 .write()
601 .expect("tool registry state lock poisoned");
602 let same_source_names = state
603 .tools
604 .iter()
605 .filter_map(|(name, entry)| (entry.source_id == source_id).then_some(name.clone()))
606 .collect::<BTreeSet<_>>();
607 let previous_overrides = state
608 .tools
609 .iter()
610 .map(|(name, entry)| (name.clone(), entry.manifest.availability_override))
611 .collect::<BTreeMap<_, _>>();
612 match policy {
613 SourceReconcilePolicy::RejectExternalConflicts => {
614 for manifest in &advertised_tools {
615 if let Some(existing) = state.tools.get(&manifest.name)
616 && existing.source_id != source_id
617 {
618 return Err(ReconfigureError::Validation(format!(
619 "duplicate tool name `{}` from source `{}` conflicts with source `{}`",
620 manifest.name, source_id, existing.source_id
621 )));
622 }
623 if let Some((existing_name, existing)) =
624 state.tools.iter().find(|(_, entry)| {
625 entry.source_id != source_id && entry.manifest.id == manifest.id
626 })
627 {
628 return Err(ReconfigureError::Validation(format!(
629 "duplicate tool id `{}` from source `{}` conflicts with tool `{}` from source `{}`",
630 manifest.id, source_id, existing_name, existing.source_id
631 )));
632 }
633 }
634 state.tools.retain(|name, entry| {
635 entry.source_id != source_id || !same_source_names.contains(name)
636 });
637 }
638 SourceReconcilePolicy::OverlayReplacingConflicts => {
639 state.tools.retain(|name, entry| {
640 entry.source_id != source_id
641 && !advertised_names.contains(name)
642 && !advertised_ids.contains(&entry.manifest.id)
643 });
644 }
645 }
646 for mut manifest in advertised_tools {
647 let name = manifest.name.clone();
648 manifest.availability_override = previous_overrides
649 .get(&name)
650 .copied()
651 .flatten()
652 .or(manifest.availability_override);
653 state
654 .tools
655 .insert(name, ToolRegistryEntry::new(manifest, source_id.clone()));
656 }
657 self.sources
658 .write()
659 .expect("tool source lock poisoned")
660 .insert(source_id, source);
661 state.generation += 1;
662 Ok(state.generation)
663 }
664
665 pub fn remove_source(&self, handle: &ToolSourceHandle) -> Result<u64, ReconfigureError> {
666 self.remove_source_id(handle.as_str())
667 }
668
669 pub fn refresh_sources(&self) -> Result<u64, ReconfigureError> {
670 let sources = self
671 .sources
672 .read()
673 .expect("tool source lock poisoned")
674 .values()
675 .cloned()
676 .collect::<Vec<_>>();
677 let mut generation = self.generation();
678 for source in sources {
679 generation = self.upsert_source(source)?;
680 }
681 Ok(generation)
682 }
683
684 pub(crate) fn remove_source_id(&self, source_id: &str) -> Result<u64, ReconfigureError> {
685 {
686 let mut sources = self.sources.write().expect("tool source lock poisoned");
687 if sources.remove(source_id).is_none() {
688 return Err(ReconfigureError::UnknownSource(source_id.to_string()));
689 }
690 }
691 let mut state = self
692 .state
693 .write()
694 .expect("tool registry state lock poisoned");
695 state.tools.retain(|_, entry| entry.source_id != source_id);
696 state.generation += 1;
697 Ok(state.generation)
698 }
699
700 pub(crate) fn fork_with_state(&self, snapshot: ToolState) -> Result<Self, ReconfigureError> {
701 let sources = self
702 .sources
703 .read()
704 .expect("tool source lock poisoned")
705 .iter()
706 .map(|(k, v)| (k.clone(), Arc::clone(v)))
707 .collect::<BTreeMap<_, _>>();
708 validate_unique_manifest_entries(snapshot.entries().values())?;
709 let tools = rebind_tool_state_entries(snapshot.entries(), &sources)?;
710 let generation = snapshot.generation.max(1);
711 Ok(Self {
712 sources: Arc::new(RwLock::new(sources)),
713 state: Arc::new(RwLock::new(ToolRegistryState {
714 generation,
715 tools,
716 next_live_source_id: 0,
717 })),
718 })
719 }
720}
721
722#[async_trait::async_trait]
723impl ToolProvider for ToolRegistry {
724 fn tool_manifests(&self) -> Vec<ToolManifest> {
725 let state = self
726 .state
727 .read()
728 .expect("tool registry state lock poisoned");
729 state
730 .tools
731 .values()
732 .map(|entry| entry.manifest.clone())
733 .collect()
734 }
735
736 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
737 if let Some(manifest) = {
738 let state = self
739 .state
740 .read()
741 .expect("tool registry state lock poisoned");
742 state.tools.get(name).map(|entry| entry.manifest.clone())
743 } {
744 return Some(manifest);
745 }
746
747 let sources = self
748 .sources
749 .read()
750 .expect("tool source lock poisoned")
751 .iter()
752 .map(|(source_id, source)| (source_id.clone(), Arc::clone(source)))
753 .collect::<Vec<_>>();
754 for (source_id, source) in sources {
755 let Some(manifest) = source.resolve_manifest(name) else {
756 continue;
757 };
758 let mut manifest = manifest_with_compact_contract(source.as_ref(), manifest);
759 let previous_override = {
760 let state = self
761 .state
762 .read()
763 .expect("tool registry state lock poisoned");
764 state
765 .tools
766 .get(&manifest.name)
767 .and_then(|entry| entry.manifest.availability_override)
768 };
769 manifest.availability_override = previous_override.or(manifest.availability_override);
770 let mut state = self
771 .state
772 .write()
773 .expect("tool registry state lock poisoned");
774 if let Some(existing) = state.tools.get(&manifest.name) {
775 return (existing.source_id == source_id).then(|| existing.manifest.clone());
776 }
777 if let Some((_, existing)) = state
778 .tools
779 .iter()
780 .find(|(_, entry)| entry.manifest.id == manifest.id)
781 {
782 return (existing.source_id == source_id).then(|| existing.manifest.clone());
783 }
784 state.tools.insert(
785 manifest.name.clone(),
786 ToolRegistryEntry::new(manifest.clone(), source_id),
787 );
788 state.generation += 1;
789 return Some(manifest);
790 }
791 None
792 }
793
794 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
795 let source_id = self.resolve_manifest(name).and_then(|_| {
796 let state = self
797 .state
798 .read()
799 .expect("tool registry state lock poisoned");
800 state.tools.get(name).map(|entry| entry.source_id.clone())
801 })?;
802 self.sources
803 .read()
804 .expect("tool source lock poisoned")
805 .get(&source_id)?
806 .resolve_contract(name)
807 }
808
809 async fn prepare_tool_call(
810 &self,
811 call: ToolPrepareCall<'_>,
812 ) -> Result<PreparedToolCall, ToolResult> {
813 let name = call.pending.tool_name.clone();
814 let source_id = self.resolve_manifest(&name).and_then(|_| {
815 let state = self
816 .state
817 .read()
818 .expect("tool registry state lock poisoned");
819 state.tools.get(&name).map(|entry| entry.source_id.clone())
820 });
821 let Some(source_id) = source_id else {
822 return Err(ToolResult::err_fmt(format_args!("Unknown tool: {name}")));
823 };
824 let source = {
825 self.sources
826 .read()
827 .expect("tool source lock poisoned")
828 .get(&source_id)
829 .cloned()
830 };
831 let Some(source) = source else {
832 return Err(ToolResult::err_fmt(format_args!(
833 "Tool source missing for tool `{name}`"
834 )));
835 };
836 source.prepare_tool_call(call).await
837 }
838
839 async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
840 let name = call.name;
841 let source_id = self.resolve_manifest(name).and_then(|_| {
842 let state = self
843 .state
844 .read()
845 .expect("tool registry state lock poisoned");
846 state.tools.get(name).map(|entry| entry.source_id.clone())
847 });
848 let Some(source_id) = source_id else {
849 return ToolResult::err_fmt(format_args!("Unknown tool: {name}"));
850 };
851 let source = {
852 self.sources
853 .read()
854 .expect("tool source lock poisoned")
855 .get(&source_id)
856 .cloned()
857 };
858 let Some(source) = source else {
859 return ToolResult::err_fmt(format_args!("Tool source missing for tool `{name}`"));
860 };
861 source
862 .execute(name, call.args, call.context, call.progress)
863 .await
864 }
865}
866
867fn validate_unique_manifests(manifests: &[ToolManifest]) -> Result<(), ReconfigureError> {
868 let mut names = BTreeSet::new();
869 let mut ids = BTreeSet::new();
870 for manifest in manifests {
871 if manifest.id.as_str().trim().is_empty() {
872 return Err(ReconfigureError::Validation(
873 "tool id cannot be empty".to_string(),
874 ));
875 }
876 if !ids.insert(manifest.id.clone()) {
877 return Err(ReconfigureError::Validation(format!(
878 "duplicate tool id `{}` in source",
879 manifest.id
880 )));
881 }
882 if manifest.name.trim().is_empty() {
883 return Err(ReconfigureError::Validation(
884 "tool name cannot be empty".to_string(),
885 ));
886 }
887 if !names.insert(manifest.name.clone()) {
888 return Err(ReconfigureError::Validation(format!(
889 "duplicate tool name `{}` in source",
890 manifest.name
891 )));
892 }
893 }
894 Ok(())
895}
896
897fn manifest_with_compact_contract(
898 source: &dyn ToolSourceExecutor,
899 mut manifest: ToolManifest,
900) -> ToolManifest {
901 if manifest.compact_contract.is_none()
902 && let Some(contract) = source.resolve_contract(&manifest.name)
903 {
904 manifest.compact_contract = Some(contract.compact_contract(&manifest));
905 }
906 manifest
907}
908
909fn export_tool_state_entries(
910 entries: &BTreeMap<String, ToolRegistryEntry>,
911) -> BTreeMap<String, ToolStateEntry> {
912 entries
913 .iter()
914 .map(|(name, entry)| (name.clone(), entry.export()))
915 .collect()
916}
917
918fn rebind_tool_state_entries(
919 entries: &BTreeMap<String, ToolStateEntry>,
920 sources: &BTreeMap<String, Arc<dyn ToolSourceExecutor>>,
921) -> Result<BTreeMap<String, ToolRegistryEntry>, ReconfigureError> {
922 let mut rebound = BTreeMap::new();
923 for (name, entry) in entries {
924 if name != &entry.manifest.name {
925 return Err(ReconfigureError::Validation(format!(
926 "tool state key `{}` does not match manifest name `{}`",
927 name, entry.manifest.name
928 )));
929 }
930
931 let mut name_matches = Vec::new();
932 for (source_id, source) in sources {
933 let Some(manifest) = source.resolve_manifest(name) else {
934 continue;
935 };
936 name_matches.push((
937 source_id.clone(),
938 manifest_with_compact_contract(source.as_ref(), manifest),
939 ));
940 }
941
942 if name_matches.is_empty() {
943 return Err(ReconfigureError::Validation(format!(
944 "no registered tool source resolves tool `{name}`"
945 )));
946 }
947
948 let matching_id = name_matches
949 .iter()
950 .filter(|(_, manifest)| manifest.id == entry.manifest.id)
951 .collect::<Vec<_>>();
952
953 if matching_id.len() == 1 {
954 let source_id = matching_id[0].0.clone();
955 rebound.insert(
956 name.clone(),
957 ToolRegistryEntry::new(entry.manifest.clone(), source_id),
958 );
959 } else if matching_id.is_empty() {
960 let resolved_ids = name_matches
961 .iter()
962 .map(|(_, manifest)| manifest.id.as_str())
963 .collect::<Vec<_>>()
964 .join(", ");
965 return Err(ReconfigureError::Validation(format!(
966 "tool `{name}` resolved with id(s) `{resolved_ids}`, expected `{}`",
967 entry.manifest.id
968 )));
969 } else {
970 return Err(ReconfigureError::Validation(format!(
971 "tool `{name}` with id `{}` is resolved by multiple registered sources",
972 entry.manifest.id
973 )));
974 }
975 }
976 Ok(rebound)
977}
978
979fn validate_unique_manifest_entries<'a>(
980 entries: impl IntoIterator<Item = &'a ToolStateEntry>,
981) -> Result<(), ReconfigureError> {
982 let manifests = entries
983 .into_iter()
984 .map(|entry| entry.manifest.clone())
985 .collect::<Vec<_>>();
986 validate_unique_manifests(&manifests)
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992 use crate::ToolDefinition;
993 use serde_json::json;
994 use std::sync::atomic::{AtomicUsize, Ordering};
995
996 struct MockTool;
997 struct MixedEnabledTool;
998 struct ExternalMockSource;
999 struct ExactResolvingSource {
1000 manifest_resolutions: Arc<AtomicUsize>,
1001 contract_resolutions: Arc<AtomicUsize>,
1002 executions: Arc<AtomicUsize>,
1003 }
1004 struct NamedExactSource {
1005 id: &'static str,
1006 }
1007 struct DynamicToolProvider {
1008 names: Arc<std::sync::Mutex<Vec<String>>>,
1009 }
1010
1011 fn test_tool(
1012 name: &str,
1013 description: &str,
1014 availability: crate::ToolAvailabilityConfig,
1015 ) -> ToolDefinition {
1016 ToolDefinition::raw_with_id(
1017 format!("tool:{name}"),
1018 name,
1019 description,
1020 ToolDefinition::default_input_schema(),
1021 json!({ "type": "string" }),
1022 )
1023 .with_availability(availability)
1024 }
1025
1026 fn manifests(definitions: Vec<ToolDefinition>) -> Vec<ToolManifest> {
1027 definitions
1028 .into_iter()
1029 .map(|tool| tool.manifest())
1030 .collect()
1031 }
1032
1033 fn contract_from(definitions: Vec<ToolDefinition>, name: &str) -> Option<Arc<ToolContract>> {
1034 definitions
1035 .into_iter()
1036 .find(|tool| tool.name() == name)
1037 .map(|tool| Arc::new(tool.contract()))
1038 }
1039
1040 fn dynamic_definition(name: &str) -> ToolDefinition {
1041 test_tool(name, "dynamic", crate::ToolAvailabilityConfig::callable())
1042 }
1043
1044 fn test_tool_context() -> crate::ToolContext<'static> {
1045 crate::ToolContext::builder(
1046 "registry-test".to_string(),
1047 Arc::new(crate::testing::MockSessionManager::default()),
1048 Arc::new(crate::testing::MockSessionManager::default()),
1049 Arc::new(crate::testing::MockSessionManager::default()),
1050 Arc::new(crate::UnavailableProcessService),
1051 Arc::new(crate::DefaultProcessCancelAbility),
1052 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
1053 crate::InlineRuntimeEffectController,
1054 )),
1055 Arc::new(crate::InMemoryAttachmentStore::new()),
1056 crate::DirectCompletionClient::unavailable(
1057 "direct completions are unavailable in this test context",
1058 ),
1059 )
1060 .build()
1061 }
1062
1063 #[async_trait::async_trait]
1064 impl ToolProvider for MockTool {
1065 fn tool_manifests(&self) -> Vec<ToolManifest> {
1066 manifests(vec![test_tool(
1067 "mock_tool",
1068 "mock",
1069 crate::ToolAvailabilityConfig::callable(),
1070 )])
1071 }
1072
1073 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1074 contract_from(
1075 vec![test_tool(
1076 "mock_tool",
1077 "mock",
1078 crate::ToolAvailabilityConfig::callable(),
1079 )],
1080 name,
1081 )
1082 }
1083
1084 async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
1085 ToolResult::ok(serde_json::json!("ok"))
1086 }
1087 }
1088
1089 #[async_trait::async_trait]
1090 impl ToolProvider for MixedEnabledTool {
1091 fn tool_manifests(&self) -> Vec<ToolManifest> {
1092 manifests(vec![
1093 test_tool(
1094 "enabled_tool",
1095 "enabled",
1096 crate::ToolAvailabilityConfig::callable(),
1097 ),
1098 test_tool(
1099 "disabled_tool",
1100 "disabled",
1101 crate::ToolAvailabilityConfig::off(),
1102 ),
1103 ])
1104 }
1105
1106 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1107 contract_from(
1108 vec![
1109 test_tool(
1110 "enabled_tool",
1111 "enabled",
1112 crate::ToolAvailabilityConfig::callable(),
1113 ),
1114 test_tool(
1115 "disabled_tool",
1116 "disabled",
1117 crate::ToolAvailabilityConfig::off(),
1118 ),
1119 ],
1120 name,
1121 )
1122 }
1123
1124 async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
1125 ToolResult::ok(serde_json::json!("ok"))
1126 }
1127 }
1128
1129 #[async_trait::async_trait]
1130 impl ToolSourceExecutor for ExternalMockSource {
1131 fn id(&self) -> &str {
1132 "external"
1133 }
1134
1135 fn advertised_tools(&self) -> Vec<ToolManifest> {
1136 manifests(vec![ToolDefinition::raw_with_id(
1137 "tool:mcp__demo__search",
1138 "mcp__demo__search",
1139 "search",
1140 json!({
1141 "type": "object",
1142 "properties": {
1143 "query": { "type": "string" }
1144 },
1145 "required": ["query"],
1146 "additionalProperties": false
1147 }),
1148 json!({ "type": "object", "additionalProperties": true }),
1149 )])
1150 }
1151
1152 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1153 contract_from(
1154 vec![ToolDefinition::raw_with_id(
1155 "tool:mcp__demo__search",
1156 "mcp__demo__search",
1157 "search",
1158 json!({
1159 "type": "object",
1160 "properties": {
1161 "query": { "type": "string" }
1162 },
1163 "required": ["query"],
1164 "additionalProperties": false
1165 }),
1166 json!({ "type": "object", "additionalProperties": true }),
1167 )],
1168 name,
1169 )
1170 }
1171
1172 async fn execute(
1173 &self,
1174 tool: &str,
1175 args: &serde_json::Value,
1176 _context: &ToolContext<'_>,
1177 _progress: Option<&ProgressSender>,
1178 ) -> ToolResult {
1179 ToolResult::ok(json!({
1180 "tool": tool,
1181 "args": args
1182 }))
1183 }
1184 }
1185
1186 #[async_trait::async_trait]
1187 impl ToolSourceExecutor for ExactResolvingSource {
1188 fn id(&self) -> &str {
1189 "exact"
1190 }
1191
1192 fn advertised_tools(&self) -> Vec<ToolManifest> {
1193 Vec::new()
1194 }
1195
1196 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
1197 self.manifest_resolutions.fetch_add(1, Ordering::SeqCst);
1198 (name == "host_only").then(|| {
1199 test_tool(
1200 "host_only",
1201 "host-only",
1202 crate::ToolAvailabilityConfig::callable(),
1203 )
1204 .manifest()
1205 })
1206 }
1207
1208 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1209 self.contract_resolutions.fetch_add(1, Ordering::SeqCst);
1210 contract_from(
1211 vec![test_tool(
1212 "host_only",
1213 "host-only",
1214 crate::ToolAvailabilityConfig::callable(),
1215 )],
1216 name,
1217 )
1218 }
1219
1220 async fn execute(
1221 &self,
1222 tool: &str,
1223 _args: &serde_json::Value,
1224 _context: &ToolContext<'_>,
1225 _progress: Option<&ProgressSender>,
1226 ) -> ToolResult {
1227 self.executions.fetch_add(1, Ordering::SeqCst);
1228 ToolResult::ok(json!(tool))
1229 }
1230 }
1231
1232 #[async_trait::async_trait]
1233 impl ToolSourceExecutor for NamedExactSource {
1234 fn id(&self) -> &str {
1235 self.id
1236 }
1237
1238 fn advertised_tools(&self) -> Vec<ToolManifest> {
1239 Vec::new()
1240 }
1241
1242 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
1243 (name == "host_only").then(|| {
1244 test_tool(
1245 "host_only",
1246 "host-only",
1247 crate::ToolAvailabilityConfig::callable(),
1248 )
1249 .manifest()
1250 })
1251 }
1252
1253 fn resolve_contract(&self, _name: &str) -> Option<Arc<ToolContract>> {
1254 None
1255 }
1256
1257 async fn execute(
1258 &self,
1259 tool: &str,
1260 _args: &serde_json::Value,
1261 _context: &ToolContext<'_>,
1262 _progress: Option<&ProgressSender>,
1263 ) -> ToolResult {
1264 ToolResult::ok(json!(tool))
1265 }
1266 }
1267
1268 #[async_trait::async_trait]
1269 impl ToolProvider for DynamicToolProvider {
1270 fn tool_manifests(&self) -> Vec<ToolManifest> {
1271 self.names
1272 .lock()
1273 .expect("dynamic tool names lock")
1274 .iter()
1275 .map(|name| dynamic_definition(name).manifest())
1276 .collect()
1277 }
1278
1279 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1280 self.names
1281 .lock()
1282 .expect("dynamic tool names lock")
1283 .iter()
1284 .any(|tool_name| tool_name == name)
1285 .then(|| Arc::new(dynamic_definition(name).contract()))
1286 }
1287
1288 async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
1289 ToolResult::ok(json!(call.name))
1290 }
1291 }
1292
1293 #[test]
1294 fn registry_preserves_initial_availability_state() {
1295 let registry =
1296 ToolRegistry::from_tool_provider(Arc::new(MixedEnabledTool)).expect("registry");
1297 let snapshot = registry.export_state();
1298 assert_eq!(
1299 snapshot
1300 .get("enabled_tool")
1301 .unwrap()
1302 .manifest()
1303 .effective_availability(),
1304 crate::ToolAvailability::Callable
1305 );
1306 assert_eq!(
1307 snapshot
1308 .get("disabled_tool")
1309 .unwrap()
1310 .manifest()
1311 .effective_availability(),
1312 crate::ToolAvailability::Off
1313 );
1314 }
1315
1316 #[test]
1317 fn exported_tool_state_is_source_free() {
1318 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1319 registry
1320 .add_tool_provider(Arc::new(MixedEnabledTool))
1321 .expect("live provider registered");
1322
1323 let value = serde_json::to_value(registry.export_state()).expect("serialized tool state");
1324 let serialized = value.to_string();
1325
1326 assert!(!serialized.contains("source_id"));
1327 assert!(!serialized.contains(PLUGIN_SOURCE_ID));
1328 assert!(!serialized.contains("live:"));
1329 }
1330
1331 #[test]
1332 fn apply_state_rebinds_source_free_snapshot_to_current_sources() {
1333 let source_registry =
1334 ToolRegistry::from_tool_provider(Arc::new(MixedEnabledTool)).expect("source registry");
1335 let snapshot = source_registry.export_state();
1336
1337 let target_registry =
1338 ToolRegistry::from_tool_provider(Arc::new(MixedEnabledTool)).expect("target registry");
1339 let next_generation = target_registry
1340 .apply_state(snapshot.with_generation(target_registry.generation()))
1341 .expect("state rebound");
1342
1343 assert_eq!(next_generation, target_registry.generation());
1344 assert!(target_registry.resolve_contract("enabled_tool").is_some());
1345 }
1346
1347 #[test]
1348 fn apply_state_rejects_tools_not_advertised_by_source() {
1349 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1350 let snapshot = registry.export_state();
1351 let generation = snapshot.generation();
1352 let mut tools = snapshot.entries().clone();
1353 tools.insert(
1354 "missing".to_string(),
1355 ToolStateEntry::new(
1356 test_tool(
1357 "missing",
1358 "missing",
1359 crate::ToolAvailabilityConfig::callable(),
1360 )
1361 .manifest(),
1362 ),
1363 );
1364 let snapshot = ToolState::new(generation, tools);
1365 assert!(matches!(
1366 registry.apply_state(snapshot),
1367 Err(ReconfigureError::Validation(_))
1368 ));
1369 }
1370
1371 #[test]
1372 fn apply_state_rejects_snapshot_when_provider_is_absent() {
1373 let source_registry =
1374 ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("source registry");
1375 source_registry
1376 .upsert_source(Arc::new(ExternalMockSource))
1377 .expect("source registered");
1378 let snapshot = source_registry.export_state();
1379
1380 let target_registry =
1381 ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target registry");
1382 let err = target_registry
1383 .apply_state(snapshot.with_generation(target_registry.generation()))
1384 .expect_err("missing provider should fail");
1385
1386 assert!(matches!(err, ReconfigureError::Validation(_)));
1387 }
1388
1389 #[test]
1390 fn apply_state_rejects_ambiguous_current_source_binding() {
1391 let registry = ToolRegistry::empty();
1392 registry
1393 .upsert_source(Arc::new(NamedExactSource { id: "exact-a" }))
1394 .expect("source a registered");
1395 registry
1396 .upsert_source(Arc::new(NamedExactSource { id: "exact-b" }))
1397 .expect("source b registered");
1398
1399 let mut tools = BTreeMap::new();
1400 tools.insert(
1401 "host_only".to_string(),
1402 ToolStateEntry::new(
1403 test_tool(
1404 "host_only",
1405 "host-only",
1406 crate::ToolAvailabilityConfig::callable(),
1407 )
1408 .manifest(),
1409 ),
1410 );
1411
1412 let err = registry
1413 .apply_state(ToolState::new(registry.generation(), tools))
1414 .expect_err("ambiguous source binding should fail");
1415
1416 assert!(matches!(err, ReconfigureError::Validation(_)));
1417 }
1418
1419 #[test]
1420 fn advertised_manifest_resolves_without_exact_host_lookup() {
1421 let manifest_resolutions = Arc::new(AtomicUsize::new(0));
1422 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1423 registry
1424 .upsert_source(Arc::new(ExactResolvingSource {
1425 manifest_resolutions: Arc::clone(&manifest_resolutions),
1426 contract_resolutions: Arc::new(AtomicUsize::new(0)),
1427 executions: Arc::new(AtomicUsize::new(0)),
1428 }))
1429 .expect("source registered");
1430
1431 assert_eq!(
1432 registry
1433 .resolve_manifest("mock_tool")
1434 .map(|manifest| manifest.name),
1435 Some("mock_tool".to_string())
1436 );
1437 assert_eq!(manifest_resolutions.load(Ordering::SeqCst), 0);
1438 }
1439
1440 #[test]
1441 fn refresh_sources_re_reads_group_provider_manifests() {
1442 let names = Arc::new(std::sync::Mutex::new(vec!["dynamic_one".to_string()]));
1443 let provider: Arc<dyn ToolProvider> = Arc::new(DynamicToolProvider {
1444 names: Arc::clone(&names),
1445 });
1446 let registry = ToolRegistry::from_tool_providers(vec![provider]).expect("registry");
1447
1448 let tool_names = || {
1449 registry
1450 .tool_manifests()
1451 .into_iter()
1452 .map(|manifest| manifest.name)
1453 .collect::<BTreeSet<_>>()
1454 };
1455
1456 assert!(tool_names().contains("dynamic_one"));
1457 assert!(!tool_names().contains("dynamic_two"));
1458
1459 names
1460 .lock()
1461 .expect("dynamic tool names lock")
1462 .push("dynamic_two".to_string());
1463 registry.refresh_sources().expect("refresh sources");
1464 let refreshed = tool_names();
1465 assert!(refreshed.contains("dynamic_one"));
1466 assert!(refreshed.contains("dynamic_two"));
1467
1468 names
1469 .lock()
1470 .expect("dynamic tool names lock")
1471 .retain(|name| name != "dynamic_one");
1472 registry.refresh_sources().expect("refresh sources");
1473 let refreshed = tool_names();
1474 assert!(!refreshed.contains("dynamic_one"));
1475 assert!(refreshed.contains("dynamic_two"));
1476 }
1477
1478 #[tokio::test]
1479 async fn unknown_manifest_exact_resolves_and_routes_to_owner() {
1480 let manifest_resolutions = Arc::new(AtomicUsize::new(0));
1481 let contract_resolutions = Arc::new(AtomicUsize::new(0));
1482 let executions = Arc::new(AtomicUsize::new(0));
1483 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1484 registry
1485 .upsert_source(Arc::new(ExactResolvingSource {
1486 manifest_resolutions: Arc::clone(&manifest_resolutions),
1487 contract_resolutions: Arc::clone(&contract_resolutions),
1488 executions: Arc::clone(&executions),
1489 }))
1490 .expect("source registered");
1491
1492 assert_eq!(
1493 registry
1494 .resolve_manifest("host_only")
1495 .map(|manifest| manifest.name),
1496 Some("host_only".to_string())
1497 );
1498 assert_eq!(manifest_resolutions.load(Ordering::SeqCst), 1);
1499
1500 let contract = registry.resolve_contract("host_only");
1501 assert!(contract.is_some());
1502 assert_eq!(manifest_resolutions.load(Ordering::SeqCst), 1);
1503 assert_eq!(contract_resolutions.load(Ordering::SeqCst), 1);
1504
1505 let context = test_tool_context();
1506 let args = json!({});
1507 let result = registry
1508 .execute(crate::ToolCall {
1509 name: "host_only",
1510 args: &args,
1511 context: &context,
1512 progress: None,
1513 })
1514 .await;
1515 assert!(result.is_success());
1516 assert_eq!(result.value_for_projection(), json!("host_only"));
1517 assert_eq!(executions.load(Ordering::SeqCst), 1);
1518 }
1519
1520 #[test]
1521 fn unknown_manifest_without_host_resolver_is_unavailable() {
1522 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1523
1524 assert!(registry.resolve_manifest("missing").is_none());
1525 assert!(registry.resolve_contract("missing").is_none());
1526 }
1527
1528 #[tokio::test]
1529 async fn upsert_source_registers_and_executes_external_tools() {
1530 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1531 registry
1532 .upsert_source(Arc::new(ExternalMockSource))
1533 .expect("source registered");
1534
1535 let defs = registry.tool_manifests();
1536 assert!(defs.iter().any(|def| def.name == "mcp__demo__search"));
1537
1538 let context = test_tool_context();
1539 let args = json!({ "query": "hello" });
1540 let result = registry
1541 .execute(crate::ToolCall {
1542 name: "mcp__demo__search",
1543 args: &args,
1544 context: &context,
1545 progress: None,
1546 })
1547 .await;
1548 assert!(result.is_success());
1549 assert_eq!(
1550 result.value_for_projection()["tool"],
1551 json!("mcp__demo__search")
1552 );
1553 assert_eq!(
1554 result.value_for_projection()["args"]["query"],
1555 json!("hello")
1556 );
1557 }
1558
1559 #[test]
1560 fn upsert_source_preserves_availability_override_on_refresh() {
1561 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1562 registry
1563 .upsert_source(Arc::new(ExternalMockSource))
1564 .expect("source registered");
1565 let mut snapshot = registry.export_state();
1566 snapshot
1567 .set_availability("mcp__demo__search", Some(crate::ToolAvailability::Off))
1568 .unwrap();
1569 registry.apply_state(snapshot).unwrap();
1570 registry
1571 .upsert_source(Arc::new(ExternalMockSource))
1572 .expect("source refreshed");
1573 let snapshot = registry.export_state();
1574 assert_eq!(
1575 snapshot
1576 .get("mcp__demo__search")
1577 .unwrap()
1578 .manifest()
1579 .effective_availability(),
1580 crate::ToolAvailability::Off
1581 );
1582 }
1583
1584 #[test]
1585 fn restore_state_adopts_generation_at_or_above_three() {
1586 let source = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("source registry");
1592 let snapshot = source.export_state().with_generation(3);
1593
1594 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target registry");
1595 assert_eq!(
1596 target.generation(),
1597 1,
1598 "a fresh registry starts at generation 1"
1599 );
1600 let restored = target
1601 .restore_state(snapshot.clone())
1602 .expect("restore adopts the snapshot generation");
1603 assert_eq!(restored, 3, "restore returns the adopted generation");
1604 assert_eq!(
1605 target.generation(),
1606 3,
1607 "restore adopts gen 3 onto a base-1 registry without bumping"
1608 );
1609 assert_eq!(target.export_state().generation(), 3);
1611
1612 let fresh = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("fresh registry");
1615 assert!(
1616 matches!(
1617 fresh.apply_state(snapshot),
1618 Err(ReconfigureError::GenerationMismatch {
1619 expected: 3,
1620 actual: 1
1621 })
1622 ),
1623 "apply_state must reject a gen-3 snapshot on a base-1 registry"
1624 );
1625 }
1626
1627 #[test]
1628 fn remove_source_removes_all_source_tools() {
1629 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1630 registry
1631 .upsert_source(Arc::new(ExternalMockSource))
1632 .expect("source registered");
1633 registry
1634 .remove_source_id("external")
1635 .expect("source removed");
1636 let defs = registry.tool_manifests();
1637 assert!(!defs.iter().any(|def| def.name == "mcp__demo__search"));
1638 }
1639
1640 #[test]
1641 fn project_tool_catalog_keeps_searchable_tools_with_surface_metadata() {
1642 fn dummy_tool(name: &str) -> crate::ToolDefinition {
1643 let tool = crate::ToolDefinition::raw_with_id(
1644 format!("tool:{name}"),
1645 name,
1646 format!("desc for {name}"),
1647 crate::ToolDefinition::default_input_schema(),
1648 serde_json::json!({}),
1649 );
1650 match name {
1651 "read_file" => {
1652 tool.with_agent_surface(crate::ToolAgentSurface::new(["files"], "read"))
1653 }
1654 "search_tools" => {
1655 tool.with_agent_surface(crate::ToolAgentSurface::new(["tools"], "search"))
1656 }
1657 _ => tool,
1658 }
1659 }
1660 let catalog = project_tool_catalog([
1661 crate::ToolSurfaceEntry {
1662 manifest: dummy_tool("read_file").manifest(),
1663 availability: crate::ToolAvailability::Showcased,
1664 },
1665 crate::ToolSurfaceEntry {
1666 manifest: dummy_tool("search_tools").manifest(),
1667 availability: crate::ToolAvailability::Callable,
1668 },
1669 ]);
1670 assert_eq!(catalog.len(), 2);
1671 assert_eq!(catalog[0]["name"], serde_json::json!("read_file"));
1672 assert_eq!(
1673 catalog[0]["contract"]["signature"],
1674 serde_json::json!("await files.read({})?")
1675 );
1676 assert_eq!(catalog[0]["showcased"], serde_json::json!(true));
1677 assert_eq!(catalog[1]["callable"], serde_json::json!(true));
1678 }
1679
1680 #[test]
1681 fn project_tool_catalog_preserves_dynamic_output_contracts() {
1682 fn dummy_tool(name: &str) -> crate::ToolDefinition {
1683 crate::ToolDefinition::raw_with_id(
1684 format!("tool:{name}"),
1685 name,
1686 format!("desc for {name}"),
1687 crate::ToolDefinition::default_input_schema(),
1688 serde_json::json!({}),
1689 )
1690 .with_agent_surface(crate::ToolAgentSurface::new(["llm"], "query"))
1691 }
1692 let catalog = project_tool_catalog([crate::ToolSurfaceEntry {
1693 manifest: dummy_tool("llm_query")
1694 .with_output_from_input_schema(
1695 "output",
1696 Some(serde_json::json!({ "type": "string" })),
1697 )
1698 .manifest(),
1699 availability: crate::ToolAvailability::Searchable,
1700 }]);
1701
1702 assert_eq!(
1703 catalog[0]["contract"]["signature"],
1704 serde_json::json!("await llm.query<T = str>({})?")
1705 );
1706 assert_eq!(catalog[0]["contract"]["returns"], serde_json::json!("T"));
1707 }
1708}
1709
1710pub(crate) fn project_tool_catalog<I>(entries: I) -> Vec<serde_json::Value>
1711where
1712 I: IntoIterator<Item = crate::ToolSurfaceEntry>,
1713{
1714 entries
1715 .into_iter()
1716 .filter(|entry| entry.availability.is_searchable())
1717 .map(|entry| {
1718 let manifest = entry.manifest;
1719 let availability = entry.availability;
1720 let agent_surface = manifest.agent_surface.executable_for(&manifest.name);
1721 let call = agent_surface.call_path();
1722 let mut projected = serde_json::json!({
1723 "id": manifest.id,
1724 "name": manifest.name,
1725 "module_path": agent_surface.module_path,
1726 "operation": agent_surface.operation,
1727 "authority_type": agent_surface.authority_type,
1728 "call": call,
1729 "description": manifest.description,
1730 "aliases": agent_surface.aliases,
1731 "availability": availability,
1732 "callable": availability.is_callable(),
1733 "showcased": availability.is_showcased(),
1734 "searchable": availability.is_searchable(),
1735 "activation": manifest.activation,
1736 });
1737 if let Some(contract) = manifest.compact_contract {
1738 projected
1739 .as_object_mut()
1740 .expect("projected tool catalog entry is an object")
1741 .insert("contract".to_string(), serde_json::json!(contract));
1742 }
1743 projected
1744 })
1745 .collect()
1746}