B1 — Die Compaction behielt blind die letzten 6 Nachrichten. Fiel diese Grenze mitten in eine Tool-Sequenz, entstand eine tool-Antwort ohne zugehoerigen assistant-tool_call; die API lehnt das mit HTTP 400 ab. FindSafeTailStart verschiebt die Grenze jetzt rueckwaerts auf eine Blockgrenze. B14 — Bei Konversationen mit hoechstens 6 Nachrichten enthielt der Tail auch die system-Nachricht, die anschliessend ein zweites Mal angehaengt wurde. Ergebnis war ein doppelter System-Prompt und eine duplizierte Konversation — die Compaction vergroesserte den Kontext, statt ihn zu verkleinern. Der Tail beginnt nun grundsaetzlich hinter dem System-Prompt; liegt davor nichts Nennenswertes, wird die Kompaktierung uebersprungen. Gefunden durch den Property-Test. Nebeneffekt: Zusammengefasst wird nur noch der Teil, der tatsaechlich wegfaellt. Der Tail bleibt woertlich erhalten und musste bisher doppelt bezahlt werden. B3 — maxTokens zaehlte kumulativ ueber alle Schritte, wurde aber wie eine Kontextgrenze konfiguriert. Da jeder Schritt den vollen Kontext erneut sendet, brach ein Chat mit 20k Kontext nach vier Schritten ab. Aufgeteilt in maxCumulativeTokens (Kostenbudget, Default 500k) und maxContextTokens (Kontextgroesse). Alte Konfigurationen werden beim Laden migriert, die Fehlermeldungen unterscheiden jetzt Schritt- und Kostenlimit. Alle 31 Tests gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
434 lines
15 KiB
C#
434 lines
15 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Models;
|
|
|
|
namespace ClawdDotNet.Services;
|
|
|
|
/// <summary>
|
|
/// Verwaltet die gesamte Verzeichnisstruktur für Instanzen und Agenten.
|
|
///
|
|
/// Layout:
|
|
/// {InstancesDir}/
|
|
/// ├── Instance-{Name}/
|
|
/// │ ├── InstanceSettings.json
|
|
/// │ ├── TokenUsage.json
|
|
/// │ └── Agents/
|
|
/// │ ├── AgentList.json
|
|
/// │ └── Agent-{Name}/
|
|
/// │ ├── AgentSettings.json
|
|
/// │ ├── Soul.md
|
|
/// │ ├── Identity.md
|
|
/// │ ├── Logs/
|
|
/// │ └── Workspace/
|
|
/// </summary>
|
|
public sealed class InstanceDirectoryManager
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
private readonly string _instancesDir;
|
|
private readonly Lock _tokenUsageLock = new();
|
|
|
|
public InstanceDirectoryManager(string instancesDirectory)
|
|
{
|
|
_instancesDir = Path.GetFullPath(instancesDirectory);
|
|
Directory.CreateDirectory(_instancesDir);
|
|
}
|
|
|
|
public string InstancesDirectory => _instancesDir;
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// INSTANZ-OPERATIONEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public static string BuildInstanceFolderName(string instanceName)
|
|
=> $"Instance-{SanitizeName(instanceName)}";
|
|
|
|
public string GetInstancePath(string instanceName)
|
|
=> Path.Combine(_instancesDir, BuildInstanceFolderName(instanceName));
|
|
|
|
public List<InstanceInfo> ListInstances()
|
|
{
|
|
var result = new List<InstanceInfo>();
|
|
|
|
if (!Directory.Exists(_instancesDir))
|
|
return result;
|
|
|
|
foreach (var dir in Directory.GetDirectories(_instancesDir, "Instance-*"))
|
|
{
|
|
var folderName = Path.GetFileName(dir);
|
|
var settingsPath = Path.Combine(dir, "InstanceSettings.json");
|
|
|
|
var info = new InstanceInfo
|
|
{
|
|
FolderName = folderName,
|
|
FolderPath = dir,
|
|
InstanceName = folderName.Replace("Instance-", "")
|
|
};
|
|
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
var config = JsonSerializer.Deserialize<InstanceConfig>(json, JsonOpts);
|
|
if (config is not null)
|
|
{
|
|
info.InstanceName = config.InstanceName;
|
|
info.ApiKeyStatus = string.IsNullOrWhiteSpace(config.OpenRouterApiKey)
|
|
? "Fehlt" : "Konfiguriert";
|
|
}
|
|
}
|
|
catch { /* defekte Config → Standardwerte */ }
|
|
}
|
|
|
|
// Agenten zählen
|
|
var agentsDir = Path.Combine(dir, "Agents");
|
|
if (Directory.Exists(agentsDir))
|
|
info.AgentCount = Directory.GetDirectories(agentsDir, "Agent-*").Length;
|
|
|
|
result.Add(info);
|
|
}
|
|
|
|
return result.OrderBy(i => i.InstanceName).ToList();
|
|
}
|
|
|
|
public string CreateInstance(string instanceName)
|
|
{
|
|
var instanceDir = GetInstancePath(instanceName);
|
|
|
|
if (Directory.Exists(instanceDir))
|
|
throw new InvalidOperationException($"Instanz '{instanceName}' existiert bereits.");
|
|
|
|
// Hauptverzeichnis
|
|
Directory.CreateDirectory(instanceDir);
|
|
|
|
// Agents-Unterverzeichnis
|
|
var agentsDir = Path.Combine(instanceDir, "Agents");
|
|
Directory.CreateDirectory(agentsDir);
|
|
|
|
// SharedWorkspace-Verzeichnis
|
|
Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
|
|
// InstanceSettings.json
|
|
var config = new InstanceConfig
|
|
{
|
|
InstanceId = Guid.NewGuid().ToString("N")[..8],
|
|
InstanceName = instanceName,
|
|
LogDirectory = "./Logs",
|
|
WorkingDirectory = instanceDir
|
|
};
|
|
SaveJson(Path.Combine(instanceDir, "InstanceSettings.json"), config);
|
|
|
|
// TokenUsage.json (leer)
|
|
var tokenUsage = new TokenUsageFile
|
|
{
|
|
InstanceId = config.InstanceId,
|
|
InstanceName = instanceName
|
|
};
|
|
SaveJson(Path.Combine(instanceDir, "TokenUsage.json"), tokenUsage);
|
|
|
|
// AgentList.json (leer)
|
|
SaveJson(Path.Combine(agentsDir, "AgentList.json"), new AgentListFile());
|
|
|
|
return instanceDir;
|
|
}
|
|
|
|
public InstanceConfig LoadInstanceConfig(string instanceDir)
|
|
{
|
|
var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json");
|
|
|
|
if (!File.Exists(settingsPath))
|
|
throw new FileNotFoundException($"InstanceSettings.json nicht gefunden in: {instanceDir}");
|
|
|
|
var json = File.ReadAllText(settingsPath);
|
|
var config = JsonSerializer.Deserialize<InstanceConfig>(json, JsonOpts)
|
|
?? throw new InvalidOperationException("InstanceSettings.json ist leer oder ungültig.");
|
|
|
|
// Agenten aus Verzeichnisstruktur laden
|
|
config.Agents.Clear();
|
|
var agentsDir = Path.Combine(instanceDir, "Agents");
|
|
|
|
// Sicherstellen, dass SharedWorkspace existiert (Migration)
|
|
Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
|
|
// AgentList.json für Descriptions laden
|
|
var agentListPath = Path.Combine(agentsDir, "AgentList.json");
|
|
var agentList = File.Exists(agentListPath)
|
|
? LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile()
|
|
: new AgentListFile();
|
|
|
|
if (Directory.Exists(agentsDir))
|
|
{
|
|
foreach (var agentDir in Directory.GetDirectories(agentsDir, "Agent-*").OrderBy(d => d))
|
|
{
|
|
var agent = LoadAgentConfig(agentDir);
|
|
|
|
var folderName = Path.GetFileName(agentDir);
|
|
var listEntry = agentList.Agents.FirstOrDefault(a => a.FolderName == folderName);
|
|
if (listEntry is not null && !string.IsNullOrWhiteSpace(listEntry.Description))
|
|
agent.Description = listEntry.Description;
|
|
|
|
config.Agents.Add(agent);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public void SaveInstanceConfig(string instanceDir, InstanceConfig config)
|
|
{
|
|
var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json");
|
|
SaveJson(settingsPath, config);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// AGENTEN-OPERATIONEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public static string BuildAgentFolderName(string agentName)
|
|
=> $"Agent-{SanitizeName(agentName)}";
|
|
|
|
public string GetAgentPath(string instanceDir, string agentName)
|
|
=> Path.Combine(instanceDir, "Agents", BuildAgentFolderName(agentName));
|
|
|
|
public string CreateAgent(string instanceDir, string agentName, string description = "")
|
|
{
|
|
var agentDir = GetAgentPath(instanceDir, agentName);
|
|
|
|
if (Directory.Exists(agentDir))
|
|
throw new InvalidOperationException($"Agent '{agentName}' existiert bereits in dieser Instanz.");
|
|
|
|
// Verzeichnisse anlegen
|
|
Directory.CreateDirectory(agentDir);
|
|
Directory.CreateDirectory(Path.Combine(agentDir, "Logs"));
|
|
Directory.CreateDirectory(Path.Combine(agentDir, "Workspace"));
|
|
|
|
// AgentSettings.json
|
|
var agentConfig = new AgentConfig
|
|
{
|
|
AgentId = SanitizeName(agentName).ToLowerInvariant(),
|
|
DisplayName = agentName,
|
|
Model = "anthropic/claude-sonnet-4-5"
|
|
};
|
|
SaveAgentSettings(agentDir, agentConfig);
|
|
|
|
// Identity.md
|
|
File.WriteAllText(Path.Combine(agentDir, "Identity.md"),
|
|
$"""
|
|
# Identity: {agentName}
|
|
|
|
Du bist **{agentName}**, ein spezialisierter KI-Agent im ClawdDotNet-System.
|
|
|
|
## Rolle
|
|
[Beschreibe hier die Rolle und Verantwortlichkeiten des Agenten]
|
|
|
|
## Expertise
|
|
[Beschreibe hier die Fachgebiete und Fähigkeiten]
|
|
|
|
## Kontext
|
|
[Beschreibe hier den Arbeitskontext und die Teamzugehörigkeit]
|
|
""");
|
|
|
|
// Soul.md
|
|
File.WriteAllText(Path.Combine(agentDir, "Soul.md"),
|
|
$"""
|
|
# Soul: {agentName}
|
|
|
|
## Persönlichkeit
|
|
- Gründlich und zuverlässig
|
|
- Klar und präzise in der Kommunikation
|
|
- Proaktiv bei der Problemerkennung
|
|
|
|
## Arbeitsweise
|
|
- Analysiere Aufgaben sorgfältig bevor du handelst
|
|
- Dokumentiere deine Entscheidungen und Ergebnisse
|
|
- Nutze die dir zugewiesenen Tools effizient
|
|
|
|
## Werte
|
|
- Genauigkeit vor Geschwindigkeit
|
|
- Transparenz in der Entscheidungsfindung
|
|
- Sicherheit und Datenschutz haben Priorität
|
|
""");
|
|
|
|
// AgentList.json aktualisieren
|
|
UpdateAgentList(instanceDir, agentName, description, BuildAgentFolderName(agentName));
|
|
|
|
return agentDir;
|
|
}
|
|
|
|
public AgentConfig LoadAgentConfig(string agentDir)
|
|
{
|
|
// AgentSettings.json laden
|
|
var settingsPath = Path.Combine(agentDir, "AgentSettings.json");
|
|
AgentConfig config;
|
|
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
config = JsonSerializer.Deserialize<AgentConfig>(json, JsonOpts) ?? new AgentConfig();
|
|
ConfigLoader.Migrate(config);
|
|
}
|
|
else
|
|
{
|
|
config = new AgentConfig
|
|
{
|
|
AgentId = Path.GetFileName(agentDir).Replace("Agent-", "").ToLowerInvariant(),
|
|
DisplayName = Path.GetFileName(agentDir).Replace("Agent-", "")
|
|
};
|
|
}
|
|
|
|
// Identity.md laden
|
|
var identityPath = Path.Combine(agentDir, "Identity.md");
|
|
if (File.Exists(identityPath))
|
|
config.Identity = File.ReadAllText(identityPath);
|
|
|
|
// Soul.md laden
|
|
var soulPath = Path.Combine(agentDir, "Soul.md");
|
|
if (File.Exists(soulPath))
|
|
config.Soul = File.ReadAllText(soulPath);
|
|
|
|
// Agent-Verzeichnis merken (stabil auch bei DisplayName-Änderungen)
|
|
config.AgentDir = Path.GetFullPath(agentDir);
|
|
|
|
// Workspace-Pfad setzen
|
|
config.WorkspacePath = Path.GetFullPath(Path.Combine(agentDir, "Workspace"));
|
|
|
|
// SharedWorkspace-Pfad setzen
|
|
var agentsDir = Path.GetDirectoryName(agentDir); // Dies ist der /Agents Ordner
|
|
if (agentsDir != null)
|
|
{
|
|
config.SharedWorkspacePath = Path.GetFullPath(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public void SaveAgentSettings(string agentDir, AgentConfig config)
|
|
{
|
|
var settingsPath = Path.Combine(agentDir, "AgentSettings.json");
|
|
SaveJson(settingsPath, config);
|
|
}
|
|
|
|
public void SaveAgentIdentity(string agentDir, string identity)
|
|
{
|
|
File.WriteAllText(Path.Combine(agentDir, "Identity.md"), identity);
|
|
}
|
|
|
|
public void SaveAgentSoul(string agentDir, string soul)
|
|
{
|
|
File.WriteAllText(Path.Combine(agentDir, "Soul.md"), soul);
|
|
}
|
|
|
|
public void RemoveAgent(string instanceDir, string agentFolderName)
|
|
{
|
|
var agentDir = Path.Combine(instanceDir, "Agents", agentFolderName);
|
|
if (Directory.Exists(agentDir))
|
|
Directory.Delete(agentDir, recursive: true);
|
|
|
|
// AgentList.json aktualisieren
|
|
var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json");
|
|
if (File.Exists(agentListPath))
|
|
{
|
|
var list = LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile();
|
|
list.Agents.RemoveAll(a => a.FolderName == agentFolderName);
|
|
SaveJson(agentListPath, list);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// TOKEN USAGE
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public void AppendTokenUsage(string instanceDir, TokenUsageRecord record)
|
|
{
|
|
lock (_tokenUsageLock)
|
|
{
|
|
var path = Path.Combine(instanceDir, "TokenUsage.json");
|
|
TokenUsageFile file;
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
file = LoadJson<TokenUsageFile>(path) ?? new TokenUsageFile();
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Korrupte Datei: Backup erstellen, neu anfangen
|
|
var backupPath = path + $".corrupt_{DateTime.Now:yyyyMMdd_HHmmss}";
|
|
try { File.Copy(path, backupPath, overwrite: true); } catch { /* best effort */ }
|
|
file = new TokenUsageFile();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
file = new TokenUsageFile();
|
|
}
|
|
|
|
file.Records.Add(record);
|
|
SaveJson(path, file);
|
|
}
|
|
}
|
|
|
|
public TokenUsageFile LoadTokenUsage(string instanceDir)
|
|
{
|
|
var path = Path.Combine(instanceDir, "TokenUsage.json");
|
|
return File.Exists(path)
|
|
? LoadJson<TokenUsageFile>(path) ?? new TokenUsageFile()
|
|
: new TokenUsageFile();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// HILFSMETHODEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
private void UpdateAgentList(string instanceDir, string agentName, string description, string folderName)
|
|
{
|
|
var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json");
|
|
var list = File.Exists(agentListPath)
|
|
? LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile()
|
|
: new AgentListFile();
|
|
|
|
// Duplikat-Check
|
|
if (list.Agents.All(a => a.FolderName != folderName))
|
|
{
|
|
list.Agents.Add(new AgentListItem
|
|
{
|
|
Name = agentName,
|
|
Description = description,
|
|
FolderName = folderName
|
|
});
|
|
}
|
|
|
|
SaveJson(agentListPath, list);
|
|
}
|
|
|
|
private static void SaveJson<T>(string path, T obj)
|
|
{
|
|
var dir = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(dir))
|
|
Directory.CreateDirectory(dir);
|
|
|
|
var json = JsonSerializer.Serialize(obj, JsonOpts);
|
|
File.WriteAllText(path, json);
|
|
}
|
|
|
|
private static T? LoadJson<T>(string path)
|
|
{
|
|
var json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<T>(json, JsonOpts);
|
|
}
|
|
|
|
private static string SanitizeName(string name)
|
|
{
|
|
var sanitized = name.Trim();
|
|
foreach (var c in Path.GetInvalidFileNameChars())
|
|
sanitized = sanitized.Replace(c, '_');
|
|
return sanitized.Replace(' ', '_');
|
|
}
|
|
}
|