Files
VolcanicAgenticCore/extensions/Volcanic.AgenticCore.Extensions.A2A/A2AResultDelivery.cs

56 lines
2.8 KiB
C#

using System.Collections.Concurrent;
using Volcanic.AgenticCore.Abstractions.Extensions;
namespace Volcanic.AgenticCore.Extensions.A2A;
/// <summary>Where finished remote tasks go. Targets are host ids (Telegram: chat ids).</summary>
public interface IA2AResultDelivery
{
/// <summary>Run <paramref name="text"/> as a follow-up prompt in the target's conversation. False: not deliverable now.</summary>
/// <remarks>
/// May run a whole agent turn before returning, and that turn may call back into the <see cref="A2AWatchRegistry"/>
/// (cancel a watch, start one): the registry never holds a lock while delivering. A delivery in flight isn't
/// interrupted, and shutting the registry down doesn't wait for it. A false result keeps the text in
/// <c>pending.json</c> for the next flush; delivery is at least once.
/// </remarks>
Task<bool> DeliverAsync(string target, string text, CancellationToken cancellationToken);
/// <summary>The target's workspace. A path computation: must not create or wake a session.</summary>
string? WorkspaceFor(string target);
/// <summary>Targets whose workspace may hold a pending.json (restore at startup).</summary>
IEnumerable<string> KnownTargets();
}
/// <summary>Default delivery: the target's live session, if any (registered on session start, removed on shutdown).</summary>
public sealed class LiveSessionDelivery : IA2AResultDelivery
{
private readonly ConcurrentDictionary<string, (IExtensionApi Api, string Workspace)> _live = new(StringComparer.Ordinal);
public void Attach(string target, string workspace, IExtensionApi api) => _live[target] = (api, workspace);
/// <summary>Detaches <paramref name="api"/> only if it is still the target's live session (a newer one may have attached).</summary>
public void Detach(string target, IExtensionApi api)
{
// TryRemove(pair) removes only if the value is still this exact entry, so a racing Attach is never undone.
if (_live.TryGetValue(target, out var entry) && ReferenceEquals(entry.Api, api)) _live.TryRemove(new(target, entry));
}
public Task<bool> DeliverAsync(string target, string text, CancellationToken cancellationToken)
{
if (!_live.TryGetValue(target, out var entry)) return Task.FromResult(false);
try
{
entry.Api.SendUserMessage(text, null, MessageDelivery.FollowUp);
return Task.FromResult(true);
}
catch (Exception ex) when (ex is InvalidOperationException or ObjectDisposedException)
{
return Task.FromResult(false);
}
}
public string? WorkspaceFor(string target) => _live.TryGetValue(target, out var entry) ? entry.Workspace : null;
public IEnumerable<string> KnownTargets() => _live.Keys.ToList();
}