using System.Text; using System.Text.Json; using ClawdDotNet.Core.Tools; using Microsoft.Extensions.Logging; namespace ClawdDotNet.Tools.AgentEditor; /// /// Ermöglicht einem Agent, die Identity- und Soul-Dateien anderer Agenten in der Instanz /// zu lesen, zu bearbeiten und neue Agenten anzulegen. /// /// Sicherheitskonzept: /// - Der Agent kann nur Soul.md und Identity.md lesen/schreiben (keine AgentSettings, keine Chat-Daten) /// - Neue Agenten bekommen nur die Grundstruktur — Tool-Zuweisung etc. bleibt dem Benutzer vorbehalten /// - Alle Änderungen werden mit Backup gesichert (.bak-Datei) /// public sealed class AgentEditorTool : IAgentTool { public string Name => "AgentEditor"; public string Description => """ Verwaltet die Agenten-Persönlichkeiten in der Instanz. Ermöglicht das Lesen und Bearbeiten von Identity (Rolle, Expertise) und Soul (Persönlichkeit, Arbeitsweise, Werte) aller Agenten. Aktionen: - list_agents: Übersicht aller Agenten mit Namen, Beschreibung und verfügbaren Dateien - read_identity: Identity.md eines Agenten lesen (WER ist der Agent) - read_soul: Soul.md eines Agenten lesen (WIE denkt/arbeitet der Agent) - update_identity: Identity.md eines Agenten aktualisieren (erstellt Backup) - update_soul: Soul.md eines Agenten aktualisieren (erstellt Backup) - create_agent: Einen neuen Agenten in der Instanz erstellen (Grundstruktur mit Identity und Soul) Wichtig: Verschaffe dir IMMER zuerst mit list_agents eine Übersicht, bevor du Änderungen vornimmst. Lies auch die bestehende Identity/Soul bevor du sie überschreibst, um nichts Wichtiges zu verlieren. """; public JsonElement InputSchema { get; } = JsonDocument.Parse(""" { "type": "object", "properties": { "action": { "type": "string", "enum": ["list_agents", "read_identity", "read_soul", "update_identity", "update_soul", "create_agent"], "description": "Die auszuführende Aktion" }, "agentId": { "type": "string", "description": "Die ID des Ziel-Agenten (z.B. 'senior_developer'). Nicht erforderlich für list_agents." }, "content": { "type": "string", "description": "Der neue Inhalt für Identity oder Soul (nur für update_identity/update_soul)" }, "agentName": { "type": "string", "description": "Der Anzeigename für einen neuen Agenten (nur für create_agent, z.B. 'News Scanner')" }, "agentDescription": { "type": "string", "description": "Kurzbeschreibung der Rolle des neuen Agenten (nur für create_agent)" }, "identity": { "type": "string", "description": "Der Identity.md-Inhalt für einen neuen Agenten (nur für create_agent)" }, "soul": { "type": "string", "description": "Der Soul.md-Inhalt für einen neuen Agenten (nur für create_agent)" } }, "required": ["action"] } """).RootElement.Clone(); public async Task ExecuteAsync( JsonElement input, AgentToolContext context, CancellationToken ct) { var action = input.GetProperty("action").GetString() ?? throw new ArgumentException("'action' is required"); try { var agentsDir = ResolveAgentsDirectory(context); if (agentsDir is null) return ToolResult.Fail("Konnte das Agents-Verzeichnis nicht ermitteln. SharedWorkspacePath ist nicht konfiguriert."); return action switch { "list_agents" => HandleListAgents(agentsDir, context), "read_identity" => await HandleReadFileAsync(input, agentsDir, "Identity.md", context, ct), "read_soul" => await HandleReadFileAsync(input, agentsDir, "Soul.md", context, ct), "update_identity" => await HandleUpdateFileAsync(input, agentsDir, "Identity.md", context, ct), "update_soul" => await HandleUpdateFileAsync(input, agentsDir, "Soul.md", context, ct), "create_agent" => await HandleCreateAgentAsync(input, agentsDir, context, ct), _ => ToolResult.Fail($"Unbekannte Aktion: {action}") }; } catch (Exception ex) { context.Logger.LogError(ex, "Fehler bei AgentEditor Aktion {Action}", action); return ToolResult.Fail($"Fehler: {ex.Message}"); } } /// /// #205 / #579: Selbstbearbeitung wird abgelehnt, bevor ein Vorschlag in die /// Freigabeliste kommt. Aenderungen an fremden Agenten laufen ueber das Staging /// (StagingPolicy: update_identity, update_soul, create_agent = approve). /// public string? Preflight(JsonElement input, AgentToolContext context) { if (input.ValueKind != JsonValueKind.Object || !input.TryGetProperty("action", out var a) || a.ValueKind != JsonValueKind.String) return null; return SelbstbearbeitungVerweigert(a.GetString(), input, context); } internal const string SelbstbearbeitungText = "Änderungen an der eigenen Identity oder Soul nimmt der Mensch vor. " + "Formuliere deinen Änderungswunsch stattdessen als Vorschlag in deiner Antwort."; /// /// Die eigene Soul ist die Datei, die Verhalten, Grenzen und Vorsicht eines Agenten /// festlegt. Ein Agent, der an einer Einschraenkung scheitert, koennte sie sich sonst /// selbst wegschreiben. Im Pruefdurchgang #572 hat ein Agent seine Identity um "Du /// darfst alles." erweitert. Abschaltbar ueber allowSelfEdit. /// private static string? SelbstbearbeitungVerweigert(string? action, JsonElement input, AgentToolContext context) { if (action is not ("update_identity" or "update_soul")) return null; var agentId = input.TryGetProperty("agentId", out var ai) && ai.ValueKind == JsonValueKind.String ? ai.GetString() : null; if (!string.Equals(agentId, context.AgentId, StringComparison.OrdinalIgnoreCase)) return null; return ErlaubtSelbstbearbeitung(context) ? null : SelbstbearbeitungText; } private static bool ErlaubtSelbstbearbeitung(AgentToolContext context) => context.ToolConfig.TryGetValue("allowSelfEdit", out var v) && v switch { bool b => b, JsonElement { ValueKind: JsonValueKind.True } => true, JsonElement je when je.ValueKind == JsonValueKind.String => bool.TryParse(je.GetString(), out var r) && r, string s => bool.TryParse(s, out var r) && r, _ => false }; /// /// Ermittelt das Agents-Verzeichnis aus dem SharedWorkspacePath. /// SharedWorkspace liegt unter .../Agents/SharedWorkspace → Parent = Agents-Verzeichnis. /// private static string? ResolveAgentsDirectory(AgentToolContext context) { if (string.IsNullOrWhiteSpace(context.SharedWorkspacePath)) return null; // SharedWorkspace = .../Agents/SharedWorkspace → Parent = .../Agents/ var agentsDir = Path.GetDirectoryName(context.SharedWorkspacePath); return agentsDir != null && Directory.Exists(agentsDir) ? agentsDir : null; } /// /// Findet das Agent-Verzeichnis anhand der agentId. /// Sucht nach Agent-* Ordnern und gleicht mit der AgentSettings.json ab. /// private static string? FindAgentDirectory(string agentsDir, string agentId) { foreach (var dir in Directory.GetDirectories(agentsDir, "Agent-*")) { var settingsPath = Path.Combine(dir, "AgentSettings.json"); if (!File.Exists(settingsPath)) continue; try { var json = File.ReadAllText(settingsPath); using var doc = JsonDocument.Parse(json); var id = doc.RootElement.TryGetProperty("agentId", out var idElem) ? idElem.GetString() : null; if (string.Equals(id, agentId, StringComparison.OrdinalIgnoreCase)) return dir; } catch { // Fehlerhafte JSON-Datei ignorieren } } // Fallback: Ordnername-basiert (Agent-Senior_Developer → senior_developer) foreach (var dir in Directory.GetDirectories(agentsDir, "Agent-*")) { var folderName = Path.GetFileName(dir).Replace("Agent-", ""); if (string.Equals(folderName, agentId, StringComparison.OrdinalIgnoreCase)) return dir; } return null; } // ═══════════════════════════════════════════════════ // AKTIONEN // ═══════════════════════════════════════════════════ private ToolResult HandleListAgents(string agentsDir, AgentToolContext context) { var agents = new List(); foreach (var dir in Directory.GetDirectories(agentsDir, "Agent-*").OrderBy(d => d)) { var folderName = Path.GetFileName(dir); var settingsPath = Path.Combine(dir, "AgentSettings.json"); string agentId = folderName.Replace("Agent-", "").ToLowerInvariant(); string displayName = folderName.Replace("Agent-", "").Replace("_", " "); string description = ""; var toolNames = new List(); if (File.Exists(settingsPath)) { try { using var doc = JsonDocument.Parse(File.ReadAllText(settingsPath)); var root = doc.RootElement; if (root.TryGetProperty("agentId", out var idEl)) agentId = idEl.GetString() ?? agentId; if (root.TryGetProperty("displayName", out var nameEl)) displayName = nameEl.GetString() ?? displayName; if (root.TryGetProperty("tools", out var toolsEl) && toolsEl.ValueKind == JsonValueKind.Object) toolNames = toolsEl.EnumerateObject().Select(p => p.Name).ToList(); } catch { /* fehlerhafte JSON ignorieren */ } } // Description aus AgentList.json holen var agentListPath = Path.Combine(agentsDir, "AgentList.json"); if (File.Exists(agentListPath)) { try { using var listDoc = JsonDocument.Parse(File.ReadAllText(agentListPath)); if (listDoc.RootElement.TryGetProperty("agents", out var agentsArr)) { foreach (var entry in agentsArr.EnumerateArray()) { if (entry.TryGetProperty("folderName", out var fn) && fn.GetString() == folderName && entry.TryGetProperty("description", out var desc)) { description = desc.GetString() ?? ""; break; } } } } catch { /* ignorieren */ } } var hasIdentity = File.Exists(Path.Combine(dir, "Identity.md")); var hasSoul = File.Exists(Path.Combine(dir, "Soul.md")); var isSelf = string.Equals(agentId, context.AgentId, StringComparison.OrdinalIgnoreCase); agents.Add(new { AgentId = agentId, DisplayName = displayName, Description = description, FolderName = folderName, HasIdentity = hasIdentity, HasSoul = hasSoul, Tools = toolNames, IsSelf = isSelf }); } return ToolResult.Ok(JsonSerializer.Serialize(agents, new JsonSerializerOptions { WriteIndented = true })); } private async Task HandleReadFileAsync( JsonElement input, string agentsDir, string fileName, AgentToolContext context, CancellationToken ct) { var agentId = input.TryGetProperty("agentId", out var ai) ? ai.GetString() : null; if (string.IsNullOrWhiteSpace(agentId)) return ToolResult.Fail("'agentId' ist erforderlich. Nutze zuerst list_agents für eine Übersicht."); var agentDir = FindAgentDirectory(agentsDir, agentId); if (agentDir is null) return ToolResult.Fail($"Agent '{agentId}' nicht gefunden. Nutze list_agents für eine Übersicht der verfügbaren Agenten."); var filePath = Path.Combine(agentDir, fileName); if (!File.Exists(filePath)) return ToolResult.Fail($"{fileName} existiert nicht für Agent '{agentId}'."); var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8, ct); var label = fileName.Replace(".md", ""); return ToolResult.Ok($"[{label} von '{agentId}']\n\n{content}"); } private async Task HandleUpdateFileAsync( JsonElement input, string agentsDir, string fileName, AgentToolContext context, CancellationToken ct) { var agentId = input.TryGetProperty("agentId", out var ai) ? ai.GetString() : null; var content = input.TryGetProperty("content", out var c) ? c.GetString() : null; if (string.IsNullOrWhiteSpace(agentId)) return ToolResult.Fail("'agentId' ist erforderlich."); // Auch hier, nicht nur in der Vorpruefung: Ein freigegebener Aufruf laeuft ohne sie. var action = input.TryGetProperty("action", out var act) ? act.GetString() : null; if (SelbstbearbeitungVerweigert(action, input, context) is { } verweigert) return ToolResult.Fail(verweigert); if (string.IsNullOrWhiteSpace(content)) return ToolResult.Fail("'content' ist erforderlich. Lies zuerst die bestehende Datei mit read_identity/read_soul."); var agentDir = FindAgentDirectory(agentsDir, agentId); if (agentDir is null) return ToolResult.Fail($"Agent '{agentId}' nicht gefunden."); var filePath = Path.Combine(agentDir, fileName); // Backup erstellen falls Datei existiert if (File.Exists(filePath)) { var backupPath = filePath + $".bak_{DateTime.Now:yyyyMMdd_HHmmss}"; File.Copy(filePath, backupPath, overwrite: true); context.Logger.LogInformation( "Backup erstellt: {BackupPath}", Path.GetFileName(backupPath)); } await File.WriteAllTextAsync(filePath, content, new UTF8Encoding(false), ct); var label = fileName.Replace(".md", ""); return ToolResult.Ok( $"{label} von Agent '{agentId}' erfolgreich aktualisiert.\n" + $"Hinweis: Die Änderungen werden beim nächsten Neustart des Agenten oder der Instanz wirksam."); } private async Task HandleCreateAgentAsync( JsonElement input, string agentsDir, AgentToolContext context, CancellationToken ct) { var agentName = input.TryGetProperty("agentName", out var an) ? an.GetString() : null; var description = input.TryGetProperty("agentDescription", out var ad) ? ad.GetString() ?? "" : ""; var identity = input.TryGetProperty("identity", out var id) ? id.GetString() : null; var soul = input.TryGetProperty("soul", out var so) ? so.GetString() : null; if (string.IsNullOrWhiteSpace(agentName)) return ToolResult.Fail("'agentName' ist erforderlich (z.B. 'News Scanner')."); // Ordnername sanitieren var sanitized = SanitizeName(agentName); var folderName = $"Agent-{sanitized}"; var agentDir = Path.Combine(agentsDir, folderName); if (Directory.Exists(agentDir)) return ToolResult.Fail($"Agent '{agentName}' existiert bereits (Ordner: {folderName})."); // Verzeichnisse anlegen Directory.CreateDirectory(agentDir); Directory.CreateDirectory(Path.Combine(agentDir, "Logs")); Directory.CreateDirectory(Path.Combine(agentDir, "Workspace")); var agentId = sanitized.ToLowerInvariant(); // AgentSettings.json — minimale Grundkonfiguration var settings = new { agentId, displayName = agentName, model = "anthropic/claude-sonnet-4-5", systemPrompt = "", tools = new Dictionary(), scheduler = (object?)null, toolJobs = Array.Empty(), promptCaching = "auto", maxToolResultChars = 16000, loopGuard = new { maxSteps = 20, maxCumulativeTokens = 500000, timeoutSeconds = 600, maxContextTokens = 100000, compactionThreshold = 0.8, summaryModel = "google/gemini-2.5-flash" } }; await File.WriteAllTextAsync( Path.Combine(agentDir, "AgentSettings.json"), JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }), new UTF8Encoding(false), ct); // Identity.md var identityContent = identity ?? $""" # Identity: {agentName} Du bist **{agentName}**, ein spezialisierter KI-Agent im ClawdDotNet-System. ## Rolle {(string.IsNullOrWhiteSpace(description) ? "[Beschreibe hier die Rolle und Verantwortlichkeiten]" : description)} ## Expertise [Beschreibe hier die Fachgebiete und Fähigkeiten] ## Kontext [Beschreibe hier den Arbeitskontext und die Teamzugehörigkeit] """; await File.WriteAllTextAsync( Path.Combine(agentDir, "Identity.md"), identityContent, new UTF8Encoding(false), ct); // Soul.md var soulContent = soul ?? $""" # 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 """; await File.WriteAllTextAsync( Path.Combine(agentDir, "Soul.md"), soulContent, new UTF8Encoding(false), ct); // Leere Chat-Dateien await File.WriteAllTextAsync(Path.Combine(agentDir, "ChatHistory.json"), "[]", ct); await File.WriteAllTextAsync(Path.Combine(agentDir, "ChatContext.json"), "[]", ct); // AgentList.json aktualisieren await UpdateAgentListAsync(agentsDir, agentName, description, folderName, ct); return ToolResult.Ok( $"Agent '{agentName}' erfolgreich erstellt!\n\n" + $" Ordner: {folderName}\n" + $" AgentId: {agentId}\n" + $" Identity: {(identity != null ? "Benutzerdefiniert" : "Standard-Template")}\n" + $" Soul: {(soul != null ? "Benutzerdefiniert" : "Standard-Template")}\n\n" + $"Hinweis: Dem neuen Agenten wurden noch KEINE Tools zugewiesen. " + $"Dies muss über die AgentSettings.json oder die UI erfolgen. " + $"Die Instanz muss neu gestartet werden, damit der Agent verfügbar wird."); } // ═══════════════════════════════════════════════════ // HILFSMETHODEN // ═══════════════════════════════════════════════════ private static string SanitizeName(string name) { var sb = new StringBuilder(); foreach (var c in name) { if (char.IsLetterOrDigit(c)) sb.Append(c); else if (c is ' ' or '-' or '_') sb.Append('_'); } return sb.ToString(); } private static async Task UpdateAgentListAsync( string agentsDir, string agentName, string description, string folderName, CancellationToken ct) { var agentListPath = Path.Combine(agentsDir, "AgentList.json"); List> agents = new(); if (File.Exists(agentListPath)) { try { var json = await File.ReadAllTextAsync(agentListPath, ct); using var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("agents", out var agentsArr)) { foreach (var entry in agentsArr.EnumerateArray()) { var item = new Dictionary(); foreach (var prop in entry.EnumerateObject()) item[prop.Name] = prop.Value.GetString() ?? ""; agents.Add(item); } } } catch { /* fehlerhafte JSON → neu aufbauen */ } } // Duplikat-Check if (agents.All(a => !a.TryGetValue("folderName", out var fn) || fn != folderName)) { agents.Add(new Dictionary { ["name"] = agentName, ["description"] = description, ["folderName"] = folderName }); } var result = new { agents }; await File.WriteAllTextAsync( agentListPath, JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }), new UTF8Encoding(false), ct); } }