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:
Richard
2026-07-26 18:21:46 +02:00
co-authored by Claude Opus 4.8
commit 2fed388c99
154 changed files with 29736 additions and 0 deletions
@@ -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}";
}
}