Skip to main content

soul_core/executor/
direct.rs

1//! Direct executor — wraps an existing `ToolRegistry` for backward compatibility.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use tokio::sync::mpsc;
7
8use crate::error::{SoulError, SoulResult};
9use crate::tool::{ToolOutput, ToolRegistry};
10use crate::types::ToolDefinition;
11
12use super::ToolExecutor;
13
14/// Wraps a `ToolRegistry` as a `ToolExecutor`.
15///
16/// This bridges the existing tool system into the executor registry,
17/// enabling zero-breaking-change migration.
18pub struct DirectExecutor {
19    tools: Arc<ToolRegistry>,
20}
21
22impl DirectExecutor {
23    pub fn new(tools: Arc<ToolRegistry>) -> Self {
24        Self { tools }
25    }
26}
27
28#[async_trait]
29impl ToolExecutor for DirectExecutor {
30    async fn execute(
31        &self,
32        definition: &ToolDefinition,
33        call_id: &str,
34        arguments: serde_json::Value,
35        partial_tx: Option<mpsc::UnboundedSender<String>>,
36    ) -> SoulResult<ToolOutput> {
37        let tool = self
38            .tools
39            .get(&definition.name)
40            .ok_or_else(|| SoulError::ToolExecution {
41                tool_name: definition.name.clone(),
42                message: format!("Unknown tool: {}", definition.name),
43            })?;
44
45        tool.execute(call_id, arguments, partial_tx).await
46    }
47
48    fn executor_name(&self) -> &str {
49        "direct"
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::tool::Tool;
57    use serde_json::json;
58
59    struct MockTool;
60
61    #[async_trait]
62    impl Tool for MockTool {
63        fn name(&self) -> &str {
64            "mock"
65        }
66
67        fn definition(&self) -> ToolDefinition {
68            ToolDefinition {
69                name: "mock".into(),
70                description: "Mock tool".into(),
71                input_schema: json!({"type": "object"}),
72            }
73        }
74
75        async fn execute(
76            &self,
77            _call_id: &str,
78            _arguments: serde_json::Value,
79            _partial_tx: Option<mpsc::UnboundedSender<String>>,
80        ) -> SoulResult<ToolOutput> {
81            Ok(ToolOutput::success("mock result"))
82        }
83    }
84
85    #[tokio::test]
86    async fn delegates_to_tool_registry() {
87        let mut registry = ToolRegistry::new();
88        registry.register(Box::new(MockTool));
89        let executor = DirectExecutor::new(Arc::new(registry));
90
91        let def = ToolDefinition {
92            name: "mock".into(),
93            description: "".into(),
94            input_schema: json!({}),
95        };
96
97        let result = executor.execute(&def, "c1", json!({}), None).await.unwrap();
98        assert_eq!(result.content, "mock result");
99    }
100
101    #[tokio::test]
102    async fn unknown_tool_errors() {
103        let registry = ToolRegistry::new();
104        let executor = DirectExecutor::new(Arc::new(registry));
105
106        let def = ToolDefinition {
107            name: "nonexistent".into(),
108            description: "".into(),
109            input_schema: json!({}),
110        };
111
112        let result = executor.execute(&def, "c1", json!({}), None).await;
113        assert!(result.is_err());
114    }
115
116    #[test]
117    fn executor_name_is_direct() {
118        let registry = ToolRegistry::new();
119        let executor = DirectExecutor::new(Arc::new(registry));
120        assert_eq!(executor.executor_name(), "direct");
121    }
122
123    #[test]
124    fn is_send_sync() {
125        fn assert_send_sync<T: Send + Sync>() {}
126        assert_send_sync::<DirectExecutor>();
127    }
128}