The first FoehnAIBuilder tool had to be a very scan tool so the LLM could understand the structure of a project. Then the ability to load a text file so it could figure out what the underlying code did. At this point the LLM couldn’t generate some code to read a file, so I wrote as basic implementation.
// Copyright (c) August 2026, devMobile Software
//
using FoehnAIBuilder.Abstractions;
using FoehnAI.Tools.ReadFile;
namespace FoehnAIBuilder.Tools.ReadFile;
/// <summary>
/// Reads and returns the full text contents of a file.
/// </summary>
public sealed class ReadFileTool : ITool
{
private const int MaxCharacters = 200_000;
private readonly ILogger<ReadFileTool> _logger;
public ReadFileTool(ILogger<ReadFileTool> logger)
{
_logger = logger;
}
public string Name => "file.read";
public string Description => "Reads and returns the full text contents of a file at the given path.";
public string Command => """
{
"type": "object",
"properties": {
"path": { "type": "string", "description": "Path to the file to read (relative or absolute)." }
},
"required": ["path"]
}
""";
public ToolRiskLevel RiskLevel => ToolRiskLevel.ReadOnly;
public async Task<ToolExecutionResult> ExecuteAsync(string argumentsJson, CancellationToken cancellationToken = default)
{
if (!ToolArguments.TryParse(argumentsJson, ReadFileJsonContext.Default.ReadFileArguments, out var args, out var jsonError))
{
_logger.LogWarning("Failed to parse read_file arguments: {Arguments} ({Error})", argumentsJson, jsonError);
return ToolExecutionResult.Fail(jsonError!);
}
var path = args.Path;
if (!ToolPath.TryResolve(path, out var fullPath, out var pathError))
return ToolExecutionResult.Fail(pathError!);
_logger.LogInformation("Reading file {Path}", path);
if (!File.Exists(fullPath))
return ToolExecutionResult.Fail($"File not found: {path}");
try
{
var content = await File.ReadAllTextAsync(fullPath, cancellationToken);
if (content.Length > MaxCharacters)
{
var truncated = content[..MaxCharacters];
return ToolExecutionResult.Ok(
$"{truncated}\n\n[Output truncated at {MaxCharacters} characters; file is {content.Length} characters long.]");
}
return ToolExecutionResult.Ok(content);
}
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
{
_logger.LogError(ex, "Error reading file {Path}", path);
return ToolExecutionResult.Fail($"Error reading \"{path}\": {ex.Message}");
}
}
}
The LLM took a couple of attempts to get the right file path. If could be the way I phrased the question or files.scan tool needs some work.
The markdown response for the review was huge
Overview
The ReadFileTool class is a tool designed to read and return the contents of a file. It implements the ITool interface and is part of the FoehnAIBuilder.Tools.ReadFile namespace. The tool is marked as ReadOnly in terms of risk level, indicating it does not modify any files or system state.
Key Components
-
Constants and Fields:
MaxCharacters: A constant set to200,000, defining the maximum number of characters to read from a file. If the file exceeds this limit, the output is truncated._logger: A logger instance for logging information, warnings, and errors.
-
Properties:
Name: Returns"file.read", the name of the tool.Description: Provides a brief description of the tool’s functionality.Command: Defines the JSON schema for the tool’s input arguments. It expects a single property,path, which is a required string.RiskLevel: Specifies the tool’s risk level asReadOnly.
-
Constructor:
- The constructor initializes the logger.
-
ExecuteAsync Method:
- This method is responsible for executing the tool’s functionality asynchronously.
- It parses the input arguments, resolves the file path, checks if the file exists, reads the file content, and handles potential errors.
Detailed Review
Argument Parsing
- The method uses
ToolArguments.TryParseto parse the input JSON into a structured object (ReadFileArguments). If parsing fails, it logs a warning and returns a failure result with the parsing error.
Path Resolution
- The tool resolves the provided path using
ToolPath.TryResolve. If the path resolution fails, it returns a failure result with the path error.
File Existence Check
- The tool checks if the file exists at the resolved path. If the file does not exist, it returns a failure result indicating the file was not found.
File Reading
- The tool reads the file content asynchronously using
File.ReadAllTextAsync. - If the file content exceeds
MaxCharacters, the content is truncated, and a message is appended to indicate truncation.
Error Handling
- The tool catches specific exceptions (
UnauthorizedAccessExceptionandIOException) and logs them as errors. It returns a failure result with a descriptive error message.
Strengths
- Asynchronous Operation: The tool uses asynchronous file reading, which is efficient and non-blocking.
- Error Handling: It handles specific exceptions and provides meaningful error messages.
- Truncation Handling: The tool gracefully handles large files by truncating the output and informing the user.
- Logging: Comprehensive logging is implemented for debugging and monitoring purposes.
Potential Improvements
- Configurable MaxCharacters: The
MaxCharactersconstant could be made configurable, allowing users to adjust the truncation limit based on their needs. - Additional File Information: The tool could optionally return additional file information, such as file size, last modified date, or encoding.
- Support for Binary Files: Currently, the tool only supports text files. Adding support for binary files could extend its functionality.
- Path Validation: Additional validation could be added to ensure the resolved path is within an allowed directory, enhancing security.
Conclusion
The ReadFileTool is a well-designed and robust tool for reading file contents. It handles errors gracefully, provides meaningful feedback, and operates efficiently. With minor enhancements, it could become even more versatile and user-friendly.
The review was pretty good so the only change I made was modifying the class to use a primary constructor. In a future version I’m considering loading the commandText from a file so it can be “tweaked” without requiring recompilation.



























