Skip to main content

leviath_tools/
defs.rs

1//! Tool definitions: the schemas advertised to the model.
2
3use super::*;
4
5/// The tool names routed to the sub-agent handler (they run against the daemon's
6/// agent engine, not the builtin/MCP executors). One list, shared by the CLI's
7/// dispatch routing and the runtime's crash-replay synthesis, so the two can't
8/// drift.
9pub const SUBAGENT_TOOLS: &[&str] = &[
10    "spawn_agent",
11    "check_agent",
12    "wait_for_agent",
13    "send_to_agent",
14    "kill_agent",
15];
16
17/// Whether `name` is a sub-agent tool.
18pub fn is_subagent_tool(name: &str) -> bool {
19    SUBAGENT_TOOLS.contains(&name)
20}
21
22/// The `shell` tool's description, naming the shell this host actually resolved
23/// instead of listing every platform's and leaving the model to guess which one
24/// it got. Pure over the shell so both wordings are testable on any platform.
25pub(crate) fn shell_tool_description(shell: &str) -> String {
26    format!(
27        "Execute a shell command in the working directory. On this machine commands run \
28         through `{shell}`, so write them in its syntax. Use this for build commands, \
29         running tests, installing dependencies, or other shell operations. Has a \
30         60-second timeout."
31    )
32}
33
34/// [`shell_tool_description`] for the resolved shell, computed once.
35///
36/// `detect_shell` reads `$SHELL` and probes the filesystem on Unix, and
37/// `tool_defs` runs on every request, so the answer is cached. The shell cannot
38/// change under a running process in any way this would need to notice.
39fn resolved_shell_description() -> &'static str {
40    static DESCRIPTION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
41    DESCRIPTION.get_or_init(|| shell_tool_description(BuiltinTools::detect_shell().0))
42}
43
44impl BuiltinTools {
45    /// All tool definitions to advertise to the LLM, minus any whose required
46    /// platform capabilities aren't provided by the current platform.
47    pub fn tool_defs(&self) -> Vec<Tool> {
48        let mut defs = vec![
49            Tool {
50                name: "read_file".to_string(),
51                description: "Read the complete contents of a file. Use this to examine existing code, configurations, or data files before making changes.".to_string(),
52                parameters: json!({
53                    "type": "object",
54                    "properties": {
55                        "path": {
56                            "type": "string",
57                            "description": "Path to the file, relative to the working directory"
58                        }
59                    },
60                    "required": ["path"]
61                }),
62            },
63            Tool {
64                name: "write_file".to_string(),
65                description: "Write content to a file, creating it (and any parent directories) if necessary. Use this to create new files or completely replace existing file content.".to_string(),
66                parameters: json!({
67                    "type": "object",
68                    "properties": {
69                        "path": {
70                            "type": "string",
71                            "description": "Path to the file, relative to the working directory"
72                        },
73                        "content": {
74                            "type": "string",
75                            "description": "The full content to write to the file"
76                        }
77                    },
78                    "required": ["path", "content"]
79                }),
80            },
81            Tool {
82                name: "edit_file".to_string(),
83                description: "Replace an exact string in an existing file. The old_str must appear exactly once in the file. Use this for targeted edits rather than rewriting entire files.".to_string(),
84                parameters: json!({
85                    "type": "object",
86                    "properties": {
87                        "path": {
88                            "type": "string",
89                            "description": "Path to the file, relative to the working directory"
90                        },
91                        "old_str": {
92                            "type": "string",
93                            "description": "The exact string to replace. Must appear exactly once in the file."
94                        },
95                        "new_str": {
96                            "type": "string",
97                            "description": "The string to replace old_str with"
98                        }
99                    },
100                    "required": ["path", "old_str", "new_str"]
101                }),
102            },
103            Tool {
104                name: "list_dir".to_string(),
105                description: "List the contents of a directory. Use this to explore the file structure before reading or writing files.".to_string(),
106                parameters: json!({
107                    "type": "object",
108                    "properties": {
109                        "path": {
110                            "type": "string",
111                            "description": "Path to the directory, relative to the working directory. Defaults to the working directory root if omitted."
112                        }
113                    },
114                    "required": []
115                }),
116            },
117            Tool {
118                name: "read_files".to_string(),
119                description: "Read multiple files at once. Returns the contents of all requested files in a single response, separated by file path headers. More efficient than calling read_file repeatedly. Use this when you need to read several files (e.g. after list_dir).".to_string(),
120                parameters: json!({
121                    "type": "object",
122                    "properties": {
123                        "paths": {
124                            "type": "array",
125                            "items": { "type": "string" },
126                            "description": "Array of file paths relative to the working directory"
127                        }
128                    },
129                    "required": ["paths"]
130                }),
131            },
132            Tool {
133                name: "shell".to_string(),
134                description: resolved_shell_description().to_string(),
135                parameters: json!({
136                    "type": "object",
137                    "properties": {
138                        "command": {
139                            "type": "string",
140                            "description": "The shell command to execute"
141                        }
142                    },
143                    "required": ["command"]
144                }),
145            },
146            Tool {
147                name: "present_for_review".to_string(),
148                description: "Present a document, plan, or report to the user for review. The agent run will pause and the dashboard will display the document prominently. Use this when you want the user to read and approve something before you continue - for example, a technical design, an implementation plan, or a summary report. The user can provide feedback or simply acknowledge to continue.".to_string(),
149                parameters: json!({
150                    "type": "object",
151                    "properties": {
152                        "title": {
153                            "type": "string",
154                            "description": "Short title for the review prompt shown to the user (e.g. 'Implementation Plan Ready for Review')"
155                        },
156                        "markdown": {
157                            "type": "string",
158                            "description": "The markdown document to present to the user. Supports headings, lists, code blocks, and mermaid diagrams."
159                        }
160                    },
161                    "required": ["title", "markdown"]
162                }),
163            },
164            Tool {
165                name: "ask_user_text".to_string(),
166                description: "Ask the user a free-form question and wait for their written answer. The run pauses until they respond. Use this when you need clarification, missing information, or a specific detail only the user knows - decide for yourself when this is necessary; don't ask about things you can figure out on your own.".to_string(),
167                parameters: json!({
168                    "type": "object",
169                    "properties": {
170                        "prompt": {
171                            "type": "string",
172                            "description": "The question to ask the user"
173                        }
174                    },
175                    "required": ["prompt"]
176                }),
177            },
178            Tool {
179                name: "ask_user_choice".to_string(),
180                description: "Ask the user to pick one option from a list and wait for their answer. The run pauses until they respond. Use this when you have a small number of distinct paths forward and want the user to decide which one, rather than guessing yourself.".to_string(),
181                parameters: json!({
182                    "type": "object",
183                    "properties": {
184                        "prompt": {
185                            "type": "string",
186                            "description": "The question to ask the user"
187                        },
188                        "options": {
189                            "type": "array",
190                            "items": { "type": "string" },
191                            "description": "At least two options for the user to choose from"
192                        }
193                    },
194                    "required": ["prompt", "options"]
195                }),
196            },
197            Tool {
198                name: "ask_user_confirm".to_string(),
199                description: "Ask the user a yes/no question and wait for their answer. The run pauses until they respond. Use this for a quick go/no-go decision before doing something significant or hard to undo.".to_string(),
200                parameters: json!({
201                    "type": "object",
202                    "properties": {
203                        "prompt": {
204                            "type": "string",
205                            "description": "The yes/no question to ask the user"
206                        }
207                    },
208                    "required": ["prompt"]
209                }),
210            },
211            Tool {
212                name: "edit_document".to_string(),
213                description: "Present a document to the user in an editable field pre-filled with its current text, and wait for them to edit it directly. The run pauses until they submit. Use this when the user wants to modify content themselves (e.g. tweak a plan or draft) rather than describe changes for you to make. Pass the current full text as `content`; the returned text is the user's edited version, which you should adopt as authoritative.".to_string(),
214                parameters: json!({
215                    "type": "object",
216                    "properties": {
217                        "content": {
218                            "type": "string",
219                            "description": "The current full document text to present for editing"
220                        },
221                        "prompt": {
222                            "type": "string",
223                            "description": "Optional instruction shown above the editable field"
224                        }
225                    },
226                    "required": ["content"]
227                }),
228            },
229            Tool {
230                name: "context_write".to_string(),
231                description: "Store or update content in a named section of your context window. This content will be included in your system prompt on subsequent turns, making it available for reference. Use this to save analysis, plans, notes, or structured information. If a key is provided and an entry with that key already exists, it will be replaced with the new content.".to_string(),
232                parameters: json!({
233                    "type": "object",
234                    "properties": {
235                        "region": {
236                            "type": "string",
237                            "description": "Name of the context window section (e.g. 'architecture', 'plan')"
238                        },
239                        "key": {
240                            "type": "string",
241                            "description": "Key for the entry. Replaces existing entry with the same key."
242                        },
243                        "content": {
244                            "type": "string",
245                            "description": "Content to store"
246                        }
247                    },
248                    "required": ["region", "content"]
249                }),
250            },
251            Tool {
252                name: "context_append".to_string(),
253                description: "Add content to an existing section of your context window without replacing what's already there.".to_string(),
254                parameters: json!({
255                    "type": "object",
256                    "properties": {
257                        "region": {
258                            "type": "string",
259                            "description": "Name of the context window section"
260                        },
261                        "key": {
262                            "type": "string",
263                            "description": "Key for the entry"
264                        },
265                        "content": {
266                            "type": "string",
267                            "description": "Content to append"
268                        }
269                    },
270                    "required": ["region", "content"]
271                }),
272            },
273            Tool {
274                name: "context_read".to_string(),
275                description: "Read what's currently stored in a section of your context window. If no key is specified and the section contains keyed entries, returns a summary of all keys and their sizes.".to_string(),
276                parameters: json!({
277                    "type": "object",
278                    "properties": {
279                        "region": {
280                            "type": "string",
281                            "description": "Name of the context window section to read"
282                        },
283                        "key": {
284                            "type": "string",
285                            "description": "Key of a specific entry to read"
286                        }
287                    },
288                    "required": ["region"]
289                }),
290            },
291            Tool {
292                name: "context_delete".to_string(),
293                description: "Remove a specific keyed entry from a section of your context window.".to_string(),
294                parameters: json!({
295                    "type": "object",
296                    "properties": {
297                        "region": {
298                            "type": "string",
299                            "description": "Name of the context window section"
300                        },
301                        "key": {
302                            "type": "string",
303                            "description": "Key of the entry to remove"
304                        }
305                    },
306                    "required": ["region", "key"]
307                }),
308            },
309            Tool {
310                name: "context_list".to_string(),
311                description: "List available sections of your context window with their current usage - section names, token counts, and number of entries. Use this to see what's available and what you've already stored.".to_string(),
312                parameters: json!({
313                    "type": "object",
314                    "properties": {
315                        "region": {
316                            "type": "string",
317                            "description": "Optional region name to list keys for"
318                        }
319                    },
320                    "required": []
321                }),
322            },
323        ];
324        defs.retain(|t| self.available(&t.name));
325        defs
326    }
327
328    /// Tool definitions for sub-agent management tools.
329    ///
330    /// These are advertised to the LLM but executed externally (by the CLI's
331    /// tool registry) since they require access to the AgentEngine.
332    pub fn subagent_tool_defs() -> Vec<Tool> {
333        vec![
334            Tool {
335                name: "spawn_agent".to_string(),
336                description: "Spawn a sub-agent from a blueprint to work on a task. Returns the new agent's ID. If wait=true, blocks until the sub-agent completes and returns its result.".to_string(),
337                parameters: json!({
338                    "type": "object",
339                    "properties": {
340                        "blueprint": {
341                            "type": "string",
342                            "description": "Name of the agent blueprint to spawn"
343                        },
344                        "task": {
345                            "type": "string",
346                            "description": "Task prompt for the sub-agent"
347                        },
348                        "wait": {
349                            "type": "boolean",
350                            "description": "If true, block until the sub-agent completes and return its result. Default: false",
351                            "default": false
352                        },
353                        "seed_context": {
354                            "type": "string",
355                            "description": "Optional initial context to inject into the sub-agent's first Pinned region"
356                        },
357                        "max_child_depth": {
358                            "type": "integer",
359                            "description": "Optional max depth for the sub-agent's own children"
360                        }
361                    },
362                    "required": ["blueprint", "task"]
363                }),
364            },
365            Tool {
366                name: "check_agent".to_string(),
367                description: "Check the status of a sub-agent. Returns its current status and result if complete. Non-blocking.".to_string(),
368                parameters: json!({
369                    "type": "object",
370                    "properties": {
371                        "agent_id": {
372                            "type": "string",
373                            "description": "ID of the agent to check"
374                        }
375                    },
376                    "required": ["agent_id"]
377                }),
378            },
379            Tool {
380                name: "wait_for_agent".to_string(),
381                description: "Block until a sub-agent completes, then return its final result.".to_string(),
382                parameters: json!({
383                    "type": "object",
384                    "properties": {
385                        "agent_id": {
386                            "type": "string",
387                            "description": "ID of the agent to wait for"
388                        }
389                    },
390                    "required": ["agent_id"]
391                }),
392            },
393            Tool {
394                name: "send_to_agent".to_string(),
395                description: "Send a message to a running sub-agent's context window.".to_string(),
396                parameters: json!({
397                    "type": "object",
398                    "properties": {
399                        "agent_id": {
400                            "type": "string",
401                            "description": "ID of the target agent"
402                        },
403                        "message": {
404                            "type": "string",
405                            "description": "Message content to send"
406                        },
407                        "target_region": {
408                            "type": "string",
409                            "description": "Context region to deliver to (default: conversation)"
410                        }
411                    },
412                    "required": ["agent_id", "message"]
413                }),
414            },
415            Tool {
416                name: "kill_agent".to_string(),
417                description: "Kill a sub-agent and all its descendants. Sets their cancellation tokens and marks them as cancelled.".to_string(),
418                parameters: json!({
419                    "type": "object",
420                    "properties": {
421                        "agent_id": {
422                            "type": "string",
423                            "description": "ID of the agent to kill"
424                        }
425                    },
426                    "required": ["agent_id"]
427                }),
428            },
429        ]
430    }
431
432    /// Names of sub-agent tools.
433    pub fn subagent_tool_names() -> Vec<String> {
434        vec![
435            "spawn_agent".to_string(),
436            "check_agent".to_string(),
437            "wait_for_agent".to_string(),
438            "send_to_agent".to_string(),
439            "kill_agent".to_string(),
440        ]
441    }
442
443    /// Names of all built-in tools, including every alias in [`TOOL_ALIASES`].
444    ///
445    /// Aliases are included so tool-call dispatch recognizes a call arriving
446    /// under an alias name as a built-in; the canonical names are what get
447    /// advertised to the model.
448    pub fn names(&self) -> Vec<String> {
449        let mut names: Vec<String> = [
450            "read_file",
451            "read_files",
452            "write_file",
453            "edit_file",
454            "list_dir",
455            "shell",
456            "present_for_review",
457            "ask_user_text",
458            "ask_user_choice",
459            "ask_user_confirm",
460            "edit_document",
461            "context_write",
462            "context_append",
463            "context_read",
464            "context_delete",
465            "context_list",
466        ]
467        .iter()
468        // Drop any canonical built-in the current platform can't provide, so a
469        // filtered-out tool (e.g. `shell` without `ProcessSpawn`) isn't even
470        // recognized as a built-in on dispatch.
471        .filter(|n| self.available(n))
472        .map(|s| s.to_string())
473        .collect();
474        // Include an alias only when its canonical target survived filtering
475        // (so `bash` disappears together with `shell`).
476        names.extend(
477            TOOL_ALIASES
478                .iter()
479                .filter(|(_, canonical)| self.available(canonical))
480                .map(|(alias, _)| alias.to_string()),
481        );
482        names
483    }
484}