From a3aee939d4a6f6b4f131c59ad57fcf769c97672d Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Wed, 7 Oct 2026 13:53:47 +0000 Subject: [PATCH] feat(telegram): /a2a lists a chat's A2A callers and recent tasks --- .../Chats/ChatCommands.cs | 26 +++++++++++++++++-- .../A2AHostTests.cs | 22 ++++++++++++++++ .../Support/ChatHost.cs | 5 ++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/hosts/Volcanic.AgenticCore.Telegram/Chats/ChatCommands.cs b/hosts/Volcanic.AgenticCore.Telegram/Chats/ChatCommands.cs index 935f20a..318f35a 100644 --- a/hosts/Volcanic.AgenticCore.Telegram/Chats/ChatCommands.cs +++ b/hosts/Volcanic.AgenticCore.Telegram/Chats/ChatCommands.cs @@ -1,6 +1,9 @@ using System.Globalization; using System.Text; using Telegram.Bot.Types; +using Volcanic.AgenticCore.A2A.Server; +using Volcanic.AgenticCore.A2A.Server.Callers; +using Volcanic.AgenticCore.A2A.Server.Tasks; using Volcanic.AgenticCore.Abstractions.Providers; using Volcanic.AgenticCore.Extensions.Scheduler; using Volcanic.AgenticCore.Hosting; @@ -11,7 +14,8 @@ using Volcanic.AgenticCore.Telegram.Triggers; namespace Volcanic.AgenticCore.Telegram.Chats; /// The bot's own slash commands. Anything else starting with / goes to the session (extension commands). -public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zones = null, JobScheduler? scheduler = null, TriggerServices? triggers = null) +public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zones = null, JobScheduler? scheduler = null, TriggerServices? triggers = null, + A2AHostOptions? a2a = null) { public static readonly IReadOnlyList Menu = [ @@ -25,6 +29,7 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon new() { Command = "timezone", Description = "Show or set this chat's time zone" }, .. SchedulerSetup.Menu, TriggersSetup.MenuCommand, + new() { Command = "a2a", Description = "A2A callers and tasks of this chat" }, new() { Command = "help", Description = "What this bot does" }, ]; @@ -119,6 +124,10 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon await ReplyAsync(chat, Jobs(chat), cancellationToken).ConfigureAwait(false); return true; + case "a2a": + await ReplyAsync(chat, A2AStatus(chat), cancellationToken).ConfigureAwait(false); + return true; + case "cancel": await ReplyAsync(chat, await CancelJobAsync(chat, command.Args, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); return true; @@ -146,7 +155,7 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon /compact – summarise the conversation to free context /thinking [level] – show or set the thinking level /timezone [IANA name|reset] – this chat's time zone - """ + SchedulerSetup.HelpLines + TriggersSetup.HelpLines + GoogleCalendarSetup.HelpLines(chat.Session.Commands); + """ + SchedulerSetup.HelpLines + "\n/a2a – A2A callers and recent tasks of this chat" + TriggersSetup.HelpLines + GoogleCalendarSetup.HelpLines(chat.Session.Commands); private string Status(ChatContext chat) { @@ -216,6 +225,19 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon string.Join('\n', jobs.Select(j => JobFormat.Line(j, zone, now))); } + private string A2AStatus(ChatContext chat) + { + if (a2a is not { Enabled: true }) return "A2A is off. Set A2A__Enabled=true (and A2A__PublicUrl) to let other agents talk to this assistant."; + var workspace = chat.Session.Cwd; + var callers = CallersFile.Read(workspace, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + var lines = new List { "**A2A callers** (.volcanic/a2a/callers.json)" }; + lines.AddRange(callers.Count == 0 ? ["none"] : callers.Select(c => $"• {c.Name} ({c.Mode}) key …{(c.Key.Length > 4 ? c.Key[^4..] : "")}")); + var tasks = A2ATaskFiles.Recent(workspace, 10); + lines.Add("**Recent A2A tasks**"); + lines.AddRange(tasks.Count == 0 ? ["none"] : tasks.Select(t => $"• {t.Caller}: {t.State} ({t.TaskId[..Math.Min(8, t.TaskId.Length)]})")); + return string.Join('\n', lines); + } + private async Task CancelJobAsync(ChatContext chat, string arg, CancellationToken cancellationToken) { if (scheduler is not { IsStarted: true }) return SchedulerSetup.DisabledReply; diff --git a/tests/Volcanic.AgenticCore.Telegram.Tests/A2AHostTests.cs b/tests/Volcanic.AgenticCore.Telegram.Tests/A2AHostTests.cs index 633b03a..c666021 100644 --- a/tests/Volcanic.AgenticCore.Telegram.Tests/A2AHostTests.cs +++ b/tests/Volcanic.AgenticCore.Telegram.Tests/A2AHostTests.cs @@ -50,4 +50,26 @@ public class A2AHostTests Assert.Contains(services, d => d.ServiceType == typeof(IServer) && d.ImplementationType == typeof(NoHttpServer)); Assert.DoesNotContain(services, d => d.ServiceType == typeof(IA2ACallerDirectory)); } + + [Fact] + public async Task A2aCommand_ListsMaskedCallersAndTasks() + { + await using var host = new ChatHost(a2a: new A2AHostOptions { Enabled = true }); + WriteCallers(host.WorkspaceOf(), """[ { "name": "travel-bot", "key": "secret-key-1234" } ]"""); + await host.SendAsync("/a2a"); + await host.SettleAsync(); + var reply = host.LastReply(); + Assert.Contains("travel-bot", reply); + Assert.Contains("…1234", reply); + Assert.DoesNotContain("secret-key-1234", reply); + } + + [Fact] + public async Task A2aCommand_DisabledReply() + { + await using var host = new ChatHost(); + await host.SendAsync("/a2a"); + await host.SettleAsync(); + Assert.Contains("A2A is off", host.LastReply()); + } } diff --git a/tests/Volcanic.AgenticCore.Telegram.Tests/Support/ChatHost.cs b/tests/Volcanic.AgenticCore.Telegram.Tests/Support/ChatHost.cs index 1a52f80..762397b 100644 --- a/tests/Volcanic.AgenticCore.Telegram.Tests/Support/ChatHost.cs +++ b/tests/Volcanic.AgenticCore.Telegram.Tests/Support/ChatHost.cs @@ -55,7 +55,8 @@ public sealed class ChatHost : IAsyncDisposable public ChatHost(Action? configure = null, Action? core = null, TimeProvider? time = null, Volcanic.AgenticCore.Extensions.GoogleCalendar.GoogleCalendarOptions? calendar = null, Volcanic.AgenticCore.Telegram.Speech.ISpeechToText? speech = null, string? root = null, SchedulerOptions? scheduler = null, - IScriptRunner? runner = null, TriggerOptions? triggers = null, IJevClient? jev = null) + IScriptRunner? runner = null, TriggerOptions? triggers = null, IJevClient? jev = null, + Volcanic.AgenticCore.A2A.Server.A2AHostOptions? a2a = null) { _ownsRoot = root is null; _root = root ?? Path.Combine(Path.GetTempPath(), "volcanic-telegram", Guid.NewGuid().ToString("N")); @@ -115,7 +116,7 @@ public sealed class ChatHost : IAsyncDisposable if (Scheduler is not null) Delivery = new TelegramJobDelivery(Chats, Settings, Options, clock, NullLogger.Instance); var media = new InboundMedia(sender, Options, time ?? TimeProvider.System, NullLogger.Instance, speech); Composer = new InboundComposer(sender, media, NullLogger.Instance); - Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers), sender, Composer, NullLogger.Instance, Impersonation, Feed); + Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers, a2a), sender, Composer, NullLogger.Instance, Impersonation, Feed); } private readonly TriggerDispatcher? _dispatcher;