Skip to main content

machi_tools/
source.rs

1//! Tool sources: static lists and merge for multi-origin tool sets.
2//!
3//! MCP and other remote adapters implement [`ToolSource`] behind optional
4//! product crates — the kernel only defines the merge contract.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use crate::registry::ToolRegistry;
10use crate::tool::SharedTool;
11
12/// Provides tools that can be merged into a [`ToolRegistry`].
13///
14/// Maturity: **core** (port). MCP and remote adapters live outside the kernel
15/// and implement this trait; merge is last-wins on tool name.
16pub trait ToolSource: Send + Sync {
17    /// Stable source id for logs (`static`, `mcp:server`, …).
18    fn name(&self) -> &str;
19
20    /// Tools contributed by this source (order not significant after merge).
21    fn tools(&self) -> Vec<SharedTool>;
22}
23
24/// Fixed list of tools (primary host-registered set).
25#[derive(Clone)]
26pub struct StaticToolSource {
27    name: String,
28    tools: Vec<SharedTool>,
29}
30
31impl std::fmt::Debug for StaticToolSource {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("StaticToolSource")
34            .field("name", &self.name)
35            .field("tools", &self.tools.len())
36            .finish()
37    }
38}
39
40impl StaticToolSource {
41    /// Named static source.
42    #[must_use]
43    pub fn new(name: impl Into<String>, tools: Vec<SharedTool>) -> Self {
44        Self {
45            name: name.into(),
46            tools,
47        }
48    }
49}
50
51impl ToolSource for StaticToolSource {
52    fn name(&self) -> &str {
53        &self.name
54    }
55
56    fn tools(&self) -> Vec<SharedTool> {
57        self.tools.clone()
58    }
59}
60
61/// Merge multiple sources into one registry.
62///
63/// **Last source wins** on tool name collision (deterministic, documented).
64#[must_use]
65pub fn merge_tool_sources<'a>(
66    sources: impl IntoIterator<Item = &'a dyn ToolSource>,
67) -> ToolRegistry {
68    let mut map: HashMap<String, SharedTool> = HashMap::new();
69    for source in sources {
70        for tool in source.tools() {
71            map.insert(tool.name().to_owned(), tool);
72        }
73    }
74    ToolRegistry::from_tools(map.into_values().collect())
75}
76
77/// Arc-wrapped dynamic source list helper.
78#[must_use]
79pub fn merge_arc_sources(sources: &[Arc<dyn ToolSource>]) -> ToolRegistry {
80    let refs: Vec<&dyn ToolSource> = sources.iter().map(AsRef::as_ref).collect();
81    merge_tool_sources(refs)
82}
83
84#[cfg(test)]
85mod tests {
86    use async_trait::async_trait;
87    use serde_json::{Value, json};
88
89    use super::*;
90    use crate::calc::CalcTool;
91    use crate::tool::DynTool;
92
93    struct NamedTool {
94        n: &'static str,
95    }
96
97    #[async_trait]
98    impl DynTool for NamedTool {
99        fn name(&self) -> &str {
100            self.n
101        }
102        fn description(&self) -> &str {
103            "t"
104        }
105        fn parameters(&self) -> Value {
106            json!({"type":"object","properties":{}})
107        }
108        async fn call(
109            &self,
110            _ctx: crate::context::ToolCallContext,
111            _arguments: Value,
112        ) -> Result<crate::tool::ToolResult, crate::error::ToolError> {
113            Ok(crate::tool::ToolResult::text(self.n))
114        }
115    }
116
117    #[test]
118    fn last_source_wins_on_name() {
119        let a = StaticToolSource::new("a", vec![Arc::new(NamedTool { n: "dup" })]);
120        let b = StaticToolSource::new(
121            "b",
122            vec![Arc::new(NamedTool { n: "dup" }), Arc::new(CalcTool)],
123        );
124        let sources: [&dyn ToolSource; 2] = [&a, &b];
125        let reg = merge_tool_sources(sources);
126        assert_eq!(reg.len(), 2);
127        assert_eq!(reg.get("dup").expect("dup").name(), "dup");
128        assert!(reg.get(CalcTool.name()).is_some());
129        assert_eq!(reg.names().len(), 2);
130    }
131
132    #[test]
133    fn empty_merge() {
134        let reg = merge_tool_sources([]);
135        assert!(reg.is_empty());
136    }
137}