Chapter 6: File System Tools
Expanding the Toolbox
In Chapter 2, we built ReadFileTool and ListFilesTool. Now we add WriteFileTool and DeleteFileTool — tools that modify the filesystem. These are the first dangerous tools, which matters when we add human-in-the-loop approval in Chapter 9.
WriteFile
Add to src/tools/file.rs:
#![allow(unused)]
fn main() {
// ─── WriteFile ────────────────────────────────────────────
pub struct WriteFileTool;
impl Tool for WriteFileTool {
fn name(&self) -> &str {
"write_file"
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".into(),
function: FunctionDefinition {
name: "write_file".into(),
description: "Write content to a file at the specified path. \
Creates parent directories if they don't exist. \
Overwrites the file if it already exists."
.into(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The file path to write to"
},
"content": {
"type": "string",
"description": "The content to write"
}
},
"required": ["path", "content"]
}),
},
}
}
fn execute(&self, args: Value) -> Result<String> {
let path = args["path"]
.as_str()
.context("Missing 'path' argument")?;
let content = args["content"]
.as_str()
.context("Missing 'content' argument")?;
// Create parent directories
if let Some(parent) = std::path::Path::new(path).parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.context("Failed to create parent directories")?;
}
}
match fs::write(path, content) {
Ok(()) => Ok(format!(
"Successfully wrote {} bytes to {path}",
content.len()
)),
Err(e) => Ok(format!("Error writing file: {e}")),
}
}
}
}
create_dir_all — The Recursive Mkdir
fs::create_dir_all is Rust’s equivalent of mkdir -p. If you write to src/deep/nested/file.rs, it creates src/deep/nested/ first. This is the only operation where we propagate Err with ? — failing to create directories is unexpected (usually a permissions issue), not a normal tool error like “file not found.”
DeleteFile
#![allow(unused)]
fn main() {
// ─── DeleteFile ───────────────────────────────────────────
pub struct DeleteFileTool;
impl Tool for DeleteFileTool {
fn name(&self) -> &str {
"delete_file"
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".into(),
function: FunctionDefinition {
name: "delete_file".into(),
description: "Delete a file at the specified path.".into(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The path to the file to delete"
}
},
"required": ["path"]
}),
},
}
}
fn execute(&self, args: Value) -> Result<String> {
let path = args["path"]
.as_str()
.context("Missing 'path' argument")?;
match fs::remove_file(path) {
Ok(()) => Ok(format!("Successfully deleted {path}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(format!("Error: File not found: {path}"))
}
Err(e) => Ok(format!("Error deleting file: {e}")),
}
}
}
}
Registering All File Tools
Update src/main.rs to include the new tools:
#![allow(unused)]
fn main() {
use tools::file::{ReadFileTool, ListFilesTool, WriteFileTool, DeleteFileTool};
// In main():
let mut registry = ToolRegistry::new();
registry.register(Box::new(ReadFileTool));
registry.register(Box::new(ListFilesTool));
registry.register(Box::new(WriteFileTool));
registry.register(Box::new(DeleteFileTool));
}
Tool Safety Classification
Not all tools are equal. read_file is safe — it can’t break anything. delete_file is dangerous. We’ll use this classification in Chapter 9 for human-in-the-loop approval. For now, let’s add a method to the Tool trait.
Update src/agent/tool_registry.rs:
#![allow(unused)]
fn main() {
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn definition(&self) -> ToolDefinition;
fn execute(&self, args: Value) -> Result<String>;
/// Whether this tool requires human approval before execution.
/// Override to return true for dangerous tools.
fn requires_approval(&self) -> bool {
false
}
}
}
Default methods in traits — tools are safe by default. Override for dangerous ones:
#![allow(unused)]
fn main() {
// In WriteFileTool
impl Tool for WriteFileTool {
// ... other methods ...
fn requires_approval(&self) -> bool {
true
}
}
// In DeleteFileTool
impl Tool for DeleteFileTool {
// ... other methods ...
fn requires_approval(&self) -> bool {
true
}
}
}
Add a lookup method to ToolRegistry:
#![allow(unused)]
fn main() {
impl ToolRegistry {
// ... existing methods ...
/// Check if a tool requires approval.
pub fn requires_approval(&self, name: &str) -> bool {
self.tools
.get(name)
.map(|t| t.requires_approval())
.unwrap_or(false)
}
}
}
Error Handling Philosophy
Look at the two error paths in WriteFileTool:
#![allow(unused)]
fn main() {
// Propagated with ? — unexpected, indicates a bug or system issue
fs::create_dir_all(parent)
.context("Failed to create parent directories")?;
// Returned as Ok(String) — expected, the LLM can recover
Err(e) => Ok(format!("Error writing file: {e}")),
}
The rule: if the LLM can do something useful with the error (try a different path, ask the user), return Ok(error_message). If the error means something is fundamentally wrong (permissions failure, disk full), propagate with ?.
Testing the Tools
cargo run
Try asking: “Create a file called test.txt with ‘Hello from the agent’, then read it back to verify.”
The agent should:
- Call
write_fileto create the file - Call
read_fileto verify its contents - Report that the file was created successfully
Summary
In this chapter you:
- Added
WriteFileToolwith recursive directory creation - Added
DeleteFileToolwith proper error handling - Introduced the
requires_approvaltrait method with default implementations - Applied the error handling philosophy:
Ok(message)for recoverable,Errfor unexpected
Next, we add web search and solve the context window management problem.