using System.Diagnostics.CodeAnalysis; using System.Text.Json; using ClawdDotNet.Core.Api.Models; namespace ClawdDotNet.Core.Engine; /// /// Liest die Fehlerantworten, die die Engine selbst fuer Werkzeugaufrufe erzeugt: /// {"error": "..."}. /// /// Erfolgreiche Werkzeugergebnisse kommen gerahmt an (#193) und beginnen deshalb nie /// mit { - ein abgerufenes JSON mit einem Feld "error" wird so nicht mit einem /// Fehlschlag verwechselt. /// 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; } } /// Ein Lauf endet, weil derselbe Aufruf wiederholt gleich gescheitert ist (#581). internal sealed class WiederholterFehlschlagException(string message) : Exception(message); /// /// Erkennt, wenn ein Agent denselben Werkzeugaufruf immer wieder gleich scheitern laesst /// (#581, Punkt 4). /// /// 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. /// /// 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. /// 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 _stand = new(StringComparer.Ordinal); /// /// 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 null. /// internal string? Pruefe(IReadOnlyList 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(); } } }