Agent-Arbeit vom Client-Disconnect entkoppeln
Zwei separate CancellationTokens: - workCt (nur Stop-Button): Anthropic API-Calls, Tool-Ausfuehrungen, DB-Writes Agent laeuft bei SSE-Disconnect weiter, Client pollt Ergebnisse - sseCt (Stop-Button + Client-Disconnect): SSE-Writes an Browser Bei Disconnect werden Writes zu stillen No-Ops Vorher: HttpContext.RequestAborted killte den gesamten Agent-Lauf wenn die SSE-Verbindung abriss (Browser-Tab idle, Netzwerk-Hickup). Jetzt arbeitet der Agent im Hintergrund weiter. OperationCanceledException = 'stopped' (nur Stop-Button) statt 'client_disconnected' (war falsch verknuepft)
This commit is contained in:
1 parent
dd99216913
commit
e9b1e8ef20
2 files changed
+72
-58
No files matched your search
@@ -175,9 +175,15 @@ public static class ChatEndpoint
|
||||
// Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann
|
||||
var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice;
|
||||
|
||||
// CancellationToken: Verknüpft Stop-Button (über StreamingTracker) + Client-Disconnect
|
||||
// Zwei separate CancellationTokens:
|
||||
// - workCt: Nur Stop-Button → für Anthropic API-Calls, Tool-Ausführungen, DB-Writes
|
||||
// Agent-Arbeit läuft bei Client-Disconnect weiter (Client pollt ja)
|
||||
// - sseCt: Stop-Button + Client-Disconnect → für SSE-Writes an den Browser
|
||||
// Bei Client-Disconnect werden SSE-Writes zu stillen No-Ops
|
||||
var stopCts = streamingTracker.Start(sessionId, ctx.ChainId);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stopCts.Token);
|
||||
var workCt = stopCts.Token;
|
||||
using var sseLinkCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, stopCts.Token);
|
||||
var sseCt = sseLinkCts.Token;
|
||||
|
||||
// SSE-Heartbeat: Alle 15s ein Kommentar senden, damit die Verbindung
|
||||
// bei langen Tool-Ausführungen nicht von Browser/Netzwerk getrennt wird.
|
||||
@@ -185,25 +191,25 @@ public static class ChatEndpoint
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!linkedCts.Token.IsCancellationRequested)
|
||||
while (!sseCt.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(15000, linkedCts.Token);
|
||||
await res.WriteAsync(": heartbeat\n\n", linkedCts.Token);
|
||||
await res.Body.FlushAsync(linkedCts.Token);
|
||||
await Task.Delay(15000, sseCt);
|
||||
await res.WriteAsync(": heartbeat\n\n", sseCt);
|
||||
await res.Body.FlushAsync(sseCt);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { }
|
||||
}, linkedCts.Token);
|
||||
}, sseCt);
|
||||
|
||||
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);
|
||||
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, workCt: workCt, sseCt: sseCt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
streamingTracker.Stop(sessionId);
|
||||
// Heartbeat-Task beenden (linkedCts wird disposed → Token cancelled)
|
||||
// Heartbeat-Task beenden (sseLinkCts wird disposed → Token cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user