fix: Verwaiste tool_result-Blöcke aus History entfernen (API 400)

Wenn eine ältere ask_user_choice-Chain als Text-only geladen wird,
geht der tool_use verloren, aber das tool_result in der nächsten Chain
bleibt – Anthropic API antwortet mit 400 "unexpected tool_use_id".
Neue FixOrphanedToolResults()-Funktion entfernt diese verwaisten Blöcke.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-24 16:52:31 +01:00
1 parent 55477742a9
commit 9391b5a6fa
1 file changed
+64
+64
View File
@@ -553,6 +553,11 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
// Post-Processing: Offene tool_use-Blöcke ohne tool_result → synthetische tool_results einfügen
FixOrphanedToolUse(history);
// Post-Processing: tool_result-Blöcke ohne passendes tool_use entfernen
// Tritt auf wenn eine ältere ask_user_choice-Chain als Text-only geladen wird:
// Der tool_use(ask_user_choice) geht verloren, aber das tool_result(iter 999) in der nächsten Chain bleibt.
FixOrphanedToolResults(history);
// Post-Processing: Orphaned web_search server_tool_use entfernen
// Bei alten DB-Einträgen wurde web_search_tool_result nur als Sources-Array gespeichert,
// nicht als Raw-JSON → BuildHistoryFromDb überspringt sie (Array-Check) →
@@ -656,6 +661,65 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
}
}
/// <summary>
/// Entfernt verwaiste tool_result-Blöcke aus user-Messages:
/// tool_results deren tool_use_id in keinem vorherigen assistant-Block existiert.
/// Tritt auf wenn eine ältere ask_user_choice-Chain als Text-only geladen wird –
/// der tool_use geht verloren, aber das tool_result in der nächsten Chain bleibt.
/// </summary>
private static void FixOrphanedToolResults(List<object> history)
{
// Alle tool_use IDs in der gesamten History sammeln
var allToolUseIds = new HashSet<string>();
foreach (var msg in history)
{
if (msg is not Dictionary<string, object> dict) continue;
if (!dict.TryGetValue("role", out var role) || role?.ToString() != "assistant") continue;
if (!dict.TryGetValue("content", out var content) || content is not List<object> contentList) continue;
foreach (var block in contentList)
{
var json = JsonSerializer.Serialize(block);
JsonElement el;
try { el = JsonSerializer.Deserialize<JsonElement>(json); }
catch { continue; }
if (!el.TryGetProperty("type", out var t) || t.GetString() != "tool_use") continue;
if (el.TryGetProperty("id", out var idProp))
{
var id = idProp.GetString();
if (!string.IsNullOrEmpty(id))
allToolUseIds.Add(id);
}
}
}
// User-Messages durchgehen und orphaned tool_results entfernen
for (int i = history.Count - 1; i >= 0; i--)
{
if (history[i] is not Dictionary<string, object> udict) continue;
if (!udict.TryGetValue("role", out var urole) || urole?.ToString() != "user") continue;
if (!udict.TryGetValue("content", out var ucontent) || ucontent is not List<object> ucontentList) continue;
ucontentList.RemoveAll(block =>
{
var json = JsonSerializer.Serialize(block);
JsonElement el;
try { el = JsonSerializer.Deserialize<JsonElement>(json); }
catch { return false; }
if (!el.TryGetProperty("type", out var t) || t.GetString() != "tool_result") return false;
if (!el.TryGetProperty("tool_use_id", out var tid)) return false;
var toolUseId = tid.GetString();
return !string.IsNullOrEmpty(toolUseId) && !allToolUseIds.Contains(toolUseId);
});
// Wenn die user-Message nach dem Entfernen leer ist, ganze Message entfernen
if (ucontentList.Count == 0)
history.RemoveAt(i);
}
}
/// <summary>
/// Prüft die gesamte History auf tool_use-Blöcke ohne passendes tool_result in der nächsten user-Message.
/// Falls gefunden, werden synthetische tool_results eingefügt – verhindert Anthropic API 400.