Files
ClawdDotNet/src/ClawdDotNet.Core/Engine/FehlschlagWache.cs
T

129 lines
4.6 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using ClawdDotNet.Core.Api.Models;
namespace ClawdDotNet.Core.Engine;
/// <summary>
/// Liest die Fehlerantworten, die die Engine selbst fuer Werkzeugaufrufe erzeugt:
/// <c>{"error": "..."}</c>.
///
/// <para>Erfolgreiche Werkzeugergebnisse kommen gerahmt an (#193) und beginnen deshalb nie
/// mit <c>{</c> - ein abgerufenes JSON mit einem Feld "error" wird so nicht mit einem
/// Fehlschlag verwechselt.</para>
/// </summary>
internal static class WerkzeugFehler
{
internal static string Json(string message) => JsonSerializer.Serialize(new { error = message });
internal static bool TryRead(string? result, [NotNullWhen(true)] out string? message)
{
message = null;
if (string.IsNullOrEmpty(result) || result[0] != '{')
return false;
try
{
using var doc = JsonDocument.Parse(result);
if (doc.RootElement.ValueKind == JsonValueKind.Object
&& doc.RootElement.TryGetProperty("error", out var error)
&& error.ValueKind == JsonValueKind.String)
{
message = error.GetString() ?? "";
return true;
}
}
catch (JsonException)
{
// Kein JSON - dann auch keine Fehlerantwort der Engine.
}
return false;
}
}
/// <summary>Ein Lauf endet, weil derselbe Aufruf wiederholt gleich gescheitert ist (#581).</summary>
internal sealed class WiederholterFehlschlagException(string message) : Exception(message);
/// <summary>
/// Erkennt, wenn ein Agent denselben Werkzeugaufruf immer wieder gleich scheitern laesst
/// (#581, Punkt 4).
///
/// <para>Im Pruefdurchgang #572 schickte ein Agent denselben fehlschlagenden Aufruf bis zur
/// Schrittgrenze - jeder Versuch kostet den vollen Kontext. Beim zweiten gleichen
/// Fehlschlag bekommt das Modell deshalb zum Fehler den Hinweis, anders vorzugehen; beim
/// dritten endet der Lauf mit eigenem Status.</para>
///
/// <para>Gleich heisst: dasselbe Werkzeug, dieselben Argumente (ohne Leerraum-Unterschiede)
/// und derselbe Fehlertext. Gezaehlt wird je Aufruf ueber den ganzen Lauf, nicht nur direkt
/// hintereinander - sonst entkaeme ein Agent, der zwei kaputte Aufrufe abwechselt. Gelingt
/// derselbe Aufruf, beginnt seine Zaehlung von vorn.</para>
/// </summary>
internal sealed class FehlschlagWache
{
internal const int HinweisAb = 2;
internal const int AbbruchAb = 3;
internal const string Hinweis =
"Dieser Aufruf ist mit denselben Argumenten bereits auf dieselbe Weise gescheitert. "
+ "Nicht noch einmal gleich aufrufen - anders vorgehen (andere Argumente, anderes Werkzeug) "
+ "oder mit einer Erklaerung abschliessen.";
private readonly Dictionary<string, (string Fehler, int Anzahl)> _stand = new(StringComparer.Ordinal);
/// <summary>
/// Prueft die Ergebnisse eines Zuges. Ergaenzt beim zweiten gleichen Fehlschlag das
/// Ergebnis um den Hinweis (an Ort und Stelle) und liefert beim dritten den Grund fuer
/// den Abbruch - sonst <c>null</c>.
/// </summary>
internal string? Pruefe(IReadOnlyList<ToolCall> calls, string?[] results)
{
string? abbruch = null;
for (var i = 0; i < calls.Count && i < results.Length; i++)
{
var name = calls[i].Function.Name;
var schluessel = name + "\0" + Normalisiere(calls[i].Function.Arguments);
if (!WerkzeugFehler.TryRead(results[i], out var fehler))
{
if (results[i] is not null)
_stand.Remove(schluessel);
continue;
}
var anzahl = _stand.TryGetValue(schluessel, out var bisher) && bisher.Fehler == fehler
? bisher.Anzahl + 1
: 1;
_stand[schluessel] = (fehler, anzahl);
if (anzahl >= AbbruchAb)
{
abbruch ??= $"Werkzeug '{name}' ist {anzahl}-mal mit denselben Argumenten gescheitert: {fehler}";
}
else if (anzahl >= HinweisAb)
{
results[i] = JsonSerializer.Serialize(new { error = fehler, hinweis = Hinweis });
}
}
return abbruch;
}
private static string Normalisiere(string? arguments)
{
if (string.IsNullOrWhiteSpace(arguments))
return "";
try
{
using var doc = JsonDocument.Parse(arguments);
return JsonSerializer.Serialize(doc.RootElement);
}
catch (JsonException)
{
return arguments.Trim();
}
}
}