Skip to main content

atman_runtime/
tool_naming.rs

1use std::collections::HashMap;
2
3/// Convert a flow-internal tool name (e.g. "fs.read") to a provider-safe
4/// wire name that matches the OpenAI function name pattern `^[a-zA-Z0-9_-]+$`.
5/// All providers use the same mapping: `.` → `_`.
6pub fn to_wire(flow_name: &str) -> String {
7    flow_name.replace('.', "_")
8}
9
10/// Reverse of [to_wire]: given a wire name from the provider, find the
11/// original flow name by matching against the tool list.
12pub fn from_wire(wire_name: &str, tools: &[crate::tool::ToolSpec]) -> String {
13    for t in tools {
14        if to_wire(&t.name) == wire_name {
15            return t.name.clone();
16        }
17    }
18    wire_name.to_string()
19}
20
21#[derive(Debug, Default, Clone)]
22pub struct ToolNaming {
23    per_provider: HashMap<String, HashMap<String, String>>,
24    reverse: HashMap<String, HashMap<String, String>>,
25}
26
27impl ToolNaming {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn map(
33        &mut self,
34        provider: impl Into<String>,
35        flow_name: impl Into<String>,
36        provider_native: impl Into<String>,
37    ) {
38        let provider = provider.into();
39        let flow_name = flow_name.into();
40        let native = provider_native.into();
41        self.per_provider
42            .entry(provider.clone())
43            .or_default()
44            .insert(flow_name.clone(), native.clone());
45        self.reverse
46            .entry(provider)
47            .or_default()
48            .insert(native, flow_name);
49    }
50
51    pub fn to_provider<'a>(&'a self, provider: &str, flow_name: &'a str) -> &'a str {
52        self.per_provider
53            .get(provider)
54            .and_then(|m| m.get(flow_name))
55            .map(|s| s.as_str())
56            .unwrap_or(flow_name)
57    }
58
59    pub fn from_provider<'a>(&'a self, provider: &str, native_name: &'a str) -> &'a str {
60        self.reverse
61            .get(provider)
62            .and_then(|m| m.get(native_name))
63            .map(|s| s.as_str())
64            .unwrap_or(native_name)
65    }
66
67    pub fn known_flow_names(&self, provider: &str) -> impl Iterator<Item = &str> {
68        self.per_provider
69            .get(provider)
70            .into_iter()
71            .flat_map(|m| m.keys().map(|s| s.as_str()))
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn to_provider_returns_mapped_name() {
81        let mut n = ToolNaming::new();
82        n.map("anthropic", "fs.read", "str_replace_based_edit_tool");
83        assert_eq!(
84            n.to_provider("anthropic", "fs.read"),
85            "str_replace_based_edit_tool"
86        );
87    }
88
89    #[test]
90    fn to_provider_falls_back_to_flow_name_when_unmapped() {
91        let n = ToolNaming::new();
92        assert_eq!(n.to_provider("anthropic", "fs.read"), "fs.read");
93    }
94
95    #[test]
96    fn from_provider_is_reverse_of_to_provider() {
97        let mut n = ToolNaming::new();
98        n.map("openai", "bash.exec", "run_bash");
99        assert_eq!(n.to_provider("openai", "bash.exec"), "run_bash");
100        assert_eq!(n.from_provider("openai", "run_bash"), "bash.exec");
101    }
102
103    #[test]
104    fn from_provider_falls_back_to_native_name_when_unmapped() {
105        let n = ToolNaming::new();
106        assert_eq!(n.from_provider("openai", "unknown_tool"), "unknown_tool");
107    }
108
109    #[test]
110    fn maps_are_per_provider() {
111        let mut n = ToolNaming::new();
112        n.map("anthropic", "fs.read", "str_replace_based_edit_tool");
113        n.map("openai", "fs.read", "read_file");
114        assert_eq!(
115            n.to_provider("anthropic", "fs.read"),
116            "str_replace_based_edit_tool"
117        );
118        assert_eq!(n.to_provider("openai", "fs.read"), "read_file");
119    }
120
121    #[test]
122    fn known_flow_names_lists_flow_side_only() {
123        let mut n = ToolNaming::new();
124        n.map("anthropic", "fs.read", "str_replace_based_edit_tool");
125        n.map("anthropic", "bash.exec", "bash");
126        let mut names: Vec<_> = n.known_flow_names("anthropic").collect();
127        names.sort();
128        assert_eq!(names, vec!["bash.exec", "fs.read"]);
129    }
130}