feat(a2a): request_input, notify_owner and an artifact chat channel for server sessions
This commit is contained in:
33
src/Volcanic.AgenticCore.A2A.Server/Agent/A2AChatChannel.cs
Normal file
33
src/Volcanic.AgenticCore.A2A.Server/Agent/A2AChatChannel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using A2A;
|
||||
using Volcanic.AgenticCore.Abstractions.Ui;
|
||||
|
||||
namespace Volcanic.AgenticCore.A2A.Server.Agent;
|
||||
|
||||
/// <summary>The <c>send</c> tool's channel in A2A sessions: every file becomes an artifact of the running task.</summary>
|
||||
internal sealed class A2AChatChannel(long maxFileBytes) : IChatChannel
|
||||
{
|
||||
private volatile TaskUpdater? _updater;
|
||||
|
||||
public ChatChannelLimits Limits => new(maxFileBytes, maxFileBytes, 1024);
|
||||
|
||||
public void Bind(TaskUpdater? updater) => _updater = updater;
|
||||
|
||||
public Task<ChatDelivery> SendTextAsync(string markdown, CancellationToken cancellationToken) =>
|
||||
EmitAsync([Part.FromText(markdown)], null, null, cancellationToken);
|
||||
|
||||
public async Task<ChatDelivery> SendFileAsync(ChatFile file, CancellationToken cancellationToken)
|
||||
{
|
||||
var name = Path.GetFileName(file.Path);
|
||||
var bytes = await File.ReadAllBytesAsync(file.Path, cancellationToken).ConfigureAwait(false);
|
||||
var delivery = await EmitAsync([Part.FromRaw(bytes, MediaTypes.For(file.Path), name)], name, file.Caption, cancellationToken).ConfigureAwait(false);
|
||||
return delivery with { SentAs = ChatFileKind.Document };
|
||||
}
|
||||
|
||||
private async Task<ChatDelivery> EmitAsync(List<Part> parts, string? name, string? description, CancellationToken ct)
|
||||
{
|
||||
var updater = _updater ?? throw new ChatDeliveryException("No A2A task is running, so there is nobody to send this to.");
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
await updater.AddArtifactAsync(parts, id, name, description, lastChunk: true, append: false, ct).ConfigureAwait(false);
|
||||
return new ChatDelivery(null, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.ComponentModel;
|
||||
using Volcanic.AgenticCore.A2A.Server.Callers;
|
||||
using Volcanic.AgenticCore.Abstractions.Extensions;
|
||||
using Volcanic.AgenticCore.Abstractions.Json;
|
||||
using Volcanic.AgenticCore.Abstractions.Messages;
|
||||
using Volcanic.AgenticCore.Abstractions.Tools;
|
||||
|
||||
namespace Volcanic.AgenticCore.A2A.Server.Agent;
|
||||
|
||||
public sealed record RequestInputArgs([property: Description("The question for the external agent")] string Question);
|
||||
|
||||
public sealed record NotifyOwnerArgs([property: Description("A short message for your owner")] string Text);
|
||||
|
||||
/// <summary>
|
||||
/// Added to every A2A server session: <c>request_input</c> (ends the run; the task becomes input-required),
|
||||
/// <c>notify_owner</c> (when the host has an <see cref="IA2AOwnerNotifier"/>), and the <c>a2a.inbound</c> event-bus
|
||||
/// message the client extension reads its hop count from.
|
||||
/// </summary>
|
||||
internal sealed class A2ASessionExtension(A2ACaller caller, IA2AOwnerNotifier? notifier) : IExtension
|
||||
{
|
||||
/// <summary>Event-bus channel; data is a JsonElement <c>{"hops": n, "caller": "name"}</c>.</summary>
|
||||
public const string InboundChannel = "a2a.inbound";
|
||||
|
||||
private IExtensionApi? _api;
|
||||
private volatile string? _pendingQuestion;
|
||||
|
||||
public string Name => "a2a-session";
|
||||
|
||||
/// <summary>The question passed to <c>request_input</c> during the current run, if any.</summary>
|
||||
public string? PendingQuestion => _pendingQuestion;
|
||||
|
||||
public void BeginRun(int hops)
|
||||
{
|
||||
_pendingQuestion = null;
|
||||
_api?.Events.Emit(InboundChannel, VolcanicJson.ToElement(new { hops, caller = caller.Name }));
|
||||
}
|
||||
|
||||
public ValueTask ActivateAsync(IExtensionApi api, CancellationToken cancellationToken)
|
||||
{
|
||||
_api = api;
|
||||
api.RegisterTool(new DelegateTool<RequestInputArgs>(
|
||||
"request_input",
|
||||
"Ask the external agent that sent this A2A message a question and wait for its answer. Ends your turn; its reply arrives as the next message.",
|
||||
(args, _, _) =>
|
||||
{
|
||||
_pendingQuestion = args.Question;
|
||||
return Task.FromResult(new ToolResult([new TextContent("Question sent to the caller; your turn ends here.")], Terminate: true));
|
||||
},
|
||||
promptSnippet: "request_input: ask the external A2A caller a question (ends your turn)"));
|
||||
if (notifier is not null)
|
||||
api.RegisterTool(new DelegateTool<NotifyOwnerArgs>(
|
||||
"notify_owner",
|
||||
"Send a short message to your owner (the person whose assistant you are) about this A2A conversation.",
|
||||
async (args, _, ct) =>
|
||||
{
|
||||
await notifier.NotifyAsync(caller, args.Text, ct).ConfigureAwait(false);
|
||||
return ToolResult.Text("delivered");
|
||||
},
|
||||
promptSnippet: "notify_owner: tell your owner something about this A2A conversation"));
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user