Messaging/IncomingOutgoing.cscsharp

Documentation

Messaging

IncomingOutgoing is the shared turn loop for any Lilith interface:

1. BeforeRead — prompt (optional) 2. ReadIncoming — one user message 3. Process — host calls the agent (HandleInputAsync, etc.) 4. WriteOutgoing — assistant/system text (optional; console often logs via Logger already) 5. AfterTurn — TTS, metrics, etc.

Subclass for each transport (Discord in the console app is the console adapter; a future Discord.NET bot would subclass the same base).

namespace Agent.Core.Messaging;/// <summary>/// Universal transport for one turn of user input and agent output./// Subclass per interface (console, Discord bot, web socket, etc.)./// </summary>public abstract class IncomingOutgoing{    /// <summary>    /// Reads one user message, invokes <paramref name="processMessageAsync"/>, then emits any outgoing content.    /// Returns false when the host should stop its session loop (e.g. user typed exit).    /// </summary>    public async Task<bool> HandleAsync(        Func<string, Task<IncomingOutgoingTurnResult>> processMessageAsync,        CancellationToken cancellationToken = default)    {        ArgumentNullException.ThrowIfNull(processMessageAsync);        await BeforeReadAsync(cancellationToken).ConfigureAwait(false);        string? incoming = await ReadIncomingAsync(cancellationToken).ConfigureAwait(false);        if (incoming is null)            return true;        if (string.IsNullOrWhiteSpace(incoming))            return true;        IncomingOutgoingTurnResult turn = await processMessageAsync(incoming).ConfigureAwait(false);        if (!string.IsNullOrWhiteSpace(turn.AssistantReply))        {            await WriteOutgoingAsync(                new OutgoingMessage(OutgoingKind.Assistant, turn.AssistantReply),                cancellationToken).ConfigureAwait(false);        }        await AfterTurnAsync(turn, cancellationToken).ConfigureAwait(false);        return turn.ContinueRunning;    }    /// <summary>Runs <see cref="HandleAsync"/> until it returns false.</summary>    public async Task RunAsync(        Func<string, Task<IncomingOutgoingTurnResult>> processMessageAsync,        CancellationToken cancellationToken = default)    {        bool running = true;        while (running)        {            running = await HandleAsync(processMessageAsync, cancellationToken).ConfigureAwait(false);        }    }    /// <summary>Hook before blocking on input (e.g. print "You: ").</summary>    protected virtual Task BeforeReadAsync(CancellationToken cancellationToken) =>        Task.CompletedTask;    /// <summary>Read one user message. Return null to skip processing but keep the session alive.</summary>    protected abstract Task<string?> ReadIncomingAsync(CancellationToken cancellationToken);    /// <summary>Emit assistant or system text to the transport.</summary>    protected virtual Task WriteOutgoingAsync(OutgoingMessage message, CancellationToken cancellationToken) =>        Task.CompletedTask;    /// <summary>Hook after a processed turn (e.g. TTS on assistant reply).</summary>    protected virtual Task AfterTurnAsync(        IncomingOutgoingTurnResult turn,        CancellationToken cancellationToken) =>        Task.CompletedTask;}/// <summary>Result of processing one user message through the agent.</summary>public sealed record IncomingOutgoingTurnResult(bool ContinueRunning, string? AssistantReply){    public static IncomingOutgoingTurnResult Continue(string? assistantReply = null) =>        new(true, assistantReply);    public static IncomingOutgoingTurnResult Exit() => new(false, null);}/// <summary>Outgoing payload written by <see cref="IncomingOutgoing"/>.</summary>public readonly record struct OutgoingMessage(OutgoingKind Kind, string Text);public enum OutgoingKind{    Assistant,    System,}