Es gab keine Obergrenze. Ein Agent in einer Schleife — etwa durch gegenseitige send_message-Aufrufe — konnte unbeaufsichtigt Guthaben verbrennen; die Credits-Anzeige war rein informativ. Verbrauchsdaten in der Datenbank Voraussetzung dafuer ist eine belastbare Erfassung. Bisher lag der Verbrauch in TokenUsage.json: Bei JEDEM Agenten-Lauf wurde die gesamte Datei geladen, ergaenzt und neu geschrieben, unter einem globalen Lock. Das waechst quadratisch und ist der eigentliche Engpass bei vielen Agenten — unabhaengig davon, welche Datenbank darunter liegt. RunUsage liegt jetzt in einer eigenen Tabelle mit Ortsdatum, damit ein Tagesbudget der Wahrnehmung des Benutzers folgt und die Abfrage ohne Zeitzonenrechnerei auskommt. Betraege werden als Text abgelegt und als decimal gelesen: Ueber REAL zu gehen wuerde bei Cent-Betraegen Rundungsfehler einsammeln, die sich ueber tausende Laeufe summieren. Ein Test weist das mit 1000 Buchungen zu je 0,0001 USD nach. BudgetGuard Zwei Arten von Grenzen, weil sich Kosten nicht immer beziffern lassen: Liefert der Anbieter fuer ein Modell keine Preise, greift die Kostengrenze nicht — die Token-Grenze dagegen immer. Wer sich absichern will, setzt beide. Ist die Summe wegen fehlender Preise unvollstaendig, steht das in der Begruendung; sonst wirkte ein niedriger Verbrauch wie ein noch offener Spielraum. Grenzen gibt es je Agent und je Instanz. Geprueft wird vor der ersten Anfrage, damit ein erschoepftes Budget gar nichts mehr kostet. Neuer Endzustand BudgetExceeded. Nebenbei: Der Namespace Usage kollidierte mit der gleichnamigen Modellklasse fuer Token-Angaben und heisst jetzt Accounting. 357 Tests gruen (209 Core, 148 Tools). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
201 lines
8.2 KiB
C#
201 lines
8.2 KiB
C#
using System.ComponentModel;
|
||
using ClawdDotNet.Core.Config;
|
||
|
||
namespace ClawdDotNet.Models;
|
||
|
||
/// <summary>Bietet die drei gültigen Prompt-Caching-Werte als Auswahlliste an.</summary>
|
||
public sealed class PromptCachingConverter : StringConverter
|
||
{
|
||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||
|
||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
|
||
=> new(new[] { "auto", "on", "off" });
|
||
}
|
||
|
||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||
public sealed class AgentSettingsViewModel
|
||
{
|
||
private readonly AgentConfig _config;
|
||
|
||
public AgentSettingsViewModel(AgentConfig config)
|
||
{
|
||
_config = config;
|
||
}
|
||
|
||
// ──────────────── Identity ────────────────
|
||
|
||
[Category("1 - Identity")]
|
||
[DisplayName("Agent-ID")]
|
||
[Description("Eindeutige ID des Agenten. Wird intern und in Logs verwendet.")]
|
||
[ReadOnly(true)]
|
||
public string AgentId => _config.AgentId;
|
||
|
||
[Category("1 - Identity")]
|
||
[DisplayName("Anzeigename")]
|
||
[Description("Freundlicher Name des Agenten (z.B. 'Marktanalyst').")]
|
||
public string DisplayName
|
||
{
|
||
get => _config.DisplayName;
|
||
set => _config.DisplayName = value;
|
||
}
|
||
|
||
[Category("1 - Identity")]
|
||
[DisplayName("Modell")]
|
||
[Description("LLM-Modell das dieser Agent verwendet. Dropdown zeigt verfügbare Modelle von OpenRouter.")]
|
||
[TypeConverter(typeof(ModelTypeConverter))]
|
||
public string Model
|
||
{
|
||
get => _config.Model;
|
||
set => _config.Model = value;
|
||
}
|
||
|
||
[Category("1 - Identity")]
|
||
[DisplayName("Identity")]
|
||
[Description("Aus Identity.md geladen – definiert WER der Agent ist. Bearbeitung über den Toolbar-Button 'Identity bearbeiten'.")]
|
||
[ReadOnly(true)]
|
||
public string IdentityStatus =>
|
||
string.IsNullOrWhiteSpace(_config.Identity) ? "(nicht definiert)" : $"✔ {_config.Identity.Split('\n').Length} Zeilen";
|
||
|
||
[Category("1 - Identity")]
|
||
[DisplayName("Soul")]
|
||
[Description("Aus Soul.md geladen – definiert WIE der Agent denkt. Bearbeitung über den Toolbar-Button 'Soul bearbeiten'.")]
|
||
[ReadOnly(true)]
|
||
public string SoulStatus =>
|
||
string.IsNullOrWhiteSpace(_config.Soul) ? "(nicht definiert)" : $"✔ {_config.Soul.Split('\n').Length} Zeilen";
|
||
|
||
// ──────────────── Loop-Schutz ────────────────
|
||
|
||
[Category("2 - Loop-Schutz")]
|
||
[DisplayName("Max. Schritte pro Run")]
|
||
[Description("Maximale Anzahl LLM-Aufrufe pro Run. Verhindert Endlosschleifen bei fehlerhaften Tool-Calls.")]
|
||
public int MaxSteps
|
||
{
|
||
get => _config.LoopGuard.MaxSteps;
|
||
set => _config.LoopGuard.MaxSteps = Math.Max(1, value);
|
||
}
|
||
|
||
[Category("2 - Loop-Schutz")]
|
||
[DisplayName("Kostenbudget pro Run (Tokens)")]
|
||
[Description("Summe ALLER abgerechneten Tokens eines Runs über alle Schritte. " +
|
||
"Da jeder Schritt den kompletten Kontext erneut sendet, wächst diese Summe " +
|
||
"überproportional — der Wert liegt deutlich über der Kontextgröße. " +
|
||
"Für die Kontextgröße ist 'Max. Kontext-Tokens' zuständig.")]
|
||
public int MaxCumulativeTokens
|
||
{
|
||
get => _config.LoopGuard.MaxCumulativeTokens;
|
||
set => _config.LoopGuard.MaxCumulativeTokens = Math.Max(10_000, value);
|
||
}
|
||
|
||
[Category("2 - Loop-Schutz")]
|
||
[DisplayName("Timeout (Sekunden)")]
|
||
[Description("Maximale Laufzeit pro Run in Sekunden. Danach wird der Run abgebrochen.")]
|
||
public int TimeoutSeconds
|
||
{
|
||
get => _config.LoopGuard.TimeoutSeconds;
|
||
set => _config.LoopGuard.TimeoutSeconds = Math.Max(10, value);
|
||
}
|
||
|
||
// ──────────────── Kontext-Management ────────────────
|
||
|
||
[Category("3 - Kontext-Management")]
|
||
[DisplayName("Max. Kontext-Tokens")]
|
||
[Description("Maximales Token-Budget für den gesamten Konversationskontext. Bei Überschreitung wird automatisch kompaktiert.")]
|
||
public int MaxContextTokens
|
||
{
|
||
get => _config.LoopGuard.MaxContextTokens;
|
||
set => _config.LoopGuard.MaxContextTokens = Math.Max(10_000, value);
|
||
}
|
||
|
||
[Category("3 - Kontext-Management")]
|
||
[DisplayName("Kompaktierungs-Schwelle (%)")]
|
||
[Description("Ab welchem Prozentsatz der Max. Kontext-Tokens wird kompaktiert. 80 = bei 80% Auslastung. Stufe 1: Tool-Results kürzen. Stufe 2: LLM-Zusammenfassung.")]
|
||
public int CompactionThresholdPercent
|
||
{
|
||
get => (int)(_config.LoopGuard.CompactionThreshold * 100);
|
||
set => _config.LoopGuard.CompactionThreshold = Math.Clamp(value, 50, 95) / 100.0;
|
||
}
|
||
|
||
[Category("3 - Kontext-Management")]
|
||
[DisplayName("Modell für Zusammenfassungen")]
|
||
[Description("Modell, mit dem beim Kompaktieren zusammengefasst wird. Leer = Modell des Agenten. " +
|
||
"Zusammenfassen ist anspruchslos — ein günstiges Modell spart hier deutlich, " +
|
||
"da bis zu 30.000 Zeichen verarbeitet werden.")]
|
||
public string SummaryModel
|
||
{
|
||
get => _config.LoopGuard.SummaryModel;
|
||
set => _config.LoopGuard.SummaryModel = value ?? "";
|
||
}
|
||
|
||
[Category("3 - Kontext-Management")]
|
||
[DisplayName("Max. Zeichen pro Tool-Ergebnis")]
|
||
[Description("Längere Tool-Ergebnisse werden gekürzt, bevor sie in den Kontext gelangen. " +
|
||
"Ohne Grenze kann ein einzelner Abruf den Kontext sprengen — 512 KB entsprechen " +
|
||
"etwa 130.000 Tokens in einer einzigen Antwort.")]
|
||
public int MaxToolResultChars
|
||
{
|
||
get => _config.MaxToolResultChars;
|
||
set => _config.MaxToolResultChars = Math.Max(1_000, value);
|
||
}
|
||
|
||
// ──────────────── Kosten ────────────────
|
||
|
||
[Category("5 - Kosten")]
|
||
[DisplayName("Prompt-Caching")]
|
||
[Description("auto = für Modelle aktivieren, die es unterstützen; on = erzwingen; off = aus. " +
|
||
"Spart erheblich, weil jeder Schritt eines Runs den kompletten Prompt erneut sendet — " +
|
||
"System-Prompt und Tool-Definitionen also dutzendfach.")]
|
||
[TypeConverter(typeof(PromptCachingConverter))]
|
||
public string PromptCaching
|
||
{
|
||
get => _config.PromptCaching;
|
||
set => _config.PromptCaching = string.IsNullOrWhiteSpace(value) ? "auto" : value;
|
||
}
|
||
|
||
[Category("5 - Kosten")]
|
||
[DisplayName("Tagesbudget (USD)")]
|
||
[Description("Hartes Tageslimit für diesen Agenten in US-Dollar. 0 = kein Limit. " +
|
||
"Bei Überschreitung wird kein Lauf mehr gestartet. " +
|
||
"Greift nur, wenn für das Modell Preise vorliegen — sonst zusätzlich " +
|
||
"das Token-Tageslimit setzen.")]
|
||
public decimal DailyBudgetUsd
|
||
{
|
||
get => _config.Budget.DailyCostUsd;
|
||
set => _config.Budget.DailyCostUsd = Math.Max(0, value);
|
||
}
|
||
|
||
[Category("5 - Kosten")]
|
||
[DisplayName("Token-Tageslimit")]
|
||
[Description("Hartes Tageslimit für diesen Agenten in Tokens. 0 = kein Limit. " +
|
||
"Wirkt auch dann, wenn für das Modell keine Preise vorliegen.")]
|
||
public long DailyTokenLimit
|
||
{
|
||
get => _config.Budget.DailyTokens;
|
||
set => _config.Budget.DailyTokens = Math.Max(0, value);
|
||
}
|
||
|
||
// ──────────────── Tools (Read-Only) ────────────────
|
||
|
||
[Category("4 - Tools")]
|
||
[DisplayName("Zugewiesene Tools")]
|
||
[Description("Liste der Tool-Namen, die diesem Agent zugewiesen sind. Zuweisung über die Tabelle unten.")]
|
||
[ReadOnly(true)]
|
||
public string AssignedTools =>
|
||
_config.Tools.Count == 0
|
||
? "(keine)"
|
||
: string.Join(", ", _config.Tools.Keys);
|
||
|
||
[Category("4 - Tools")]
|
||
[DisplayName("Anzahl")]
|
||
[ReadOnly(true)]
|
||
public int ToolCount => _config.Tools.Count;
|
||
|
||
// ──────────────── Intern ────────────────
|
||
|
||
[Browsable(false)]
|
||
public AgentConfig UnderlyingConfig => _config;
|
||
|
||
public override string ToString() =>
|
||
string.IsNullOrWhiteSpace(_config.DisplayName) ? _config.AgentId : _config.DisplayName;
|
||
}
|