using System.Text.Json.Serialization; namespace ClawdDotNet.Core.Config; public sealed class AgentConfig { [JsonPropertyName("agentId")] public string AgentId { get; set; } = ""; [JsonPropertyName("displayName")] public string DisplayName { get; set; } = ""; /// #683: The unit this agent belongs to; null for the top level. [JsonPropertyName("unitId")] public string? UnitId { get; set; } /// /// #683: Since when the agent is in its unit (UTC) — so it can be told that it moved, /// and its lead that someone joined. /// [JsonPropertyName("unitSinceUtc")] public DateTime? UnitSinceUtc { get; set; } /// /// #683: The unit the agent left with its last move — so the lead it left is told as /// well as the one it joined. null after the first placement. /// [JsonPropertyName("previousUnitId")] public string? PreviousUnitId { get; set; } [JsonPropertyName("model")] public string Model { get; set; } = "anthropic/claude-sonnet-4-5"; [JsonPropertyName("systemPrompt")] public string SystemPrompt { get; set; } = ""; /// /// Identität des Agenten (aus Identity.md geladen). /// Definiert WER der Agent ist: Name, Rolle, Hintergrund. /// [JsonIgnore] public string Identity { get; set; } = ""; /// /// Seele des Agenten (aus Soul.md geladen). /// Definiert WIE der Agent denkt: Persönlichkeit, Werte, Verhaltensmuster. /// [JsonIgnore] public string Soul { get; set; } = ""; /// /// Lokaler Pfad zum Workspace-Verzeichnis des Agenten. /// Wird zur Laufzeit vom Host gesetzt. /// /// /// Kurzbeschreibung der Rolle/Aufgabe des Agenten. /// Wird aus AgentList.json geladen und bei list_agents angezeigt. /// [JsonIgnore] public string Description { get; set; } = ""; /// /// Absoluter Pfad zum Agent-Verzeichnis (Agent-XYZ). /// Wird zur Laufzeit gesetzt und bleibt stabil auch bei DisplayName-Änderungen. /// [JsonIgnore] public string AgentDir { get; set; } = ""; [JsonIgnore] public string WorkspacePath { get; set; } = ""; /// /// Lokaler Pfad zum geteilten Workspace-Verzeichnis (SharedWorkspace). /// Wird zur Laufzeit vom Host gesetzt. /// [JsonIgnore] public string SharedWorkspacePath { get; set; } = ""; /// /// Baut den vollständigen System-Prompt aus Identity + Soul + SystemPrompt zusammen. /// [JsonIgnore] public string FullSystemPrompt { get { var parts = new List(); if (!string.IsNullOrWhiteSpace(Identity)) parts.Add($"# Identity\n{Identity}"); if (!string.IsNullOrWhiteSpace(Soul)) parts.Add($"# Soul\n{Soul}"); if (!string.IsNullOrWhiteSpace(SystemPrompt)) parts.Add(SystemPrompt); return string.Join("\n\n", parts); } } [JsonPropertyName("tools")] public Dictionary> Tools { get; set; } = new(); /// /// #715: Treat the agent as reading foreign content even though none of its tools does — /// for an agent whose input comes from outside some other way. Can only tighten: an agent /// whose tools read foreign content is in that tier whatever this says. /// [JsonPropertyName("treatAsForeignReader")] public bool TreatAsForeignReader { get; set; } [JsonPropertyName("scheduler")] public SchedulerConfig? Scheduler { get; set; } [JsonPropertyName("toolJobs")] public List ToolJobs { get; set; } = new(); [JsonPropertyName("loopGuard")] public LoopGuardConfig LoopGuard { get; set; } = new(); /// /// Prompt-Caching: "auto" (Default), "on" oder "off". /// /// Bei "auto" wird es für Modelle aktiviert, die cache_control unterstützen. /// Der Nutzen ist erheblich, weil jeder Schritt eines Runs den kompletten Prompt /// erneut sendet — System-Prompt und Tool-Definitionen also dutzendfach. /// [JsonPropertyName("promptCaching")] public string PromptCaching { get; set; } = "auto"; /// /// Maximale Länge eines einzelnen Tool-Ergebnisses in Zeichen, bevor es gekürzt in /// den Kontext wandert. Ohne Grenze kann ein einziger Abruf den gesamten Kontext /// sprengen (ein WebFetch mit 512 KB entspricht etwa 130.000 Tokens). /// [JsonPropertyName("maxToolResultChars")] public int MaxToolResultChars { get; set; } = 16_000; /// /// Wie viele als nebenläufig gekennzeichnete Werkzeuge eines Zuges gleichzeitig /// laufen dürfen. 1 heißt: nacheinander, wie bisher. /// /// Die Grenze ist keine Leistungsstellschraube, sondern eine Rücksichtnahme — /// zwanzig gleichzeitige Abrufe gegen dieselbe Gegenstelle sind aus deren Sicht /// nicht von einem Angriff zu unterscheiden. /// [JsonPropertyName("maxParallelToolCalls")] public int MaxParallelToolCalls { get; set; } = 4; /// /// Denktiefe: "off" (Vorgabe), "minimal", "low", "medium", "high", "xhigh", "max" /// — oder eine Tokenzahl als Zeichenkette, etwa "4096". /// /// Vorgabe ist "off", damit sich für bestehende Agenten nichts ändert: Denk-Tokens /// zählen als Ausgabe-Tokens und damit gegen /// wie gegen das Budget. /// [JsonPropertyName("reasoningEffort")] public string ReasoningEffort { get; set; } = "off"; /// /// Ob das Gateway nur an Anbieter weiterreichen darf, die alle mitgeschickten /// Parameter beherrschen. /// /// Vorgabe true. Ohne das darf ein Anbieter cache_control, /// reasoning oder sogar tools stillschweigend übergehen — der Lauf /// liefert dann eine gewöhnliche Textantwort, wo ein Werkzeugaufruf hingehört /// hätte, und nichts weist darauf hin. Lieber eine sichtbare Absage als ein /// unbemerkt entwerteter Lauf. /// [JsonPropertyName("requireProviderParameters")] public bool RequireProviderParameters { get; set; } = true; [JsonPropertyName("budget")] public AgentBudget Budget { get; set; } = new(); /// /// #660: Per-agent retention of the prompt history. null retention = instance default. /// [JsonPropertyName("promptHistory")] public Transcripts.AgentPromptHistorySettings PromptHistory { get; set; } = new(); /// /// Übersetzt in das Anfragefeld. Null heißt: /// nichts mitschicken, das Modell denkt wie voreingestellt. /// public Api.Models.ReasoningRequest? BuildReasoningRequest() { var value = (ReasoningEffort ?? "").Trim(); if (value.Length == 0 || value.Equals("off", StringComparison.OrdinalIgnoreCase)) return null; if (int.TryParse(value, out var tokens)) return tokens > 0 ? new Api.Models.ReasoningRequest { MaxTokens = tokens } : null; return new Api.Models.ReasoningRequest { Effort = value.ToLowerInvariant() }; } } /// /// Tagesgrenzen eines Agenten. 0 bedeutet jeweils: keine Grenze. /// /// Zwei Arten, weil sich Kosten nicht immer beziffern lassen: Liefert der Anbieter für /// ein Modell keine Preise, greift die Kostengrenze nicht — die Token-Grenze immer. /// public sealed class AgentBudget { [JsonPropertyName("dailyCostUsd")] public decimal DailyCostUsd { get; set; } [JsonPropertyName("dailyTokens")] public long DailyTokens { get; set; } /// /// #716: What happens to a run of a recurring task when the budget is exhausted — /// expire (default) or catchUpOnce. One-time tasks are always carried /// over to the next day. Kept as text so an unknown value falls back instead of /// failing the whole configuration. /// [JsonPropertyName("whenExhausted")] public string WhenExhausted { get; set; } = "expire"; [JsonIgnore] public Tasks.BudgetMissPolicy MissPolicy => string.Equals(WhenExhausted?.Trim(), "catchUpOnce", StringComparison.OrdinalIgnoreCase) ? Tasks.BudgetMissPolicy.CatchUpOnce : Tasks.BudgetMissPolicy.Expire; [JsonIgnore] public bool HasAnyLimit => DailyCostUsd > 0 || DailyTokens > 0; } /// Tagesgrenzen über alle Agenten einer Instanz hinweg. public sealed class InstanceBudget { [JsonPropertyName("dailyCostUsd")] public decimal DailyCostUsd { get; set; } [JsonPropertyName("dailyTokens")] public long DailyTokens { get; set; } [JsonIgnore] public bool HasAnyLimit => DailyCostUsd > 0 || DailyTokens > 0; public static InstanceBudget Unlimited { get; } = new(); } public sealed class SchedulerConfig { [JsonPropertyName("cron")] public string Cron { get; set; } = ""; [JsonPropertyName("runOnStart")] public bool RunOnStart { get; set; } [JsonPropertyName("taskMessage")] public string TaskMessage { get; set; } = "Führe deine zugewiesenen Aufgaben aus."; } public sealed class LoopGuardConfig { [JsonPropertyName("maxSteps")] public int MaxSteps { get; set; } = 20; /// /// Kostenbudget für einen kompletten Run: Summe der abgerechneten Tokens über /// ALLE Schritte hinweg. /// /// Achtung — dieser Wert ist nicht mit der Kontextgröße zu verwechseln: Da jeder /// Schritt den vollständigen Kontext erneut sendet, wächst die Summe quadratisch. /// Ein Agent mit stabilem 20k-Kontext verbraucht über 10 Schritte bereits ~200k. /// Der Default ist deshalb bewusst großzügig; für die Kontextgröße ist /// zuständig. /// [JsonPropertyName("maxCumulativeTokens")] public int MaxCumulativeTokens { get; set; } = 500_000; /// /// Veraltet: hieß früher "maxTokens" und wurde fälschlich als Kontextgrenze /// verstanden, obwohl er kumulativ zählt. Wird nur noch für die Migration alter /// Konfigurationen gelesen — siehe . /// [JsonPropertyName("maxTokens")] public int? LegacyMaxTokens { get; set; } [JsonPropertyName("timeoutSeconds")] public int TimeoutSeconds { get; set; } = 600; [JsonPropertyName("maxContextTokens")] public int MaxContextTokens { get; set; } = 100_000; /// /// Obergrenze für die Ausgabe eines einzelnen Schritts (max_tokens im Request). /// Deckelt die teuerste Token-Art gegen Ausreißer (B11/T8). 0 = keine Angabe, dann gilt /// der Standard des Anbieters. /// [JsonPropertyName("maxResponseTokens")] public int MaxResponseTokens { get; set; } = 8_192; [JsonPropertyName("compactionThreshold")] public double CompactionThreshold { get; set; } = 0.80; /// /// Modell für die Zusammenfassung beim Kompaktieren. Leer = Modell des Agenten. /// /// Zusammenfassen ist eine anspruchslose Aufgabe; sie mit einem teuren Modell zu /// erledigen kostet leicht mehr als der halbe Run, weil bis zu 30.000 Zeichen /// verarbeitet werden. /// [JsonPropertyName("summaryModel")] public string SummaryModel { get; set; } = "google/gemini-2.5-flash"; [JsonIgnore] public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds); }