Skip to main content

brainwires_tool_runtime/orchestrator/
mod.rs

1//! Tool Orchestrator - Rhai-based tool orchestration for AI agents
2//!
3//! Implements Anthropic's "Programmatic Tool Calling" pattern for token-efficient
4//! tool orchestration. Instead of sequential tool calls, AI writes Rhai scripts
5//! that orchestrate multiple tools, returning only the final result.
6//!
7//! ## Features
8//!
9//! This module supports multiple build targets via feature flags:
10//!
11//! - **`orchestrator`** (default for native) - Thread-safe Rust library with `Arc`/`Mutex`
12//! - **`orchestrator-wasm`** - WebAssembly bindings for browser/Node.js via `wasm-bindgen`
13//!
14//! ## Benefits
15//!
16//! - **37% token reduction** - intermediate results don't pollute context
17//! - **Parallel execution** - multiple tools in one pass
18//! - **Complex orchestration** - loops, conditionals, data processing
19
20// When both features are active (e.g. --all-features), native `orchestrator` takes priority.
21
22pub mod engine;
23pub mod sandbox;
24pub mod types;
25
26pub use engine::{ToolExecutor, ToolOrchestrator, dynamic_to_json};
27pub use sandbox::{
28    // Default limit constants
29    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    // Profile constants
36    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
46// ── OrchestratorTool wrapper ───────────────────────────────────────────────
47//
48// High-level tool wrapper that integrates the Rhai orchestrator with the
49// brainwires tool system.
50
51use 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
59/// Orchestrator tool for executing Rhai scripts with access to registered tools
60pub struct OrchestratorTool {
61    /// The underlying Rhai orchestrator
62    orchestrator: Arc<RwLock<ToolOrchestrator>>,
63}
64
65impl OrchestratorTool {
66    /// Create a new OrchestratorTool
67    pub fn new() -> Self {
68        Self {
69            orchestrator: Arc::new(RwLock::new(ToolOrchestrator::new())),
70        }
71    }
72
73    /// Get the tool definition for execute_script
74    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    /// Register a tool executor function
157    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    /// Execute the orchestrator tool
166    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(&params.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    /// Get the underlying orchestrator for direct access
237    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}