using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Instances; namespace ClawdDotNet.App.Services; /// Die Angaben, die einen neuen Agenten ausmachen. public sealed record NewAgent( string AgentId, string DisplayName, string Model, string Description = ""); /// /// Agenten anlegen, entfernen, ihre Werkzeuge zulassen und einstellen. /// /// Diese Regeln standen bis zum 2026-09-06 in /// ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs — die Prüfung auf /// doppelte IDs, das Anlegen, das Speichern. Dort war es nicht falsch, solange es genau /// eine Oberfläche gab. Mit der Weboberfläche daneben wäre jede dieser Regeln ein /// zweites Mal geschrieben worden, und ab dem Tag danach wären beide Fassungen /// auseinandergelaufen. /// /// Alles, was schreibt, geht über — nur so /// erfährt die jeweils andere Oberfläche davon. /// public sealed class AgentAdministration(AppHost host) { /// /// Die Werkzeuge, die eine Oberfläche zum Ankreuzen anbietet. /// /// Fest verdrahtet und länger als das, was in einer Instanz tatsächlich /// registriert ist: Ein Werkzeug soll auch dann in der Liste stehen, wenn es in /// dieser Instanz gerade nicht geladen ist — sonst verschwände mit dem Werkzeug /// stillschweigend auch seine Konfiguration aus der Ansicht. Welche davon wirklich /// da sind, sagt : dort bleibt die Beschreibung leer. /// /// public static readonly IReadOnlyList KnownTools = [ "FileRW", "Telegram", "Mail", "Database", "FTP", "DirectAPI", "WebFetch", "WebMonitor", "AgentComm", "SocialMediaManager", "AgentSpawn", "AgentEditor", "Memory", "Taskboard" ]; public IReadOnlyList All => host.Instance.Agents; public AgentConfig? Find(string agentId) => host.Instance.Agents.FirstOrDefault( a => a.AgentId.Equals(agentId, StringComparison.OrdinalIgnoreCase)); /// /// Beschreibungen der tatsächlich geladenen Werkzeuge, nach Namen. Für ein Werkzeug /// aus , das hier fehlt, bleibt die Beschreibung leer. /// public IReadOnlyDictionary ToolDescriptions() => host.Tools.GetAll() .GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First().Description, StringComparer.OrdinalIgnoreCase); /// /// Legt einen Agenten an — mit Verzeichnis. /// /// Die ID wird ohne Rücksicht auf Groß- und Kleinschreibung geprüft. Zwei /// Agenten, die sich nur darin unterscheiden, wären auf der Zielplattform zwar zwei /// verschiedene Ordner, aber für jeden Menschen derselbe Agent. /// /// Das Verzeichnis ist keine Beigabe, sondern der Ort, an dem ein Agent /// wohnt. LoadInstanceConfig leert beim Start die Agentenliste und baut /// sie aus den Ordnern Agents/Agent-* wieder auf — die Liste in /// InstanceSettings.json wird dabei nicht gelesen. Ein Agent, der nur dort /// steht, ist nach dem nächsten Start weg. Genau das tat die Oberfläche bis zum /// 2026-09-06. /// public AdminResult Add(NewAgent request) { var agentId = request.AgentId.Trim(); var displayName = request.DisplayName.Trim(); if (agentId.Length == 0) return AdminResult.Failure("Ohne Agenten-ID geht es nicht."); if (displayName.Length == 0) return AdminResult.Failure("Ohne Anzeigenamen geht es nicht."); if (Find(agentId) is not null) return AdminResult.Failure($"Ein Agent mit der ID '{agentId}' existiert bereits."); string agentDir; try { // Legt Ordner, AgentSettings.json, Identity.md, Soul.md und den Eintrag in // AgentList.json an. Wirft, wenn es den Ordner schon gibt — zwei Agenten mit // verschiedener ID, aber gleichem Anzeigenamen teilten sich sonst ein // Verzeichnis. agentDir = host.Directories.CreateAgent( host.InstancePath, displayName, request.Description); } catch (Exception ex) { return AdminResult.Failure($"Verzeichnis konnte nicht angelegt werden: {ex.Message}"); } var config = host.Directories.LoadAgentConfig(agentDir); // CreateAgent leitet die ID aus dem Namen ab; hier zählt die, nach der gefragt // wurde. config.AgentId = agentId; config.DisplayName = displayName; config.Model = request.Model; config.Description = request.Description; host.Directories.SaveAgentSettings(agentDir, config); host.Instance.Agents.Add(config); host.SaveInstance(new InstanceChange(InstanceChangeKind.Agents, agentId)); return AdminResult.Success($"Agent '{displayName}' angelegt."); } /// /// Nimmt den Agenten aus dem Betrieb. /// /// Sein Verzeichnis wird umbenannt, nicht gelöscht — es heißt danach /// Entfernt-… und wird beim Start nicht mehr eingelesen. Nur aus der /// Instanzliste zu streichen genügte nicht: Beim nächsten Start stünde der Agent /// wieder da, weil sein Ordner noch da ist. Ihn zu löschen wäre die andere /// Übertreibung — im Arbeitsordner steht seine Arbeit. /// public AdminResult Remove(string agentId) { if (Find(agentId) is not { } agent) return AdminResult.Failure($"Kein Agent mit der ID '{agentId}'."); host.Instance.Agents.Remove(agent); var folder = agent.AgentDir.Length > 0 ? Path.GetFileName(agent.AgentDir) : InstanceDirectoryManager.BuildAgentFolderName(agent.DisplayName); string? archived = null; try { archived = host.Directories.ArchiveAgent(host.InstancePath, folder); } catch (Exception ex) { // Der Agent ist aus der Liste; dass sein Ordner liegen bleibt, ist ein // Ärgernis und kein Grund, den Aufruf scheitern zu lassen — sonst stünde er // halb entfernt da. Beim nächsten Start käme er allerdings wieder, und genau // das muss in der Meldung stehen. host.SaveInstance(new InstanceChange(InstanceChangeKind.Agents, agentId)); return AdminResult.Failure( $"Agent '{agentId}' aus der Liste genommen, aber sein Verzeichnis ließ sich " + $"nicht beiseiteräumen ({ex.Message}). Beim nächsten Start ist er wieder da."); } host.SaveInstance(new InstanceChange(InstanceChangeKind.Agents, agentId)); return AdminResult.Success(archived is null ? $"Agent '{agentId}' entfernt." : $"Agent '{agentId}' entfernt. Sein Verzeichnis liegt unter " + $"'{Path.GetFileName(archived)}'."); } /// /// Lässt ein Werkzeug zu oder nimmt es weg. /// /// Zulassen legt eine leere Konfiguration an — das Werkzeug läuft dann mit /// seinen Vorgabewerten. Wegnehmen entfernt den Eintrag samt Einstellungen; das ist /// seit jeher so und der Grund, warum die Oberfläche zwischen „ankreuzen" und /// „Einstellungen speichern" unterscheidet. /// public AdminResult SetToolEnabled(string agentId, string toolName, bool enabled) { if (Find(agentId) is not { } agent) return AdminResult.Failure($"Kein Agent mit der ID '{agentId}'."); if (enabled) { if (!agent.Tools.ContainsKey(toolName)) agent.Tools[toolName] = new Dictionary(); } else { agent.Tools.Remove(toolName); } PersistAgent(agent); host.SaveInstance(new InstanceChange(InstanceChangeKind.Agents, agentId)); return AdminResult.Success(enabled ? $"Werkzeug '{toolName}' für '{agent.DisplayName}' zugelassen." : $"Werkzeug '{toolName}' für '{agent.DisplayName}' entfernt."); } /// /// Schreibt die Einstellungen eines Werkzeugs. /// /// Einstellungen zu schreiben heißt, das Werkzeug zu wollen: Es wird dabei /// zugelassen, falls es das noch nicht war. /// public AdminResult SetToolSettings( string agentId, string toolName, Dictionary settings) { if (Find(agentId) is not { } agent) return AdminResult.Failure($"Kein Agent mit der ID '{agentId}'."); agent.Tools[toolName] = settings; PersistAgent(agent); host.SaveInstance(new InstanceChange(InstanceChangeKind.Agents, agentId)); return AdminResult.Success($"Einstellungen für '{toolName}' gespeichert."); } /// /// Schreibt die Instanzkonfiguration, nachdem der Aufrufer die /// -Objekte selbst verändert hat. /// /// Für die Avalonia-Oberfläche, deren Ansichtsmodelle über Bindungen direkt auf /// den Konfigurationsobjekten arbeiten und am Ende nur noch speichern wollen. Die /// Weboberfläche hat diesen Fall nicht — sie schickt ganze Felder und sollte die /// Aufrufe oben nehmen. /// public void Save() { foreach (var agent in host.Instance.Agents) PersistAgent(agent); host.SaveInstance(new InstanceChange(InstanceChangeKind.Agents)); } /// /// Schreibt einen Agenten dorthin, wo er beim nächsten Start gelesen wird. /// /// Das ist sein eigener Ordner, nicht InstanceSettings.json. /// LoadInstanceConfig leert die Agentenliste und baut sie aus /// Agents/Agent-*/AgentSettings.json wieder auf. Bis zum 2026-09-06 rief /// niemand im laufenden Betrieb SaveAgentSettings — jede Änderung an Modell, /// Prompt, Budget oder Werkzeugen stand nur in InstanceSettings.json und war /// nach dem nächsten Start weg. Es sah aus wie Speichern und war keines. /// /// Identity und Soul gehen in ihre eigenen Dateien: Von dort /// werden sie gelesen, und was in AgentSettings.json danebenstünde, überschriebe /// der nächste Ladevorgang ohnehin. /// private void PersistAgent(AgentConfig agent) { // Ein Agent ohne Verzeichnis ist einer aus einer von Hand gepflegten // Instanzdatei. Ihm hier einen Ordner anzulegen waere eine Nebenwirkung des // Speicherns; er bleibt, wie er ist. if (string.IsNullOrWhiteSpace(agent.AgentDir) || !Directory.Exists(agent.AgentDir)) return; host.Directories.SaveAgentSettings(agent.AgentDir, agent); host.Directories.SaveAgentIdentity(agent.AgentDir, agent.Identity); host.Directories.SaveAgentSoul(agent.AgentDir, agent.Soul); } }