OrgAdministration (App), damit WebUI und Desktop dieselben Regeln anwenden: - Einheiten anlegen, umbenennen, Ziel setzen, loeschen (nur leer); Leitung setzen (nur ein Mitglied). - Agent umziehen: haelt fest, seit wann und woher (UnitSinceUtc, PreviousUnitId); eine Leitung, die geht, laesst ihre Einheit ohne Leitung. Einheit umziehen: nie unter sich selbst; die Mitglieder bleiben in ihrer Einheit, ihre Position aendert sich, und das wird ihnen gesagt. - Kein Umzug unter einem laufenden Agenten. Das Konzept sagt "bei gestoppter Instanz", damit niemand mitten im Lauf andere Rechte bekommt. Die Verwaltung laeuft in der Instanz und kann sie nicht stoppen; sie lehnt ab, solange ein betroffener Agent laeuft. Das dient demselben Zweck, nur enger. - Rechte-Obergrenze: beim Schreiben geprueft (SetToolEnabled, SetToolSettings, und als Sicherheitsnetz Save() fuer den Desktop, der Werkzeuge direkt umschaltet). Obergrenze senken und in eine Obergrenze umziehen zeigen in der Vorschau, welche Werkzeuge wegfallen, und brauchen dafuer eine ausdrueckliche Bestaetigung. - Sammelwerte (Tagesbudget, Tokens, Modell, whenExhausted) mit Vorschau fuer die Einheit samt Untereinheiten: ein Wert je Agent oder ein verteilter Gesamtbetrag im Verhaeltnis der bisherigen Werte. Ausreisser (Faktor 3 zum Median der anderen) sind markiert und standardmaessig nicht gewaehlt. Geschrieben wird nur, was gewaehlt ist; die Einheit merkt sich den Wert nur als Vorgabe fuer neue Mitglieder. - Ein neuer Agent, der direkt in eine Einheit gelegt wird, bekommt deren Vorgaben; ein umgezogener behaelt seine Werte. - Jede Strukturaenderung steht mit Begruendung im Audit (Quelle "admin", Werkzeug "Organisation"). Der Organisationsabschnitt nennt der Leitung jetzt auch, wer die Einheit kuerzlich verlassen hat und wohin: Die abgebende Leitung wird unterrichtet, nicht nur die aufnehmende. Tests: OrgAdministrationTests (16). 1.333 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
306 lines
11 KiB
C#
306 lines
11 KiB
C#
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; } = "";
|
|
|
|
/// <summary>#683: The unit this agent belongs to; <c>null</c> for the top level.</summary>
|
|
[JsonPropertyName("unitId")]
|
|
public string? UnitId { get; set; }
|
|
|
|
/// <summary>
|
|
/// #683: Since when the agent is in its unit (UTC) — so it can be told that it moved,
|
|
/// and its lead that someone joined.
|
|
/// </summary>
|
|
[JsonPropertyName("unitSinceUtc")]
|
|
public DateTime? UnitSinceUtc { get; set; }
|
|
|
|
/// <summary>
|
|
/// #683: The unit the agent left with its last move — so the lead it left is told as
|
|
/// well as the one it joined. <c>null</c> after the first placement.
|
|
/// </summary>
|
|
[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; } = "";
|
|
|
|
/// <summary>
|
|
/// Identität des Agenten (aus Identity.md geladen).
|
|
/// Definiert WER der Agent ist: Name, Rolle, Hintergrund.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string Identity { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Seele des Agenten (aus Soul.md geladen).
|
|
/// Definiert WIE der Agent denkt: Persönlichkeit, Werte, Verhaltensmuster.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string Soul { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Lokaler Pfad zum Workspace-Verzeichnis des Agenten.
|
|
/// Wird zur Laufzeit vom Host gesetzt.
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Kurzbeschreibung der Rolle/Aufgabe des Agenten.
|
|
/// Wird aus AgentList.json geladen und bei list_agents angezeigt.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string Description { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Absoluter Pfad zum Agent-Verzeichnis (Agent-XYZ).
|
|
/// Wird zur Laufzeit gesetzt und bleibt stabil auch bei DisplayName-Änderungen.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string AgentDir { get; set; } = "";
|
|
|
|
[JsonIgnore]
|
|
public string WorkspacePath { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Lokaler Pfad zum geteilten Workspace-Verzeichnis (SharedWorkspace).
|
|
/// Wird zur Laufzeit vom Host gesetzt.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string SharedWorkspacePath { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Baut den vollständigen System-Prompt aus Identity + Soul + SystemPrompt zusammen.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string FullSystemPrompt
|
|
{
|
|
get
|
|
{
|
|
var parts = new List<string>();
|
|
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<string, Dictionary<string, object?>> Tools { get; set; } = new();
|
|
|
|
[JsonPropertyName("scheduler")]
|
|
public SchedulerConfig? Scheduler { get; set; }
|
|
|
|
[JsonPropertyName("toolJobs")]
|
|
public List<ToolJobConfig> ToolJobs { get; set; } = new();
|
|
|
|
[JsonPropertyName("loopGuard")]
|
|
public LoopGuardConfig LoopGuard { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[JsonPropertyName("promptCaching")]
|
|
public string PromptCaching { get; set; } = "auto";
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
[JsonPropertyName("maxToolResultChars")]
|
|
public int MaxToolResultChars { get; set; } = 16_000;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[JsonPropertyName("maxParallelToolCalls")]
|
|
public int MaxParallelToolCalls { get; set; } = 4;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="LoopGuardConfig.MaxResponseTokens"/>
|
|
/// wie gegen das Budget.
|
|
/// </summary>
|
|
[JsonPropertyName("reasoningEffort")]
|
|
public string ReasoningEffort { get; set; } = "off";
|
|
|
|
/// <summary>
|
|
/// Ob das Gateway nur an Anbieter weiterreichen darf, die alle mitgeschickten
|
|
/// Parameter beherrschen.
|
|
///
|
|
/// Vorgabe <c>true</c>. Ohne das darf ein Anbieter <c>cache_control</c>,
|
|
/// <c>reasoning</c> oder sogar <c>tools</c> 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.
|
|
/// </summary>
|
|
[JsonPropertyName("requireProviderParameters")]
|
|
public bool RequireProviderParameters { get; set; } = true;
|
|
|
|
[JsonPropertyName("budget")]
|
|
public AgentBudget Budget { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// #660: Per-agent retention of the prompt history. <c>null</c> retention = instance default.
|
|
/// </summary>
|
|
[JsonPropertyName("promptHistory")]
|
|
public Transcripts.AgentPromptHistorySettings PromptHistory { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Übersetzt <see cref="ReasoningEffort"/> in das Anfragefeld. Null heißt:
|
|
/// nichts mitschicken, das Modell denkt wie voreingestellt.
|
|
/// </summary>
|
|
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() };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class AgentBudget
|
|
{
|
|
[JsonPropertyName("dailyCostUsd")]
|
|
public decimal DailyCostUsd { get; set; }
|
|
|
|
[JsonPropertyName("dailyTokens")]
|
|
public long DailyTokens { get; set; }
|
|
|
|
/// <summary>
|
|
/// #716: What happens to a run of a recurring task when the budget is exhausted —
|
|
/// <c>expire</c> (default) or <c>catchUpOnce</c>. 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.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>Tagesgrenzen über alle Agenten einer Instanz hinweg.</summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="MaxContextTokens"/> zuständig.
|
|
/// </summary>
|
|
[JsonPropertyName("maxCumulativeTokens")]
|
|
public int MaxCumulativeTokens { get; set; } = 500_000;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="MaxCumulativeTokens"/>.
|
|
/// </summary>
|
|
[JsonPropertyName("maxTokens")]
|
|
public int? LegacyMaxTokens { get; set; }
|
|
|
|
[JsonPropertyName("timeoutSeconds")]
|
|
public int TimeoutSeconds { get; set; } = 600;
|
|
|
|
[JsonPropertyName("maxContextTokens")]
|
|
public int MaxContextTokens { get; set; } = 100_000;
|
|
|
|
/// <summary>
|
|
/// Obergrenze für die Ausgabe eines einzelnen Schritts (<c>max_tokens</c> im Request).
|
|
/// Deckelt die teuerste Token-Art gegen Ausreißer (B11/T8). 0 = keine Angabe, dann gilt
|
|
/// der Standard des Anbieters.
|
|
/// </summary>
|
|
[JsonPropertyName("maxResponseTokens")]
|
|
public int MaxResponseTokens { get; set; } = 8_192;
|
|
|
|
[JsonPropertyName("compactionThreshold")]
|
|
public double CompactionThreshold { get; set; } = 0.80;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[JsonPropertyName("summaryModel")]
|
|
public string SummaryModel { get; set; } = "google/gemini-2.5-flash";
|
|
|
|
[JsonIgnore]
|
|
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
|
|
}
|