Skip to main content

a3s_code_core/tools/
registry.rs

1//! Tool Registry
2//!
3//! Central registry for all tools (built-in and dynamic).
4//! Provides thread-safe registration, lookup, and execution.
5
6use super::artifacts::{ArtifactStore, ArtifactStoreLimits, ToolArtifact};
7use super::types::{Tool, ToolContext, ToolOutput};
8use super::ToolResult;
9use super::{
10    merge_tool_output_artifact_metadata, truncate_tool_output_with_artifact, ToolOutputArtifact,
11};
12use crate::llm::ToolDefinition;
13use crate::trace::{InMemoryTraceSink, TraceEvent, TraceSink};
14use anyhow::Result;
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::sync::{Arc, RwLock};
18
19/// Tool registry for managing all available tools
20pub struct ToolRegistry {
21    tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
22    /// Names of builtin tools that cannot be overridden
23    builtins: RwLock<std::collections::HashSet<String>>,
24    context: RwLock<ToolContext>,
25    artifact_store: ArtifactStore,
26    trace_sink: RwLock<Arc<dyn TraceSink>>,
27}
28
29impl ToolRegistry {
30    /// Create a new tool registry
31    pub fn new(workspace: PathBuf) -> Self {
32        Self::with_artifact_limits(workspace, ArtifactStoreLimits::default())
33    }
34
35    /// Create a new tool registry with custom artifact retention limits.
36    pub fn with_artifact_limits(workspace: PathBuf, artifact_limits: ArtifactStoreLimits) -> Self {
37        Self::with_artifact_limits_and_workspace_services(
38            workspace.clone(),
39            artifact_limits,
40            crate::workspace::WorkspaceServices::local(workspace),
41        )
42    }
43
44    /// Create a new tool registry with custom artifact limits and workspace backend.
45    pub fn with_artifact_limits_and_workspace_services(
46        workspace: PathBuf,
47        artifact_limits: ArtifactStoreLimits,
48        workspace_services: Arc<crate::workspace::WorkspaceServices>,
49    ) -> Self {
50        let context = ToolContext::new(workspace).with_workspace_services(workspace_services);
51        Self {
52            tools: RwLock::new(HashMap::new()),
53            builtins: RwLock::new(std::collections::HashSet::new()),
54            context: RwLock::new(context),
55            artifact_store: ArtifactStore::with_limits(artifact_limits),
56            trace_sink: RwLock::new(Arc::new(InMemoryTraceSink::default())),
57        }
58    }
59
60    /// Register a builtin tool (cannot be overridden by dynamic tools)
61    pub fn register_builtin(&self, tool: Arc<dyn Tool>) {
62        let name = tool.name().to_string();
63        let mut tools = self.tools.write().unwrap();
64        let mut builtins = self.builtins.write().unwrap();
65        tracing::debug!("Registering builtin tool: {}", name);
66        tools.insert(name.clone(), tool);
67        builtins.insert(name);
68    }
69
70    /// Register a tool
71    ///
72    /// If a tool with the same name already exists as a builtin, the registration
73    /// is rejected to prevent shadowing of core tools.
74    pub fn register(&self, tool: Arc<dyn Tool>) {
75        let name = tool.name().to_string();
76        // All operations that need both registry locks take `tools` first.
77        // This keeps the builtin check and insertion atomic with
78        // `register_builtin` and avoids lock-order inversion.
79        let mut tools = self.tools.write().unwrap();
80        let builtins = self.builtins.read().unwrap();
81        if builtins.contains(&name) {
82            tracing::warn!(
83                "Rejected registration of tool '{}': cannot shadow builtin",
84                name
85            );
86            return;
87        }
88        tracing::debug!("Registering tool: {}", name);
89        tools.insert(name, tool);
90    }
91
92    /// Register a dynamic tool and return the tool it shadowed.
93    ///
94    /// The lookup and replacement happen under one write lock so lifecycle
95    /// owners can later restore the exact prior registration without racing a
96    /// concurrent dynamic registration. The boolean is `false` when a builtin
97    /// owns the name and the dynamic registration was rejected.
98    pub(crate) fn register_with_shadow(
99        &self,
100        tool: Arc<dyn Tool>,
101    ) -> (bool, Option<Arc<dyn Tool>>) {
102        let name = tool.name().to_string();
103        let mut tools = self.tools.write().unwrap();
104        let builtins = self.builtins.read().unwrap();
105        if builtins.contains(&name) {
106            tracing::warn!(
107                "Rejected registration of tool '{}': cannot shadow builtin",
108                name
109            );
110            return (false, None);
111        }
112        tracing::debug!("Registering owned dynamic tool: {}", name);
113        (true, tools.insert(name, tool))
114    }
115
116    /// Restore a shadowed registration only while `expected` still owns the
117    /// name.
118    ///
119    /// This compare-and-replace prevents one lifecycle owner from deleting or
120    /// overwriting a tool installed later by another dynamic source.
121    pub(crate) fn restore_if_same(
122        &self,
123        name: &str,
124        expected: &Arc<dyn Tool>,
125        replacement: Option<Arc<dyn Tool>>,
126    ) -> bool {
127        let mut tools = self.tools.write().unwrap();
128        let Some(current) = tools.get(name) else {
129            return false;
130        };
131        if !Arc::ptr_eq(current, expected) {
132            return false;
133        }
134
135        match replacement {
136            Some(tool) => {
137                tools.insert(name.to_string(), tool);
138            }
139            None => {
140                tools.remove(name);
141            }
142        }
143        true
144    }
145
146    /// Register a dynamic tool only when no source currently owns its name.
147    pub(crate) fn register_if_absent(&self, tool: Arc<dyn Tool>) -> bool {
148        let name = tool.name().to_string();
149        let mut tools = self.tools.write().unwrap();
150        if tools.contains_key(&name) {
151            return false;
152        }
153        tracing::debug!("Registering previously absent dynamic tool: {}", name);
154        tools.insert(name, tool);
155        true
156    }
157
158    /// Unregister a tool by name
159    ///
160    /// Returns true if the tool was found and removed.
161    pub fn unregister(&self, name: &str) -> bool {
162        let mut tools = self.tools.write().unwrap();
163        let builtins = self.builtins.read().unwrap();
164        if builtins.contains(name) {
165            tracing::warn!(
166                "Rejected unregister of tool '{}': builtin tools cannot be removed through dynamic unregister",
167                name
168            );
169            return false;
170        }
171        tracing::debug!("Unregistering tool: {}", name);
172        tools.remove(name).is_some()
173    }
174
175    /// Unregister all tools whose names start with the given prefix.
176    pub fn unregister_by_prefix(&self, prefix: &str) {
177        let mut tools = self.tools.write().unwrap();
178        let builtins = self.builtins.read().unwrap();
179        tools.retain(|name, _| builtins.contains(name) || !name.starts_with(prefix));
180        tracing::debug!("Unregistered tools with prefix: {}", prefix);
181    }
182
183    /// Get a tool by name
184    pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
185        let tools = self.tools.read().unwrap();
186        tools.get(name).cloned()
187    }
188
189    /// Check if a tool exists
190    pub fn contains(&self, name: &str) -> bool {
191        let tools = self.tools.read().unwrap();
192        tools.contains_key(name)
193    }
194
195    /// Get all tool definitions for LLM
196    pub fn definitions(&self) -> Vec<ToolDefinition> {
197        let tools = self.tools.read().unwrap();
198        let mut definitions = tools
199            .values()
200            .map(|tool| ToolDefinition {
201                name: tool.name().to_string(),
202                description: tool.description().to_string(),
203                parameters: tool.parameters(),
204            })
205            .collect::<Vec<_>>();
206        definitions.sort_by(|a, b| a.name.cmp(&b.name));
207        definitions
208    }
209
210    /// List all registered tool names
211    pub fn list(&self) -> Vec<String> {
212        let tools = self.tools.read().unwrap();
213        let mut names = tools.keys().cloned().collect::<Vec<_>>();
214        names.sort();
215        names
216    }
217
218    /// Get the number of registered tools
219    pub fn len(&self) -> usize {
220        let tools = self.tools.read().unwrap();
221        tools.len()
222    }
223
224    /// Check if registry is empty
225    pub fn is_empty(&self) -> bool {
226        self.len() == 0
227    }
228
229    /// Get the tool context
230    pub fn context(&self) -> ToolContext {
231        self.context.read().unwrap().clone()
232    }
233
234    /// Return a clone of the registry's artifact store handle.
235    pub fn artifact_store(&self) -> ArtifactStore {
236        self.artifact_store.clone()
237    }
238
239    /// Get a stored tool artifact by URI.
240    pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
241        self.artifact_store.get(artifact_uri)
242    }
243
244    /// Replace the trace sink used for compact tool/program execution events.
245    pub fn set_trace_sink(&self, sink: Arc<dyn TraceSink>) {
246        *self.trace_sink.write().unwrap() = sink;
247    }
248
249    /// Return the current trace sink.
250    pub fn trace_sink(&self) -> Arc<dyn TraceSink> {
251        Arc::clone(&self.trace_sink.read().unwrap())
252    }
253
254    /// Set the search configuration for the tool context
255    pub fn set_search_config(&self, config: crate::config::SearchConfig) {
256        let mut ctx = self.context.write().unwrap();
257        *ctx = ctx.clone().with_search_config(config);
258    }
259
260    /// Set a sandbox executor so that `bash` tool calls use the sandbox even
261    /// when executed without an explicit `ToolContext` (i.e., via `execute()`).
262    pub fn set_sandbox(&self, sandbox: std::sync::Arc<dyn crate::sandbox::BashSandbox>) {
263        let mut ctx = self.context.write().unwrap();
264        *ctx = ctx.clone().with_sandbox(sandbox);
265    }
266
267    /// Set environment overrides used by subprocess-backed tools when executed
268    /// without an explicit context.
269    pub fn set_command_env(&self, env: Arc<HashMap<String, String>>) {
270        let mut ctx = self.context.write().unwrap();
271        *ctx = ctx.clone().with_command_env(env);
272    }
273
274    /// Execute a tool by name using the registry's default context.
275    ///
276    /// This is the lowest-level standalone registry boundary. It does not run
277    /// agent/session permission, HITL, hook, budget, queue, timeout,
278    /// cancellation, or sanitization policy.
279    pub async fn execute(&self, name: &str, args: &serde_json::Value) -> Result<ToolResult> {
280        let ctx = self.context();
281        self.execute_with_context(name, args, &ctx).await
282    }
283
284    /// Execute a tool by name with an external caller-owned context.
285    ///
286    /// This remains a low-level ungoverned call; agent/session paths must use
287    /// their scoped invocation gateway instead.
288    pub async fn execute_with_context(
289        &self,
290        name: &str,
291        args: &serde_json::Value,
292        ctx: &ToolContext,
293    ) -> Result<ToolResult> {
294        let start = std::time::Instant::now();
295
296        let tool = self.get(name);
297
298        let result = match tool {
299            Some(tool) => {
300                let mut output = tool.execute(args, ctx).await?;
301                let original_content = output.content.clone();
302                let truncated = truncate_tool_output_with_artifact(name, &output.content);
303                output.content = truncated.content;
304                if let Some(artifact) = truncated.artifact {
305                    self.store_tool_artifact(name, &original_content, &artifact);
306                    output.metadata = Some(merge_tool_output_artifact_metadata(
307                        output.metadata,
308                        &artifact,
309                    ));
310                }
311                Ok(ToolResult {
312                    name: name.to_string(),
313                    output: output.content,
314                    exit_code: if output.success { 0 } else { 1 },
315                    metadata: output.metadata,
316                    images: output.images,
317                    error_kind: output.error_kind,
318                })
319            }
320            None => Ok(ToolResult::error(name, format!("Unknown tool: {}", name))),
321        };
322
323        if let Ok(ref r) = result {
324            crate::telemetry::record_tool_result(r.exit_code, start.elapsed());
325            self.record_trace_event(name, r, start.elapsed());
326        }
327
328        result
329    }
330
331    /// Execute a tool and return raw output using the registry's default context
332    pub async fn execute_raw(
333        &self,
334        name: &str,
335        args: &serde_json::Value,
336    ) -> Result<Option<ToolOutput>> {
337        let ctx = self.context();
338        self.execute_raw_with_context(name, args, &ctx).await
339    }
340
341    /// Execute a tool and return raw output with an external context
342    pub async fn execute_raw_with_context(
343        &self,
344        name: &str,
345        args: &serde_json::Value,
346        ctx: &ToolContext,
347    ) -> Result<Option<ToolOutput>> {
348        let tool = self.get(name);
349
350        match tool {
351            Some(tool) => {
352                let mut output = tool.execute(args, ctx).await?;
353                let original_content = output.content.clone();
354                let truncated = truncate_tool_output_with_artifact(name, &output.content);
355                output.content = truncated.content;
356                if let Some(artifact) = truncated.artifact {
357                    self.store_tool_artifact(name, &original_content, &artifact);
358                    output.metadata = Some(merge_tool_output_artifact_metadata(
359                        output.metadata,
360                        &artifact,
361                    ));
362                }
363                Ok(Some(output))
364            }
365            None => Ok(None),
366        }
367    }
368
369    fn store_tool_artifact(&self, tool_name: &str, content: &str, artifact: &ToolOutputArtifact) {
370        self.artifact_store.put(ToolArtifact {
371            artifact_id: artifact.artifact_id.clone(),
372            artifact_uri: artifact.artifact_uri.clone(),
373            tool_name: tool_name.to_string(),
374            content: content.to_string(),
375            original_bytes: artifact.original_bytes,
376            shown_bytes: artifact.shown_bytes,
377        });
378    }
379
380    fn record_trace_event(&self, name: &str, result: &ToolResult, duration: std::time::Duration) {
381        let sink = self.trace_sink();
382        sink.record(TraceEvent::tool_execution(
383            name,
384            result.exit_code == 0,
385            result.exit_code,
386            duration,
387            result.output.len(),
388            result.metadata.as_ref(),
389        ));
390
391        if name == "program" {
392            sink.record(TraceEvent::program_execution(
393                name,
394                result.exit_code == 0,
395                result.exit_code,
396                duration,
397                result.output.len(),
398                result.metadata.as_ref(),
399            ));
400        }
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::trace::{InMemoryTraceSink, TraceEventKind};
408    use async_trait::async_trait;
409
410    struct MockTool {
411        name: String,
412    }
413
414    #[async_trait]
415    impl Tool for MockTool {
416        fn name(&self) -> &str {
417            &self.name
418        }
419
420        fn description(&self) -> &str {
421            "A mock tool for testing"
422        }
423
424        fn parameters(&self) -> serde_json::Value {
425            serde_json::json!({
426                "type": "object",
427                "additionalProperties": false,
428                "properties": {},
429                "required": []
430            })
431        }
432
433        async fn execute(
434            &self,
435            _args: &serde_json::Value,
436            _ctx: &ToolContext,
437        ) -> Result<ToolOutput> {
438            Ok(ToolOutput::success("mock output"))
439        }
440    }
441
442    #[test]
443    fn test_registry_register_and_get() {
444        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
445
446        let tool = Arc::new(MockTool {
447            name: "test".to_string(),
448        });
449        registry.register(tool);
450
451        assert!(registry.contains("test"));
452        assert!(!registry.contains("nonexistent"));
453
454        let retrieved = registry.get("test");
455        assert!(retrieved.is_some());
456        assert_eq!(retrieved.unwrap().name(), "test");
457    }
458
459    #[test]
460    fn test_registry_unregister() {
461        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
462
463        let tool = Arc::new(MockTool {
464            name: "test".to_string(),
465        });
466        registry.register(tool);
467
468        assert!(registry.contains("test"));
469        assert!(registry.unregister("test"));
470        assert!(!registry.contains("test"));
471        assert!(!registry.unregister("test")); // Already removed
472    }
473
474    #[test]
475    fn test_registry_unregister_preserves_builtins() {
476        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
477        registry.register_builtin(Arc::new(MockTool {
478            name: "read".to_string(),
479        }));
480
481        assert!(!registry.unregister("read"));
482        assert!(registry.contains("read"));
483    }
484
485    #[test]
486    fn test_registry_unregister_by_prefix_preserves_builtins() {
487        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
488        registry.register_builtin(Arc::new(MockTool {
489            name: "mcp__builtin".to_string(),
490        }));
491        registry.register(Arc::new(MockTool {
492            name: "mcp__dynamic".to_string(),
493        }));
494
495        registry.unregister_by_prefix("mcp__");
496
497        assert!(registry.contains("mcp__builtin"));
498        assert!(!registry.contains("mcp__dynamic"));
499    }
500
501    #[test]
502    fn concurrent_owned_registration_cannot_overwrite_builtin() {
503        for iteration in 0..32 {
504            let registry = Arc::new(ToolRegistry::new(PathBuf::from("/tmp")));
505            let name = format!("atomic_builtin_{iteration}");
506            let builtin: Arc<dyn Tool> = Arc::new(MockTool { name: name.clone() });
507            let dynamic: Arc<dyn Tool> = Arc::new(MockTool { name: name.clone() });
508            let barrier = Arc::new(std::sync::Barrier::new(3));
509
510            let builtin_registry = Arc::clone(&registry);
511            let builtin_tool = Arc::clone(&builtin);
512            let builtin_barrier = Arc::clone(&barrier);
513            let builtin_thread = std::thread::spawn(move || {
514                builtin_barrier.wait();
515                builtin_registry.register_builtin(builtin_tool);
516            });
517
518            let dynamic_registry = Arc::clone(&registry);
519            let dynamic_barrier = Arc::clone(&barrier);
520            let dynamic_thread = std::thread::spawn(move || {
521                dynamic_barrier.wait();
522                dynamic_registry.register_with_shadow(dynamic);
523            });
524
525            barrier.wait();
526            builtin_thread.join().unwrap();
527            dynamic_thread.join().unwrap();
528
529            let current = registry.get(&name).unwrap();
530            assert!(Arc::ptr_eq(&current, &builtin));
531            assert!(!registry.unregister(&name));
532        }
533    }
534
535    #[test]
536    fn test_registry_definitions() {
537        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
538
539        registry.register(Arc::new(MockTool {
540            name: "tool2".to_string(),
541        }));
542        registry.register(Arc::new(MockTool {
543            name: "tool1".to_string(),
544        }));
545
546        let definitions = registry.definitions();
547        assert_eq!(definitions.len(), 2);
548        let names: Vec<&str> = definitions
549            .iter()
550            .map(|definition| definition.name.as_str())
551            .collect();
552        assert_eq!(names, vec!["tool1", "tool2"]);
553    }
554
555    #[tokio::test]
556    async fn test_registry_execute() {
557        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
558
559        registry.register(Arc::new(MockTool {
560            name: "test".to_string(),
561        }));
562
563        let result = registry
564            .execute("test", &serde_json::json!({}))
565            .await
566            .unwrap();
567        assert_eq!(result.exit_code, 0);
568        assert_eq!(result.output, "mock output");
569    }
570
571    #[tokio::test]
572    async fn test_registry_execute_unknown() {
573        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
574
575        let result = registry
576            .execute("unknown", &serde_json::json!({}))
577            .await
578            .unwrap();
579        assert_eq!(result.exit_code, 1);
580        assert!(result.output.contains("Unknown tool"));
581    }
582
583    #[tokio::test]
584    async fn test_registry_execute_with_context_success() {
585        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
586        let ctx = ToolContext::new(PathBuf::from("/tmp"));
587        let trace_sink = InMemoryTraceSink::default();
588        registry.set_trace_sink(Arc::new(trace_sink.clone()));
589
590        registry.register(Arc::new(MockTool {
591            name: "my_tool".to_string(),
592        }));
593
594        let result = registry
595            .execute_with_context("my_tool", &serde_json::json!({}), &ctx)
596            .await
597            .unwrap();
598        assert_eq!(result.name, "my_tool");
599        assert_eq!(result.exit_code, 0);
600        assert_eq!(result.output, "mock output");
601
602        let events = trace_sink.events();
603        assert_eq!(events.len(), 1);
604        assert_eq!(events[0].kind, TraceEventKind::ToolExecution);
605        assert_eq!(events[0].name, "my_tool");
606        assert!(events[0].success);
607        assert_eq!(events[0].output_bytes, "mock output".len());
608    }
609
610    #[tokio::test]
611    async fn test_registry_execute_with_context_unknown_tool() {
612        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
613        let ctx = ToolContext::new(PathBuf::from("/tmp"));
614
615        let result = registry
616            .execute_with_context("nonexistent", &serde_json::json!({}), &ctx)
617            .await
618            .unwrap();
619        assert_eq!(result.exit_code, 1);
620        assert!(result.output.contains("Unknown tool: nonexistent"));
621    }
622
623    struct FailingTool;
624
625    #[async_trait]
626    impl Tool for FailingTool {
627        fn name(&self) -> &str {
628            "failing"
629        }
630
631        fn description(&self) -> &str {
632            "A tool that returns failure"
633        }
634
635        fn parameters(&self) -> serde_json::Value {
636            serde_json::json!({
637                "type": "object",
638                "additionalProperties": false,
639                "properties": {},
640                "required": []
641            })
642        }
643
644        async fn execute(
645            &self,
646            _args: &serde_json::Value,
647            _ctx: &ToolContext,
648        ) -> Result<ToolOutput> {
649            Ok(ToolOutput::error("something went wrong"))
650        }
651    }
652
653    #[tokio::test]
654    async fn test_registry_execute_failing_tool() {
655        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
656        registry.register(Arc::new(FailingTool));
657
658        let result = registry
659            .execute("failing", &serde_json::json!({}))
660            .await
661            .unwrap();
662        assert_eq!(result.exit_code, 1);
663        assert_eq!(result.output, "something went wrong");
664    }
665
666    struct LargeOutputTool;
667
668    #[async_trait]
669    impl Tool for LargeOutputTool {
670        fn name(&self) -> &str {
671            "large_output"
672        }
673
674        fn description(&self) -> &str {
675            "A tool that returns more than the maximum output size"
676        }
677
678        fn parameters(&self) -> serde_json::Value {
679            serde_json::json!({
680                "type": "object",
681                "additionalProperties": false,
682                "properties": {},
683                "required": []
684            })
685        }
686
687        async fn execute(
688            &self,
689            _args: &serde_json::Value,
690            _ctx: &ToolContext,
691        ) -> Result<ToolOutput> {
692            Ok(ToolOutput::success(
693                "x".repeat(super::super::MAX_OUTPUT_SIZE + 1),
694            ))
695        }
696    }
697
698    #[tokio::test]
699    async fn test_registry_truncates_large_tool_output() {
700        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
701        let trace_sink = InMemoryTraceSink::default();
702        registry.set_trace_sink(Arc::new(trace_sink.clone()));
703        registry.register(Arc::new(LargeOutputTool));
704
705        let result = registry
706            .execute("large_output", &serde_json::json!({}))
707            .await
708            .unwrap();
709
710        assert_eq!(result.exit_code, 0);
711        assert!(result.output.contains("[tool output truncated:"));
712        assert!(result
713            .output
714            .contains("Full output artifact: a3s://tool-output/large_output/"));
715        assert!(result.output.len() < super::super::MAX_OUTPUT_SIZE + 512);
716        let metadata = result.metadata.expect("artifact metadata");
717        assert_eq!(
718            metadata["artifact"]["original_bytes"],
719            serde_json::json!(super::super::MAX_OUTPUT_SIZE + 1)
720        );
721        assert_eq!(
722            metadata["artifact"]["shown_bytes"],
723            serde_json::json!(super::super::MAX_OUTPUT_SIZE)
724        );
725        assert!(metadata["artifact"]["artifact_id"]
726            .as_str()
727            .unwrap()
728            .starts_with("tool-output:large_output:"));
729        assert!(metadata["artifact"]["artifact_uri"]
730            .as_str()
731            .unwrap()
732            .starts_with("a3s://tool-output/large_output/"));
733
734        let artifact_uri = metadata["artifact"]["artifact_uri"].as_str().unwrap();
735        let artifact = registry
736            .get_artifact(artifact_uri)
737            .expect("full output artifact");
738        assert_eq!(artifact.tool_name, "large_output");
739        assert_eq!(artifact.original_bytes, super::super::MAX_OUTPUT_SIZE + 1);
740        assert_eq!(artifact.shown_bytes, super::super::MAX_OUTPUT_SIZE);
741        assert_eq!(
742            artifact.content,
743            "x".repeat(super::super::MAX_OUTPUT_SIZE + 1)
744        );
745
746        let events = trace_sink.events();
747        assert_eq!(events.len(), 1);
748        assert_eq!(events[0].artifact_uris, vec![artifact_uri]);
749    }
750
751    #[tokio::test]
752    async fn test_registry_execute_raw_success() {
753        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
754        registry.register(Arc::new(MockTool {
755            name: "raw_test".to_string(),
756        }));
757
758        let output = registry
759            .execute_raw("raw_test", &serde_json::json!({}))
760            .await
761            .unwrap();
762        assert!(output.is_some());
763        let output = output.unwrap();
764        assert!(output.success);
765        assert_eq!(output.content, "mock output");
766    }
767
768    #[tokio::test]
769    async fn test_registry_execute_raw_stores_truncated_artifact() {
770        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
771        registry.register(Arc::new(LargeOutputTool));
772
773        let output = registry
774            .execute_raw("large_output", &serde_json::json!({}))
775            .await
776            .unwrap()
777            .expect("raw output");
778
779        assert!(output.content.contains("[tool output truncated:"));
780        let metadata = output.metadata.expect("artifact metadata");
781        let artifact_uri = metadata["artifact"]["artifact_uri"].as_str().unwrap();
782        let artifact = registry
783            .get_artifact(artifact_uri)
784            .expect("full output artifact");
785        assert_eq!(artifact.tool_name, "large_output");
786        assert_eq!(artifact.content.len(), super::super::MAX_OUTPUT_SIZE + 1);
787    }
788
789    #[tokio::test]
790    async fn test_registry_execute_raw_unknown() {
791        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
792
793        let output = registry
794            .execute_raw("missing", &serde_json::json!({}))
795            .await
796            .unwrap();
797        assert!(output.is_none());
798    }
799
800    #[test]
801    fn test_registry_list() {
802        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
803        registry.register(Arc::new(MockTool {
804            name: "beta".to_string(),
805        }));
806        registry.register(Arc::new(MockTool {
807            name: "alpha".to_string(),
808        }));
809
810        let names = registry.list();
811        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
812    }
813
814    #[test]
815    fn test_registry_len_and_is_empty() {
816        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
817        assert!(registry.is_empty());
818        assert_eq!(registry.len(), 0);
819
820        registry.register(Arc::new(MockTool {
821            name: "t".to_string(),
822        }));
823        assert!(!registry.is_empty());
824        assert_eq!(registry.len(), 1);
825    }
826
827    #[test]
828    fn test_registry_replace_tool() {
829        let registry = ToolRegistry::new(PathBuf::from("/tmp"));
830        registry.register(Arc::new(MockTool {
831            name: "dup".to_string(),
832        }));
833        registry.register(Arc::new(MockTool {
834            name: "dup".to_string(),
835        }));
836        // Should still have only 1 tool (replaced)
837        assert_eq!(registry.len(), 1);
838    }
839}