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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user