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}