T1 — Prompt-Caching. Bisher wurde bei jedem Schritt eines Runs der komplette Prompt neu berechnet, inklusive Tool-Definitionen und System-Prompt, die sich nie aendern. Bei zehn Schritten und einem 15k-Praefix sind das 150.000 statt 15.000 Eingabe-Tokens. ChatMessage bekommt dafuer einen eigenen JsonConverter: Der Inhalt geht weiterhin als String raus, bei gesetztem CacheBreakpoint jedoch als Blockarray mit cache_control. Beim Lesen werden beide Formate akzeptiert, damit bestehende ChatContext.json weiter geladen werden koennen. PromptCache setzt zwei Breakpoints: einen auf den System-Prompt (deckt Tool-Definitionen und System-Prompt ab) und einen rollierenden auf die letzte Nachricht mit Inhalt. Vorherige Markierungen werden vorher entfernt, damit sie sich nicht ansammeln. Aktivierung ueber promptCaching: auto (Default, aktiv fuer Modelle mit Unterstuetzung), on oder off. Der wichtigste Test dazu prueft die Praefix-Stabilitaet: Der System-Prompt muss ueber alle Schritte zeichengleich serialisiert werden. Ein einziger Zeitstempel darin wuerde den Cache still verwerfen — die Kosten blieben unveraendert, ohne dass es irgendwo auffiele. T9 — Usage liest prompt_tokens_details.cached_tokens; die Zahl wird bis in AgentRunResult durchgereicht. Ohne sie liesse sich die Wirkung nicht belegen. T2 — Tool-Ergebnisse werden jetzt zentral in ExecuteToolCallAsync gekappt (maxToolResultChars, Default 16.000). Bisher konnte ein einzelner WebFetch mit dem 512-KB-Standardlimit rund 130.000 Tokens in EINER Antwort erzeugen; die Compaction griff erst danach, bezahlt war der Request laengst. T3 — Die Zusammenfassung beim Kompaktieren laeuft ueber ein konfigurierbares summaryModel (Default gemini-2.5-flash) statt ueber das teure Agentenmodell. B4 — AgentRunResult fuehrt Prompt- und Completion-Tokens getrennt; die Kostenanzeige schaetzte bisher 50/50, real liegt das Verhaeltnis eher bei 95:5. Die veraltete Preistabelle bleibt offen. Neue Einstellungen sind im PropertyGrid sichtbar und werden vom AgentEditor bei neuen Agenten mitgeschrieben. Alle 91 Tests gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
7.3 KiB
C#
179 lines
7.3 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;
|
||
}
|
||
|
||
// ──────────────── 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;
|
||
}
|