brainwires_tool_runtime/orchestrator/
mod.rs1pub mod engine;
23pub mod sandbox;
24pub mod types;
25
26pub use engine::{ToolExecutor, ToolOrchestrator, dynamic_to_json};
27pub use sandbox::{
28 DEFAULT_MAX_ARRAY_SIZE,
30 DEFAULT_MAX_MAP_SIZE,
31 DEFAULT_MAX_OPERATIONS,
32 DEFAULT_MAX_STRING_SIZE,
33 DEFAULT_MAX_TOOL_CALLS,
34 DEFAULT_TIMEOUT_MS,
35 EXTENDED_MAX_OPERATIONS,
37 EXTENDED_MAX_TOOL_CALLS,
38 EXTENDED_TIMEOUT_MS,
39 ExecutionLimits,
40 QUICK_MAX_OPERATIONS,
41 QUICK_MAX_TOOL_CALLS,
42 QUICK_TIMEOUT_MS,
43};
44pub use types::{OrchestratorError, OrchestratorResult, ToolCall};
45
46use serde::Deserialize;
52use serde_json::{Value, json};
53use std::collections::HashMap;
54use std::sync::Arc;
55use tokio::sync::RwLock;
56
57use brainwires_core::{Tool, ToolContext, ToolInputSchema, ToolResult};
58
59pub struct OrchestratorTool {
61 orchestrator: Arc<RwLock<ToolOrchestrator>>,
63}
64
65impl OrchestratorTool {
66 pub fn new() -> Self {
68 Self {
69 orchestrator: Arc::new(RwLock::new(ToolOrchestrator::new())),
70 }
71 }
72
73 pub fn get_tools() -> Vec<Tool> {
75 vec![Self::execute_script_tool()]
76 }
77
78 fn execute_script_tool() -> Tool {
79 let mut properties = HashMap::new();
80 properties.insert(
81 "script".to_string(),
82 json!({
83 "type": "string",
84 "description": "The Rhai script to execute. Use registered tool functions (e.g., read_file(path), search_code(pattern)) and return a final value."
85 }),
86 );
87 properties.insert(
88 "max_operations".to_string(),
89 json!({
90 "type": "integer",
91 "description": "Maximum operations allowed (default: 100000)",
92 "default": 100000
93 }),
94 );
95 properties.insert(
96 "max_tool_calls".to_string(),
97 json!({
98 "type": "integer",
99 "description": "Maximum tool calls allowed (default: 50)",
100 "default": 50
101 }),
102 );
103 properties.insert(
104 "timeout_ms".to_string(),
105 json!({
106 "type": "integer",
107 "description": "Timeout in milliseconds (default: 30000)",
108 "default": 30000
109 }),
110 );
111
112 Tool {
113 name: "execute_script".to_string(),
114 description:
115 r#"PRIMARY TOOL: Execute a Rhai script for programmatic tool orchestration.
116
117This is the preferred way to interact with tools. Write Rhai scripts to orchestrate
118multiple tool calls efficiently, with intermediate results staying out of the context window.
119
120Benefits:
121- 37% token reduction vs sequential tool calls
122- Loops, conditionals, and data transformation
123- Batch operations in a single execution
124- Only final result enters context
125
126Available tools can be discovered via `search_tools`. All tools are callable as functions.
127
128Rhai Syntax Quick Reference:
129- Variables: `let x = 42;`
130- Strings: `let s = "hello";` or template: `` `Hello ${name}` ``
131- Arrays: `let arr = [1, 2, 3];`
132- Objects: `let obj = #{ key: "value" };`
133- Loops: `for item in items { ... }`
134- Conditionals: `if condition { ... } else { ... }`
135
136Example - Find and count TODOs:
137```rhai
138let files = list_directory("src");
139let count = 0;
140for file in files {
141 if file.ends_with(".rs") {
142 let content = read_file(file);
143 count += content.matches("TODO").len();
144 }
145}
146`Found ${count} TODO comments`
147```"#
148 .to_string(),
149 input_schema: ToolInputSchema::object(properties, vec!["script".to_string()]),
150 requires_approval: false,
151 defer_loading: false,
152 ..Default::default()
153 }
154 }
155
156 pub async fn register_executor<F>(&self, name: impl Into<String>, executor: F)
158 where
159 F: Fn(serde_json::Value) -> Result<String, String> + Send + Sync + 'static,
160 {
161 let mut orchestrator = self.orchestrator.write().await;
162 orchestrator.register_executor(name, executor);
163 }
164
165 pub async fn execute(
167 &self,
168 tool_use_id: &str,
169 tool_name: &str,
170 input: &Value,
171 _context: &ToolContext,
172 ) -> ToolResult {
173 let result = match tool_name {
174 "execute_script" => self.execute_script(input).await,
175 _ => Err(anyhow::anyhow!("Unknown orchestrator tool: {}", tool_name)),
176 };
177
178 match result {
179 Ok(output) => ToolResult::success(tool_use_id.to_string(), output),
180 Err(e) => ToolResult::error(
181 tool_use_id.to_string(),
182 format!("Script execution failed: {}", e),
183 ),
184 }
185 }
186
187 async fn execute_script(&self, input: &Value) -> anyhow::Result<String> {
188 #[derive(Deserialize)]
189 struct Input {
190 script: String,
191 #[serde(default = "default_max_ops")]
192 max_operations: u64,
193 #[serde(default = "default_max_calls")]
194 max_tool_calls: usize,
195 #[serde(default = "default_timeout")]
196 timeout_ms: u64,
197 }
198
199 fn default_max_ops() -> u64 {
200 100_000
201 }
202 fn default_max_calls() -> usize {
203 50
204 }
205 fn default_timeout() -> u64 {
206 30_000
207 }
208
209 let params: Input = serde_json::from_value(input.clone())?;
210
211 let limits = ExecutionLimits::default()
212 .with_max_operations(params.max_operations)
213 .with_max_tool_calls(params.max_tool_calls)
214 .with_timeout_ms(params.timeout_ms);
215
216 let orchestrator = self.orchestrator.read().await;
217 let result = orchestrator.execute(¶ms.script, limits)?;
218
219 if result.success {
220 let mut output = result.output;
221 if !result.tool_calls.is_empty() {
222 output.push_str(&format!(
223 "\n\n--- Script executed {} tool call(s) in {}ms ---",
224 result.tool_calls.len(),
225 result.execution_time_ms
226 ));
227 }
228 Ok(output)
229 } else {
230 Err(anyhow::anyhow!(
231 result.error.unwrap_or_else(|| "Unknown error".to_string())
232 ))
233 }
234 }
235
236 pub fn orchestrator(&self) -> Arc<RwLock<ToolOrchestrator>> {
238 Arc::clone(&self.orchestrator)
239 }
240}
241
242impl Default for OrchestratorTool {
243 fn default() -> Self {
244 Self::new()
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_get_tools() {
254 let tools = OrchestratorTool::get_tools();
255 assert_eq!(tools.len(), 1);
256 assert_eq!(tools[0].name, "execute_script");
257 assert!(!tools[0].requires_approval);
258 assert!(!tools[0].defer_loading);
259 }
260
261 #[test]
262 fn test_execute_script_tool_definition() {
263 let tool = OrchestratorTool::execute_script_tool();
264 assert_eq!(tool.name, "execute_script");
265 assert!(tool.description.contains("Rhai"));
266 assert!(tool.description.contains("programmatic"));
267 }
268
269 #[tokio::test]
270 async fn test_orchestrator_creation() {
271 let orchestrator_tool = OrchestratorTool::new();
272 let orchestrator = orchestrator_tool.orchestrator.read().await;
273 assert!(orchestrator.registered_tools().is_empty());
274 }
275
276 #[tokio::test]
277 async fn test_register_executor() {
278 let orchestrator_tool = OrchestratorTool::new();
279 orchestrator_tool
280 .register_executor("test_tool", |_| Ok("success".to_string()))
281 .await;
282
283 let orchestrator = orchestrator_tool.orchestrator.read().await;
284 assert!(orchestrator.registered_tools().contains(&"test_tool"));
285 }
286
287 #[tokio::test]
288 async fn test_execute_simple_script() {
289 let orchestrator_tool = OrchestratorTool::new();
290 let context = ToolContext::default();
291
292 let input = json!({
293 "script": "let x = 1 + 2; x"
294 });
295
296 let result = orchestrator_tool
297 .execute("test-id", "execute_script", &input, &context)
298 .await;
299 assert!(!result.is_error);
300 assert!(result.content.contains("3"));
301 }
302
303 #[tokio::test]
304 async fn test_execute_with_tool() {
305 let orchestrator_tool = OrchestratorTool::new();
306 orchestrator_tool
307 .register_executor("greet", |input| {
308 let name = input.as_str().unwrap_or("world");
309 Ok(format!("Hello, {}!", name))
310 })
311 .await;
312
313 let context = ToolContext::default();
314 let input = json!({
315 "script": r#"greet("Claude")"#
316 });
317
318 let result = orchestrator_tool
319 .execute("test-id", "execute_script", &input, &context)
320 .await;
321 assert!(!result.is_error);
322 assert!(result.content.contains("Hello, Claude!"));
323 }
324
325 #[tokio::test]
326 async fn test_execute_unknown_tool_name() {
327 let orchestrator_tool = OrchestratorTool::new();
328 let context = ToolContext::default();
329
330 let result = orchestrator_tool
331 .execute("test-id", "unknown_tool", &json!({}), &context)
332 .await;
333 assert!(result.is_error);
334 assert!(result.content.contains("Unknown orchestrator tool"));
335 }
336}