Files
gott/Services/SseHelper.cs
T
MelekiandClaude Opus 4.8 f21ed9ff9f Stream panel member responses live with a parallel SSE write-lock
Panel members now stream token-by-token and show tool uses in real time
(like the session agent) instead of delivering text only at completion.
The key constraint is byte-level safety: N members flushing SSE frames
concurrently on the shared HttpResponse would interleave and corrupt the
stream, so a SemaphoreSlim gate serializes the writes (not the model work).

- SseHelper.TryWriteSse takes an optional write-lock (default null, backward
  compatible)
- NonStreamingLoopCallbacks carries SseGate + MemberIndex; _text/_tool/_error
  events are tagged with the member index (review/finalizer stay at -1, no gate)
- CLI members forward TextDelta/ToolStart/ToolResult live instead of
  discarding everything but the final text
- Anthropic-API members stream per tool-round via the loop callbacks; OpenAI
  members fall back to collected text at panel_member_done
- Frontend demuxes by member index into per-bubble interleaved text/tool
  rendering

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:18:13 +02:00

99 lines
3.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Models;
using Microsoft.AspNetCore.Http;
namespace Gott.Services;
/// <summary>
/// Shared SSE-Streaming-Utilities für AnthropicService und OpenAiService.
/// </summary>
public static class SseHelper
{
/// <summary>
/// Schreibt ein SSE-Event (wirft bei CancellationToken).
/// </summary>
public static async Task WriteSseEvent(HttpResponse response, string eventType, object data, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var json = JsonSerializer.Serialize(data);
await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n", ct);
await response.Body.FlushAsync(ct);
}
/// <summary>
/// Versucht ein SSE-Event zu schreiben, ignoriert Fehler (Client disconnected).
/// Optionaler <paramref name="gate"/>: serialisiert die Byte-Writes, wenn mehrere
/// Producer (z.B. parallele Panel-Member) auf DIESELBE HttpResponse schreiben –
/// sonst vermischen sich die SSE-Frames auf Byte-Ebene.
/// </summary>
public static async Task TryWriteSse(HttpResponse response, string eventType, object data, CancellationToken ct, SemaphoreSlim gate = null)
{
if (ct.IsCancellationRequested) return;
bool acquired = false;
try
{
if (gate != null)
{
await gate.WaitAsync(ct);
acquired = true;
}
await WriteSseEvent(response, eventType, data, ct);
}
catch
{
// Client bereits disconnected / abgebrochen – ignorieren
}
finally
{
if (acquired) gate.Release();
}
}
/// <summary>
/// Finalisiert eine Chain in der Datenbank (Token-Counts, Stopgrund, Dauer).
/// </summary>
public static async Task FinalizeChain(DatabaseService db, ChainRecord chain, TokenUsage usage, int iterations, string stopReason, Stopwatch sw, string streamErrorInfo = null)
{
chain.InputTokens = usage.InputTokens;
chain.OutputTokens = usage.OutputTokens;
chain.CacheCreationTokens = usage.CacheCreationTokens;
chain.CacheReadTokens = usage.CacheReadTokens;
chain.Iterations = iterations;
chain.StopReason = stopReason;
chain.StreamErrorInfo = streamErrorInfo;
chain.DurationMs = sw.ElapsedMilliseconds;
await db.UpdateChain(chain);
}
/// <summary>
/// Kürzt Tool-Results auf max. Zeichenzahl.
/// </summary>
public const int MaxToolResultChars = 20_000;
public static string TruncateToolResult(string result)
{
return result.Length > MaxToolResultChars
? result[..MaxToolResultChars] + $"\n\n[... gekürzt, {result.Length - MaxToolResultChars} weitere Zeichen ...]"
: result;
}
}
/// <summary>
/// Token-Zähler für eine Chat-Session (Input, Output, Cache).
/// </summary>
public class TokenUsage
{
public long InputTokens { get; set; }
public long OutputTokens { get; set; }
public long CacheCreationTokens { get; set; }
public long CacheReadTokens { get; set; }
}
/// <summary>
/// Ergebnis einer Tool-Ausführung.
/// </summary>
public record ToolResult(string Id, string Result, bool IsError);