feat: consolidate file write, move, grep, and delete operations into unified tools and update context compaction heuristics
This commit is contained in:
@@ -71,10 +71,10 @@ internal sealed partial class ContextCompactor(IChatClient client)
|
||||
string reason = "";
|
||||
|
||||
// Rule 1: Deduplication. If we have already seen this file in a newer message (since we are walking backward), redact this one.
|
||||
if (filesRead.TryGetValue(filePath, out int count) && count >= 3)
|
||||
if (filesRead.TryGetValue(filePath, out int count) && count >= 5)
|
||||
{
|
||||
shouldRedact = true;
|
||||
reason = "deduplication — you read this file 3 or more times later";
|
||||
reason = "deduplication — you read this file 5 or more times later";
|
||||
}
|
||||
// Rule 2: TTL. If this was read 2 or more user turns ago, redact it.
|
||||
else if (userTurnsSeen >= 2)
|
||||
|
||||
@@ -12,22 +12,18 @@ internal static class ToolRegistry
|
||||
return new List<AITool>
|
||||
{
|
||||
AIFunctionFactory.Create(FileTools.ReadFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(FileTools.GrepFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(FileTools.Grep, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(FileTools.ListDir, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.ReplaceLines, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.InsertAfter, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.DeleteRange, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.CreateFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.DeleteFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.MoveFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(DirTools.CreateDir, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(DirTools.RenameDir, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(DirTools.DeleteDir, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.Delete, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(FileTools.FindFiles, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(FileTools.GrepRecursive, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(FileTools.GetFileInfo, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.AppendToFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.WriteToFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(CommandTool.ExecuteCommand, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(EditTools.MoveFile, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(DirTools.RenameDir, serializerOptions: jsonOptions),
|
||||
AIFunctionFactory.Create(DirTools.CreateDir, serializerOptions: jsonOptions),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,27 +39,6 @@ internal static class DirTools
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Delete a directory and all its contents permanently.")]
|
||||
public static string DeleteDir(
|
||||
[Description("Path to the directory to delete.")] string path,
|
||||
[Description("If true, delete recursively. Defaults to true.")] bool recursive = true)
|
||||
{
|
||||
path = ResolvePath(path);
|
||||
Log($"Deleting directory: {path}");
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
return $"ERROR: Directory not found: {path}";
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, recursive);
|
||||
return $"OK: Directory deleted: '{path}'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR deleting directory '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
[Description("Create a new directory. Creates parent directories if they don't exist. Returns OK on success, or an error message if the directory already exists or creation fails.")]
|
||||
public static string CreateDir(
|
||||
[Description("Path to the directory to create.")] string path)
|
||||
|
||||
@@ -90,44 +90,6 @@ internal static partial class EditTools
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Insert lines after the specified line:hash anchor.")]
|
||||
public static string InsertAfter(
|
||||
[Description("Path to the file.")] string path,
|
||||
[Description("line:hash anchor to insert after (e.g. '3:0e').")] string anchor,
|
||||
[Description("Raw source code to insert. Do NOT include 'lineNumber:hash|' prefixes.")] string[] newLines)
|
||||
{
|
||||
newLines = SanitizeNewLines(newLines);
|
||||
path = FileTools.ResolvePath(path);
|
||||
Log($"INSERT_AFTER: {path}");
|
||||
Log($" Anchor: {anchor}");
|
||||
Log($" Inserting {newLines.Length} lines after line {anchor.Split(':')[0]}");
|
||||
|
||||
|
||||
if (!File.Exists(path))
|
||||
return $"ERROR: File not found: {path}";
|
||||
|
||||
try
|
||||
{
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
|
||||
if (!HashlineValidator.TryResolve(anchor, lines, out int idx, out string error))
|
||||
return $"ERROR: {error}";
|
||||
|
||||
|
||||
|
||||
var result = new List<string>(lines.Length + newLines.Length);
|
||||
result.AddRange(lines[..(idx + 1)]);
|
||||
result.AddRange(newLines);
|
||||
result.AddRange(lines[(idx + 1)..]);
|
||||
|
||||
File.WriteAllLines(path, result);
|
||||
return $"OK fp:{HashlineEncoder.FileFingerprint([.. result])}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR modifying '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Delete a range of lines.")]
|
||||
public static string DeleteRange(
|
||||
@@ -162,45 +124,34 @@ internal static partial class EditTools
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Create a new file (parents auto-created). Max initial lines: 200. Alternatively, append lines later.")]
|
||||
public static string CreateFile(
|
||||
[Description("Path to the new file to create.")] string path,
|
||||
[Description("Optional initial raw source code. Do NOT include 'lineNumber:hash|' prefixes.")] string[]? initialLines = null)
|
||||
|
||||
|
||||
[Description("Delete a file or directory. Use mode='file' to delete a file, mode='dir' to delete a directory.")]
|
||||
public static string Delete(
|
||||
[Description("Path to the file or directory to delete.")] string path,
|
||||
[Description("Type of deletion: 'file' or 'dir'. Defaults to 'file'.")] string mode = "file")
|
||||
{
|
||||
path = FileTools.ResolvePath(path);
|
||||
Log($"Creating file: {path}");
|
||||
string targetType = mode.ToLower() == "dir" ? "directory" : "file";
|
||||
Log($"Deleting {targetType}: {path}");
|
||||
|
||||
if (File.Exists(path))
|
||||
return $"ERROR: File already exists: {path}";
|
||||
if (mode.ToLower() == "dir")
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
return $"ERROR: Directory not found: {path}";
|
||||
|
||||
try
|
||||
{
|
||||
if (initialLines is not null)
|
||||
initialLines = SanitizeNewLines(initialLines);
|
||||
string? dir = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
if (initialLines is not null && initialLines.Length > 0)
|
||||
File.WriteAllLines(path, initialLines);
|
||||
else
|
||||
File.WriteAllText(path, "");
|
||||
|
||||
return $"OK fp:{HashlineEncoder.FileFingerprint(initialLines ?? [])}";
|
||||
Directory.Delete(path, true);
|
||||
return $"OK: Directory deleted: '{path}'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR creating '{path}': {ex.Message}";
|
||||
return $"ERROR deleting directory '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Delete a file permanently.")]
|
||||
public static string DeleteFile(
|
||||
[Description("Path to the file to delete.")] string path)
|
||||
else
|
||||
{
|
||||
path = FileTools.ResolvePath(path);
|
||||
Log($"Deleting file: {path}");
|
||||
|
||||
if (!File.Exists(path))
|
||||
return $"ERROR: File not found: {path}";
|
||||
|
||||
@@ -214,6 +165,7 @@ internal static partial class EditTools
|
||||
return $"ERROR deleting '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Move or copy a file to a new location.")]
|
||||
public static string MoveFile(
|
||||
@@ -250,15 +202,19 @@ internal static partial class EditTools
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Append lines to EOF (auto-creating the file if missing).")]
|
||||
public static string AppendToFile(
|
||||
[Description("Path to the file to append to.")] string path,
|
||||
[Description("Raw source code to append. Do NOT include 'lineNumber:hash|' prefixes.")] string[] lines)
|
||||
|
||||
[Description("Write to a file with different modes: create, append, or insert.")]
|
||||
public static string WriteToFile(
|
||||
[Description("Path to the file.")] string path,
|
||||
[Description("Content to write.")] string[] content,
|
||||
[Description("Write mode: 'create' (error if exists), 'append' (creates if missing), 'insert' (requires anchor)")] string mode = "create",
|
||||
[Description("line:hash anchor to insert after (required for mode='insert', e.g. '3:0e').")] string? anchor = null)
|
||||
{
|
||||
lines = SanitizeNewLines(lines);
|
||||
content = SanitizeNewLines(content);
|
||||
path = FileTools.ResolvePath(path);
|
||||
Log($"Appending to file: {path}");
|
||||
Log($" Appending {lines.Length} lines");
|
||||
Log($"WRITE_TO_FILE: {path}");
|
||||
Log($" Mode: {mode}");
|
||||
Log($" Writing {content.Length} lines");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -266,6 +222,20 @@ internal static partial class EditTools
|
||||
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
switch (mode.ToLower())
|
||||
{
|
||||
case "create":
|
||||
if (File.Exists(path))
|
||||
return $"ERROR: File already exists: {path}";
|
||||
|
||||
if (content.Length > 0)
|
||||
File.WriteAllLines(path, content);
|
||||
else
|
||||
File.WriteAllText(path, "");
|
||||
|
||||
return $"OK fp:{HashlineEncoder.FileFingerprint(content)}";
|
||||
|
||||
case "append":
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
File.WriteAllText(path, "");
|
||||
@@ -274,18 +244,42 @@ internal static partial class EditTools
|
||||
|
||||
using (var writer = new System.IO.StreamWriter(path, true))
|
||||
{
|
||||
foreach (var line in lines)
|
||||
foreach (var line in content)
|
||||
{
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
string[] allLines = File.ReadAllLines(path);
|
||||
return $"OK fp:{HashlineEncoder.FileFingerprint([.. allLines])}";
|
||||
string[] appendedLines = File.ReadAllLines(path);
|
||||
return $"OK fp:{HashlineEncoder.FileFingerprint([.. appendedLines])}";
|
||||
|
||||
case "insert":
|
||||
if (!File.Exists(path))
|
||||
return $"ERROR: File not found: {path}";
|
||||
|
||||
if (string.IsNullOrEmpty(anchor))
|
||||
return "ERROR: mode='insert' requires an anchor parameter";
|
||||
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
|
||||
if (!HashlineValidator.TryResolve(anchor, lines, out int idx, out string error))
|
||||
return $"ERROR: {error}";
|
||||
|
||||
var result = new List<string>(lines.Length + content.Length);
|
||||
result.AddRange(lines[..(idx + 1)]);
|
||||
result.AddRange(content);
|
||||
result.AddRange(lines[(idx + 1)..]);
|
||||
|
||||
File.WriteAllLines(path, result);
|
||||
return $"OK fp:{HashlineEncoder.FileFingerprint([.. result])}";
|
||||
|
||||
default:
|
||||
return $"ERROR: Unknown mode '{mode}'. Valid modes: create, append, insert";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR appending to '{path}': {ex.Message}";
|
||||
return $"ERROR writing to '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,54 +56,6 @@ internal static class FileTools
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Search a file for a regex pattern. Returns matches with line:hash| anchors.")]
|
||||
public static string GrepFile(
|
||||
[Description("Path to the file to search.")] string path,
|
||||
[Description("Regex pattern.")] string pattern)
|
||||
{
|
||||
path = ResolvePath(path);
|
||||
Log($"Searching file: {path}");
|
||||
|
||||
if (!File.Exists(path))
|
||||
return $"ERROR: File not found: {path}";
|
||||
|
||||
Regex regex;
|
||||
try
|
||||
{
|
||||
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR: Invalid regex pattern '{pattern}': {ex.Message}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
int matchCount = 0;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
if (regex.IsMatch(lines[i]))
|
||||
{
|
||||
int lineNumber = i + 1;
|
||||
string hash = HashlineEncoder.ComputeHash(lines[i].AsSpan(), lineNumber);
|
||||
sb.Append(lineNumber).Append(':').Append(hash).Append('|').AppendLine(lines[i]);
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchCount == 0)
|
||||
return $"(no matches for '{pattern}' in {path})";
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR searching '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[Description("List files and subdirectories.")]
|
||||
public static string ListDir(
|
||||
@@ -174,13 +126,63 @@ internal static class FileTools
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Recursive regex search across all files. Returns matches with file:line:hash| format.")]
|
||||
public static string GrepRecursive(
|
||||
[Description("Directory to search.")] string path,
|
||||
|
||||
[Description("Consolidated grep operation for single file or recursive directory search.")]
|
||||
public static string Grep(
|
||||
[Description("Directory to search (for recursive mode) or file path (for file mode).")] string path,
|
||||
[Description("Regex pattern.")] string pattern,
|
||||
[Description("Optional glob to filter files (e.g. '*.cs').")] string? filePattern = null)
|
||||
[Description("Mode: 'file' for single file, 'recursive' for directory search.")] string mode = "recursive",
|
||||
[Description("Optional glob to filter files in recursive mode (e.g. '*.cs').")] string? filePattern = null)
|
||||
{
|
||||
path = ResolvePath(path);
|
||||
mode = mode.ToLowerInvariant();
|
||||
|
||||
if (mode == "file")
|
||||
{
|
||||
Log($"Searching file: {path}");
|
||||
|
||||
if (!File.Exists(path))
|
||||
return $"ERROR: File not found: {path}";
|
||||
|
||||
Regex regex;
|
||||
try
|
||||
{
|
||||
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR: Invalid regex pattern '{pattern}': {ex.Message}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
int matchCount = 0;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
if (regex.IsMatch(lines[i]))
|
||||
{
|
||||
int lineNumber = i + 1;
|
||||
string hash = HashlineEncoder.ComputeHash(lines[i].AsSpan(), lineNumber);
|
||||
sb.Append(lineNumber).Append(':').Append(hash).Append('|').AppendLine(lines[i]);
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchCount == 0)
|
||||
return $"(no matches for '{pattern}' in {path})";
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"ERROR searching '{path}': {ex.Message}";
|
||||
}
|
||||
}
|
||||
else if (mode == "recursive")
|
||||
{
|
||||
Log($"Recursive grep: {pattern} in {path}" + (filePattern != null ? $" (files: {filePattern})" : ""));
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
@@ -245,6 +247,11 @@ internal static class FileTools
|
||||
return $"ERROR in recursive grep: {ex.Message}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"ERROR: Invalid mode '{mode}'. Use 'file' or 'recursive'.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely enumerates files recursively, skipping inaccessible and non-useful directories.
|
||||
|
||||
@@ -49,7 +49,7 @@ public static string MoveFile(
|
||||
- Both create parent directories
|
||||
- Similar error handling patterns
|
||||
|
||||
## 3. Grep Operations
|
||||
## 4. Grep Operations ✅ DONE
|
||||
|
||||
**Current tools:** `GrepFile`, `GrepRecursive`
|
||||
|
||||
@@ -59,13 +59,13 @@ public static string MoveFile(
|
||||
public static string Grep(
|
||||
string path,
|
||||
string pattern,
|
||||
bool recursive = false,
|
||||
string mode = "recursive",
|
||||
string? filePattern = null)
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- `recursive=false` - Searches single file (current GrepFile)
|
||||
- `recursive=true` - Searches directory recursively (current GrepRecursive)
|
||||
- `mode="file"` - Searches single file (current GrepFile)
|
||||
- `mode="recursive"` - Searches directory recursively (current GrepRecursive)
|
||||
- `filePattern` - Optional glob to filter files when recursive
|
||||
|
||||
**Benefits:**
|
||||
@@ -73,7 +73,7 @@ public static string Grep(
|
||||
- Reduces 2 tools to 1
|
||||
- Cleaner API for LLM
|
||||
|
||||
## 4. Delete Operations
|
||||
## 5. Delete Operations ✅ DONE
|
||||
|
||||
**Current tools:** `DeleteFile`, `DeleteDir`
|
||||
|
||||
@@ -82,31 +82,24 @@ public static string Grep(
|
||||
```csharp
|
||||
public static string Delete(
|
||||
string path,
|
||||
bool recursive = true)
|
||||
string mode = "file")
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Auto-detects if path is file or directory
|
||||
- `recursive=true` - Delete directory and all contents
|
||||
- `recursive=false` - Only matters for directories (error if not empty)
|
||||
- `mode="file"` - Deletes a file
|
||||
- `mode="dir"` - Deletes a directory (recursive)
|
||||
|
||||
**Benefits:**
|
||||
- Auto-detects file vs directory
|
||||
- Unified interface for all deletion
|
||||
- Similar error handling patterns
|
||||
- Reduces 2 tools to 1
|
||||
|
||||
## Summary
|
||||
These consolidations reduced the tool count from 17 to 13 tools (4 completed), making the API simpler and easier for the LLM to use effectively.
|
||||
|
||||
These consolidations would reduce the tool count from 17 to 13 tools, making the API simpler and easier for the LLM to use effectively.
|
||||
**Completed merges**:
|
||||
1. ✅ File Move Operations (2 → 1) - **DONE**
|
||||
2. ✅ File Write Operations (3 → 1) - **DONE**
|
||||
3. ✅ Delete Operations (2 → 1) - **DONE**
|
||||
4. ✅ Grep Operations (2 → 1) - **DONE**
|
||||
|
||||
**High priority merges:**
|
||||
1. ✅ File Write Operations (3 → 1)
|
||||
2. ✅ File Move Operations (2 → 1)
|
||||
3. ✅ Grep Operations (2 → 1)
|
||||
4. ✅ Delete Operations (2 → 1)
|
||||
|
||||
**Kept separate:**
|
||||
- `ReadFile` - distinct read-only operation
|
||||
- `ListDir`, `FindFiles`, `GetFileInfo` - different purposes
|
||||
- `CreateDir` - simple enough to keep standalone
|
||||
- `ReplaceLines`, `InsertAfter`, `DeleteRange` - too complex to merge without confusing LLM
|
||||
**All high priority merges completed!**
|
||||
|
||||
Reference in New Issue
Block a user