1use super::*;
4
5pub const SUBAGENT_TOOLS: &[&str] = &[
10 "spawn_agent",
11 "check_agent",
12 "wait_for_agent",
13 "send_to_agent",
14 "kill_agent",
15];
16
17pub fn is_subagent_tool(name: &str) -> bool {
19 SUBAGENT_TOOLS.contains(&name)
20}
21
22pub(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
34pub fn submit_output_description(described: &str) -> String {
45 let base = "Submit your final answer for this run. This is the value the caller receives - a \
46 person reading the run, a parent agent, the API. Nothing else you write is \
47 returned to them, so put the answer itself here rather than a pointer to it. \
48 Call this once, when your work is done; calling it again replaces what you \
49 submitted.\n\nYour answer is one response, so it cannot hold a large dataset or a \
50 very long document. Write those to files as you go, then name them in \
51 `artifacts` and describe them here.";
52 match described.is_empty() {
53 true => base.to_string(),
54 false => format!("{base}\n\n{described}"),
55 }
56}
57
58fn resolved_shell_description() -> &'static str {
64 static DESCRIPTION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
65 DESCRIPTION.get_or_init(|| shell_tool_description(BuiltinTools::detect_shell().0))
66}
67
68impl BuiltinTools {
69 pub fn tool_defs(&self) -> Vec<Tool> {
72 let mut defs = vec![
73 Tool {
74 name: "read_file".to_string(),
75 description: "Read the complete contents of a file. Use this to examine existing code, configurations, or data files before making changes.".to_string(),
76 parameters: json!({
77 "type": "object",
78 "properties": {
79 "path": {
80 "type": "string",
81 "description": "Path to the file, relative to the working directory"
82 }
83 },
84 "required": ["path"]
85 }),
86 },
87 Tool {
88 name: "write_file".to_string(),
89 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(),
90 parameters: json!({
91 "type": "object",
92 "properties": {
93 "path": {
94 "type": "string",
95 "description": "Path to the file, relative to the working directory"
96 },
97 "content": {
98 "type": "string",
99 "description": "The full content to write to the file"
100 }
101 },
102 "required": ["path", "content"]
103 }),
104 },
105 Tool {
106 name: "edit_file".to_string(),
107 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(),
108 parameters: json!({
109 "type": "object",
110 "properties": {
111 "path": {
112 "type": "string",
113 "description": "Path to the file, relative to the working directory"
114 },
115 "old_str": {
116 "type": "string",
117 "description": "The exact string to replace. Must appear exactly once in the file."
118 },
119 "new_str": {
120 "type": "string",
121 "description": "The string to replace old_str with"
122 }
123 },
124 "required": ["path", "old_str", "new_str"]
125 }),
126 },
127 Tool {
128 name: "list_dir".to_string(),
129 description: "List the contents of a directory. Use this to explore the file structure before reading or writing files.".to_string(),
130 parameters: json!({
131 "type": "object",
132 "properties": {
133 "path": {
134 "type": "string",
135 "description": "Path to the directory, relative to the working directory. Defaults to the working directory root if omitted."
136 }
137 },
138 "required": []
139 }),
140 },
141 Tool {
142 name: "read_files".to_string(),
143 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(),
144 parameters: json!({
145 "type": "object",
146 "properties": {
147 "paths": {
148 "type": "array",
149 "items": { "type": "string" },
150 "description": "Array of file paths relative to the working directory"
151 }
152 },
153 "required": ["paths"]
154 }),
155 },
156 Tool {
157 name: "shell".to_string(),
158 description: resolved_shell_description().to_string(),
159 parameters: json!({
160 "type": "object",
161 "properties": {
162 "command": {
163 "type": "string",
164 "description": "The shell command to execute"
165 }
166 },
167 "required": ["command"]
168 }),
169 },
170 Tool {
171 name: "present_for_review".to_string(),
172 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(),
173 parameters: json!({
174 "type": "object",
175 "properties": {
176 "title": {
177 "type": "string",
178 "description": "Short title for the review prompt shown to the user (e.g. 'Implementation Plan Ready for Review')"
179 },
180 "markdown": {
181 "type": "string",
182 "description": "The markdown document to present to the user. Supports headings, lists, code blocks, and mermaid diagrams."
183 }
184 },
185 "required": ["title", "markdown"]
186 }),
187 },
188 Tool {
189 name: "ask_user_text".to_string(),
190 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(),
191 parameters: json!({
192 "type": "object",
193 "properties": {
194 "prompt": {
195 "type": "string",
196 "description": "The question to ask the user"
197 }
198 },
199 "required": ["prompt"]
200 }),
201 },
202 Tool {
203 name: "ask_user_choice".to_string(),
204 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(),
205 parameters: json!({
206 "type": "object",
207 "properties": {
208 "prompt": {
209 "type": "string",
210 "description": "The question to ask the user"
211 },
212 "options": {
213 "type": "array",
214 "items": { "type": "string" },
215 "description": "At least two options for the user to choose from"
216 }
217 },
218 "required": ["prompt", "options"]
219 }),
220 },
221 Tool {
222 name: "ask_user_confirm".to_string(),
223 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(),
224 parameters: json!({
225 "type": "object",
226 "properties": {
227 "prompt": {
228 "type": "string",
229 "description": "The yes/no question to ask the user"
230 }
231 },
232 "required": ["prompt"]
233 }),
234 },
235 Tool {
236 name: "edit_document".to_string(),
237 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(),
238 parameters: json!({
239 "type": "object",
240 "properties": {
241 "content": {
242 "type": "string",
243 "description": "The current full document text to present for editing"
244 },
245 "prompt": {
246 "type": "string",
247 "description": "Optional instruction shown above the editable field"
248 }
249 },
250 "required": ["content"]
251 }),
252 },
253 Tool {
254 name: "context_write".to_string(),
255 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(),
256 parameters: json!({
257 "type": "object",
258 "properties": {
259 "region": {
260 "type": "string",
261 "description": "Name of the context window section (e.g. 'architecture', 'plan')"
262 },
263 "key": {
264 "type": "string",
265 "description": "Key for the entry. Replaces existing entry with the same key."
266 },
267 "content": {
268 "type": "string",
269 "description": "Content to store"
270 }
271 },
272 "required": ["region", "content"]
273 }),
274 },
275 Tool {
276 name: "todo_add".to_string(),
277 description: "Add an item to a checklist region. Returns the item's id, which todo_done and todo_note take. Use this for work you have identified but not finished, so that what is left is tracked rather than remembered.".to_string(),
278 parameters: json!({
279 "type": "object",
280 "properties": {
281 "region": {
282 "type": "string",
283 "description": "Name of the checklist region (e.g. 'todos')"
284 },
285 "item": {
286 "type": "string",
287 "description": "What needs doing, in one line"
288 }
289 },
290 "required": ["region", "item"]
291 }),
292 },
293 Tool {
294 name: "todo_done".to_string(),
295 description: "Mark a checklist item finished, by the id todo_add returned. Items you have finished must be ticked off: a stage can be held until its checklist has no open items.".to_string(),
296 parameters: json!({
297 "type": "object",
298 "properties": {
299 "region": {
300 "type": "string",
301 "description": "Name of the checklist region"
302 },
303 "id": {
304 "type": "integer",
305 "description": "The item's id, as returned by todo_add"
306 }
307 },
308 "required": ["region", "id"]
309 }),
310 },
311 Tool {
312 name: "todo_note".to_string(),
313 description: "Record a note against a checklist item without closing it - what you tried, what blocked you, what it depends on.".to_string(),
314 parameters: json!({
315 "type": "object",
316 "properties": {
317 "region": {
318 "type": "string",
319 "description": "Name of the checklist region"
320 },
321 "id": {
322 "type": "integer",
323 "description": "The item's id"
324 },
325 "note": {
326 "type": "string",
327 "description": "The note to record"
328 }
329 },
330 "required": ["region", "id", "note"]
331 }),
332 },
333 Tool {
334 name: "context_append".to_string(),
335 description: "Add content to an existing section of your context window without replacing what's already there.".to_string(),
336 parameters: json!({
337 "type": "object",
338 "properties": {
339 "region": {
340 "type": "string",
341 "description": "Name of the context window section"
342 },
343 "key": {
344 "type": "string",
345 "description": "Optional name for this entry. Naming it lets you release it later with context_delete once you are done with it - worth doing for anything bulky you expect to finish with, like a fetched source you plan to distill."
346 },
347 "content": {
348 "type": "string",
349 "description": "Content to append"
350 }
351 },
352 "required": ["region", "content"]
353 }),
354 },
355 Tool {
356 name: "context_read".to_string(),
357 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(),
358 parameters: json!({
359 "type": "object",
360 "properties": {
361 "region": {
362 "type": "string",
363 "description": "Name of the context window section to read"
364 },
365 "key": {
366 "type": "string",
367 "description": "Key of a specific entry to read"
368 }
369 },
370 "required": ["region"]
371 }),
372 },
373 Tool {
374 name: "context_delete".to_string(),
375 description: "Release an entry you are finished with from a section of your context window, freeing its tokens. Use this when you have distilled what matters out of something bulky and no longer need the original - a source you have summarized, a file you have extracted from. Name the entry by 'key' if you gave it one, by 'index' as shown in context_list, or drop the oldest few with 'oldest'.".to_string(),
376 parameters: json!({
377 "type": "object",
378 "properties": {
379 "region": {
380 "type": "string",
381 "description": "Name of the context window section"
382 },
383 "key": {
384 "type": "string",
385 "description": "Key of the entry to release, if it has one"
386 },
387 "index": {
388 "type": "integer",
389 "description": "Position of the entry to release, counting from 0 as the oldest, as shown by context_list"
390 },
391 "oldest": {
392 "type": "integer",
393 "description": "Release this many of the oldest entries. Use when you need room and the oldest material is the least useful."
394 }
395 },
396 "required": ["region"]
397 }),
398 },
399 Tool {
400 name: "context_list".to_string(),
401 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(),
402 parameters: json!({
403 "type": "object",
404 "properties": {
405 "region": {
406 "type": "string",
407 "description": "Optional region name to list keys for"
408 }
409 },
410 "required": []
411 }),
412 },
413 Tool {
414 name: crate::SUBMIT_OUTPUT_TOOL.to_string(),
418 description: submit_output_description(""),
419 parameters: json!({
420 "type": "object",
421 "properties": {
422 "content": {
423 "type": "string",
424 "description": "Your final answer, in full."
425 },
426 "artifacts": {
427 "type": "array",
428 "items": { "type": "string" },
429 "description": "Files you produced that the caller should read, as paths relative to the working directory. Use this for anything too large to put in the answer: a dataset, a long report, a generated file. Name the file here rather than only mentioning it in prose."
430 }
431 },
432 "required": ["content"]
433 }),
434 },
435 ];
436 defs.retain(|t| self.available(&t.name));
437 defs
438 }
439
440 pub fn subagent_tool_defs() -> Vec<Tool> {
445 vec![
446 Tool {
447 name: "spawn_agent".to_string(),
448 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(),
449 parameters: json!({
450 "type": "object",
451 "properties": {
452 "blueprint": {
453 "type": "string",
454 "description": "Name of the agent blueprint to spawn"
455 },
456 "task": {
457 "type": "string",
458 "description": "Task prompt for the sub-agent"
459 },
460 "wait": {
461 "type": "boolean",
462 "description": "If true, block until the sub-agent completes and return its result. Default: false",
463 "default": false
464 },
465 "seed_context": {
466 "type": "string",
467 "description": "Optional initial context to inject into the sub-agent's first Pinned region"
468 },
469 "max_child_depth": {
470 "type": "integer",
471 "description": "Optional max depth for the sub-agent's own children"
472 },
473 "output_format": {
474 "type": "string",
475 "description": "Optional shape to ask the sub-agent for its final answer in, overriding its blueprint's. Any label works (markdown, json, xml, a media type, your own); it is passed to the sub-agent, not interpreted here."
476 },
477 "output_instructions": {
478 "type": "string",
479 "description": "Optional extra guidance about that shape, passed to the sub-agent alongside output_format."
480 }
481 },
482 "required": ["blueprint", "task"]
483 }),
484 },
485 Tool {
486 name: "check_agent".to_string(),
487 description: "Check the status of a sub-agent. Returns its current status and result if complete. Non-blocking.".to_string(),
488 parameters: json!({
489 "type": "object",
490 "properties": {
491 "agent_id": {
492 "type": "string",
493 "description": "ID of the agent to check"
494 }
495 },
496 "required": ["agent_id"]
497 }),
498 },
499 Tool {
500 name: "wait_for_agent".to_string(),
501 description: "Block until a sub-agent completes, then return its final result.".to_string(),
502 parameters: json!({
503 "type": "object",
504 "properties": {
505 "agent_id": {
506 "type": "string",
507 "description": "ID of the agent to wait for"
508 }
509 },
510 "required": ["agent_id"]
511 }),
512 },
513 Tool {
514 name: "send_to_agent".to_string(),
515 description: "Send a message to a running sub-agent's context window.".to_string(),
516 parameters: json!({
517 "type": "object",
518 "properties": {
519 "agent_id": {
520 "type": "string",
521 "description": "ID of the target agent"
522 },
523 "message": {
524 "type": "string",
525 "description": "Message content to send"
526 },
527 "target_region": {
528 "type": "string",
529 "description": "Context region to deliver to (default: conversation)"
530 }
531 },
532 "required": ["agent_id", "message"]
533 }),
534 },
535 Tool {
536 name: "kill_agent".to_string(),
537 description: "Kill a sub-agent and all its descendants. Sets their cancellation tokens and marks them as cancelled.".to_string(),
538 parameters: json!({
539 "type": "object",
540 "properties": {
541 "agent_id": {
542 "type": "string",
543 "description": "ID of the agent to kill"
544 }
545 },
546 "required": ["agent_id"]
547 }),
548 },
549 ]
550 }
551
552 pub fn subagent_tool_names() -> Vec<String> {
554 vec![
555 "spawn_agent".to_string(),
556 "check_agent".to_string(),
557 "wait_for_agent".to_string(),
558 "send_to_agent".to_string(),
559 "kill_agent".to_string(),
560 ]
561 }
562
563 pub fn names(&self) -> Vec<String> {
569 let mut names: Vec<String> = [
570 "read_file",
571 "read_files",
572 "write_file",
573 "edit_file",
574 "list_dir",
575 "shell",
576 "present_for_review",
577 "ask_user_text",
578 "ask_user_choice",
579 "ask_user_confirm",
580 "edit_document",
581 "context_write",
582 "context_append",
583 "context_read",
584 "context_delete",
585 "context_list",
586 "todo_add",
587 "todo_done",
588 "todo_note",
589 crate::SUBMIT_OUTPUT_TOOL,
590 ]
591 .iter()
592 .filter(|n| self.available(n))
596 .map(|s| s.to_string())
597 .collect();
598 names.extend(
601 TOOL_ALIASES
602 .iter()
603 .filter(|(_, canonical)| self.available(canonical))
604 .map(|(alias, _)| alias.to_string()),
605 );
606 names
607 }
608}