- Engine: leere Antworten nachfassen, Aufgaben enden mit Ergebniszeile (EmptyResponse/Incomplete), gleiche Fehlschlaege erkennen (RepeatedFailure), Zeitzeile vor Chatnachrichten, Werkzeugergebnisse gerahmt, Vorpruefung vor dem Staging, Belegkosten wie RunUsage - Staging: gescheiterte Freigaben als Failed; AgentEditor-Aenderungen freigabepflichtig, Selbstbearbeitung gesperrt (allowSelfEdit) - Taskboard: Termine beim Anlegen pruefen, naechster/letzter Lauf, pausieren, Sofortlauf, Kostenprognose, Hinweis auf wachsenden Kontext; Jobs-Reiter und /api/jobs entfernt - Release: Instanzmanager samt Weboberflaeche im Paket, setup.json fragt manager.json ab, Token nicht mehr ins systemd-Journal Version 0.1.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
520 lines
22 KiB
C#
520 lines
22 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using ClawdDotNet.Core.Tools;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ClawdDotNet.Tools.AgentEditor;
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
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<ToolResult> 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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// #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).
|
|
/// </summary>
|
|
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.";
|
|
|
|
/// <summary>
|
|
/// 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 <c>allowSelfEdit</c>.
|
|
/// </summary>
|
|
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
|
|
};
|
|
|
|
/// <summary>
|
|
/// Ermittelt das Agents-Verzeichnis aus dem SharedWorkspacePath.
|
|
/// SharedWorkspace liegt unter .../Agents/SharedWorkspace → Parent = Agents-Verzeichnis.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Findet das Agent-Verzeichnis anhand der agentId.
|
|
/// Sucht nach Agent-* Ordnern und gleicht mit der AgentSettings.json ab.
|
|
/// </summary>
|
|
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<object>();
|
|
|
|
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<string>();
|
|
|
|
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<ToolResult> 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<ToolResult> 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<ToolResult> 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<string, object>(),
|
|
scheduler = (object?)null,
|
|
toolJobs = Array.Empty<object>(),
|
|
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<Dictionary<string, string>> 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<string, string>();
|
|
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<string, string>
|
|
{
|
|
["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);
|
|
}
|
|
}
|