feat(telegram): /a2a lists a chat's A2A callers and recent tasks

This commit is contained in:
EugeneTes
2026-10-07 13:53:47 +00:00
parent c1a8744eb7
commit a3aee939d4
3 changed files with 49 additions and 4 deletions

View File

@@ -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;
/// <summary>The bot's own slash commands. Anything else starting with <c>/</c> goes to the session (extension commands).</summary>
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<BotCommand> 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<string> { "**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<string> CancelJobAsync(ChatContext chat, string arg, CancellationToken cancellationToken)
{
if (scheduler is not { IsStarted: true }) return SchedulerSetup.DisabledReply;

View File

@@ -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());
}
}

View File

@@ -55,7 +55,8 @@ public sealed class ChatHost : IAsyncDisposable
public ChatHost(Action<ChatHost, IServiceCollection>? configure = null, Action<AgenticCoreOptions>? 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<TelegramJobDelivery>.Instance);
var media = new InboundMedia(sender, Options, time ?? TimeProvider.System, NullLogger<InboundMedia>.Instance, speech);
Composer = new InboundComposer(sender, media, NullLogger<InboundComposer>.Instance);
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers), sender, Composer, NullLogger<UpdateRouter>.Instance, Impersonation, Feed);
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers, a2a), sender, Composer, NullLogger<UpdateRouter>.Instance, Impersonation, Feed);
}
private readonly TriggerDispatcher? _dispatcher;