feat: Auto-Retry bei prompt-too-long – History dynamisch kürzen

- Bei API 400 'prompt is too long' / 'exceed context limit':
  - 20% der ältesten Messages aus In-Memory-History entfernen
  - TrimMessagesCleanly() verhindert Trennung von tool_use/tool_result-Paaren
  - Retry mit kürzerer History (max 5 Retries im Worker)
- Alle 3 API-Call-Stellen abgesichert (StreamChat, RunAgentChat, ChatNonStreaming)
- Frontend: Neues 'info'-Event als Warning-Bubble angezeigt
- MaxHistory 220 bleibt als Obergrenze, dynamische Kürzung bei Token-Überschreitung
This commit is contained in:
Meleki committed 2026-03-10 11:56:09 +01:00
1 parent 8dd962ac49
commit c9b1523e86
2 files changed
+95

No files matched your search

+91
View File
@@ -60,6 +60,7 @@ public class AnthropicService
int iterations = 0;
int reviewRound = 0;
string lastStopReason = "";
int promptTrimRetries = 0;
var chain = new ChainRecord
{
@@ -106,6 +107,23 @@ public class AnthropicService
{
var errBody = await httpResp.Content.ReadAsStringAsync(workCt);
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
// Auto-Retry bei "prompt too long" / "exceed context limit": History kürzen und erneut versuchen
if ((int)httpResp.StatusCode == 400 &&
(errBody.Contains("prompt is too long") || errBody.Contains("exceed context limit")) &&
messages.Count > 4 && promptTrimRetries < 5)
{
promptTrimRetries++;
int trimCount = Math.Max(2, messages.Count / 5); // 20% der Messages entfernen
trimCount = TrimMessagesCleanly(messages, trimCount);
if (trimCount > 0)
{
await TryWriteSse(response, "info", new { message = $"History zu lang – {trimCount} ältere Messages entfernt, Retry ({promptTrimRetries}/5)..." }, sseCt);
iterations--; // Iteration nicht zählen, war ja kein echter Versuch
continue; // → nächste while-Iteration mit kürzerer History
}
}
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
@@ -120,6 +138,7 @@ public class AnthropicService
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt);
lastStopReason = stopReason;
promptTrimRetries = 0; // Erfolgreicher API-Call → Retry-Counter resetten
messages.Add(new { role = "assistant", content = assistantContent });
@@ -726,6 +745,20 @@ public class AnthropicService
if (!httpResp.IsSuccessStatusCode)
{
// Auto-Retry bei "prompt too long" in Sub-Agents
if ((int)httpResp.StatusCode == 400 &&
(respBody.Contains("prompt is too long") || respBody.Contains("exceed context limit")) &&
messages.Count > 4)
{
int trimCount = Math.Max(2, messages.Count / 5);
trimCount = TrimMessagesCleanly(messages, trimCount);
if (trimCount > 0)
{
await TryWriteSse(response, "info", new { message = $"{tag}: History zu lang – {trimCount} Messages entfernt, Retry..." }, sseCt);
iterations--;
continue;
}
}
return ($"{tag}-Agent API Fehler: {(int)httpResp.StatusCode}: {respBody}", iterations);
}
@@ -931,6 +964,20 @@ public class AnthropicService
if (!httpResp.IsSuccessStatusCode)
{
// Auto-Retry bei "prompt too long" in Non-Streaming
if ((int)httpResp.StatusCode == 400 &&
(respBody.Contains("prompt is too long") || respBody.Contains("exceed context limit")) &&
messages.Count > 4)
{
int trimCount = Math.Max(2, messages.Count / 5);
trimCount = TrimMessagesCleanly(messages, trimCount);
if (trimCount > 0)
{
iterations--;
continue;
}
}
var errMsg = $"API {(int)httpResp.StatusCode}: {respBody}";
await _db.SaveMessage(new MessageRecord
{
@@ -1485,6 +1532,50 @@ public class AnthropicService
payload["tool_choice"] = new { type = effectiveChoice };
}
/// <summary>
/// Entfernt Messages vom Anfang der Liste, ohne tool_use/tool_result-Paare zu trennen.
/// Gibt die tatsächliche Anzahl entfernter Messages zurück.
/// </summary>
private static int TrimMessagesCleanly(List<object> messages, int targetRemoveCount)
{
if (targetRemoveCount <= 0 || targetRemoveCount >= messages.Count)
return 0;
int removeCount = targetRemoveCount;
// Sicherstellen, dass wir nicht mitten in einem tool_use/tool_result-Paar abschneiden
while (removeCount < messages.Count - 1)
{
var firstRemaining = messages[removeCount];
var firstRemainingJson = JsonSerializer.Serialize(firstRemaining);
// Wenn die erste verbleibende Message ein tool_result ist → vorherige auch behalten
if (firstRemainingJson.Contains("\"tool_result\""))
{
removeCount--;
if (removeCount <= 0) return 0;
continue;
}
// Wenn die letzte entfernte Message ein assistant mit tool_use ist → tool_result auch entfernen
var lastRemovedJson = JsonSerializer.Serialize(messages[removeCount - 1]);
if (lastRemovedJson.Contains("\"tool_use\"") && lastRemovedJson.Contains("\"assistant\""))
{
removeCount++;
continue;
}
break;
}
if (removeCount > 0 && removeCount < messages.Count)
{
messages.RemoveRange(0, removeCount);
return removeCount;
}
return 0;
}
private HttpClient CreateClient()
{
var client = _httpFactory.CreateClient();
+4
View File
@@ -107,6 +107,10 @@ function handleStreamEvent(eventType, data) {
finalizeReviewBubble('done', data, 'finalizer');
break;
case 'info':
addWarningBubble(data.message, true);
break;
case 'error':
endStreamBubble();
addErrorBubble(data.error, true);