Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
using ClawdDotNet.Core.Api;
|
||||
using ClawdDotNet.Core.Api.Models;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Engine;
|
||||
|
||||
public sealed class ContextCompactor
|
||||
{
|
||||
private readonly OpenRouterClient _client;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private const int ProtectedTailMessages = 6;
|
||||
private const int MaxToolResultChars = 2000;
|
||||
private const string TruncatedMarker = "\n\n[... Ergebnis gekürzt ...]";
|
||||
|
||||
public ContextCompactor(OpenRouterClient client, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_client = client;
|
||||
_logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Engine.ContextCompactor");
|
||||
}
|
||||
|
||||
public static int EstimateTokens(List<ChatMessage> messages)
|
||||
{
|
||||
var totalChars = 0;
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
totalChars += msg.Content?.Length ?? 0;
|
||||
totalChars += msg.Role.Length + 10;
|
||||
|
||||
if (msg.ToolCalls is not null)
|
||||
{
|
||||
foreach (var tc in msg.ToolCalls)
|
||||
totalChars += tc.Function.Name.Length + tc.Function.Arguments.Length + 20;
|
||||
}
|
||||
}
|
||||
return totalChars / 4;
|
||||
}
|
||||
|
||||
public async Task<bool> CompactIfNeededAsync(
|
||||
List<ChatMessage> messages,
|
||||
int lastPromptTokens,
|
||||
LoopGuardConfig guard,
|
||||
string model,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var contextTokens = lastPromptTokens > 0
|
||||
? lastPromptTokens
|
||||
: EstimateTokens(messages);
|
||||
|
||||
var threshold = (int)(guard.MaxContextTokens * guard.CompactionThreshold);
|
||||
|
||||
if (contextTokens < threshold)
|
||||
return false;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Context kompaktierung gestartet: {Tokens} Tokens (Schwelle: {Threshold})",
|
||||
contextTokens, threshold);
|
||||
|
||||
// Stufe 1: Tool-Results kürzen
|
||||
var pruned = PruneToolResults(messages);
|
||||
if (pruned)
|
||||
{
|
||||
var afterPrune = EstimateTokens(messages);
|
||||
_logger.LogInformation("Stufe 1 (Tool-Pruning): {Before} → {After} geschätzte Tokens",
|
||||
contextTokens, afterPrune);
|
||||
|
||||
if (afterPrune < threshold)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stufe 2: Auto-Compaction via LLM
|
||||
await CompactViaLlmAsync(messages, model, ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool PruneToolResults(List<ChatMessage> messages)
|
||||
{
|
||||
var pruned = false;
|
||||
var protectedStart = Math.Max(0, messages.Count - ProtectedTailMessages);
|
||||
|
||||
for (var i = 0; i < protectedStart; i++)
|
||||
{
|
||||
var msg = messages[i];
|
||||
if (msg.Role != "tool" || msg.Content is null)
|
||||
continue;
|
||||
|
||||
if (msg.Content.Length <= MaxToolResultChars)
|
||||
continue;
|
||||
|
||||
msg.Content = msg.Content[..MaxToolResultChars] + TruncatedMarker;
|
||||
pruned = true;
|
||||
}
|
||||
|
||||
return pruned;
|
||||
}
|
||||
|
||||
private async Task CompactViaLlmAsync(
|
||||
List<ChatMessage> messages, string model, CancellationToken ct)
|
||||
{
|
||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||
var conversationParts = messages
|
||||
.Where(m => m.Role != "system")
|
||||
.Select(FormatMessageForSummary);
|
||||
|
||||
var conversationText = string.Join("\n", conversationParts);
|
||||
|
||||
// Auf max 30k Zeichen begrenzen für den Summarization-Call
|
||||
if (conversationText.Length > 30_000)
|
||||
conversationText = conversationText[..30_000] + "\n[... weitere Nachrichten ausgelassen ...]";
|
||||
|
||||
var summaryRequest = new ChatRequest
|
||||
{
|
||||
Model = model,
|
||||
Messages =
|
||||
[
|
||||
ChatMessage.System(
|
||||
"Du bist ein Konversations-Zusammenfasser. Erstelle eine präzise Zusammenfassung " +
|
||||
"der bisherigen Konversation. Behalte alle wichtigen Fakten, Entscheidungen, " +
|
||||
"Ergebnisse von Tool-Aufrufen und den aktuellen Arbeitsstand bei. " +
|
||||
"Schreibe in der dritten Person. Format: Strukturierte Stichpunkte."),
|
||||
ChatMessage.User(
|
||||
"Fasse die folgende Konversation zusammen. Behalte alle wichtigen Details:\n\n" +
|
||||
conversationText)
|
||||
]
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _client.CompleteAsync(summaryRequest, ct);
|
||||
var summary = response.Choices.FirstOrDefault()?.Message?.Content;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
{
|
||||
_logger.LogWarning("Compaction: Keine Zusammenfassung erhalten");
|
||||
return;
|
||||
}
|
||||
|
||||
// Nachrichten ersetzen: System-Prompt + Zusammenfassung + geschützte letzte Nachrichten
|
||||
var tail = messages
|
||||
.Skip(Math.Max(0, messages.Count - ProtectedTailMessages))
|
||||
.ToList();
|
||||
|
||||
messages.Clear();
|
||||
|
||||
if (systemMsg is not null)
|
||||
messages.Add(systemMsg);
|
||||
|
||||
messages.Add(ChatMessage.User(
|
||||
"[Zusammenfassung der bisherigen Konversation]\n\n" + summary));
|
||||
messages.Add(ChatMessage.Assistant(
|
||||
"Verstanden. Ich habe den Kontext der bisherigen Konversation erfasst und arbeite weiter."));
|
||||
|
||||
messages.AddRange(tail);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Stufe 2 (Auto-Compaction): Konversation auf {Count} Nachrichten kompaktiert",
|
||||
messages.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Compaction fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatMessageForSummary(ChatMessage msg)
|
||||
{
|
||||
if (msg.Role == "tool")
|
||||
{
|
||||
var preview = msg.Content?.Length > 200
|
||||
? msg.Content[..200] + "..."
|
||||
: msg.Content;
|
||||
return $"[Tool-Result ({msg.ToolCallId})]: {preview}";
|
||||
}
|
||||
|
||||
if (msg.ToolCalls is { Count: > 0 })
|
||||
{
|
||||
var calls = string.Join(", ",
|
||||
msg.ToolCalls.Select(tc => $"{tc.Function.Name}({tc.Function.Arguments[..Math.Min(100, tc.Function.Arguments.Length)]})"));
|
||||
return $"[Assistant → Tool-Calls]: {calls}";
|
||||
}
|
||||
|
||||
var content = msg.Content?.Length > 500
|
||||
? msg.Content[..500] + "..."
|
||||
: msg.Content;
|
||||
|
||||
return $"[{msg.Role}]: {content}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user