using System.Text.Json.Nodes; using backend.Conversations; using backend.Tools; using Microsoft.Extensions.Logging; namespace backend.Realtime; public interface ISessionOutput { Task WriteEnvelopeAsync(JsonObject envelope, CancellationToken ct); Task WriteBinaryAsync(ReadOnlyMemory bytes, CancellationToken ct); } public class RealtimeSession { private readonly Guid _deviceId; private readonly IRealtimeUpstream _upstream; private readonly ISessionOutput _output; private readonly ConversationLog _log; private readonly ToolRegistry _registry; private readonly RealtimeSettings _cfg; private readonly IDeviceChannel _channel; private readonly ILogger _logger; private Conversation? _conversation; private EndReason _endReason = EndReason.Unknown; private bool _endRequested; public RealtimeSession( Guid deviceId, IRealtimeUpstream upstream, ISessionOutput output, ConversationLog log, ToolRegistry registry, RealtimeSettings cfg, IDeviceChannel channel, ILogger logger) { _deviceId = deviceId; _upstream = upstream; _output = output; _log = log; _registry = registry; _cfg = cfg; _channel = channel; _logger = logger; } public Guid? ConversationId => _conversation?.Id; public async Task RunAsync(CancellationToken ct) { try { var enabled = _registry.EnabledFor(_cfg.EnabledTools).ToList(); await _upstream.SendJsonAsync(RealtimeEvents.SessionUpdate(_cfg, enabled), ct); var ack = await WaitForUpstreamTypeAsync("session.updated", ct); if (ack is null) { _endReason = EndReason.Error; return; } _conversation = await _log.StartAsync(_deviceId, ct); await _output.WriteEnvelopeAsync(new JsonObject { ["type"] = "session_started", ["conversation_id"] = _conversation.Id.ToString(), }, ct); await PumpAsync(ct); } catch (OperationCanceledException) { if (_endReason == EndReason.Unknown) _endReason = EndReason.Error; } catch (Exception ex) { _logger.LogError(ex, "RealtimeSession crashed"); _endReason = EndReason.Error; } finally { await _upstream.DisposeAsync(); if (_conversation is not null) { await _log.EndAsync(_conversation.Id, _endReason, CancellationToken.None); } var endEnv = new JsonObject { ["type"] = "session_ended", ["reason"] = _endReason.ToString().ToLowerInvariant(), }; await _output.WriteEnvelopeAsync(endEnv, CancellationToken.None); } } private async Task PumpAsync(CancellationToken ct) { while (!ct.IsCancellationRequested && !_endRequested) { var evt = await _upstream.ReceiveJsonAsync(ct); if (evt is null) { _endReason = EndReason.Error; return; } } } private async Task WaitForUpstreamTypeAsync(string type, CancellationToken ct) { while (true) { var evt = await _upstream.ReceiveJsonAsync(ct); if (evt is null) return null; if ((string?)evt["type"] == type) return evt; } } }