fix: SSE-Stream-Stabilität und History-Trimming tool_use/tool_result-Schutz

- Kestrel MinResponseDataRate per-Request deaktivieren für SSE-Streams
  (Default 240 bytes/s + 5s Grace → client_disconnected bei langen Tools)
- SSE-Heartbeat alle 15s während Stream (schützt gegen Browser/Netzwerk-Idle)
- History-Trimming: RemoveRange schneidet nicht mehr in tool_use/tool_result-Paare
- Reminder-Insertion: kein Einfügen zwischen tool_use und tool_result
This commit is contained in:
Meleki committed 2026-03-09 20:13:22 +01:00
1 parent e81983cab6
commit dd99216913
3 files changed
+101 -2

No files matched your search

+26
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
@@ -124,6 +125,13 @@ public static class ChatEndpoint
var bufferingFeature = res.HttpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
bufferingFeature?.DisableBuffering();
// Kestrel MinResponseDataRate deaktivieren für diesen SSE-Stream.
// Ohne dies killt Kestrel die Verbindung wenn bei langen Tool-Ausführungen
// keine Daten fließen (Default: 240 bytes/s, 5s Grace → "client_disconnected").
var minRateFeature = res.HttpContext.Features.Get<Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature>();
if (minRateFeature != null)
minRateFeature.MinDataRate = null;
// ── Preparer-Agent Phase (vor dem Worker) ──
if (ctx.Preparer != null && !isChoiceResponse)
{
@@ -171,6 +179,23 @@ public static class ChatEndpoint
var stopCts = streamingTracker.Start(sessionId, ctx.ChainId);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stopCts.Token);
// SSE-Heartbeat: Alle 15s ein Kommentar senden, damit die Verbindung
// bei langen Tool-Ausführungen nicht von Browser/Netzwerk getrennt wird.
var heartbeatTask = Task.Run(async () =>
{
try
{
while (!linkedCts.Token.IsCancellationRequested)
{
await Task.Delay(15000, linkedCts.Token);
await res.WriteAsync(": heartbeat\n\n", linkedCts.Token);
await res.Body.FlushAsync(linkedCts.Token);
}
}
catch (OperationCanceledException) { }
catch { }
}, linkedCts.Token);
try
{
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, cancellationToken: linkedCts.Token);
@@ -178,6 +203,7 @@ public static class ChatEndpoint
finally
{
streamingTracker.Stop(sessionId);
// Heartbeat-Task beenden (linkedCts wird disposed → Token cancelled)
}
}