Skip to main content

sac/tools/
mod.rs

1use std::collections::HashSet;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use serde_json::Value;
6use tokio::sync::Mutex;
7
8use crate::events::EventSink;
9use crate::mcp::McpRegistry;
10use crate::sandbox::SandboxSession;
11use crate::skills::SkillRegistry;
12use crate::terminal::TerminalManager;
13use crate::types::ToolDefinition;
14
15pub mod edit;
16pub mod exec_command;
17pub mod goal;
18pub mod read;
19pub mod terminal;
20pub mod thread;
21pub mod workset;
22pub mod write;
23
24pub struct ToolResult {
25    pub content: String,
26    pub is_error: bool,
27}
28
29#[derive(Clone)]
30pub struct ToolRuntime {
31    pub store_path: PathBuf,
32    pub session_id: Option<String>,
33    pub worker_executable: Option<PathBuf>,
34    pub active_threads: Arc<Mutex<HashSet<String>>>,
35    pub event_sink: EventSink,
36    pub sandbox: Option<SandboxSession>,
37    pub mcp: Option<Arc<McpRegistry>>,
38    pub skills: Option<Arc<SkillRegistry>>,
39    pub activated_skills: Arc<Mutex<HashSet<String>>>,
40    pub terminal_manager: TerminalManager,
41    pub thread_timeout_secs: u64,
42}
43
44static WRITE_LOCK: Mutex<()> = Mutex::const_new(());
45
46pub async fn acquire_write_lock() -> tokio::sync::MutexGuard<'static, ()> {
47    WRITE_LOCK.lock().await
48}
49
50pub fn worker_tool_definitions() -> Vec<ToolDefinition> {
51    use serde_json::json;
52
53    let mut tools = vec![
54        def(
55            "read",
56            "Read file contents with line numbers. Supports offset and limit.",
57            json!({
58                "type": "object",
59                "properties": {
60                    "path": { "type": "string", "description": "Path to file" },
61                    "offset": { "type": "integer", "description": "Line number to start from (0-indexed, optional)" },
62                    "limit": { "type": "integer", "description": "Max lines to read (optional, default 2000)" }
63                },
64                "required": ["path"]
65            }),
66        ),
67        def(
68            "write",
69            "Create a new file or completely overwrite an existing file. Creates parent directories automatically.\n\nUse this tool to:\n- Create new files that don't exist yet\n- Completely replace a file's content when most of it changes\n\nPrefer the `edit` tool for partial modifications to existing files.",
70            json!({
71                "type": "object",
72                "properties": {
73                    "path": { "type": "string", "description": "Path to file" },
74                    "content": { "type": "string", "description": "Content to write" }
75                },
76                "required": ["path", "content"]
77            }),
78        ),
79        def(
80            "edit",
81            "Replace exact text in a file. This is your PRIMARY tool for modifying existing files.\n\nUsage:\n- old_text must be an EXACT substring match of the file's current content (whitespace-sensitive)\n- old_text must appear exactly ONCE in the file. If it appears multiple times, include more surrounding context lines to make it unique\n- Include complete lines in old_text, not partial lines\n- For multiple changes in one file, call edit multiple times\n\nPrefer this tool over exec_command with sed/python for all file modifications.",
82            json!({
83                "type": "object",
84                "properties": {
85                    "path": { "type": "string", "description": "Path to file" },
86                    "old_text": { "type": "string", "description": "The exact text to find in the file. Must be unique — include enough surrounding lines for a unique match. Use complete lines, not fragments." },
87                    "new_text": { "type": "string", "description": "The replacement text. Can be empty to delete the matched text." }
88                },
89                "required": ["path", "old_text", "new_text"]
90            }),
91        ),
92    ];
93
94    tools.push(exec_command::exec_command_definition());
95    tools.push(exec_command::write_stdin_definition());
96    tools.push(terminal::terminal_definition());
97
98    tools
99}
100
101pub fn orchestrator_tool_definitions() -> Vec<ToolDefinition> {
102    vec![
103        thread::dispatch_definition(),
104        thread::threads_definition(),
105        thread::thread_read_definition(),
106        thread::thread_delete_definition(),
107        workset::define_definition(),
108        workset::read_definition(),
109        workset::list_definition(),
110        workset::update_item_definition(),
111        goal::get_goal_definition(),
112        goal::create_goal_definition(),
113        goal::update_goal_definition(),
114    ]
115}
116
117fn def(name: &str, description: &str, parameters: Value) -> ToolDefinition {
118    ToolDefinition {
119        def_type: "function".to_string(),
120        function: crate::types::FunctionDef {
121            name: name.to_string(),
122            description: description.to_string(),
123            parameters,
124        },
125    }
126}
127
128pub fn require_str(args: &Value, key: &str) -> Result<String, ToolResult> {
129    args.get(key)
130        .and_then(|value| value.as_str())
131        .map(|value| value.to_string())
132        .ok_or_else(|| ToolResult {
133            content: format!("Error: '{}' argument required", key),
134            is_error: true,
135        })
136}
137
138pub fn require_string_array(args: &Value, key: &str) -> Result<Vec<String>, ToolResult> {
139    let Some(value) = args.get(key) else {
140        return Ok(Vec::new());
141    };
142
143    let Some(items) = value.as_array() else {
144        return Err(ToolResult {
145            content: format!("Error: '{}' must be an array of strings", key),
146            is_error: true,
147        });
148    };
149
150    let mut out = Vec::with_capacity(items.len());
151    for item in items {
152        let Some(value) = item.as_str() else {
153            return Err(ToolResult {
154                content: format!("Error: '{}' must be an array of strings", key),
155                is_error: true,
156            });
157        };
158        out.push(value.to_string());
159    }
160
161    Ok(out)
162}
163
164pub async fn execute_tool(
165    name: &str,
166    args: Value,
167    runtime: &ToolRuntime,
168    client: &crate::model::ModelClient,
169) -> ToolResult {
170    if name.starts_with("mcp__") {
171        let Some(registry) = &runtime.mcp else {
172            return ToolResult {
173                content: format!("Error: MCP tool '{}' is not available", name),
174                is_error: true,
175            };
176        };
177        return registry.call_tool(name, args).await;
178    }
179
180    match name {
181        "activate_skill" => crate::skills::execute_activate_skill(args, runtime).await,
182        "read" => read::execute(args, runtime).await,
183        "write" => write::execute(args, runtime).await,
184        "edit" => edit::execute(args, runtime).await,
185        "exec_command" => match exec_command::execute_exec_command(&args, runtime).await {
186            Ok(content) => ToolResult {
187                content,
188                is_error: false,
189            },
190            Err(e) => ToolResult {
191                content: format!("Error: {:#}", e),
192                is_error: true,
193            },
194        },
195        "write_stdin" => match exec_command::execute_write_stdin(&args, runtime).await {
196            Ok(content) => ToolResult {
197                content,
198                is_error: false,
199            },
200            Err(e) => ToolResult {
201                content: format!("Error: {:#}", e),
202                is_error: true,
203            },
204        },
205        "terminal" => match terminal::execute_terminal(&args, runtime).await {
206            Ok(content) => ToolResult {
207                content,
208                is_error: false,
209            },
210            Err(e) => ToolResult {
211                content: format!("Error: {:#}", e),
212                is_error: true,
213            },
214        },
215        "thread" => thread::execute_dispatch(args, runtime, client).await,
216        "threads" => thread::execute_threads(runtime).await,
217        "thread_read" => thread::execute_thread_read(args, runtime).await,
218        "thread_delete" => thread::execute_thread_delete(args, runtime).await,
219        "workset_define" => workset::execute_define(args, runtime).await,
220        "workset_read" => workset::execute_read(args, runtime).await,
221        "workset_list" => workset::execute_list(args, runtime).await,
222        "workset_update_item" => workset::execute_update_item(args, runtime).await,
223        "get_goal" => goal::execute_get_goal(args, runtime).await,
224        "create_goal" => goal::execute_create_goal(args, runtime).await,
225        "update_goal" => goal::execute_update_goal(args, runtime).await,
226        unknown => ToolResult {
227            content: format!("Error: unknown tool '{}'", unknown),
228            is_error: true,
229        },
230    }
231}