leviath-tools 0.3.8

Native built-in tools for Leviath agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
//! Tool definitions: the schemas advertised to the model.

use super::*;

/// The tool names routed to the sub-agent handler (they run against the daemon's
/// agent engine, not the builtin/MCP executors). One list, shared by the CLI's
/// dispatch routing and the runtime's crash-replay synthesis, so the two can't
/// drift.
pub const SUBAGENT_TOOLS: &[&str] = &[
    "spawn_agent",
    "check_agent",
    "wait_for_agent",
    "send_to_agent",
    "kill_agent",
];

/// Whether `name` is a sub-agent tool.
pub fn is_subagent_tool(name: &str) -> bool {
    SUBAGENT_TOOLS.contains(&name)
}

/// The `shell` tool's description, naming the shell this host actually resolved
/// instead of listing every platform's and leaving the model to guess which one
/// it got. Pure over the shell so both wordings are testable on any platform.
pub(crate) fn shell_tool_description(shell: &str) -> String {
    format!(
        "Execute a shell command in the working directory. On this machine commands run \
         through `{shell}`, so write them in its syntax. Use this for build commands, \
         running tests, installing dependencies, or other shell operations. Has a \
         60-second timeout."
    )
}

/// The `submit_output` tool's description, built from the output shape resolved
/// for the stage rather than fixed at compile time.
///
/// This is the whole mechanism by which an arbitrary format works. There is no
/// per-format code anywhere in the engine; what makes a model produce a2ui, or a
/// house schema, or a format invented after this function was written, is that
/// the format label, the author's instructions, and a literal example all arrive
/// here and go straight to the model. `described` is
/// [`leviath_core::describe_spec`]'s rendering of the resolved spec, and is
/// empty when nothing was declared.
pub fn submit_output_description(described: &str) -> String {
    let base = "Submit your final answer for this run. This is the value the caller receives - a \
                person reading the run, a parent agent, the API. Nothing else you write is \
                returned to them, so put the answer itself here rather than a pointer to it. \
                Call this once, when your work is done; calling it again replaces what you \
                submitted.\n\nYour answer is one response, so it cannot hold a large dataset or a \
                very long document. Write those to files as you go, then name them in \
                `artifacts` and describe them here.";
    match described.is_empty() {
        true => base.to_string(),
        false => format!("{base}\n\n{described}"),
    }
}

/// [`shell_tool_description`] for the resolved shell, computed once.
///
/// `detect_shell` reads `$SHELL` and probes the filesystem on Unix, and
/// `tool_defs` runs on every request, so the answer is cached. The shell cannot
/// change under a running process in any way this would need to notice.
fn resolved_shell_description() -> &'static str {
    static DESCRIPTION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
    DESCRIPTION.get_or_init(|| shell_tool_description(BuiltinTools::detect_shell().0))
}

impl BuiltinTools {
    /// All tool definitions to advertise to the LLM, minus any whose required
    /// platform capabilities aren't provided by the current platform.
    pub fn tool_defs(&self) -> Vec<Tool> {
        let mut defs = vec![
            Tool {
                name: "read_file".to_string(),
                description: "Read the complete contents of a file. Use this to examine existing code, configurations, or data files before making changes.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Path to the file, relative to the working directory"
                        }
                    },
                    "required": ["path"]
                }),
            },
            Tool {
                name: "write_file".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Path to the file, relative to the working directory"
                        },
                        "content": {
                            "type": "string",
                            "description": "The full content to write to the file"
                        }
                    },
                    "required": ["path", "content"]
                }),
            },
            Tool {
                name: "edit_file".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Path to the file, relative to the working directory"
                        },
                        "old_str": {
                            "type": "string",
                            "description": "The exact string to replace. Must appear exactly once in the file."
                        },
                        "new_str": {
                            "type": "string",
                            "description": "The string to replace old_str with"
                        }
                    },
                    "required": ["path", "old_str", "new_str"]
                }),
            },
            Tool {
                name: "list_dir".to_string(),
                description: "List the contents of a directory. Use this to explore the file structure before reading or writing files.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Path to the directory, relative to the working directory. Defaults to the working directory root if omitted."
                        }
                    },
                    "required": []
                }),
            },
            Tool {
                name: "read_files".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "paths": {
                            "type": "array",
                            "items": { "type": "string" },
                            "description": "Array of file paths relative to the working directory"
                        }
                    },
                    "required": ["paths"]
                }),
            },
            Tool {
                name: "shell".to_string(),
                description: resolved_shell_description().to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "command": {
                            "type": "string",
                            "description": "The shell command to execute"
                        }
                    },
                    "required": ["command"]
                }),
            },
            Tool {
                name: "present_for_review".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "title": {
                            "type": "string",
                            "description": "Short title for the review prompt shown to the user (e.g. 'Implementation Plan Ready for Review')"
                        },
                        "markdown": {
                            "type": "string",
                            "description": "The markdown document to present to the user. Supports headings, lists, code blocks, and mermaid diagrams."
                        }
                    },
                    "required": ["title", "markdown"]
                }),
            },
            Tool {
                name: "ask_user_text".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "prompt": {
                            "type": "string",
                            "description": "The question to ask the user"
                        }
                    },
                    "required": ["prompt"]
                }),
            },
            Tool {
                name: "ask_user_choice".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "prompt": {
                            "type": "string",
                            "description": "The question to ask the user"
                        },
                        "options": {
                            "type": "array",
                            "items": { "type": "string" },
                            "description": "At least two options for the user to choose from"
                        }
                    },
                    "required": ["prompt", "options"]
                }),
            },
            Tool {
                name: "ask_user_confirm".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "prompt": {
                            "type": "string",
                            "description": "The yes/no question to ask the user"
                        }
                    },
                    "required": ["prompt"]
                }),
            },
            Tool {
                name: "edit_document".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "content": {
                            "type": "string",
                            "description": "The current full document text to present for editing"
                        },
                        "prompt": {
                            "type": "string",
                            "description": "Optional instruction shown above the editable field"
                        }
                    },
                    "required": ["content"]
                }),
            },
            Tool {
                name: "context_write".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the context window section (e.g. 'architecture', 'plan')"
                        },
                        "key": {
                            "type": "string",
                            "description": "Key for the entry. Replaces existing entry with the same key."
                        },
                        "content": {
                            "type": "string",
                            "description": "Content to store"
                        }
                    },
                    "required": ["region", "content"]
                }),
            },
            Tool {
                name: "todo_add".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the checklist region (e.g. 'todos')"
                        },
                        "item": {
                            "type": "string",
                            "description": "What needs doing, in one line"
                        }
                    },
                    "required": ["region", "item"]
                }),
            },
            Tool {
                name: "todo_done".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the checklist region"
                        },
                        "id": {
                            "type": "integer",
                            "description": "The item's id, as returned by todo_add"
                        }
                    },
                    "required": ["region", "id"]
                }),
            },
            Tool {
                name: "todo_note".to_string(),
                description: "Record a note against a checklist item without closing it - what you tried, what blocked you, what it depends on.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the checklist region"
                        },
                        "id": {
                            "type": "integer",
                            "description": "The item's id"
                        },
                        "note": {
                            "type": "string",
                            "description": "The note to record"
                        }
                    },
                    "required": ["region", "id", "note"]
                }),
            },
            Tool {
                name: "context_append".to_string(),
                description: "Add content to an existing section of your context window without replacing what's already there.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the context window section"
                        },
                        "key": {
                            "type": "string",
                            "description": "Key for the entry"
                        },
                        "content": {
                            "type": "string",
                            "description": "Content to append"
                        }
                    },
                    "required": ["region", "content"]
                }),
            },
            Tool {
                name: "context_read".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the context window section to read"
                        },
                        "key": {
                            "type": "string",
                            "description": "Key of a specific entry to read"
                        }
                    },
                    "required": ["region"]
                }),
            },
            Tool {
                name: "context_delete".to_string(),
                description: "Remove a specific keyed entry from a section of your context window.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Name of the context window section"
                        },
                        "key": {
                            "type": "string",
                            "description": "Key of the entry to remove"
                        }
                    },
                    "required": ["region", "key"]
                }),
            },
            Tool {
                name: "context_list".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "region": {
                            "type": "string",
                            "description": "Optional region name to list keys for"
                        }
                    },
                    "required": []
                }),
            },
            Tool {
                // The shape lives in the description, not the arguments, so
                // that a stage asking for a2ui and one asking for markdown
                // advertise the same schema. Nothing here parses `content`.
                name: crate::SUBMIT_OUTPUT_TOOL.to_string(),
                description: submit_output_description(""),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "content": {
                            "type": "string",
                            "description": "Your final answer, in full."
                        },
                        "artifacts": {
                            "type": "array",
                            "items": { "type": "string" },
                            "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."
                        }
                    },
                    "required": ["content"]
                }),
            },
        ];
        defs.retain(|t| self.available(&t.name));
        defs
    }

    /// Tool definitions for sub-agent management tools.
    ///
    /// These are advertised to the LLM but executed externally (by the CLI's
    /// tool registry) since they require access to the AgentEngine.
    pub fn subagent_tool_defs() -> Vec<Tool> {
        vec![
            Tool {
                name: "spawn_agent".to_string(),
                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(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "blueprint": {
                            "type": "string",
                            "description": "Name of the agent blueprint to spawn"
                        },
                        "task": {
                            "type": "string",
                            "description": "Task prompt for the sub-agent"
                        },
                        "wait": {
                            "type": "boolean",
                            "description": "If true, block until the sub-agent completes and return its result. Default: false",
                            "default": false
                        },
                        "seed_context": {
                            "type": "string",
                            "description": "Optional initial context to inject into the sub-agent's first Pinned region"
                        },
                        "max_child_depth": {
                            "type": "integer",
                            "description": "Optional max depth for the sub-agent's own children"
                        },
                        "output_format": {
                            "type": "string",
                            "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."
                        },
                        "output_instructions": {
                            "type": "string",
                            "description": "Optional extra guidance about that shape, passed to the sub-agent alongside output_format."
                        }
                    },
                    "required": ["blueprint", "task"]
                }),
            },
            Tool {
                name: "check_agent".to_string(),
                description: "Check the status of a sub-agent. Returns its current status and result if complete. Non-blocking.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "agent_id": {
                            "type": "string",
                            "description": "ID of the agent to check"
                        }
                    },
                    "required": ["agent_id"]
                }),
            },
            Tool {
                name: "wait_for_agent".to_string(),
                description: "Block until a sub-agent completes, then return its final result.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "agent_id": {
                            "type": "string",
                            "description": "ID of the agent to wait for"
                        }
                    },
                    "required": ["agent_id"]
                }),
            },
            Tool {
                name: "send_to_agent".to_string(),
                description: "Send a message to a running sub-agent's context window.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "agent_id": {
                            "type": "string",
                            "description": "ID of the target agent"
                        },
                        "message": {
                            "type": "string",
                            "description": "Message content to send"
                        },
                        "target_region": {
                            "type": "string",
                            "description": "Context region to deliver to (default: conversation)"
                        }
                    },
                    "required": ["agent_id", "message"]
                }),
            },
            Tool {
                name: "kill_agent".to_string(),
                description: "Kill a sub-agent and all its descendants. Sets their cancellation tokens and marks them as cancelled.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "agent_id": {
                            "type": "string",
                            "description": "ID of the agent to kill"
                        }
                    },
                    "required": ["agent_id"]
                }),
            },
        ]
    }

    /// Names of sub-agent tools.
    pub fn subagent_tool_names() -> Vec<String> {
        vec![
            "spawn_agent".to_string(),
            "check_agent".to_string(),
            "wait_for_agent".to_string(),
            "send_to_agent".to_string(),
            "kill_agent".to_string(),
        ]
    }

    /// Names of all built-in tools, including every alias in [`TOOL_ALIASES`].
    ///
    /// Aliases are included so tool-call dispatch recognizes a call arriving
    /// under an alias name as a built-in; the canonical names are what get
    /// advertised to the model.
    pub fn names(&self) -> Vec<String> {
        let mut names: Vec<String> = [
            "read_file",
            "read_files",
            "write_file",
            "edit_file",
            "list_dir",
            "shell",
            "present_for_review",
            "ask_user_text",
            "ask_user_choice",
            "ask_user_confirm",
            "edit_document",
            "context_write",
            "context_append",
            "context_read",
            "context_delete",
            "context_list",
            "todo_add",
            "todo_done",
            "todo_note",
            crate::SUBMIT_OUTPUT_TOOL,
        ]
        .iter()
        // Drop any canonical built-in the current platform can't provide, so a
        // filtered-out tool (e.g. `shell` without `ProcessSpawn`) isn't even
        // recognized as a built-in on dispatch.
        .filter(|n| self.available(n))
        .map(|s| s.to_string())
        .collect();
        // Include an alias only when its canonical target survived filtering
        // (so `bash` disappears together with `shell`).
        names.extend(
            TOOL_ALIASES
                .iter()
                .filter(|(_, canonical)| self.available(canonical))
                .map(|(alias, _)| alias.to_string()),
        );
        names
    }
}