1
0

feat: Add file editing tools with anchor validation and improve context compaction by redacting stale ReadFile results based on deduplication and time-to-live.

This commit is contained in:
2026-03-04 16:22:57 +01:00
parent 31cf7cb4c1
commit d7a94436d1
4 changed files with 101 additions and 46 deletions

View File

@@ -34,25 +34,73 @@ internal sealed partial class ContextCompactor(IChatClient client)
/// Messages before this index are from previous turns and eligible for compaction.
/// </param>
/// <returns>Number of tool results that were compacted.</returns>
public static int CompactStaleToolResults(List<ChatMessage> history, int currentTurnStartIndex)
public static int CompactStaleToolResults(List<ChatMessage> history)
{
int compacted = 0;
int userTurnsSeen = 0;
var filesRead = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < currentTurnStartIndex && i < history.Count; i++)
// Walk backwards: index 0 is system prompt, so we stop at 1.
for (int i = history.Count - 1; i >= 1; i--)
{
var msg = history[i];
foreach (var content in msg.Contents)
if (msg.Role == ChatRole.User)
{
if (content is FunctionResultContent frc &&
frc.Result is string resultStr &&
resultStr.Length >= MinResultLength &&
HashlinePattern.IsMatch(resultStr))
userTurnsSeen++;
}
else if (msg.Role == ChatRole.Assistant)
{
// Find all FunctionCalls in this assistant message to map call IDs to file paths
var calls = msg.Contents.OfType<FunctionCallContent>().ToList();
// If the very next message is a System/Tool role containing FunctionResults
if (i + 1 < history.Count && history[i + 1].Role == ChatRole.Tool)
{
// Count lines and replace with compact summary
int lineCount = resultStr.Count(c => c == '\n');
frc.Result = $"[File content: {lineCount} lines — already consumed. Re-read the file if you need fresh anchors.]";
compacted++;
var resultMsg = history[i + 1];
foreach (var content in resultMsg.Contents)
{
if (content is FunctionResultContent frc &&
frc.Result is string resultStr &&
resultStr.Length >= MinResultLength &&
HashlinePattern.IsMatch(resultStr))
{
// Find the corresponding function call to check its name/arguments
var call = calls.FirstOrDefault(c => c.CallId == frc.CallId);
if (call?.Name == "ReadFile" && call.Arguments != null && call.Arguments.TryGetValue("path", out var pathObj))
{
string filePath = pathObj?.ToString() ?? "";
bool shouldRedact = false;
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.Contains(filePath))
{
shouldRedact = true;
reason = "deduplication — you read this file again later";
}
// Rule 2: TTL. If this was read 2 or more user turns ago, redact it.
else if (userTurnsSeen >= 2)
{
shouldRedact = true;
reason = "expired — read over 2 turns ago";
}
if (shouldRedact)
{
int lineCount = resultStr.Count(c => c == '\n');
frc.Result = $"[File content: {lineCount} lines redacted for {reason}. Re-read the file if you need fresh anchors.]";
compacted++;
}
else
{
// Keep it, but mark that we've seen it so older reads of the same file are redacted.
filesRead.Add(filePath);
}
}
}
}
}
}
}