B1, B14 und B3 beheben: Compaction-Paarung und Token-Semantik

B1 — Die Compaction behielt blind die letzten 6 Nachrichten. Fiel diese Grenze
mitten in eine Tool-Sequenz, entstand eine tool-Antwort ohne zugehoerigen
assistant-tool_call; die API lehnt das mit HTTP 400 ab. FindSafeTailStart
verschiebt die Grenze jetzt rueckwaerts auf eine Blockgrenze.

B14 — Bei Konversationen mit hoechstens 6 Nachrichten enthielt der Tail auch die
system-Nachricht, die anschliessend ein zweites Mal angehaengt wurde. Ergebnis
war ein doppelter System-Prompt und eine duplizierte Konversation — die
Compaction vergroesserte den Kontext, statt ihn zu verkleinern. Der Tail beginnt
nun grundsaetzlich hinter dem System-Prompt; liegt davor nichts Nennenswertes,
wird die Kompaktierung uebersprungen. Gefunden durch den Property-Test.

Nebeneffekt: Zusammengefasst wird nur noch der Teil, der tatsaechlich wegfaellt.
Der Tail bleibt woertlich erhalten und musste bisher doppelt bezahlt werden.

B3 — maxTokens zaehlte kumulativ ueber alle Schritte, wurde aber wie eine
Kontextgrenze konfiguriert. Da jeder Schritt den vollen Kontext erneut sendet,
brach ein Chat mit 20k Kontext nach vier Schritten ab. Aufgeteilt in
maxCumulativeTokens (Kostenbudget, Default 500k) und maxContextTokens
(Kontextgroesse). Alte Konfigurationen werden beim Laden migriert, die
Fehlermeldungen unterscheiden jetzt Schritt- und Kostenlimit.

Alle 31 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-27 10:15:50 +02:00
co-authored by Claude Opus 4.8
parent 667cecce25
commit eae13771cf
10 changed files with 209 additions and 23 deletions
+19 -1
View File
@@ -107,8 +107,26 @@ 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 MaxTokens { get; set; } = 80_000;
public int? LegacyMaxTokens { get; set; }
[JsonPropertyName("timeoutSeconds")]
public int TimeoutSeconds { get; set; } = 600;
@@ -20,10 +20,41 @@ public static class ConfigLoader
var config = await JsonSerializer.DeserializeAsync<InstanceConfig>(stream, JsonOptions, ct)
?? throw new InvalidOperationException($"Config file is empty or invalid: {filePath}");
Migrate(config);
Validate(config, filePath);
return config;
}
/// <summary>
/// Hebt alte Konfigurationen auf das aktuelle Schema.
///
/// "maxTokens" wurde früher als Kontextgrenze verstanden, zählte aber kumulativ
/// über alle Schritte — dadurch brachen normale Läufe vorzeitig ab. Der Wert wird
/// nicht übernommen, sondern durch den großzügigen Default für das Kostenbudget
/// ersetzt; die Kontextsteuerung übernimmt maxContextTokens.
/// </summary>
public static void Migrate(InstanceConfig config)
{
foreach (var agent in config.Agents)
Migrate(agent);
}
/// <summary>
/// Migration für einen einzeln geladenen Agenten (AgentSettings.json).
/// </summary>
public static void Migrate(AgentConfig agent)
{
var guard = agent.LoopGuard;
if (guard.LegacyMaxTokens is not { } legacy)
return;
// Nur übernehmen, wenn bewusst großzügiger konfiguriert als der Default.
if (legacy > guard.MaxCumulativeTokens)
guard.MaxCumulativeTokens = legacy;
guard.LegacyMaxTokens = null;
}
private static void Validate(InstanceConfig config, string filePath)
{
if (string.IsNullOrWhiteSpace(config.OpenRouterApiKey))