Skip to main content

soul_coder/
presets.rs

1//! Preset tool collections for common use cases.
2//!
3//! Two integration modes:
4//! - **ToolRegistry** (simple): `coding_tools()`, `read_only_tools()`, `all_tools()`
5//! - **ExecutorRegistry** (config-driven): `coding_executor()`, `all_executor()`
6
7use std::sync::Arc;
8
9use soul_core::executor::direct::DirectExecutor;
10use soul_core::executor::{ConfigTool, ExecutorRegistry, ToolExecutor};
11use soul_core::tool::ToolRegistry;
12use soul_core::vexec::VirtualExecutor;
13use soul_core::vfs::VirtualFs;
14
15use crate::tools::{
16    bash::BashTool, edit::EditTool, find::FindTool, grep::GrepTool, ls::LsTool, read::ReadTool,
17    write::WriteTool,
18};
19
20/// Create coding tools: read, write, edit, bash.
21/// Full modification access for interactive coding sessions.
22pub fn coding_tools(
23    fs: Arc<dyn VirtualFs>,
24    executor: Arc<dyn VirtualExecutor>,
25    cwd: impl Into<String>,
26) -> ToolRegistry {
27    let cwd = cwd.into();
28    let mut registry = ToolRegistry::new();
29    registry.register(Box::new(ReadTool::new(fs.clone(), &cwd)));
30    registry.register(Box::new(WriteTool::new(fs.clone(), &cwd)));
31    registry.register(Box::new(EditTool::new(fs, &cwd)));
32    registry.register(Box::new(BashTool::new(executor, &cwd)));
33    registry
34}
35
36/// Create read-only tools: read, grep, find, ls.
37/// Exploration without modification access.
38pub fn read_only_tools(
39    fs: Arc<dyn VirtualFs>,
40    cwd: impl Into<String>,
41) -> ToolRegistry {
42    let cwd = cwd.into();
43    let mut registry = ToolRegistry::new();
44    registry.register(Box::new(ReadTool::new(fs.clone(), &cwd)));
45    registry.register(Box::new(GrepTool::new(fs.clone(), &cwd)));
46    registry.register(Box::new(FindTool::new(fs.clone(), &cwd)));
47    registry.register(Box::new(LsTool::new(fs, &cwd)));
48    registry
49}
50
51/// Create all tools: read, write, edit, bash, grep, find, ls.
52/// Complete toolkit for full agent capabilities.
53pub fn all_tools(
54    fs: Arc<dyn VirtualFs>,
55    executor: Arc<dyn VirtualExecutor>,
56    cwd: impl Into<String>,
57) -> ToolRegistry {
58    let cwd = cwd.into();
59    let mut registry = ToolRegistry::new();
60    registry.register(Box::new(ReadTool::new(fs.clone(), &cwd)));
61    registry.register(Box::new(WriteTool::new(fs.clone(), &cwd)));
62    registry.register(Box::new(EditTool::new(fs.clone(), &cwd)));
63    registry.register(Box::new(BashTool::new(executor, &cwd)));
64    registry.register(Box::new(GrepTool::new(fs.clone(), &cwd)));
65    registry.register(Box::new(FindTool::new(fs.clone(), &cwd)));
66    registry.register(Box::new(LsTool::new(fs, &cwd)));
67    registry
68}
69
70/// Create an [`ExecutorRegistry`] with all coding tools wired via [`DirectExecutor`].
71///
72/// This integrates soul-coder tools into soul-core's config-driven executor system,
73/// enabling routing alongside other executor backends (shell, HTTP, MCP, LLM).
74///
75/// ```rust
76/// use std::sync::Arc;
77/// use soul_core::vfs::MemoryFs;
78/// use soul_core::vexec::NoopExecutor;
79///
80/// let fs = Arc::new(MemoryFs::new());
81/// let exec = Arc::new(NoopExecutor);
82/// let registry = soul_coder::presets::all_executor(fs, exec, "/workspace");
83///
84/// assert!(registry.has_tool("read"));
85/// assert!(registry.has_tool("bash"));
86/// ```
87pub fn all_executor(
88    fs: Arc<dyn VirtualFs>,
89    executor: Arc<dyn VirtualExecutor>,
90    cwd: impl Into<String>,
91) -> ExecutorRegistry {
92    let tools = all_tools(fs, executor, cwd);
93    wrap_as_executor(tools)
94}
95
96/// Create an [`ExecutorRegistry`] with coding tools (read, write, edit, bash)
97/// wired via [`DirectExecutor`].
98pub fn coding_executor(
99    fs: Arc<dyn VirtualFs>,
100    executor: Arc<dyn VirtualExecutor>,
101    cwd: impl Into<String>,
102) -> ExecutorRegistry {
103    let tools = coding_tools(fs, executor, cwd);
104    wrap_as_executor(tools)
105}
106
107/// Wrap any [`ToolRegistry`] into an [`ExecutorRegistry`] with [`DirectExecutor`]
108/// as the fallback, and all tool definitions registered as [`ConfigTool`] entries.
109pub fn wrap_as_executor(tools: ToolRegistry) -> ExecutorRegistry {
110    let definitions = tools.definitions();
111    let direct = Arc::new(DirectExecutor::new(Arc::new(tools)));
112
113    let mut registry = ExecutorRegistry::new();
114    registry.register_executor(direct.clone() as Arc<dyn ToolExecutor>);
115
116    for def in definitions {
117        registry.register_config_tool(ConfigTool {
118            definition: def,
119            executor_name: "direct".into(),
120            executor_config: serde_json::json!({}),
121        });
122    }
123
124    registry
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use soul_core::vexec::NoopExecutor;
131    use soul_core::vfs::MemoryFs;
132
133    #[test]
134    fn coding_tools_has_four() {
135        let fs = Arc::new(MemoryFs::new());
136        let exec = Arc::new(NoopExecutor);
137        let registry = coding_tools(fs, exec, "/");
138        assert_eq!(registry.len(), 4);
139        assert!(registry.get("read").is_some());
140        assert!(registry.get("write").is_some());
141        assert!(registry.get("edit").is_some());
142        assert!(registry.get("bash").is_some());
143    }
144
145    #[test]
146    fn read_only_tools_has_four() {
147        let fs = Arc::new(MemoryFs::new());
148        let registry = read_only_tools(fs, "/");
149        assert_eq!(registry.len(), 4);
150        assert!(registry.get("read").is_some());
151        assert!(registry.get("grep").is_some());
152        assert!(registry.get("find").is_some());
153        assert!(registry.get("ls").is_some());
154    }
155
156    #[test]
157    fn all_tools_has_seven() {
158        let fs = Arc::new(MemoryFs::new());
159        let exec = Arc::new(NoopExecutor);
160        let registry = all_tools(fs, exec, "/");
161        assert_eq!(registry.len(), 7);
162        let names = registry.names();
163        assert!(names.contains(&"read"));
164        assert!(names.contains(&"write"));
165        assert!(names.contains(&"edit"));
166        assert!(names.contains(&"bash"));
167        assert!(names.contains(&"grep"));
168        assert!(names.contains(&"find"));
169        assert!(names.contains(&"ls"));
170    }
171
172    #[test]
173    fn definitions_all_have_schemas() {
174        let fs = Arc::new(MemoryFs::new());
175        let exec = Arc::new(NoopExecutor);
176        let registry = all_tools(fs, exec, "/");
177        for def in registry.definitions() {
178            assert!(!def.name.is_empty());
179            assert!(!def.description.is_empty());
180            assert!(def.input_schema.is_object());
181        }
182    }
183
184    #[test]
185    fn all_executor_has_tools() {
186        let fs = Arc::new(MemoryFs::new());
187        let exec = Arc::new(NoopExecutor);
188        let registry = all_executor(fs, exec, "/");
189        assert!(registry.has_tool("read"));
190        assert!(registry.has_tool("write"));
191        assert!(registry.has_tool("edit"));
192        assert!(registry.has_tool("bash"));
193        assert!(registry.has_tool("grep"));
194        assert!(registry.has_tool("find"));
195        assert!(registry.has_tool("ls"));
196        assert_eq!(registry.definitions().len(), 7);
197    }
198
199    #[test]
200    fn coding_executor_has_four() {
201        let fs = Arc::new(MemoryFs::new());
202        let exec = Arc::new(NoopExecutor);
203        let registry = coding_executor(fs, exec, "/");
204        assert!(registry.has_tool("read"));
205        assert!(registry.has_tool("bash"));
206        assert_eq!(registry.definitions().len(), 4);
207    }
208
209    #[tokio::test]
210    async fn executor_registry_routes_correctly() {
211        let fs = Arc::new(MemoryFs::new());
212        fs.write("/test.txt", "hello").await.unwrap();
213        let exec = Arc::new(NoopExecutor);
214        let registry = all_executor(fs, exec, "/");
215
216        let result = registry
217            .execute("read", "c1", serde_json::json!({"path": "/test.txt"}), None)
218            .await
219            .unwrap();
220        assert!(!result.is_error);
221        assert!(result.content.contains("hello"));
222    }
223}