Stop-Button: Zwei Stufen statt einer:
- 1. Klick: Graceful Stop (workCt) - laufende API-Calls/Tools abbrechen
- 2. Klick: Hard Stop (hardCt) - sofortiger Abbruch aus while-Loop
StreamingTracker: Zwei separate CancellationTokenSources (StopCts + HardCts),
Cancel() gibt 'graceful'/'hard'/null zurück je nach Stufe.
ReadLineAsync Cancellation-Fix: StreamReader.ReadLineAsync propagiert den
CancellationToken nicht zuverlässig an den underlying NetworkStream.
Bei extended thinking blockiert ReadLineAsync minutenlang ohne Reaktion
auf Cancellation. Fix: workCt.Register() disposed den HTTP-Stream,
damit ReadLineAsync sofort mit Exception abbricht.
Bug-Fix: OperationCanceledException in RunAgentChat/ChatNonStreaming wurde
vom generischen catch(Exception) geschluckt - Agent lief trotz Stop weiter.
Jetzt: catch(OperationCanceledException) { throw; } vor catch(Exception).
Frontend: Stop-Button zeigt Hard-Stop visuell (rot + Pulse-Animation),
SSE-Stream wird erst beim 2. Klick clientseitig abgebrochen.
63 lines
2.1 KiB
C#
63 lines
2.1 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Threading;
|
|
|
|
namespace Gott.Services;
|
|
|
|
public class StreamingTracker
|
|
{
|
|
private readonly ConcurrentDictionary<string, StreamingState> _streaming = new ConcurrentDictionary<string, StreamingState>();
|
|
|
|
public StreamingTokens Start(string sessionId, string chainId)
|
|
{
|
|
var stopCts = new CancellationTokenSource();
|
|
var hardCts = new CancellationTokenSource();
|
|
var state = new StreamingState(chainId, stopCts, hardCts);
|
|
_streaming[sessionId] = state;
|
|
return new StreamingTokens(stopCts.Token, hardCts.Token);
|
|
}
|
|
|
|
public void Stop(string sessionId)
|
|
{
|
|
if (_streaming.TryRemove(sessionId, out var state))
|
|
{
|
|
state.StopCts.Dispose();
|
|
state.HardCts.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels the running stream for a session.
|
|
/// First call = graceful (laufende Iteration wird fertig).
|
|
/// Second call = hard (sofortiger Abbruch aus while-Loop).
|
|
/// Returns "graceful", "hard" or null if not streaming.
|
|
/// </summary>
|
|
public string Cancel(string sessionId)
|
|
{
|
|
if (_streaming.TryGetValue(sessionId, out var state))
|
|
{
|
|
if (!state.StopCts.IsCancellationRequested)
|
|
{
|
|
// Erster Klick → graceful stop
|
|
try { state.StopCts.Cancel(); } catch { }
|
|
return "graceful";
|
|
}
|
|
else if (!state.HardCts.IsCancellationRequested)
|
|
{
|
|
// Zweiter Klick → hard stop
|
|
try { state.HardCts.Cancel(); } catch { }
|
|
return "hard";
|
|
}
|
|
return "hard"; // Bereits beides gecancelled
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public bool IsStreaming(string sessionId) => _streaming.ContainsKey(sessionId);
|
|
|
|
public string GetChainId(string sessionId) => _streaming.TryGetValue(sessionId, out var state) ? state.ChainId : null;
|
|
|
|
public record StreamingTokens(CancellationToken StopToken, CancellationToken HardToken);
|
|
|
|
private record StreamingState(string ChainId, CancellationTokenSource StopCts, CancellationTokenSource HardCts);
|
|
}
|