1use std::sync::Arc;
2
3use serde_json::Value;
4
5use crate::extensions::runtime::process::RegisteredExtensionToolSpec;
6use crate::extensions::runtime::ExtensionHandler;
7use crate::{Result, RuntimeError, ToolContext};
8
9pub struct ExtensionTool {
10 runtime_name: String,
11 description: String,
12 input_schema: Value,
13 handler: Arc<dyn ExtensionHandler>,
14 tool_name: String,
15 plugin_id: String,
16}
17
18impl ExtensionTool {
19 pub fn new(
20 plugin_id: &str,
21 spec: RegisteredExtensionToolSpec,
22 handler: Arc<dyn ExtensionHandler>,
23 ) -> Self {
24 Self {
25 runtime_name: format!("{}:{}", plugin_id, spec.name),
26 description: spec.description,
27 input_schema: spec.input_schema,
28 handler,
29 tool_name: spec.name,
30 plugin_id: plugin_id.to_string(),
31 }
32 }
33}
34
35fn caller_of(ctx: &ToolContext) -> Option<agent_core::memory::author::Author> {
41 ctx.capabilities
42 .memory_backend
43 .as_ref()
44 .map(|binding| binding.author().clone())
45}
46
47#[async_trait::async_trait]
48impl crate::tools::Tool for ExtensionTool {
49 fn name(&self) -> &str {
50 &self.runtime_name
51 }
52
53 fn description(&self) -> &str {
54 &self.description
55 }
56
57 fn parameters(&self) -> Value {
58 self.input_schema.clone()
59 }
60
61 async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
62 let caller = caller_of(&ctx);
63 let value = self
64 .handler
65 .call_tool_as(
66 &self.tool_name,
67 params,
68 ctx.capabilities.session_id.as_deref(),
69 ctx.capabilities.cwd.as_deref(),
70 caller.as_ref(),
71 )
72 .await
73 .map_err(RuntimeError::Tool)?;
74 if let Some(text) = value.as_str() {
75 Ok(text.to_string())
76 } else if let Some(text) = value.get("content").and_then(Value::as_str) {
77 Ok(text.to_string())
78 } else {
79 Ok(value.to_string())
80 }
81 }
82
83 fn extension_id(&self) -> Option<&str> {
84 Some(&self.plugin_id)
85 }
86
87 fn source_operation(&self) -> Option<&str> {
88 Some(&self.tool_name)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use std::sync::atomic::{AtomicUsize, Ordering};
95
96 use serde_json::json;
97
98 use super::*;
99 use crate::extensions::hooks::events::{HookEvent, HookResult};
100 use crate::tools::test_helpers::{context_with_memory, create_tool_context, memory_binding};
101 use crate::tools::{Tool, ToolRegistry};
102
103 #[derive(Default)]
105 struct Counting(AtomicUsize);
106
107 #[async_trait::async_trait]
108 impl ExtensionHandler for Counting {
109 fn id(&self) -> &str {
110 "fixture"
111 }
112 async fn handle(&self, _event: &HookEvent) -> HookResult {
113 HookResult::Continue
114 }
115 async fn shutdown(&self) {}
116 async fn call_tool_for(
117 &self,
118 _name: &str,
119 _input: Value,
120 _session_id: Option<&str>,
121 _cwd: Option<&std::path::Path>,
122 ) -> std::result::Result<Value, String> {
123 self.0.fetch_add(1, Ordering::SeqCst);
124 Ok(json!("extension result"))
125 }
126 }
127
128 fn tool(plugin: &str, operation: &str, handler: &Arc<Counting>) -> ExtensionTool {
129 ExtensionTool::new(
130 plugin,
131 RegisteredExtensionToolSpec {
132 name: operation.into(),
133 description: format!("{plugin} {operation}"),
134 input_schema: json!({"type": "object"}),
135 },
136 handler.clone(),
137 )
138 }
139
140 async fn call(tool: &dyn Tool) -> [Result<String>; 2] {
142 let dir = tempfile::tempdir().unwrap();
143 let binding = memory_binding(dir.path());
144 [
145 tool.execute(json!({}), context_with_memory(binding)).await,
146 tool.execute(json!({}), create_tool_context()).await,
147 ]
148 }
149
150 #[tokio::test]
154 async fn extension_tools_named_like_memory_tools_run() {
155 let handler = Arc::new(Counting::default());
156 let named = [
157 ("memory", "search"),
158 ("memory", "fetch"),
159 ("memory", "store"),
160 ("memory", "forget"),
161 ("plug", "memory.search"),
162 ("Memory", "Search"),
163 ("axel", "memory_search"),
164 ("axel-memory-manager", "memory_search"),
165 ("axel-memory-manager", "memory_fetch"),
166 ("axel-memory-manager", "memory_store"),
167 ("axel-memory-manager", "memory_forget"),
168 ("weather", "search"),
169 ("memory", "summarize"),
170 ];
171 for (plugin, operation) in named {
172 let tool = tool(plugin, operation, &handler);
173 for result in call(&tool).await {
174 assert_eq!(result.unwrap(), "extension result", "{plugin}:{operation}");
175 }
176 }
177 assert_eq!(handler.0.load(Ordering::SeqCst), named.len() * 2, "every call reached the extension");
178 }
179
180 #[tokio::test]
183 async fn the_suffixed_wire_name_of_an_alias_runs() {
184 let handler = Arc::new(Counting::default());
185 let registry = ToolRegistry::from_tools_for_tests(vec![
186 Arc::new(crate::tools::memory::MemorySearchTool),
187 Arc::new(tool("memory", "search", &handler)),
188 ]);
189 assert_eq!(registry.runtime_name_for_api("memory_search"), "memory_search");
190 assert_eq!(registry.runtime_name_for_api("memory_search_2"), "memory:search");
191 let alias = registry.get("memory:search").expect("alias registered");
192 for result in call(alias.as_ref()).await {
193 assert_eq!(result.unwrap(), "extension result");
194 }
195 assert_eq!(handler.0.load(Ordering::SeqCst), 2);
196 }
197}
198
199#[cfg(all(test, unix))]
203mod caller_tests {
204 use std::path::Path;
205 use std::time::Duration;
206
207 use agent_core::memory::author::Author;
208 use serde_json::json;
209
210 use super::*;
211 use crate::extensions::manifest::CURRENT_EXTENSION_PROTOCOL_VERSION;
212 use crate::extensions::runtime::process::ProcessExtension;
213 use crate::memory_backend::MemoryBinding;
214 use crate::tools::test_helpers::{context_with_memory, create_tool_context};
215 use crate::tools::Tool;
216
217 async fn wire_tool(log: &Path) -> ExtensionTool {
221 let init = json!({
222 "protocol_version": CURRENT_EXTENSION_PROTOCOL_VERSION,
223 "capabilities": {}
224 });
225 let script = format!(
226 r#"while IFS= read -r header; do
227 length=${{header#Content-Length: }}
228 length=${{length%?}}
229 IFS= read -r blank || exit 1
230 body=$(dd bs=1 count="$length" 2>/dev/null) || exit 1
231 id=${{body##*\"id\":}}
232 id=${{id%%[!0-9]*}}
233 case "$body" in
234 *'"method":"initialize"'*) result='{init}' ;;
235 *'"method":"tool.call"'*) printf '%s\n' "$body" >> '{log}'; result='"ok"' ;;
236 *) result=null ;;
237 esac
238 reply='{{"jsonrpc":"2.0","id":'"$id"',"result":'"$result"'}}'
239 printf 'Content-Length: %s\r\n\r\n%s' "${{#reply}}" "$reply"
240done"#,
241 log = log.display(),
242 );
243 let process = ProcessExtension::spawn("caller-fixture", "/bin/sh", &["-c".into(), script])
244 .await
245 .expect("spawn the fixture");
246 tokio::time::timeout(Duration::from_secs(5), process.initialize_for_test(None))
247 .await
248 .expect("bounded initialize")
249 .expect("initialize the fixture");
250 ExtensionTool::new(
251 "fixture",
252 RegisteredExtensionToolSpec {
253 name: "whoami".into(),
254 description: "keeps the call".into(),
255 input_schema: json!({"type": "object"}),
256 },
257 Arc::new(process),
258 )
259 }
260
261 async fn params_sent(tool: &ExtensionTool, log: &Path, ctx: ToolContext) -> Value {
263 let before = sent(log).len();
264 let reply = tokio::time::timeout(Duration::from_secs(5), tool.execute(json!({}), ctx))
265 .await
266 .expect("bounded call")
267 .expect("the fixture answers");
268 assert_eq!(reply, "ok");
269 let all = sent(log);
270 assert_eq!(all.len(), before + 1, "one tool.call a call");
271 let params = all[before]["params"].clone();
272 assert_eq!(params["name"], json!("whoami"));
273 params
274 }
275
276 fn sent(log: &Path) -> Vec<Value> {
277 std::fs::read_to_string(log)
278 .unwrap_or_default()
279 .lines()
280 .map(|line| serde_json::from_str(line).expect("a tool.call body"))
281 .collect()
282 }
283
284 fn chat_binding(dir: &Path) -> MemoryBinding {
286 MemoryBinding::for_cwd(Some(dir.to_path_buf()))
287 }
288
289 fn author_of(binding: &MemoryBinding) -> Value {
291 serde_json::to_value(binding.author()).unwrap()
292 }
293
294 fn worker_binding(chat: &MemoryBinding) -> MemoryBinding {
297 let mut worker = crate::Runtime::new_headless();
298 crate::tools::subagent::apply_subagent_runtime_policy(
299 &mut worker,
300 &Default::default(),
301 Some(chat),
302 None,
303 None,
304 &Default::default(),
305 );
306 worker.memory_backend_for_test()
307 }
308
309 fn as_author(caller: &Value) -> Author {
310 let author: Author = serde_json::from_value(caller.clone()).expect("an Author");
311 author.validate().expect("a valid Author");
312 author
313 }
314
315 #[tokio::test]
316 async fn a_chats_tool_call_names_the_author_its_binding_holds() {
317 let dir = tempfile::tempdir().unwrap();
318 let dir = dir.path().canonicalize().unwrap();
319 let log = dir.join("calls.jsonl");
320 let tool = wire_tool(&log).await;
321 let chat = chat_binding(&dir);
322
323 let params = params_sent(&tool, &log, context_with_memory(chat.clone())).await;
324 assert_eq!(params["caller"], author_of(&chat), "the run's own author");
325
326 let caller = as_author(¶ms["caller"]);
329 assert_eq!(&caller, chat.author());
330 assert!(caller.parent.is_none());
331 let mut keys: Vec<&str> =
332 params["caller"].as_object().unwrap().keys().map(String::as_str).collect();
333 keys.sort_unstable();
334 assert_eq!(keys, ["actor", "group"]);
335 let again = params_sent(&tool, &log, context_with_memory(chat.clone())).await;
337 assert_eq!(again["caller"], params["caller"]);
338 let other = params_sent(&tool, &log, context_with_memory(chat_binding(&dir))).await;
340 let other = as_author(&other["caller"]);
341 assert_ne!(other.actor, caller.actor);
342 assert_ne!(other.group, caller.group);
343 }
344
345 #[tokio::test]
346 async fn a_workers_tool_call_names_its_own_actor_in_the_chats_group() {
347 let dir = tempfile::tempdir().unwrap();
348 let dir = dir.path().canonicalize().unwrap();
349 let log = dir.join("calls.jsonl");
350 let tool = wire_tool(&log).await;
351 let chat = chat_binding(&dir);
352 let (first, second) = (worker_binding(&chat), worker_binding(&chat));
353
354 let of_chat = params_sent(&tool, &log, context_with_memory(chat.clone())).await;
355 let of_chat = as_author(&of_chat["caller"]);
356 let mut actors = vec![of_chat.actor.clone()];
357 for worker in [&first, &second] {
358 let params = params_sent(&tool, &log, context_with_memory(worker.clone())).await;
359 assert_eq!(
360 params["caller"],
361 author_of(worker),
362 "a worker's call names the worker's own author"
363 );
364 let caller = as_author(¶ms["caller"]);
365 assert_eq!(caller.group, of_chat.group);
366 assert_eq!(caller.parent.as_deref(), Some(of_chat.actor.as_str()));
367 assert!(!actors.contains(&caller.actor), "each run its own actor");
368 actors.push(caller.actor);
369 }
370 let after = params_sent(&tool, &log, context_with_memory(chat.clone())).await;
372 assert_eq!(as_author(&after["caller"]), of_chat);
373 }
374
375 #[tokio::test]
378 async fn a_call_with_no_binding_has_no_caller_key() {
379 let dir = tempfile::tempdir().unwrap();
380 let dir = dir.path().canonicalize().unwrap();
381 let log = dir.join("calls.jsonl");
382 let tool = wire_tool(&log).await;
383 let ctx = create_tool_context();
384 assert!(ctx.capabilities.memory_backend.is_none());
385 let params = params_sent(&tool, &log, ctx).await;
386 let mut keys: Vec<&str> = params.as_object().unwrap().keys().map(String::as_str).collect();
387 keys.sort_unstable();
388 assert_eq!(keys, ["cwd", "input", "name", "session_id"]);
389 }
390}