agent_engine/memory_backend/
competing.rs1use crate::tools::{Tool, ToolOrigin, ToolRegistry};
3
4fn memory_tool_name(name: &str) -> bool {
8 super::competing_note_tool(&name.to_ascii_lowercase())
9 || super::competing_note_tool(&ToolRegistry::unclaimed_wire_name(name).to_ascii_lowercase())
10}
11
12pub fn competing_memory_tool(tool: &dyn Tool) -> bool {
24 let typed = match tool.origin() {
25 ToolOrigin::Builtin => return false,
26 ToolOrigin::Mcp {
27 server_tool_name, ..
28 } => memory_tool_name(&server_tool_name),
29 ToolOrigin::Plugin { tool_name, .. } => memory_tool_name(&tool_name),
30 ToolOrigin::Extension { .. } | ToolOrigin::Unknown => false,
31 };
32 let name = tool.name();
33 let last = name.rsplit(':').next().unwrap_or(name);
34 typed
35 || tool.source_operation().is_some_and(memory_tool_name)
36 || memory_tool_name(name)
37 || memory_tool_name(last)
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use crate::tools::ToolContext;
44 use serde_json::Value;
45
46 struct Named {
48 name: &'static str,
49 operation: Option<&'static str>,
50 origin: ToolOrigin,
51 }
52
53 #[async_trait::async_trait]
54 impl Tool for Named {
55 fn name(&self) -> &str {
56 self.name
57 }
58 fn description(&self) -> &str {
59 "fixture"
60 }
61 fn parameters(&self) -> Value {
62 serde_json::json!({"type": "object"})
63 }
64 async fn execute(&self, _params: Value, _ctx: ToolContext) -> crate::Result<String> {
65 Ok(String::new())
66 }
67 fn source_operation(&self) -> Option<&str> {
68 self.operation
69 }
70 fn origin(&self) -> ToolOrigin {
71 self.origin.clone()
72 }
73 }
74
75 fn extension(plugin: &str, operation: &'static str) -> Named {
76 Named {
77 name: Box::leak(format!("{plugin}:{operation}").into_boxed_str()),
78 operation: Some(operation),
79 origin: ToolOrigin::Extension {
80 extension_id: plugin.into(),
81 },
82 }
83 }
84
85 fn mcp(server: &str, tool: &str) -> Named {
86 Named {
87 name: Box::leak(format!("ext__{server}__{tool}").into_boxed_str()),
88 operation: None,
89 origin: ToolOrigin::Mcp {
90 server_id: server.into(),
91 server_tool_name: tool.into(),
92 },
93 }
94 }
95
96 #[test]
97 fn a_registered_memory_operation_is_competing_under_any_plugin_name() {
98 for operation in [
99 "memory_store",
100 "memory_search",
101 "memory_fetch",
102 "memory_forget",
103 "memory_capture",
104 "memory_recall",
105 ] {
106 assert!(
107 competing_memory_tool(&extension("axel-memory-manager", operation)),
108 "{operation}"
109 );
110 assert!(
111 competing_memory_tool(&extension("plug", operation)),
112 "{operation}"
113 );
114 }
115 }
116
117 #[test]
121 fn a_runtime_name_offered_as_a_memory_tool_name_is_competing() {
122 for (plugin, operation) in [
123 ("memory", "search"),
124 ("memory", "fetch"),
125 ("memory", "store"),
126 ("memory", "forget"),
127 ("Memory", "Search"),
128 ("plug", "memory.search"),
129 ("plug", "memory:fetch"),
130 ("plug", "MEMORY_STORE"),
131 ] {
132 assert!(
133 competing_memory_tool(&extension(plugin, operation)),
134 "{plugin}:{operation}"
135 );
136 }
137 }
138
139 #[test]
140 fn other_names_are_not_competing() {
141 for (plugin, operation) in [
142 ("weather", "search"),
143 ("memory", "summarize"),
144 ("memory", "search_web"),
145 ("notes", "memory_searcher"),
146 ("memory_search", "status"),
147 ("memory-search", "run"),
148 ] {
149 assert!(
150 !competing_memory_tool(&extension(plugin, operation)),
151 "{plugin}:{operation}"
152 );
153 }
154 }
155
156 #[test]
157 fn builtins_are_never_classified() {
158 let builtin = Named {
159 name: "memory_search",
160 operation: Some("memory_search"),
161 origin: ToolOrigin::Builtin,
162 };
163 assert!(!competing_memory_tool(&builtin));
164 }
165
166 #[test]
167 fn mcp_tools_are_classified_by_their_server_tool_name_and_wire_form() {
168 assert!(competing_memory_tool(&mcp("notes", "memory_fetch")));
169 assert!(competing_memory_tool(&mcp("notes", "memory.search")));
170 assert!(competing_memory_tool(&mcp("notes", "Memory_Store")));
171 assert!(!competing_memory_tool(&mcp("memory", "search")));
173 assert!(!competing_memory_tool(&mcp("notes", "lookup")));
174 }
175
176 #[test]
179 fn an_undeclared_tool_is_classified_by_its_runtime_name() {
180 let undeclared = |name: &'static str| Named {
181 name,
182 operation: None,
183 origin: ToolOrigin::Unknown,
184 };
185 assert!(competing_memory_tool(&undeclared("memory:recall")));
186 assert!(competing_memory_tool(&undeclared("x:memory_capture")));
187 assert!(!competing_memory_tool(&undeclared("x:recall")));
188 }
189}