Skip to main content

a3s_code_core/tools/dynamic/
mod.rs

1//! Dynamic tool implementations
2//!
3//! These tools are loaded at runtime from skills:
4//! - BinaryTool: Execute external binaries
5//! - HttpTool: Make HTTP API calls
6//! - ScriptTool: Execute scripts with interpreters
7
8mod binary;
9mod http;
10mod script;
11
12pub use binary::BinaryTool;
13pub use http::HttpTool;
14pub use script::ScriptTool;
15
16use super::types::{ToolBackend, ToolEventSender, ToolStreamEvent};
17use super::Tool;
18use super::MAX_OUTPUT_SIZE;
19use anyhow::Result;
20use std::sync::Arc;
21use tokio::io::{AsyncBufReadExt, BufReader};
22use tokio::process::Child;
23
24/// Substitute `${arg_name}` placeholders in a template with values from a JSON object.
25///
26/// Shared by BinaryTool and HttpTool to avoid duplicating substitution logic.
27pub(crate) fn substitute_template_args(template: &str, args: &serde_json::Value) -> String {
28    let mut result = template.to_string();
29
30    if let Some(obj) = args.as_object() {
31        for (key, value) in obj {
32            let placeholder = format!("${{{}}}", key);
33            let replacement = match value {
34                serde_json::Value::String(s) => s.clone(),
35                serde_json::Value::Number(n) => n.to_string(),
36                serde_json::Value::Bool(b) => b.to_string(),
37                _ => value.to_string(),
38            };
39            result = result.replace(&placeholder, &replacement);
40        }
41    }
42
43    result
44}
45
46/// Read stdout/stderr from a child process with a size limit and timeout.
47///
48/// Returns `(output, timed_out)`. If `timed_out` is true, the child is killed.
49/// Shared by BinaryTool and ScriptTool to avoid duplicating the select loop.
50///
51/// When `event_tx` is provided, each line is also sent as a
52/// `ToolStreamEvent::OutputDelta` for real-time streaming to the client.
53pub(crate) async fn read_process_output(
54    child: &mut Child,
55    timeout_secs: u64,
56    event_tx: Option<&ToolEventSender>,
57) -> (String, bool) {
58    let stdout = child.stdout.take().unwrap();
59    let stderr = child.stderr.take().unwrap();
60
61    let mut stdout_reader = BufReader::new(stdout).lines();
62    let mut stderr_reader = BufReader::new(stderr).lines();
63
64    let mut output = String::new();
65    let mut total_size = 0usize;
66
67    let timeout = tokio::time::Duration::from_secs(timeout_secs);
68    let result = tokio::time::timeout(timeout, async {
69        loop {
70            tokio::select! {
71                line = stdout_reader.next_line() => {
72                    match line {
73                        Ok(Some(line)) => {
74                            if total_size < MAX_OUTPUT_SIZE {
75                                output.push_str(&line);
76                                output.push('\n');
77                                total_size += line.len() + 1;
78                            }
79                            if let Some(tx) = event_tx {
80                                let mut delta = line;
81                                delta.push('\n');
82                                tx.send(ToolStreamEvent::OutputDelta(delta)).await.ok();
83                            }
84                        }
85                        Ok(None) => break,
86                        Err(_) => break,
87                    }
88                }
89                line = stderr_reader.next_line() => {
90                    match line {
91                        Ok(Some(line)) => {
92                            if total_size < MAX_OUTPUT_SIZE {
93                                output.push_str(&line);
94                                output.push('\n');
95                                total_size += line.len() + 1;
96                            }
97                            if let Some(tx) = event_tx {
98                                let mut delta = line;
99                                delta.push('\n');
100                                tx.send(ToolStreamEvent::OutputDelta(delta)).await.ok();
101                            }
102                        }
103                        Ok(None) => {}
104                        Err(_) => {}
105                    }
106                }
107            }
108        }
109    })
110    .await;
111
112    if result.is_err() {
113        child.kill().await.ok();
114        return (output, true);
115    }
116
117    (output, false)
118}
119
120/// Create a dynamic tool from a backend specification.
121///
122/// Used by skill loading to instantiate tools from `ToolBackend` config.
123pub fn create_tool(
124    name: String,
125    description: String,
126    parameters: serde_json::Value,
127    backend: ToolBackend,
128) -> Result<Arc<dyn Tool>> {
129    match backend {
130        ToolBackend::Builtin => {
131            anyhow::bail!("Cannot create builtin tool through create_tool() — register builtin tools directly")
132        }
133        ToolBackend::Binary {
134            url,
135            path,
136            args_template,
137        } => Ok(Arc::new(BinaryTool::new(
138            name,
139            description,
140            parameters,
141            url,
142            path,
143            args_template,
144        ))),
145        ToolBackend::Http {
146            url,
147            method,
148            headers,
149            body_template,
150            timeout_ms,
151        } => Ok(Arc::new(HttpTool::new(
152            name,
153            description,
154            parameters,
155            url,
156            method,
157            headers,
158            body_template,
159            timeout_ms,
160        ))),
161        ToolBackend::Script {
162            interpreter,
163            script,
164            interpreter_args,
165        } => Ok(Arc::new(ScriptTool::new(
166            name,
167            description,
168            parameters,
169            interpreter,
170            script,
171            interpreter_args,
172        ))),
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_create_script_tool() {
182        let tool = create_tool(
183            "test".to_string(),
184            "A test tool".to_string(),
185            serde_json::json!({"type": "object", "properties": {}}),
186            ToolBackend::Script {
187                interpreter: "bash".to_string(),
188                script: "echo hello".to_string(),
189                interpreter_args: vec![],
190            },
191        )
192        .unwrap();
193
194        assert_eq!(tool.name(), "test");
195        assert_eq!(tool.description(), "A test tool");
196    }
197
198    #[test]
199    fn test_create_http_tool() {
200        let tool = create_tool(
201            "api".to_string(),
202            "An API tool".to_string(),
203            serde_json::json!({"type": "object", "properties": {}}),
204            ToolBackend::Http {
205                url: "https://api.example.com".to_string(),
206                method: "POST".to_string(),
207                headers: std::collections::HashMap::new(),
208                body_template: None,
209                timeout_ms: 30_000,
210            },
211        )
212        .unwrap();
213
214        assert_eq!(tool.name(), "api");
215    }
216
217    #[test]
218    fn test_create_binary_tool() {
219        let tool = create_tool(
220            "bin".to_string(),
221            "A binary tool".to_string(),
222            serde_json::json!({"type": "object", "properties": {}}),
223            ToolBackend::Binary {
224                url: None,
225                path: Some("/usr/bin/echo".to_string()),
226                args_template: Some("${message}".to_string()),
227            },
228        )
229        .unwrap();
230
231        assert_eq!(tool.name(), "bin");
232    }
233
234    #[test]
235    fn test_create_builtin_returns_error() {
236        let result = create_tool(
237            "builtin".to_string(),
238            "A builtin tool".to_string(),
239            serde_json::json!({}),
240            ToolBackend::Builtin,
241        );
242        assert!(result.is_err());
243        assert!(result
244            .err()
245            .unwrap()
246            .to_string()
247            .contains("Cannot create builtin tool"));
248    }
249}